+ {{#if (and this.post.isPublished (not this.post.emailOnly))}}
+ {{#if this.showPostCount}}
+ Keep up the good work. Now, share your post with the world!
+ {{else}}
+ Spread the word to your audience and increase your reach.
+ {{/if}}
+ {{else}}
+ {{#if this.post.isSent}}
+ It
+ {{else}}
+ {{if this.post.emailOnly "Your email" "Your post"}}
+ {{/if}}
+ {{if this.post.isScheduled "will be" "was"}}
+ {{#if this.post.emailOnly}}
+ sent to
+ {{else if this.post.willEmail}}
+ published on your site, and sent to
+ {{else}}
+ published on your site
+ {{/if}}
+
+ {{#if (or this.post.hasEmail this.post.willEmail)}}
+ {{#let (members-count-fetcher query=(hash filter=this.post.fullRecipientFilter)) as |countFetcher|}}
+
+ {{if (eq @recipientType "all") "all"}}
+
+ {{format-number countFetcher.count}}
+
+ {{!-- @recipientType = free/paid/all/specific --}}
+ {{if (not-eq @recipientType "all") @recipientType}}
+
+ {{gh-pluralize countFetcher.count "subscriber" without-count=true}}
+
+
+ of {{this.post.newsletter.name}}
+ {{/let}}
+ {{/if}}
+
+ {{#let (moment-site-tz this.post.publishedAtUTC) as |publishedAt|}}
+ on
+ {{moment-format publishedAt "D MMM YYYY"}}
+ at
+ {{moment-format publishedAt "HH:mm"}}.
+ {{/let}}
+ {{/if}}
+
+
+
+
diff --git a/ghost/admin/app/components/modal-post-success.js b/ghost/admin/app/components/modal-post-success.js
new file mode 100644
index 0000000000..32ed28216c
--- /dev/null
+++ b/ghost/admin/app/components/modal-post-success.js
@@ -0,0 +1,95 @@
+import Component from '@glimmer/component';
+import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
+import {action} from '@ember/object';
+import {capitalize} from '@ember/string';
+import {inject as service} from '@ember/service';
+import {task, timeout} from 'ember-concurrency';
+
+export default class PostSuccessModal extends Component {
+ @service store;
+ @service router;
+ @service notifications;
+
+ static modalOptions = {
+ className: 'fullscreen-modal-wide fullscreen-modal-action modal-post-success'
+ };
+
+ get post() {
+ return this.args.data.post;
+ }
+
+ get postCount() {
+ return this.args.data.postCount;
+ }
+
+ get showPostCount() {
+ return this.args.data.showPostCount;
+ }
+
+ @action
+ handleTwitter() {
+ window.open(`https://twitter.com/intent/tweet?url=${encodeURI(this.post.url)}`, '_blank');
+ }
+
+ @action
+ handleThreads() {
+ window.open(`https://threads.net/intent/post?text=${encodeURI(this.post.url)}`, '_blank');
+ }
+
+ @action
+ handleFacebook() {
+ window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURI(this.post.url)}`, '_blank');
+ }
+
+ @action
+ handleLinkedIn() {
+ window.open(`http://www.linkedin.com/shareArticle?mini=true&url=${encodeURI(this.post.url)}`, '_blank');
+ }
+
+ @action
+ viewInBrowser() {
+ window.open(this.post.url, '_blank');
+ }
+
+ @task
+ *handleCopyLink() {
+ copyTextToClipboard(this.post.url);
+ yield timeout(1000);
+ return true;
+ }
+
+ @task
+ *handleCopyPreviewLink() {
+ copyTextToClipboard(this.post.previewUrl);
+ yield timeout(1000);
+ return true;
+ }
+
+ @task
+ *revertToDraftTask() {
+ const currentPost = this.post;
+ const originalStatus = currentPost.status;
+ const originalPublishedAtUTC = currentPost.publishedAtUTC;
+
+ try {
+ if (currentPost.isScheduled) {
+ currentPost.publishedAtUTC = null;
+ }
+
+ currentPost.status = 'draft';
+ currentPost.emailOnly = false;
+
+ yield currentPost.save();
+ this.router.transitionTo('lexical-editor.edit', 'post', currentPost.id);
+
+ const postType = capitalize(currentPost.displayName);
+ this.notifications.showNotification(`${postType} reverted to a draft.`, {type: 'success'});
+
+ return true;
+ } catch (e) {
+ currentPost.status = originalStatus;
+ currentPost.publishedAtUTC = originalPublishedAtUTC;
+ throw e;
+ }
+ }
+}
diff --git a/ghost/admin/app/components/posts-list/list.hbs b/ghost/admin/app/components/posts-list/list.hbs
index 99e0dcf7d5..c9cf59e331 100644
--- a/ghost/admin/app/components/posts-list/list.hbs
+++ b/ghost/admin/app/components/posts-list/list.hbs
@@ -1,5 +1,5 @@
- {{!-- always order as scheduled, draft, remainder --}}
+ {{!-- always order as scheduled, draft, remainder --}}
{{#if (or @model.scheduledInfinityModel (or @model.draftInfinityModel @model.publishedAndSentInfinityModel))}}
{{#if @model.scheduledInfinityModel}}
{{#each @model.scheduledInfinityModel as |post|}}
@@ -42,4 +42,4 @@
as |menu|
>
-
+
\ No newline at end of file
diff --git a/ghost/admin/app/components/posts-list/list.js b/ghost/admin/app/components/posts-list/list.js
index acf7d30687..d0ecbb7a42 100644
--- a/ghost/admin/app/components/posts-list/list.js
+++ b/ghost/admin/app/components/posts-list/list.js
@@ -1,7 +1,39 @@
import Component from '@glimmer/component';
+import PostSuccessModal from '../modal-post-success';
+import {inject as service} from '@ember/service';
+import {task} from 'ember-concurrency';
export default class PostsList extends Component {
+ @service store;
+ @service modals;
+ @service feature;
+
+ latestScheduledPost = null;
+
+ constructor() {
+ super(...arguments);
+ if (this.feature.publishFlowEndScreen) {
+ this.checkPublishFlowModal();
+ }
+ }
+
+ async checkPublishFlowModal() {
+ if (localStorage.getItem('ghost-last-scheduled-post')) {
+ await this.getLatestScheduledPost.perform();
+ this.modals.open(PostSuccessModal, {
+ post: this.latestScheduledPost
+ });
+ localStorage.removeItem('ghost-last-scheduled-post');
+ }
+ }
+
get list() {
return this.args.list;
}
+
+ @task
+ *getLatestScheduledPost() {
+ const result = yield this.store.query('post', {filter: `id:${localStorage.getItem('ghost-last-scheduled-post')}`, limit: 1});
+ this.latestScheduledPost = result.toArray()[0];
+ }
}
diff --git a/ghost/admin/app/components/posts/analytics.hbs b/ghost/admin/app/components/posts/analytics.hbs
index c08e6aeb71..6f20b826af 100644
--- a/ghost/admin/app/components/posts/analytics.hbs
+++ b/ghost/admin/app/components/posts/analytics.hbs
@@ -34,9 +34,61 @@
{{moment-format publishedAt "HH:mm"}}
{{/let}}
-
- {{svg-jar "pen" title=""}}Edit post
-
+ {{#if (feature "publishFlowEndScreen")}}
+
+ {{#if (feature "postAnalyticsRefresh")}}
+
+ {{/if}}
+ {{#unless this.post.emailOnly}}
+
+ {{/unless}}
+
+
+
+
+ {{svg-jar "dotdotdot"}}
+ Actions
+
+
+
+
+ Edit post
+
+
+ View in browser
+
+
+
+
+
+
+
+ {{else}}
+
+ {{svg-jar "pen" title=""}}Edit post
+
+ {{/if}}
@@ -201,4 +253,4 @@
{{/if}}
-
+
\ No newline at end of file
diff --git a/ghost/admin/app/components/posts/analytics.js b/ghost/admin/app/components/posts/analytics.js
index 0f81f7454d..3fe7e136d3 100644
--- a/ghost/admin/app/components/posts/analytics.js
+++ b/ghost/admin/app/components/posts/analytics.js
@@ -1,4 +1,6 @@
import Component from '@glimmer/component';
+import DeletePostModal from '../modals/delete-post';
+import PostSuccessModal from '../modal-post-success';
import {action} from '@ember/object';
import {didCancel, task} from 'ember-concurrency';
import {inject as service} from '@ember/service';
@@ -24,6 +26,9 @@ export default class Analytics extends Component {
@service utils;
@service feature;
@service store;
+ @service router;
+ @service modals;
+ @service notifications;
@tracked sources = null;
@tracked links = null;
@@ -31,12 +36,47 @@ export default class Analytics extends Component {
@tracked sortColumn = 'signups';
@tracked showSuccess;
@tracked updateLinkId;
+ @tracked _post = null;
+ @tracked postCount = null;
+ @tracked showPostCount = false;
displayOptions = DISPLAY_OPTIONS;
+ constructor() {
+ super(...arguments);
+ if (this.feature.publishFlowEndScreen) {
+ this.checkPublishFlowModal();
+ }
+ }
+
+ openPublishFlowModal() {
+ this.modals.open(PostSuccessModal, {
+ post: this.post,
+ postCount: this.postCount,
+ showPostCount: this.showPostCount
+ });
+ }
+
+ async checkPublishFlowModal() {
+ if (localStorage.getItem('ghost-last-published-post')) {
+ await this.fetchPostCountTask.perform();
+ this.showPostCount = true;
+ this.openPublishFlowModal();
+ localStorage.removeItem('ghost-last-published-post');
+ }
+ }
+
get post() {
+ if (this.feature.publishFlowEndScreen) {
+ return this._post ?? this.args.post;
+ }
+
return this.args.post;
}
+ set post(value) {
+ this._post = value;
+ }
+
get allowedDisplayOptions() {
if (!this.hasPaidConversionData) {
return this.displayOptions.filter(d => d.value === 'signups');
@@ -142,6 +182,19 @@ export default class Analytics extends Component {
}
}
+ @action
+ togglePublishFlowModal() {
+ this.showPostCount = false;
+ this.openPublishFlowModal();
+ }
+
+ @action
+ confirmDeleteMember() {
+ this.modals.open(DeletePostModal, {
+ post: this.post
+ });
+ }
+
updateLinkData(linksData) {
let updatedLinks;
if (this.links?.length) {
@@ -302,6 +355,29 @@ export default class Analytics extends Component {
this.mentions = yield this.store.query('mention', {limit: 5, order: 'created_at desc', filter});
}
+ @task
+ *fetchPostCountTask() {
+ if (!this.post.emailOnly) {
+ const result = yield this.store.query('post', {filter: 'status:published', limit: 1});
+ let count = result.meta.pagination.total;
+
+ this.postCount = count;
+ }
+ }
+
+ @task
+ *fetchPostTask() {
+ const result = yield this.store.query('post', {filter: `id:${this.post.id}`, limit: 1});
+ this.post = result.toArray()[0];
+
+ if (this.post.email) {
+ this.notifications.showNotification('Post analytics refreshing', {
+ description: 'It can take up to five minutes for all data to show.',
+ type: 'success'
+ });
+ }
+ }
+
get showLinks() {
return this.post.showEmailClickAnalytics;
}
diff --git a/ghost/admin/app/services/feature.js b/ghost/admin/app/services/feature.js
index 09f4042a45..14be59d768 100644
--- a/ghost/admin/app/services/feature.js
+++ b/ghost/admin/app/services/feature.js
@@ -78,6 +78,8 @@ export default class FeatureService extends Service {
@feature('ActivityPub') ActivityPub;
@feature('editorExcerpt') editorExcerpt;
@feature('contentVisibility') contentVisibility;
+ @feature('publishFlowEndScreen') publishFlowEndScreen;
+ @feature('postAnalyticsRefresh') postAnalyticsRefresh;
_user = null;
diff --git a/ghost/admin/app/styles/components/dropdowns.css b/ghost/admin/app/styles/components/dropdowns.css
index 099644e81d..96a1b808a1 100644
--- a/ghost/admin/app/styles/components/dropdowns.css
+++ b/ghost/admin/app/styles/components/dropdowns.css
@@ -393,7 +393,8 @@ Post context menu
stroke-width: 1.8px;
}
-.gh-posts-context-menu li:last-child::before {
+.gh-posts-context-menu li:last-child::before,
+.gh-analytics-actions-menu li:last-child::before {
display: block;
position: relative;
content: "";
diff --git a/ghost/admin/app/styles/components/publishmenu.css b/ghost/admin/app/styles/components/publishmenu.css
index 6a781a9d63..d1e164c81e 100644
--- a/ghost/admin/app/styles/components/publishmenu.css
+++ b/ghost/admin/app/styles/components/publishmenu.css
@@ -880,3 +880,123 @@
height: 20px;
margin-right: 6px;
}
+
+/* Publish flow modal
+/* ---------------------------------------------------------- */
+
+.modal-post-success {
+ max-width: 640px;
+ --padding: 40px;
+ --radius: 12px;
+}
+
+.modal-post-success .modal-content {
+ padding: var(--padding);
+ border-radius: var(--radius);
+}
+
+.modal-post-success .modal-image {
+ aspect-ratio: 16 / 7.55;
+ overflow: hidden;
+ margin: calc(var(--padding) * -1) calc(var(--padding) * -1) var(--padding);
+}
+
+.modal-post-success .modal-image img {
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: var(--radius) var(--radius) 0 0;
+}
+
+.modal-post-success .modal-header {
+ margin: 0;
+}
+
+.modal-post-success .modal-header h1 {
+ display: flex;
+ flex-direction: column;
+ margin: 0;
+ font-size: 3.6rem;
+ font-weight: 700;
+ letter-spacing: -0.03em;
+}
+
+.modal-post-success .modal-header h1 span:has(+ span) {
+ color: var(--green);
+}
+
+.modal-post-success .modal-body {
+ margin-top: 16px;
+ font-size: 1.8rem;
+ line-height: 1.4;
+ letter-spacing: -0.002em;
+}
+
+.modal-post-success .modal-footer {
+ gap: 16px;
+ margin-top: var(--padding);
+}
+
+.modal-post-success .modal-footer .gh-btn {
+ min-width: 64px;
+ height: 44px;
+ border-radius: 4px;
+}
+
+.modal-post-success .modal-footer .gh-btn:not(:first-child) {
+ margin: 0;
+}
+
+.modal-post-success .modal-footer .gh-btn span {
+ padding-inline: 18px;
+ font-size: 1.6rem;
+}
+
+.modal-post-success .modal-footer .gh-btn-primary {
+ min-width: 80px;
+}
+
+.modal-post-success .modal-footer:has(.twitter) .gh-btn-primary {
+ flex-grow: 1;
+}
+
+.modal-post-success .modal-footer .gh-btn:is(.twitter, .threads, .facebook, .linkedin) {
+ width: 56px;
+}
+
+.modal-post-success .modal-footer .gh-btn:is(.twitter, .threads, .facebook, .linkedin) span {
+ font-size: 0;
+}
+
+.modal-post-success .modal-footer .gh-btn svg {
+ width: 18px;
+ height: 18px;
+}
+
+.modal-post-success .modal-footer .gh-btn.twitter svg path {
+ fill: black;
+}
+
+.modal-post-success:has(.modal-image) .close {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 32px;
+ height: 32px;
+ background-color: rgba(0, 0, 0, 0.2);
+ border-radius: 50%;
+}
+
+.modal-post-success:has(.modal-image) .close:hover {
+ background-color: rgba(0, 0, 0, 0.25);
+}
+
+.modal-post-success:has(.modal-image) .close svg {
+ width: 14px;
+ height: 14px;
+}
+
+.modal-post-success:has(.modal-image) .close svg path {
+ fill: white;
+}
diff --git a/ghost/admin/app/styles/layouts/content.css b/ghost/admin/app/styles/layouts/content.css
index a4e6f6aa5c..3964ce4374 100644
--- a/ghost/admin/app/styles/layouts/content.css
+++ b/ghost/admin/app/styles/layouts/content.css
@@ -776,6 +776,17 @@
border-radius: var(--border-radius);
}
+.gh-analytics-actions-menu {
+ top: calc(100% + 6px);
+ left: auto;
+ right: 0;
+}
+
+.gh-analytics-actions-menu.fade-out {
+ animation-duration: .001s;
+ pointer-events: none;
+}
+
.feature-audienceFeedback .gh-post-analytics-box.gh-post-analytics-newsletter-clicks,
.feature-audienceFeedback .gh-post-analytics-box.gh-post-analytics-source-attribution,
.gh-post-analytics-box.gh-post-analytics-mentions {
@@ -1523,6 +1534,10 @@
transition: all .1s linear;
}
+span.dropdown .gh-post-list-cta > span {
+ padding: 0;
+}
+
.gh-post-list-cta.edit.is-hovered > *,
.gh-post-list-cta.edit.is-hovered:hover > *,
.gh-post-list-cta.edit:not(.is-hovered):hover > * {
diff --git a/ghost/admin/app/templates/posts.hbs b/ghost/admin/app/templates/posts.hbs
index 3d99d9ee66..dc3e26361d 100644
--- a/ghost/admin/app/templates/posts.hbs
+++ b/ghost/admin/app/templates/posts.hbs
@@ -73,4 +73,4 @@
{{outlet}}
-
+
\ No newline at end of file
diff --git a/ghost/admin/public/assets/icons/reload.svg b/ghost/admin/public/assets/icons/reload.svg
index a094276a30..5d17641a8a 100644
--- a/ghost/admin/public/assets/icons/reload.svg
+++ b/ghost/admin/public/assets/icons/reload.svg
@@ -1,6 +1,6 @@