@mh-gg/base-plugins 0.1.1-alpha.20260613T085325975Z
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/README.md +247 -0
- package/package.json +48 -0
- package/src/approvals/index.cjs +65 -0
- package/src/attachments/index.cjs +75 -0
- package/src/calendar/index.cjs +48 -0
- package/src/checklists/index.cjs +58 -0
- package/src/comments/index.cjs +15 -0
- package/src/comments/plugin.cjs +66 -0
- package/src/comments/reducer.cjs +81 -0
- package/src/comments/schemas.cjs +60 -0
- package/src/comments/state.cjs +17 -0
- package/src/comments/threads.cjs +44 -0
- package/src/comments/views.cjs +27 -0
- package/src/composer/capabilities.cjs +19 -0
- package/src/composer/compose.cjs +37 -0
- package/src/composer/index.cjs +15 -0
- package/src/composer/operations.cjs +42 -0
- package/src/composer/registry.cjs +155 -0
- package/src/composer/selection.cjs +39 -0
- package/src/composer/suite.cjs +32 -0
- package/src/crdt/client.mjs +207 -0
- package/src/crdt/index.cjs +258 -0
- package/src/embeds/index.cjs +90 -0
- package/src/files/index.cjs +133 -0
- package/src/index.cjs +19 -0
- package/src/labels/index.cjs +46 -0
- package/src/location-pins/index.cjs +142 -0
- package/src/markdown/documents/index.cjs +128 -0
- package/src/markdown/index.cjs +8 -0
- package/src/markdown/parser/index.cjs +127 -0
- package/src/markdown/providers/audio.cjs +77 -0
- package/src/markdown/providers/cloud.cjs +72 -0
- package/src/markdown/providers/developer.cjs +45 -0
- package/src/markdown/providers/direct.cjs +49 -0
- package/src/markdown/providers/games.cjs +26 -0
- package/src/markdown/providers/images.cjs +88 -0
- package/src/markdown/providers/index.cjs +97 -0
- package/src/markdown/providers/maps.cjs +24 -0
- package/src/markdown/providers/productivity.cjs +30 -0
- package/src/markdown/providers/res-inspired.cjs +11 -0
- package/src/markdown/providers/social.cjs +33 -0
- package/src/markdown/providers/video.cjs +139 -0
- package/src/markdown/resolve.cjs +87 -0
- package/src/media-rooms/index.cjs +244 -0
- package/src/presence/index.cjs +193 -0
- package/src/reactions/index.cjs +47 -0
- package/src/screen-share/index.cjs +84 -0
- package/src/shared/constants.cjs +87 -0
- package/src/shared/embed.cjs +82 -0
- package/src/shared/index.cjs +20 -0
- package/src/shared/roles.cjs +5 -0
- package/src/shared/scopes.cjs +15 -0
- package/src/shared/url.cjs +32 -0
- package/src/shared/validation.cjs +31 -0
- package/test/composable-plugins.test.cjs +170 -0
- package/test/crdt-plugin.test.cjs +168 -0
- package/test/embed-autodetect-providers.test.cjs +138 -0
- package/test/markdown-media-workflow-plugins.test.cjs +201 -0
- package/test/markdown-parser-edge-cases.test.cjs +86 -0
- package/test/plugin-structure.test.cjs +69 -0
- package/test/shared-plugin-edges.test.cjs +207 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const { activity, actorName, entityId, moderatorOrBetter } = require("../shared/index.cjs");
|
|
2
|
+
const { createThread, ensureThread, findComment, findThreadByScope } = require("./threads.cjs");
|
|
3
|
+
|
|
4
|
+
function createThreadOperation(state, op) {
|
|
5
|
+
const duplicate = findThreadByScope(state, op.payload.scopeType, op.payload.scopeId);
|
|
6
|
+
if (duplicate) return state;
|
|
7
|
+
const thread = createThread(op, op.payload);
|
|
8
|
+
return { ...state, threads: { ...state.threads, [thread.id]: thread }, activity: activity(state, op, `${actorName(op.actor)} opened ${thread.title}`) };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function addCommentOperation(state, op) {
|
|
12
|
+
const ensured = ensureThread(state, op, op.payload);
|
|
13
|
+
const comment = {
|
|
14
|
+
id: entityId("comment", op),
|
|
15
|
+
threadId: ensured.thread.id,
|
|
16
|
+
parentId: op.payload.parentId || null,
|
|
17
|
+
authorId: op.actor.memberId,
|
|
18
|
+
authorName: actorName(op.actor),
|
|
19
|
+
body: op.payload.body,
|
|
20
|
+
reactions: {},
|
|
21
|
+
createdAt: op.createdAt,
|
|
22
|
+
updatedAt: op.createdAt
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
...ensured.state,
|
|
26
|
+
threads: { ...ensured.state.threads, [ensured.thread.id]: { ...ensured.thread, lastCommentAt: op.createdAt, resolved: false } },
|
|
27
|
+
comments: { ...ensured.state.comments, [comment.id]: comment },
|
|
28
|
+
activity: activity(ensured.state, op, `${comment.authorName} commented on ${ensured.thread.title}`)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function assertAuthorOrModerator(op, comment, action) {
|
|
33
|
+
if (!moderatorOrBetter(op.actor) && comment.authorId !== op.actor.memberId) throw new Error(`Only authors or moderators can ${action} comments`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function editCommentOperation(state, op) {
|
|
37
|
+
const comment = findComment(state, op.payload.commentId);
|
|
38
|
+
assertAuthorOrModerator(op, comment, "edit");
|
|
39
|
+
return { ...state, comments: { ...state.comments, [comment.id]: { ...comment, body: op.payload.body, editedAt: op.createdAt, updatedAt: op.createdAt } }, activity: activity(state, op, `${actorName(op.actor)} edited a comment`) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function reactToCommentOperation(state, op) {
|
|
43
|
+
const comment = findComment(state, op.payload.commentId);
|
|
44
|
+
const current = new Set(comment.reactions?.[op.payload.emoji] || []);
|
|
45
|
+
if (current.has(op.actor.memberId)) current.delete(op.actor.memberId); else current.add(op.actor.memberId);
|
|
46
|
+
return { ...state, comments: { ...state.comments, [comment.id]: { ...comment, reactions: { ...(comment.reactions || {}), [op.payload.emoji]: [...current].sort() } } } };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function deleteCommentOperation(state, op) {
|
|
50
|
+
const comment = findComment(state, op.payload.commentId);
|
|
51
|
+
assertAuthorOrModerator(op, comment, "delete");
|
|
52
|
+
return { ...state, comments: { ...state.comments, [comment.id]: { ...comment, body: "", deletedAt: op.createdAt, deleteReason: op.payload.reason } }, activity: activity(state, op, `${actorName(op.actor)} deleted a comment`) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveThreadOperation(state, op) {
|
|
56
|
+
const thread = state.threads[op.payload.threadId];
|
|
57
|
+
if (!thread || thread.deletedAt) throw new Error(`Thread ${op.payload.threadId} not found`);
|
|
58
|
+
return { ...state, threads: { ...state.threads, [thread.id]: { ...thread, resolved: op.payload.resolved, resolvedAt: op.payload.resolved ? op.createdAt : undefined, resolvedBy: op.payload.resolved ? op.actor.memberId : undefined } }, activity: activity(state, op, `${actorName(op.actor)} ${op.payload.resolved ? "resolved" : "reopened"} ${thread.title}`) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function reduceCommentsState(_ctx, state, op) {
|
|
62
|
+
switch (op.type) {
|
|
63
|
+
case "comments.thread.create": return createThreadOperation(state, op);
|
|
64
|
+
case "comments.add": return addCommentOperation(state, op);
|
|
65
|
+
case "comments.edit": return editCommentOperation(state, op);
|
|
66
|
+
case "comments.react": return reactToCommentOperation(state, op);
|
|
67
|
+
case "comments.delete": return deleteCommentOperation(state, op);
|
|
68
|
+
case "comments.resolve": return resolveThreadOperation(state, op);
|
|
69
|
+
default: return state;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
addCommentOperation,
|
|
75
|
+
createThreadOperation,
|
|
76
|
+
deleteCommentOperation,
|
|
77
|
+
editCommentOperation,
|
|
78
|
+
reactToCommentOperation,
|
|
79
|
+
reduceCommentsState,
|
|
80
|
+
resolveThreadOperation
|
|
81
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const { boolean, enumValue, object, optionalString, scopePayload, string } = require("../shared/index.cjs");
|
|
2
|
+
|
|
3
|
+
const commentsOperationSchemaDescriptor = Object.freeze({
|
|
4
|
+
"comments.thread.create": { required: ["scopeType", "scopeId"], optional: ["title", "visibility"], authorize: { roles: ["member"] } },
|
|
5
|
+
"comments.add": { required: ["body"], optional: ["threadId", "scopeType", "scopeId", "parentId"], authorize: { roles: ["member"] } },
|
|
6
|
+
"comments.edit": { required: ["commentId", "body"], authorize: { roles: ["member"] } },
|
|
7
|
+
"comments.react": { required: ["commentId", "emoji"], authorize: { roles: ["member"] } },
|
|
8
|
+
"comments.delete": { required: ["commentId"], optional: ["reason"], authorize: { roles: ["member"] } },
|
|
9
|
+
"comments.resolve": { required: ["threadId"], optional: ["resolved"], authorize: { roles: ["moderator"] } }
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function commentsThreadPayload(payload) {
|
|
13
|
+
const value = object(payload, "comments.thread.create payload");
|
|
14
|
+
return {
|
|
15
|
+
...scopePayload(value),
|
|
16
|
+
title: optionalString(value.title, "title", 160),
|
|
17
|
+
visibility: value.visibility ? enumValue(value.visibility, "visibility", ["members", "moderators", "private"]) : "members"
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function commentsAddPayload(payload) {
|
|
22
|
+
const value = object(payload, "comments.add payload");
|
|
23
|
+
const threadId = optionalString(value.threadId, "threadId", 160);
|
|
24
|
+
return {
|
|
25
|
+
threadId,
|
|
26
|
+
...(threadId ? {} : scopePayload(value, "comments.add scope")),
|
|
27
|
+
parentId: optionalString(value.parentId, "parentId", 160),
|
|
28
|
+
body: string(value.body, "body", 4000)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function commentsEditPayload(payload) {
|
|
33
|
+
const value = object(payload, "comments.edit payload");
|
|
34
|
+
return { commentId: string(value.commentId, "commentId"), body: string(value.body, "body", 4000) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function commentsReactPayload(payload) {
|
|
38
|
+
const value = object(payload, "comments.react payload");
|
|
39
|
+
return { commentId: string(value.commentId, "commentId"), emoji: string(value.emoji, "emoji", 32) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function commentsDeletePayload(payload) {
|
|
43
|
+
const value = object(payload, "comments.delete payload");
|
|
44
|
+
return { commentId: string(value.commentId, "commentId"), reason: optionalString(value.reason, "reason", 240) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function commentsResolvePayload(payload) {
|
|
48
|
+
const value = object(payload, "comments.resolve payload");
|
|
49
|
+
return { threadId: string(value.threadId, "threadId"), resolved: value.resolved === undefined ? true : boolean(value.resolved) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
commentsAddPayload,
|
|
54
|
+
commentsDeletePayload,
|
|
55
|
+
commentsEditPayload,
|
|
56
|
+
commentsOperationSchemaDescriptor,
|
|
57
|
+
commentsReactPayload,
|
|
58
|
+
commentsResolvePayload,
|
|
59
|
+
commentsThreadPayload
|
|
60
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const { array, object, COMMENTS_PLUGIN_ID } = require("../shared/index.cjs");
|
|
2
|
+
|
|
3
|
+
const commentsStateSchemaDescriptor = Object.freeze({ plugin: COMMENTS_PLUGIN_ID, shape: ["threads", "comments", "activity"] });
|
|
4
|
+
|
|
5
|
+
function createInitialCommentsState() {
|
|
6
|
+
return { threads: {}, comments: {}, activity: [] };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function parseCommentsState(state) {
|
|
10
|
+
const value = object(state, "comments state");
|
|
11
|
+
object(value.threads, "threads");
|
|
12
|
+
object(value.comments, "comments");
|
|
13
|
+
array(value.activity, "activity");
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { commentsStateSchemaDescriptor, createInitialCommentsState, parseCommentsState };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const { actorName, entityId } = require("../shared/index.cjs");
|
|
2
|
+
|
|
3
|
+
function threadKey(scopeType, scopeId) {
|
|
4
|
+
return `${encodeURIComponent(scopeType)}:${encodeURIComponent(scopeId)}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function findThreadByScope(state, scopeType, scopeId) {
|
|
8
|
+
return Object.values(state.threads).find((thread) => thread.scopeType === scopeType && thread.scopeId === scopeId && !thread.deletedAt);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function createThread(op, payload) {
|
|
12
|
+
return {
|
|
13
|
+
id: entityId("thread", op),
|
|
14
|
+
scopeType: payload.scopeType,
|
|
15
|
+
scopeId: payload.scopeId,
|
|
16
|
+
scopeKey: threadKey(payload.scopeType, payload.scopeId),
|
|
17
|
+
title: payload.title || `${payload.scopeType} discussion`,
|
|
18
|
+
visibility: payload.visibility || "members",
|
|
19
|
+
createdBy: op.actor.memberId,
|
|
20
|
+
createdByName: actorName(op.actor),
|
|
21
|
+
createdAt: op.createdAt,
|
|
22
|
+
resolved: false
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ensureThread(state, op, payload) {
|
|
27
|
+
if (payload.threadId) {
|
|
28
|
+
const thread = state.threads[payload.threadId];
|
|
29
|
+
if (!thread || thread.deletedAt) throw new Error(`Thread ${payload.threadId} not found`);
|
|
30
|
+
return { state, thread };
|
|
31
|
+
}
|
|
32
|
+
const existing = findThreadByScope(state, payload.scopeType, payload.scopeId);
|
|
33
|
+
if (existing) return { state, thread: existing };
|
|
34
|
+
const thread = createThread(op, payload);
|
|
35
|
+
return { state: { ...state, threads: { ...state.threads, [thread.id]: thread } }, thread };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function findComment(state, commentId) {
|
|
39
|
+
const comment = state.comments[commentId];
|
|
40
|
+
if (!comment || comment.deletedAt) throw new Error(`Comment ${commentId} not found`);
|
|
41
|
+
return comment;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { createThread, ensureThread, findComment, findThreadByScope, threadKey };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const { scopePayload } = require("../shared/index.cjs");
|
|
2
|
+
|
|
3
|
+
function publicCommentsView(_ctx, state) {
|
|
4
|
+
const visibleThreads = Object.fromEntries(Object.entries(state.threads).filter(([, thread]) => !thread.deletedAt && _ctx.access.canView(thread.scopeType, thread.scopeId)));
|
|
5
|
+
const visibleThreadIds = new Set(Object.keys(visibleThreads));
|
|
6
|
+
const comments = Object.fromEntries(Object.entries(state.comments).filter(([, comment]) => !comment.deletedAt && visibleThreadIds.has(comment.threadId)));
|
|
7
|
+
return { ...state, threads: visibleThreads, comments };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function commentsForScope(_ctx, state, input = {}) {
|
|
11
|
+
const scoped = scopePayload(input, "commentsForScope input");
|
|
12
|
+
if (!_ctx.access.canView(scoped.scopeType, scoped.scopeId)) return { threads: [], comments: [] };
|
|
13
|
+
const threads = Object.values(state.threads).filter((thread) => thread.scopeType === scoped.scopeType && thread.scopeId === scoped.scopeId && !thread.deletedAt);
|
|
14
|
+
const threadIds = new Set(threads.map((thread) => thread.id));
|
|
15
|
+
const comments = Object.values(state.comments).filter((comment) => threadIds.has(comment.threadId) && !comment.deletedAt).sort((a, b) => a.createdAt - b.createdAt);
|
|
16
|
+
return { threads, comments };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function unresolvedThreads(_ctx, state) {
|
|
20
|
+
return Object.values(state.threads).filter((thread) => !thread.deletedAt && !thread.resolved && _ctx.access.canView(thread.scopeType, thread.scopeId)).sort((a, b) => (b.lastCommentAt || b.createdAt) - (a.lastCommentAt || a.createdAt));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function countForScope(ctx, input = {}) {
|
|
24
|
+
return commentsForScope(ctx, ctx.state, input).comments.length;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { commentsForScope, countForScope, publicCommentsView, unresolvedThreads };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const { unique, freezeArray } = require("./registry.cjs");
|
|
2
|
+
|
|
3
|
+
function summarizeCapabilities(plugins) {
|
|
4
|
+
const required = [];
|
|
5
|
+
const provides = [];
|
|
6
|
+
for (const plugin of plugins) {
|
|
7
|
+
required.push(...(plugin.capabilities?.requires || []));
|
|
8
|
+
provides.push(...(plugin.capabilities?.provides || []));
|
|
9
|
+
}
|
|
10
|
+
return Object.freeze({ required: freezeArray(unique(required).sort()), provides: freezeArray(unique(provides).sort()) });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function appCapabilitySet({ required = ["room.state", "room.roles", "relay.route"], optional = ["relay.event-cache"], suites = [] } = {}) {
|
|
14
|
+
const allSuites = Array.isArray(suites) ? suites : [suites];
|
|
15
|
+
const suiteCapabilities = allSuites.flatMap((suite) => (suite?.capabilities?.provides || []));
|
|
16
|
+
return Object.freeze({ required: freezeArray(unique(required).sort()), optional: freezeArray(unique([...optional, ...suiteCapabilities]).sort()) });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { appCapabilitySet, summarizeCapabilities };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const { freezeArray } = require("./registry.cjs");
|
|
2
|
+
const { getReusablePluginEntry } = require("./selection.cjs");
|
|
3
|
+
|
|
4
|
+
function normalizeCompositionItem(item) {
|
|
5
|
+
if (!item) return [];
|
|
6
|
+
if (Array.isArray(item)) return item.flatMap(normalizeCompositionItem);
|
|
7
|
+
if (item.kind === "matterhorn.example-plugin-suite") return item.entries.slice();
|
|
8
|
+
if (item.entries && Array.isArray(item.entries)) return item.entries.flatMap(normalizeCompositionItem);
|
|
9
|
+
if (item.plugin) return [item];
|
|
10
|
+
if (item.id && item.version) return [{ plugin: item }];
|
|
11
|
+
if (typeof item === "string") return [getReusablePluginEntry(item)];
|
|
12
|
+
throw new Error("Composition items must be plugins, plugin entries, suites, selections, or arrays");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function composeHostPluginEntries(...items) {
|
|
16
|
+
const seen = new Map();
|
|
17
|
+
const composed = [];
|
|
18
|
+
for (const entry of items.flatMap(normalizeCompositionItem)) {
|
|
19
|
+
const plugin = entry.plugin;
|
|
20
|
+
if (!plugin?.id) throw new Error("Plugin entry is missing plugin.id");
|
|
21
|
+
const existing = seen.get(plugin.id);
|
|
22
|
+
if (existing) {
|
|
23
|
+
if (existing.plugin.version !== plugin.version) throw new Error(`Conflicting versions for plugin ${plugin.id}`);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const frozen = Object.freeze({ ...entry });
|
|
27
|
+
seen.set(plugin.id, frozen);
|
|
28
|
+
composed.push(frozen);
|
|
29
|
+
}
|
|
30
|
+
return freezeArray(composed);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function composeHostPlugins(...items) {
|
|
34
|
+
return freezeArray(composeHostPluginEntries(...items).map((entry) => entry.plugin));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { composeHostPluginEntries, composeHostPlugins, normalizeCompositionItem };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const registry = require("./registry.cjs");
|
|
2
|
+
const capabilities = require("./capabilities.cjs");
|
|
3
|
+
const compose = require("./compose.cjs");
|
|
4
|
+
const operations = require("./operations.cjs");
|
|
5
|
+
const selection = require("./selection.cjs");
|
|
6
|
+
const suite = require("./suite.cjs");
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
...registry,
|
|
10
|
+
...capabilities,
|
|
11
|
+
...compose,
|
|
12
|
+
...operations,
|
|
13
|
+
...selection,
|
|
14
|
+
...suite
|
|
15
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const { manifestHash } = require("@mh-gg/base");
|
|
2
|
+
const { ensureOperationIdentity } = require("@mh-gg/protocol");
|
|
3
|
+
const { commentsHostPlugin } = require("./registry.cjs");
|
|
4
|
+
const { getReusablePlugin } = require("./selection.cjs");
|
|
5
|
+
|
|
6
|
+
function defaultActor() {
|
|
7
|
+
return {
|
|
8
|
+
memberId: "admin",
|
|
9
|
+
role: "admin",
|
|
10
|
+
displayName: "Admin",
|
|
11
|
+
deviceId: "admin"
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function reusablePluginOperationFactory({ appPack, roomId, actor, startSeq = 1, startTime = 1000 } = {}) {
|
|
16
|
+
if (!appPack) throw new Error("appPack is required");
|
|
17
|
+
let seq = startSeq - 1;
|
|
18
|
+
return function makePluginOperation(plugin, type, payload, overrides = {}) {
|
|
19
|
+
seq += 1;
|
|
20
|
+
const opSeq = overrides.seq ?? seq;
|
|
21
|
+
const resolvedPlugin = typeof plugin === "string" ? getReusablePlugin(plugin) : plugin;
|
|
22
|
+
const opActor = overrides.actor || actor || defaultActor();
|
|
23
|
+
const createdAt = overrides.createdAt ?? (startTime + opSeq);
|
|
24
|
+
const operation = {
|
|
25
|
+
clientOperationId: overrides.id || `demo_${opSeq}`,
|
|
26
|
+
roomId: overrides.roomId || roomId,
|
|
27
|
+
appPackId: appPack.id,
|
|
28
|
+
appPackHash: manifestHash(appPack),
|
|
29
|
+
pluginId: resolvedPlugin.id || commentsHostPlugin.id,
|
|
30
|
+
type,
|
|
31
|
+
actor: opActor,
|
|
32
|
+
seq: opSeq,
|
|
33
|
+
createdAt,
|
|
34
|
+
payload,
|
|
35
|
+
hlc: overrides.hlc,
|
|
36
|
+
auth: overrides.auth || { credentialId: `cred_${opActor.memberId}`, signature: "sig" }
|
|
37
|
+
};
|
|
38
|
+
return ensureOperationIdentity(operation, { now: createdAt, nodeId: opActor.deviceId || opActor.memberId || "matterhorn-sdk" });
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { reusablePluginOperationFactory };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const {
|
|
2
|
+
APPROVALS_PLUGIN_ID,
|
|
3
|
+
APPROVALS_PLUGIN_KEY,
|
|
4
|
+
ATTACHMENTS_PLUGIN_ID,
|
|
5
|
+
ATTACHMENTS_PLUGIN_KEY,
|
|
6
|
+
CALENDAR_PLUGIN_ID,
|
|
7
|
+
CALENDAR_PLUGIN_KEY,
|
|
8
|
+
CHECKLISTS_PLUGIN_ID,
|
|
9
|
+
CHECKLISTS_PLUGIN_KEY,
|
|
10
|
+
COMMENTS_PLUGIN_ID,
|
|
11
|
+
COMMENTS_PLUGIN_KEY,
|
|
12
|
+
CRDT_PLUGIN_ID,
|
|
13
|
+
CRDT_PLUGIN_KEY,
|
|
14
|
+
EMBEDS_PLUGIN_ID,
|
|
15
|
+
EMBEDS_PLUGIN_KEY,
|
|
16
|
+
FILES_PLUGIN_ID,
|
|
17
|
+
FILES_PLUGIN_KEY,
|
|
18
|
+
BASE_PLUGIN_VERSION,
|
|
19
|
+
BASE_PLUGINS_PACKAGE,
|
|
20
|
+
LABELS_PLUGIN_ID,
|
|
21
|
+
LABELS_PLUGIN_KEY,
|
|
22
|
+
LOCATION_PINS_PLUGIN_ID,
|
|
23
|
+
LOCATION_PINS_PLUGIN_KEY,
|
|
24
|
+
MARKDOWN_PLUGIN_ID,
|
|
25
|
+
MARKDOWN_PLUGIN_KEY,
|
|
26
|
+
MEDIA_ROOMS_PLUGIN_ID,
|
|
27
|
+
MEDIA_ROOMS_PLUGIN_KEY,
|
|
28
|
+
PRESENCE_PLUGIN_ID,
|
|
29
|
+
PRESENCE_PLUGIN_KEY,
|
|
30
|
+
REACTIONS_PLUGIN_ID,
|
|
31
|
+
REACTIONS_PLUGIN_KEY,
|
|
32
|
+
SCREEN_SHARE_PLUGIN_ID,
|
|
33
|
+
SCREEN_SHARE_PLUGIN_KEY
|
|
34
|
+
} = require("../shared/constants.cjs");
|
|
35
|
+
const { commentsHostPlugin } = require("../comments/index.cjs");
|
|
36
|
+
const { presenceHostPlugin } = require("../presence/index.cjs");
|
|
37
|
+
const { mediaRoomsHostPlugin } = require("../media-rooms/index.cjs");
|
|
38
|
+
const { screenShareHostPlugin } = require("../screen-share/index.cjs");
|
|
39
|
+
const { markdownHostPlugin } = require("../markdown/documents/index.cjs");
|
|
40
|
+
const { embedsHostPlugin } = require("../embeds/index.cjs");
|
|
41
|
+
const { attachmentsHostPlugin } = require("../attachments/index.cjs");
|
|
42
|
+
const { filesHostPlugin } = require("../files/index.cjs");
|
|
43
|
+
const { reactionsHostPlugin } = require("../reactions/index.cjs");
|
|
44
|
+
const { labelsHostPlugin } = require("../labels/index.cjs");
|
|
45
|
+
const { approvalsHostPlugin } = require("../approvals/index.cjs");
|
|
46
|
+
const { checklistsHostPlugin } = require("../checklists/index.cjs");
|
|
47
|
+
const { calendarHostPlugin } = require("../calendar/index.cjs");
|
|
48
|
+
const { locationPinsHostPlugin } = require("../location-pins/index.cjs");
|
|
49
|
+
const { crdtHostPlugin } = require("../crdt/index.cjs");
|
|
50
|
+
|
|
51
|
+
const DEFAULT_PLUGIN_ORDER = Object.freeze([
|
|
52
|
+
COMMENTS_PLUGIN_KEY,
|
|
53
|
+
PRESENCE_PLUGIN_KEY,
|
|
54
|
+
MEDIA_ROOMS_PLUGIN_KEY,
|
|
55
|
+
SCREEN_SHARE_PLUGIN_KEY,
|
|
56
|
+
MARKDOWN_PLUGIN_KEY,
|
|
57
|
+
EMBEDS_PLUGIN_KEY,
|
|
58
|
+
ATTACHMENTS_PLUGIN_KEY,
|
|
59
|
+
FILES_PLUGIN_KEY,
|
|
60
|
+
REACTIONS_PLUGIN_KEY,
|
|
61
|
+
LABELS_PLUGIN_KEY,
|
|
62
|
+
APPROVALS_PLUGIN_KEY,
|
|
63
|
+
CHECKLISTS_PLUGIN_KEY,
|
|
64
|
+
CALENDAR_PLUGIN_KEY,
|
|
65
|
+
LOCATION_PINS_PLUGIN_KEY
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
function record({ key, id, plugin, exportName, aliases }) {
|
|
69
|
+
return Object.freeze({ key, id, plugin, packageName: BASE_PLUGINS_PACKAGE, exportName, aliases: Object.freeze([key, id, ...(aliases || [])]) });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const PLUGIN_REGISTRY = Object.freeze({
|
|
73
|
+
[COMMENTS_PLUGIN_KEY]: record({ key: COMMENTS_PLUGIN_KEY, id: COMMENTS_PLUGIN_ID, plugin: commentsHostPlugin, exportName: "commentsHostPlugin", aliases: ["comment", "thread", "threads", "discussion", "discussions"] }),
|
|
74
|
+
[PRESENCE_PLUGIN_KEY]: record({ key: PRESENCE_PLUGIN_KEY, id: PRESENCE_PLUGIN_ID, plugin: presenceHostPlugin, exportName: "presenceHostPlugin", aliases: ["status", "online"] }),
|
|
75
|
+
[MEDIA_ROOMS_PLUGIN_KEY]: record({ key: MEDIA_ROOMS_PLUGIN_KEY, id: MEDIA_ROOMS_PLUGIN_ID, plugin: mediaRoomsHostPlugin, exportName: "mediaRoomsHostPlugin", aliases: ["media", "voice", "video", "rooms", "calls"] }),
|
|
76
|
+
[SCREEN_SHARE_PLUGIN_KEY]: record({ key: SCREEN_SHARE_PLUGIN_KEY, id: SCREEN_SHARE_PLUGIN_ID, plugin: screenShareHostPlugin, exportName: "screenShareHostPlugin", aliases: ["screenshare", "screen", "share", "presentation"] }),
|
|
77
|
+
[MARKDOWN_PLUGIN_KEY]: record({ key: MARKDOWN_PLUGIN_KEY, id: MARKDOWN_PLUGIN_ID, plugin: markdownHostPlugin, exportName: "markdownHostPlugin", aliases: ["md", "content", "richText", "rich-text"] }),
|
|
78
|
+
[EMBEDS_PLUGIN_KEY]: record({ key: EMBEDS_PLUGIN_KEY, id: EMBEDS_PLUGIN_ID, plugin: embedsHostPlugin, exportName: "embedsHostPlugin", aliases: ["embed", "links", "youtube", "spotify", "dropbox", "drive", "gdrive", "gphotos", "google-photos", "mediaLinks"] }),
|
|
79
|
+
[ATTACHMENTS_PLUGIN_KEY]: record({ key: ATTACHMENTS_PLUGIN_KEY, id: ATTACHMENTS_PLUGIN_ID, plugin: attachmentsHostPlugin, exportName: "attachmentsHostPlugin", aliases: ["attachment", "externalFiles", "cloudFiles", "cloud-files"] }),
|
|
80
|
+
[FILES_PLUGIN_KEY]: record({ key: FILES_PLUGIN_KEY, id: FILES_PLUGIN_ID, plugin: filesHostPlugin, exportName: "filesHostPlugin", aliases: ["file", "uploads", "upload", "nostrFiles", "nostr-files"] }),
|
|
81
|
+
[REACTIONS_PLUGIN_KEY]: record({ key: REACTIONS_PLUGIN_KEY, id: REACTIONS_PLUGIN_ID, plugin: reactionsHostPlugin, exportName: "reactionsHostPlugin", aliases: ["reaction", "emoji", "votes-light"] }),
|
|
82
|
+
[LABELS_PLUGIN_KEY]: record({ key: LABELS_PLUGIN_KEY, id: LABELS_PLUGIN_ID, plugin: labelsHostPlugin, exportName: "labelsHostPlugin", aliases: ["label", "tags", "tag", "taxonomy"] }),
|
|
83
|
+
[APPROVALS_PLUGIN_KEY]: record({ key: APPROVALS_PLUGIN_KEY, id: APPROVALS_PLUGIN_ID, plugin: approvalsHostPlugin, exportName: "approvalsHostPlugin", aliases: ["approval", "review", "reviews"] }),
|
|
84
|
+
[CHECKLISTS_PLUGIN_KEY]: record({ key: CHECKLISTS_PLUGIN_KEY, id: CHECKLISTS_PLUGIN_ID, plugin: checklistsHostPlugin, exportName: "checklistsHostPlugin", aliases: ["checklist", "todos", "todo", "check-items"] }),
|
|
85
|
+
[CALENDAR_PLUGIN_KEY]: record({ key: CALENDAR_PLUGIN_KEY, id: CALENDAR_PLUGIN_ID, plugin: calendarHostPlugin, exportName: "calendarHostPlugin", aliases: ["schedule", "timeline", "events"] }),
|
|
86
|
+
[LOCATION_PINS_PLUGIN_KEY]: record({ key: LOCATION_PINS_PLUGIN_KEY, id: LOCATION_PINS_PLUGIN_ID, plugin: locationPinsHostPlugin, exportName: "locationPinsHostPlugin", aliases: ["location", "map", "maps", "pin", "geo"] }),
|
|
87
|
+
[CRDT_PLUGIN_KEY]: record({ key: CRDT_PLUGIN_KEY, id: CRDT_PLUGIN_ID, plugin: crdtHostPlugin, exportName: "crdtHostPlugin", aliases: ["sharedDoc", "shared-doc", "collaborative-text"] })
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const PRESETS = Object.freeze({
|
|
91
|
+
none: Object.freeze([]),
|
|
92
|
+
comments: Object.freeze([COMMENTS_PLUGIN_KEY]),
|
|
93
|
+
discussion: Object.freeze([COMMENTS_PLUGIN_KEY, REACTIONS_PLUGIN_KEY]),
|
|
94
|
+
presence: Object.freeze([PRESENCE_PLUGIN_KEY]),
|
|
95
|
+
media: Object.freeze([MEDIA_ROOMS_PLUGIN_KEY, SCREEN_SHARE_PLUGIN_KEY]),
|
|
96
|
+
calls: Object.freeze([MEDIA_ROOMS_PLUGIN_KEY, SCREEN_SHARE_PLUGIN_KEY]),
|
|
97
|
+
content: Object.freeze([MARKDOWN_PLUGIN_KEY, EMBEDS_PLUGIN_KEY, FILES_PLUGIN_KEY]),
|
|
98
|
+
docs: Object.freeze([MARKDOWN_PLUGIN_KEY, EMBEDS_PLUGIN_KEY, FILES_PLUGIN_KEY, COMMENTS_PLUGIN_KEY, LABELS_PLUGIN_KEY]),
|
|
99
|
+
workflow: Object.freeze([CHECKLISTS_PLUGIN_KEY, APPROVALS_PLUGIN_KEY, CALENDAR_PLUGIN_KEY, LABELS_PLUGIN_KEY]),
|
|
100
|
+
collaboration: Object.freeze([COMMENTS_PLUGIN_KEY, PRESENCE_PLUGIN_KEY]),
|
|
101
|
+
project: Object.freeze([COMMENTS_PLUGIN_KEY, PRESENCE_PLUGIN_KEY, CHECKLISTS_PLUGIN_KEY, APPROVALS_PLUGIN_KEY, LABELS_PLUGIN_KEY]),
|
|
102
|
+
community: Object.freeze([COMMENTS_PLUGIN_KEY, PRESENCE_PLUGIN_KEY, MEDIA_ROOMS_PLUGIN_KEY, SCREEN_SHARE_PLUGIN_KEY, MARKDOWN_PLUGIN_KEY, EMBEDS_PLUGIN_KEY, REACTIONS_PLUGIN_KEY]),
|
|
103
|
+
all: DEFAULT_PLUGIN_ORDER
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
function freezeArray(values) { return Object.freeze(values.slice()); }
|
|
107
|
+
function unique(values) { return [...new Set(values.filter((value) => value !== undefined && value !== null))]; }
|
|
108
|
+
function registryRecords() { return Object.keys(PLUGIN_REGISTRY).map((key) => PLUGIN_REGISTRY[key]); }
|
|
109
|
+
|
|
110
|
+
function pluginKeyFor(value) {
|
|
111
|
+
if (!value) throw new Error("Plugin selection is required");
|
|
112
|
+
if (typeof value === "string") {
|
|
113
|
+
if (PLUGIN_REGISTRY[value]) return value;
|
|
114
|
+
if (PRESETS[value]) return PRESETS[value].slice();
|
|
115
|
+
const found = registryRecords().find((entry) => entry.aliases.includes(value));
|
|
116
|
+
if (found) return found.key;
|
|
117
|
+
throw new Error(`Unknown reusable plugin selection ${value}`);
|
|
118
|
+
}
|
|
119
|
+
const plugin = value.plugin || value;
|
|
120
|
+
if (plugin?.id) {
|
|
121
|
+
const found = registryRecords().find((entry) => entry.id === plugin.id);
|
|
122
|
+
if (found) return found.key;
|
|
123
|
+
}
|
|
124
|
+
throw new Error(`Unknown reusable plugin ${plugin?.id || typeof value}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
APPROVALS_PLUGIN_KEY,
|
|
129
|
+
ATTACHMENTS_PLUGIN_KEY,
|
|
130
|
+
CALENDAR_PLUGIN_KEY,
|
|
131
|
+
CHECKLISTS_PLUGIN_KEY,
|
|
132
|
+
COMMENTS_PLUGIN_KEY,
|
|
133
|
+
CRDT_PLUGIN_KEY,
|
|
134
|
+
DEFAULT_PLUGIN_ORDER,
|
|
135
|
+
EMBEDS_PLUGIN_KEY,
|
|
136
|
+
FILES_PLUGIN_KEY,
|
|
137
|
+
BASE_PLUGIN_VERSION,
|
|
138
|
+
LABELS_PLUGIN_KEY,
|
|
139
|
+
LOCATION_PINS_PLUGIN_KEY,
|
|
140
|
+
MARKDOWN_PLUGIN_KEY,
|
|
141
|
+
MEDIA_ROOMS_PLUGIN_KEY,
|
|
142
|
+
PLUGIN_REGISTRY,
|
|
143
|
+
PRESENCE_PLUGIN_KEY,
|
|
144
|
+
PRESETS,
|
|
145
|
+
REACTIONS_PLUGIN_KEY,
|
|
146
|
+
SCREEN_SHARE_PLUGIN_KEY,
|
|
147
|
+
commentsHostPlugin,
|
|
148
|
+
crdtHostPlugin,
|
|
149
|
+
freezeArray,
|
|
150
|
+
filesHostPlugin,
|
|
151
|
+
locationPinsHostPlugin,
|
|
152
|
+
pluginKeyFor,
|
|
153
|
+
registryRecords,
|
|
154
|
+
unique
|
|
155
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const { DEFAULT_PLUGIN_ORDER, PLUGIN_REGISTRY, pluginKeyFor } = require("./registry.cjs");
|
|
2
|
+
|
|
3
|
+
function normalizeSelection(selection = "all") {
|
|
4
|
+
const raw = Array.isArray(selection) ? selection : [selection];
|
|
5
|
+
const keys = [];
|
|
6
|
+
for (const item of raw) {
|
|
7
|
+
if (item === undefined || item === null || item === false) continue;
|
|
8
|
+
const normalized = pluginKeyFor(item);
|
|
9
|
+
if (Array.isArray(normalized)) keys.push(...normalized); else keys.push(normalized);
|
|
10
|
+
}
|
|
11
|
+
const requested = new Set(keys);
|
|
12
|
+
const ordered = DEFAULT_PLUGIN_ORDER.filter((key) => requested.has(key));
|
|
13
|
+
for (const key of keys) {
|
|
14
|
+
if (!ordered.includes(key)) ordered.push(key);
|
|
15
|
+
}
|
|
16
|
+
return ordered;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getReusablePlugin(selection) {
|
|
20
|
+
const key = pluginKeyFor(selection);
|
|
21
|
+
if (Array.isArray(key)) throw new Error(`${selection} resolves to multiple plugins; use createCollaborationPluginSuite instead`);
|
|
22
|
+
return PLUGIN_REGISTRY[key].plugin;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getReusablePluginEntry(selection, overrides = {}) {
|
|
26
|
+
const key = pluginKeyFor(selection);
|
|
27
|
+
if (Array.isArray(key)) throw new Error(`${selection} resolves to multiple plugins; use createCollaborationPluginSuite instead`);
|
|
28
|
+
const entry = PLUGIN_REGISTRY[key];
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
plugin: entry.plugin,
|
|
31
|
+
packageName: overrides.packageName || entry.packageName,
|
|
32
|
+
exportName: overrides.exportName || entry.exportName,
|
|
33
|
+
source: overrides.source,
|
|
34
|
+
dependsOn: overrides.dependsOn,
|
|
35
|
+
conflictsWith: overrides.conflictsWith
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { getReusablePlugin, getReusablePluginEntry, normalizeSelection };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const { DEFAULT_PLUGIN_ORDER, BASE_PLUGIN_VERSION, freezeArray, pluginKeyFor } = require("./registry.cjs");
|
|
2
|
+
const { summarizeCapabilities } = require("./capabilities.cjs");
|
|
3
|
+
const { getReusablePluginEntry, normalizeSelection } = require("./selection.cjs");
|
|
4
|
+
|
|
5
|
+
function createCollaborationPluginSuite(selection = "all", options = {}) {
|
|
6
|
+
const keys = normalizeSelection(selection);
|
|
7
|
+
const entries = keys.map((key) => getReusablePluginEntry(key));
|
|
8
|
+
const plugins = entries.map((entry) => entry.plugin);
|
|
9
|
+
const capabilities = summarizeCapabilities(plugins);
|
|
10
|
+
return Object.freeze({
|
|
11
|
+
kind: "matterhorn.example-plugin-suite",
|
|
12
|
+
id: options.id || `com.matterhorn.examples.plugins.suite.${keys.length ? keys.join("-") : "none"}`,
|
|
13
|
+
version: options.version || BASE_PLUGIN_VERSION,
|
|
14
|
+
selection: freezeArray(keys),
|
|
15
|
+
entries: freezeArray(entries),
|
|
16
|
+
plugins: freezeArray(plugins),
|
|
17
|
+
pluginIds: freezeArray(plugins.map((plugin) => plugin.id)),
|
|
18
|
+
capabilities,
|
|
19
|
+
hasPlugin(pluginOrKey) {
|
|
20
|
+
const key = pluginKeyFor(pluginOrKey);
|
|
21
|
+
if (Array.isArray(key)) return key.every((item) => keys.includes(item));
|
|
22
|
+
return keys.includes(key);
|
|
23
|
+
},
|
|
24
|
+
hasCapability(capability) { return capabilities.provides.includes(capability) || capabilities.required.includes(capability); }
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const collaborationPluginSuite = createCollaborationPluginSuite(DEFAULT_PLUGIN_ORDER);
|
|
29
|
+
const reusableHostPluginEntries = collaborationPluginSuite.entries;
|
|
30
|
+
const reusableHostPlugins = collaborationPluginSuite.plugins;
|
|
31
|
+
|
|
32
|
+
module.exports = { collaborationPluginSuite, createCollaborationPluginSuite, reusableHostPluginEntries, reusableHostPlugins };
|