2023-07-04 21:21:34 +00:00
|
|
|
import {makeT} from 'app/client/lib/localization';
|
|
|
|
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';
|
2024-03-23 17:11:06 +00:00
|
|
|
import {basicButton, basicButtonLink, bigPrimaryButton} from 'app/client/ui2018/buttons';
|
2024-09-12 17:10:55 +00:00
|
|
|
import {colors, testId, theme, vars} from 'app/client/ui2018/cssVars';
|
2023-07-04 21:21:34 +00:00
|
|
|
import {icon} from 'app/client/ui2018/icons';
|
|
|
|
import {cssLink} from 'app/client/ui2018/links';
|
2024-09-12 17:10:55 +00:00
|
|
|
import {modal} from 'app/client/ui2018/modals';
|
2024-03-23 17:11:06 +00:00
|
|
|
import {commonUrls, isFeatureEnabled} from 'app/common/gristUrls';
|
2023-07-04 21:21:34 +00:00
|
|
|
import {getGristConfig} from 'app/common/urlUtils';
|
2024-09-12 17:10:55 +00:00
|
|
|
import {Computed, Disposable, dom, DomContents, Observable, styled} from 'grainjs';
|
2023-07-04 21:21:34 +00:00
|
|
|
|
|
|
|
const t = makeT('SupportGristNudge');
|
|
|
|
|
|
|
|
/**
|
2024-09-12 17:10:55 +00:00
|
|
|
* Button that nudges users to support Grist by opting in to telemetry or sponsoring on Github.
|
2024-03-23 17:11:06 +00:00
|
|
|
*
|
2024-09-12 17:10:55 +00:00
|
|
|
* For installation admins, this includes a modal with a nudge which collapses into a "Support
|
2024-03-23 17:11:06 +00:00
|
|
|
* Grist" button in the top bar. When that's not applicable, it is only a "Support Grist" button
|
|
|
|
* that links to the Github sponsorship page.
|
2023-07-04 21:21:34 +00:00
|
|
|
*
|
2024-09-12 17:10:55 +00:00
|
|
|
* Users can dismiss this button.
|
2023-07-04 21:21:34 +00:00
|
|
|
*/
|
2024-09-12 17:10:55 +00:00
|
|
|
export class SupportGristButton extends Disposable {
|
|
|
|
private readonly _showButton: Computed<null|'link'|'expand'>;
|
2024-03-23 17:11:06 +00:00
|
|
|
private readonly _telemetryModel: TelemetryModel = TelemetryModelImpl.create(this, this._appModel);
|
2023-07-04 21:21:34 +00:00
|
|
|
|
2024-03-23 17:11:06 +00:00
|
|
|
constructor(private _appModel: AppModel) {
|
|
|
|
super();
|
|
|
|
const {deploymentType, telemetry} = getGristConfig();
|
|
|
|
const isEnabled = (deploymentType === 'core') && isFeatureEnabled("supportGrist");
|
|
|
|
const isAdmin = _appModel.isInstallAdmin();
|
|
|
|
const isTelemetryOn = (telemetry && telemetry.telemetryLevel !== 'off');
|
|
|
|
const isAdminNudgeApplicable = isAdmin && !isTelemetryOn;
|
|
|
|
|
|
|
|
this._showButton = Computed.create(this, use => {
|
2024-09-12 17:10:55 +00:00
|
|
|
if (!isEnabled || use(_appModel.dismissedPopups).includes('supportGrist')) {
|
|
|
|
return null;
|
|
|
|
}
|
2024-03-23 17:11:06 +00:00
|
|
|
|
2024-09-12 17:10:55 +00:00
|
|
|
return isAdminNudgeApplicable ? 'expand' : 'link';
|
2024-03-23 17:11:06 +00:00
|
|
|
});
|
2023-07-04 21:21:34 +00:00
|
|
|
}
|
|
|
|
|
2024-09-12 17:10:55 +00:00
|
|
|
public buildDom(): DomContents {
|
2024-03-23 17:11:06 +00:00
|
|
|
return dom.domComputed(this._showButton, (which) => {
|
|
|
|
if (!which) { return null; }
|
|
|
|
const elemType = (which === 'link') ? basicButtonLink : basicButton;
|
|
|
|
return cssContributeButton(
|
|
|
|
elemType(cssHeartIcon('💛 '), t('Support Grist'),
|
|
|
|
(which === 'link' ?
|
|
|
|
{href: commonUrls.githubSponsorGristLabs, target: '_blank'} :
|
2024-09-12 17:10:55 +00:00
|
|
|
dom.on('click', () => this._buildNudgeModal())
|
2024-03-23 17:11:06 +00:00
|
|
|
),
|
|
|
|
|
|
|
|
cssContributeButtonCloseButton(
|
|
|
|
icon('CrossSmall'),
|
|
|
|
dom.on('click', (ev) => {
|
|
|
|
ev.stopPropagation();
|
|
|
|
ev.preventDefault();
|
2024-09-12 17:10:55 +00:00
|
|
|
this._markDismissed();
|
2024-03-23 17:11:06 +00:00
|
|
|
}),
|
|
|
|
testId('support-grist-button-dismiss'),
|
|
|
|
),
|
|
|
|
testId('support-grist-button'),
|
|
|
|
)
|
|
|
|
);
|
|
|
|
});
|
2023-07-04 21:21:34 +00:00
|
|
|
}
|
|
|
|
|
2024-09-12 17:10:55 +00:00
|
|
|
private _buildNudgeModal() {
|
|
|
|
return modal((ctl, owner) => {
|
|
|
|
const currentStep = Observable.create<'opt-in' | 'opted-in'>(owner, 'opt-in');
|
|
|
|
|
|
|
|
return [
|
|
|
|
cssModal.cls(''),
|
|
|
|
cssCloseButton(
|
|
|
|
icon('CrossBig'),
|
|
|
|
dom.on('click', () => ctl.close()),
|
|
|
|
testId('support-nudge-close'),
|
2024-03-23 17:11:06 +00:00
|
|
|
),
|
2024-09-12 17:10:55 +00:00
|
|
|
dom.domComputed(currentStep, (step) => {
|
|
|
|
return step === 'opt-in'
|
|
|
|
? this._buildOptInScreen(async () => {
|
|
|
|
await this._optInToTelemetry();
|
|
|
|
currentStep.set('opted-in');
|
|
|
|
})
|
|
|
|
: this._buildOptedInScreen(() => ctl.close());
|
|
|
|
}),
|
|
|
|
];
|
|
|
|
}, {});
|
2023-07-04 21:21:34 +00:00
|
|
|
}
|
|
|
|
|
2024-09-12 17:10:55 +00:00
|
|
|
private _buildOptInScreen(onOptIn: () => Promise<void>) {
|
2023-07-04 21:21:34 +00:00
|
|
|
return [
|
|
|
|
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(),
|
2024-03-23 17:11:06 +00:00
|
|
|
supportGristLink: adminPanelLink(),
|
2023-07-04 21:21:34 +00:00
|
|
|
},
|
|
|
|
),
|
|
|
|
),
|
|
|
|
cssFullWidthButton(
|
|
|
|
t('Opt in to Telemetry'),
|
2024-09-12 17:10:55 +00:00
|
|
|
dom.on('click', () => onOptIn()),
|
2024-03-23 17:11:06 +00:00
|
|
|
testId('support-nudge-opt-in'),
|
2023-07-04 21:21:34 +00:00
|
|
|
),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2024-09-12 17:10:55 +00:00
|
|
|
private _buildOptedInScreen(onClose: () => void) {
|
2023-07-04 21:21:34 +00:00
|
|
|
return [
|
|
|
|
cssCenteredFlex(cssSparks()),
|
|
|
|
cssCenterAlignedHeader(t('Opted In')),
|
|
|
|
cssParagraph(
|
|
|
|
t(
|
2024-04-15 07:55:57 +00:00
|
|
|
'Thank you! Your trust and support is greatly appreciated.\
|
|
|
|
Opt out any time from the {{link}} in the user menu.',
|
2024-03-23 17:11:06 +00:00
|
|
|
{link: adminPanelLink()},
|
2023-07-04 21:21:34 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
cssCenteredFlex(
|
|
|
|
cssPrimaryButton(
|
|
|
|
t('Close'),
|
2024-09-12 17:10:55 +00:00
|
|
|
dom.on('click', () => onClose()),
|
2024-03-23 17:11:06 +00:00
|
|
|
testId('support-nudge-close-button'),
|
2023-07-04 21:21:34 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2024-03-23 17:11:06 +00:00
|
|
|
private _markDismissed() {
|
|
|
|
this._appModel.dismissPopup('supportGrist', true);
|
|
|
|
}
|
|
|
|
|
2023-07-04 21:21:34 +00:00
|
|
|
private async _optInToTelemetry() {
|
|
|
|
await this._telemetryModel.updateTelemetryPrefs({telemetryLevel: 'limited'});
|
2024-03-23 17:11:06 +00:00
|
|
|
this._markDismissed();
|
2023-07-04 21:21:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function helpCenterLink() {
|
|
|
|
return cssLink(
|
|
|
|
t('Help Center'),
|
|
|
|
{href: commonUrls.helpTelemetryLimited, target: '_blank'},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-03-23 17:11:06 +00:00
|
|
|
function adminPanelLink() {
|
2023-07-04 21:21:34 +00:00
|
|
|
return cssLink(
|
2024-03-23 17:11:06 +00:00
|
|
|
t('Admin Panel'),
|
|
|
|
{href: urlState().makeUrl({adminPanel: 'admin'}), target: '_blank'},
|
2023-07-04 21:21:34 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const cssCenteredFlex = styled('div', `
|
|
|
|
display: flex;
|
|
|
|
justify-content: center;
|
|
|
|
align-items: center;
|
|
|
|
`);
|
|
|
|
|
2024-09-12 17:10:55 +00:00
|
|
|
const cssContributeButton = styled('div', ``);
|
2023-07-04 21:21:34 +00:00
|
|
|
|
|
|
|
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;
|
2024-03-23 17:11:06 +00:00
|
|
|
--icon-color: ${colors.light};
|
2023-07-04 21:21:34 +00:00
|
|
|
|
|
|
|
.${cssContributeButton.className}:hover & {
|
|
|
|
display: flex;
|
|
|
|
}
|
2024-03-23 17:11:06 +00:00
|
|
|
&:hover {
|
|
|
|
--icon-color: ${colors.lightGreen};
|
|
|
|
}
|
2023-07-04 21:21:34 +00:00
|
|
|
`);
|
|
|
|
|
|
|
|
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;
|
2023-09-21 16:57:58 +00:00
|
|
|
--icon-color: ${theme.popupCloseButtonFg};
|
2023-07-04 21:21:34 +00:00
|
|
|
|
|
|
|
&:hover {
|
2023-09-21 16:57:58 +00:00
|
|
|
background-color: ${theme.hover};
|
2023-07-04 21:21:34 +00:00
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
const cssSparks = styled('div', `
|
|
|
|
height: 48px;
|
|
|
|
width: 48px;
|
|
|
|
background-image: var(--icon-Sparks);
|
|
|
|
display: inline-block;
|
|
|
|
background-repeat: no-repeat;
|
|
|
|
`);
|
2024-03-23 17:11:06 +00:00
|
|
|
|
|
|
|
// This is just to avoid the emoji pushing the button to be taller.
|
|
|
|
const cssHeartIcon = styled('span', `
|
|
|
|
line-height: 1;
|
|
|
|
`);
|
2024-09-12 17:10:55 +00:00
|
|
|
|
|
|
|
const cssModal = styled('div', `
|
|
|
|
position: relative;
|
|
|
|
width: 100%;
|
|
|
|
max-width: 400px;
|
|
|
|
min-width: 0px;
|
|
|
|
`);
|