@better-apps/comments 0.0.0-stage → 0.0.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/LICENSE +21 -0
- package/README.md +179 -2
- package/dist/access.d.ts +66 -0
- package/dist/access.js +186 -0
- package/dist/client.d.ts +140 -0
- package/dist/client.js +598 -0
- package/dist/content.d.ts +45 -0
- package/dist/content.js +136 -0
- package/dist/cursor.d.ts +7 -0
- package/dist/cursor.js +23 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +11 -0
- package/dist/events.d.ts +74 -0
- package/dist/events.js +21 -0
- package/dist/feature.d.ts +270 -0
- package/dist/feature.js +97 -0
- package/dist/formats.d.ts +26 -0
- package/dist/formats.js +25 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +9 -0
- package/dist/inputs.d.ts +108 -0
- package/dist/inputs.js +69 -0
- package/dist/mentions.d.ts +5 -0
- package/dist/mentions.js +48 -0
- package/dist/options.d.ts +161 -0
- package/dist/options.js +1 -0
- package/dist/react.d.ts +122 -0
- package/dist/react.js +179 -0
- package/dist/reads.d.ts +54 -0
- package/dist/reads.js +297 -0
- package/dist/runtime.d.ts +52 -0
- package/dist/runtime.js +110 -0
- package/dist/schema.d.ts +62 -0
- package/dist/schema.js +53 -0
- package/dist/subjects.d.ts +17 -0
- package/dist/subjects.js +93 -0
- package/dist/ulid.d.ts +4 -0
- package/dist/ulid.js +24 -0
- package/dist/upkeep.d.ts +27 -0
- package/dist/upkeep.js +189 -0
- package/dist/writes.d.ts +27 -0
- package/dist/writes.js +463 -0
- package/package.json +89 -3
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "better-apps";
|
|
2
|
+
/**
|
|
3
|
+
* What a comment body is and how it's made safe. The server never depends on an editor: a format checks and sanitizes
|
|
4
|
+
* a body, and turns it into plain text and mentions, so search, emails and notifications work with any editor.
|
|
5
|
+
*/
|
|
6
|
+
export interface BodyFormat<Name extends string = string, Body = unknown> {
|
|
7
|
+
name: Name;
|
|
8
|
+
/** Checks a body's shape. */
|
|
9
|
+
schema: StandardSchemaV1<unknown, Body>;
|
|
10
|
+
/** Makes a checked body safe to store and show, such as by removing markup the format doesn't allow. */
|
|
11
|
+
sanitize?(body: Body): Body;
|
|
12
|
+
/**
|
|
13
|
+
* Plain text for search, emails and previews. Without `names`, mentions stay as tokens, so a removed user's name is
|
|
14
|
+
* never stored. With it, they become names, and users it can't name show as "Former user".
|
|
15
|
+
*/
|
|
16
|
+
toText(body: Body, names?: (userId: string) => string | undefined): string;
|
|
17
|
+
/** The ids of the users the body mentions, each once, in order. */
|
|
18
|
+
mentions(body: Body): string[];
|
|
19
|
+
}
|
|
20
|
+
/** Declares a body format and keeps its name and body types for inference. */
|
|
21
|
+
export declare function defineFormat<const Name extends string, Body>(format: BodyFormat<Name, Body>): BodyFormat<Name, Body>;
|
|
22
|
+
/**
|
|
23
|
+
* Plain text with mention tokens such as `@[user:u_123]`, built with `mentionToken` from `@better-apps/mentions`. It
|
|
24
|
+
* holds no markup, so there's nothing to sanitize away.
|
|
25
|
+
*/
|
|
26
|
+
export declare const textFormat: BodyFormat<"text", string>;
|
package/dist/formats.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { parseMentions, replaceMentions } from "@better-apps/mentions";
|
|
2
|
+
/** Declares a body format and keeps its name and body types for inference. */
|
|
3
|
+
export function defineFormat(format) {
|
|
4
|
+
return format;
|
|
5
|
+
}
|
|
6
|
+
const textSchema = {
|
|
7
|
+
"~standard": {
|
|
8
|
+
version: 1,
|
|
9
|
+
vendor: "better-apps",
|
|
10
|
+
validate: (value) => typeof value === "string" && value.trim() !== ""
|
|
11
|
+
? { value }
|
|
12
|
+
: { issues: [{ message: "A text comment needs some text." }] }
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Plain text with mention tokens such as `@[user:u_123]`, built with `mentionToken` from `@better-apps/mentions`. It
|
|
17
|
+
* holds no markup, so there's nothing to sanitize away.
|
|
18
|
+
*/
|
|
19
|
+
export const textFormat = defineFormat({
|
|
20
|
+
name: "text",
|
|
21
|
+
schema: textSchema,
|
|
22
|
+
sanitize: (body) => body.replace(/\r\n?/g, "\n").trim(),
|
|
23
|
+
toText: (body, names) => (names ? replaceMentions(body, names) : body),
|
|
24
|
+
mentions: parseMentions
|
|
25
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { limits } from "./content.js";
|
|
2
|
+
export { CommentsError, type CommentsErrorCode } from "./errors.js";
|
|
3
|
+
export { commentEvents, type CommentEventData, type CommentsEventData, type StoredAnchor, type ThreadDeletedData, type ThreadEventData, type ThreadMovedData } from "./events.js";
|
|
4
|
+
export { comments, type CommentsFeature, type CountThreadsInput, type GetThreadInput, type ListThreadsInput, type SuggestMentionsInput, type ThreadCounts, type WriteResult } from "./feature.js";
|
|
5
|
+
export { defineFormat, textFormat, type BodyFormat } from "./formats.js";
|
|
6
|
+
export { commentMentions } from "./mentions.js";
|
|
7
|
+
export type { BeforeHooks, CommentRules, CommentTarget, CommentsOptions, CommentsTypes, CreateThreadInput, EditCommentInput, HookArgs, ReplyInput, ThreadTarget, TypesOf, UpdateAnchorInput, UpdateThreadInput } from "./options.js";
|
|
8
|
+
export type { ClientEvent, LiveViewer, ThreadPage, ThreadWithComments } from "./reads.js";
|
|
9
|
+
export { commentModel, commentsSchema, threadModel, type AdditionalFields, type Comment, type CommentThread, type InferFields } from "./schema.js";
|
|
10
|
+
export { commentSubjectTypes } from "./subjects.js";
|
|
11
|
+
export { isUlid, ulid } from "./ulid.js";
|
|
12
|
+
export type { UpkeepStep } from "./upkeep.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { limits } from "./content.js";
|
|
2
|
+
export { CommentsError } from "./errors.js";
|
|
3
|
+
export { commentEvents } from "./events.js";
|
|
4
|
+
export { comments } from "./feature.js";
|
|
5
|
+
export { defineFormat, textFormat } from "./formats.js";
|
|
6
|
+
export { commentMentions } from "./mentions.js";
|
|
7
|
+
export { commentModel, commentsSchema, threadModel } from "./schema.js";
|
|
8
|
+
export { commentSubjectTypes } from "./subjects.js";
|
|
9
|
+
export { isUlid, ulid } from "./ulid.js";
|
package/dist/inputs.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const inputs: {
|
|
3
|
+
listThreads: z.ZodObject<{
|
|
4
|
+
subjectType: z.ZodString;
|
|
5
|
+
subjectId: z.ZodString;
|
|
6
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
7
|
+
open: "open";
|
|
8
|
+
resolved: "resolved";
|
|
9
|
+
}>>;
|
|
10
|
+
anchorKind: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
11
|
+
where: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>>;
|
|
12
|
+
order: z.ZodOptional<z.ZodEnum<{
|
|
13
|
+
activity: "activity";
|
|
14
|
+
created: "created";
|
|
15
|
+
}>>;
|
|
16
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
17
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
18
|
+
}, z.core.$strip>;
|
|
19
|
+
getThread: z.ZodObject<{
|
|
20
|
+
threadId: z.ZodString;
|
|
21
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
22
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
countThreads: z.ZodObject<{
|
|
25
|
+
subjectType: z.ZodString;
|
|
26
|
+
subjectIds: z.ZodArray<z.ZodString>;
|
|
27
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
28
|
+
open: "open";
|
|
29
|
+
resolved: "resolved";
|
|
30
|
+
}>>;
|
|
31
|
+
}, z.core.$strip>;
|
|
32
|
+
createThread: z.ZodObject<{
|
|
33
|
+
format: z.ZodOptional<z.ZodString>;
|
|
34
|
+
body: z.ZodUnknown;
|
|
35
|
+
id: z.ZodOptional<z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>>;
|
|
36
|
+
commentId: z.ZodOptional<z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>>;
|
|
37
|
+
subjectType: z.ZodString;
|
|
38
|
+
subjectId: z.ZodString;
|
|
39
|
+
visibility: z.ZodOptional<z.ZodString>;
|
|
40
|
+
anchor: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
41
|
+
kind: z.ZodString;
|
|
42
|
+
value: z.ZodUnknown;
|
|
43
|
+
version: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
44
|
+
}, z.core.$strip>>>;
|
|
45
|
+
metadata: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
46
|
+
fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
47
|
+
commentFields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
48
|
+
}, z.core.$strip>;
|
|
49
|
+
reply: z.ZodObject<{
|
|
50
|
+
format: z.ZodOptional<z.ZodString>;
|
|
51
|
+
body: z.ZodUnknown;
|
|
52
|
+
id: z.ZodOptional<z.ZodString & z.ZodType<string, string, z.core.$ZodTypeInternals<string, string>>>;
|
|
53
|
+
threadId: z.ZodString;
|
|
54
|
+
fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
55
|
+
}, z.core.$strip>;
|
|
56
|
+
editComment: z.ZodObject<{
|
|
57
|
+
format: z.ZodOptional<z.ZodString>;
|
|
58
|
+
body: z.ZodUnknown;
|
|
59
|
+
commentId: z.ZodString;
|
|
60
|
+
}, z.core.$strip>;
|
|
61
|
+
deleteComment: z.ZodObject<{
|
|
62
|
+
commentId: z.ZodString;
|
|
63
|
+
}, z.core.$strip>;
|
|
64
|
+
thread: z.ZodObject<{
|
|
65
|
+
threadId: z.ZodString;
|
|
66
|
+
}, z.core.$strip>;
|
|
67
|
+
updateThread: z.ZodObject<{
|
|
68
|
+
threadId: z.ZodString;
|
|
69
|
+
metadata: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
70
|
+
visibility: z.ZodOptional<z.ZodString>;
|
|
71
|
+
fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
72
|
+
}, z.core.$strip>;
|
|
73
|
+
updateAnchor: z.ZodObject<{
|
|
74
|
+
threadId: z.ZodString;
|
|
75
|
+
anchor: z.ZodNullable<z.ZodObject<{
|
|
76
|
+
kind: z.ZodString;
|
|
77
|
+
value: z.ZodUnknown;
|
|
78
|
+
version: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
79
|
+
}, z.core.$strip>>;
|
|
80
|
+
}, z.core.$strip>;
|
|
81
|
+
suggestMentions: z.ZodUnion<readonly [z.ZodObject<{
|
|
82
|
+
threadId: z.ZodString;
|
|
83
|
+
query: z.ZodString;
|
|
84
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
85
|
+
subjectType: z.ZodString;
|
|
86
|
+
subjectId: z.ZodString;
|
|
87
|
+
query: z.ZodString;
|
|
88
|
+
}, z.core.$strip>]>;
|
|
89
|
+
removeSubject: z.ZodObject<{
|
|
90
|
+
subjectType: z.ZodString;
|
|
91
|
+
subjectId: z.ZodString;
|
|
92
|
+
}, z.core.$strip>;
|
|
93
|
+
moveSubject: z.ZodObject<{
|
|
94
|
+
subjectType: z.ZodString;
|
|
95
|
+
subjectId: z.ZodString;
|
|
96
|
+
toOrgId: z.ZodNullable<z.ZodString>;
|
|
97
|
+
}, z.core.$strip>;
|
|
98
|
+
removeUser: z.ZodObject<{
|
|
99
|
+
userId: z.ZodString;
|
|
100
|
+
}, z.core.$strip>;
|
|
101
|
+
recountThreads: z.ZodUnion<readonly [z.ZodObject<{
|
|
102
|
+
threadIds: z.ZodArray<z.ZodString>;
|
|
103
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
104
|
+
subjectType: z.ZodString;
|
|
105
|
+
subjectId: z.ZodString;
|
|
106
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
107
|
+
}, z.core.$strip>]>;
|
|
108
|
+
};
|
package/dist/inputs.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { limits } from "./content.js";
|
|
3
|
+
import { isUlid } from "./ulid.js";
|
|
4
|
+
// Shapes only. Bodies, anchors, metadata and additional fields are checked by their format, anchor kind, schema or
|
|
5
|
+
// field type once any hook has run.
|
|
6
|
+
const newId = z.string().refine(isUlid, "Ids are ULIDs.");
|
|
7
|
+
const rowId = z.string().min(1).max(64);
|
|
8
|
+
const subjectType = z.string().min(1).max(64);
|
|
9
|
+
const subjectId = z.string().min(1).max(255);
|
|
10
|
+
const cursor = z.string().min(1).max(512);
|
|
11
|
+
const record = z.record(z.string(), z.unknown());
|
|
12
|
+
const anchor = z.object({ kind: z.string().min(1).max(64), value: z.unknown(), version: z.string().max(255).nullish() });
|
|
13
|
+
const content = { format: z.string().min(1).max(64).optional(), body: z.unknown() };
|
|
14
|
+
export const inputs = {
|
|
15
|
+
listThreads: z.object({
|
|
16
|
+
subjectType,
|
|
17
|
+
subjectId,
|
|
18
|
+
status: z.enum(["open", "resolved"]).optional(),
|
|
19
|
+
anchorKind: z.string().min(1).max(64).nullable().optional(),
|
|
20
|
+
where: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()])).optional(),
|
|
21
|
+
order: z.enum(["activity", "created"]).optional(),
|
|
22
|
+
cursor: cursor.optional(),
|
|
23
|
+
limit: z.number().int().min(1).max(limits.maxThreadsPerPage).optional()
|
|
24
|
+
}),
|
|
25
|
+
getThread: z.object({
|
|
26
|
+
threadId: rowId,
|
|
27
|
+
cursor: cursor.optional(),
|
|
28
|
+
limit: z.number().int().min(1).max(limits.maxCommentsPerPage).optional()
|
|
29
|
+
}),
|
|
30
|
+
countThreads: z.object({
|
|
31
|
+
subjectType,
|
|
32
|
+
subjectIds: z.array(subjectId).min(1).max(limits.subjectsPerCount),
|
|
33
|
+
status: z.enum(["open", "resolved"]).optional()
|
|
34
|
+
}),
|
|
35
|
+
createThread: z.object({
|
|
36
|
+
id: newId.optional(),
|
|
37
|
+
commentId: newId.optional(),
|
|
38
|
+
subjectType,
|
|
39
|
+
subjectId,
|
|
40
|
+
visibility: z.string().min(1).max(64).optional(),
|
|
41
|
+
anchor: anchor.nullish(),
|
|
42
|
+
metadata: record.nullish(),
|
|
43
|
+
fields: record.optional(),
|
|
44
|
+
commentFields: record.optional(),
|
|
45
|
+
...content
|
|
46
|
+
}),
|
|
47
|
+
reply: z.object({ id: newId.optional(), threadId: rowId, fields: record.optional(), ...content }),
|
|
48
|
+
editComment: z.object({ commentId: rowId, ...content }),
|
|
49
|
+
deleteComment: z.object({ commentId: rowId }),
|
|
50
|
+
thread: z.object({ threadId: rowId }),
|
|
51
|
+
updateThread: z.object({
|
|
52
|
+
threadId: rowId,
|
|
53
|
+
metadata: record.nullish(),
|
|
54
|
+
visibility: z.string().min(1).max(64).optional(),
|
|
55
|
+
fields: record.optional()
|
|
56
|
+
}),
|
|
57
|
+
updateAnchor: z.object({ threadId: rowId, anchor: anchor.nullable() }),
|
|
58
|
+
suggestMentions: z.union([
|
|
59
|
+
z.object({ threadId: rowId, query: z.string().max(100) }),
|
|
60
|
+
z.object({ subjectType, subjectId, query: z.string().max(100) })
|
|
61
|
+
]),
|
|
62
|
+
removeSubject: z.object({ subjectType, subjectId }),
|
|
63
|
+
moveSubject: z.object({ subjectType, subjectId, toOrgId: z.string().max(255).nullable() }),
|
|
64
|
+
removeUser: z.object({ userId: z.string().min(1).max(255) }),
|
|
65
|
+
recountThreads: z.union([
|
|
66
|
+
z.object({ threadIds: z.array(rowId).min(1).max(limits.maxThreadsPerPage) }),
|
|
67
|
+
z.object({ subjectType, subjectId, cursor: cursor.optional() })
|
|
68
|
+
])
|
|
69
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Records comments' mentions for the mentions feature, so each user can list where they were mentioned:
|
|
3
|
+
* `mentions({ sources: [commentMentions] })`. A mention's record is the comment, and its group the thread.
|
|
4
|
+
*/
|
|
5
|
+
export declare const commentMentions: import("@better-apps/mentions").MentionSource;
|
package/dist/mentions.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { defineMentionSource, parseMentions } from "@better-apps/mentions";
|
|
2
|
+
import { commentSubjectTypes } from "./subjects.js";
|
|
3
|
+
const subjectType = commentSubjectTypes.comment;
|
|
4
|
+
/**
|
|
5
|
+
* Records comments' mentions for the mentions feature, so each user can list where they were mentioned:
|
|
6
|
+
* `mentions({ sources: [commentMentions] })`. A mention's record is the comment, and its group the thread.
|
|
7
|
+
*/
|
|
8
|
+
export const commentMentions = defineMentionSource({
|
|
9
|
+
feature: "comments",
|
|
10
|
+
subjectTypes: [subjectType],
|
|
11
|
+
events: {
|
|
12
|
+
"comment.mentioned": (event) => {
|
|
13
|
+
const { userId, orgId, thread, comment } = event.data;
|
|
14
|
+
return {
|
|
15
|
+
add: [
|
|
16
|
+
{
|
|
17
|
+
userId,
|
|
18
|
+
orgId: orgId === "" ? null : orgId,
|
|
19
|
+
authorId: comment.authorType === "user" ? comment.authorId : null,
|
|
20
|
+
subjectType,
|
|
21
|
+
subjectId: comment.id,
|
|
22
|
+
groupId: thread.id,
|
|
23
|
+
excerpt: comment.bodyText
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
// Body text keeps mentions as tokens in every format, so it says who an edited comment still mentions.
|
|
29
|
+
"comment.edited": (event) => {
|
|
30
|
+
const { comment } = event.data;
|
|
31
|
+
return {
|
|
32
|
+
edit: [{ subjectType, subjectId: comment.id, keep: parseMentions(comment.bodyText), excerpt: comment.bodyText }]
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
"comment.deleted": (event) => {
|
|
36
|
+
const { comment } = event.data;
|
|
37
|
+
return { remove: [{ subjectType, subjectId: comment.id }] };
|
|
38
|
+
},
|
|
39
|
+
"thread.deleted": (event) => {
|
|
40
|
+
const { thread } = event.data;
|
|
41
|
+
return { removeGroups: [thread.id] };
|
|
42
|
+
},
|
|
43
|
+
"user.removed": (event) => {
|
|
44
|
+
const { userId } = event.data;
|
|
45
|
+
return { removeUsers: [userId] };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { AnchorKind } from "@better-apps/anchors";
|
|
2
|
+
import type { Actor, Author, StandardSchemaV1, SubjectArgs } from "better-apps";
|
|
3
|
+
import type { BodyFormat } from "./formats.js";
|
|
4
|
+
import type { AdditionalFields, CommentThread, InferFields } from "./schema.js";
|
|
5
|
+
type Check<Args> = (args: Args) => boolean | Promise<boolean>;
|
|
6
|
+
/**
|
|
7
|
+
* How comments work on one subject type, such as `invoice`. Who can see a subject, and so its threads, comes from the
|
|
8
|
+
* app's subject type; these rules add who can comment, moderate and see restricted threads. Comments enforce them on
|
|
9
|
+
* every action.
|
|
10
|
+
*/
|
|
11
|
+
export interface CommentRules {
|
|
12
|
+
/** Whether the actor can create threads, reply, and resolve or reopen threads. */
|
|
13
|
+
canComment: Check<SubjectArgs>;
|
|
14
|
+
/** Whether the actor can delete any comment or thread, move any anchor and change a thread's visibility. Nobody, by default. */
|
|
15
|
+
canModerate?: Check<SubjectArgs> | undefined;
|
|
16
|
+
/** Extra visibilities, such as `staff`, each with a check saying who can see threads in it. */
|
|
17
|
+
visibility?: Readonly<Record<string, Check<SubjectArgs>>> | undefined;
|
|
18
|
+
/** A schema that lets clients write a thread's `metadata`. Without one, only server code writes it. */
|
|
19
|
+
metadata?: StandardSchemaV1<unknown, Record<string, unknown>> | undefined;
|
|
20
|
+
/** Whether a user can be mentioned in a thread. The default is whether they can view it. */
|
|
21
|
+
canMention?: Check<{
|
|
22
|
+
actor: Actor;
|
|
23
|
+
thread: CommentThread;
|
|
24
|
+
}> | undefined;
|
|
25
|
+
}
|
|
26
|
+
/** What every before hook receives. */
|
|
27
|
+
export interface HookArgs<Input> {
|
|
28
|
+
/** The signed-in user, or `null` for server code that names an author. */
|
|
29
|
+
actor: Actor | null;
|
|
30
|
+
author: Author;
|
|
31
|
+
/** The thread the action targets, or `null` for `createThread`. */
|
|
32
|
+
thread: CommentThread | null;
|
|
33
|
+
input: Input;
|
|
34
|
+
}
|
|
35
|
+
type Hook<Input> = (args: HookArgs<Input>) => Input | Promise<Input>;
|
|
36
|
+
/**
|
|
37
|
+
* Checks and changes before writes, keyed by action. A hook runs after the access check, so it only sees callers who
|
|
38
|
+
* may act. It returns the input, changed or not, or throws. It can't change the subject or thread the action targets.
|
|
39
|
+
*/
|
|
40
|
+
export interface BeforeHooks<Types extends CommentsTypes = CommentsTypes> {
|
|
41
|
+
createThread?: Hook<CreateThreadInput<Types>> | undefined;
|
|
42
|
+
reply?: Hook<ReplyInput<Types>> | undefined;
|
|
43
|
+
editComment?: Hook<EditCommentInput<Types>> | undefined;
|
|
44
|
+
deleteComment?: Hook<CommentTarget> | undefined;
|
|
45
|
+
resolveThread?: Hook<ThreadTarget> | undefined;
|
|
46
|
+
/** Also runs when a reply reopens a resolved thread, so a rule on reopening can't be skipped by replying. */
|
|
47
|
+
reopenThread?: Hook<ThreadTarget> | undefined;
|
|
48
|
+
updateThread?: Hook<UpdateThreadInput<Types>> | undefined;
|
|
49
|
+
updateAnchor?: Hook<UpdateAnchorInput<Types>> | undefined;
|
|
50
|
+
deleteThread?: Hook<ThreadTarget> | undefined;
|
|
51
|
+
}
|
|
52
|
+
/** How comments are set up in an app. */
|
|
53
|
+
export interface CommentsOptions {
|
|
54
|
+
/**
|
|
55
|
+
* The subject types that can have comments, keyed by name, with who can comment on each. The app declares each one in
|
|
56
|
+
* its own `subjects`, with who can see it. Register at least one.
|
|
57
|
+
*/
|
|
58
|
+
subjects: Readonly<Record<string, CommentRules>>;
|
|
59
|
+
/** Anchor kinds threads may use. Without any, threads are always on the whole subject. */
|
|
60
|
+
anchors?: readonly AnchorKind[] | undefined;
|
|
61
|
+
/** Body formats besides the built-in `text`. */
|
|
62
|
+
formats?: readonly BodyFormat[] | undefined;
|
|
63
|
+
hooks?: {
|
|
64
|
+
before?: BeforeHooks | undefined;
|
|
65
|
+
} | undefined;
|
|
66
|
+
additionalFields?: AdditionalFields | undefined;
|
|
67
|
+
}
|
|
68
|
+
/** The types an app's options give comments, for typed inputs and records. */
|
|
69
|
+
export interface CommentsTypes {
|
|
70
|
+
subjectType: string;
|
|
71
|
+
visibility: string;
|
|
72
|
+
anchor: {
|
|
73
|
+
kind: string;
|
|
74
|
+
value: unknown;
|
|
75
|
+
version?: string | null | undefined;
|
|
76
|
+
};
|
|
77
|
+
content: {
|
|
78
|
+
format?: string | undefined;
|
|
79
|
+
body: unknown;
|
|
80
|
+
};
|
|
81
|
+
threadFields: Record<string, unknown>;
|
|
82
|
+
commentFields: Record<string, unknown>;
|
|
83
|
+
}
|
|
84
|
+
type VisibilityOf<Subject> = Subject extends {
|
|
85
|
+
visibility?: infer Visibility;
|
|
86
|
+
} ? Visibility extends Readonly<Record<string, unknown>> ? keyof Visibility & string : never : never;
|
|
87
|
+
type AnchorOf<Anchors> = Anchors extends readonly (infer Anchor)[] ? Anchor extends AnchorKind<infer Kind, infer Value> ? {
|
|
88
|
+
kind: Kind;
|
|
89
|
+
value: Value;
|
|
90
|
+
version?: string | null | undefined;
|
|
91
|
+
} : never : never;
|
|
92
|
+
type ContentOf<Formats> = {
|
|
93
|
+
format?: "text" | undefined;
|
|
94
|
+
body: string;
|
|
95
|
+
} | (Formats extends readonly (infer Format)[] ? Format extends BodyFormat<infer Name, infer Body> ? {
|
|
96
|
+
format: Name;
|
|
97
|
+
body: Body;
|
|
98
|
+
} : never : never);
|
|
99
|
+
/** The types a set of options gives comments. */
|
|
100
|
+
export interface TypesOf<Options extends CommentsOptions> {
|
|
101
|
+
subjectType: keyof Options["subjects"] & string;
|
|
102
|
+
visibility: "default" | {
|
|
103
|
+
[Name in keyof Options["subjects"]]: VisibilityOf<Options["subjects"][Name]>;
|
|
104
|
+
}[keyof Options["subjects"]];
|
|
105
|
+
anchor: AnchorOf<Options["anchors"]>;
|
|
106
|
+
content: ContentOf<Options["formats"]>;
|
|
107
|
+
threadFields: Options["additionalFields"] extends {
|
|
108
|
+
thread: infer Fields;
|
|
109
|
+
} ? InferFields<Fields> : Record<never, never>;
|
|
110
|
+
commentFields: Options["additionalFields"] extends {
|
|
111
|
+
comment: infer Fields;
|
|
112
|
+
} ? InferFields<Fields> : Record<never, never>;
|
|
113
|
+
}
|
|
114
|
+
/** A thread id, for actions on a thread. */
|
|
115
|
+
export interface ThreadTarget {
|
|
116
|
+
threadId: string;
|
|
117
|
+
}
|
|
118
|
+
/** A comment id, for actions on a comment. */
|
|
119
|
+
export interface CommentTarget {
|
|
120
|
+
commentId: string;
|
|
121
|
+
}
|
|
122
|
+
/** The input of `createThread`. */
|
|
123
|
+
export type CreateThreadInput<Types extends CommentsTypes = CommentsTypes> = Types["content"] & {
|
|
124
|
+
/** The thread's ULID, so a retry can't create it twice. The server makes one when it's left out. */
|
|
125
|
+
id?: string | undefined;
|
|
126
|
+
/** The first comment's ULID. */
|
|
127
|
+
commentId?: string | undefined;
|
|
128
|
+
subjectType: Types["subjectType"];
|
|
129
|
+
subjectId: string;
|
|
130
|
+
visibility?: Types["visibility"] | undefined;
|
|
131
|
+
anchor?: Types["anchor"] | null | undefined;
|
|
132
|
+
metadata?: Record<string, unknown> | null | undefined;
|
|
133
|
+
fields?: Partial<Types["threadFields"]> | undefined;
|
|
134
|
+
commentFields?: Partial<Types["commentFields"]> | undefined;
|
|
135
|
+
};
|
|
136
|
+
/** The input of `reply`. */
|
|
137
|
+
export type ReplyInput<Types extends CommentsTypes = CommentsTypes> = Types["content"] & {
|
|
138
|
+
/** The comment's ULID, so a retry can't create it twice. */
|
|
139
|
+
id?: string | undefined;
|
|
140
|
+
threadId: string;
|
|
141
|
+
fields?: Partial<Types["commentFields"]> | undefined;
|
|
142
|
+
};
|
|
143
|
+
/** The input of `editComment`. */
|
|
144
|
+
export type EditCommentInput<Types extends CommentsTypes = CommentsTypes> = Types["content"] & {
|
|
145
|
+
commentId: string;
|
|
146
|
+
};
|
|
147
|
+
/** The input of `updateThread`. */
|
|
148
|
+
export interface UpdateThreadInput<Types extends CommentsTypes = CommentsTypes> {
|
|
149
|
+
threadId: string;
|
|
150
|
+
metadata?: Record<string, unknown> | null | undefined;
|
|
151
|
+
/** Moderators only. */
|
|
152
|
+
visibility?: Types["visibility"] | undefined;
|
|
153
|
+
fields?: Partial<Types["threadFields"]> | undefined;
|
|
154
|
+
}
|
|
155
|
+
/** The input of `updateAnchor`. */
|
|
156
|
+
export interface UpdateAnchorInput<Types extends CommentsTypes = CommentsTypes> {
|
|
157
|
+
threadId: string;
|
|
158
|
+
/** `null` moves the thread to the whole subject. */
|
|
159
|
+
anchor: Types["anchor"] | null;
|
|
160
|
+
}
|
|
161
|
+
export {};
|
package/dist/options.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { BetterApp, IdentityUser } from "better-apps";
|
|
2
|
+
import { type CommentsState, type ListOptions, type LoadState, type Subject } from "./client.js";
|
|
3
|
+
import type { CommentsTypes } from "./options.js";
|
|
4
|
+
import type { Comment, CommentThread } from "./schema.js";
|
|
5
|
+
export * from "./client.js";
|
|
6
|
+
/**
|
|
7
|
+
* The comments client for the nearest `BetterAppsProvider`. Each Better Apps client has one, so every hook shares one
|
|
8
|
+
* store, and so does code that called `createCommentsClient` with the same client, such as a router hydrating a server
|
|
9
|
+
* render's loads. Pass `typeof app` for the app's types.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useComments<App = BetterApp>(): {
|
|
12
|
+
store: import("better-apps/client").Store<CommentsState<CommentThread & import("./client.js").AppCommentsTypes<App>["threadFields"], Comment & import("./client.js").AppCommentsTypes<App>["commentFields"]>>;
|
|
13
|
+
loadThreads(subject: Subject<import("./client.js").AppCommentsTypes<App>["subjectType"]>, options?: ListOptions): Promise<void>;
|
|
14
|
+
loadMoreThreads(subject: Subject<import("./client.js").AppCommentsTypes<App>["subjectType"]>, options?: ListOptions): Promise<void>;
|
|
15
|
+
loadThread(threadId: string): Promise<void>;
|
|
16
|
+
loadMoreComments(threadId: string): Promise<void>;
|
|
17
|
+
loadCounts(subjectType: import("./client.js").AppCommentsTypes<App>["subjectType"], subjectIds: readonly string[]): Promise<void>;
|
|
18
|
+
createThread(input: import("./options.js").CreateThreadInput<import("./client.js").AppCommentsTypes<App>>): Promise<{
|
|
19
|
+
thread: CommentThread & import("./client.js").AppCommentsTypes<App>["threadFields"];
|
|
20
|
+
comment: Comment & import("./client.js").AppCommentsTypes<App>["commentFields"];
|
|
21
|
+
droppedMentions: string[];
|
|
22
|
+
}>;
|
|
23
|
+
reply(input: import("./options.js").ReplyInput<import("./client.js").AppCommentsTypes<App>>): Promise<{
|
|
24
|
+
thread: CommentThread & import("./client.js").AppCommentsTypes<App>["threadFields"];
|
|
25
|
+
comment: Comment & import("./client.js").AppCommentsTypes<App>["commentFields"];
|
|
26
|
+
droppedMentions: string[];
|
|
27
|
+
}>;
|
|
28
|
+
editComment(input: import("./options.js").EditCommentInput<import("./client.js").AppCommentsTypes<App>>): Promise<{
|
|
29
|
+
comment: Comment & import("./client.js").AppCommentsTypes<App>["commentFields"];
|
|
30
|
+
droppedMentions: string[];
|
|
31
|
+
}>;
|
|
32
|
+
deleteComment(input: import("./options.js").CommentTarget): Promise<{
|
|
33
|
+
comment: Comment & import("./client.js").AppCommentsTypes<App>["commentFields"];
|
|
34
|
+
threadDeleted: boolean;
|
|
35
|
+
}>;
|
|
36
|
+
resolveThread: (input: import("./options.js").ThreadTarget) => Promise<CommentThread & import("./client.js").AppCommentsTypes<App>["threadFields"]>;
|
|
37
|
+
reopenThread: (input: import("./options.js").ThreadTarget) => Promise<CommentThread & import("./client.js").AppCommentsTypes<App>["threadFields"]>;
|
|
38
|
+
updateThread(input: import("./options.js").UpdateThreadInput<import("./client.js").AppCommentsTypes<App>>): Promise<CommentThread & import("./client.js").AppCommentsTypes<App>["threadFields"]>;
|
|
39
|
+
updateAnchor(input: import("./options.js").UpdateAnchorInput<import("./client.js").AppCommentsTypes<App>>): Promise<CommentThread & import("./client.js").AppCommentsTypes<App>["threadFields"]>;
|
|
40
|
+
deleteThread(input: import("./options.js").ThreadTarget): Promise<{
|
|
41
|
+
threadId: string;
|
|
42
|
+
}>;
|
|
43
|
+
suggestMentions(input: import("./feature.js").SuggestMentionsInput<import("./client.js").AppCommentsTypes<App>["subjectType"]>): Promise<IdentityUser[]>;
|
|
44
|
+
applyEvent(event: import("./reads.js").ClientEvent | import("better-apps/client").Serialized<import("./reads.js").ClientEvent>): void;
|
|
45
|
+
dehydrate(): import("./client.js").DehydratedComments;
|
|
46
|
+
reset(): void;
|
|
47
|
+
onReset(listener: () => void): () => void;
|
|
48
|
+
hydrate(snapshot: import("./client.js").DehydratedComments): void;
|
|
49
|
+
};
|
|
50
|
+
type ThreadOf<Types extends CommentsTypes> = CommentThread & Types["threadFields"];
|
|
51
|
+
type MessageOf<Types extends CommentsTypes> = Comment & Types["commentFields"];
|
|
52
|
+
/** A thread list as hooks return it. */
|
|
53
|
+
export interface ThreadsResult<Thread> {
|
|
54
|
+
threads: Thread[];
|
|
55
|
+
state: LoadState;
|
|
56
|
+
error: Error | null;
|
|
57
|
+
hasMore: boolean;
|
|
58
|
+
loadMore(): Promise<void>;
|
|
59
|
+
refresh(): Promise<void>;
|
|
60
|
+
users: Readonly<Record<string, IdentityUser>>;
|
|
61
|
+
}
|
|
62
|
+
/** A subject's threads, loaded on first use. A list a server render already loaded isn't fetched again. */
|
|
63
|
+
export declare function useThreads<Types extends CommentsTypes = CommentsTypes>(subject: Subject<Types["subjectType"]>, options?: ListOptions): ThreadsResult<ThreadOf<Types>>;
|
|
64
|
+
/** A thread with its comments, oldest first, loaded on first use. */
|
|
65
|
+
export declare function useThread<Types extends CommentsTypes = CommentsTypes>(threadId: string | null): {
|
|
66
|
+
thread: ThreadOf<Types> | undefined;
|
|
67
|
+
comments: MessageOf<Types>[];
|
|
68
|
+
state: LoadState;
|
|
69
|
+
error: Error | null;
|
|
70
|
+
hasMore: boolean;
|
|
71
|
+
loadMore: () => Promise<void>;
|
|
72
|
+
users: Readonly<Record<string, IdentityUser>>;
|
|
73
|
+
/** Whether a thread or comment is still waiting for the server. */
|
|
74
|
+
isPending: (id: string) => boolean;
|
|
75
|
+
};
|
|
76
|
+
/** Open and total thread counts for many records, such as badges in a table. Lists over 50 are split into calls. */
|
|
77
|
+
export declare function useThreadCounts<Types extends CommentsTypes = CommentsTypes>(subjectType: Types["subjectType"], subjectIds: readonly string[]): Readonly<Record<string, {
|
|
78
|
+
open: number;
|
|
79
|
+
total: number;
|
|
80
|
+
}>>;
|
|
81
|
+
/** A write as hooks return it: `mutate` runs it, and the rest describes the latest run. */
|
|
82
|
+
export interface Mutation<Input, Output> {
|
|
83
|
+
mutate(input: Input): Promise<Output>;
|
|
84
|
+
status: "idle" | "pending" | "success" | "error";
|
|
85
|
+
error: Error | null;
|
|
86
|
+
reset(): void;
|
|
87
|
+
}
|
|
88
|
+
/** Starts a thread, shown at once and rolled back if the server refuses it. */
|
|
89
|
+
export declare const useCreateThread: () => Mutation<import("./options.js").CreateThreadInput<import("./options.js").TypesOf<import("./options.js").CommentsOptions>>, {
|
|
90
|
+
thread: CommentThread & Record<never, never>;
|
|
91
|
+
comment: Comment & Record<never, never>;
|
|
92
|
+
droppedMentions: string[];
|
|
93
|
+
}>;
|
|
94
|
+
/** Replies to a thread, reopening it if it was resolved. */
|
|
95
|
+
export declare const useReply: () => Mutation<import("./options.js").ReplyInput<import("./options.js").TypesOf<import("./options.js").CommentsOptions>>, {
|
|
96
|
+
thread: CommentThread & Record<never, never>;
|
|
97
|
+
comment: Comment & Record<never, never>;
|
|
98
|
+
droppedMentions: string[];
|
|
99
|
+
}>;
|
|
100
|
+
export declare const useEditComment: () => Mutation<import("./options.js").EditCommentInput<import("./options.js").TypesOf<import("./options.js").CommentsOptions>>, {
|
|
101
|
+
comment: Comment & Record<never, never>;
|
|
102
|
+
droppedMentions: string[];
|
|
103
|
+
}>;
|
|
104
|
+
export declare const useDeleteComment: () => Mutation<import("./options.js").CommentTarget, {
|
|
105
|
+
comment: Comment & Record<never, never>;
|
|
106
|
+
threadDeleted: boolean;
|
|
107
|
+
}>;
|
|
108
|
+
export declare const useResolveThread: () => Mutation<import("./options.js").ThreadTarget, CommentThread & Record<never, never>>;
|
|
109
|
+
export declare const useReopenThread: () => Mutation<import("./options.js").ThreadTarget, CommentThread & Record<never, never>>;
|
|
110
|
+
export declare const useUpdateThread: () => Mutation<import("./options.js").UpdateThreadInput<import("./options.js").TypesOf<import("./options.js").CommentsOptions>>, CommentThread & Record<never, never>>;
|
|
111
|
+
export declare const useUpdateAnchor: () => Mutation<import("./options.js").UpdateAnchorInput<import("./options.js").TypesOf<import("./options.js").CommentsOptions>>, CommentThread & Record<never, never>>;
|
|
112
|
+
export declare const useDeleteThread: () => Mutation<import("./options.js").ThreadTarget, {
|
|
113
|
+
threadId: string;
|
|
114
|
+
}>;
|
|
115
|
+
/** Mention suggestions for a thread, or for a subject before its thread exists. `search` asks for a query's matches. */
|
|
116
|
+
export declare function useSuggestMentions(target: {
|
|
117
|
+
threadId: string;
|
|
118
|
+
} | Subject): {
|
|
119
|
+
suggestions: IdentityUser[];
|
|
120
|
+
error: Error | null;
|
|
121
|
+
search: (query: string) => Promise<IdentityUser[]>;
|
|
122
|
+
};
|