@tldraw/tlschema 5.3.0-next.d7d8ced023d5 → 5.3.0-next.dec36c5c2930
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/dist-cjs/index.d.ts +21 -46
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/records/TLComment.js.map +2 -2
- package/dist-esm/index.d.mts +21 -46
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/records/TLComment.mjs.map +2 -2
- package/package.json +5 -5
- package/src/records/TLComment.ts +24 -51
package/dist-cjs/index.d.ts
CHANGED
|
@@ -882,16 +882,10 @@ export declare function createCommentReaction(props: {
|
|
|
882
882
|
|
|
883
883
|
/**
|
|
884
884
|
* The id of one user's reaction to one comment, derived from the (comment, user, emoji) triple
|
|
885
|
-
* rather than random
|
|
886
|
-
*
|
|
887
|
-
*
|
|
888
|
-
*
|
|
889
|
-
* not a property of the id. The sync authorizer leans on this derivation, so it must be injective.
|
|
890
|
-
*
|
|
891
|
-
* `emoji` here is the reaction's token — the emoji glyph for the default palette, or whatever
|
|
892
|
-
* opaque string a consumer's palette uses. All three parts are URI-encoded before joining, so a
|
|
893
|
-
* `:` in any of them can't shift the boundary and collapse two different triples onto one id. The
|
|
894
|
-
* id is only ever recomputed for comparison, never parsed back, so the encoding needs no inverse.
|
|
885
|
+
* rather than random, so the same triple always addresses the same record and two tabs converge
|
|
886
|
+
* instead of racing to create duplicates. The sync authorizer leans on this, so it must be
|
|
887
|
+
* injective: all three parts are URI-encoded before joining, so a `:` in any of them can't shift
|
|
888
|
+
* the boundary and collapse two triples onto one id.
|
|
895
889
|
*
|
|
896
890
|
* @public
|
|
897
891
|
*/
|
|
@@ -2330,25 +2324,22 @@ export declare function isBinding(record?: UnknownRecord): record is TLBinding;
|
|
|
2330
2324
|
export declare function isBindingId(id?: string): id is TLBindingId;
|
|
2331
2325
|
|
|
2332
2326
|
/**
|
|
2333
|
-
* Type guard for `TLCommentId`. See `isCommentThreadId
|
|
2334
|
-
* correctly rejected here.
|
|
2327
|
+
* Type guard for `TLCommentId`. See `isCommentThreadId`.
|
|
2335
2328
|
*
|
|
2336
2329
|
* @public
|
|
2337
2330
|
*/
|
|
2338
2331
|
export declare function isCommentId(id: string): id is TLCommentId;
|
|
2339
2332
|
|
|
2340
2333
|
/**
|
|
2341
|
-
* Type guard for `TLCommentReactionId`.
|
|
2342
|
-
* prefix never overlaps `comment:` — the character after `comment` differs (`-` vs `:`).
|
|
2334
|
+
* Type guard for `TLCommentReactionId`. See `isCommentThreadId`.
|
|
2343
2335
|
*
|
|
2344
2336
|
* @public
|
|
2345
2337
|
*/
|
|
2346
2338
|
export declare function isCommentReactionId(id: string): id is TLCommentReactionId;
|
|
2347
2339
|
|
|
2348
2340
|
/**
|
|
2349
|
-
* Type guard for `TLCommentThreadId`.
|
|
2350
|
-
*
|
|
2351
|
-
* so the two never overlap.
|
|
2341
|
+
* Type guard for `TLCommentThreadId`. `isCommentId` rejects these — `comment-thread:` and
|
|
2342
|
+
* `comment:` differ in the character after `comment`, so the prefixes never overlap.
|
|
2352
2343
|
*
|
|
2353
2344
|
* @public
|
|
2354
2345
|
*/
|
|
@@ -4155,12 +4146,7 @@ export declare interface TLComment extends BaseRecord<'comment', TLCommentId> {
|
|
|
4155
4146
|
editedAt: null | number;
|
|
4156
4147
|
/** Rich text body. Use `toRichText(...)` for plaintext input. */
|
|
4157
4148
|
body: TLRichText;
|
|
4158
|
-
/**
|
|
4159
|
-
* Whether the comment is soft-deleted. Same model as `TLCommentThread.isDeleted` — clients
|
|
4160
|
-
* delete a comment by setting this flag and leaving the record in place, hidden from
|
|
4161
|
-
* rendering. Sync servers are expected to enforce the flag as write-once and author-only,
|
|
4162
|
-
* reject hard deletes from clients, and drop flagged comments from future room loads.
|
|
4163
|
-
*/
|
|
4149
|
+
/** Whether the comment is soft-deleted. Same model as `TLCommentThread.isDeleted`. */
|
|
4164
4150
|
isDeleted: boolean;
|
|
4165
4151
|
meta: JsonObject;
|
|
4166
4152
|
}
|
|
@@ -4169,11 +4155,10 @@ export declare interface TLComment extends BaseRecord<'comment', TLCommentId> {
|
|
|
4169
4155
|
* Where a comment thread is anchored on the canvas. Modeled as a discriminated union so new
|
|
4170
4156
|
* anchor kinds can be added without breaking existing threads:
|
|
4171
4157
|
*
|
|
4172
|
-
* - `shape` — pinned to a shape. `x`/`y` are normalized (0–1) within the shape's
|
|
4173
|
-
*
|
|
4174
|
-
*
|
|
4175
|
-
*
|
|
4176
|
-
* spot (top-right out of the box), and `x`/`y` are the remembered precise position
|
|
4158
|
+
* - `shape` — pinned to a shape. `x`/`y` are normalized (0–1) within the shape's bounds, so the pin
|
|
4159
|
+
* keeps its spot as the shape moves, resizes, and rotates. `isPrecise` mirrors arrow bindings:
|
|
4160
|
+
* when false (the default) the pin sits at a consumer-defined spot instead, top-right out of the
|
|
4161
|
+
* box, and `x`/`y` are the remembered precise position
|
|
4177
4162
|
* - `point` — pinned to a fixed point on the page, in page coordinates
|
|
4178
4163
|
* - `region` — pinned to a rectangular area of the page, in page coordinates
|
|
4179
4164
|
* - `page` — a page-level thread with no spatial anchor
|
|
@@ -4210,16 +4195,11 @@ export declare type TLCommentId = RecordId<TLComment>;
|
|
|
4210
4195
|
/**
|
|
4211
4196
|
* One person's emoji reaction to one comment.
|
|
4212
4197
|
*
|
|
4213
|
-
* A reaction is its own record rather than a field on the comment
|
|
4214
|
-
*
|
|
4215
|
-
* are owner-only for updates, so a reaction could never be written onto them; and holding
|
|
4216
|
-
* everyone's reactions in one shared field would mean each write carried the whole set, so two
|
|
4217
|
-
* people reacting at once would race to overwrite each other. A record per person keeps every
|
|
4218
|
-
* write to its own record, which is what lets concurrent reactions coexist.
|
|
4198
|
+
* A reaction is its own record rather than a field on the comment: comment records are owner-only
|
|
4199
|
+
* for updates, and a shared field would make concurrent reactions race to overwrite each other.
|
|
4219
4200
|
*
|
|
4220
|
-
* A user holds at most one record per (comment, emoji) pair
|
|
4221
|
-
*
|
|
4222
|
-
* Restricting a user to a single reaction overall is a client-side policy layered on top (see the
|
|
4201
|
+
* A user holds at most one record per (comment, emoji) pair, enforced by the derived id — see
|
|
4202
|
+
* `createCommentReactionId`. Limiting a user to one reaction overall is client-side policy (the
|
|
4223
4203
|
* commenting package's `reactionMode`), not a property of this record.
|
|
4224
4204
|
*
|
|
4225
4205
|
* @public
|
|
@@ -4263,11 +4243,7 @@ export declare interface TLCommentThread extends BaseRecord<'comment-thread', TL
|
|
|
4263
4243
|
pageId: TLPageId;
|
|
4264
4244
|
/** Where the thread is anchored on that page. */
|
|
4265
4245
|
anchor: TLCommentAnchor;
|
|
4266
|
-
/**
|
|
4267
|
-
* Who started the thread. Client-supplied for now; the sync server should stamp/verify this
|
|
4268
|
-
* from the session's authenticated identity once record-level authorization lands (the same
|
|
4269
|
-
* model presence already uses for self-ownership).
|
|
4270
|
-
*/
|
|
4246
|
+
/** Who started the thread. Client-supplied; sync servers are expected to stamp/verify it. */
|
|
4271
4247
|
createdBy: string;
|
|
4272
4248
|
createdAt: number;
|
|
4273
4249
|
/** Resolution state: when and by whom the thread was resolved, or null while open. */
|
|
@@ -4276,10 +4252,9 @@ export declare interface TLCommentThread extends BaseRecord<'comment-thread', TL
|
|
|
4276
4252
|
by: string;
|
|
4277
4253
|
} | null;
|
|
4278
4254
|
/**
|
|
4279
|
-
* Whether the thread is soft-deleted. Clients
|
|
4280
|
-
*
|
|
4281
|
-
*
|
|
4282
|
-
* clients, and drop flagged threads from future room loads.
|
|
4255
|
+
* Whether the thread is soft-deleted. Clients set the flag and leave the record in place. Sync
|
|
4256
|
+
* servers are expected to enforce it as write-once and creator-only, reject client hard
|
|
4257
|
+
* deletes, and drop flagged threads from future room loads.
|
|
4283
4258
|
*/
|
|
4284
4259
|
isDeleted: boolean;
|
|
4285
4260
|
meta: JsonObject;
|
package/dist-cjs/index.js
CHANGED
|
@@ -221,7 +221,7 @@ var import_translations = require("./translations/translations");
|
|
|
221
221
|
var import_b64Vecs = require("./misc/b64Vecs");
|
|
222
222
|
(0, import_utils.registerTldrawLibraryVersion)(
|
|
223
223
|
"@tldraw/tlschema",
|
|
224
|
-
"5.3.0-next.
|
|
224
|
+
"5.3.0-next.dec36c5c2930",
|
|
225
225
|
"cjs"
|
|
226
226
|
);
|
|
227
227
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/records/TLComment.ts"],
|
|
4
|
-
"sourcesContent": ["import { BaseRecord, RecordId, createRecordMigrationSequence } from '@tldraw/store'\nimport { JsonObject } from '@tldraw/utils'\nimport { T } from '@tldraw/validate'\nimport { idValidator } from '../misc/id-validator'\nimport { richTextValidator, TLRichText } from '../misc/TLRichText'\nimport { CustomRecordInfo, createCustomRecordId, isCustomRecordId } from './TLCustomRecord'\nimport { TLPageId } from './TLPage'\nimport { TLShapeId } from './TLShape'\n\n/**\n * Where a comment thread is anchored on the canvas. Modeled as a discriminated union so new\n * anchor kinds can be added without breaking existing threads:\n *\n * - `shape` \u2014 pinned to a shape. `x`/`y` are normalized (0\u20131) within the shape's own bounds and\n * resolved through the shape's page transform, so the pin keeps its spot as the shape moves,\n * resizes, and rotates. `isPrecise` mirrors arrow bindings: when\n * true the pin sits at exactly `x`/`y`; when false (the default) it sits at a consumer-defined\n * spot (top-right out of the box), and `x`/`y` are the remembered precise position\n * - `point` \u2014 pinned to a fixed point on the page, in page coordinates\n * - `region` \u2014 pinned to a rectangular area of the page, in page coordinates\n * - `page` \u2014 a page-level thread with no spatial anchor\n *\n * @public\n */\nexport type TLCommentAnchor =\n\t| { type: 'shape'; shapeId: TLShapeId; x: number; y: number; isPrecise: boolean }\n\t| { type: 'point'; x: number; y: number }\n\t| {\n\t\t\ttype: 'region'\n\t\t\tx: number\n\t\t\ty: number\n\t\t\tw: number\n\t\t\th: number\n\t\t\t/** The normalized (0\u20131) corner the pin sits on \u2014 where the creating drag was released.\n\t\t\t * Absent on older records; consumers fall back to a corner of their own choosing. */\n\t\t\tpinX?: number\n\t\t\tpinY?: number\n\t }\n\t| { type: 'page' }\n\n/**\n * A comment thread. The thread owns the anchor (where the conversation lives on the canvas) and\n * the resolution state; the messages themselves are `TLComment` records pointing at the thread\n * via `threadId`, ordered by `createdAt`. v1 threads are flat \u2014 no nested replies.\n *\n * Threads and comments are document records, but they're intended to be served through the sync\n * server's object-store lane: gated by the session's `objectAccess` rather than `isReadonly`\n * (so \"can comment but not edit\" is expressible), excluded from document snapshots and `.tldr`\n * exports server-side, and persisted in a separate lane from the main document.\n *\n * Opt-in: register with `createTLSchema({ records: commentSchemaRecords })` on the server and\n * the matching `records` option on the client \u2014 neither type is part of the default schema, and\n * both sides must register them identically.\n *\n * @public\n */\nexport interface TLCommentThread extends BaseRecord<'comment-thread', TLCommentThreadId> {\n\t/** The page the thread lives on. */\n\tpageId: TLPageId\n\t/** Where the thread is anchored on that page. */\n\tanchor: TLCommentAnchor\n\t/**\n\t * Who started the thread. Client-supplied for now; the sync server should stamp/verify this\n\t * from the session's authenticated identity once record-level authorization lands (the same\n\t * model presence already uses for self-ownership).\n\t */\n\tcreatedBy: string\n\tcreatedAt: number\n\t/** Resolution state: when and by whom the thread was resolved, or null while open. */\n\tresolved: { at: number; by: string } | null\n\t/**\n\t * Whether the thread is soft-deleted. Clients delete a thread by setting this flag and\n\t * leaving the record (and its comments) in place, hidden from rendering. Sync servers are\n\t * expected to enforce the flag as write-once and creator-only, reject hard deletes from\n\t * clients, and drop flagged threads from future room loads.\n\t */\n\tisDeleted: boolean\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentThreadId = RecordId<TLCommentThread>\n\n/**\n * A single comment message within a thread. See `TLCommentThread` for the overall model and\n * sync/registration notes.\n *\n * Comment mutations are deliberately not undoable \u2014 create/edit them with\n * `{ history: 'ignore' }` to avoid multiplayer surprises like a comment reappearing after\n * someone else deleted it.\n *\n * @public\n */\nexport interface TLComment extends BaseRecord<'comment', TLCommentId> {\n\t/** The thread this comment belongs to. */\n\tthreadId: TLCommentThreadId\n\t/** Denormalized from the thread so per-page queries don't need a join. */\n\tpageId: TLPageId\n\t/** See `TLCommentThread.createdBy` \u2014 same server-stamping caveat applies. */\n\tauthorId: string\n\tcreatedAt: number\n\t/** Null until the comment is first edited. */\n\teditedAt: number | null\n\t/** Rich text body. Use `toRichText(...)` for plaintext input. */\n\tbody: TLRichText\n\t/**\n\t * Whether the comment is soft-deleted. Same model as `TLCommentThread.isDeleted` \u2014 clients\n\t * delete a comment by setting this flag and leaving the record in place, hidden from\n\t * rendering. Sync servers are expected to enforce the flag as write-once and author-only,\n\t * reject hard deletes from clients, and drop flagged comments from future room loads.\n\t */\n\tisDeleted: boolean\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentId = RecordId<TLComment>\n\n/**\n * One person's emoji reaction to one comment.\n *\n * A reaction is its own record rather than a field on the comment, for the same reason read\n * receipts are their own row: it's one person's data about someone else's comment. Comment records\n * are owner-only for updates, so a reaction could never be written onto them; and holding\n * everyone's reactions in one shared field would mean each write carried the whole set, so two\n * people reacting at once would race to overwrite each other. A record per person keeps every\n * write to its own record, which is what lets concurrent reactions coexist.\n *\n * A user holds at most one record per (comment, emoji) pair \u2014 so one user can react with several\n * different emoji to the same comment. That's enforced by the id: see `createCommentReactionId`.\n * Restricting a user to a single reaction overall is a client-side policy layered on top (see the\n * commenting package's `reactionMode`), not a property of this record.\n *\n * @public\n */\nexport interface TLCommentReaction extends BaseRecord<'comment-reaction', TLCommentReactionId> {\n\t/** The comment being reacted to. */\n\tcommentId: TLCommentId\n\t/** Denormalized from the comment, so a thread's reactions can be found without a join. */\n\tthreadId: TLCommentThreadId\n\t/** Denormalized from the comment \u2014 see `TLComment.pageId`. */\n\tpageId: TLPageId\n\t/** Who reacted. See `TLCommentThread.createdBy` \u2014 same server-stamping caveat applies. */\n\tuserId: string\n\t/** The emoji itself, as a string (e.g. `'\uD83D\uDC4D'`), not a shortcode. */\n\temoji: string\n\tcreatedAt: number\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentReactionId = RecordId<TLCommentReaction>\n\n/**\n * An emoji shortcode or literal. Bounded because reaction ids embed the emoji verbatim\n * (see {@link createCommentReactionId}), so an unbounded value means an unbounded record id.\n */\nconst emojiValidator = T.string.check((value) => {\n\tif (value.length === 0 || value.length > 64) {\n\t\tthrow new T.ValidationError(`Expected an emoji of 1-64 characters, got ${value.length}`)\n\t}\n})\n\nconst commentAnchorValidator: T.Validator<TLCommentAnchor> = T.union('type', {\n\tshape: T.object({\n\t\ttype: T.literal('shape'),\n\t\tshapeId: idValidator<TLShapeId>('shape'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t\tisPrecise: T.boolean,\n\t}),\n\tpoint: T.object({\n\t\ttype: T.literal('point'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t}),\n\tregion: T.object({\n\t\ttype: T.literal('region'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t\tw: T.number,\n\t\th: T.number,\n\t\tpinX: T.number.optional(),\n\t\tpinY: T.number.optional(),\n\t}),\n\tpage: T.object({\n\t\ttype: T.literal('page'),\n\t}),\n})\n\n/**\n * Guard migrations for the comment record types. Each sequence is retroactive and starts with an\n * identity migration that has no `down`: a sync server whose schema registers the comment types\n * cannot down-migrate records for a session whose schema predates them, so such sessions are\n * rejected with CLIENT_TOO_OLD (prompting a refresh) instead of being sent record types their\n * store cannot represent.\n */\nfunction createCommentGuardMigrations(\n\ttypeName: 'comment' | 'comment-thread' | 'comment-reaction',\n\textra: Parameters<typeof createRecordMigrationSequence>[0]['sequence'] = []\n) {\n\treturn createRecordMigrationSequence({\n\t\tsequenceId: `com.tldraw.${typeName}`,\n\t\trecordType: typeName,\n\t\tretroactive: true,\n\t\tsequence: [\n\t\t\t{\n\t\t\t\tid: `com.tldraw.${typeName}/1`,\n\t\t\t\tup: (record) => record,\n\t\t\t},\n\t\t\t...extra,\n\t\t],\n\t})\n}\n\n/**\n * Config for registering the `comment-thread` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLCommentThread`.\n *\n * @public\n */\nexport const commentThreadRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment-thread', [\n\t\t{\n\t\t\t// Shape anchors gained normalized x/y + isPrecise; existing ones were imprecise (top-right).\n\t\t\tid: 'com.tldraw.comment-thread/2',\n\t\t\tup: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'shape' && anchor.x === undefined) {\n\t\t\t\t\t;(record as any).anchor = { ...anchor, x: 1, y: 0, isPrecise: false }\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t\tdown: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'shape') {\n\t\t\t\t\t;(record as any).anchor = { type: 'shape', shapeId: anchor.shapeId }\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// Region anchors gained an optional pin corner (where the creating drag released).\n\t\t\tid: 'com.tldraw.comment-thread/3',\n\t\t\tup: (record) => record,\n\t\t\tdown: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'region') {\n\t\t\t\t\tconst { pinX: _pinX, pinY: _pinY, ...rest } = anchor\n\t\t\t\t\t;(record as any).anchor = rest\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t},\n\t]),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentThreadId>('comment-thread'),\n\t\ttypeName: T.literal('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tanchor: commentAnchorValidator,\n\t\tcreatedBy: T.string,\n\t\tcreatedAt: T.number,\n\t\tresolved: T.object({ at: T.number, by: T.string }).nullable(),\n\t\tisDeleted: T.boolean,\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * Config for registering the `comment` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLComment`.\n *\n * @public\n */\nexport const commentRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment'),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentId>('comment'),\n\t\ttypeName: T.literal('comment'),\n\t\tthreadId: idValidator<TLCommentThreadId>('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tauthorId: T.string,\n\t\tcreatedAt: T.number,\n\t\teditedAt: T.number.nullable(),\n\t\tbody: richTextValidator,\n\t\tisDeleted: T.boolean,\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * Config for registering the `comment-reaction` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLCommentReaction`.\n *\n * @public\n */\nexport const commentReactionRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment-reaction'),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentReactionId>('comment-reaction'),\n\t\ttypeName: T.literal('comment-reaction'),\n\t\tcommentId: idValidator<TLCommentId>('comment'),\n\t\tthreadId: idValidator<TLCommentThreadId>('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tuserId: T.string,\n\t\temoji: emojiValidator,\n\t\tcreatedAt: T.number,\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * The `records` map to pass to `createTLSchema` / the client `records` option so comment\n * threads, comments, and reactions sync. Register the types together \u2014 one without the others\n * will fail schema validation on one side of the connection.\n *\n * @public\n */\nexport const commentSchemaRecords = {\n\t'comment-thread': commentThreadRecordConfig,\n\tcomment: commentRecordConfig,\n\t'comment-reaction': commentReactionRecordConfig,\n}\n\n/** @public */\nexport function createCommentThreadId(id?: string): TLCommentThreadId {\n\treturn createCustomRecordId('comment-thread', id) as TLCommentThreadId\n}\n\n/** @public */\nexport function createCommentId(id?: string): TLCommentId {\n\treturn createCustomRecordId('comment', id) as TLCommentId\n}\n\n/**\n * The id of one user's reaction to one comment, derived from the (comment, user, emoji) triple\n * rather than random. This is what makes reaction identity structural: the same triple always\n * addresses the same record, so re-picking an emoji toggles that one record and two tabs converge\n * instead of racing to create duplicates. A user can hold one record per emoji on a comment\n * (multiple reactions); enforcing \"at most one reaction total\" is a client concern layered on top,\n * not a property of the id. The sync authorizer leans on this derivation, so it must be injective.\n *\n * `emoji` here is the reaction's token \u2014 the emoji glyph for the default palette, or whatever\n * opaque string a consumer's palette uses. All three parts are URI-encoded before joining, so a\n * `:` in any of them can't shift the boundary and collapse two different triples onto one id. The\n * id is only ever recomputed for comparison, never parsed back, so the encoding needs no inverse.\n *\n * @public\n */\nexport function createCommentReactionId(\n\tcommentId: TLCommentId,\n\tuserId: string,\n\temoji: string\n): TLCommentReactionId {\n\treturn createCustomRecordId(\n\t\t'comment-reaction',\n\t\t`${encodeURIComponent(commentId)}:${encodeURIComponent(userId)}:${encodeURIComponent(emoji)}`\n\t) as TLCommentReactionId\n}\n\n/**\n * Type guard for `TLCommentThreadId`. Note `isCommentId` does not accept these: the prefixes\n * are `comment-thread:` vs `comment:`, and the character after `comment` differs (`-` vs `:`),\n * so the two never overlap.\n *\n * @public\n */\nexport function isCommentThreadId(id: string): id is TLCommentThreadId {\n\treturn isCustomRecordId('comment-thread', id)\n}\n\n/**\n * Type guard for `TLCommentId`. See `isCommentThreadId` for why `comment-thread:...` ids are\n * correctly rejected here.\n *\n * @public\n */\nexport function isCommentId(id: string): id is TLCommentId {\n\treturn isCustomRecordId('comment', id)\n}\n\n/**\n * Type guard for `TLCommentReactionId`. As with `isCommentThreadId`, the `comment-reaction:`\n * prefix never overlaps `comment:` \u2014 the character after `comment` differs (`-` vs `:`).\n *\n * @public\n */\nexport function isCommentReactionId(id: string): id is TLCommentReactionId {\n\treturn isCustomRecordId('comment-reaction', id)\n}\n\n/**\n * Create a new comment thread record. Pair with `createComment` for the thread's first message.\n *\n * @public\n */\nexport function createCommentThread(props: {\n\tpageId: TLPageId\n\tanchor: TLCommentAnchor\n\tcreatedBy: string\n\tnow?: number\n\tmeta?: JsonObject\n}): TLCommentThread {\n\treturn {\n\t\tid: createCommentThreadId(),\n\t\ttypeName: 'comment-thread',\n\t\tpageId: props.pageId,\n\t\tanchor: props.anchor,\n\t\tcreatedBy: props.createdBy,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\tresolved: null,\n\t\tisDeleted: false,\n\t\tmeta: props.meta ?? {},\n\t}\n}\n\n/**\n * Create a new comment record within a thread.\n *\n * @public\n */\nexport function createComment(props: {\n\tthreadId: TLCommentThreadId\n\tpageId: TLPageId\n\tauthorId: string\n\tbody: TLRichText\n\tnow?: number\n\tmeta?: JsonObject\n}): TLComment {\n\treturn {\n\t\tid: createCommentId(),\n\t\ttypeName: 'comment',\n\t\tthreadId: props.threadId,\n\t\tpageId: props.pageId,\n\t\tauthorId: props.authorId,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\teditedAt: null,\n\t\tbody: props.body,\n\t\tisDeleted: false,\n\t\tmeta: props.meta ?? {},\n\t}\n}\n\n/**\n * Create a reaction record for one user's reaction to one comment. The id is derived from the\n * comment and user (see `createCommentReactionId`), so re-reacting overwrites rather than adding.\n *\n * @public\n */\nexport function createCommentReaction(props: {\n\tcommentId: TLCommentId\n\tthreadId: TLCommentThreadId\n\tpageId: TLPageId\n\tuserId: string\n\temoji: string\n\tnow?: number\n\tmeta?: JsonObject\n}): TLCommentReaction {\n\treturn {\n\t\tid: createCommentReactionId(props.commentId, props.userId, props.emoji),\n\t\ttypeName: 'comment-reaction',\n\t\tcommentId: props.commentId,\n\t\tthreadId: props.threadId,\n\t\tpageId: props.pageId,\n\t\tuserId: props.userId,\n\t\temoji: props.emoji,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\tmeta: props.meta ?? {},\n\t}\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAoE;AAEpE,sBAAkB;AAClB,0BAA4B;AAC5B,wBAA8C;AAC9C,4BAAyE;
|
|
4
|
+
"sourcesContent": ["import { BaseRecord, RecordId, createRecordMigrationSequence } from '@tldraw/store'\nimport { JsonObject } from '@tldraw/utils'\nimport { T } from '@tldraw/validate'\nimport { idValidator } from '../misc/id-validator'\nimport { richTextValidator, TLRichText } from '../misc/TLRichText'\nimport { CustomRecordInfo, createCustomRecordId, isCustomRecordId } from './TLCustomRecord'\nimport { TLPageId } from './TLPage'\nimport { TLShapeId } from './TLShape'\n\n/**\n * Where a comment thread is anchored on the canvas. Modeled as a discriminated union so new\n * anchor kinds can be added without breaking existing threads:\n *\n * - `shape` \u2014 pinned to a shape. `x`/`y` are normalized (0\u20131) within the shape's bounds, so the pin\n * keeps its spot as the shape moves, resizes, and rotates. `isPrecise` mirrors arrow bindings:\n * when false (the default) the pin sits at a consumer-defined spot instead, top-right out of the\n * box, and `x`/`y` are the remembered precise position\n * - `point` \u2014 pinned to a fixed point on the page, in page coordinates\n * - `region` \u2014 pinned to a rectangular area of the page, in page coordinates\n * - `page` \u2014 a page-level thread with no spatial anchor\n *\n * @public\n */\nexport type TLCommentAnchor =\n\t| { type: 'shape'; shapeId: TLShapeId; x: number; y: number; isPrecise: boolean }\n\t| { type: 'point'; x: number; y: number }\n\t| {\n\t\t\ttype: 'region'\n\t\t\tx: number\n\t\t\ty: number\n\t\t\tw: number\n\t\t\th: number\n\t\t\t/** The normalized (0\u20131) corner the pin sits on \u2014 where the creating drag was released.\n\t\t\t * Absent on older records; consumers fall back to a corner of their own choosing. */\n\t\t\tpinX?: number\n\t\t\tpinY?: number\n\t }\n\t| { type: 'page' }\n\n/**\n * A comment thread. The thread owns the anchor (where the conversation lives on the canvas) and\n * the resolution state; the messages themselves are `TLComment` records pointing at the thread\n * via `threadId`, ordered by `createdAt`. v1 threads are flat \u2014 no nested replies.\n *\n * Threads and comments are document records, but they're intended to be served through the sync\n * server's object-store lane: gated by the session's `objectAccess` rather than `isReadonly`\n * (so \"can comment but not edit\" is expressible), excluded from document snapshots and `.tldr`\n * exports server-side, and persisted in a separate lane from the main document.\n *\n * Opt-in: register with `createTLSchema({ records: commentSchemaRecords })` on the server and\n * the matching `records` option on the client \u2014 neither type is part of the default schema, and\n * both sides must register them identically.\n *\n * @public\n */\nexport interface TLCommentThread extends BaseRecord<'comment-thread', TLCommentThreadId> {\n\t/** The page the thread lives on. */\n\tpageId: TLPageId\n\t/** Where the thread is anchored on that page. */\n\tanchor: TLCommentAnchor\n\t/** Who started the thread. Client-supplied; sync servers are expected to stamp/verify it. */\n\tcreatedBy: string\n\tcreatedAt: number\n\t/** Resolution state: when and by whom the thread was resolved, or null while open. */\n\tresolved: { at: number; by: string } | null\n\t/**\n\t * Whether the thread is soft-deleted. Clients set the flag and leave the record in place. Sync\n\t * servers are expected to enforce it as write-once and creator-only, reject client hard\n\t * deletes, and drop flagged threads from future room loads.\n\t */\n\tisDeleted: boolean\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentThreadId = RecordId<TLCommentThread>\n\n/**\n * A single comment message within a thread. See `TLCommentThread` for the overall model and\n * sync/registration notes.\n *\n * Comment mutations are deliberately not undoable \u2014 create/edit them with\n * `{ history: 'ignore' }` to avoid multiplayer surprises like a comment reappearing after\n * someone else deleted it.\n *\n * @public\n */\nexport interface TLComment extends BaseRecord<'comment', TLCommentId> {\n\t/** The thread this comment belongs to. */\n\tthreadId: TLCommentThreadId\n\t/** Denormalized from the thread so per-page queries don't need a join. */\n\tpageId: TLPageId\n\t/** See `TLCommentThread.createdBy` \u2014 same server-stamping caveat applies. */\n\tauthorId: string\n\tcreatedAt: number\n\t/** Null until the comment is first edited. */\n\teditedAt: number | null\n\t/** Rich text body. Use `toRichText(...)` for plaintext input. */\n\tbody: TLRichText\n\t/** Whether the comment is soft-deleted. Same model as `TLCommentThread.isDeleted`. */\n\tisDeleted: boolean\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentId = RecordId<TLComment>\n\n/**\n * One person's emoji reaction to one comment.\n *\n * A reaction is its own record rather than a field on the comment: comment records are owner-only\n * for updates, and a shared field would make concurrent reactions race to overwrite each other.\n *\n * A user holds at most one record per (comment, emoji) pair, enforced by the derived id \u2014 see\n * `createCommentReactionId`. Limiting a user to one reaction overall is client-side policy (the\n * commenting package's `reactionMode`), not a property of this record.\n *\n * @public\n */\nexport interface TLCommentReaction extends BaseRecord<'comment-reaction', TLCommentReactionId> {\n\t/** The comment being reacted to. */\n\tcommentId: TLCommentId\n\t/** Denormalized from the comment, so a thread's reactions can be found without a join. */\n\tthreadId: TLCommentThreadId\n\t/** Denormalized from the comment \u2014 see `TLComment.pageId`. */\n\tpageId: TLPageId\n\t/** Who reacted. See `TLCommentThread.createdBy` \u2014 same server-stamping caveat applies. */\n\tuserId: string\n\t/** The emoji itself, as a string (e.g. `'\uD83D\uDC4D'`), not a shortcode. */\n\temoji: string\n\tcreatedAt: number\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentReactionId = RecordId<TLCommentReaction>\n\n/**\n * An emoji shortcode or literal. Bounded because reaction ids embed the emoji verbatim\n * (see {@link createCommentReactionId}), so an unbounded value means an unbounded record id.\n */\nconst emojiValidator = T.string.check((value) => {\n\tif (value.length === 0 || value.length > 64) {\n\t\tthrow new T.ValidationError(`Expected an emoji of 1-64 characters, got ${value.length}`)\n\t}\n})\n\nconst commentAnchorValidator: T.Validator<TLCommentAnchor> = T.union('type', {\n\tshape: T.object({\n\t\ttype: T.literal('shape'),\n\t\tshapeId: idValidator<TLShapeId>('shape'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t\tisPrecise: T.boolean,\n\t}),\n\tpoint: T.object({\n\t\ttype: T.literal('point'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t}),\n\tregion: T.object({\n\t\ttype: T.literal('region'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t\tw: T.number,\n\t\th: T.number,\n\t\tpinX: T.number.optional(),\n\t\tpinY: T.number.optional(),\n\t}),\n\tpage: T.object({\n\t\ttype: T.literal('page'),\n\t}),\n})\n\n/**\n * Guard migrations for the comment record types. Each sequence starts with an identity migration\n * that has no `down`, so a session whose schema predates the comment types is rejected with\n * CLIENT_TOO_OLD instead of being sent records its store can't represent.\n */\nfunction createCommentGuardMigrations(\n\ttypeName: 'comment' | 'comment-thread' | 'comment-reaction',\n\textra: Parameters<typeof createRecordMigrationSequence>[0]['sequence'] = []\n) {\n\treturn createRecordMigrationSequence({\n\t\tsequenceId: `com.tldraw.${typeName}`,\n\t\trecordType: typeName,\n\t\tretroactive: true,\n\t\tsequence: [\n\t\t\t{\n\t\t\t\tid: `com.tldraw.${typeName}/1`,\n\t\t\t\tup: (record) => record,\n\t\t\t},\n\t\t\t...extra,\n\t\t],\n\t})\n}\n\n/**\n * Config for registering the `comment-thread` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLCommentThread`.\n *\n * @public\n */\nexport const commentThreadRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment-thread', [\n\t\t{\n\t\t\t// Shape anchors gained normalized x/y + isPrecise; existing ones were imprecise (top-right).\n\t\t\tid: 'com.tldraw.comment-thread/2',\n\t\t\tup: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'shape' && anchor.x === undefined) {\n\t\t\t\t\t;(record as any).anchor = { ...anchor, x: 1, y: 0, isPrecise: false }\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t\tdown: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'shape') {\n\t\t\t\t\t;(record as any).anchor = { type: 'shape', shapeId: anchor.shapeId }\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// Region anchors gained an optional pin corner (where the creating drag released).\n\t\t\tid: 'com.tldraw.comment-thread/3',\n\t\t\tup: (record) => record,\n\t\t\tdown: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'region') {\n\t\t\t\t\tconst { pinX: _pinX, pinY: _pinY, ...rest } = anchor\n\t\t\t\t\t;(record as any).anchor = rest\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t},\n\t]),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentThreadId>('comment-thread'),\n\t\ttypeName: T.literal('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tanchor: commentAnchorValidator,\n\t\tcreatedBy: T.string,\n\t\tcreatedAt: T.number,\n\t\tresolved: T.object({ at: T.number, by: T.string }).nullable(),\n\t\tisDeleted: T.boolean,\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * Config for registering the `comment` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLComment`.\n *\n * @public\n */\nexport const commentRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment'),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentId>('comment'),\n\t\ttypeName: T.literal('comment'),\n\t\tthreadId: idValidator<TLCommentThreadId>('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tauthorId: T.string,\n\t\tcreatedAt: T.number,\n\t\teditedAt: T.number.nullable(),\n\t\tbody: richTextValidator,\n\t\tisDeleted: T.boolean,\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * Config for registering the `comment-reaction` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLCommentReaction`.\n *\n * @public\n */\nexport const commentReactionRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment-reaction'),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentReactionId>('comment-reaction'),\n\t\ttypeName: T.literal('comment-reaction'),\n\t\tcommentId: idValidator<TLCommentId>('comment'),\n\t\tthreadId: idValidator<TLCommentThreadId>('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tuserId: T.string,\n\t\temoji: emojiValidator,\n\t\tcreatedAt: T.number,\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * The `records` map to pass to `createTLSchema` / the client `records` option so comment\n * threads, comments, and reactions sync. Register the types together \u2014 one without the others\n * will fail schema validation on one side of the connection.\n *\n * @public\n */\nexport const commentSchemaRecords = {\n\t'comment-thread': commentThreadRecordConfig,\n\tcomment: commentRecordConfig,\n\t'comment-reaction': commentReactionRecordConfig,\n}\n\n/** @public */\nexport function createCommentThreadId(id?: string): TLCommentThreadId {\n\treturn createCustomRecordId('comment-thread', id) as TLCommentThreadId\n}\n\n/** @public */\nexport function createCommentId(id?: string): TLCommentId {\n\treturn createCustomRecordId('comment', id) as TLCommentId\n}\n\n/**\n * The id of one user's reaction to one comment, derived from the (comment, user, emoji) triple\n * rather than random, so the same triple always addresses the same record and two tabs converge\n * instead of racing to create duplicates. The sync authorizer leans on this, so it must be\n * injective: all three parts are URI-encoded before joining, so a `:` in any of them can't shift\n * the boundary and collapse two triples onto one id.\n *\n * @public\n */\nexport function createCommentReactionId(\n\tcommentId: TLCommentId,\n\tuserId: string,\n\temoji: string\n): TLCommentReactionId {\n\treturn createCustomRecordId(\n\t\t'comment-reaction',\n\t\t`${encodeURIComponent(commentId)}:${encodeURIComponent(userId)}:${encodeURIComponent(emoji)}`\n\t) as TLCommentReactionId\n}\n\n/**\n * Type guard for `TLCommentThreadId`. `isCommentId` rejects these \u2014 `comment-thread:` and\n * `comment:` differ in the character after `comment`, so the prefixes never overlap.\n *\n * @public\n */\nexport function isCommentThreadId(id: string): id is TLCommentThreadId {\n\treturn isCustomRecordId('comment-thread', id)\n}\n\n/**\n * Type guard for `TLCommentId`. See `isCommentThreadId`.\n *\n * @public\n */\nexport function isCommentId(id: string): id is TLCommentId {\n\treturn isCustomRecordId('comment', id)\n}\n\n/**\n * Type guard for `TLCommentReactionId`. See `isCommentThreadId`.\n *\n * @public\n */\nexport function isCommentReactionId(id: string): id is TLCommentReactionId {\n\treturn isCustomRecordId('comment-reaction', id)\n}\n\n/**\n * Create a new comment thread record. Pair with `createComment` for the thread's first message.\n *\n * @public\n */\nexport function createCommentThread(props: {\n\tpageId: TLPageId\n\tanchor: TLCommentAnchor\n\tcreatedBy: string\n\tnow?: number\n\tmeta?: JsonObject\n}): TLCommentThread {\n\treturn {\n\t\tid: createCommentThreadId(),\n\t\ttypeName: 'comment-thread',\n\t\tpageId: props.pageId,\n\t\tanchor: props.anchor,\n\t\tcreatedBy: props.createdBy,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\tresolved: null,\n\t\tisDeleted: false,\n\t\tmeta: props.meta ?? {},\n\t}\n}\n\n/**\n * Create a new comment record within a thread.\n *\n * @public\n */\nexport function createComment(props: {\n\tthreadId: TLCommentThreadId\n\tpageId: TLPageId\n\tauthorId: string\n\tbody: TLRichText\n\tnow?: number\n\tmeta?: JsonObject\n}): TLComment {\n\treturn {\n\t\tid: createCommentId(),\n\t\ttypeName: 'comment',\n\t\tthreadId: props.threadId,\n\t\tpageId: props.pageId,\n\t\tauthorId: props.authorId,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\teditedAt: null,\n\t\tbody: props.body,\n\t\tisDeleted: false,\n\t\tmeta: props.meta ?? {},\n\t}\n}\n\n/**\n * Create a reaction record for one user's reaction to one comment. The id is derived from the\n * comment and user (see `createCommentReactionId`), so re-reacting overwrites rather than adding.\n *\n * @public\n */\nexport function createCommentReaction(props: {\n\tcommentId: TLCommentId\n\tthreadId: TLCommentThreadId\n\tpageId: TLPageId\n\tuserId: string\n\temoji: string\n\tnow?: number\n\tmeta?: JsonObject\n}): TLCommentReaction {\n\treturn {\n\t\tid: createCommentReactionId(props.commentId, props.userId, props.emoji),\n\t\ttypeName: 'comment-reaction',\n\t\tcommentId: props.commentId,\n\t\tthreadId: props.threadId,\n\t\tpageId: props.pageId,\n\t\tuserId: props.userId,\n\t\temoji: props.emoji,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\tmeta: props.meta ?? {},\n\t}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-esm/index.d.mts
CHANGED
|
@@ -882,16 +882,10 @@ export declare function createCommentReaction(props: {
|
|
|
882
882
|
|
|
883
883
|
/**
|
|
884
884
|
* The id of one user's reaction to one comment, derived from the (comment, user, emoji) triple
|
|
885
|
-
* rather than random
|
|
886
|
-
*
|
|
887
|
-
*
|
|
888
|
-
*
|
|
889
|
-
* not a property of the id. The sync authorizer leans on this derivation, so it must be injective.
|
|
890
|
-
*
|
|
891
|
-
* `emoji` here is the reaction's token — the emoji glyph for the default palette, or whatever
|
|
892
|
-
* opaque string a consumer's palette uses. All three parts are URI-encoded before joining, so a
|
|
893
|
-
* `:` in any of them can't shift the boundary and collapse two different triples onto one id. The
|
|
894
|
-
* id is only ever recomputed for comparison, never parsed back, so the encoding needs no inverse.
|
|
885
|
+
* rather than random, so the same triple always addresses the same record and two tabs converge
|
|
886
|
+
* instead of racing to create duplicates. The sync authorizer leans on this, so it must be
|
|
887
|
+
* injective: all three parts are URI-encoded before joining, so a `:` in any of them can't shift
|
|
888
|
+
* the boundary and collapse two triples onto one id.
|
|
895
889
|
*
|
|
896
890
|
* @public
|
|
897
891
|
*/
|
|
@@ -2330,25 +2324,22 @@ export declare function isBinding(record?: UnknownRecord): record is TLBinding;
|
|
|
2330
2324
|
export declare function isBindingId(id?: string): id is TLBindingId;
|
|
2331
2325
|
|
|
2332
2326
|
/**
|
|
2333
|
-
* Type guard for `TLCommentId`. See `isCommentThreadId
|
|
2334
|
-
* correctly rejected here.
|
|
2327
|
+
* Type guard for `TLCommentId`. See `isCommentThreadId`.
|
|
2335
2328
|
*
|
|
2336
2329
|
* @public
|
|
2337
2330
|
*/
|
|
2338
2331
|
export declare function isCommentId(id: string): id is TLCommentId;
|
|
2339
2332
|
|
|
2340
2333
|
/**
|
|
2341
|
-
* Type guard for `TLCommentReactionId`.
|
|
2342
|
-
* prefix never overlaps `comment:` — the character after `comment` differs (`-` vs `:`).
|
|
2334
|
+
* Type guard for `TLCommentReactionId`. See `isCommentThreadId`.
|
|
2343
2335
|
*
|
|
2344
2336
|
* @public
|
|
2345
2337
|
*/
|
|
2346
2338
|
export declare function isCommentReactionId(id: string): id is TLCommentReactionId;
|
|
2347
2339
|
|
|
2348
2340
|
/**
|
|
2349
|
-
* Type guard for `TLCommentThreadId`.
|
|
2350
|
-
*
|
|
2351
|
-
* so the two never overlap.
|
|
2341
|
+
* Type guard for `TLCommentThreadId`. `isCommentId` rejects these — `comment-thread:` and
|
|
2342
|
+
* `comment:` differ in the character after `comment`, so the prefixes never overlap.
|
|
2352
2343
|
*
|
|
2353
2344
|
* @public
|
|
2354
2345
|
*/
|
|
@@ -4155,12 +4146,7 @@ export declare interface TLComment extends BaseRecord<'comment', TLCommentId> {
|
|
|
4155
4146
|
editedAt: null | number;
|
|
4156
4147
|
/** Rich text body. Use `toRichText(...)` for plaintext input. */
|
|
4157
4148
|
body: TLRichText;
|
|
4158
|
-
/**
|
|
4159
|
-
* Whether the comment is soft-deleted. Same model as `TLCommentThread.isDeleted` — clients
|
|
4160
|
-
* delete a comment by setting this flag and leaving the record in place, hidden from
|
|
4161
|
-
* rendering. Sync servers are expected to enforce the flag as write-once and author-only,
|
|
4162
|
-
* reject hard deletes from clients, and drop flagged comments from future room loads.
|
|
4163
|
-
*/
|
|
4149
|
+
/** Whether the comment is soft-deleted. Same model as `TLCommentThread.isDeleted`. */
|
|
4164
4150
|
isDeleted: boolean;
|
|
4165
4151
|
meta: JsonObject;
|
|
4166
4152
|
}
|
|
@@ -4169,11 +4155,10 @@ export declare interface TLComment extends BaseRecord<'comment', TLCommentId> {
|
|
|
4169
4155
|
* Where a comment thread is anchored on the canvas. Modeled as a discriminated union so new
|
|
4170
4156
|
* anchor kinds can be added without breaking existing threads:
|
|
4171
4157
|
*
|
|
4172
|
-
* - `shape` — pinned to a shape. `x`/`y` are normalized (0–1) within the shape's
|
|
4173
|
-
*
|
|
4174
|
-
*
|
|
4175
|
-
*
|
|
4176
|
-
* spot (top-right out of the box), and `x`/`y` are the remembered precise position
|
|
4158
|
+
* - `shape` — pinned to a shape. `x`/`y` are normalized (0–1) within the shape's bounds, so the pin
|
|
4159
|
+
* keeps its spot as the shape moves, resizes, and rotates. `isPrecise` mirrors arrow bindings:
|
|
4160
|
+
* when false (the default) the pin sits at a consumer-defined spot instead, top-right out of the
|
|
4161
|
+
* box, and `x`/`y` are the remembered precise position
|
|
4177
4162
|
* - `point` — pinned to a fixed point on the page, in page coordinates
|
|
4178
4163
|
* - `region` — pinned to a rectangular area of the page, in page coordinates
|
|
4179
4164
|
* - `page` — a page-level thread with no spatial anchor
|
|
@@ -4210,16 +4195,11 @@ export declare type TLCommentId = RecordId<TLComment>;
|
|
|
4210
4195
|
/**
|
|
4211
4196
|
* One person's emoji reaction to one comment.
|
|
4212
4197
|
*
|
|
4213
|
-
* A reaction is its own record rather than a field on the comment
|
|
4214
|
-
*
|
|
4215
|
-
* are owner-only for updates, so a reaction could never be written onto them; and holding
|
|
4216
|
-
* everyone's reactions in one shared field would mean each write carried the whole set, so two
|
|
4217
|
-
* people reacting at once would race to overwrite each other. A record per person keeps every
|
|
4218
|
-
* write to its own record, which is what lets concurrent reactions coexist.
|
|
4198
|
+
* A reaction is its own record rather than a field on the comment: comment records are owner-only
|
|
4199
|
+
* for updates, and a shared field would make concurrent reactions race to overwrite each other.
|
|
4219
4200
|
*
|
|
4220
|
-
* A user holds at most one record per (comment, emoji) pair
|
|
4221
|
-
*
|
|
4222
|
-
* Restricting a user to a single reaction overall is a client-side policy layered on top (see the
|
|
4201
|
+
* A user holds at most one record per (comment, emoji) pair, enforced by the derived id — see
|
|
4202
|
+
* `createCommentReactionId`. Limiting a user to one reaction overall is client-side policy (the
|
|
4223
4203
|
* commenting package's `reactionMode`), not a property of this record.
|
|
4224
4204
|
*
|
|
4225
4205
|
* @public
|
|
@@ -4263,11 +4243,7 @@ export declare interface TLCommentThread extends BaseRecord<'comment-thread', TL
|
|
|
4263
4243
|
pageId: TLPageId;
|
|
4264
4244
|
/** Where the thread is anchored on that page. */
|
|
4265
4245
|
anchor: TLCommentAnchor;
|
|
4266
|
-
/**
|
|
4267
|
-
* Who started the thread. Client-supplied for now; the sync server should stamp/verify this
|
|
4268
|
-
* from the session's authenticated identity once record-level authorization lands (the same
|
|
4269
|
-
* model presence already uses for self-ownership).
|
|
4270
|
-
*/
|
|
4246
|
+
/** Who started the thread. Client-supplied; sync servers are expected to stamp/verify it. */
|
|
4271
4247
|
createdBy: string;
|
|
4272
4248
|
createdAt: number;
|
|
4273
4249
|
/** Resolution state: when and by whom the thread was resolved, or null while open. */
|
|
@@ -4276,10 +4252,9 @@ export declare interface TLCommentThread extends BaseRecord<'comment-thread', TL
|
|
|
4276
4252
|
by: string;
|
|
4277
4253
|
} | null;
|
|
4278
4254
|
/**
|
|
4279
|
-
* Whether the thread is soft-deleted. Clients
|
|
4280
|
-
*
|
|
4281
|
-
*
|
|
4282
|
-
* clients, and drop flagged threads from future room loads.
|
|
4255
|
+
* Whether the thread is soft-deleted. Clients set the flag and leave the record in place. Sync
|
|
4256
|
+
* servers are expected to enforce it as write-once and creator-only, reject client hard
|
|
4257
|
+
* deletes, and drop flagged threads from future room loads.
|
|
4283
4258
|
*/
|
|
4284
4259
|
isDeleted: boolean;
|
|
4285
4260
|
meta: JsonObject;
|
package/dist-esm/index.mjs
CHANGED
|
@@ -213,7 +213,7 @@ import {
|
|
|
213
213
|
} from "./translations/translations.mjs";
|
|
214
214
|
registerTldrawLibraryVersion(
|
|
215
215
|
"@tldraw/tlschema",
|
|
216
|
-
"5.3.0-next.
|
|
216
|
+
"5.3.0-next.dec36c5c2930",
|
|
217
217
|
"esm"
|
|
218
218
|
);
|
|
219
219
|
import { DIM_2D, DIM_3D, b64Vecs } from "./misc/b64Vecs.mjs";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/records/TLComment.ts"],
|
|
4
|
-
"sourcesContent": ["import { BaseRecord, RecordId, createRecordMigrationSequence } from '@tldraw/store'\nimport { JsonObject } from '@tldraw/utils'\nimport { T } from '@tldraw/validate'\nimport { idValidator } from '../misc/id-validator'\nimport { richTextValidator, TLRichText } from '../misc/TLRichText'\nimport { CustomRecordInfo, createCustomRecordId, isCustomRecordId } from './TLCustomRecord'\nimport { TLPageId } from './TLPage'\nimport { TLShapeId } from './TLShape'\n\n/**\n * Where a comment thread is anchored on the canvas. Modeled as a discriminated union so new\n * anchor kinds can be added without breaking existing threads:\n *\n * - `shape` \u2014 pinned to a shape. `x`/`y` are normalized (0\u20131) within the shape's own bounds and\n * resolved through the shape's page transform, so the pin keeps its spot as the shape moves,\n * resizes, and rotates. `isPrecise` mirrors arrow bindings: when\n * true the pin sits at exactly `x`/`y`; when false (the default) it sits at a consumer-defined\n * spot (top-right out of the box), and `x`/`y` are the remembered precise position\n * - `point` \u2014 pinned to a fixed point on the page, in page coordinates\n * - `region` \u2014 pinned to a rectangular area of the page, in page coordinates\n * - `page` \u2014 a page-level thread with no spatial anchor\n *\n * @public\n */\nexport type TLCommentAnchor =\n\t| { type: 'shape'; shapeId: TLShapeId; x: number; y: number; isPrecise: boolean }\n\t| { type: 'point'; x: number; y: number }\n\t| {\n\t\t\ttype: 'region'\n\t\t\tx: number\n\t\t\ty: number\n\t\t\tw: number\n\t\t\th: number\n\t\t\t/** The normalized (0\u20131) corner the pin sits on \u2014 where the creating drag was released.\n\t\t\t * Absent on older records; consumers fall back to a corner of their own choosing. */\n\t\t\tpinX?: number\n\t\t\tpinY?: number\n\t }\n\t| { type: 'page' }\n\n/**\n * A comment thread. The thread owns the anchor (where the conversation lives on the canvas) and\n * the resolution state; the messages themselves are `TLComment` records pointing at the thread\n * via `threadId`, ordered by `createdAt`. v1 threads are flat \u2014 no nested replies.\n *\n * Threads and comments are document records, but they're intended to be served through the sync\n * server's object-store lane: gated by the session's `objectAccess` rather than `isReadonly`\n * (so \"can comment but not edit\" is expressible), excluded from document snapshots and `.tldr`\n * exports server-side, and persisted in a separate lane from the main document.\n *\n * Opt-in: register with `createTLSchema({ records: commentSchemaRecords })` on the server and\n * the matching `records` option on the client \u2014 neither type is part of the default schema, and\n * both sides must register them identically.\n *\n * @public\n */\nexport interface TLCommentThread extends BaseRecord<'comment-thread', TLCommentThreadId> {\n\t/** The page the thread lives on. */\n\tpageId: TLPageId\n\t/** Where the thread is anchored on that page. */\n\tanchor: TLCommentAnchor\n\t/**\n\t * Who started the thread. Client-supplied for now; the sync server should stamp/verify this\n\t * from the session's authenticated identity once record-level authorization lands (the same\n\t * model presence already uses for self-ownership).\n\t */\n\tcreatedBy: string\n\tcreatedAt: number\n\t/** Resolution state: when and by whom the thread was resolved, or null while open. */\n\tresolved: { at: number; by: string } | null\n\t/**\n\t * Whether the thread is soft-deleted. Clients delete a thread by setting this flag and\n\t * leaving the record (and its comments) in place, hidden from rendering. Sync servers are\n\t * expected to enforce the flag as write-once and creator-only, reject hard deletes from\n\t * clients, and drop flagged threads from future room loads.\n\t */\n\tisDeleted: boolean\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentThreadId = RecordId<TLCommentThread>\n\n/**\n * A single comment message within a thread. See `TLCommentThread` for the overall model and\n * sync/registration notes.\n *\n * Comment mutations are deliberately not undoable \u2014 create/edit them with\n * `{ history: 'ignore' }` to avoid multiplayer surprises like a comment reappearing after\n * someone else deleted it.\n *\n * @public\n */\nexport interface TLComment extends BaseRecord<'comment', TLCommentId> {\n\t/** The thread this comment belongs to. */\n\tthreadId: TLCommentThreadId\n\t/** Denormalized from the thread so per-page queries don't need a join. */\n\tpageId: TLPageId\n\t/** See `TLCommentThread.createdBy` \u2014 same server-stamping caveat applies. */\n\tauthorId: string\n\tcreatedAt: number\n\t/** Null until the comment is first edited. */\n\teditedAt: number | null\n\t/** Rich text body. Use `toRichText(...)` for plaintext input. */\n\tbody: TLRichText\n\t/**\n\t * Whether the comment is soft-deleted. Same model as `TLCommentThread.isDeleted` \u2014 clients\n\t * delete a comment by setting this flag and leaving the record in place, hidden from\n\t * rendering. Sync servers are expected to enforce the flag as write-once and author-only,\n\t * reject hard deletes from clients, and drop flagged comments from future room loads.\n\t */\n\tisDeleted: boolean\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentId = RecordId<TLComment>\n\n/**\n * One person's emoji reaction to one comment.\n *\n * A reaction is its own record rather than a field on the comment, for the same reason read\n * receipts are their own row: it's one person's data about someone else's comment. Comment records\n * are owner-only for updates, so a reaction could never be written onto them; and holding\n * everyone's reactions in one shared field would mean each write carried the whole set, so two\n * people reacting at once would race to overwrite each other. A record per person keeps every\n * write to its own record, which is what lets concurrent reactions coexist.\n *\n * A user holds at most one record per (comment, emoji) pair \u2014 so one user can react with several\n * different emoji to the same comment. That's enforced by the id: see `createCommentReactionId`.\n * Restricting a user to a single reaction overall is a client-side policy layered on top (see the\n * commenting package's `reactionMode`), not a property of this record.\n *\n * @public\n */\nexport interface TLCommentReaction extends BaseRecord<'comment-reaction', TLCommentReactionId> {\n\t/** The comment being reacted to. */\n\tcommentId: TLCommentId\n\t/** Denormalized from the comment, so a thread's reactions can be found without a join. */\n\tthreadId: TLCommentThreadId\n\t/** Denormalized from the comment \u2014 see `TLComment.pageId`. */\n\tpageId: TLPageId\n\t/** Who reacted. See `TLCommentThread.createdBy` \u2014 same server-stamping caveat applies. */\n\tuserId: string\n\t/** The emoji itself, as a string (e.g. `'\uD83D\uDC4D'`), not a shortcode. */\n\temoji: string\n\tcreatedAt: number\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentReactionId = RecordId<TLCommentReaction>\n\n/**\n * An emoji shortcode or literal. Bounded because reaction ids embed the emoji verbatim\n * (see {@link createCommentReactionId}), so an unbounded value means an unbounded record id.\n */\nconst emojiValidator = T.string.check((value) => {\n\tif (value.length === 0 || value.length > 64) {\n\t\tthrow new T.ValidationError(`Expected an emoji of 1-64 characters, got ${value.length}`)\n\t}\n})\n\nconst commentAnchorValidator: T.Validator<TLCommentAnchor> = T.union('type', {\n\tshape: T.object({\n\t\ttype: T.literal('shape'),\n\t\tshapeId: idValidator<TLShapeId>('shape'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t\tisPrecise: T.boolean,\n\t}),\n\tpoint: T.object({\n\t\ttype: T.literal('point'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t}),\n\tregion: T.object({\n\t\ttype: T.literal('region'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t\tw: T.number,\n\t\th: T.number,\n\t\tpinX: T.number.optional(),\n\t\tpinY: T.number.optional(),\n\t}),\n\tpage: T.object({\n\t\ttype: T.literal('page'),\n\t}),\n})\n\n/**\n * Guard migrations for the comment record types. Each sequence is retroactive and starts with an\n * identity migration that has no `down`: a sync server whose schema registers the comment types\n * cannot down-migrate records for a session whose schema predates them, so such sessions are\n * rejected with CLIENT_TOO_OLD (prompting a refresh) instead of being sent record types their\n * store cannot represent.\n */\nfunction createCommentGuardMigrations(\n\ttypeName: 'comment' | 'comment-thread' | 'comment-reaction',\n\textra: Parameters<typeof createRecordMigrationSequence>[0]['sequence'] = []\n) {\n\treturn createRecordMigrationSequence({\n\t\tsequenceId: `com.tldraw.${typeName}`,\n\t\trecordType: typeName,\n\t\tretroactive: true,\n\t\tsequence: [\n\t\t\t{\n\t\t\t\tid: `com.tldraw.${typeName}/1`,\n\t\t\t\tup: (record) => record,\n\t\t\t},\n\t\t\t...extra,\n\t\t],\n\t})\n}\n\n/**\n * Config for registering the `comment-thread` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLCommentThread`.\n *\n * @public\n */\nexport const commentThreadRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment-thread', [\n\t\t{\n\t\t\t// Shape anchors gained normalized x/y + isPrecise; existing ones were imprecise (top-right).\n\t\t\tid: 'com.tldraw.comment-thread/2',\n\t\t\tup: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'shape' && anchor.x === undefined) {\n\t\t\t\t\t;(record as any).anchor = { ...anchor, x: 1, y: 0, isPrecise: false }\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t\tdown: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'shape') {\n\t\t\t\t\t;(record as any).anchor = { type: 'shape', shapeId: anchor.shapeId }\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// Region anchors gained an optional pin corner (where the creating drag released).\n\t\t\tid: 'com.tldraw.comment-thread/3',\n\t\t\tup: (record) => record,\n\t\t\tdown: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'region') {\n\t\t\t\t\tconst { pinX: _pinX, pinY: _pinY, ...rest } = anchor\n\t\t\t\t\t;(record as any).anchor = rest\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t},\n\t]),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentThreadId>('comment-thread'),\n\t\ttypeName: T.literal('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tanchor: commentAnchorValidator,\n\t\tcreatedBy: T.string,\n\t\tcreatedAt: T.number,\n\t\tresolved: T.object({ at: T.number, by: T.string }).nullable(),\n\t\tisDeleted: T.boolean,\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * Config for registering the `comment` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLComment`.\n *\n * @public\n */\nexport const commentRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment'),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentId>('comment'),\n\t\ttypeName: T.literal('comment'),\n\t\tthreadId: idValidator<TLCommentThreadId>('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tauthorId: T.string,\n\t\tcreatedAt: T.number,\n\t\teditedAt: T.number.nullable(),\n\t\tbody: richTextValidator,\n\t\tisDeleted: T.boolean,\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * Config for registering the `comment-reaction` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLCommentReaction`.\n *\n * @public\n */\nexport const commentReactionRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment-reaction'),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentReactionId>('comment-reaction'),\n\t\ttypeName: T.literal('comment-reaction'),\n\t\tcommentId: idValidator<TLCommentId>('comment'),\n\t\tthreadId: idValidator<TLCommentThreadId>('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tuserId: T.string,\n\t\temoji: emojiValidator,\n\t\tcreatedAt: T.number,\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * The `records` map to pass to `createTLSchema` / the client `records` option so comment\n * threads, comments, and reactions sync. Register the types together \u2014 one without the others\n * will fail schema validation on one side of the connection.\n *\n * @public\n */\nexport const commentSchemaRecords = {\n\t'comment-thread': commentThreadRecordConfig,\n\tcomment: commentRecordConfig,\n\t'comment-reaction': commentReactionRecordConfig,\n}\n\n/** @public */\nexport function createCommentThreadId(id?: string): TLCommentThreadId {\n\treturn createCustomRecordId('comment-thread', id) as TLCommentThreadId\n}\n\n/** @public */\nexport function createCommentId(id?: string): TLCommentId {\n\treturn createCustomRecordId('comment', id) as TLCommentId\n}\n\n/**\n * The id of one user's reaction to one comment, derived from the (comment, user, emoji) triple\n * rather than random. This is what makes reaction identity structural: the same triple always\n * addresses the same record, so re-picking an emoji toggles that one record and two tabs converge\n * instead of racing to create duplicates. A user can hold one record per emoji on a comment\n * (multiple reactions); enforcing \"at most one reaction total\" is a client concern layered on top,\n * not a property of the id. The sync authorizer leans on this derivation, so it must be injective.\n *\n * `emoji` here is the reaction's token \u2014 the emoji glyph for the default palette, or whatever\n * opaque string a consumer's palette uses. All three parts are URI-encoded before joining, so a\n * `:` in any of them can't shift the boundary and collapse two different triples onto one id. The\n * id is only ever recomputed for comparison, never parsed back, so the encoding needs no inverse.\n *\n * @public\n */\nexport function createCommentReactionId(\n\tcommentId: TLCommentId,\n\tuserId: string,\n\temoji: string\n): TLCommentReactionId {\n\treturn createCustomRecordId(\n\t\t'comment-reaction',\n\t\t`${encodeURIComponent(commentId)}:${encodeURIComponent(userId)}:${encodeURIComponent(emoji)}`\n\t) as TLCommentReactionId\n}\n\n/**\n * Type guard for `TLCommentThreadId`. Note `isCommentId` does not accept these: the prefixes\n * are `comment-thread:` vs `comment:`, and the character after `comment` differs (`-` vs `:`),\n * so the two never overlap.\n *\n * @public\n */\nexport function isCommentThreadId(id: string): id is TLCommentThreadId {\n\treturn isCustomRecordId('comment-thread', id)\n}\n\n/**\n * Type guard for `TLCommentId`. See `isCommentThreadId` for why `comment-thread:...` ids are\n * correctly rejected here.\n *\n * @public\n */\nexport function isCommentId(id: string): id is TLCommentId {\n\treturn isCustomRecordId('comment', id)\n}\n\n/**\n * Type guard for `TLCommentReactionId`. As with `isCommentThreadId`, the `comment-reaction:`\n * prefix never overlaps `comment:` \u2014 the character after `comment` differs (`-` vs `:`).\n *\n * @public\n */\nexport function isCommentReactionId(id: string): id is TLCommentReactionId {\n\treturn isCustomRecordId('comment-reaction', id)\n}\n\n/**\n * Create a new comment thread record. Pair with `createComment` for the thread's first message.\n *\n * @public\n */\nexport function createCommentThread(props: {\n\tpageId: TLPageId\n\tanchor: TLCommentAnchor\n\tcreatedBy: string\n\tnow?: number\n\tmeta?: JsonObject\n}): TLCommentThread {\n\treturn {\n\t\tid: createCommentThreadId(),\n\t\ttypeName: 'comment-thread',\n\t\tpageId: props.pageId,\n\t\tanchor: props.anchor,\n\t\tcreatedBy: props.createdBy,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\tresolved: null,\n\t\tisDeleted: false,\n\t\tmeta: props.meta ?? {},\n\t}\n}\n\n/**\n * Create a new comment record within a thread.\n *\n * @public\n */\nexport function createComment(props: {\n\tthreadId: TLCommentThreadId\n\tpageId: TLPageId\n\tauthorId: string\n\tbody: TLRichText\n\tnow?: number\n\tmeta?: JsonObject\n}): TLComment {\n\treturn {\n\t\tid: createCommentId(),\n\t\ttypeName: 'comment',\n\t\tthreadId: props.threadId,\n\t\tpageId: props.pageId,\n\t\tauthorId: props.authorId,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\teditedAt: null,\n\t\tbody: props.body,\n\t\tisDeleted: false,\n\t\tmeta: props.meta ?? {},\n\t}\n}\n\n/**\n * Create a reaction record for one user's reaction to one comment. The id is derived from the\n * comment and user (see `createCommentReactionId`), so re-reacting overwrites rather than adding.\n *\n * @public\n */\nexport function createCommentReaction(props: {\n\tcommentId: TLCommentId\n\tthreadId: TLCommentThreadId\n\tpageId: TLPageId\n\tuserId: string\n\temoji: string\n\tnow?: number\n\tmeta?: JsonObject\n}): TLCommentReaction {\n\treturn {\n\t\tid: createCommentReactionId(props.commentId, props.userId, props.emoji),\n\t\ttypeName: 'comment-reaction',\n\t\tcommentId: props.commentId,\n\t\tthreadId: props.threadId,\n\t\tpageId: props.pageId,\n\t\tuserId: props.userId,\n\t\temoji: props.emoji,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\tmeta: props.meta ?? {},\n\t}\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAA+B,qCAAqC;AAEpE,SAAS,SAAS;AAClB,SAAS,mBAAmB;AAC5B,SAAS,yBAAqC;AAC9C,SAA2B,sBAAsB,wBAAwB;
|
|
4
|
+
"sourcesContent": ["import { BaseRecord, RecordId, createRecordMigrationSequence } from '@tldraw/store'\nimport { JsonObject } from '@tldraw/utils'\nimport { T } from '@tldraw/validate'\nimport { idValidator } from '../misc/id-validator'\nimport { richTextValidator, TLRichText } from '../misc/TLRichText'\nimport { CustomRecordInfo, createCustomRecordId, isCustomRecordId } from './TLCustomRecord'\nimport { TLPageId } from './TLPage'\nimport { TLShapeId } from './TLShape'\n\n/**\n * Where a comment thread is anchored on the canvas. Modeled as a discriminated union so new\n * anchor kinds can be added without breaking existing threads:\n *\n * - `shape` \u2014 pinned to a shape. `x`/`y` are normalized (0\u20131) within the shape's bounds, so the pin\n * keeps its spot as the shape moves, resizes, and rotates. `isPrecise` mirrors arrow bindings:\n * when false (the default) the pin sits at a consumer-defined spot instead, top-right out of the\n * box, and `x`/`y` are the remembered precise position\n * - `point` \u2014 pinned to a fixed point on the page, in page coordinates\n * - `region` \u2014 pinned to a rectangular area of the page, in page coordinates\n * - `page` \u2014 a page-level thread with no spatial anchor\n *\n * @public\n */\nexport type TLCommentAnchor =\n\t| { type: 'shape'; shapeId: TLShapeId; x: number; y: number; isPrecise: boolean }\n\t| { type: 'point'; x: number; y: number }\n\t| {\n\t\t\ttype: 'region'\n\t\t\tx: number\n\t\t\ty: number\n\t\t\tw: number\n\t\t\th: number\n\t\t\t/** The normalized (0\u20131) corner the pin sits on \u2014 where the creating drag was released.\n\t\t\t * Absent on older records; consumers fall back to a corner of their own choosing. */\n\t\t\tpinX?: number\n\t\t\tpinY?: number\n\t }\n\t| { type: 'page' }\n\n/**\n * A comment thread. The thread owns the anchor (where the conversation lives on the canvas) and\n * the resolution state; the messages themselves are `TLComment` records pointing at the thread\n * via `threadId`, ordered by `createdAt`. v1 threads are flat \u2014 no nested replies.\n *\n * Threads and comments are document records, but they're intended to be served through the sync\n * server's object-store lane: gated by the session's `objectAccess` rather than `isReadonly`\n * (so \"can comment but not edit\" is expressible), excluded from document snapshots and `.tldr`\n * exports server-side, and persisted in a separate lane from the main document.\n *\n * Opt-in: register with `createTLSchema({ records: commentSchemaRecords })` on the server and\n * the matching `records` option on the client \u2014 neither type is part of the default schema, and\n * both sides must register them identically.\n *\n * @public\n */\nexport interface TLCommentThread extends BaseRecord<'comment-thread', TLCommentThreadId> {\n\t/** The page the thread lives on. */\n\tpageId: TLPageId\n\t/** Where the thread is anchored on that page. */\n\tanchor: TLCommentAnchor\n\t/** Who started the thread. Client-supplied; sync servers are expected to stamp/verify it. */\n\tcreatedBy: string\n\tcreatedAt: number\n\t/** Resolution state: when and by whom the thread was resolved, or null while open. */\n\tresolved: { at: number; by: string } | null\n\t/**\n\t * Whether the thread is soft-deleted. Clients set the flag and leave the record in place. Sync\n\t * servers are expected to enforce it as write-once and creator-only, reject client hard\n\t * deletes, and drop flagged threads from future room loads.\n\t */\n\tisDeleted: boolean\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentThreadId = RecordId<TLCommentThread>\n\n/**\n * A single comment message within a thread. See `TLCommentThread` for the overall model and\n * sync/registration notes.\n *\n * Comment mutations are deliberately not undoable \u2014 create/edit them with\n * `{ history: 'ignore' }` to avoid multiplayer surprises like a comment reappearing after\n * someone else deleted it.\n *\n * @public\n */\nexport interface TLComment extends BaseRecord<'comment', TLCommentId> {\n\t/** The thread this comment belongs to. */\n\tthreadId: TLCommentThreadId\n\t/** Denormalized from the thread so per-page queries don't need a join. */\n\tpageId: TLPageId\n\t/** See `TLCommentThread.createdBy` \u2014 same server-stamping caveat applies. */\n\tauthorId: string\n\tcreatedAt: number\n\t/** Null until the comment is first edited. */\n\teditedAt: number | null\n\t/** Rich text body. Use `toRichText(...)` for plaintext input. */\n\tbody: TLRichText\n\t/** Whether the comment is soft-deleted. Same model as `TLCommentThread.isDeleted`. */\n\tisDeleted: boolean\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentId = RecordId<TLComment>\n\n/**\n * One person's emoji reaction to one comment.\n *\n * A reaction is its own record rather than a field on the comment: comment records are owner-only\n * for updates, and a shared field would make concurrent reactions race to overwrite each other.\n *\n * A user holds at most one record per (comment, emoji) pair, enforced by the derived id \u2014 see\n * `createCommentReactionId`. Limiting a user to one reaction overall is client-side policy (the\n * commenting package's `reactionMode`), not a property of this record.\n *\n * @public\n */\nexport interface TLCommentReaction extends BaseRecord<'comment-reaction', TLCommentReactionId> {\n\t/** The comment being reacted to. */\n\tcommentId: TLCommentId\n\t/** Denormalized from the comment, so a thread's reactions can be found without a join. */\n\tthreadId: TLCommentThreadId\n\t/** Denormalized from the comment \u2014 see `TLComment.pageId`. */\n\tpageId: TLPageId\n\t/** Who reacted. See `TLCommentThread.createdBy` \u2014 same server-stamping caveat applies. */\n\tuserId: string\n\t/** The emoji itself, as a string (e.g. `'\uD83D\uDC4D'`), not a shortcode. */\n\temoji: string\n\tcreatedAt: number\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentReactionId = RecordId<TLCommentReaction>\n\n/**\n * An emoji shortcode or literal. Bounded because reaction ids embed the emoji verbatim\n * (see {@link createCommentReactionId}), so an unbounded value means an unbounded record id.\n */\nconst emojiValidator = T.string.check((value) => {\n\tif (value.length === 0 || value.length > 64) {\n\t\tthrow new T.ValidationError(`Expected an emoji of 1-64 characters, got ${value.length}`)\n\t}\n})\n\nconst commentAnchorValidator: T.Validator<TLCommentAnchor> = T.union('type', {\n\tshape: T.object({\n\t\ttype: T.literal('shape'),\n\t\tshapeId: idValidator<TLShapeId>('shape'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t\tisPrecise: T.boolean,\n\t}),\n\tpoint: T.object({\n\t\ttype: T.literal('point'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t}),\n\tregion: T.object({\n\t\ttype: T.literal('region'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t\tw: T.number,\n\t\th: T.number,\n\t\tpinX: T.number.optional(),\n\t\tpinY: T.number.optional(),\n\t}),\n\tpage: T.object({\n\t\ttype: T.literal('page'),\n\t}),\n})\n\n/**\n * Guard migrations for the comment record types. Each sequence starts with an identity migration\n * that has no `down`, so a session whose schema predates the comment types is rejected with\n * CLIENT_TOO_OLD instead of being sent records its store can't represent.\n */\nfunction createCommentGuardMigrations(\n\ttypeName: 'comment' | 'comment-thread' | 'comment-reaction',\n\textra: Parameters<typeof createRecordMigrationSequence>[0]['sequence'] = []\n) {\n\treturn createRecordMigrationSequence({\n\t\tsequenceId: `com.tldraw.${typeName}`,\n\t\trecordType: typeName,\n\t\tretroactive: true,\n\t\tsequence: [\n\t\t\t{\n\t\t\t\tid: `com.tldraw.${typeName}/1`,\n\t\t\t\tup: (record) => record,\n\t\t\t},\n\t\t\t...extra,\n\t\t],\n\t})\n}\n\n/**\n * Config for registering the `comment-thread` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLCommentThread`.\n *\n * @public\n */\nexport const commentThreadRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment-thread', [\n\t\t{\n\t\t\t// Shape anchors gained normalized x/y + isPrecise; existing ones were imprecise (top-right).\n\t\t\tid: 'com.tldraw.comment-thread/2',\n\t\t\tup: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'shape' && anchor.x === undefined) {\n\t\t\t\t\t;(record as any).anchor = { ...anchor, x: 1, y: 0, isPrecise: false }\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t\tdown: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'shape') {\n\t\t\t\t\t;(record as any).anchor = { type: 'shape', shapeId: anchor.shapeId }\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// Region anchors gained an optional pin corner (where the creating drag released).\n\t\t\tid: 'com.tldraw.comment-thread/3',\n\t\t\tup: (record) => record,\n\t\t\tdown: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'region') {\n\t\t\t\t\tconst { pinX: _pinX, pinY: _pinY, ...rest } = anchor\n\t\t\t\t\t;(record as any).anchor = rest\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t},\n\t]),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentThreadId>('comment-thread'),\n\t\ttypeName: T.literal('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tanchor: commentAnchorValidator,\n\t\tcreatedBy: T.string,\n\t\tcreatedAt: T.number,\n\t\tresolved: T.object({ at: T.number, by: T.string }).nullable(),\n\t\tisDeleted: T.boolean,\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * Config for registering the `comment` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLComment`.\n *\n * @public\n */\nexport const commentRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment'),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentId>('comment'),\n\t\ttypeName: T.literal('comment'),\n\t\tthreadId: idValidator<TLCommentThreadId>('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tauthorId: T.string,\n\t\tcreatedAt: T.number,\n\t\teditedAt: T.number.nullable(),\n\t\tbody: richTextValidator,\n\t\tisDeleted: T.boolean,\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * Config for registering the `comment-reaction` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLCommentReaction`.\n *\n * @public\n */\nexport const commentReactionRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment-reaction'),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentReactionId>('comment-reaction'),\n\t\ttypeName: T.literal('comment-reaction'),\n\t\tcommentId: idValidator<TLCommentId>('comment'),\n\t\tthreadId: idValidator<TLCommentThreadId>('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tuserId: T.string,\n\t\temoji: emojiValidator,\n\t\tcreatedAt: T.number,\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * The `records` map to pass to `createTLSchema` / the client `records` option so comment\n * threads, comments, and reactions sync. Register the types together \u2014 one without the others\n * will fail schema validation on one side of the connection.\n *\n * @public\n */\nexport const commentSchemaRecords = {\n\t'comment-thread': commentThreadRecordConfig,\n\tcomment: commentRecordConfig,\n\t'comment-reaction': commentReactionRecordConfig,\n}\n\n/** @public */\nexport function createCommentThreadId(id?: string): TLCommentThreadId {\n\treturn createCustomRecordId('comment-thread', id) as TLCommentThreadId\n}\n\n/** @public */\nexport function createCommentId(id?: string): TLCommentId {\n\treturn createCustomRecordId('comment', id) as TLCommentId\n}\n\n/**\n * The id of one user's reaction to one comment, derived from the (comment, user, emoji) triple\n * rather than random, so the same triple always addresses the same record and two tabs converge\n * instead of racing to create duplicates. The sync authorizer leans on this, so it must be\n * injective: all three parts are URI-encoded before joining, so a `:` in any of them can't shift\n * the boundary and collapse two triples onto one id.\n *\n * @public\n */\nexport function createCommentReactionId(\n\tcommentId: TLCommentId,\n\tuserId: string,\n\temoji: string\n): TLCommentReactionId {\n\treturn createCustomRecordId(\n\t\t'comment-reaction',\n\t\t`${encodeURIComponent(commentId)}:${encodeURIComponent(userId)}:${encodeURIComponent(emoji)}`\n\t) as TLCommentReactionId\n}\n\n/**\n * Type guard for `TLCommentThreadId`. `isCommentId` rejects these \u2014 `comment-thread:` and\n * `comment:` differ in the character after `comment`, so the prefixes never overlap.\n *\n * @public\n */\nexport function isCommentThreadId(id: string): id is TLCommentThreadId {\n\treturn isCustomRecordId('comment-thread', id)\n}\n\n/**\n * Type guard for `TLCommentId`. See `isCommentThreadId`.\n *\n * @public\n */\nexport function isCommentId(id: string): id is TLCommentId {\n\treturn isCustomRecordId('comment', id)\n}\n\n/**\n * Type guard for `TLCommentReactionId`. See `isCommentThreadId`.\n *\n * @public\n */\nexport function isCommentReactionId(id: string): id is TLCommentReactionId {\n\treturn isCustomRecordId('comment-reaction', id)\n}\n\n/**\n * Create a new comment thread record. Pair with `createComment` for the thread's first message.\n *\n * @public\n */\nexport function createCommentThread(props: {\n\tpageId: TLPageId\n\tanchor: TLCommentAnchor\n\tcreatedBy: string\n\tnow?: number\n\tmeta?: JsonObject\n}): TLCommentThread {\n\treturn {\n\t\tid: createCommentThreadId(),\n\t\ttypeName: 'comment-thread',\n\t\tpageId: props.pageId,\n\t\tanchor: props.anchor,\n\t\tcreatedBy: props.createdBy,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\tresolved: null,\n\t\tisDeleted: false,\n\t\tmeta: props.meta ?? {},\n\t}\n}\n\n/**\n * Create a new comment record within a thread.\n *\n * @public\n */\nexport function createComment(props: {\n\tthreadId: TLCommentThreadId\n\tpageId: TLPageId\n\tauthorId: string\n\tbody: TLRichText\n\tnow?: number\n\tmeta?: JsonObject\n}): TLComment {\n\treturn {\n\t\tid: createCommentId(),\n\t\ttypeName: 'comment',\n\t\tthreadId: props.threadId,\n\t\tpageId: props.pageId,\n\t\tauthorId: props.authorId,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\teditedAt: null,\n\t\tbody: props.body,\n\t\tisDeleted: false,\n\t\tmeta: props.meta ?? {},\n\t}\n}\n\n/**\n * Create a reaction record for one user's reaction to one comment. The id is derived from the\n * comment and user (see `createCommentReactionId`), so re-reacting overwrites rather than adding.\n *\n * @public\n */\nexport function createCommentReaction(props: {\n\tcommentId: TLCommentId\n\tthreadId: TLCommentThreadId\n\tpageId: TLPageId\n\tuserId: string\n\temoji: string\n\tnow?: number\n\tmeta?: JsonObject\n}): TLCommentReaction {\n\treturn {\n\t\tid: createCommentReactionId(props.commentId, props.userId, props.emoji),\n\t\ttypeName: 'comment-reaction',\n\t\tcommentId: props.commentId,\n\t\tthreadId: props.threadId,\n\t\tpageId: props.pageId,\n\t\tuserId: props.userId,\n\t\temoji: props.emoji,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\tmeta: props.meta ?? {},\n\t}\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAA+B,qCAAqC;AAEpE,SAAS,SAAS;AAClB,SAAS,mBAAmB;AAC5B,SAAS,yBAAqC;AAC9C,SAA2B,sBAAsB,wBAAwB;AAwIzE,MAAM,iBAAiB,EAAE,OAAO,MAAM,CAAC,UAAU;AAChD,MAAI,MAAM,WAAW,KAAK,MAAM,SAAS,IAAI;AAC5C,UAAM,IAAI,EAAE,gBAAgB,6CAA6C,MAAM,MAAM,EAAE;AAAA,EACxF;AACD,CAAC;AAED,MAAM,yBAAuD,EAAE,MAAM,QAAQ;AAAA,EAC5E,OAAO,EAAE,OAAO;AAAA,IACf,MAAM,EAAE,QAAQ,OAAO;AAAA,IACvB,SAAS,YAAuB,OAAO;AAAA,IACvC,GAAG,EAAE;AAAA,IACL,GAAG,EAAE;AAAA,IACL,WAAW,EAAE;AAAA,EACd,CAAC;AAAA,EACD,OAAO,EAAE,OAAO;AAAA,IACf,MAAM,EAAE,QAAQ,OAAO;AAAA,IACvB,GAAG,EAAE;AAAA,IACL,GAAG,EAAE;AAAA,EACN,CAAC;AAAA,EACD,QAAQ,EAAE,OAAO;AAAA,IAChB,MAAM,EAAE,QAAQ,QAAQ;AAAA,IACxB,GAAG,EAAE;AAAA,IACL,GAAG,EAAE;AAAA,IACL,GAAG,EAAE;AAAA,IACL,GAAG,EAAE;AAAA,IACL,MAAM,EAAE,OAAO,SAAS;AAAA,IACxB,MAAM,EAAE,OAAO,SAAS;AAAA,EACzB,CAAC;AAAA,EACD,MAAM,EAAE,OAAO;AAAA,IACd,MAAM,EAAE,QAAQ,MAAM;AAAA,EACvB,CAAC;AACF,CAAC;AAOD,SAAS,6BACR,UACA,QAAyE,CAAC,GACzE;AACD,SAAO,8BAA8B;AAAA,IACpC,YAAY,cAAc,QAAQ;AAAA,IAClC,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,UAAU;AAAA,MACT;AAAA,QACC,IAAI,cAAc,QAAQ;AAAA,QAC1B,IAAI,CAAC,WAAW;AAAA,MACjB;AAAA,MACA,GAAG;AAAA,IACJ;AAAA,EACD,CAAC;AACF;AAQO,MAAM,4BAA8C;AAAA,EAC1D,OAAO;AAAA,EACP,YAAY,6BAA6B,kBAAkB;AAAA,IAC1D;AAAA;AAAA,MAEC,IAAI;AAAA,MACJ,IAAI,CAAC,WAAW;AACf,cAAM,SAAU,OAAe;AAC/B,YAAI,QAAQ,SAAS,WAAW,OAAO,MAAM,QAAW;AACvD;AAAC,UAAC,OAAe,SAAS,EAAE,GAAG,QAAQ,GAAG,GAAG,GAAG,GAAG,WAAW,MAAM;AAAA,QACrE;AACA,eAAO;AAAA,MACR;AAAA,MACA,MAAM,CAAC,WAAW;AACjB,cAAM,SAAU,OAAe;AAC/B,YAAI,QAAQ,SAAS,SAAS;AAC7B;AAAC,UAAC,OAAe,SAAS,EAAE,MAAM,SAAS,SAAS,OAAO,QAAQ;AAAA,QACpE;AACA,eAAO;AAAA,MACR;AAAA,IACD;AAAA,IACA;AAAA;AAAA,MAEC,IAAI;AAAA,MACJ,IAAI,CAAC,WAAW;AAAA,MAChB,MAAM,CAAC,WAAW;AACjB,cAAM,SAAU,OAAe;AAC/B,YAAI,QAAQ,SAAS,UAAU;AAC9B,gBAAM,EAAE,MAAM,OAAO,MAAM,OAAO,GAAG,KAAK,IAAI;AAC7C,UAAC,OAAe,SAAS;AAAA,QAC3B;AACA,eAAO;AAAA,MACR;AAAA,IACD;AAAA,EACD,CAAC;AAAA,EACD,WAAW,EAAE,OAAO;AAAA,IACnB,IAAI,YAA+B,gBAAgB;AAAA,IACnD,UAAU,EAAE,QAAQ,gBAAgB;AAAA,IACpC,QAAQ,YAAsB,MAAM;AAAA,IACpC,QAAQ;AAAA,IACR,WAAW,EAAE;AAAA,IACb,WAAW,EAAE;AAAA,IACb,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,IAAI,EAAE,OAAO,CAAC,EAAE,SAAS;AAAA,IAC5D,WAAW,EAAE;AAAA,IACb,MAAM,EAAE;AAAA,EACT,CAAC;AACF;AAQO,MAAM,sBAAwC;AAAA,EACpD,OAAO;AAAA,EACP,YAAY,6BAA6B,SAAS;AAAA,EAClD,WAAW,EAAE,OAAO;AAAA,IACnB,IAAI,YAAyB,SAAS;AAAA,IACtC,UAAU,EAAE,QAAQ,SAAS;AAAA,IAC7B,UAAU,YAA+B,gBAAgB;AAAA,IACzD,QAAQ,YAAsB,MAAM;AAAA,IACpC,UAAU,EAAE;AAAA,IACZ,WAAW,EAAE;AAAA,IACb,UAAU,EAAE,OAAO,SAAS;AAAA,IAC5B,MAAM;AAAA,IACN,WAAW,EAAE;AAAA,IACb,MAAM,EAAE;AAAA,EACT,CAAC;AACF;AAQO,MAAM,8BAAgD;AAAA,EAC5D,OAAO;AAAA,EACP,YAAY,6BAA6B,kBAAkB;AAAA,EAC3D,WAAW,EAAE,OAAO;AAAA,IACnB,IAAI,YAAiC,kBAAkB;AAAA,IACvD,UAAU,EAAE,QAAQ,kBAAkB;AAAA,IACtC,WAAW,YAAyB,SAAS;AAAA,IAC7C,UAAU,YAA+B,gBAAgB;AAAA,IACzD,QAAQ,YAAsB,MAAM;AAAA,IACpC,QAAQ,EAAE;AAAA,IACV,OAAO;AAAA,IACP,WAAW,EAAE;AAAA,IACb,MAAM,EAAE;AAAA,EACT,CAAC;AACF;AASO,MAAM,uBAAuB;AAAA,EACnC,kBAAkB;AAAA,EAClB,SAAS;AAAA,EACT,oBAAoB;AACrB;AAGO,SAAS,sBAAsB,IAAgC;AACrE,SAAO,qBAAqB,kBAAkB,EAAE;AACjD;AAGO,SAAS,gBAAgB,IAA0B;AACzD,SAAO,qBAAqB,WAAW,EAAE;AAC1C;AAWO,SAAS,wBACf,WACA,QACA,OACsB;AACtB,SAAO;AAAA,IACN;AAAA,IACA,GAAG,mBAAmB,SAAS,CAAC,IAAI,mBAAmB,MAAM,CAAC,IAAI,mBAAmB,KAAK,CAAC;AAAA,EAC5F;AACD;AAQO,SAAS,kBAAkB,IAAqC;AACtE,SAAO,iBAAiB,kBAAkB,EAAE;AAC7C;AAOO,SAAS,YAAY,IAA+B;AAC1D,SAAO,iBAAiB,WAAW,EAAE;AACtC;AAOO,SAAS,oBAAoB,IAAuC;AAC1E,SAAO,iBAAiB,oBAAoB,EAAE;AAC/C;AAOO,SAAS,oBAAoB,OAMhB;AACnB,SAAO;AAAA,IACN,IAAI,sBAAsB;AAAA,IAC1B,UAAU;AAAA,IACV,QAAQ,MAAM;AAAA,IACd,QAAQ,MAAM;AAAA,IACd,WAAW,MAAM;AAAA,IACjB,WAAW,MAAM,OAAO,KAAK,IAAI;AAAA,IACjC,UAAU;AAAA,IACV,WAAW;AAAA,IACX,MAAM,MAAM,QAAQ,CAAC;AAAA,EACtB;AACD;AAOO,SAAS,cAAc,OAOhB;AACb,SAAO;AAAA,IACN,IAAI,gBAAgB;AAAA,IACpB,UAAU;AAAA,IACV,UAAU,MAAM;AAAA,IAChB,QAAQ,MAAM;AAAA,IACd,UAAU,MAAM;AAAA,IAChB,WAAW,MAAM,OAAO,KAAK,IAAI;AAAA,IACjC,UAAU;AAAA,IACV,MAAM,MAAM;AAAA,IACZ,WAAW;AAAA,IACX,MAAM,MAAM,QAAQ,CAAC;AAAA,EACtB;AACD;AAQO,SAAS,sBAAsB,OAQhB;AACrB,SAAO;AAAA,IACN,IAAI,wBAAwB,MAAM,WAAW,MAAM,QAAQ,MAAM,KAAK;AAAA,IACtE,UAAU;AAAA,IACV,WAAW,MAAM;AAAA,IACjB,UAAU,MAAM;AAAA,IAChB,QAAQ,MAAM;AAAA,IACd,QAAQ,MAAM;AAAA,IACd,OAAO,MAAM;AAAA,IACb,WAAW,MAAM,OAAO,KAAK,IAAI;AAAA,IACjC,MAAM,MAAM,QAAQ,CAAC;AAAA,EACtB;AACD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/tlschema",
|
|
3
3
|
"description": "tldraw infinite canvas SDK (schema).",
|
|
4
|
-
"version": "5.3.0-next.
|
|
4
|
+
"version": "5.3.0-next.dec36c5c2930",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -51,10 +51,10 @@
|
|
|
51
51
|
"vitest": "^4.1.7"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@tldraw/state": "5.3.0-next.
|
|
55
|
-
"@tldraw/store": "5.3.0-next.
|
|
56
|
-
"@tldraw/utils": "5.3.0-next.
|
|
57
|
-
"@tldraw/validate": "5.3.0-next.
|
|
54
|
+
"@tldraw/state": "5.3.0-next.dec36c5c2930",
|
|
55
|
+
"@tldraw/store": "5.3.0-next.dec36c5c2930",
|
|
56
|
+
"@tldraw/utils": "5.3.0-next.dec36c5c2930",
|
|
57
|
+
"@tldraw/validate": "5.3.0-next.dec36c5c2930"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"react": "^18.2.0 || ^19.2.1",
|
package/src/records/TLComment.ts
CHANGED
|
@@ -11,11 +11,10 @@ import { TLShapeId } from './TLShape'
|
|
|
11
11
|
* Where a comment thread is anchored on the canvas. Modeled as a discriminated union so new
|
|
12
12
|
* anchor kinds can be added without breaking existing threads:
|
|
13
13
|
*
|
|
14
|
-
* - `shape` — pinned to a shape. `x`/`y` are normalized (0–1) within the shape's
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* spot (top-right out of the box), and `x`/`y` are the remembered precise position
|
|
14
|
+
* - `shape` — pinned to a shape. `x`/`y` are normalized (0–1) within the shape's bounds, so the pin
|
|
15
|
+
* keeps its spot as the shape moves, resizes, and rotates. `isPrecise` mirrors arrow bindings:
|
|
16
|
+
* when false (the default) the pin sits at a consumer-defined spot instead, top-right out of the
|
|
17
|
+
* box, and `x`/`y` are the remembered precise position
|
|
19
18
|
* - `point` — pinned to a fixed point on the page, in page coordinates
|
|
20
19
|
* - `region` — pinned to a rectangular area of the page, in page coordinates
|
|
21
20
|
* - `page` — a page-level thread with no spatial anchor
|
|
@@ -59,20 +58,15 @@ export interface TLCommentThread extends BaseRecord<'comment-thread', TLCommentT
|
|
|
59
58
|
pageId: TLPageId
|
|
60
59
|
/** Where the thread is anchored on that page. */
|
|
61
60
|
anchor: TLCommentAnchor
|
|
62
|
-
/**
|
|
63
|
-
* Who started the thread. Client-supplied for now; the sync server should stamp/verify this
|
|
64
|
-
* from the session's authenticated identity once record-level authorization lands (the same
|
|
65
|
-
* model presence already uses for self-ownership).
|
|
66
|
-
*/
|
|
61
|
+
/** Who started the thread. Client-supplied; sync servers are expected to stamp/verify it. */
|
|
67
62
|
createdBy: string
|
|
68
63
|
createdAt: number
|
|
69
64
|
/** Resolution state: when and by whom the thread was resolved, or null while open. */
|
|
70
65
|
resolved: { at: number; by: string } | null
|
|
71
66
|
/**
|
|
72
|
-
* Whether the thread is soft-deleted. Clients
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* clients, and drop flagged threads from future room loads.
|
|
67
|
+
* Whether the thread is soft-deleted. Clients set the flag and leave the record in place. Sync
|
|
68
|
+
* servers are expected to enforce it as write-once and creator-only, reject client hard
|
|
69
|
+
* deletes, and drop flagged threads from future room loads.
|
|
76
70
|
*/
|
|
77
71
|
isDeleted: boolean
|
|
78
72
|
meta: JsonObject
|
|
@@ -103,12 +97,7 @@ export interface TLComment extends BaseRecord<'comment', TLCommentId> {
|
|
|
103
97
|
editedAt: number | null
|
|
104
98
|
/** Rich text body. Use `toRichText(...)` for plaintext input. */
|
|
105
99
|
body: TLRichText
|
|
106
|
-
/**
|
|
107
|
-
* Whether the comment is soft-deleted. Same model as `TLCommentThread.isDeleted` — clients
|
|
108
|
-
* delete a comment by setting this flag and leaving the record in place, hidden from
|
|
109
|
-
* rendering. Sync servers are expected to enforce the flag as write-once and author-only,
|
|
110
|
-
* reject hard deletes from clients, and drop flagged comments from future room loads.
|
|
111
|
-
*/
|
|
100
|
+
/** Whether the comment is soft-deleted. Same model as `TLCommentThread.isDeleted`. */
|
|
112
101
|
isDeleted: boolean
|
|
113
102
|
meta: JsonObject
|
|
114
103
|
}
|
|
@@ -119,16 +108,11 @@ export type TLCommentId = RecordId<TLComment>
|
|
|
119
108
|
/**
|
|
120
109
|
* One person's emoji reaction to one comment.
|
|
121
110
|
*
|
|
122
|
-
* A reaction is its own record rather than a field on the comment
|
|
123
|
-
*
|
|
124
|
-
* are owner-only for updates, so a reaction could never be written onto them; and holding
|
|
125
|
-
* everyone's reactions in one shared field would mean each write carried the whole set, so two
|
|
126
|
-
* people reacting at once would race to overwrite each other. A record per person keeps every
|
|
127
|
-
* write to its own record, which is what lets concurrent reactions coexist.
|
|
111
|
+
* A reaction is its own record rather than a field on the comment: comment records are owner-only
|
|
112
|
+
* for updates, and a shared field would make concurrent reactions race to overwrite each other.
|
|
128
113
|
*
|
|
129
|
-
* A user holds at most one record per (comment, emoji) pair
|
|
130
|
-
*
|
|
131
|
-
* Restricting a user to a single reaction overall is a client-side policy layered on top (see the
|
|
114
|
+
* A user holds at most one record per (comment, emoji) pair, enforced by the derived id — see
|
|
115
|
+
* `createCommentReactionId`. Limiting a user to one reaction overall is client-side policy (the
|
|
132
116
|
* commenting package's `reactionMode`), not a property of this record.
|
|
133
117
|
*
|
|
134
118
|
* @public
|
|
@@ -189,11 +173,9 @@ const commentAnchorValidator: T.Validator<TLCommentAnchor> = T.union('type', {
|
|
|
189
173
|
})
|
|
190
174
|
|
|
191
175
|
/**
|
|
192
|
-
* Guard migrations for the comment record types. Each sequence
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* rejected with CLIENT_TOO_OLD (prompting a refresh) instead of being sent record types their
|
|
196
|
-
* store cannot represent.
|
|
176
|
+
* Guard migrations for the comment record types. Each sequence starts with an identity migration
|
|
177
|
+
* that has no `down`, so a session whose schema predates the comment types is rejected with
|
|
178
|
+
* CLIENT_TOO_OLD instead of being sent records its store can't represent.
|
|
197
179
|
*/
|
|
198
180
|
function createCommentGuardMigrations(
|
|
199
181
|
typeName: 'comment' | 'comment-thread' | 'comment-reaction',
|
|
@@ -337,16 +319,10 @@ export function createCommentId(id?: string): TLCommentId {
|
|
|
337
319
|
|
|
338
320
|
/**
|
|
339
321
|
* The id of one user's reaction to one comment, derived from the (comment, user, emoji) triple
|
|
340
|
-
* rather than random
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
* not a property of the id. The sync authorizer leans on this derivation, so it must be injective.
|
|
345
|
-
*
|
|
346
|
-
* `emoji` here is the reaction's token — the emoji glyph for the default palette, or whatever
|
|
347
|
-
* opaque string a consumer's palette uses. All three parts are URI-encoded before joining, so a
|
|
348
|
-
* `:` in any of them can't shift the boundary and collapse two different triples onto one id. The
|
|
349
|
-
* id is only ever recomputed for comparison, never parsed back, so the encoding needs no inverse.
|
|
322
|
+
* rather than random, so the same triple always addresses the same record and two tabs converge
|
|
323
|
+
* instead of racing to create duplicates. The sync authorizer leans on this, so it must be
|
|
324
|
+
* injective: all three parts are URI-encoded before joining, so a `:` in any of them can't shift
|
|
325
|
+
* the boundary and collapse two triples onto one id.
|
|
350
326
|
*
|
|
351
327
|
* @public
|
|
352
328
|
*/
|
|
@@ -362,9 +338,8 @@ export function createCommentReactionId(
|
|
|
362
338
|
}
|
|
363
339
|
|
|
364
340
|
/**
|
|
365
|
-
* Type guard for `TLCommentThreadId`.
|
|
366
|
-
*
|
|
367
|
-
* so the two never overlap.
|
|
341
|
+
* Type guard for `TLCommentThreadId`. `isCommentId` rejects these — `comment-thread:` and
|
|
342
|
+
* `comment:` differ in the character after `comment`, so the prefixes never overlap.
|
|
368
343
|
*
|
|
369
344
|
* @public
|
|
370
345
|
*/
|
|
@@ -373,8 +348,7 @@ export function isCommentThreadId(id: string): id is TLCommentThreadId {
|
|
|
373
348
|
}
|
|
374
349
|
|
|
375
350
|
/**
|
|
376
|
-
* Type guard for `TLCommentId`. See `isCommentThreadId
|
|
377
|
-
* correctly rejected here.
|
|
351
|
+
* Type guard for `TLCommentId`. See `isCommentThreadId`.
|
|
378
352
|
*
|
|
379
353
|
* @public
|
|
380
354
|
*/
|
|
@@ -383,8 +357,7 @@ export function isCommentId(id: string): id is TLCommentId {
|
|
|
383
357
|
}
|
|
384
358
|
|
|
385
359
|
/**
|
|
386
|
-
* Type guard for `TLCommentReactionId`.
|
|
387
|
-
* prefix never overlaps `comment:` — the character after `comment` differs (`-` vs `:`).
|
|
360
|
+
* Type guard for `TLCommentReactionId`. See `isCommentThreadId`.
|
|
388
361
|
*
|
|
389
362
|
* @public
|
|
390
363
|
*/
|