mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-10-11 05:36:36 +08:00
Fix: properly implement Basic limits on read receipts / tracking
This commit is contained in:
parent
24fac237cf
commit
e6fe78cf61
16 changed files with 112 additions and 108 deletions
|
@ -103,10 +103,7 @@ pluginValue = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
messages[1].directlyAttachMetadata(OPEN_TRACKING_ID, pluginValue);
|
messages[1].directlyAttachMetadata(OPEN_TRACKING_ID, pluginValue);
|
||||||
pluginValue = {
|
pluginValue = {};
|
||||||
links: [],
|
|
||||||
tracked: false,
|
|
||||||
};
|
|
||||||
messages[1].directlyAttachMetadata(LINK_TRACKING_ID, pluginValue);
|
messages[1].directlyAttachMetadata(LINK_TRACKING_ID, pluginValue);
|
||||||
pluginValue = {
|
pluginValue = {
|
||||||
open_count: 0,
|
open_count: 0,
|
||||||
|
|
BIN
app/internal_packages/link-tracking/assets/ic-modal-image@2x.png
Normal file
BIN
app/internal_packages/link-tracking/assets/ic-modal-image@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.9 KiB |
|
@ -17,11 +17,6 @@ export default class LinkTrackingButton extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_title(enabled) {
|
|
||||||
const dir = enabled ? 'Disable' : 'Enable';
|
|
||||||
return `${dir} link tracking`;
|
|
||||||
}
|
|
||||||
|
|
||||||
_errorMessage(error) {
|
_errorMessage(error) {
|
||||||
if (
|
if (
|
||||||
error instanceof APIError &&
|
error instanceof APIError &&
|
||||||
|
@ -35,12 +30,10 @@ export default class LinkTrackingButton extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<MetadataComposerToggleButton
|
<MetadataComposerToggleButton
|
||||||
title={this._title}
|
|
||||||
iconName="icon-composer-linktracking.png"
|
iconName="icon-composer-linktracking.png"
|
||||||
pluginId={PLUGIN_ID}
|
pluginId={PLUGIN_ID}
|
||||||
pluginName={PLUGIN_NAME}
|
pluginName={PLUGIN_NAME}
|
||||||
metadataEnabledValue={{ tracked: true }}
|
metadataEnabledValue={{ tracked: true }}
|
||||||
stickyToggle
|
|
||||||
errorMessage={this._errorMessage}
|
errorMessage={this._errorMessage}
|
||||||
draft={this.props.draft}
|
draft={this.props.draft}
|
||||||
session={this.props.session}
|
session={this.props.session}
|
||||||
|
|
|
@ -467,22 +467,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(to right, #6061c7 0%, #4782ad 100%)
|
||||||
to right,
|
|
||||||
rgba(167, 214, 134, 1) 0%,
|
|
||||||
rgba(122, 201, 201, 1) 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes slideIn {
|
|
||||||
from {
|
|
||||||
transform: translate3d(20, 0, 0);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -498,11 +483,7 @@
|
||||||
.steps-container {
|
.steps-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(to right, #696AE8 0%, #4c9ad2 100%);
|
||||||
to right,
|
|
||||||
rgba(149, 205, 107, 1) 0%,
|
|
||||||
rgba(60, 176, 176, 1) 100%
|
|
||||||
);
|
|
||||||
color: white;
|
color: white;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
BIN
app/internal_packages/open-tracking/assets/ic-modal-image@2x.png
Normal file
BIN
app/internal_packages/open-tracking/assets/ic-modal-image@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.4 KiB |
|
@ -17,11 +17,6 @@ export default class OpenTrackingButton extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_title(enabled) {
|
|
||||||
const dir = enabled ? 'Disable' : 'Enable';
|
|
||||||
return `${dir} open tracking`;
|
|
||||||
}
|
|
||||||
|
|
||||||
_errorMessage(error) {
|
_errorMessage(error) {
|
||||||
if (
|
if (
|
||||||
error instanceof APIError &&
|
error instanceof APIError &&
|
||||||
|
@ -40,12 +35,10 @@ export default class OpenTrackingButton extends React.Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MetadataComposerToggleButton
|
<MetadataComposerToggleButton
|
||||||
title={this._title}
|
|
||||||
iconUrl="mailspring://open-tracking/assets/icon-composer-eye@2x.png"
|
iconUrl="mailspring://open-tracking/assets/icon-composer-eye@2x.png"
|
||||||
pluginId={PLUGIN_ID}
|
pluginId={PLUGIN_ID}
|
||||||
pluginName={PLUGIN_NAME}
|
pluginName={PLUGIN_NAME}
|
||||||
metadataEnabledValue={enabledValue}
|
metadataEnabledValue={enabledValue}
|
||||||
stickyToggle
|
|
||||||
errorMessage={this._errorMessage}
|
errorMessage={this._errorMessage}
|
||||||
draft={this.props.draft}
|
draft={this.props.draft}
|
||||||
session={this.props.session}
|
session={this.props.session}
|
||||||
|
|
|
@ -216,7 +216,7 @@ export default class SidebarParticipantProfile extends React.Component {
|
||||||
|
|
||||||
_onClickedToTry = async () => {
|
_onClickedToTry = async () => {
|
||||||
try {
|
try {
|
||||||
await FeatureUsageStore.asyncUseFeature('contact-profiles', {
|
await FeatureUsageStore.markUsedOrUpgrade('contact-profiles', {
|
||||||
usedUpHeader: 'All Contact Previews Used',
|
usedUpHeader: 'All Contact Previews Used',
|
||||||
usagePhrase: 'view contact profiles for',
|
usagePhrase: 'view contact profiles for',
|
||||||
iconUrl: 'mailspring://participant-profile/assets/ic-contact-profile-modal@2x.png',
|
iconUrl: 'mailspring://participant-profile/assets/ic-contact-profile-modal@2x.png',
|
||||||
|
|
|
@ -63,7 +63,7 @@ class SendLaterButton extends Component {
|
||||||
// already set to send later.
|
// already set to send later.
|
||||||
if (!currentSendLaterDate) {
|
if (!currentSendLaterDate) {
|
||||||
try {
|
try {
|
||||||
await FeatureUsageStore.asyncUseFeature('send-later', {
|
await FeatureUsageStore.markUsedOrUpgrade('send-later', {
|
||||||
usedUpHeader: 'All Scheduled Sends Used',
|
usedUpHeader: 'All Scheduled Sends Used',
|
||||||
usagePhrase: 'schedule sending of',
|
usagePhrase: 'schedule sending of',
|
||||||
iconUrl: 'mailspring://send-later/assets/ic-send-later-modal@2x.png',
|
iconUrl: 'mailspring://send-later/assets/ic-send-later-modal@2x.png',
|
||||||
|
|
|
@ -24,7 +24,7 @@ async function incrementMetadataUse(model, expiration) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await FeatureUsageStore.asyncUseFeature(PLUGIN_ID, FEATURE_LEXICON);
|
await FeatureUsageStore.markUsedOrUpgrade(PLUGIN_ID, FEATURE_LEXICON);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof FeatureUsageStore.NoProAccessError) {
|
if (error instanceof FeatureUsageStore.NoProAccessError) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -46,7 +46,7 @@ class SnoozeStore extends MailspringStore {
|
||||||
_onSnoozeThreads = async (threads, snoozeDate, label) => {
|
_onSnoozeThreads = async (threads, snoozeDate, label) => {
|
||||||
try {
|
try {
|
||||||
// ensure the user is authorized to use this feature
|
// ensure the user is authorized to use this feature
|
||||||
await FeatureUsageStore.asyncUseFeature('snooze', {
|
await FeatureUsageStore.markUsedOrUpgrade('snooze', {
|
||||||
usedUpHeader: 'All Snoozes Used',
|
usedUpHeader: 'All Snoozes Used',
|
||||||
usagePhrase: 'snooze',
|
usagePhrase: 'snooze',
|
||||||
iconUrl: 'mailspring://thread-snooze/assets/ic-snooze-modal@2x.png',
|
iconUrl: 'mailspring://thread-snooze/assets/ic-snooze-modal@2x.png',
|
||||||
|
|
|
@ -26,18 +26,18 @@ describe('FeatureUsageStore', function featureUsageStoreSpec() {
|
||||||
IdentityStore._identity = this.oldIdent;
|
IdentityStore._identity = this.oldIdent;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('_isUsable', () => {
|
describe('isUsable', () => {
|
||||||
it("returns true if a feature hasn't met it's quota", () => {
|
it("returns true if a feature hasn't met it's quota", () => {
|
||||||
expect(FeatureUsageStore._isUsable('is-usable')).toBe(true);
|
expect(FeatureUsageStore.isUsable('is-usable')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false if a feature is at its quota', () => {
|
it('returns false if a feature is at its quota', () => {
|
||||||
expect(FeatureUsageStore._isUsable('not-usable')).toBe(false);
|
expect(FeatureUsageStore.isUsable('not-usable')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns true if no quota is present for the feature', () => {
|
it('returns true if no quota is present for the feature', () => {
|
||||||
spyOn(AppEnv, 'reportError');
|
spyOn(AppEnv, 'reportError');
|
||||||
expect(FeatureUsageStore._isUsable('unsupported')).toBe(true);
|
expect(FeatureUsageStore.isUsable('unsupported')).toBe(true);
|
||||||
expect(AppEnv.reportError).toHaveBeenCalled();
|
expect(AppEnv.reportError).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -74,7 +74,7 @@ describe('FeatureUsageStore', function featureUsageStoreSpec() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("marks the feature used if it's usable", async () => {
|
it("marks the feature used if it's usable", async () => {
|
||||||
await FeatureUsageStore.asyncUseFeature('is-usable');
|
await FeatureUsageStore.markUsedOrUpgrade('is-usable');
|
||||||
expect(FeatureUsageStore._markFeatureUsed).toHaveBeenCalled();
|
expect(FeatureUsageStore._markFeatureUsed).toHaveBeenCalled();
|
||||||
expect(FeatureUsageStore._markFeatureUsed.callCount).toBe(1);
|
expect(FeatureUsageStore._markFeatureUsed.callCount).toBe(1);
|
||||||
});
|
});
|
||||||
|
@ -93,7 +93,7 @@ describe('FeatureUsageStore', function featureUsageStoreSpec() {
|
||||||
IdentityStore._identity.featureUsage['not-usable'].quota = 10000;
|
IdentityStore._identity.featureUsage['not-usable'].quota = 10000;
|
||||||
FeatureUsageStore._onModalClose();
|
FeatureUsageStore._onModalClose();
|
||||||
});
|
});
|
||||||
await FeatureUsageStore.asyncUseFeature('not-usable', this.lexicon);
|
await FeatureUsageStore.markUsedOrUpgrade('not-usable', this.lexicon);
|
||||||
expect(Actions.openModal).toHaveBeenCalled();
|
expect(Actions.openModal).toHaveBeenCalled();
|
||||||
expect(Actions.openModal.calls.length).toBe(1);
|
expect(Actions.openModal.calls.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
@ -103,7 +103,7 @@ describe('FeatureUsageStore', function featureUsageStoreSpec() {
|
||||||
IdentityStore._identity.featureUsage['not-usable'].quota = 10000;
|
IdentityStore._identity.featureUsage['not-usable'].quota = 10000;
|
||||||
FeatureUsageStore._onModalClose();
|
FeatureUsageStore._onModalClose();
|
||||||
});
|
});
|
||||||
await FeatureUsageStore.asyncUseFeature('not-usable', this.lexicon);
|
await FeatureUsageStore.markUsedOrUpgrade('not-usable', this.lexicon);
|
||||||
expect(Actions.openModal).toHaveBeenCalled();
|
expect(Actions.openModal).toHaveBeenCalled();
|
||||||
expect(Actions.openModal.calls.length).toBe(1);
|
expect(Actions.openModal.calls.length).toBe(1);
|
||||||
const component = Actions.openModal.calls[0].args[0].component;
|
const component = Actions.openModal.calls[0].args[0].component;
|
||||||
|
@ -122,7 +122,7 @@ describe('FeatureUsageStore', function featureUsageStoreSpec() {
|
||||||
FeatureUsageStore._onModalClose();
|
FeatureUsageStore._onModalClose();
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await FeatureUsageStore.asyncUseFeature('not-usable', this.lexicon);
|
await FeatureUsageStore.markUsedOrUpgrade('not-usable', this.lexicon);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
expect(err instanceof FeatureUsageStore.NoProAccessError).toBe(true);
|
expect(err instanceof FeatureUsageStore.NoProAccessError).toBe(true);
|
||||||
caughtError = true;
|
caughtError = true;
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
import { React, PropTypes, Actions, MailspringAPIRequest, APIError } from 'mailspring-exports';
|
import {
|
||||||
|
React,
|
||||||
|
PropTypes,
|
||||||
|
Actions,
|
||||||
|
MailspringAPIRequest,
|
||||||
|
APIError,
|
||||||
|
FeatureUsageStore,
|
||||||
|
} from 'mailspring-exports';
|
||||||
import { RetinaImg } from 'mailspring-component-kit';
|
import { RetinaImg } from 'mailspring-component-kit';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import _ from 'underscore';
|
import _ from 'underscore';
|
||||||
|
@ -7,34 +14,33 @@ export default class MetadataComposerToggleButton extends React.Component {
|
||||||
static displayName = 'MetadataComposerToggleButton';
|
static displayName = 'MetadataComposerToggleButton';
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
title: PropTypes.func.isRequired,
|
|
||||||
iconUrl: PropTypes.string,
|
iconUrl: PropTypes.string,
|
||||||
iconName: PropTypes.string,
|
iconName: PropTypes.string,
|
||||||
pluginId: PropTypes.string.isRequired,
|
pluginId: PropTypes.string.isRequired,
|
||||||
pluginName: PropTypes.string.isRequired,
|
pluginName: PropTypes.string.isRequired,
|
||||||
metadataEnabledValue: PropTypes.object.isRequired,
|
metadataEnabledValue: PropTypes.object.isRequired,
|
||||||
stickyToggle: PropTypes.bool,
|
|
||||||
errorMessage: PropTypes.func.isRequired,
|
errorMessage: PropTypes.func.isRequired,
|
||||||
|
|
||||||
draft: PropTypes.object.isRequired,
|
draft: PropTypes.object.isRequired,
|
||||||
session: PropTypes.object.isRequired,
|
session: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
stickyToggle: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
pending: false,
|
pending: false,
|
||||||
|
onByDefaultButUsedUp: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
if (this._isEnabledByDefault() && !this._isEnabled()) {
|
if (this._isEnabledByDefault() && !this._isEnabled()) {
|
||||||
this._setEnabled(true);
|
if (FeatureUsageStore.isUsable(this.props.pluginId)) {
|
||||||
|
this._setEnabled(true);
|
||||||
|
} else {
|
||||||
|
this.setState({ onByDefaultButUsedUp: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,11 +67,9 @@ export default class MetadataComposerToggleButton extends React.Component {
|
||||||
try {
|
try {
|
||||||
session.changes.addPluginMetadata(pluginId, metadataValue);
|
session.changes.addPluginMetadata(pluginId, metadataValue);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const { stickyToggle, errorMessage } = this.props;
|
const { errorMessage } = this.props;
|
||||||
|
|
||||||
if (stickyToggle) {
|
AppEnv.config.set(this._configKey(), false);
|
||||||
AppEnv.config.set(this._configKey(), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
let title = 'Error';
|
let title = 'Error';
|
||||||
if (!(error instanceof APIError)) {
|
if (!(error instanceof APIError)) {
|
||||||
|
@ -82,23 +86,41 @@ export default class MetadataComposerToggleButton extends React.Component {
|
||||||
this.setState({ pending: false });
|
this.setState({ pending: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
_onClick = () => {
|
_onClick = async () => {
|
||||||
|
const { pluginName, pluginId } = this.props;
|
||||||
|
let nextEnabled = !this._isEnabled();
|
||||||
|
const dir = nextEnabled ? 'Enabled' : 'Disabled';
|
||||||
|
|
||||||
if (this.state.pending) {
|
if (this.state.pending) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const enabled = this._isEnabled();
|
// note: we don't actually increment the usage counters until you /send/
|
||||||
const dir = enabled ? 'Disabled' : 'Enabled';
|
// the message with link and open tracking, we just display the notice
|
||||||
Actions.recordUserEvent(`${this.props.pluginName} ${dir}`);
|
|
||||||
if (this.props.stickyToggle) {
|
if (nextEnabled && !FeatureUsageStore.isUsable(pluginId)) {
|
||||||
AppEnv.config.set(this._configKey(), !enabled);
|
try {
|
||||||
|
await FeatureUsageStore.displayUpgradeModal(pluginId, {
|
||||||
|
usedUpHeader: `All used up!`,
|
||||||
|
usagePhrase: 'get open and click notifications for',
|
||||||
|
iconUrl: `mailspring://${pluginId}/assets/ic-modal-image@2x.png`,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// user does not have access to this feature
|
||||||
|
if (this.state.onByDefaultButUsedUp) {
|
||||||
|
this.setState({ onByDefaultButUsedUp: false });
|
||||||
|
}
|
||||||
|
nextEnabled = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this._setEnabled(!enabled);
|
|
||||||
|
Actions.recordUserEvent(`${pluginName} ${dir}`);
|
||||||
|
AppEnv.config.set(this._configKey(), nextEnabled);
|
||||||
|
this._setEnabled(nextEnabled);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const enabled = this._isEnabled();
|
const enabled = this._isEnabled();
|
||||||
const title = this.props.title(enabled);
|
|
||||||
|
|
||||||
const className = classnames({
|
const className = classnames({
|
||||||
btn: true,
|
btn: true,
|
||||||
|
@ -115,7 +137,17 @@ export default class MetadataComposerToggleButton extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={className} onClick={this._onClick} title={title} tabIndex={-1}>
|
<button
|
||||||
|
className={className}
|
||||||
|
onClick={this._onClick}
|
||||||
|
title={`${enabled ? 'Disable' : 'Enable'} ${this.props.pluginName}`}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{this.state.onByDefaultButUsedUp ? (
|
||||||
|
<div style={{ position: 'absolute', zIndex: 2, transform: 'translate(14px, -4px)' }}>
|
||||||
|
<RetinaImg name="tiny-warning-sign.png" mode={RetinaImg.Mode.ContentPreserve} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<RetinaImg {...attrs} mode={RetinaImg.Mode.ContentIsMask} />
|
<RetinaImg {...attrs} mode={RetinaImg.Mode.ContentIsMask} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -68,7 +68,7 @@ class FeatureUsageStore extends MailspringStore {
|
||||||
this._usub();
|
this._usub();
|
||||||
}
|
}
|
||||||
|
|
||||||
displayUpgradeModal(feature, { lexicon }) {
|
displayUpgradeModal(feature, lexicon) {
|
||||||
const { headerText, rechargeText } = this._modalText(feature, lexicon);
|
const { headerText, rechargeText } = this._modalText(feature, lexicon);
|
||||||
|
|
||||||
Actions.openModal({
|
Actions.openModal({
|
||||||
|
@ -83,25 +83,45 @@ class FeatureUsageStore extends MailspringStore {
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
async asyncUseFeature(feature, lexicon = {}) {
|
|
||||||
if (this._isUsable(feature)) {
|
|
||||||
this._markFeatureUsed(feature);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.displayUpgradeModal(feature, { lexicon });
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this._waitForModalClose.push({ resolve, reject, feature });
|
this._waitForModalClose.push({ resolve, reject, feature });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isUsable(feature) {
|
||||||
|
const { usedInPeriod, quota } = this._dataForFeature(feature);
|
||||||
|
if (!quota) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return usedInPeriod < quota;
|
||||||
|
}
|
||||||
|
|
||||||
|
async markUsedOrUpgrade(feature, lexicon = {}) {
|
||||||
|
if (!this.isUsable(feature)) {
|
||||||
|
// throws if the user declines
|
||||||
|
await this.displayUpgradeModal(feature, lexicon);
|
||||||
|
}
|
||||||
|
this.markUsed(feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
markUsed(feature) {
|
||||||
|
const next = JSON.parse(JSON.stringify(IdentityStore.identity()));
|
||||||
|
console.log('Next:');
|
||||||
|
console.log(JSON.stringify(next));
|
||||||
|
|
||||||
|
if (next.featureUsage[feature]) {
|
||||||
|
next.featureUsage[feature].usedInPeriod += 1;
|
||||||
|
IdentityStore.saveIdentity(next);
|
||||||
|
}
|
||||||
|
if (!UsageRecordedServerSide.includes(feature)) {
|
||||||
|
Actions.queueTask(new SendFeatureUsageEventTask({ feature }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_onModalClose = async () => {
|
_onModalClose = async () => {
|
||||||
for (const { feature, resolve, reject } of this._waitForModalClose) {
|
for (const { feature, resolve, reject } of this._waitForModalClose) {
|
||||||
if (this._isUsable(feature)) {
|
if (this.isUsable(feature)) {
|
||||||
this._markFeatureUsed(feature);
|
|
||||||
resolve();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
reject(new NoProAccessError(feature));
|
reject(new NoProAccessError(feature));
|
||||||
|
@ -145,25 +165,6 @@ class FeatureUsageStore extends MailspringStore {
|
||||||
}
|
}
|
||||||
return usage[feature];
|
return usage[feature];
|
||||||
}
|
}
|
||||||
|
|
||||||
_isUsable(feature) {
|
|
||||||
const { usedInPeriod, quota } = this._dataForFeature(feature);
|
|
||||||
if (!quota) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return usedInPeriod < quota;
|
|
||||||
}
|
|
||||||
|
|
||||||
_markFeatureUsed(feature) {
|
|
||||||
const next = JSON.parse(JSON.stringify(IdentityStore.identity()));
|
|
||||||
if (next.featureUsage[feature]) {
|
|
||||||
next.featureUsage[feature].usedInPeriod += 1;
|
|
||||||
IdentityStore.saveIdentity(next);
|
|
||||||
}
|
|
||||||
if (!UsageRecordedServerSide.includes(feature)) {
|
|
||||||
Actions.queueTask(new SendFeatureUsageEventTask({ feature }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new FeatureUsageStore();
|
export default new FeatureUsageStore();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/* eslint global-require: 0 */
|
/* eslint global-require: 0 */
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
import AccountStore from '../stores/account-store';
|
import AccountStore from '../stores/account-store';
|
||||||
|
import FeatureUsageStore from '../stores/feature-usage-store';
|
||||||
import Task from './task';
|
import Task from './task';
|
||||||
import Actions from '../actions';
|
import Actions from '../actions';
|
||||||
import Attributes from '../attributes';
|
import Attributes from '../attributes';
|
||||||
|
@ -57,13 +58,11 @@ export default class SendDraftTask extends Task {
|
||||||
}
|
}
|
||||||
|
|
||||||
isOpenTrackingEnabled() {
|
isOpenTrackingEnabled() {
|
||||||
const metadata = this.draft.metadataForPluginId('open-tracking');
|
return !!this.draft.metadataForPluginId('open-tracking');
|
||||||
return metadata && Object.keys(metadata).length > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isLinkTrackingEnabled() {
|
isLinkTrackingEnabled() {
|
||||||
const metadata = this.draft.metadataForPluginId('link-tracking');
|
return !!this.draft.metadataForPluginId('link-tracking');
|
||||||
return metadata && Object.keys(metadata).length > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
label() {
|
label() {
|
||||||
|
@ -97,6 +96,14 @@ export default class SendDraftTask extends Task {
|
||||||
if (AppEnv.config.get('core.sending.sounds') && !this.silent) {
|
if (AppEnv.config.get('core.sending.sounds') && !this.silent) {
|
||||||
SoundRegistry.playSound('send');
|
SoundRegistry.playSound('send');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fire off events to record the usage of open and link tracking
|
||||||
|
if (this.isOpenTrackingEnabled()) {
|
||||||
|
FeatureUsageStore.markUsed('open-tracking');
|
||||||
|
}
|
||||||
|
if (this.isLinkTrackingEnabled()) {
|
||||||
|
FeatureUsageStore.markUsed('link-tracking');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onError({ key, debuginfo }) {
|
onError({ key, debuginfo }) {
|
||||||
|
|
BIN
app/static/images/toolbar/tiny-warning-sign@2x.png
Normal file
BIN
app/static/images/toolbar/tiny-warning-sign@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
2
mailsync
2
mailsync
|
@ -1 +1 @@
|
||||||
Subproject commit eaf7c846dd4caf81e399ed1073df961711eaba23
|
Subproject commit 4c6e9e557f98ae7c950ac651f153b329ac2b7225
|
Loading…
Add table
Reference in a new issue