Skip to content

Commit 6c594c7

Browse files
authored
🎨 Improved What's New to hide updates for new users
ref https://linear.app/ghost/issue/DES-1155 When new users first access Ghost admin, they see notifications for changelog entries published before they created their account. This creates confusion because they're being notified about updates to a baseline they never experienced - everything is new to them. We now only show changelog notifications for changes which are published after new Users visit the admin interface for the first time.
1 parent 03f29fa commit 6c594c7

File tree

5 files changed

+844
-55
lines changed

5 files changed

+844
-55
lines changed

ghost/admin/app/components/gh-nav-menu/footer-banner.hbs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@
1111

1212
{{#if (and this.showWhatsNew this.whatsNew.hasNew)}}
1313
{{#let (get this.whatsNew.entries "0") as |entry|}}
14-
<div class="gh-sidebar-banner gh-whatsnew-toast">
15-
<button class="gh-sidebar-banner-close" type="button" {{on "click" this.dismissWhatsNewToast}}>&#xd7;</button>
16-
<a class="gh-sidebar-banner-container" href={{entry.url}} target="_blank" rel="noopener noreferrer" {{on "click" (fn this.openFeaturedWhatsNew entry.url)}}>
14+
<div class="gh-sidebar-banner gh-whatsnew-toast" data-test-toast="whats-new">
15+
<button class="gh-sidebar-banner-close" data-test-toast-close type="button" {{on "click" this.dismissWhatsNewToast}}>&#xd7;</button>
16+
<a class="gh-sidebar-banner-container" data-test-toast-link href={{entry.url}} target="_blank" rel="noopener noreferrer" {{on "click" (fn this.openFeaturedWhatsNew entry.url)}}>
1717
<div class="gh-sidebar-banner-head">
1818
{{svg-jar "sparkle-fill" class="gh-sidebar-banner-icon gh-whatsnew-banner-icon"}}
19-
<span class="gh-sidebar-banner-subhead">Whats new?</span>
19+
<span class="gh-sidebar-banner-subhead">What's new?</span>
2020
</div>
21-
<div class="gh-sidebar-banner-msg">{{entry.title}}</div>
21+
<div class="gh-sidebar-banner-msg" data-test-toast-title>{{entry.title}}</div>
2222
{{#if entry.custom_excerpt}}
23-
<div class="gh-sidebar-banner-details">{{entry.custom_excerpt}}</div>
23+
<div class="gh-sidebar-banner-details" data-test-toast-excerpt>{{entry.custom_excerpt}}</div>
2424
{{/if}}
2525
</a>
2626
</div>
Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
<div class="modal-content">
2-
<h1 class="gh-whasnew-modal-title">What’s new?</h1>
3-
<section class="gh-whatsnew-modal-entries" {{did-insert (perform this.whatsNew.updateLastSeen)}}>
1+
<div class="modal-content" data-test-modal="whats-new">
2+
<h1 class="gh-whasnew-modal-title" data-test-modal-title>What’s new?</h1>
3+
<section class="gh-whatsnew-modal-entries" data-test-entries {{did-insert (perform this.whatsNew.updateLastSeen)}}>
44
{{#each this.whatsNew.entries as |entry|}}
5-
<a class="gh-whatsnew-modal-entry" href={{entry.url}} target="_blank" rel="noopener noreferrer">
5+
<a class="gh-whatsnew-modal-entry" data-test-entry href={{entry.url}} target="_blank" rel="noopener noreferrer">
66
{{#if entry.feature_image}}
7-
<img class="gh-whatsnew-modal-entry-featureimage" src={{entry.feature_image}} alt={{entry.title}}>
7+
<img class="gh-whatsnew-modal-entry-featureimage" data-test-entry-image src={{entry.feature_image}} alt={{entry.title}}>
88
{{/if}}
99
<div class="gh-whatsnew-modal-entrycontent">
10-
<h2>{{entry.title}}</h2>
10+
<h2 data-test-entry-title>{{entry.title}}</h2>
1111
{{#if entry.custom_excerpt}}
12-
<p>{{entry.custom_excerpt}}</p>
12+
<p data-test-entry-excerpt>{{entry.custom_excerpt}}</p>
1313
{{/if}}
14-
<span>{{moment-format entry.published_at "DD MMMM YYYY"}}</span>
14+
<span data-test-entry-date>{{moment-format entry.published_at "DD MMMM YYYY"}}</span>
1515
</div>
1616
</a>
1717
{{/each}}
@@ -21,4 +21,4 @@
2121
<a href="https://ghost.org/changelog/#/portal/signup" class="gh-btn" type="button" target="_blank" rel="noopener noreferrer"><span>Turn on notifications</span></a>
2222
<a class="gh-btn gh-btn-primary" href="https://ghost.org/changelog" target="_blank" rel="noopener noreferrer"><span>All updates &rarr;</span></a>
2323
</div>
24-
</div>
24+
</div>

ghost/admin/app/services/whats-new.js

Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,36 @@ import {task} from 'ember-concurrency';
77

88
export default Service.extend({
99
session: service(),
10-
store: service(),
11-
response: null,
1210

1311
entries: null,
1412
changelogUrl: 'https://ghost.org/blog/',
1513
isShowingModal: false,
1614

17-
_user: null,
15+
// We only want to request the changelog once, so we store the initial
16+
// response so we don't request it again.
17+
_changelog_response: null,
18+
19+
// Track only the whatsNew slice of user.accessibility settings
20+
_whatsNewSettings: null,
1821

1922
init() {
2023
this._super(...arguments);
2124
this.entries = [];
25+
// Set sensible default for new users - they've "seen" everything up to now
26+
this._whatsNewSettings = {
27+
lastSeenDate: moment.utc().toISOString()
28+
};
2229
},
2330

24-
whatsNewSettings: computed('_user.accessibility', function () {
25-
let settingsJson = this.get('_user.accessibility') || '{}';
26-
let settings = JSON.parse(settingsJson);
27-
return settings.whatsNew;
28-
}),
29-
30-
hasNew: computed('whatsNewSettings.lastSeenDate', 'entries.[]', function () {
31+
hasNew: computed('_whatsNewSettings.lastSeenDate', 'entries.[]', function () {
3132
if (isEmpty(this.entries)) {
3233
return false;
3334
}
3435

3536
let [latestEntry] = this.entries;
37+
let lastSeenMoment = moment(this._whatsNewSettings.lastSeenDate);
38+
let latestMoment = moment(latestEntry.published_at);
3639

37-
let lastSeenDate = this.get('whatsNewSettings.lastSeenDate') || '2019-01-01 00:00:00';
38-
let lastSeenMoment = moment(lastSeenDate);
39-
let latestDate = latestEntry.published_at;
40-
let latestMoment = moment(latestDate || lastSeenDate);
4140
return latestMoment.isAfter(lastSeenMoment);
4241
}),
4342

@@ -64,45 +63,61 @@ export default Service.extend({
6463
}),
6564

6665
fetchLatest: task(function* () {
66+
if (this._changelog_response) {
67+
// We've already fetched the changelog so we don't fetch it again.
68+
return;
69+
}
70+
6771
try {
68-
if (!this.response) {
69-
// we should already be logged in at this point so lets grab the user
70-
// record and store it locally so that we don't have to deal with
71-
// session.user being a promise and causing issues with CPs
72-
let user = yield this.session.user;
73-
this.set('_user', user);
74-
75-
this.response = yield fetch('https://ghost.org/changelog.json');
76-
if (!this.response.ok) {
77-
// eslint-disable-next-line
78-
return console.error('Failed to fetch changelog', {response});
79-
}
80-
81-
let result = yield this.response.json();
82-
this.set('entries', result.posts || []);
83-
this.set('changelogUrl', result.changelogUrl);
72+
// Load user's persisted settings
73+
let user = yield this.session.user;
74+
let accessibility = JSON.parse(user.accessibility || '{}');
75+
let whatsNewSettings = accessibility.whatsNew;
76+
77+
if (!whatsNewSettings?.lastSeenDate) {
78+
// New user - persist the defaults from init()
79+
whatsNewSettings = this._whatsNewSettings;
80+
accessibility.whatsNew = whatsNewSettings;
81+
user.set('accessibility', JSON.stringify(accessibility));
82+
yield user.save();
8483
}
84+
85+
// Always set (either loaded existing settings or the defaults we just persisted)
86+
this.set('_whatsNewSettings', whatsNewSettings);
87+
88+
// Fetch changelog
89+
this._changelog_response = yield fetch('https://ghost.org/changelog.json');
90+
if (!this._changelog_response.ok) {
91+
// eslint-disable-next-line
92+
return console.error('Failed to fetch changelog', this._changelog_response);
93+
}
94+
95+
let result = yield this._changelog_response.json();
96+
this.set('entries', result.posts || []);
97+
this.set('changelogUrl', result.changelogUrl);
8598
} catch (e) {
8699
console.error(e); // eslint-disable-line
87100
}
88101
}),
89102

90103
updateLastSeen: task(function* () {
91-
let settingsJson = this._user.accessibility || '{}';
92-
let settings = JSON.parse(settingsJson);
93104
let [latestEntry] = this.entries;
94105

95106
if (!latestEntry) {
96107
return;
97108
}
98109

99-
if (!settings.whatsNew) {
100-
settings.whatsNew = {};
101-
}
102-
103-
settings.whatsNew.lastSeenDate = latestEntry.published_at;
104-
105-
this._user.set('accessibility', JSON.stringify(settings));
106-
yield this._user.save();
110+
// Update our local whatsNew settings
111+
this.set('_whatsNewSettings', {
112+
...this._whatsNewSettings,
113+
lastSeenDate: latestEntry.published_at
114+
});
115+
116+
// Persist using read-merge-write pattern
117+
let user = yield this.session.user;
118+
let accessibility = JSON.parse(user.accessibility || '{}');
119+
accessibility.whatsNew = this._whatsNewSettings;
120+
user.set('accessibility', JSON.stringify(accessibility));
121+
yield user.save();
107122
})
108123
});

0 commit comments

Comments
 (0)