import $ from 'jquery'; import Ember from 'ember'; import EmberError from '@ember/error'; import Service, {inject as service} from '@ember/service'; import classic from 'ember-classic-decorator'; import {computed, set} from '@ember/object'; import {inject} from 'ghost-admin/decorators/inject'; export function feature(name, options = {}) { let {user, onChange} = options; let watchedProps = user ? [`accessibility.${name}`] : [`config.${name}`, `labs.${name}`]; return computed.apply(Ember, watchedProps.concat({ get() { let enabled = false; if (user) { enabled = this.get(`accessibility.${name}`); } else if (this.get(`config.${name}`)) { enabled = this.get(`config.${name}`); } else { enabled = this.get(`labs.${name}`) || false; } return enabled; }, set(key, value) { this.update(key, value, options); if (onChange) { // value must be passed here because the value isn't set until // the setter function returns this.get(onChange).bind(this)(value); } return value; } })); } @classic export default class FeatureService extends Service { @service lazyLoader; @service notifications; @service session; @service settings; @service store; @inject config; // features @feature('emailAnalytics') emailAnalytics; // user-specific flags @feature('nightShift', {user: true, onChange: '_setAdminTheme'}) nightShift; // user-specific referral invitation @feature('referralInviteDismissed', {user: true}) referralInviteDismissed; // labs flags @feature('urlCache') urlCache; @feature('memberAttribution') memberAttribution; @feature('sourceAttribution') sourceAttribution; @feature('lexicalEditor') lexicalEditor; @feature('lexicalMultiplayer') lexicalMultiplayer; @feature('audienceFeedback') audienceFeedback; @feature('suppressionList') suppressionList; @feature('webmentions') webmentions; @feature('emailErrors') emailErrors; @feature('websockets') websockets; @feature('stripeAutomaticTax') stripeAutomaticTax; @feature('makingItRain') makingItRain; @feature('migrateApp') migrateApp; @feature('i18n') i18n; @feature('postHistory') postHistory; @feature('announcementBar') announcementBar; _user = null; @computed('settings.labs') get labs() { let labs = this.settings.labs; try { return JSON.parse(labs) || {}; } catch (e) { return {}; } } @computed('_user.accessibility') get accessibility() { let accessibility = this.get('_user.accessibility'); try { return JSON.parse(accessibility) || {}; } catch (e) { return {}; } } fetch() { return this.settings.fetch().then(() => { this.set('_user', this.session.user); return this._setAdminTheme().then(() => true); }); } update(key, value, options = {}) { let serviceProperty = options.user ? 'accessibility' : 'labs'; let model = this.get(options.user ? '_user' : 'settings'); let featureObject = this.get(serviceProperty); // set the new key value for either the labs property or the accessibility property set(featureObject, key, value); if (options.requires && value === true) { options.requires.forEach((flag) => { set(featureObject, flag, true); }); } // update the 'labs' or 'accessibility' key of the model model.set(serviceProperty, JSON.stringify(featureObject)); return model.save().then(() => { // return the labs key value that we get from the server this.notifyPropertyChange(serviceProperty); return this.get(`${serviceProperty}.${key}`); }).catch((error) => { model.rollbackAttributes(); this.notifyPropertyChange(serviceProperty); // we'll always have an errors object unless we hit a // validation error if (!error) { throw new EmberError(`Validation of the feature service ${options.user ? 'user' : 'settings'} model failed when updating ${serviceProperty}.`); } this.notifications.showAPIError(error); return this.get(`${serviceProperty}.${key}`); }); } _setAdminTheme(enabled) { let nightShift = enabled; if (typeof nightShift === 'undefined') { nightShift = enabled || this.nightShift; } return this.lazyLoader.loadStyle('dark', 'assets/ghost-dark.css', true).then(() => { $('link[title=dark]').prop('disabled', !nightShift); }).catch(() => { //TODO: Also disable toggle from settings and Labs hover $('link[title=dark]').prop('disabled', true); }); } }