gristlabs_grist-core/app/client/ui/SupportGristNudge.ts

328 lines
8.3 KiB
TypeScript
Raw Normal View History

import {makeT} from 'app/client/lib/localization';
import {localStorageObs} from 'app/client/lib/localStorageObs';
import {getStorage} from 'app/client/lib/storage';
import {tokenFieldStyles} from 'app/client/lib/TokenField';
import {AppModel} from 'app/client/models/AppModel';
import {urlState} from 'app/client/models/gristUrlState';
import {TelemetryModel, TelemetryModelImpl} from 'app/client/models/TelemetryModel';
import {bigPrimaryButton} from 'app/client/ui2018/buttons';
import {colors, isNarrowScreenObs, theme, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {cssLink} from 'app/client/ui2018/links';
import {commonUrls} from 'app/common/gristUrls';
import {getGristConfig} from 'app/common/urlUtils';
import {Disposable, dom, makeTestId, Observable, styled} from 'grainjs';
const testId = makeTestId('test-support-grist-nudge-');
const t = makeT('SupportGristNudge');
type ButtonState =
| 'collapsed'
| 'expanded';
type CardPage =
| 'support'
| 'opted-in';
/**
* Nudges users to support Grist by opting in to telemetry.
*
* This currently includes a button that opens a card with the nudge.
* The button is hidden when the card is visible, and vice versa.
*/
export class SupportGristNudge extends Disposable {
private readonly _telemetryModel: TelemetryModel = new TelemetryModelImpl(this._appModel);
private readonly _buttonState: Observable<ButtonState>;
private readonly _currentPage: Observable<CardPage>;
private readonly _isClosed: Observable<boolean>;
constructor(private _appModel: AppModel) {
super();
if (!this._shouldShowCardOrButton()) { return; }
this._buttonState = localStorageObs(
`u=${this._appModel.currentValidUser?.id ?? 0};supportGristNudge`, 'expanded'
) as Observable<ButtonState>;
this._currentPage = Observable.create(null, 'support');
this._isClosed = Observable.create(this, false);
}
public showButton() {
if (!this._shouldShowCardOrButton()) { return null; }
return dom.maybe(
use => !use(isNarrowScreenObs()) && (use(this._buttonState) === 'collapsed' && !use(this._isClosed)),
() => this._buildButton()
);
}
public showCard() {
if (!this._shouldShowCardOrButton()) { return null; }
return dom.maybe(
use => !use(isNarrowScreenObs()) && (use(this._buttonState) === 'expanded' && !use(this._isClosed)),
() => this._buildCard()
);
}
private _markAsDismissed() {
this._appModel.dismissedPopup('supportGrist').set(true);
getStorage().removeItem(
`u=${this._appModel.currentValidUser?.id ?? 0};supportGristNudge`);
}
private _close() {
this._isClosed.set(true);
}
private _dismissAndClose() {
this._markAsDismissed();
this._close();
}
private _shouldShowCardOrButton() {
if (this._appModel.dismissedPopups.get().includes('supportGrist')) {
return false;
}
const {activation, deploymentType, telemetry} = getGristConfig();
if (deploymentType !== 'core' || !activation?.isManager) {
return false;
}
if (telemetry && telemetry.telemetryLevel !== 'off') {
return false;
}
return true;
}
private _buildButton() {
return cssContributeButton(
cssButtonIconAndText(
icon('Fireworks'),
t('Contribute'),
),
cssContributeButtonCloseButton(
icon('CrossSmall'),
dom.on('click', (ev) => {
ev.stopPropagation();
this._dismissAndClose();
}),
testId('contribute-button-close'),
),
dom.on('click', () => { this._buttonState.set('expanded'); }),
testId('contribute-button'),
);
}
private _buildCard() {
return cssCard(
dom.domComputed(this._currentPage, page => {
if (page === 'support') {
return this._buildSupportGristCardContent();
} else {
return this._buildOptedInCardContent();
}
}),
testId('card'),
);
}
private _buildSupportGristCardContent() {
return [
cssCloseButton(
icon('CrossBig'),
dom.on('click', () => this._buttonState.set('collapsed')),
testId('card-close'),
),
cssLeftAlignedHeader(t('Support Grist')),
cssParagraph(t(
'Opt in to telemetry to help us understand how the product ' +
'is used, so that we can prioritize future improvements.'
)),
cssParagraph(
t(
'We only collect usage statistics, as detailed in our {{helpCenterLink}}, never ' +
'document contents. Opt out any time from the {{supportGristLink}} in the user menu.',
{
helpCenterLink: helpCenterLink(),
supportGristLink: supportGristLink(),
},
),
),
cssFullWidthButton(
t('Opt in to Telemetry'),
dom.on('click', () => this._optInToTelemetry()),
testId('card-opt-in'),
),
];
}
private _buildOptedInCardContent() {
return [
cssCloseButton(
icon('CrossBig'),
dom.on('click', () => this._close()),
testId('card-close-icon-button'),
),
cssCenteredFlex(cssSparks()),
cssCenterAlignedHeader(t('Opted In')),
cssParagraph(
t(
'Thank you! Your trust and support is greatly appreciated. ' +
'Opt out any time from the {{link}} in the user menu.',
{link: supportGristLink()},
),
),
cssCenteredFlex(
cssPrimaryButton(
t('Close'),
dom.on('click', () => this._close()),
testId('card-close-button'),
),
),
];
}
private async _optInToTelemetry() {
await this._telemetryModel.updateTelemetryPrefs({telemetryLevel: 'limited'});
this._currentPage.set('opted-in');
this._markAsDismissed();
}
}
function helpCenterLink() {
return cssLink(
t('Help Center'),
{href: commonUrls.helpTelemetryLimited, target: '_blank'},
);
}
function supportGristLink() {
return cssLink(
t('Support Grist page'),
{href: urlState().makeUrl({supportGrist: 'support'}), target: '_blank'},
);
}
const cssCenteredFlex = styled('div', `
display: flex;
justify-content: center;
align-items: center;
`);
const cssContributeButton = styled('div', `
position: relative;
background: ${theme.controlPrimaryBg};
color: ${theme.controlPrimaryFg};
border-radius: 25px;
padding: 4px 12px 4px 8px;
font-style: normal;
font-weight: medium;
font-size: 13px;
line-height: 16px;
cursor: pointer;
--icon-color: ${theme.controlPrimaryFg};
&:hover {
background: ${theme.controlPrimaryHoverBg};
}
`);
const cssButtonIconAndText = styled('div', `
display: flex;
gap: 8px;
`);
const cssContributeButtonCloseButton = styled(tokenFieldStyles.cssDeleteButton, `
margin-left: 4px;
vertical-align: bottom;
line-height: 1;
position: absolute;
top: -4px;
right: -8px;
border-radius: 16px;
background-color: ${colors.dark};
width: 18px;
height: 18px;
cursor: pointer;
z-index: 1;
display: none;
align-items: center;
justify-content: center;
.${cssContributeButton.className}:hover & {
display: flex;
}
`);
const cssCard = styled('div', `
width: 297px;
padding: 24px;
color: ${theme.announcementPopupFg};
background: ${theme.announcementPopupBg};
border-radius: 4px;
align-self: flex-start;
position: sticky;
flex-shrink: 0;
top: 0px;
`);
const cssHeader = styled('div', `
font-size: ${vars.xxxlargeFontSize};
font-weight: 600;
margin-bottom: 16px;
`);
const cssLeftAlignedHeader = styled(cssHeader, `
text-align: left;
`);
const cssCenterAlignedHeader = styled(cssHeader, `
text-align: center;
`);
const cssParagraph = styled('div', `
font-size: 13px;
line-height: 18px;
margin-bottom: 12px;
`);
const cssPrimaryButton = styled(bigPrimaryButton, `
display: flex;
justify-content: center;
align-items: center;
margin-top: 32px;
text-align: center;
`);
const cssFullWidthButton = styled(cssPrimaryButton, `
width: 100%;
`);
const cssCloseButton = styled('div', `
position: absolute;
top: 8px;
right: 8px;
padding: 4px;
border-radius: 4px;
cursor: pointer;
--icon-color: ${theme.popupCloseButtonFg};
&:hover {
background-color: ${theme.hover};
}
`);
const cssSparks = styled('div', `
height: 48px;
width: 48px;
background-image: var(--icon-Sparks);
display: inline-block;
background-repeat: no-repeat;
`);