mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-01 13:14:16 +08:00
Replace participant sidebar data source, introduce free version query limit
This commit is contained in:
parent
170b05120d
commit
ec9b771c30
12 changed files with 270 additions and 220 deletions
Binary file not shown.
After Width: | Height: | Size: 94 KiB |
|
@ -1,93 +0,0 @@
|
|||
import { MailspringAPIRequest } from 'mailspring-exports';
|
||||
const { makeRequest } = MailspringAPIRequest;
|
||||
|
||||
const MAX_RETRY = 10;
|
||||
|
||||
export default class ClearbitDataSource {
|
||||
async find({ email, tryCount = 0 }) {
|
||||
if (tryCount >= MAX_RETRY) {
|
||||
return null;
|
||||
}
|
||||
let body = null;
|
||||
try {
|
||||
body = await makeRequest({
|
||||
server: 'identity',
|
||||
method: 'GET',
|
||||
path: `/api/info-for-email/${email}`,
|
||||
});
|
||||
} catch (err) {
|
||||
// we don't care about errors returned by this clearbit proxy
|
||||
}
|
||||
return await this.parseResponse(body, email, tryCount);
|
||||
}
|
||||
|
||||
parseResponse(body = {}, requestedEmail, tryCount = 0) {
|
||||
// This means it's in the process of fetching. Return null so we don't
|
||||
// cache and try again.
|
||||
return new Promise((resolve, reject) => {
|
||||
if (body.error) {
|
||||
if (body.error.type === 'queued') {
|
||||
setTimeout(() => {
|
||||
this.find({
|
||||
email: requestedEmail,
|
||||
tryCount: tryCount + 1,
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}, 1000);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let person = body.person;
|
||||
|
||||
// This means there was no data about the person available. Return a
|
||||
// valid, but empty object for us to cache. This can happen when we
|
||||
// have company data, but no personal data.
|
||||
if (!person) {
|
||||
person = { email: requestedEmail };
|
||||
}
|
||||
|
||||
resolve({
|
||||
cacheDate: Date.now(),
|
||||
email: requestedEmail, // Used as checksum
|
||||
bio:
|
||||
person.bio ||
|
||||
(person.twitter && person.twitter.bio) ||
|
||||
(person.aboutme && person.aboutme.bio),
|
||||
location: person.location || (person.geo && person.geo.city) || null,
|
||||
currentTitle: person.employment && person.employment.title,
|
||||
currentEmployer: person.employment && person.employment.name,
|
||||
profilePhotoUrl: person.avatar,
|
||||
rawClearbitData: body,
|
||||
socialProfiles: this._socialProfiles(person),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_socialProfiles(person = {}) {
|
||||
const profiles = {};
|
||||
|
||||
if (((person.twitter && person.twitter.handle) || '').length > 0) {
|
||||
profiles.twitter = {
|
||||
handle: person.twitter.handle,
|
||||
url: `https://twitter.com/${person.twitter.handle}`,
|
||||
};
|
||||
}
|
||||
if (((person.facebook && person.facebook.handle) || '').length > 0) {
|
||||
profiles.facebook = {
|
||||
handle: person.facebook.handle,
|
||||
url: `https://facebook.com/${person.facebook.handle}`,
|
||||
};
|
||||
}
|
||||
if (((person.linkedin && person.linkedin.handle) || '').length > 0) {
|
||||
profiles.linkedin = {
|
||||
handle: person.linkedin.handle,
|
||||
url: `https://linkedin.com/${person.linkedin.handle}`,
|
||||
};
|
||||
}
|
||||
return profiles;
|
||||
}
|
||||
}
|
|
@ -1,10 +1,8 @@
|
|||
import { ComponentRegistry } from 'mailspring-exports';
|
||||
import ParticipantProfileStore from './participant-profile-store';
|
||||
import SidebarParticipantProfile from './sidebar-participant-profile';
|
||||
import SidebarRelatedThreads from './sidebar-related-threads';
|
||||
|
||||
export function activate() {
|
||||
ParticipantProfileStore.activate();
|
||||
ComponentRegistry.register(SidebarParticipantProfile, { role: 'MessageListSidebar:ContactCard' });
|
||||
ComponentRegistry.register(SidebarRelatedThreads, { role: 'MessageListSidebar:ContactCard' });
|
||||
}
|
||||
|
@ -12,7 +10,6 @@ export function activate() {
|
|||
export function deactivate() {
|
||||
ComponentRegistry.unregister(SidebarParticipantProfile);
|
||||
ComponentRegistry.unregister(SidebarRelatedThreads);
|
||||
ParticipantProfileStore.deactivate();
|
||||
}
|
||||
|
||||
export function serialize() {}
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
import { MailspringAPIRequest, Utils } from 'mailspring-exports';
|
||||
const { makeRequest } = MailspringAPIRequest;
|
||||
|
||||
const CACHE_SIZE = 200;
|
||||
const CACHE_INDEX_KEY = 'pp-cache-keys';
|
||||
const CACHE_KEY_PREFIX = 'pp-cache-';
|
||||
|
||||
class ParticipantProfileDataSource {
|
||||
constructor() {
|
||||
try {
|
||||
this._cacheIndex = JSON.parse(window.localStorage.getItem(CACHE_INDEX_KEY) || `[]`);
|
||||
} catch (err) {
|
||||
this._cacheIndex = [];
|
||||
}
|
||||
}
|
||||
|
||||
async find(email) {
|
||||
if (!email || Utils.likelyNonHumanEmail(email)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const data = this.getCache(email);
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
let body = null;
|
||||
|
||||
try {
|
||||
body = await makeRequest({
|
||||
server: 'identity',
|
||||
method: 'GET',
|
||||
path: `/api/info-for-email-v2/${email}`,
|
||||
});
|
||||
} catch (err) {
|
||||
// we don't care about errors returned by this clearbit proxy
|
||||
return {};
|
||||
}
|
||||
|
||||
let person = (body || {}).person;
|
||||
|
||||
// This means there was no data about the person available. Return a
|
||||
// valid, but empty object for us to cache. This can happen when we
|
||||
// have company data, but no personal data.
|
||||
if (!person) {
|
||||
person = { email };
|
||||
}
|
||||
|
||||
const result = {
|
||||
cacheDate: Date.now(),
|
||||
email: email, // Used as checksum
|
||||
bio:
|
||||
person.bio ||
|
||||
(person.twitter && person.twitter.bio) ||
|
||||
(person.aboutme && person.aboutme.bio),
|
||||
location: person.location || (person.geo && person.geo.city) || null,
|
||||
currentTitle: person.employment && person.employment.title,
|
||||
currentEmployer: person.employment && person.employment.name,
|
||||
profilePhotoUrl: person.avatar,
|
||||
rawClearbitData: body,
|
||||
socialProfiles: this._socialProfiles(person),
|
||||
};
|
||||
|
||||
this.setCache(email, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
_socialProfiles(person = {}) {
|
||||
const profiles = {};
|
||||
|
||||
if (((person.twitter && person.twitter.handle) || '').length > 0) {
|
||||
profiles.twitter = {
|
||||
handle: person.twitter.handle,
|
||||
url: `https://twitter.com/${person.twitter.handle}`,
|
||||
};
|
||||
}
|
||||
if (((person.facebook && person.facebook.handle) || '').length > 0) {
|
||||
profiles.facebook = {
|
||||
handle: person.facebook.handle,
|
||||
url: `https://facebook.com/${person.facebook.handle}`,
|
||||
};
|
||||
}
|
||||
if (((person.linkedin && person.linkedin.handle) || '').length > 0) {
|
||||
profiles.linkedin = {
|
||||
handle: person.linkedin.handle,
|
||||
url: `https://linkedin.com/${person.linkedin.handle}`,
|
||||
};
|
||||
}
|
||||
return profiles;
|
||||
}
|
||||
|
||||
// LocalStorage Retrieval / Saving
|
||||
|
||||
hasCache(email) {
|
||||
return localStorage.getItem(`${CACHE_KEY_PREFIX}${email}`) !== null;
|
||||
}
|
||||
|
||||
getCache(email) {
|
||||
const raw = localStorage.getItem(`${CACHE_KEY_PREFIX}${email}`);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
setCache(email, value) {
|
||||
localStorage.setItem(`${CACHE_KEY_PREFIX}${email}`, JSON.stringify(value));
|
||||
const updatedIndex = this._cacheIndex.filter(e => e !== email);
|
||||
updatedIndex.push(email);
|
||||
|
||||
if (updatedIndex.length > CACHE_SIZE) {
|
||||
const oldestKey = updatedIndex.shift();
|
||||
localStorage.removeItem(`${CACHE_KEY_PREFIX}${oldestKey}`);
|
||||
}
|
||||
|
||||
localStorage.setItem(CACHE_INDEX_KEY, JSON.stringify(updatedIndex));
|
||||
this._cacheIndex = updatedIndex;
|
||||
}
|
||||
}
|
||||
|
||||
export default new ParticipantProfileDataSource();
|
|
@ -1,82 +0,0 @@
|
|||
import { Utils } from 'mailspring-exports';
|
||||
import MailspringStore from 'mailspring-store';
|
||||
import ClearbitDataSource from './clearbit-data-source';
|
||||
|
||||
const contactCache = {};
|
||||
const CACHE_SIZE = 100;
|
||||
const contactCacheKeyIndex = [];
|
||||
|
||||
// TODO: Put cache into localstorage
|
||||
|
||||
class ParticipantProfileStore extends MailspringStore {
|
||||
constructor() {
|
||||
super();
|
||||
this.cacheExpiry = 1000 * 60 * 60 * 24; // 1 day
|
||||
this.dataSource = new ClearbitDataSource();
|
||||
}
|
||||
|
||||
activate() {}
|
||||
|
||||
deactivate() {
|
||||
// no op
|
||||
}
|
||||
|
||||
dataForContact(contact) {
|
||||
if (!contact) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (Utils.likelyNonHumanEmail(contact.email)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (this.inCache(contact)) {
|
||||
const data = this.getCache(contact);
|
||||
if (data.cacheDate) {
|
||||
return data;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
this.dataSource
|
||||
.find({ email: contact.email })
|
||||
.then(data => {
|
||||
if (data && data.email === contact.email) {
|
||||
this.setCache(contact, data);
|
||||
this.trigger();
|
||||
}
|
||||
})
|
||||
.catch((err = {}) => {
|
||||
if (err.statusCode !== 404) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
return {};
|
||||
}
|
||||
|
||||
getCache(contact) {
|
||||
return contactCache[contact.email];
|
||||
}
|
||||
|
||||
inCache(contact) {
|
||||
const cache = contactCache[contact.email];
|
||||
if (!cache) {
|
||||
return false;
|
||||
}
|
||||
if (!cache.cacheDate || Date.now() - cache.cacheDate > this.cacheExpiry) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
setCache(contact, value) {
|
||||
contactCache[contact.email] = value;
|
||||
contactCacheKeyIndex.push(contact.email);
|
||||
if (contactCacheKeyIndex.length > CACHE_SIZE) {
|
||||
delete contactCache[contactCacheKeyIndex.shift()];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export default new ParticipantProfileStore();
|
|
@ -1,6 +1,29 @@
|
|||
import { React, PropTypes, DOMUtils, RegExpUtils, Utils } from 'mailspring-exports';
|
||||
import {
|
||||
IdentityStore,
|
||||
FeatureUsageStore,
|
||||
React,
|
||||
PropTypes,
|
||||
DOMUtils,
|
||||
RegExpUtils,
|
||||
Utils,
|
||||
} from 'mailspring-exports';
|
||||
import { RetinaImg } from 'mailspring-component-kit';
|
||||
import ParticipantProfileStore from './participant-profile-store';
|
||||
|
||||
import ParticipantProfileDataSource from './participant-profile-data-source';
|
||||
|
||||
/* We expect ParticipantProfileDataSource.find to return the
|
||||
* following schema:
|
||||
* {
|
||||
* profilePhotoUrl: string
|
||||
* bio: string
|
||||
* location: string
|
||||
* currentTitle: string
|
||||
* currentEmployer: string
|
||||
* socialProfiles: hash keyed by type: ('twitter', 'facebook' etc)
|
||||
* url: string
|
||||
* handle: string
|
||||
* }
|
||||
*/
|
||||
|
||||
export default class SidebarParticipantProfile extends React.Component {
|
||||
static displayName = 'SidebarParticipantProfile';
|
||||
|
@ -17,30 +40,50 @@ export default class SidebarParticipantProfile extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
/* We expect ParticipantProfileStore.dataForContact to return the
|
||||
* following schema:
|
||||
* {
|
||||
* profilePhotoUrl: string
|
||||
* bio: string
|
||||
* location: string
|
||||
* currentTitle: string
|
||||
* currentEmployer: string
|
||||
* socialProfiles: hash keyed by type: ('twitter', 'facebook' etc)
|
||||
* url: string
|
||||
* handle: string
|
||||
* }
|
||||
*/
|
||||
this.state = ParticipantProfileStore.dataForContact(props.contact);
|
||||
this.state = {
|
||||
loaded: false,
|
||||
loading: false,
|
||||
trialing: !IdentityStore.hasProFeatures(),
|
||||
};
|
||||
const contactState = ParticipantProfileDataSource.getCache(props.contact.email);
|
||||
if (contactState) {
|
||||
this.state = Object.assign(this.state, { loaded: true }, contactState);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.usub = ParticipantProfileStore.listen(() => {
|
||||
this.setState(ParticipantProfileStore.dataForContact(this.props.contact));
|
||||
});
|
||||
this._mounted = true;
|
||||
if (!this.state.loaded && !this.state.trialing) {
|
||||
this._findContact();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.usub();
|
||||
this._mounted = false;
|
||||
}
|
||||
|
||||
_onClickedToTry = async () => {
|
||||
try {
|
||||
await FeatureUsageStore.asyncUseFeature('contact-profiles', {
|
||||
usedUpHeader: 'All Contact Previews Used',
|
||||
usagePhrase: 'view contact profiles for',
|
||||
iconUrl: 'mailspring://participant-profile/assets/ic-contact-profile-modal@2x.png',
|
||||
});
|
||||
} catch (err) {
|
||||
// user does not have access to this feature
|
||||
return;
|
||||
}
|
||||
this._findContact();
|
||||
};
|
||||
|
||||
async _findContact() {
|
||||
this.setState({ loading: true });
|
||||
ParticipantProfileDataSource.find(this.props.contact.email).then(result => {
|
||||
if (!this._mounted) {
|
||||
return;
|
||||
}
|
||||
this.setState(Object.assign({ loading: false, loaded: true }, result));
|
||||
});
|
||||
}
|
||||
|
||||
_renderProfilePhoto() {
|
||||
|
@ -199,12 +242,35 @@ export default class SidebarParticipantProfile extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_renderFindCTA() {
|
||||
if (!this.state.trialing || this.state.loaded) {
|
||||
return;
|
||||
}
|
||||
if (!this.props.contact.email || Utils.likelyNonHumanEmail(this.props.contact.email)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<p>
|
||||
The contact sidebar in Mailspring Pro shows information about the people and companies
|
||||
you're emailing with.
|
||||
</p>
|
||||
<div className="btn" onClick={!this.state.loading ? this._onClickedToTry : null}>
|
||||
{!this.state.loading ? `Try it Now` : `Loading...`}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="participant-profile">
|
||||
{this._renderProfilePhoto()}
|
||||
{this._renderCorePersonalInfo()}
|
||||
{this._renderAdditionalInfo()}
|
||||
|
||||
{this._renderFindCTA()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
@import 'ui-variables';
|
||||
|
||||
.related-threads {
|
||||
width: calc(~"100% + 30px");
|
||||
width: calc(~'100% + 30px');
|
||||
position: relative;
|
||||
left: -15px;
|
||||
border-top: 1px solid rgba(0,0,0,0.15);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.15);
|
||||
transition: height 150ms ease-in-out;
|
||||
top: 15px;
|
||||
margin-top: -15px;
|
||||
|
@ -17,7 +17,7 @@
|
|||
color: @text-color-very-subtle;
|
||||
width: 100%;
|
||||
padding: 0.5em 15px;
|
||||
border-top: 1px solid rgba(0,0,0,0.08);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
|
||||
&:hover {
|
||||
background: @list-hover-bg;
|
||||
|
@ -40,7 +40,7 @@
|
|||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding: 0.5em 15px;
|
||||
border-top: 1px solid rgba(0,0,0,0.08);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
color: @text-color-link;
|
||||
}
|
||||
}
|
||||
|
@ -53,9 +53,9 @@
|
|||
height: 50px;
|
||||
border-radius: @border-radius-base;
|
||||
padding: 3px;
|
||||
box-shadow: 0 0 1px rgba(0,0,0,0.5);
|
||||
box-shadow: 0 0 1px rgba(0, 0, 0, 0.5);
|
||||
position: absolute;
|
||||
left: calc(~"50% - 25px");
|
||||
left: calc(~'50% - 25px');
|
||||
top: -31px;
|
||||
background: @background-primary;
|
||||
|
||||
|
@ -66,7 +66,8 @@
|
|||
width: 44px;
|
||||
height: 44px;
|
||||
|
||||
img, .default-profile-image {
|
||||
img,
|
||||
.default-profile-image {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
@ -76,8 +77,12 @@
|
|||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
box-shadow: inset 0 0 1px rgba(0,0,0,0.18);
|
||||
background-image: linear-gradient(to bottom, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0) 100%);
|
||||
box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.18);
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, 0.15) 0%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -87,7 +92,8 @@
|
|||
text-align: center;
|
||||
margin-bottom: @spacing-standard;
|
||||
|
||||
.full-name, .email {
|
||||
.full-name,
|
||||
.email {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
@ -129,3 +135,22 @@ body.platform-win32 {
|
|||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-usage-modal.contact-profiles {
|
||||
@send-later-color: #777ff0;
|
||||
.feature-header {
|
||||
@from: @send-later-color;
|
||||
@to: lighten(@send-later-color, 10%);
|
||||
background: linear-gradient(to top, @from, @to);
|
||||
}
|
||||
.feature-name {
|
||||
color: @send-later-color;
|
||||
}
|
||||
.pro-description {
|
||||
li {
|
||||
&:before {
|
||||
color: @send-later-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -525,6 +525,7 @@ Utils =
|
|||
"notification[s]?#{at}"
|
||||
"support#{at}"
|
||||
"alert[s]?#{at}"
|
||||
"notify",
|
||||
"news#{at}"
|
||||
"info#{at}"
|
||||
"automated#{at}"
|
||||
|
|
|
@ -8,6 +8,8 @@ import SendFeatureUsageEventTask from '../tasks/send-feature-usage-event-task';
|
|||
|
||||
class NoProAccessError extends Error {}
|
||||
|
||||
const UsageRecordedServerSide = ['contact-profiles'];
|
||||
|
||||
/**
|
||||
* FeatureUsageStore is backed by the IdentityStore
|
||||
*
|
||||
|
@ -158,7 +160,9 @@ class FeatureUsageStore extends MailspringStore {
|
|||
next.featureUsage[feature].usedInPeriod += 1;
|
||||
IdentityStore.saveIdentity(next);
|
||||
}
|
||||
Actions.queueTask(new SendFeatureUsageEventTask({ feature }));
|
||||
if (!UsageRecordedServerSide.includes(feature)) {
|
||||
Actions.queueTask(new SendFeatureUsageEventTask({ feature }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,6 +50,10 @@ class IdentityStore extends MailspringStore {
|
|||
return this._identity.id;
|
||||
}
|
||||
|
||||
hasProFeatures() {
|
||||
return this._identity && this._identity.stripePlanEffective !== 'Basic';
|
||||
}
|
||||
|
||||
_fetchAndPollRemoteIdentity() {
|
||||
if (!AppEnv.isMainWindow()) return;
|
||||
setTimeout(() => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {ComponentRegistry} from 'mailspring-exports';
|
||||
import { ComponentRegistry } from 'mailspring-exports';
|
||||
|
||||
import MyComposerButton from './my-composer-button';
|
||||
import MyMessageSidebar from './my-message-sidebar';
|
||||
|
@ -19,8 +19,7 @@ export function activate() {
|
|||
// You can return a state object that will be passed back to your package
|
||||
// when it is re-activated.
|
||||
//
|
||||
export function serialize() {
|
||||
}
|
||||
export function serialize() {}
|
||||
|
||||
// This **optional** method is called when the window is shutting down,
|
||||
// or when your package is being updated or disabled. If your package is
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
import {ComponentRegistry} from 'mailspring-exports';
|
||||
import {activate, deactivate} from '../lib/main';
|
||||
import { ComponentRegistry } from 'mailspring-exports';
|
||||
import { activate, deactivate } from '../lib/main';
|
||||
|
||||
import MyMessageSidebar from '../lib/my-message-sidebar';
|
||||
import MyComposerButton from '../lib/my-composer-button';
|
||||
|
||||
describe("activate", () => {
|
||||
it("should register the composer button and sidebar", () => {
|
||||
describe('activate', () => {
|
||||
it('should register the composer button and sidebar', () => {
|
||||
spyOn(ComponentRegistry, 'register');
|
||||
activate();
|
||||
expect(ComponentRegistry.register).toHaveBeenCalledWith(MyComposerButton, {role: 'Composer:ActionButton'});
|
||||
expect(ComponentRegistry.register).toHaveBeenCalledWith(MyMessageSidebar, {role: 'MessageListSidebar:ContactCard'});
|
||||
expect(ComponentRegistry.register).toHaveBeenCalledWith(MyComposerButton, {
|
||||
role: 'Composer:ActionButton',
|
||||
});
|
||||
expect(ComponentRegistry.register).toHaveBeenCalledWith(MyMessageSidebar, {
|
||||
role: 'MessageListSidebar:ContactCard',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("deactivate", () => {
|
||||
it("should unregister the composer button and sidebar", () => {
|
||||
describe('deactivate', () => {
|
||||
it('should unregister the composer button and sidebar', () => {
|
||||
spyOn(ComponentRegistry, 'unregister');
|
||||
deactivate();
|
||||
expect(ComponentRegistry.unregister).toHaveBeenCalledWith(MyComposerButton);
|
||||
|
|
Loading…
Reference in a new issue