ghost 5.43.0 → 5.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/tryghost-adapter-cache-memory-ttl-5.45.0.tgz +0 -0
- package/components/{tryghost-adapter-cache-redis-5.43.0.tgz → tryghost-adapter-cache-redis-5.45.0.tgz} +0 -0
- package/components/{tryghost-adapter-manager-5.43.0.tgz → tryghost-adapter-manager-5.45.0.tgz} +0 -0
- package/components/tryghost-announcement-bar-settings-5.45.0.tgz +0 -0
- package/components/{tryghost-api-framework-5.43.0.tgz → tryghost-api-framework-5.45.0.tgz} +0 -0
- package/components/{tryghost-api-version-compatibility-service-5.43.0.tgz → tryghost-api-version-compatibility-service-5.45.0.tgz} +0 -0
- package/components/tryghost-audience-feedback-5.45.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.45.0.tgz +0 -0
- package/components/tryghost-constants-5.45.0.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.43.0.tgz → tryghost-custom-theme-settings-service-5.45.0.tgz} +0 -0
- package/components/{tryghost-data-generator-5.43.0.tgz → tryghost-data-generator-5.45.0.tgz} +0 -0
- package/components/tryghost-domain-events-5.45.0.tgz +0 -0
- package/components/{tryghost-dynamic-routing-events-5.43.0.tgz → tryghost-dynamic-routing-events-5.45.0.tgz} +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.45.0.tgz +0 -0
- package/components/{tryghost-email-analytics-service-5.43.0.tgz → tryghost-email-analytics-service-5.45.0.tgz} +0 -0
- package/components/{tryghost-email-content-generator-5.43.0.tgz → tryghost-email-content-generator-5.45.0.tgz} +0 -0
- package/components/{tryghost-email-events-5.43.0.tgz → tryghost-email-events-5.45.0.tgz} +0 -0
- package/components/{tryghost-email-service-5.43.0.tgz → tryghost-email-service-5.45.0.tgz} +0 -0
- package/components/tryghost-email-suppression-list-5.45.0.tgz +0 -0
- package/components/{tryghost-event-aware-cache-wrapper-5.43.0.tgz → tryghost-event-aware-cache-wrapper-5.45.0.tgz} +0 -0
- package/components/{tryghost-express-dynamic-redirects-5.43.0.tgz → tryghost-express-dynamic-redirects-5.45.0.tgz} +0 -0
- package/components/{tryghost-external-media-inliner-5.43.0.tgz → tryghost-external-media-inliner-5.45.0.tgz} +0 -0
- package/components/{tryghost-extract-api-key-5.43.0.tgz → tryghost-extract-api-key-5.45.0.tgz} +0 -0
- package/components/{tryghost-html-to-plaintext-5.43.0.tgz → tryghost-html-to-plaintext-5.45.0.tgz} +0 -0
- package/components/tryghost-i18n-5.45.0.tgz +0 -0
- package/components/{tryghost-importer-handler-content-files-5.43.0.tgz → tryghost-importer-handler-content-files-5.45.0.tgz} +0 -0
- package/components/{tryghost-importer-revue-5.43.0.tgz → tryghost-importer-revue-5.45.0.tgz} +0 -0
- package/components/{tryghost-job-manager-5.43.0.tgz → tryghost-job-manager-5.45.0.tgz} +0 -0
- package/components/{tryghost-link-redirects-5.43.0.tgz → tryghost-link-redirects-5.45.0.tgz} +0 -0
- package/components/{tryghost-link-replacer-5.43.0.tgz → tryghost-link-replacer-5.45.0.tgz} +0 -0
- package/components/{tryghost-link-tracking-5.43.0.tgz → tryghost-link-tracking-5.45.0.tgz} +0 -0
- package/components/{tryghost-magic-link-5.43.0.tgz → tryghost-magic-link-5.45.0.tgz} +0 -0
- package/components/tryghost-mailgun-client-5.45.0.tgz +0 -0
- package/components/{tryghost-member-attribution-5.43.0.tgz → tryghost-member-attribution-5.45.0.tgz} +0 -0
- package/components/{tryghost-member-events-5.43.0.tgz → tryghost-member-events-5.45.0.tgz} +0 -0
- package/components/tryghost-members-api-5.45.0.tgz +0 -0
- package/components/{tryghost-members-csv-5.43.0.tgz → tryghost-members-csv-5.45.0.tgz} +0 -0
- package/components/{tryghost-members-events-service-5.43.0.tgz → tryghost-members-events-service-5.45.0.tgz} +0 -0
- package/components/{tryghost-members-importer-5.43.0.tgz → tryghost-members-importer-5.45.0.tgz} +0 -0
- package/components/{tryghost-members-offers-5.43.0.tgz → tryghost-members-offers-5.45.0.tgz} +0 -0
- package/components/{tryghost-members-payments-5.43.0.tgz → tryghost-members-payments-5.45.0.tgz} +0 -0
- package/components/{tryghost-members-ssr-5.43.0.tgz → tryghost-members-ssr-5.45.0.tgz} +0 -0
- package/components/{tryghost-members-stripe-service-5.43.0.tgz → tryghost-members-stripe-service-5.45.0.tgz} +0 -0
- package/components/{tryghost-mentions-email-report-5.43.0.tgz → tryghost-mentions-email-report-5.45.0.tgz} +0 -0
- package/components/tryghost-milestones-5.45.0.tgz +0 -0
- package/components/{tryghost-minifier-5.43.0.tgz → tryghost-minifier-5.45.0.tgz} +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.45.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.45.0.tgz +0 -0
- package/components/{tryghost-mw-error-handler-5.43.0.tgz → tryghost-mw-error-handler-5.45.0.tgz} +0 -0
- package/components/{tryghost-mw-session-from-token-5.43.0.tgz → tryghost-mw-session-from-token-5.45.0.tgz} +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.45.0.tgz +0 -0
- package/components/{tryghost-mw-version-match-5.43.0.tgz → tryghost-mw-version-match-5.45.0.tgz} +0 -0
- package/components/tryghost-mw-vhost-5.45.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.45.0.tgz +0 -0
- package/components/tryghost-package-json-5.45.0.tgz +0 -0
- package/components/tryghost-post-revisions-5.45.0.tgz +0 -0
- package/components/tryghost-posts-service-5.45.0.tgz +0 -0
- package/components/tryghost-referrers-5.45.0.tgz +0 -0
- package/components/{tryghost-security-5.43.0.tgz → tryghost-security-5.45.0.tgz} +0 -0
- package/components/tryghost-session-service-5.45.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.45.0.tgz +0 -0
- package/components/tryghost-slack-notifications-5.45.0.tgz +0 -0
- package/components/{tryghost-staff-service-5.43.0.tgz → tryghost-staff-service-5.45.0.tgz} +0 -0
- package/components/{tryghost-stats-service-5.43.0.tgz → tryghost-stats-service-5.45.0.tgz} +0 -0
- package/components/{tryghost-tiers-5.43.0.tgz → tryghost-tiers-5.45.0.tgz} +0 -0
- package/components/{tryghost-update-check-service-5.43.0.tgz → tryghost-update-check-service-5.45.0.tgz} +0 -0
- package/components/{tryghost-verification-trigger-5.43.0.tgz → tryghost-verification-trigger-5.45.0.tgz} +0 -0
- package/components/tryghost-version-notifications-data-service-5.45.0.tgz +0 -0
- package/components/{tryghost-webmentions-5.43.0.tgz → tryghost-webmentions-5.45.0.tgz} +0 -0
- package/content/themes/casper/assets/built/global.css +1 -1
- package/content/themes/casper/assets/built/global.css.map +1 -1
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/screen.css +1 -0
- package/content/themes/casper/package.json +1 -1
- package/core/boot.js +4 -0
- package/core/built/admin/assets/{chunk.234.b9c875c2897cfa768789.js → chunk.11.28a4582ffea7775d1149.js} +3242 -2666
- package/core/built/admin/assets/{chunk.234.b9c875c2897cfa768789.js.LICENSE.txt → chunk.11.28a4582ffea7775d1149.js.LICENSE.txt} +6 -0
- package/core/built/admin/assets/chunk.143.7a235ab8ccdad1dcbf28.js +35 -0
- package/core/built/admin/assets/{chunk.178.34b3da4d314e03aecc4f.js → chunk.178.0e17fa3600642a55e384.js} +4 -4
- package/core/built/admin/assets/ghost-dark-5021e708aa7f2df5e1d87f868f62766d.css +1 -0
- package/core/built/admin/assets/ghost-ebf85cd63feaffb755a1daad9893a964.css +1 -0
- package/core/built/admin/assets/{ghost-0fff0a0f3ed3022b69003f7766050cb4.js → ghost-fe1350703afcb2f9ea83b42cfd969e03.js} +550 -340
- package/core/built/admin/assets/img/pintura-75cf0fd7cbc6ca3c8fbdb3f1b31f730b.png +0 -0
- package/core/built/admin/assets/img/pintura-screenshot-7668975389aac80607ec489b48b75e0f.png +0 -0
- package/core/built/admin/assets/{vendor-da13470386c6ee05178d49830c64d7ab.js → vendor-2c96c9001fe1dccd9b16b7642357da0e.js} +49 -41
- package/core/built/admin/index.html +6 -6
- package/core/frontend/helpers/ghost_head.js +18 -0
- package/core/server/api/endpoints/pages.js +50 -0
- package/core/server/api/endpoints/posts.js +11 -5
- package/core/server/api/endpoints/settings-public.js +15 -5
- package/core/server/api/endpoints/settings.js +1 -1
- package/core/server/api/endpoints/snippets.js +18 -2
- package/core/server/api/endpoints/utils/serializers/input/pages.js +8 -0
- package/core/server/api/endpoints/utils/serializers/input/posts.js +1 -1
- package/core/server/api/endpoints/utils/serializers/input/settings.js +7 -1
- package/core/server/api/endpoints/utils/serializers/input/utils/url.js +1 -1
- package/core/server/api/endpoints/utils/serializers/output/config.js +2 -1
- package/core/server/api/endpoints/utils/serializers/output/mappers/snippets.js +2 -0
- package/core/server/api/endpoints/utils/serializers/output/pages.js +20 -0
- package/core/server/api/endpoints/utils/validators/input/settings.js +41 -2
- package/core/server/data/migrations/utils/settings.js +8 -3
- package/core/server/data/migrations/versions/5.44/2023-04-14-04-17-add-snippets-lexical-column.js +8 -0
- package/core/server/data/migrations/versions/5.45/2023-04-17-11-05-add-post-revision-author.js +22 -0
- package/core/server/data/migrations/versions/5.45/2023-04-18-12-56-add-announcement-settings.js +18 -0
- package/core/server/data/migrations/versions/5.45/2023-04-19-13-45-add-pintura-settings.js +22 -0
- package/core/server/data/migrations/versions/5.45/2023-04-20-14-19-add-announcement-visibility-setting.js +32 -0
- package/core/server/data/migrations/versions/5.45/2023-04-21-08-54-add-post-revision-status.js +17 -0
- package/core/server/data/migrations/versions/5.45/2023-04-21-10-30-add-feature-image-to-revisions.js +7 -0
- package/core/server/data/migrations/versions/5.45/2023-04-21-13-01-add-feature-image-meta-to-post-revisions.js +14 -0
- package/core/server/data/schema/default-settings/default-settings.json +43 -0
- package/core/server/data/schema/schema.js +9 -1
- package/core/server/models/post-revision.js +16 -3
- package/core/server/models/post.js +79 -34
- package/core/server/models/settings.js +2 -2
- package/core/server/models/snippet.js +36 -0
- package/core/server/services/announcement-bar-service/index.js +12 -0
- package/core/server/services/lexical-multiplayer/index.js +1 -0
- package/core/server/services/lexical-multiplayer/service.js +141 -0
- package/core/server/services/lexical-multiplayer/y-websocket.js +244 -0
- package/core/server/services/public-config/config.js +2 -1
- package/core/server/services/settings/settings-bread-service.js +15 -0
- package/core/server/services/users.js +16 -0
- package/core/server/web/api/endpoints/admin/middleware.js +2 -1
- package/core/server/web/api/endpoints/admin/routes.js +2 -0
- package/core/server/web/api/endpoints/content/routes.js +2 -1
- package/core/shared/config/defaults.json +4 -0
- package/core/shared/labs.js +6 -1
- package/core/shared/settings-cache/cache.js +1 -1
- package/package.json +141 -133
- package/yarn.lock +92 -37
- package/components/tryghost-adapter-cache-memory-ttl-5.43.0.tgz +0 -0
- package/components/tryghost-audience-feedback-5.43.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.43.0.tgz +0 -0
- package/components/tryghost-constants-5.43.0.tgz +0 -0
- package/components/tryghost-domain-events-5.43.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.43.0.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.43.0.tgz +0 -0
- package/components/tryghost-i18n-5.43.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.43.0.tgz +0 -0
- package/components/tryghost-members-api-5.43.0.tgz +0 -0
- package/components/tryghost-milestones-5.43.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.43.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.43.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.43.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.43.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.43.0.tgz +0 -0
- package/components/tryghost-package-json-5.43.0.tgz +0 -0
- package/components/tryghost-posts-service-5.43.0.tgz +0 -0
- package/components/tryghost-referrers-5.43.0.tgz +0 -0
- package/components/tryghost-session-service-5.43.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.43.0.tgz +0 -0
- package/components/tryghost-slack-notifications-5.43.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.43.0.tgz +0 -0
- package/core/built/admin/assets/chunk.143.726fc7d5add7caf3e2e7.js +0 -35
- package/core/built/admin/assets/ghost-b84e41170d69676f9f8a688c76906a6d.css +0 -1
- package/core/built/admin/assets/ghost-dark-bd0c63ccb9c91035a7daa6bdf2b8553c.css +0 -1
|
@@ -19,6 +19,8 @@ const urlUtils = require('../../shared/url-utils');
|
|
|
19
19
|
const {Tag} = require('./tag');
|
|
20
20
|
const {Newsletter} = require('./newsletter');
|
|
21
21
|
const {BadRequestError} = require('@tryghost/errors');
|
|
22
|
+
const PostRevisions = require('@tryghost/post-revisions');
|
|
23
|
+
const labs = require('../../shared/labs');
|
|
22
24
|
|
|
23
25
|
const messages = {
|
|
24
26
|
isAlreadyPublished: 'Your post is already published, please reload your page.',
|
|
@@ -341,6 +343,11 @@ Post = ghostBookshelf.Model.extend({
|
|
|
341
343
|
tableName: 'posts_meta',
|
|
342
344
|
type: 'oneToOne',
|
|
343
345
|
joinFrom: 'post_id'
|
|
346
|
+
},
|
|
347
|
+
post_revisions: {
|
|
348
|
+
tableName: 'post_revisions',
|
|
349
|
+
type: 'oneToMany',
|
|
350
|
+
joinFrom: 'post_id'
|
|
344
351
|
}
|
|
345
352
|
};
|
|
346
353
|
},
|
|
@@ -862,37 +869,76 @@ Post = ghostBookshelf.Model.extend({
|
|
|
862
869
|
});
|
|
863
870
|
}
|
|
864
871
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
872
|
+
if (!labs.isSet('postHistory')) {
|
|
873
|
+
if (model.hasChanged('lexical') && !model.get('mobiledoc') && !options.importing && !options.migrating) {
|
|
874
|
+
ops.push(function updateRevisions() {
|
|
875
|
+
return ghostBookshelf.model('PostRevision')
|
|
876
|
+
.findAll(Object.assign({
|
|
877
|
+
filter: `post_id:${model.id}`,
|
|
878
|
+
columns: ['id']
|
|
879
|
+
}, _.pick(options, 'transacting')))
|
|
880
|
+
.then((revisions) => {
|
|
881
|
+
// Store previous + latest lexical content
|
|
882
|
+
if (!revisions.length && options.method !== 'insert') {
|
|
883
|
+
model.set('post_revisions', [{
|
|
884
|
+
post_id: model.id,
|
|
885
|
+
lexical: model.previous('lexical'),
|
|
886
|
+
created_at_ts: Date.now() - 1
|
|
887
|
+
}, {
|
|
888
|
+
post_id: model.id,
|
|
889
|
+
lexical: model.get('lexical'),
|
|
890
|
+
created_at_ts: Date.now()
|
|
891
|
+
}]);
|
|
892
|
+
} else {
|
|
893
|
+
const revisionsJSON = revisions.toJSON().slice(0, POST_REVISIONS_COUNT - 1);
|
|
894
|
+
|
|
895
|
+
model.set('post_revisions', revisionsJSON.concat([{
|
|
896
|
+
post_id: model.id,
|
|
897
|
+
lexical: model.get('lexical'),
|
|
898
|
+
created_at_ts: Date.now()
|
|
899
|
+
}]));
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
} else {
|
|
905
|
+
if (!model.get('mobiledoc') && !options.importing && !options.migrating) {
|
|
906
|
+
const postRevisions = new PostRevisions({
|
|
907
|
+
config: {
|
|
908
|
+
max_revisions: POST_REVISIONS_COUNT
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
const authorId = this.contextUser(options);
|
|
912
|
+
ops.push(async function updateRevisions() {
|
|
913
|
+
const revisionModels = await ghostBookshelf.model('PostRevision')
|
|
914
|
+
.findAll(Object.assign({
|
|
915
|
+
filter: `post_id:${model.id}`,
|
|
916
|
+
columns: ['id', 'lexical', 'created_at', 'author_id', 'title', 'reason', 'post_status']
|
|
917
|
+
}, _.pick(options, 'transacting')));
|
|
918
|
+
|
|
919
|
+
const revisions = revisionModels.toJSON();
|
|
920
|
+
|
|
921
|
+
const current = {
|
|
922
|
+
id: model.id,
|
|
923
|
+
lexical: model.get('lexical'),
|
|
924
|
+
html: model.get('html'),
|
|
925
|
+
author_id: authorId,
|
|
926
|
+
feature_image: model.get('feature_image'),
|
|
927
|
+
feature_image_alt: model.get('posts_meta')?.feature_image_alt,
|
|
928
|
+
feature_image_caption: model.get('posts_meta')?.feature_image_caption,
|
|
929
|
+
title: model.get('title'),
|
|
930
|
+
post_status: model.get('status')
|
|
931
|
+
};
|
|
887
932
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
933
|
+
// This can be refactored once we have the status stored in each revision
|
|
934
|
+
const revisionOptions = {
|
|
935
|
+
forceRevision: options.save_revision,
|
|
936
|
+
isPublished: newStatus === 'published'
|
|
937
|
+
};
|
|
938
|
+
const newRevisions = await postRevisions.getRevisions(current, revisions, revisionOptions);
|
|
939
|
+
model.set('post_revisions', newRevisions);
|
|
940
|
+
});
|
|
941
|
+
}
|
|
896
942
|
}
|
|
897
943
|
|
|
898
944
|
if (this.get('tiers')) {
|
|
@@ -1004,9 +1050,8 @@ Post = ghostBookshelf.Model.extend({
|
|
|
1004
1050
|
|
|
1005
1051
|
attrs = this.formatsToJSON(attrs, options);
|
|
1006
1052
|
|
|
1007
|
-
// CASE: never expose the revisions
|
|
1053
|
+
// CASE: never expose the mobiledoc revisions
|
|
1008
1054
|
delete attrs.mobiledoc_revisions;
|
|
1009
|
-
delete attrs.post_revisions;
|
|
1010
1055
|
|
|
1011
1056
|
// If the current column settings allow it...
|
|
1012
1057
|
if (!options.columns || (options.columns && options.columns.indexOf('primary_tag') > -1)) {
|
|
@@ -1128,7 +1173,7 @@ Post = ghostBookshelf.Model.extend({
|
|
|
1128
1173
|
findPage: ['status'],
|
|
1129
1174
|
findAll: ['columns', 'filter'],
|
|
1130
1175
|
destroy: ['destroyAll', 'destroyBy'],
|
|
1131
|
-
edit: ['filter', 'email_segment', 'force_rerender', 'newsletter']
|
|
1176
|
+
edit: ['filter', 'email_segment', 'force_rerender', 'newsletter', 'save_revision']
|
|
1132
1177
|
};
|
|
1133
1178
|
|
|
1134
1179
|
// The post model additionally supports having a formats option
|
|
@@ -1151,7 +1196,7 @@ Post = ghostBookshelf.Model.extend({
|
|
|
1151
1196
|
*/
|
|
1152
1197
|
defaultRelations: function defaultRelations(methodName, options) {
|
|
1153
1198
|
if (['edit', 'add', 'destroy'].indexOf(methodName) !== -1) {
|
|
1154
|
-
options.withRelated = _.union(['authors', 'tags'], options.withRelated || []);
|
|
1199
|
+
options.withRelated = _.union(['authors', 'tags', 'post_revisions', 'post_revisions.author'], options.withRelated || []);
|
|
1155
1200
|
}
|
|
1156
1201
|
|
|
1157
1202
|
const META_ATTRIBUTES = _.without(ghostBookshelf.model('PostsMeta').prototype.permittedAttributes(), 'id', 'post_id');
|
|
@@ -161,7 +161,7 @@ Settings = ghostBookshelf.Model.extend({
|
|
|
161
161
|
},
|
|
162
162
|
|
|
163
163
|
formatOnWrite(attrs) {
|
|
164
|
-
if (attrs.value && ['cover_image', 'logo', 'icon', 'portal_button_icon', 'og_image', 'twitter_image'].includes(attrs.key)) {
|
|
164
|
+
if (attrs.value && ['cover_image', 'logo', 'icon', 'portal_button_icon', 'og_image', 'twitter_image', 'pintura_js_url', 'pintura_css_url'].includes(attrs.key)) {
|
|
165
165
|
attrs.value = urlUtils.toTransformReady(attrs.value);
|
|
166
166
|
}
|
|
167
167
|
|
|
@@ -183,7 +183,7 @@ Settings = ghostBookshelf.Model.extend({
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
// transform URLs from __GHOST_URL__ to absolute
|
|
186
|
-
if (['cover_image', 'logo', 'icon', 'portal_button_icon', 'og_image', 'twitter_image'].includes(attrs.key)) {
|
|
186
|
+
if (['cover_image', 'logo', 'icon', 'portal_button_icon', 'og_image', 'twitter_image', 'pintura_js_url', 'pintura_css_url'].includes(attrs.key)) {
|
|
187
187
|
attrs.value = urlUtils.transformReadyToAbsolute(attrs.value);
|
|
188
188
|
}
|
|
189
189
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const ghostBookshelf = require('./base');
|
|
2
2
|
const urlUtils = require('../../shared/url-utils');
|
|
3
3
|
const mobiledocLib = require('../lib/mobiledoc');
|
|
4
|
+
const lexicalLib = require('../lib/lexical');
|
|
5
|
+
const _ = require('lodash');
|
|
4
6
|
|
|
5
7
|
const Snippet = ghostBookshelf.Model.extend({
|
|
6
8
|
tableName: 'snippets',
|
|
@@ -10,6 +12,13 @@ const Snippet = ghostBookshelf.Model.extend({
|
|
|
10
12
|
attrs.mobiledoc = urlUtils.mobiledocToTransformReady(attrs.mobiledoc, {cardTransformers: mobiledocLib.cards});
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
if (attrs.lexical) {
|
|
16
|
+
attrs.lexical = urlUtils.lexicalToTransformReady(attrs.lexical, {
|
|
17
|
+
nodes: lexicalLib.nodes,
|
|
18
|
+
transformMap: lexicalLib.urlTransformMap
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
return attrs;
|
|
14
23
|
},
|
|
15
24
|
|
|
@@ -20,8 +29,35 @@ const Snippet = ghostBookshelf.Model.extend({
|
|
|
20
29
|
attrs.mobiledoc = urlUtils.transformReadyToAbsolute(attrs.mobiledoc);
|
|
21
30
|
}
|
|
22
31
|
|
|
32
|
+
if (attrs.lexical) {
|
|
33
|
+
attrs.lexical = urlUtils.transformReadyToAbsolute(attrs.lexical);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return attrs;
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
formatsToJSON: function formatsToJSON(attrs, options) {
|
|
40
|
+
const defaultFormats = ['mobiledoc'];
|
|
41
|
+
const formatsToKeep = options.formats || defaultFormats;
|
|
42
|
+
|
|
43
|
+
// Iterate over all known formats, and if they are not in the keep list, remove them
|
|
44
|
+
_.each(Snippet.allowedFormats, function (format) {
|
|
45
|
+
if (formatsToKeep.indexOf(format) === -1) {
|
|
46
|
+
delete attrs[format];
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return attrs;
|
|
51
|
+
},
|
|
52
|
+
toJSON: function toJSON(options) {
|
|
53
|
+
let attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
|
|
54
|
+
|
|
55
|
+
attrs = this.formatsToJSON(attrs, options);
|
|
56
|
+
|
|
23
57
|
return attrs;
|
|
24
58
|
}
|
|
59
|
+
}, {
|
|
60
|
+
allowedFormats: ['mobiledoc', 'lexical']
|
|
25
61
|
});
|
|
26
62
|
|
|
27
63
|
const Snippets = ghostBookshelf.Collection.extend({
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const settingsCache = require('../../../shared/settings-cache');
|
|
2
|
+
const AnnouncementBarSettings = require('@tryghost/announcement-bar-settings');
|
|
3
|
+
|
|
4
|
+
const announcementBarService = new AnnouncementBarSettings({
|
|
5
|
+
getAnnouncementSettings: () => ({
|
|
6
|
+
announcement: settingsCache.get('announcement_content'),
|
|
7
|
+
announcement_background: settingsCache.get('announcement_background'),
|
|
8
|
+
announcement_visibility: settingsCache.get('announcement_visibility')
|
|
9
|
+
})
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
module.exports = announcementBarService;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./service');
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const debug = require('@tryghost/debug')('lexical-multiplayer'); // eslint-disable-line no-unused-vars
|
|
2
|
+
const logging = require('@tryghost/logging');
|
|
3
|
+
const {getSession} = require('../auth/session/express-session');
|
|
4
|
+
const models = require('../../models');
|
|
5
|
+
const labs = require('../../../shared/labs');
|
|
6
|
+
|
|
7
|
+
let wss;
|
|
8
|
+
|
|
9
|
+
const onSocketError = (error) => {
|
|
10
|
+
logging.error(error);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const onUnauthorized = (socket) => {
|
|
14
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
15
|
+
socket.destroy();
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const handleUpgrade = async (request, socket, head) => {
|
|
19
|
+
socket.on('error', onSocketError);
|
|
20
|
+
|
|
21
|
+
// make sure the request is on the supported path
|
|
22
|
+
// TODO: check handling of subdirectories
|
|
23
|
+
if (!request.url.startsWith('/ghost/api/admin/posts/multiplayer/')) {
|
|
24
|
+
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
|
|
25
|
+
socket.destroy();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// grab the session from the request
|
|
30
|
+
const session = await getSession(request, {});
|
|
31
|
+
if (!session || !session.user_id) {
|
|
32
|
+
return onUnauthorized(socket);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// fetch the session's user from the db
|
|
36
|
+
const user = await models.User.findOne({id: session.user_id});
|
|
37
|
+
if (!user) {
|
|
38
|
+
return onUnauthorized(socket);
|
|
39
|
+
}
|
|
40
|
+
request.user = user;
|
|
41
|
+
|
|
42
|
+
// TODO: check if user has access to the post
|
|
43
|
+
|
|
44
|
+
// TODO: (elsewhere) close websocket connections on logout
|
|
45
|
+
// - probably need to create a map of sockets to users?
|
|
46
|
+
|
|
47
|
+
socket.removeListener('error', onSocketError);
|
|
48
|
+
|
|
49
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
50
|
+
wss.emit('connection', ws, request);
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let _enable;
|
|
55
|
+
let _disable;
|
|
56
|
+
let _isClosing = false;
|
|
57
|
+
let _closePromise;
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
async init(ghostServer) {
|
|
61
|
+
_enable = async () => {
|
|
62
|
+
if (_isClosing) {
|
|
63
|
+
logging.info('Waiting for previous Lexical multiplayer websockets service to close');
|
|
64
|
+
await _closePromise;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (wss) {
|
|
68
|
+
logging.info('Lexical multiplayer websockets service already started');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (labs.isSet('lexicalMultiplayer')) {
|
|
73
|
+
logging.info('Starting lexical multiplayer websockets service');
|
|
74
|
+
|
|
75
|
+
// TODO: can we use or adapt patterns from https://github.com/HenningM/express-ws?
|
|
76
|
+
const WS = require('ws');
|
|
77
|
+
wss = new WS.Server({noServer: true});
|
|
78
|
+
const {setupWSConnection} = require('./y-websocket');
|
|
79
|
+
|
|
80
|
+
wss.on('connection', (socket, request) => {
|
|
81
|
+
socket.on('error', onSocketError);
|
|
82
|
+
|
|
83
|
+
// TODO: better method for extracting doc name from URL
|
|
84
|
+
const docName = request.url.replace('/ghost/api/admin/posts/multiplayer/', '');
|
|
85
|
+
setupWSConnection(socket, request, {docName});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// TODO: this should probably be at a higher level, especially if we
|
|
89
|
+
// want to support multiple websocket services
|
|
90
|
+
ghostServer.httpServer.on('upgrade', handleUpgrade);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
_disable = async () => {
|
|
95
|
+
logging.info('Stopping lexical multiplayer websockets service');
|
|
96
|
+
ghostServer.httpServer.off('upgrade', handleUpgrade);
|
|
97
|
+
|
|
98
|
+
if (wss) {
|
|
99
|
+
_isClosing = true;
|
|
100
|
+
_closePromise = new Promise((resolve) => {
|
|
101
|
+
// first sweep, soft close
|
|
102
|
+
wss.clients.forEach((socket) => {
|
|
103
|
+
socket.close();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
// second sweep, hard close
|
|
108
|
+
wss.clients.forEach((socket) => {
|
|
109
|
+
if ([socket.OPEN, socket.CLOSING].includes(socket.readyState)) {
|
|
110
|
+
socket.terminate();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
resolve();
|
|
115
|
+
}, 5000);
|
|
116
|
+
}).finally(() => {
|
|
117
|
+
wss = null;
|
|
118
|
+
_isClosing = false;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return _closePromise;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
async enable() {
|
|
127
|
+
if (!_enable) {
|
|
128
|
+
logging.error('Lexical multiplayer service must be initialized before it can be enabled/disabled');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
return _enable();
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
async disable() {
|
|
135
|
+
if (!_enable) {
|
|
136
|
+
logging.error('Lexical multiplayer service must be initialized before it can be enabled/disabled');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
return _disable();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// based on https://github.com/yjs/y-websocket/blob/master/bin/utils.js
|
|
2
|
+
|
|
3
|
+
const Y = require('yjs');
|
|
4
|
+
const syncProtocol = require('y-protocols/dist/sync.cjs');
|
|
5
|
+
const awarenessProtocol = require('y-protocols/dist/awareness.cjs');
|
|
6
|
+
|
|
7
|
+
const encoding = require('lib0/dist/encoding.cjs');
|
|
8
|
+
const decoding = require('lib0/dist/decoding.cjs');
|
|
9
|
+
const map = require('lib0/dist/map.cjs');
|
|
10
|
+
|
|
11
|
+
const wsReadyStateConnecting = 0;
|
|
12
|
+
const wsReadyStateOpen = 1;
|
|
13
|
+
const wsReadyStateClosing = 2 // eslint-disable-line
|
|
14
|
+
const wsReadyStateClosed = 3 // eslint-disable-line
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @type {Map<string,WSSharedDoc>}
|
|
18
|
+
*/
|
|
19
|
+
const docs = new Map();
|
|
20
|
+
module.exports.docs = docs;
|
|
21
|
+
|
|
22
|
+
const messageSync = 0;
|
|
23
|
+
const messageAwareness = 1;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {Uint8Array} update
|
|
27
|
+
* @param {any} origin
|
|
28
|
+
* @param {WSSharedDoc} doc
|
|
29
|
+
*/
|
|
30
|
+
const updateHandler = (update, origin, doc) => {
|
|
31
|
+
const encoder = encoding.createEncoder();
|
|
32
|
+
encoding.writeVarUint(encoder, messageSync);
|
|
33
|
+
syncProtocol.writeUpdate(encoder, update);
|
|
34
|
+
const message = encoding.toUint8Array(encoder);
|
|
35
|
+
doc.conns.forEach((_, conn) => send(doc, conn, message));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
class WSSharedDoc extends Y.Doc {
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} name
|
|
41
|
+
*/
|
|
42
|
+
constructor(name) {
|
|
43
|
+
super({gc: true});
|
|
44
|
+
this.name = name;
|
|
45
|
+
/**
|
|
46
|
+
* Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed
|
|
47
|
+
* @type {Map<Object, Set<number>>}
|
|
48
|
+
*/
|
|
49
|
+
this.conns = new Map();
|
|
50
|
+
/**
|
|
51
|
+
* @type {awarenessProtocol.Awareness}
|
|
52
|
+
*/
|
|
53
|
+
this.awareness = new awarenessProtocol.Awareness(this);
|
|
54
|
+
this.awareness.setLocalState(null);
|
|
55
|
+
/**
|
|
56
|
+
* @param {{ added: Array<number>, updated: Array<number>, removed: Array<number> }} changes
|
|
57
|
+
* @param {Object | null} conn Origin is the connection that made the change
|
|
58
|
+
*/
|
|
59
|
+
const awarenessChangeHandler = ({added, updated, removed}, conn) => {
|
|
60
|
+
const changedClients = added.concat(updated, removed);
|
|
61
|
+
if (conn !== null) {
|
|
62
|
+
const connControlledIDs = /** @type {Set<number>} */ (this.conns.get(conn));
|
|
63
|
+
if (connControlledIDs !== undefined) {
|
|
64
|
+
added.forEach((clientID) => {
|
|
65
|
+
connControlledIDs.add(clientID);
|
|
66
|
+
});
|
|
67
|
+
removed.forEach((clientID) => {
|
|
68
|
+
connControlledIDs.delete(clientID);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// broadcast awareness update
|
|
73
|
+
const encoder = encoding.createEncoder();
|
|
74
|
+
encoding.writeVarUint(encoder, messageAwareness);
|
|
75
|
+
encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients));
|
|
76
|
+
const buff = encoding.toUint8Array(encoder);
|
|
77
|
+
this.conns.forEach((_, c) => {
|
|
78
|
+
send(this, c, buff);
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
this.awareness.on('update', awarenessChangeHandler);
|
|
82
|
+
this.on('update', updateHandler);
|
|
83
|
+
// if (isCallbackSet) {
|
|
84
|
+
// this.on('update', debounce(
|
|
85
|
+
// callbackHandler,
|
|
86
|
+
// CALLBACK_DEBOUNCE_WAIT,
|
|
87
|
+
// { maxWait: CALLBACK_DEBOUNCE_MAXWAIT }
|
|
88
|
+
// ))
|
|
89
|
+
// }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Gets a Y.Doc by name, whether in memory or on disk
|
|
95
|
+
*
|
|
96
|
+
* @param {string} docname - the name of the Y.Doc to find or create
|
|
97
|
+
* @param {boolean} gc - whether to allow gc on the doc (applies only when created)
|
|
98
|
+
* @return {WSSharedDoc}
|
|
99
|
+
*/
|
|
100
|
+
const getYDoc = (docname, gc = true) => map.setIfUndefined(docs, docname, () => {
|
|
101
|
+
const doc = new WSSharedDoc(docname);
|
|
102
|
+
doc.gc = gc;
|
|
103
|
+
// if (persistence !== null) {
|
|
104
|
+
// persistence.bindState(docname, doc);
|
|
105
|
+
// }
|
|
106
|
+
docs.set(docname, doc);
|
|
107
|
+
return doc;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
module.exports.getYDoc = getYDoc;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {any} conn
|
|
114
|
+
* @param {WSSharedDoc} doc
|
|
115
|
+
* @param {Uint8Array} message
|
|
116
|
+
*/
|
|
117
|
+
const messageListener = (conn, doc, message) => {
|
|
118
|
+
try {
|
|
119
|
+
const encoder = encoding.createEncoder();
|
|
120
|
+
const decoder = decoding.createDecoder(message);
|
|
121
|
+
const messageType = decoding.readVarUint(decoder);
|
|
122
|
+
switch (messageType) {
|
|
123
|
+
case messageSync:
|
|
124
|
+
encoding.writeVarUint(encoder, messageSync);
|
|
125
|
+
syncProtocol.readSyncMessage(decoder, encoder, doc, conn);
|
|
126
|
+
|
|
127
|
+
// If the `encoder` only contains the type of reply message and no
|
|
128
|
+
// message, there is no need to send the message. When `encoder` only
|
|
129
|
+
// contains the type of reply, its length is 1.
|
|
130
|
+
if (encoding.length(encoder) > 1) {
|
|
131
|
+
send(doc, conn, encoding.toUint8Array(encoder));
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
case messageAwareness: {
|
|
135
|
+
awarenessProtocol.applyAwarenessUpdate(doc.awareness, decoding.readVarUint8Array(decoder), conn);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
doc.emit('error', [err]);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @param {WSSharedDoc} doc
|
|
146
|
+
* @param {any} conn
|
|
147
|
+
*/
|
|
148
|
+
const closeConn = (doc, conn) => {
|
|
149
|
+
if (doc.conns.has(conn)) {
|
|
150
|
+
/**
|
|
151
|
+
* @type {Set<number>}
|
|
152
|
+
*/
|
|
153
|
+
const controlledIds = doc.conns.get(conn);
|
|
154
|
+
doc.conns.delete(conn);
|
|
155
|
+
awarenessProtocol.removeAwarenessStates(doc.awareness, Array.from(controlledIds), null);
|
|
156
|
+
if (doc.conns.size === 0/* && persistence !== null*/) {
|
|
157
|
+
// if persisted, we store state and destroy ydocument
|
|
158
|
+
// persistence.writeState(doc.name, doc).then(() => {
|
|
159
|
+
// doc.destroy();
|
|
160
|
+
// });
|
|
161
|
+
docs.delete(doc.name);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
conn.close();
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {WSSharedDoc} doc
|
|
169
|
+
* @param {any} conn
|
|
170
|
+
* @param {Uint8Array} m
|
|
171
|
+
*/
|
|
172
|
+
const send = (doc, conn, m) => {
|
|
173
|
+
if (conn.readyState !== wsReadyStateConnecting && conn.readyState !== wsReadyStateOpen) {
|
|
174
|
+
closeConn(doc, conn);
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
conn.send(m, (err) => {
|
|
178
|
+
if (err !== null && err !== undefined) {
|
|
179
|
+
closeConn(doc, conn);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
} catch (e) {
|
|
183
|
+
closeConn(doc, conn);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const pingTimeout = 30000;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {any} conn
|
|
191
|
+
* @param {any} req
|
|
192
|
+
* @param {any} opts
|
|
193
|
+
*/
|
|
194
|
+
module.exports.setupWSConnection = (conn, req, {docName = req.url.slice(1).split('?')[0], gc = true} = {}) => {
|
|
195
|
+
conn.binaryType = 'arraybuffer';
|
|
196
|
+
// get doc, initialize if it does not exist yet
|
|
197
|
+
const doc = getYDoc(docName, gc);
|
|
198
|
+
doc.conns.set(conn, new Set());
|
|
199
|
+
// listen and reply to events
|
|
200
|
+
conn.on('message', /** @param {ArrayBuffer} message */ message => messageListener(conn, doc, new Uint8Array(message)));
|
|
201
|
+
|
|
202
|
+
// Check if connection is still alive
|
|
203
|
+
let pongReceived = true;
|
|
204
|
+
const pingInterval = setInterval(() => {
|
|
205
|
+
if (!pongReceived) {
|
|
206
|
+
if (doc.conns.has(conn)) {
|
|
207
|
+
closeConn(doc, conn);
|
|
208
|
+
}
|
|
209
|
+
clearInterval(pingInterval);
|
|
210
|
+
} else if (doc.conns.has(conn)) {
|
|
211
|
+
pongReceived = false;
|
|
212
|
+
try {
|
|
213
|
+
conn.ping();
|
|
214
|
+
} catch (e) {
|
|
215
|
+
closeConn(doc, conn);
|
|
216
|
+
clearInterval(pingInterval);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}, pingTimeout);
|
|
220
|
+
conn.on('close', () => {
|
|
221
|
+
closeConn(doc, conn);
|
|
222
|
+
clearInterval(pingInterval);
|
|
223
|
+
});
|
|
224
|
+
conn.on('pong', () => {
|
|
225
|
+
pongReceived = true;
|
|
226
|
+
});
|
|
227
|
+
// put the following in a variables in a block so the interval handlers don't keep in in
|
|
228
|
+
// scope
|
|
229
|
+
{
|
|
230
|
+
// send sync step 1
|
|
231
|
+
const syncEncoder = encoding.createEncoder();
|
|
232
|
+
encoding.writeVarUint(syncEncoder, messageSync);
|
|
233
|
+
syncProtocol.writeSyncStep1(syncEncoder, doc);
|
|
234
|
+
send(doc, conn, encoding.toUint8Array(syncEncoder));
|
|
235
|
+
|
|
236
|
+
const awarenessStates = doc.awareness.getStates();
|
|
237
|
+
if (awarenessStates.size > 0) {
|
|
238
|
+
const awarenessEncoder = encoding.createEncoder();
|
|
239
|
+
encoding.writeVarUint(awarenessEncoder, messageAwareness);
|
|
240
|
+
encoding.writeVarUint8Array(awarenessEncoder, awarenessProtocol.encodeAwarenessUpdate(doc.awareness, Array.from(awarenessStates.keys())));
|
|
241
|
+
send(doc, conn, encoding.toUint8Array(awarenessEncoder));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
};
|
|
@@ -19,7 +19,8 @@ module.exports = function getConfigProperties() {
|
|
|
19
19
|
emailAnalytics: config.get('emailAnalytics'),
|
|
20
20
|
hostSettings: config.get('hostSettings'),
|
|
21
21
|
tenor: config.get('tenor'),
|
|
22
|
-
editor: config.get('editor')
|
|
22
|
+
editor: config.get('editor'),
|
|
23
|
+
pintura: config.get('pintura')
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
const billingUrl = config.get('hostSettings:billing:enabled') ? config.get('hostSettings:billing:url') : '';
|
|
@@ -214,6 +214,21 @@ class SettingsBREADService {
|
|
|
214
214
|
const {filteredSettings: refilteredSettings, emailsToVerify} = await this.prepSettingsForEmailVerification(filteredSettings, getSetting);
|
|
215
215
|
|
|
216
216
|
const modelArray = await this.SettingsModel.edit(refilteredSettings, options).then((result) => {
|
|
217
|
+
// TODO: temporary fix for starting/stopping lexicalMultiplayer service when labs flag is changed
|
|
218
|
+
// this should be removed along with the flag, or set up in a more generic way
|
|
219
|
+
const labsSetting = result.find(setting => setting.get('key') === 'labs');
|
|
220
|
+
if (labsSetting) {
|
|
221
|
+
const lexicalMultiplayer = require('../lexical-multiplayer');
|
|
222
|
+
const previous = JSON.parse(labsSetting.previousAttributes().value);
|
|
223
|
+
const current = JSON.parse(labsSetting.get('value'));
|
|
224
|
+
|
|
225
|
+
if (!previous.lexicalMultiplayer && current.lexicalMultiplayer) {
|
|
226
|
+
lexicalMultiplayer.enable();
|
|
227
|
+
} else if (previous.lexicalMultiplayer && !current.lexicalMultiplayer) {
|
|
228
|
+
lexicalMultiplayer.disable();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
217
232
|
return this._formatBrowse(_.keyBy(_.invokeMap(result, 'toJSON'), 'key'), options.context);
|
|
218
233
|
});
|
|
219
234
|
|