From 5ee67892dc2494d741b59b0789c8f6f8c3bf8e60 Mon Sep 17 00:00:00 2001
From: Sodbileg Gansukh <sodbileg.gansukh@gmail.com>
Date: Thu, 15 Aug 2024 18:22:32 +0800
Subject: [PATCH] Improved publishing flow end screen (#20701)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

ref DES-594

- this update introduces some improvements to the publishing flow end screen
- everything's behind a feature flag — publishFlowEndScreen
---
 .../src/assets/icons/share.svg                |   1 +
 .../settings/advanced/labs/AlphaFeatures.tsx  |   8 +
 .../components/editor/modals/publish-flow.hbs |  14 +-
 .../editor/modals/publish-flow/confirm.js     |  10 ++
 .../app/components/modal-post-success.hbs     | 152 ++++++++++++++++++
 .../app/components/modal-post-success.js      |  95 +++++++++++
 .../admin/app/components/posts-list/list.hbs  |   4 +-
 ghost/admin/app/components/posts-list/list.js |  32 ++++
 .../admin/app/components/posts/analytics.hbs  |  60 ++++++-
 ghost/admin/app/components/posts/analytics.js |  76 +++++++++
 ghost/admin/app/services/feature.js           |   2 +
 .../admin/app/styles/components/dropdowns.css |   3 +-
 .../app/styles/components/publishmenu.css     | 120 ++++++++++++++
 ghost/admin/app/styles/layouts/content.css    |  15 ++
 ghost/admin/app/templates/posts.hbs           |   2 +-
 ghost/admin/public/assets/icons/reload.svg    |   2 +-
 ghost/admin/public/assets/icons/share.svg     |   1 +
 .../public/assets/icons/social-threads.svg    |  10 ++
 ghost/core/core/shared/labs.js                |   4 +-
 19 files changed, 595 insertions(+), 16 deletions(-)
 create mode 100644 apps/admin-x-design-system/src/assets/icons/share.svg
 create mode 100644 ghost/admin/app/components/modal-post-success.hbs
 create mode 100644 ghost/admin/app/components/modal-post-success.js
 create mode 100644 ghost/admin/public/assets/icons/share.svg
 create mode 100644 ghost/admin/public/assets/icons/social-threads.svg

diff --git a/apps/admin-x-design-system/src/assets/icons/share.svg b/apps/admin-x-design-system/src/assets/icons/share.svg
new file mode 100644
index 0000000000..6feac81448
--- /dev/null
+++ b/apps/admin-x-design-system/src/assets/icons/share.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="Share-1--Streamline-Streamline--3.0.svg" height="24" width="24"><desc>Share 1 Streamline Icon: https://streamlinehq.com</desc><defs></defs><title>share-1</title><path d="M17.25 8.25h1.5a1.5 1.5 0 0 1 1.5 1.5v12a1.5 1.5 0 0 1 -1.5 1.5H5.25a1.5 1.5 0 0 1 -1.5 -1.5v-12a1.5 1.5 0 0 1 1.5 -1.5h1.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path><path d="m12 0.75 0 10.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path><path d="M8.25 4.5 12 0.75l3.75 3.75" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path></svg>
\ No newline at end of file
diff --git a/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx b/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx
index f23705753d..39dd01a525 100644
--- a/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx
+++ b/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx
@@ -59,6 +59,14 @@ const features = [{
     title: 'Content Visibility',
     description: 'Enables content visibility in Emails',
     flag: 'contentVisibility'
+},{
+    title: 'Publish Flow — End Screen',
+    description: 'Enables improved publish flow',
+    flag: 'publishFlowEndScreen'
+},{
+    title: 'Post Analytics — Refresh',
+    description: 'Adds a refresh button to the post analytics screen',
+    flag: 'postAnalyticsRefresh'
 }];
 
 const AlphaFeatures: React.FC = () => {
diff --git a/ghost/admin/app/components/editor/modals/publish-flow.hbs b/ghost/admin/app/components/editor/modals/publish-flow.hbs
index e8264e4746..e202eb7bff 100644
--- a/ghost/admin/app/components/editor/modals/publish-flow.hbs
+++ b/ghost/admin/app/components/editor/modals/publish-flow.hbs
@@ -45,12 +45,14 @@
                 @close={{@close}}
             />
         {{else if this.isComplete}}
-            <Editor::Modals::PublishFlow::Complete
-                @publishOptions={{@data.publishOptions}}
-                @recipientType={{this.recipientType}}
-                @postCount={{this.postCount}}
-                @close={{@close}}
-            />
+            {{#unless (feature "publishFlowEndScreen")}}
+                <Editor::Modals::PublishFlow::Complete
+                    @publishOptions={{@data.publishOptions}}
+                    @recipientType={{this.recipientType}}
+                    @postCount={{this.postCount}}
+                    @close={{@close}}
+                />
+            {{/unless}}
         {{else}}
             <Editor::Modals::PublishFlow::Options
                 @publishOptions={{@data.publishOptions}}
diff --git a/ghost/admin/app/components/editor/modals/publish-flow/confirm.js b/ghost/admin/app/components/editor/modals/publish-flow/confirm.js
index 54410befbe..9152a8b825 100644
--- a/ghost/admin/app/components/editor/modals/publish-flow/confirm.js
+++ b/ghost/admin/app/components/editor/modals/publish-flow/confirm.js
@@ -13,6 +13,7 @@ function isString(str) {
 
 export default class PublishFlowOptions extends Component {
     @service settings;
+    @service feature;
 
     @tracked errorMessage;
 
@@ -91,6 +92,15 @@ export default class PublishFlowOptions extends Component {
 
         try {
             yield this.args.saveTask.perform();
+            if (this.feature.publishFlowEndScreen) {
+                if (this.args.publishOptions.isScheduled) {
+                    localStorage.setItem('ghost-last-scheduled-post', this.args.publishOptions.post.id);
+                    window.location.href = '/ghost/#/posts?type=scheduled';
+                } else {
+                    localStorage.setItem('ghost-last-published-post', this.args.publishOptions.post.id);
+                    window.location.href = `/ghost/#/posts/analytics/${this.args.publishOptions.post.id}`;
+                }
+            }
         } catch (e) {
             if (e === undefined && this.args.publishOptions.post.errors.length !== 0) {
                 // validation error
diff --git a/ghost/admin/app/components/modal-post-success.hbs b/ghost/admin/app/components/modal-post-success.hbs
new file mode 100644
index 0000000000..2f8715db6d
--- /dev/null
+++ b/ghost/admin/app/components/modal-post-success.hbs
@@ -0,0 +1,152 @@
+<div class="modal-content">
+    {{#if this.post.featureImage}}
+        <figure class="modal-image">
+            <img src="{{this.post.featureImage}}" alt="{{this.post.title}}">
+        </figure>
+    {{else if this.post.twitterImage}}
+        <figure class="modal-image">
+            <img src="{{this.post.twitterImage}}" alt="{{this.post.title}}">
+        </figure>
+    {{else if this.post.ogImage}}
+        <figure class="modal-image">
+            <img src="{{this.post.ogImage}}" alt="{{this.post.title}}">
+        </figure>
+    {{/if}}
+
+    <header class="modal-header">
+        <h1>
+            {{#if this.post.isScheduled}}
+                <span>All set!</span>
+            {{else}}
+                <span>
+                    {{#if this.showPostCount}}
+                        Boom! It's out there.
+                    {{else}}
+                        Your post is out there.
+                    {{/if}}
+                </span>
+                <span>
+                    {{#if this.post.emailOnly}}
+                        Your email has been sent.
+                    {{else}}
+                        {{#if this.showPostCount}}
+                            That's {{format-number this.postCount}} {{gh-pluralize this.postCount "post" without-count=true}} published.
+                        {{else}}
+                            Share it with the world!
+                        {{/if}}
+                    {{/if}}
+                </span>
+            {{/if}}
+        </h1>
+    </header>
+
+    <button type="button" class="close" title="Close" {{on "click" @close}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
+
+    <div class="modal-body">
+        {{#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|}}
+                    <strong>
+                        {{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}}
+                    </strong>
+
+                    of <strong>{{this.post.newsletter.name}}</strong>
+                {{/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}}
+    </div>
+
+    <footer class="modal-footer">
+        {{#if (and this.post.isPublished (not this.post.emailOnly))}}
+            <button
+                class="gh-btn twitter"
+                type="button"
+                {{on "click" this.handleTwitter}}
+                {{on "mousedown" (optional this.noop)}}
+            >
+                <span>{{svg-jar "social-x"}}</span>
+            </button>
+            <button
+                class="gh-btn threads"
+                type="button"
+                {{on "click" this.handleThreads}}
+                {{on "mousedown" (optional this.noop)}}
+            >
+                <span>{{svg-jar "social-threads"}}</span>
+            </button>
+            <button
+                class="gh-btn facebook"
+                type="button"
+                {{on "click" this.handleFacebook}}
+                {{on "mousedown" (optional this.noop)}}
+            >
+                <span>{{svg-jar "social-facebook"}}</span>
+            </button>
+            <button
+                class="gh-btn linkedin"
+                type="button"
+                {{on "click" this.handleLinkedIn}}
+                {{on "mousedown" (optional this.noop)}}
+            >
+                <span>{{svg-jar "social-linkedin"}}</span>
+            </button>
+            <GhTaskButton
+                @buttonText="Copy link"
+                @task={{this.handleCopyLink}}
+                @showIcon={{true}}
+                @successText="Link copied"
+                @class="gh-btn gh-btn-primary gh-btn-icon copy-link" />
+        {{else}}
+            {{#if (and this.post.isScheduled (not this.post.emailOnly))}}
+                <GhTaskButton
+                    @buttonText="Copy preview link"
+                    @task={{this.handleCopyPreviewLink}}
+                    @successText="Link copied"
+                    @class="gh-btn gh-btn-icon copy-preview-link" />
+            {{/if}}
+
+            <button
+                class="gh-btn gh-btn-primary dismiss"
+                type="button"
+                {{on "click" @close}}
+                {{on "mousedown" (optional this.noop)}}
+            >
+                <span>OK</span>
+            </button>
+        {{/if}}
+    </footer>
+</div>
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 @@
 <MultiList::List @model={{@list}} class="posts-list gh-list {{unless @model "no-posts"}} feature-memberAttribution" as |list| >
-    {{!-- 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|
 >
     <PostsList::ContextMenu @menu={{menu}} />
-</GhContextMenu>
+</GhContextMenu>
\ 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}}
                 </div>
-                <LinkTo @route="lexical-editor.edit" @models={{array this.post.displayName this.post.id}} class="gh-post-list-cta edit" title="">
-                    {{svg-jar "pen" title=""}}<span>Edit post</span>
-                </LinkTo>
+                {{#if (feature "publishFlowEndScreen")}}
+                    <div style="display: flex; gap: 8px;">
+                        {{#if (feature "postAnalyticsRefresh")}}
+                            <button type="button" class="gh-post-list-cta edit" {{on "click" this.fetchPostTask.perform}}>
+                                {{svg-jar "reload" title="Refresh post analytics"}}<span>Refresh</span>
+                            </button>
+                        {{/if}}
+                        {{#unless this.post.emailOnly}}
+                            <button type="button" class="gh-post-list-cta edit share" {{on "click" this.togglePublishFlowModal}}>
+                                {{svg-jar "share" title="Share post"}}<span>Share</span>
+                            </button>
+                        {{/unless}}
+
+                        <span class="dropdown">
+                            <GhDropdownButton
+                                @dropdownName="analytics-actions-menu"
+                                @classNames="gh-post-list-cta edit gh-btn-icon icon-only gh-btn-action-icon"
+                                @title="Analytics Actions"
+                                data-test-button="analytics-actions"
+                            >
+                                <span>
+                                    {{svg-jar "dotdotdot"}}
+                                    <span class="hidden">Actions</span>
+                                </span>
+                            </GhDropdownButton>
+                            <GhDropdown
+                                @name="analytics-actions-menu"
+                                @tagName="ul"
+                                @classNames="gh-analytics-actions-menu dropdown-menu dropdown-triangle-top-right"
+                                @closeOnClick={{true}}
+                            >
+                                <li>
+                                    <LinkTo class="edit-post" @route="lexical-editor.edit" @models={{array this.post.displayName this.post.id}}>Edit post</LinkTo>
+                                </li>
+                                <li>
+                                    <a class="view-browser" href="{{this.post.url}}" target="_blank" rel="noopener noreferrer">View in browser</a>
+                                </li>
+                                <li>
+                                    <button
+                                        type="button"
+                                        class="delete-post mr2"
+                                        {{on "click" this.confirmDeleteMember}}
+                                        data-test-button="delete-post"
+                                    >
+                                        <span class="red">Delete post</span>
+                                    </button>
+                                </li>
+                            </GhDropdown>
+                        </span>
+                    </div>
+                {{else}}
+                    <LinkTo @route="lexical-editor.edit" @models={{array this.post.displayName this.post.id}} class="gh-post-list-cta edit" title="">
+                        {{svg-jar "pen" title=""}}<span>Edit post</span>
+                    </LinkTo>
+                {{/if}}
             </div>
         </div>
     </GhCanvasHeader>
@@ -201,4 +253,4 @@
             </div>
         </div>
     {{/if}}
-</section>
+</section>
\ 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 @@
 
     </section>
     {{outlet}}
-</section>
+</section>
\ 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 @@
 <svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
   <title>reload</title>
-  <g fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke="#000" stroke-width="1.5">
+  <g fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke="currentColor" stroke-width="1.5">
     <path d="M15.667 9.752h6.09V2.444"/>
     <path d="M21.705 9.579C20.622 5.226 16.688 2 12 2 6.477 2 2 6.477 2 12s4.477 10 10 10c3.3 0 6.228-1.6 8.05-4.065"/>
   </g>
diff --git a/ghost/admin/public/assets/icons/share.svg b/ghost/admin/public/assets/icons/share.svg
new file mode 100644
index 0000000000..6feac81448
--- /dev/null
+++ b/ghost/admin/public/assets/icons/share.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="Share-1--Streamline-Streamline--3.0.svg" height="24" width="24"><desc>Share 1 Streamline Icon: https://streamlinehq.com</desc><defs></defs><title>share-1</title><path d="M17.25 8.25h1.5a1.5 1.5 0 0 1 1.5 1.5v12a1.5 1.5 0 0 1 -1.5 1.5H5.25a1.5 1.5 0 0 1 -1.5 -1.5v-12a1.5 1.5 0 0 1 1.5 -1.5h1.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path><path d="m12 0.75 0 10.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path><path d="M8.25 4.5 12 0.75l3.75 3.75" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path></svg>
\ No newline at end of file
diff --git a/ghost/admin/public/assets/icons/social-threads.svg b/ghost/admin/public/assets/icons/social-threads.svg
new file mode 100644
index 0000000000..dc95af6271
--- /dev/null
+++ b/ghost/admin/public/assets/icons/social-threads.svg
@@ -0,0 +1,10 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_351_18008)">
+<path d="M13.0332 8.37917C12.96 8.34407 12.8856 8.3103 12.8103 8.27795C12.6791 5.86015 11.3579 4.47596 9.1396 4.4618C9.12955 4.46173 9.11955 4.46173 9.1095 4.46173C7.78265 4.46173 6.67913 5.0281 5.99992 6.05871L7.21993 6.89561C7.72733 6.12579 8.52364 5.96167 9.11008 5.96167C9.11686 5.96167 9.12366 5.96168 9.13036 5.96174C9.86078 5.96639 10.4119 6.17876 10.7687 6.59291C11.0283 6.89442 11.2019 7.31107 11.2879 7.8369C10.6403 7.72683 9.93993 7.69299 9.19122 7.73592C7.08214 7.8574 5.72624 9.08747 5.81731 10.7967C5.86352 11.6637 6.29544 12.4096 7.03346 12.8968C7.65745 13.3087 8.46111 13.5101 9.29635 13.4645C10.3994 13.4041 11.2647 12.9832 11.8684 12.2137C12.3268 11.6293 12.6168 10.872 12.7448 9.91782C13.2705 10.2351 13.6601 10.6525 13.8753 11.1544C14.2411 12.0075 14.2624 13.4094 13.1186 14.5523C12.1164 15.5535 10.9117 15.9866 9.09104 16C7.07147 15.9851 5.54409 15.3374 4.55103 14.0749C3.62111 12.8928 3.14053 11.1854 3.1226 9C3.14053 6.8146 3.62111 5.10714 4.55103 3.92503C5.54409 2.66262 7.07144 2.01495 9.09101 1.99994C11.1252 2.01506 12.6792 2.66585 13.7103 3.93435C14.2159 4.55641 14.597 5.3387 14.8483 6.25081L16.278 5.86936C15.9734 4.74665 15.4942 3.7792 14.842 2.97686C13.5201 1.35059 11.5869 0.517279 9.096 0.5H9.08603C6.60019 0.517219 4.68862 1.3537 3.40443 2.98619C2.26168 4.4389 1.67221 6.46024 1.65241 8.99402L1.65234 9L1.65241 9.00598C1.67221 11.5397 2.26168 13.5611 3.40443 15.0138C4.68862 16.6463 6.60019 17.4828 9.08603 17.5H9.096C11.306 17.4847 12.8638 16.9061 14.1472 15.6239C15.8262 13.9465 15.7756 11.8439 15.2222 10.5531C14.8252 9.62749 14.0683 8.8757 13.0332 8.37917ZM9.21739 11.9668C8.29301 12.0188 7.33268 11.6039 7.28533 10.7152C7.25023 10.0563 7.75426 9.32105 9.27412 9.23347C9.44817 9.22343 9.61897 9.21852 9.78676 9.21852C10.3388 9.21852 10.8553 9.27215 11.3248 9.3748C11.1497 11.562 10.1224 11.9171 9.21739 11.9668Z" fill="black"/>
+</g>
+<defs>
+<clipPath id="clip0_351_18008">
+<rect width="17" height="17" fill="white" transform="translate(0.5 0.5)"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/ghost/core/core/shared/labs.js b/ghost/core/core/shared/labs.js
index e9a1a7ad68..4f242c4ccc 100644
--- a/ghost/core/core/shared/labs.js
+++ b/ghost/core/core/shared/labs.js
@@ -45,7 +45,9 @@ const ALPHA_FEATURES = [
     'importMemberTier',
     'lexicalIndicators',
     'adminXDemo',
-    'contentVisibility'
+    'contentVisibility',
+    'publishFlowEndScreen',
+    'postAnalyticsRefresh'
 ];
 
 module.exports.GA_KEYS = [...GA_FEATURES];