@anthusai/papyrus 5.58.0 → 5.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/amplify/data/message-actions.js +68 -0
- package/amplify/data/message-actions.ts +70 -0
- package/amplify/data/resolvers/delete-own-message.js +30 -0
- package/amplify/data/resolvers/edit-message.js +33 -0
- package/amplify/data/resolvers/moderate-message.js +32 -0
- package/amplify/data/resolvers/post-message.js +68 -0
- package/amplify/data/resolvers/rename-thread.js +28 -0
- package/amplify/data/resource.js +8 -5
- package/amplify/data/resource.ts +8 -5
- package/amplify/data/schema.js +14 -7
- package/amplify/data/schema.ts +14 -7
- package/components/conversation-context.tsx +29 -2
- package/components/conversation-provider.tsx +5 -1
- package/components/conversation-thread.tsx +133 -19
- package/components/conversations-memory-source.ts +43 -6
- package/components/conversations-network-source.ts +13 -1
- package/components/conversations-source.ts +15 -3
- package/components/conversations-view.tsx +437 -76
- package/components/list-detail-layout.tsx +44 -0
- package/components/needs-me-bodies.tsx +14 -14
- package/components/needs-me-feed.tsx +8 -18
- package/components/news-desk-taxonomy-client.ts +142 -57
- package/components/newsroom-article-editor.tsx +10 -4
- package/components/newsroom-organizer.css +10 -1
- package/components/newsroom-references-view.tsx +41 -100
- package/components/notification-cards.tsx +4 -4
- package/components/notification-center.tsx +5 -7
- package/components/proposals-view.tsx +4 -4
- package/components/reference-conversation.tsx +5 -0
- package/components/reference-feed.tsx +69 -85
- package/components/reference-review-card.tsx +1 -1
- package/components/references-page-loading.tsx +12 -6
- package/components/topic-steering-workspace.tsx +4 -4
- package/components/use-conversation.ts +36 -2
- package/components/use-feed-preferences.ts +33 -0
- package/lib/category-repository.ts +7 -38
- package/lib/console-chat-client.ts +8 -178
- package/lib/conversation.ts +73 -2
- package/lib/needs-me-items.ts +4 -4
- package/lib/needs-me-ranking.ts +9 -22
- package/lib/newsroom-board.ts +10 -1
- package/lib/newsroom-demo-dashboard.ts +0 -7
- package/lib/newsroom-organizer-nav.ts +2 -2
- package/lib/post-message.ts +54 -0
- package/lib/proposals-admin.ts +2 -2
- package/lib/reference-feed-ranking.ts +77 -160
- package/lib/review-drafts.ts +2 -2
- package/lib/review-fixture.ts +1 -1
- package/lib/review-requests.ts +4 -4
- package/lib/semantic-graph.ts +1 -1
- package/package.json +4 -1
- package/routes.manifest.json +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { a } from "@aws-amplify/backend";
|
|
2
|
+
/**
|
|
3
|
+
* Messages belong to their author, and the backend says so. The browser cannot create, edit or delete a Message through the
|
|
4
|
+
* model (editors and admins only read it); it calls these four mutations, whose resolvers stamp the author from the signed-in
|
|
5
|
+
* identity and check ownership against the stored `authorSub` in the same DynamoDB write. `renameThread` does the same for a
|
|
6
|
+
* conversation's title. Functions and IAM callers (the router, procedures, the console responder, email, Slack) write the
|
|
7
|
+
* model directly with their own grants, as before.
|
|
8
|
+
*
|
|
9
|
+
* Each resolver is one self-contained file (AppSync takes a single file and offers `@aws-appsync/utils` only).
|
|
10
|
+
*/
|
|
11
|
+
const people = (allow) => [allow.group("editor"), allow.group("admin")];
|
|
12
|
+
const adminsOnly = (allow) => [allow.group("admin")];
|
|
13
|
+
const onMessages = (entry) => a.handler.custom({ dataSource: a.ref("Message"), entry });
|
|
14
|
+
const onThreads = (entry) => a.handler.custom({ dataSource: a.ref("MessageThread"), entry });
|
|
15
|
+
export const messageActionsSchemaDefinitions = {
|
|
16
|
+
postMessage: a
|
|
17
|
+
.mutation()
|
|
18
|
+
.arguments({
|
|
19
|
+
id: a.id().required(),
|
|
20
|
+
threadId: a.id().required(),
|
|
21
|
+
content: a.string(),
|
|
22
|
+
summary: a.string(),
|
|
23
|
+
parentMessageId: a.id(),
|
|
24
|
+
sequenceNumber: a.integer(),
|
|
25
|
+
messageKind: a.string().required(),
|
|
26
|
+
messageDomain: a.string().required(),
|
|
27
|
+
messageType: a.string(),
|
|
28
|
+
role: a.string(),
|
|
29
|
+
source: a.string(),
|
|
30
|
+
authorLabel: a.string(),
|
|
31
|
+
semanticLayer: a.string(),
|
|
32
|
+
searchVisibility: a.string(),
|
|
33
|
+
responseTarget: a.string(),
|
|
34
|
+
responseStatus: a.string(),
|
|
35
|
+
responseOwner: a.string(),
|
|
36
|
+
responseStartedAt: a.datetime(),
|
|
37
|
+
responseCompletedAt: a.datetime(),
|
|
38
|
+
metadata: a.json(),
|
|
39
|
+
newsroomFeedKey: a.string(),
|
|
40
|
+
})
|
|
41
|
+
.returns(a.ref("Message"))
|
|
42
|
+
.authorization(people)
|
|
43
|
+
.handler(onMessages("./resolvers/post-message.js")),
|
|
44
|
+
editMessage: a
|
|
45
|
+
.mutation()
|
|
46
|
+
.arguments({ id: a.id().required(), content: a.string().required(), summary: a.string() })
|
|
47
|
+
.returns(a.ref("Message"))
|
|
48
|
+
.authorization(people)
|
|
49
|
+
.handler(onMessages("./resolvers/edit-message.js")),
|
|
50
|
+
deleteOwnMessage: a
|
|
51
|
+
.mutation()
|
|
52
|
+
.arguments({ id: a.id().required() })
|
|
53
|
+
.returns(a.ref("Message"))
|
|
54
|
+
.authorization(people)
|
|
55
|
+
.handler(onMessages("./resolvers/delete-own-message.js")),
|
|
56
|
+
moderateMessage: a
|
|
57
|
+
.mutation()
|
|
58
|
+
.arguments({ id: a.id().required() })
|
|
59
|
+
.returns(a.ref("Message"))
|
|
60
|
+
.authorization(adminsOnly)
|
|
61
|
+
.handler(onMessages("./resolvers/moderate-message.js")),
|
|
62
|
+
renameThread: a
|
|
63
|
+
.mutation()
|
|
64
|
+
.arguments({ id: a.id().required(), title: a.string().required() })
|
|
65
|
+
.returns(a.ref("MessageThread"))
|
|
66
|
+
.authorization(people)
|
|
67
|
+
.handler(onThreads("./resolvers/rename-thread.js")),
|
|
68
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { a } from "@aws-amplify/backend";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Messages belong to their author, and the backend says so. The browser cannot create, edit or delete a Message through the
|
|
5
|
+
* model (editors and admins only read it); it calls these four mutations, whose resolvers stamp the author from the signed-in
|
|
6
|
+
* identity and check ownership against the stored `authorSub` in the same DynamoDB write. `renameThread` does the same for a
|
|
7
|
+
* conversation's title. Functions and IAM callers (the router, procedures, the console responder, email, Slack) write the
|
|
8
|
+
* model directly with their own grants, as before.
|
|
9
|
+
*
|
|
10
|
+
* Each resolver is one self-contained file (AppSync takes a single file and offers `@aws-appsync/utils` only).
|
|
11
|
+
*/
|
|
12
|
+
const people = (allow: any) => [allow.group("editor"), allow.group("admin")];
|
|
13
|
+
const adminsOnly = (allow: any) => [allow.group("admin")];
|
|
14
|
+
const onMessages = (entry: string) => a.handler.custom({ dataSource: a.ref("Message"), entry });
|
|
15
|
+
const onThreads = (entry: string) => a.handler.custom({ dataSource: a.ref("MessageThread"), entry });
|
|
16
|
+
|
|
17
|
+
export const messageActionsSchemaDefinitions = {
|
|
18
|
+
postMessage: a
|
|
19
|
+
.mutation()
|
|
20
|
+
.arguments({
|
|
21
|
+
id: a.id().required(),
|
|
22
|
+
threadId: a.id().required(),
|
|
23
|
+
content: a.string(),
|
|
24
|
+
summary: a.string(),
|
|
25
|
+
parentMessageId: a.id(),
|
|
26
|
+
sequenceNumber: a.integer(),
|
|
27
|
+
messageKind: a.string().required(),
|
|
28
|
+
messageDomain: a.string().required(),
|
|
29
|
+
messageType: a.string(),
|
|
30
|
+
role: a.string(),
|
|
31
|
+
source: a.string(),
|
|
32
|
+
authorLabel: a.string(),
|
|
33
|
+
semanticLayer: a.string(),
|
|
34
|
+
searchVisibility: a.string(),
|
|
35
|
+
responseTarget: a.string(),
|
|
36
|
+
responseStatus: a.string(),
|
|
37
|
+
responseOwner: a.string(),
|
|
38
|
+
responseStartedAt: a.datetime(),
|
|
39
|
+
responseCompletedAt: a.datetime(),
|
|
40
|
+
metadata: a.json(),
|
|
41
|
+
newsroomFeedKey: a.string(),
|
|
42
|
+
})
|
|
43
|
+
.returns(a.ref("Message"))
|
|
44
|
+
.authorization(people)
|
|
45
|
+
.handler(onMessages("./resolvers/post-message.js")),
|
|
46
|
+
editMessage: a
|
|
47
|
+
.mutation()
|
|
48
|
+
.arguments({ id: a.id().required(), content: a.string().required(), summary: a.string() })
|
|
49
|
+
.returns(a.ref("Message"))
|
|
50
|
+
.authorization(people)
|
|
51
|
+
.handler(onMessages("./resolvers/edit-message.js")),
|
|
52
|
+
deleteOwnMessage: a
|
|
53
|
+
.mutation()
|
|
54
|
+
.arguments({ id: a.id().required() })
|
|
55
|
+
.returns(a.ref("Message"))
|
|
56
|
+
.authorization(people)
|
|
57
|
+
.handler(onMessages("./resolvers/delete-own-message.js")),
|
|
58
|
+
moderateMessage: a
|
|
59
|
+
.mutation()
|
|
60
|
+
.arguments({ id: a.id().required() })
|
|
61
|
+
.returns(a.ref("Message"))
|
|
62
|
+
.authorization(adminsOnly)
|
|
63
|
+
.handler(onMessages("./resolvers/moderate-message.js")),
|
|
64
|
+
renameThread: a
|
|
65
|
+
.mutation()
|
|
66
|
+
.arguments({ id: a.id().required(), title: a.string().required() })
|
|
67
|
+
.returns(a.ref("MessageThread"))
|
|
68
|
+
.authorization(people)
|
|
69
|
+
.handler(onThreads("./resolvers/rename-thread.js")),
|
|
70
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// deleteOwnMessage: the author deletes their own Message. A soft delete: the Message stays (replies keep their place) marked
|
|
2
|
+
// deleted, with when and by whom. Nothing is removed.
|
|
3
|
+
import { util } from "@aws-appsync/utils";
|
|
4
|
+
import * as ddb from "@aws-appsync/utils/dynamodb";
|
|
5
|
+
|
|
6
|
+
export function request(ctx) {
|
|
7
|
+
const identity = ctx.identity;
|
|
8
|
+
if (!identity || !identity.sub) util.error("Sign in to delete a message.", "Unauthorized");
|
|
9
|
+
const now = util.time.nowISO8601();
|
|
10
|
+
return ddb.update({
|
|
11
|
+
key: { id: ctx.args.id },
|
|
12
|
+
update: {
|
|
13
|
+
status: ddb.operations.replace("deleted"),
|
|
14
|
+
deletedAt: ddb.operations.replace(now),
|
|
15
|
+
deletedBySub: ddb.operations.replace(identity.sub),
|
|
16
|
+
updatedAt: ddb.operations.replace(now),
|
|
17
|
+
},
|
|
18
|
+
condition: { authorSub: { eq: identity.sub }, status: { ne: "deleted" } },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function response(ctx) {
|
|
23
|
+
if (ctx.error) {
|
|
24
|
+
if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
|
|
25
|
+
util.error("Only the author can delete their own message, and it may already be deleted.", "Unauthorized");
|
|
26
|
+
}
|
|
27
|
+
util.error(ctx.error.message, ctx.error.type);
|
|
28
|
+
}
|
|
29
|
+
return ctx.result;
|
|
30
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// editMessage: a person changes the text of their own Message, and no one else's. The condition is checked by DynamoDB against the
|
|
2
|
+
// stored authorSub in the same write, so there is no window between the check and the change. A deleted Message cannot be edited.
|
|
3
|
+
// Authorship cannot be reassigned: the update never touches authorSub.
|
|
4
|
+
import { util } from "@aws-appsync/utils";
|
|
5
|
+
import * as ddb from "@aws-appsync/utils/dynamodb";
|
|
6
|
+
|
|
7
|
+
export function request(ctx) {
|
|
8
|
+
const identity = ctx.identity;
|
|
9
|
+
if (!identity || !identity.sub) util.error("Sign in to edit a message.", "Unauthorized");
|
|
10
|
+
const content = typeof ctx.args.content === "string" ? ctx.args.content.trim() : "";
|
|
11
|
+
if (!content) util.error("A message cannot be empty.", "BadRequest");
|
|
12
|
+
const now = util.time.nowISO8601();
|
|
13
|
+
return ddb.update({
|
|
14
|
+
key: { id: ctx.args.id },
|
|
15
|
+
update: {
|
|
16
|
+
content: ddb.operations.replace(content),
|
|
17
|
+
summary: ddb.operations.replace(ctx.args.summary ? ctx.args.summary : content.slice(0, 120)),
|
|
18
|
+
editedAt: ddb.operations.replace(now),
|
|
19
|
+
updatedAt: ddb.operations.replace(now),
|
|
20
|
+
},
|
|
21
|
+
condition: { authorSub: { eq: identity.sub }, status: { ne: "deleted" } },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function response(ctx) {
|
|
26
|
+
if (ctx.error) {
|
|
27
|
+
if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
|
|
28
|
+
util.error("Only the author can edit a message, and a deleted message cannot be edited.", "Unauthorized");
|
|
29
|
+
}
|
|
30
|
+
util.error(ctx.error.message, ctx.error.type);
|
|
31
|
+
}
|
|
32
|
+
return ctx.result;
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// moderateMessage: an admin removes anyone's Message. The same soft delete as the author's, with no change to the text, and only
|
|
2
|
+
// for the admin group (checked here as well as by the mutation's authorization).
|
|
3
|
+
import { util } from "@aws-appsync/utils";
|
|
4
|
+
import * as ddb from "@aws-appsync/utils/dynamodb";
|
|
5
|
+
|
|
6
|
+
export function request(ctx) {
|
|
7
|
+
const identity = ctx.identity;
|
|
8
|
+
if (!identity || !identity.sub) util.error("Sign in to moderate a message.", "Unauthorized");
|
|
9
|
+
const groups = identity.groups || [];
|
|
10
|
+
if (groups.indexOf("admin") < 0) util.error("Only an admin can delete someone else's message.", "Unauthorized");
|
|
11
|
+
const now = util.time.nowISO8601();
|
|
12
|
+
return ddb.update({
|
|
13
|
+
key: { id: ctx.args.id },
|
|
14
|
+
update: {
|
|
15
|
+
status: ddb.operations.replace("deleted"),
|
|
16
|
+
deletedAt: ddb.operations.replace(now),
|
|
17
|
+
deletedBySub: ddb.operations.replace(identity.sub),
|
|
18
|
+
updatedAt: ddb.operations.replace(now),
|
|
19
|
+
},
|
|
20
|
+
condition: { id: { attributeExists: true }, status: { ne: "deleted" } },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function response(ctx) {
|
|
25
|
+
if (ctx.error) {
|
|
26
|
+
if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
|
|
27
|
+
util.error("That message does not exist or is already deleted.", "NotFound");
|
|
28
|
+
}
|
|
29
|
+
util.error(ctx.error.message, ctx.error.type);
|
|
30
|
+
}
|
|
31
|
+
return ctx.result;
|
|
32
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// postMessage: the only way a person's Message is created. The author is the signed-in user (the identity's sub), whatever the
|
|
2
|
+
// client sends: the mutation has no author argument, and a label is only a label. One DynamoDB PutItem that refuses to replace an
|
|
3
|
+
// existing Message (ids are derived from the sequence number, so a lost race comes back as a conflict the caller retries).
|
|
4
|
+
import { util } from "@aws-appsync/utils";
|
|
5
|
+
import * as ddb from "@aws-appsync/utils/dynamodb";
|
|
6
|
+
|
|
7
|
+
function clean(value) {
|
|
8
|
+
const out = {};
|
|
9
|
+
const keys = Object.keys(value);
|
|
10
|
+
for (let index = 0; index < keys.length; index++) {
|
|
11
|
+
const key = keys[index];
|
|
12
|
+
if (value[key] !== null && value[key] !== undefined) out[key] = value[key];
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function request(ctx) {
|
|
18
|
+
const identity = ctx.identity;
|
|
19
|
+
if (!identity || !identity.sub) util.error("Sign in to post a message.", "Unauthorized");
|
|
20
|
+
const args = ctx.args;
|
|
21
|
+
const content = typeof args.content === "string" ? args.content.trim() : "";
|
|
22
|
+
// A placeholder waiting for a responder (PENDING) may start empty; any other message needs text.
|
|
23
|
+
if (!content && args.responseStatus !== "PENDING") util.error("A message cannot be empty.", "BadRequest");
|
|
24
|
+
if (!args.id || !args.threadId) util.error("A message needs an id and a conversation.", "BadRequest");
|
|
25
|
+
const now = util.time.nowISO8601();
|
|
26
|
+
const claims = identity.claims || {};
|
|
27
|
+
const email = typeof claims.email === "string" ? claims.email : "";
|
|
28
|
+
const label = typeof args.authorLabel === "string" && args.authorLabel.trim() ? args.authorLabel.trim() : email ? email : "editor";
|
|
29
|
+
const item = clean({
|
|
30
|
+
__typename: "Message",
|
|
31
|
+
id: args.id,
|
|
32
|
+
messageKind: args.messageKind,
|
|
33
|
+
messageDomain: args.messageDomain,
|
|
34
|
+
status: "active",
|
|
35
|
+
summary: args.summary ? args.summary : content.slice(0, 120),
|
|
36
|
+
source: args.source,
|
|
37
|
+
authorSub: identity.sub,
|
|
38
|
+
authorLabel: label,
|
|
39
|
+
threadId: args.threadId,
|
|
40
|
+
parentMessageId: args.parentMessageId,
|
|
41
|
+
sequenceNumber: args.sequenceNumber,
|
|
42
|
+
role: args.role ? args.role : "human",
|
|
43
|
+
messageType: args.messageType,
|
|
44
|
+
content: content,
|
|
45
|
+
semanticLayer: args.semanticLayer,
|
|
46
|
+
searchVisibility: args.searchVisibility,
|
|
47
|
+
responseTarget: args.responseTarget,
|
|
48
|
+
responseStatus: args.responseStatus ? args.responseStatus : "COMPLETED",
|
|
49
|
+
responseOwner: args.responseOwner,
|
|
50
|
+
responseStartedAt: args.responseStartedAt,
|
|
51
|
+
responseCompletedAt: args.responseCompletedAt,
|
|
52
|
+
metadata: args.metadata,
|
|
53
|
+
createdAt: now,
|
|
54
|
+
updatedAt: now,
|
|
55
|
+
newsroomFeedKey: args.newsroomFeedKey,
|
|
56
|
+
});
|
|
57
|
+
return ddb.put({ key: { id: args.id }, item: item, condition: { id: { attributeExists: false } } });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function response(ctx) {
|
|
61
|
+
if (ctx.error) {
|
|
62
|
+
if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
|
|
63
|
+
util.error("A message with this id already exists (conditional check failed); try again.", "Conflict");
|
|
64
|
+
}
|
|
65
|
+
util.error(ctx.error.message, ctx.error.type);
|
|
66
|
+
}
|
|
67
|
+
return ctx.result;
|
|
68
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// renameThread: the person who started a conversation, or an admin, gives it a new title. The browser's own update of a thread
|
|
2
|
+
// cannot change the title (the field is read and create only), so this is the one way. Nothing else about the thread changes.
|
|
3
|
+
import { util } from "@aws-appsync/utils";
|
|
4
|
+
import * as ddb from "@aws-appsync/utils/dynamodb";
|
|
5
|
+
|
|
6
|
+
export function request(ctx) {
|
|
7
|
+
const identity = ctx.identity;
|
|
8
|
+
if (!identity || !identity.sub) util.error("Sign in to rename a conversation.", "Unauthorized");
|
|
9
|
+
const title = typeof ctx.args.title === "string" ? ctx.args.title.trim() : "";
|
|
10
|
+
if (!title) util.error("A title cannot be empty.", "BadRequest");
|
|
11
|
+
const groups = identity.groups || [];
|
|
12
|
+
const isAdmin = groups.indexOf("admin") >= 0;
|
|
13
|
+
return ddb.update({
|
|
14
|
+
key: { id: ctx.args.id },
|
|
15
|
+
update: { title: ddb.operations.replace(title.slice(0, 120)) },
|
|
16
|
+
condition: isAdmin ? { id: { attributeExists: true } } : { createdBySub: { eq: identity.sub } },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function response(ctx) {
|
|
21
|
+
if (ctx.error) {
|
|
22
|
+
if (ctx.error.type === "DynamoDB:ConditionalCheckFailedException") {
|
|
23
|
+
util.error("Only the person who started this conversation, or an admin, can rename it.", "Unauthorized");
|
|
24
|
+
}
|
|
25
|
+
util.error(ctx.error.message, ctx.error.type);
|
|
26
|
+
}
|
|
27
|
+
return ctx.result;
|
|
28
|
+
}
|
package/amplify/data/resource.js
CHANGED
|
@@ -1575,6 +1575,9 @@ const schema = a.schema({
|
|
|
1575
1575
|
curationDecisionRelationId: a.string(),
|
|
1576
1576
|
newsroomFeedKey: a.string(),
|
|
1577
1577
|
reviewedFeedKey: a.string(),
|
|
1578
|
+
discoveredByMonitorId: a.string(),
|
|
1579
|
+
discoveryDetails: a.json(),
|
|
1580
|
+
discoveryRule: a.json(),
|
|
1578
1581
|
// The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
|
|
1579
1582
|
// "<corpusId>#unrated" or "<corpusId>#rated" for a current, not archived Reference (null otherwise: it is in no feed).
|
|
1580
1583
|
// `predictionSortKey` orders Best bets, `feedArrivalAt` orders Newest. The prediction is the Cyclotron's ORIGINAL
|
|
@@ -1596,6 +1599,7 @@ const schema = a.schema({
|
|
|
1596
1599
|
index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
|
|
1597
1600
|
index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
|
|
1598
1601
|
index("reviewedFeedKey").sortKeys(["updatedAt"]).queryField("listReferencesByReviewedFeedAndUpdatedAt"),
|
|
1602
|
+
index("discoveredByMonitorId").sortKeys(["createdAt"]).queryField("listReferencesByMonitorAndCreatedAt"),
|
|
1599
1603
|
index("feedStateKey").sortKeys(["predictionSortKey"]).queryField("listReferencesByFeedStateAndPrediction"),
|
|
1600
1604
|
index("feedStateKey").sortKeys(["feedArrivalAt"]).queryField("listReferencesByFeedStateAndArrival"),
|
|
1601
1605
|
])
|
|
@@ -1722,12 +1726,11 @@ const schema = a.schema({
|
|
|
1722
1726
|
index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
|
|
1723
1727
|
])
|
|
1724
1728
|
.authorization((allow) => [
|
|
1725
|
-
//
|
|
1726
|
-
|
|
1727
|
-
allow.group(
|
|
1729
|
+
// Messages belong to their author, and the backend stamps the author: people read Messages here and write them only through
|
|
1730
|
+
// postMessage, editMessage, deleteOwnMessage and moderateMessage (message-actions.ts). Functions and IAM callers write directly.
|
|
1731
|
+
allow.group("editor").to(["read"]),
|
|
1732
|
+
allow.group(adminGroup).to(["read"]),
|
|
1728
1733
|
allow.authenticated("identityPool").to(["read", "create", "update"]),
|
|
1729
|
-
// Messages belong to their author: the author edits (and soft-deletes) their own, and only an admin can remove someone else's.
|
|
1730
|
-
allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
|
|
1731
1734
|
]),
|
|
1732
1735
|
MessageThread: a
|
|
1733
1736
|
.model({
|
package/amplify/data/resource.ts
CHANGED
|
@@ -1688,6 +1688,9 @@ const schema = a.schema({
|
|
|
1688
1688
|
curationDecisionRelationId: a.string(),
|
|
1689
1689
|
newsroomFeedKey: a.string(),
|
|
1690
1690
|
reviewedFeedKey: a.string(),
|
|
1691
|
+
discoveredByMonitorId: a.string(),
|
|
1692
|
+
discoveryDetails: a.json(),
|
|
1693
|
+
discoveryRule: a.json(),
|
|
1691
1694
|
// The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
|
|
1692
1695
|
// "<corpusId>#unrated" or "<corpusId>#rated" for a current, not archived Reference (null otherwise: it is in no feed).
|
|
1693
1696
|
// `predictionSortKey` orders Best bets, `feedArrivalAt` orders Newest. The prediction is the Cyclotron's ORIGINAL
|
|
@@ -1709,6 +1712,7 @@ const schema = a.schema({
|
|
|
1709
1712
|
index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
|
|
1710
1713
|
index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
|
|
1711
1714
|
index("reviewedFeedKey").sortKeys(["updatedAt"]).queryField("listReferencesByReviewedFeedAndUpdatedAt"),
|
|
1715
|
+
index("discoveredByMonitorId").sortKeys(["createdAt"]).queryField("listReferencesByMonitorAndCreatedAt"),
|
|
1712
1716
|
index("feedStateKey").sortKeys(["predictionSortKey"]).queryField("listReferencesByFeedStateAndPrediction"),
|
|
1713
1717
|
index("feedStateKey").sortKeys(["feedArrivalAt"]).queryField("listReferencesByFeedStateAndArrival"),
|
|
1714
1718
|
])
|
|
@@ -1838,12 +1842,11 @@ const schema = a.schema({
|
|
|
1838
1842
|
index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
|
|
1839
1843
|
])
|
|
1840
1844
|
.authorization((allow) => [
|
|
1841
|
-
//
|
|
1842
|
-
|
|
1843
|
-
allow.group(
|
|
1845
|
+
// Messages belong to their author, and the backend stamps the author: people read Messages here and write them only through
|
|
1846
|
+
// postMessage, editMessage, deleteOwnMessage and moderateMessage (message-actions.ts). Functions and IAM callers write directly.
|
|
1847
|
+
allow.group("editor").to(["read"]),
|
|
1848
|
+
allow.group(adminGroup).to(["read"]),
|
|
1844
1849
|
allow.authenticated("identityPool").to(["read", "create", "update"]),
|
|
1845
|
-
// Messages belong to their author: the author edits (and soft-deletes) their own, and only an admin can remove someone else's.
|
|
1846
|
-
allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
|
|
1847
1850
|
]),
|
|
1848
1851
|
|
|
1849
1852
|
MessageThread: a
|
package/amplify/data/schema.js
CHANGED
|
@@ -4,6 +4,7 @@ import { categoryAction } from "../functions/category-action/resource.js";
|
|
|
4
4
|
import { editorialChecksSchemaDefinitions } from "../functions/editorial-checks/schema.js";
|
|
5
5
|
import { automationViewSchemaDefinitions } from "../functions/automation-view/schema.js";
|
|
6
6
|
import { proposalActionsSchemaDefinitions } from "../functions/proposal-actions/schema.js";
|
|
7
|
+
import { messageActionsSchemaDefinitions } from "./message-actions.js";
|
|
7
8
|
import { monitoringViewSchemaDefinitions } from "../functions/monitoring-view/schema.js";
|
|
8
9
|
import { contentActions } from "../functions/content-actions/resource.js";
|
|
9
10
|
import { knowledgeQuery } from "../functions/knowledge-query/resource.js";
|
|
@@ -1481,6 +1482,9 @@ function buildBaseDefinitions(includeEditions) {
|
|
|
1481
1482
|
curationDecisionRelationId: a.string(),
|
|
1482
1483
|
newsroomFeedKey: a.string(),
|
|
1483
1484
|
reviewedFeedKey: a.string(),
|
|
1485
|
+
discoveredByMonitorId: a.string(),
|
|
1486
|
+
discoveryDetails: a.json(),
|
|
1487
|
+
discoveryRule: a.json(),
|
|
1484
1488
|
// The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
|
|
1485
1489
|
// "<corpusId>#unrated" or "<corpusId>#rated" for a current, not archived Reference (null otherwise: it is in no feed).
|
|
1486
1490
|
// `predictionSortKey` orders Best bets, `feedArrivalAt` orders Newest. The prediction is the Cyclotron's ORIGINAL
|
|
@@ -1502,6 +1506,7 @@ function buildBaseDefinitions(includeEditions) {
|
|
|
1502
1506
|
index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
|
|
1503
1507
|
index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
|
|
1504
1508
|
index("reviewedFeedKey").sortKeys(["updatedAt"]).queryField("listReferencesByReviewedFeedAndUpdatedAt"),
|
|
1509
|
+
index("discoveredByMonitorId").sortKeys(["createdAt"]).queryField("listReferencesByMonitorAndCreatedAt"),
|
|
1505
1510
|
index("feedStateKey").sortKeys(["predictionSortKey"]).queryField("listReferencesByFeedStateAndPrediction"),
|
|
1506
1511
|
index("feedStateKey").sortKeys(["feedArrivalAt"]).queryField("listReferencesByFeedStateAndArrival"),
|
|
1507
1512
|
])
|
|
@@ -1625,23 +1630,24 @@ function buildBaseDefinitions(includeEditions) {
|
|
|
1625
1630
|
index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
|
|
1626
1631
|
])
|
|
1627
1632
|
.authorization((allow) => [
|
|
1628
|
-
//
|
|
1629
|
-
|
|
1630
|
-
allow.group(
|
|
1631
|
-
|
|
1632
|
-
allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
|
|
1633
|
+
// Messages belong to their author, and the backend stamps the author: people read Messages here and write them only through
|
|
1634
|
+
// postMessage, editMessage, deleteOwnMessage and moderateMessage (message-actions.ts). Functions and IAM callers write directly.
|
|
1635
|
+
allow.group("editor").to(["read"]),
|
|
1636
|
+
allow.group(adminGroup).to(["read"]),
|
|
1633
1637
|
]),
|
|
1634
1638
|
MessageThread: realtimeModel({
|
|
1635
1639
|
id: a.id().required(),
|
|
1636
1640
|
threadKind: a.string().required(),
|
|
1637
1641
|
status: a.string().required(),
|
|
1638
|
-
title
|
|
1642
|
+
// The title and the creator are set when a conversation is created and read afterwards; a title changes only through
|
|
1643
|
+
// renameThread (its creator or an admin), and the creator never changes.
|
|
1644
|
+
title: a.string().required().authorization((allow) => [allow.group("editor").to(["read", "create"]), allow.group(adminGroup).to(["read", "create"])]),
|
|
1639
1645
|
summary: a.string(),
|
|
1640
1646
|
primaryAnchorKind: a.string(),
|
|
1641
1647
|
primaryAnchorId: a.id(),
|
|
1642
1648
|
primaryAnchorLineageId: a.id(),
|
|
1643
1649
|
primaryAnchorKey: a.string(),
|
|
1644
|
-
createdBySub: a.string(),
|
|
1650
|
+
createdBySub: a.string().authorization((allow) => [allow.group("editor").to(["read", "create"]), allow.group(adminGroup).to(["read", "create"])]),
|
|
1645
1651
|
createdByUserProfileId: a.id(),
|
|
1646
1652
|
createdByLabel: a.string(),
|
|
1647
1653
|
messageCount: a.integer(),
|
|
@@ -2126,6 +2132,7 @@ function buildBaseDefinitions(includeEditions) {
|
|
|
2126
2132
|
...monitoringViewSchemaDefinitions,
|
|
2127
2133
|
...automationViewSchemaDefinitions,
|
|
2128
2134
|
...proposalActionsSchemaDefinitions,
|
|
2135
|
+
...messageActionsSchemaDefinitions,
|
|
2129
2136
|
};
|
|
2130
2137
|
if (!includeEditions) {
|
|
2131
2138
|
const mutableDefinitions = definitions;
|
package/amplify/data/schema.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { categoryAction } from "../functions/category-action/resource";
|
|
|
4
4
|
import { editorialChecksSchemaDefinitions } from "../functions/editorial-checks/schema";
|
|
5
5
|
import { automationViewSchemaDefinitions } from "../functions/automation-view/schema";
|
|
6
6
|
import { proposalActionsSchemaDefinitions } from "../functions/proposal-actions/schema";
|
|
7
|
+
import { messageActionsSchemaDefinitions } from "./message-actions";
|
|
7
8
|
import { monitoringViewSchemaDefinitions } from "../functions/monitoring-view/schema";
|
|
8
9
|
import { contentActions } from "../functions/content-actions/resource";
|
|
9
10
|
import { knowledgeQuery } from "../functions/knowledge-query/resource";
|
|
@@ -1592,6 +1593,9 @@ const definitions = {
|
|
|
1592
1593
|
curationDecisionRelationId: a.string(),
|
|
1593
1594
|
newsroomFeedKey: a.string(),
|
|
1594
1595
|
reviewedFeedKey: a.string(),
|
|
1596
|
+
discoveredByMonitorId: a.string(),
|
|
1597
|
+
discoveryDetails: a.json(),
|
|
1598
|
+
discoveryRule: a.json(),
|
|
1595
1599
|
// The References page reads the feed through two indexes (docs/reference-feed-indexes.md). `feedStateKey` is
|
|
1596
1600
|
// "<corpusId>#unrated" or "<corpusId>#rated" for a current, not archived Reference (null otherwise: it is in no feed).
|
|
1597
1601
|
// `predictionSortKey` orders Best bets, `feedArrivalAt` orders Newest. The prediction is the Cyclotron's ORIGINAL
|
|
@@ -1613,6 +1617,7 @@ const definitions = {
|
|
|
1613
1617
|
index("curationStatus").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusAndUpdatedAt"),
|
|
1614
1618
|
index("curationStatusKey").sortKeys(["curationStatusUpdatedAt"]).queryField("listReferencesByCurationStatusKeyAndUpdatedAt"),
|
|
1615
1619
|
index("reviewedFeedKey").sortKeys(["updatedAt"]).queryField("listReferencesByReviewedFeedAndUpdatedAt"),
|
|
1620
|
+
index("discoveredByMonitorId").sortKeys(["createdAt"]).queryField("listReferencesByMonitorAndCreatedAt"),
|
|
1616
1621
|
index("feedStateKey").sortKeys(["predictionSortKey"]).queryField("listReferencesByFeedStateAndPrediction"),
|
|
1617
1622
|
index("feedStateKey").sortKeys(["feedArrivalAt"]).queryField("listReferencesByFeedStateAndArrival"),
|
|
1618
1623
|
])
|
|
@@ -1739,24 +1744,25 @@ const definitions = {
|
|
|
1739
1744
|
index("searchVisibility").sortKeys(["createdAt"]).name("messagesBySearchVisibilityCreatedAt").queryField("listMessagesBySearchVisibilityAndCreatedAt"),
|
|
1740
1745
|
])
|
|
1741
1746
|
.authorization((allow) => [
|
|
1742
|
-
//
|
|
1743
|
-
|
|
1744
|
-
allow.group(
|
|
1745
|
-
|
|
1746
|
-
allow.ownerDefinedIn("authorSub").identityClaim("sub").to(["read", "update", "delete"]),
|
|
1747
|
+
// Messages belong to their author, and the backend stamps the author: people read Messages here and write them only through
|
|
1748
|
+
// postMessage, editMessage, deleteOwnMessage and moderateMessage (message-actions.ts). Functions and IAM callers write directly.
|
|
1749
|
+
allow.group("editor").to(["read"]),
|
|
1750
|
+
allow.group(adminGroup).to(["read"]),
|
|
1747
1751
|
]),
|
|
1748
1752
|
|
|
1749
1753
|
MessageThread: realtimeModel({
|
|
1750
1754
|
id: a.id().required(),
|
|
1751
1755
|
threadKind: a.string().required(),
|
|
1752
1756
|
status: a.string().required(),
|
|
1753
|
-
title
|
|
1757
|
+
// The title and the creator are set when a conversation is created and read afterwards; a title changes only through
|
|
1758
|
+
// renameThread (its creator or an admin), and the creator never changes.
|
|
1759
|
+
title: a.string().required().authorization((allow) => [allow.group("editor").to(["read", "create"]), allow.group(adminGroup).to(["read", "create"])]),
|
|
1754
1760
|
summary: a.string(),
|
|
1755
1761
|
primaryAnchorKind: a.string(),
|
|
1756
1762
|
primaryAnchorId: a.id(),
|
|
1757
1763
|
primaryAnchorLineageId: a.id(),
|
|
1758
1764
|
primaryAnchorKey: a.string(),
|
|
1759
|
-
createdBySub: a.string(),
|
|
1765
|
+
createdBySub: a.string().authorization((allow) => [allow.group("editor").to(["read", "create"]), allow.group(adminGroup).to(["read", "create"])]),
|
|
1760
1766
|
createdByUserProfileId: a.id(),
|
|
1761
1767
|
createdByLabel: a.string(),
|
|
1762
1768
|
messageCount: a.integer(),
|
|
@@ -2256,6 +2262,7 @@ const definitions = {
|
|
|
2256
2262
|
...monitoringViewSchemaDefinitions,
|
|
2257
2263
|
...automationViewSchemaDefinitions,
|
|
2258
2264
|
...proposalActionsSchemaDefinitions,
|
|
2265
|
+
...messageActionsSchemaDefinitions,
|
|
2259
2266
|
};
|
|
2260
2267
|
if (!includeEditions) {
|
|
2261
2268
|
const mutableDefinitions = definitions as Record<string, unknown>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { createContext, useContext, type ReactNode } from "react";
|
|
4
|
-
import type
|
|
4
|
+
import { NO_VIEWER, canDeleteMessage, canEditMessage, type ConversationAnchor, type ConversationMessage, type ConversationViewer } from "../lib/conversation";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Where conversations are read and written. The newsroom provides the network one; a demo or a test provides the in-memory one. A
|
|
@@ -14,6 +14,11 @@ export type ConversationStore = {
|
|
|
14
14
|
append: (input: { anchor: ConversationAnchor; body: string; parentMessageId: string | null }) => Promise<ConversationMessage>;
|
|
15
15
|
/** Message counts by Reference lineage, for the feed cards. */
|
|
16
16
|
counts: () => Promise<Map<string, number>>;
|
|
17
|
+
/** Who is looking, and display names by profile id: what Edit, Delete and the author's name are decided from. */
|
|
18
|
+
viewer: () => Promise<ConversationViewer>;
|
|
19
|
+
authors: (profileIds: string[]) => Promise<Map<string, string>>;
|
|
20
|
+
edit: (messageId: string, body: string) => Promise<ConversationMessage>;
|
|
21
|
+
remove: (messageId: string, moderate: boolean) => Promise<ConversationMessage>;
|
|
17
22
|
};
|
|
18
23
|
|
|
19
24
|
const ConversationStoreContext = createContext<ConversationStore | null>(null);
|
|
@@ -27,11 +32,32 @@ export function useConversationStore(): ConversationStore | null {
|
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
/** A store that keeps everything in memory: the demo newsroom, and the tests. */
|
|
30
|
-
export function createMemoryConversationStore(authorLabel: string, seed: Record<string, ConversationMessage[]> = {}): ConversationStore {
|
|
35
|
+
export function createMemoryConversationStore(authorLabel: string, seed: Record<string, ConversationMessage[]> = {}, who: { viewer?: ConversationViewer; profiles?: Record<string, string> } = {}): ConversationStore {
|
|
36
|
+
const viewer = who.viewer ?? NO_VIEWER;
|
|
31
37
|
const byAnchor = new Map<string, ConversationMessage[]>(Object.entries(seed));
|
|
32
38
|
const keyOf = (anchor: ConversationAnchor) => `${anchor.kind}#${anchor.lineageId ?? ""}`;
|
|
39
|
+
const change = (messageId: string, apply: (message: ConversationMessage) => ConversationMessage) => {
|
|
40
|
+
for (const [key, list] of byAnchor) {
|
|
41
|
+
const found = list.find((message) => message.id === messageId);
|
|
42
|
+
if (!found) continue;
|
|
43
|
+
const changed = apply(found);
|
|
44
|
+
byAnchor.set(key, list.map((message) => (message.id === messageId ? changed : message)));
|
|
45
|
+
return changed;
|
|
46
|
+
}
|
|
47
|
+
throw new Error("That message is not here.");
|
|
48
|
+
};
|
|
33
49
|
return {
|
|
34
50
|
authorLabel,
|
|
51
|
+
viewer: async () => viewer,
|
|
52
|
+
authors: async (ids) => new Map(ids.filter((id) => who.profiles?.[id]).map((id) => [id, who.profiles![id]])),
|
|
53
|
+
edit: async (messageId, body) => change(messageId, (message) => {
|
|
54
|
+
if (!canEditMessage(viewer, message)) throw new Error("Only the author can edit a message.");
|
|
55
|
+
return { ...message, content: body.trim(), editedAt: new Date().toISOString() };
|
|
56
|
+
}),
|
|
57
|
+
remove: async (messageId) => change(messageId, (message) => {
|
|
58
|
+
if (!canDeleteMessage(viewer, message)) throw new Error("Only the author or an admin can delete a message.");
|
|
59
|
+
return { ...message, content: "", deleted: true, deletedAt: new Date().toISOString() };
|
|
60
|
+
}),
|
|
35
61
|
load: async (anchor) => [...(byAnchor.get(keyOf(anchor)) ?? [])],
|
|
36
62
|
append: async ({ anchor, body, parentMessageId }) => {
|
|
37
63
|
const list = byAnchor.get(keyOf(anchor)) ?? [];
|
|
@@ -40,6 +66,7 @@ export function createMemoryConversationStore(authorLabel: string, seed: Record<
|
|
|
40
66
|
id: `message-memory-${sequenceNumber}-${list.length}`,
|
|
41
67
|
parentMessageId,
|
|
42
68
|
authorLabel,
|
|
69
|
+
authorSub: viewer.sub,
|
|
43
70
|
content: body.trim(),
|
|
44
71
|
createdAt: new Date().toISOString(),
|
|
45
72
|
sequenceNumber,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useMemo, type ReactNode } from "react";
|
|
4
4
|
import { ConversationStoreProvider, createMemoryConversationStore, type ConversationStore } from "./conversation-context";
|
|
5
|
-
import { appendConversationMessage, loadConversationMessages, loadReferenceConversationCounts } from "./news-desk-taxonomy-client";
|
|
5
|
+
import { appendConversationMessage, deleteConversationMessage, editConversationMessage, loadConversationMessages, loadConversationViewer, loadReferenceConversationCounts, loadUserProfileNames } from "./news-desk-taxonomy-client";
|
|
6
6
|
|
|
7
7
|
function networkConversationStore(authorLabel: string): ConversationStore {
|
|
8
8
|
return {
|
|
@@ -10,6 +10,10 @@ function networkConversationStore(authorLabel: string): ConversationStore {
|
|
|
10
10
|
load: loadConversationMessages,
|
|
11
11
|
append: async ({ anchor, body, parentMessageId }) => (await appendConversationMessage({ anchor, body, parentMessageId })).message,
|
|
12
12
|
counts: loadReferenceConversationCounts,
|
|
13
|
+
viewer: loadConversationViewer,
|
|
14
|
+
authors: loadUserProfileNames,
|
|
15
|
+
edit: editConversationMessage,
|
|
16
|
+
remove: (messageId, moderate) => deleteConversationMessage(messageId, moderate),
|
|
13
17
|
};
|
|
14
18
|
}
|
|
15
19
|
|