Skip to content
This repository has been archived by the owner on Nov 28, 2022. It is now read-only.

Commit

Permalink
🐛 Fixed confusing member count shown in save notification and editor …
Browse files Browse the repository at this point in the history
…header

closes TryGhost/Product#776

Since switching to using a real NQL filter in the `posts.email_recipient_filter` field where we used to show `free members`, `paid members`, or `all members` we were showing `status:free`, `status:-free`, and `status:free,status:-free` respectively. If labels are used in a filter the text became even longer.

- added a `membersCountCache` service
  - `.count(filter)` fetches a numeric count from the members API, if the filter has been counted in the last minute it returns the count directly from a cache instead to avoid hammering the members API when we show counts in multiple places across the UI
  - `.countString(filter)` fetches a count but returns a humanized string with the logic extracted from what we displayed in the confirm email sending modal
- added a `<GhRecipientFilterCount @filter="" />` component that acts as a wrapper around the async count from `membersCountCache`
- updated confirm email send modal, plus save notification and editor status displays for scheduled posts to use the new service and component
  • Loading branch information
kevinansfield committed Jun 11, 2021
1 parent 097c775 commit 27363df
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 36 deletions.
4 changes: 3 additions & 1 deletion app/components/gh-editor-post-status.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
{{/if}}
{{else if @post.isScheduled}}
<time datetime="{{@post.publishedAtUTC}}" class="ml1 green f8" data-test-schedule-countdown>
Will be published {{this.scheduledText}}
Will be published and sent to
<GhRecipientFilterCount @filter={{@post.emailRecipientFilter}} />
{{this.scheduledTime}}
</time>
{{else if @post.isNew}}
New
Expand Down
13 changes: 2 additions & 11 deletions app/components/gh-editor-post-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,14 @@ export default class GhEditorPostStatusComponent extends Component {
return this._isSaving;
}

get scheduledText() {
get scheduledTime() {
// force a recompute every second
get(this.clock, 'second');

let text = [];
const sendEmailWhenPublished = this.args.post.emailRecipientFilter;
if (sendEmailWhenPublished && sendEmailWhenPublished !== 'none') {
text.push(`and sent to ${sendEmailWhenPublished} members`);
}

let formattedTime = formatPostTime(
return formatPostTime(
this.args.post.publishedAtUTC,
{timezone: this.settings.get('timezone'), scheduled: true}
);
text.push(formattedTime);

return text.join(' ');
}

@task({drop: true})
Expand Down
1 change: 1 addition & 0 deletions app/components/gh-recipient-filter-count.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{this.recipientCount}}
28 changes: 28 additions & 0 deletions app/components/gh-recipient-filter-count.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Component from '@glimmer/component';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency-decorators';
import {tracked} from '@glimmer/tracking';

export default class GhRecipientFilterCountComponent extends Component {
@service membersCountCache;

@tracked recipientCount;

constructor() {
super(...arguments);
this.getRecipientCountTask.perform();
}

@task
*getRecipientCountTask() {
if (!this.args.filter) {
this.recipientCount = 'no members';
return;
}

this.recipientCount = yield this.membersCountCache.countString(
`subscribed:true+(${this.args.filter})`,
{knownCount: this.args.knownCount}
);
}
}
16 changes: 1 addition & 15 deletions app/components/modal-confirm-email-send.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,7 @@
{{else}}
<p>
Your post will be delivered to
<strong>
{{#if (eq this.model.sendEmailWhenPublished 'status:-free')}}
{{!-- TODO: remove editor fallback once editors can query member counts --}}
{{if this.session.user.isEditor "all paid members" (gh-pluralize this.memberCount "paid member")}}
{{else if (eq this.model.sendEmailWhenPublished 'status:free')}}
{{!-- TODO: remove editor fallback once editors can query member counts --}}
{{if this.session.user.isEditor "all free members" (gh-pluralize this.memberCount "free member")}}
{{else if (eq this.model.sendEmailWhenPublished 'status:free,status:-free')}}
{{!-- TODO: remove editor fallback once editors can query member counts --}}
{{if this.session.user.isEditor "all members" (gh-pluralize this.memberCount "member")}}
{{else}}
{{!-- TODO: remove editor fallback once editors can query member counts --}}
{{if this.session.user.isEditor "a custom members segment" (gh-pluralize this.memberCount "member")}}
{{/if}}
</strong>
<strong>{{this.memberCount}}</strong>
and will be published on your site{{#if this.model.isScheduled}} at the scheduled time{{/if}}. Sound good?
</p>
{{/if}}
Expand Down
12 changes: 6 additions & 6 deletions app/components/modal-confirm-email-send.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';

export default ModalComponent.extend({
membersCountCache: service(),
session: service(),
store: service(),

Expand All @@ -26,15 +27,14 @@ export default ModalComponent.extend({
},

countRecipients: action(function () {
// TODO: remove editor conditional once editors can query member counts
if (this.model.sendEmailWhenPublished && !this.session.get('user.isEditor')) {
this.countRecipientsTask.perform();
}
this.countRecipientsTask.perform();
}),

countRecipientsTask: task(function* () {
const result = yield this.store.query('member', {filter: `subscribed:true+(${this.model.sendEmailWhenPublished})`, limit: 1, page: 1});
this.set('memberCount', result.meta.pagination.total);
const {sendEmailWhenPublished} = this.model;
const filter = `subscribed:true+(${sendEmailWhenPublished})`;
const result = sendEmailWhenPublished ? yield this.membersCountCache.countString(filter) : 'no members';
this.set('memberCount', result);
}),

confirmAndCheckErrorTask: task(function* () {
Expand Down
7 changes: 4 additions & 3 deletions app/controllers/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const messageMap = {
export default Controller.extend({
application: controller(),
feature: service(),
membersCountCache: service(),
notifications: service(),
router: service(),
slugGenerator: service(),
Expand Down Expand Up @@ -895,7 +896,7 @@ export default Controller.extend({
notifications.showNotification(message, {type: 'success', actions: (actions && actions.htmlSafe()), delayed});
},

_showScheduledNotification(delayed) {
async _showScheduledNotification(delayed) {
let {
publishedAtUTC,
emailRecipientFilter,
Expand All @@ -907,8 +908,8 @@ export default Controller.extend({
let description = ['Will be published'];

if (emailRecipientFilter && emailRecipientFilter !== 'none') {
description.push('and delivered to');
description.push(`<span><strong>${emailRecipientFilter} members</strong></span>`);
const recipientCount = await this.membersCountCache.countString(`subscribed:true+(${emailRecipientFilter})`);
description.push(`and delivered to <span><strong>${recipientCount}</strong></span>`);
}

description.push(`on <span><strong>${publishedAtBlogTZ.format('MMM Do')}</strong></span>`);
Expand Down
79 changes: 79 additions & 0 deletions app/services/members-count-cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import Service from '@ember/service';
import moment from 'moment';
import {ghPluralize} from 'ghost-admin/helpers/gh-pluralize';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency-decorators';

export default class MembersCountCacheService extends Service {
@service session;
@service store;

cache = {};

async count(filter) {
const cachedValue = this.cache[filter];

if (cachedValue && moment().diff(cachedValue.time, 'seconds') > 60) {
return cachedValue.count;
}

const count = this._countMembersTask.perform(filter);

this.cache[filter] = {count, time: moment()};

return count;
}

async countString(filter, {knownCount} = {}) {
const user = await this.session.user;

const basicFilter = filter.replace(/^subscribed:true\+\((.*)\)$/, '$1');
const filterParts = basicFilter.split(',');
const isFree = filterParts.length === 1 && filterParts[0] === 'status:free';
const isPaid = filterParts.length === 1 && filterParts[0] === 'status:-free';
const isAll = filterParts.includes('status:free') && filterParts.includes('status:-free');

// editors don't have permission to browse members so can't retrieve a count
// TODO: remove when editors have relevant permissions or we have a different way of fetching counts
if (user.isEditor && knownCount === undefined) {
if (isFree) {
return 'all free members';
}
if (isPaid) {
return 'all paid members';
}
if (isAll) {
return 'all members';
}

return 'a custom members segment';
}

const recipientCount = knownCount !== undefined ? knownCount : await this.count(filter);

if (isFree) {
return ghPluralize(recipientCount, 'free member');
}

if (isPaid) {
return ghPluralize(recipientCount, 'paid member');
}

return ghPluralize(recipientCount, 'member');
}

@task
*_countMembersTask(filter) {
if (!filter) {
return 0;
}

try {
const result = yield this.store.query('member', {filter, limit: 1, page: 1});
return result.meta.pagination.total;
} catch (e) {
console.error(e); // eslint-disable-line
return 0;
}
}
}

0 comments on commit 27363df

Please sign in to comment.