2022-12-20 02:06:39 +00:00
|
|
|
import {showBehavioralPrompt} from 'app/client/components/modals';
|
|
|
|
import {AppModel} from 'app/client/models/AppModel';
|
2023-01-13 07:39:33 +00:00
|
|
|
import {getUserPrefObs} from 'app/client/models/UserPrefs';
|
2022-12-20 02:06:39 +00:00
|
|
|
import {GristBehavioralPrompts} from 'app/client/ui/GristTooltips';
|
|
|
|
import {isNarrowScreen} from 'app/client/ui2018/cssVars';
|
2023-01-13 07:39:33 +00:00
|
|
|
import {BehavioralPrompt, BehavioralPromptPrefs} from 'app/common/Prefs';
|
2022-12-20 16:37:11 +00:00
|
|
|
import {getGristConfig} from 'app/common/urlUtils';
|
2023-01-13 07:39:33 +00:00
|
|
|
import {Computed, Disposable, dom, Observable} from 'grainjs';
|
2022-12-20 02:06:39 +00:00
|
|
|
import {IPopupOptions} from 'popweasel';
|
|
|
|
|
|
|
|
export interface AttachOptions {
|
2023-03-27 17:25:25 +00:00
|
|
|
/** Defaults to false. */
|
|
|
|
forceShow?: boolean;
|
2022-12-20 02:06:39 +00:00
|
|
|
/** Defaults to false. */
|
|
|
|
hideArrow?: boolean;
|
2023-03-27 17:25:25 +00:00
|
|
|
/** Defaults to false. */
|
|
|
|
hideDontShowTips?: boolean;
|
|
|
|
/** Defaults to true. */
|
|
|
|
markAsSeen?: boolean;
|
|
|
|
/** Defaults to false. */
|
|
|
|
showOnMobile?: boolean;
|
2022-12-20 02:06:39 +00:00
|
|
|
popupOptions?: IPopupOptions;
|
|
|
|
onDispose?(): void;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface QueuedTip {
|
|
|
|
prompt: BehavioralPrompt;
|
|
|
|
refElement: Element;
|
|
|
|
options: AttachOptions;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Manages tips that are shown the first time a user performs some action.
|
|
|
|
*
|
|
|
|
* Tips are shown in the order that they are attached.
|
|
|
|
*/
|
2023-01-13 07:39:33 +00:00
|
|
|
export class BehavioralPromptsManager extends Disposable {
|
2023-03-29 19:52:04 +00:00
|
|
|
private _isDisabled: boolean = false;
|
|
|
|
|
2023-01-13 07:39:33 +00:00
|
|
|
private readonly _prefs = getUserPrefObs(this._appModel.userPrefsObs, 'behavioralPrompts',
|
|
|
|
{ defaultValue: { dontShowTips: false, dismissedTips: [] } }) as Observable<BehavioralPromptPrefs>;
|
|
|
|
|
2022-12-20 02:06:39 +00:00
|
|
|
private _dismissedTips: Computed<Set<BehavioralPrompt>> = Computed.create(this, use => {
|
|
|
|
const {dismissedTips} = use(this._prefs);
|
|
|
|
return new Set(dismissedTips.filter(BehavioralPrompt.guard));
|
|
|
|
});
|
2023-01-13 07:39:33 +00:00
|
|
|
|
2022-12-20 02:06:39 +00:00
|
|
|
private _queuedTips: QueuedTip[] = [];
|
|
|
|
|
|
|
|
constructor(private _appModel: AppModel) {
|
|
|
|
super();
|
|
|
|
}
|
|
|
|
|
|
|
|
public showTip(refElement: Element, prompt: BehavioralPrompt, options: AttachOptions = {}) {
|
|
|
|
this._queueTip(refElement, prompt, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
public attachTip(prompt: BehavioralPrompt, options: AttachOptions = {}) {
|
|
|
|
return (element: Element) => {
|
|
|
|
this._queueTip(element, prompt, options);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public hasSeenTip(prompt: BehavioralPrompt) {
|
|
|
|
return this._dismissedTips.get().has(prompt);
|
|
|
|
}
|
|
|
|
|
2023-03-29 13:14:30 +00:00
|
|
|
public shouldShowTips() {
|
|
|
|
return !this._prefs.get().dontShowTips;
|
|
|
|
}
|
|
|
|
|
2023-03-29 19:52:04 +00:00
|
|
|
public enable() {
|
|
|
|
this._isDisabled = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
public disable() {
|
|
|
|
this._isDisabled = true;
|
|
|
|
}
|
|
|
|
|
2023-04-14 10:09:50 +00:00
|
|
|
public reset() {
|
|
|
|
this._prefs.set({...this._prefs.get(), dismissedTips: [], dontShowTips: false});
|
|
|
|
this.enable();
|
|
|
|
}
|
|
|
|
|
2022-12-20 02:06:39 +00:00
|
|
|
private _queueTip(refElement: Element, prompt: BehavioralPrompt, options: AttachOptions) {
|
2022-12-20 16:37:11 +00:00
|
|
|
if (
|
2023-03-29 19:52:04 +00:00
|
|
|
this._isDisabled ||
|
2022-12-20 16:37:11 +00:00
|
|
|
// Don't show tips if surveying is disabled.
|
|
|
|
// TODO: Move this into a dedicated variable - this is only a short-term fix for hiding
|
|
|
|
// tips in grist-core.
|
2023-03-27 17:25:25 +00:00
|
|
|
(!getGristConfig().survey && prompt !== 'rickRow') ||
|
|
|
|
// Or if this tip shouldn't be shown on mobile.
|
|
|
|
(isNarrowScreen() && !options.showOnMobile) ||
|
2022-12-20 16:37:11 +00:00
|
|
|
// Or if "Don't show tips" was checked in the past.
|
2023-03-27 17:25:25 +00:00
|
|
|
(this._prefs.get().dontShowTips && !options.forceShow) ||
|
2022-12-20 16:37:11 +00:00
|
|
|
// Or if this tip has been shown and dismissed in the past.
|
|
|
|
this.hasSeenTip(prompt)
|
|
|
|
) {
|
2022-12-20 02:06:39 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._queuedTips.push({prompt, refElement, options});
|
|
|
|
if (this._queuedTips.length > 1) {
|
|
|
|
// If we're already showing a tip, wait for that one to be dismissed, which will
|
|
|
|
// cause the next one in the queue to be shown.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._showTip(refElement, prompt, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
private _showTip(refElement: Element, prompt: BehavioralPrompt, options: AttachOptions) {
|
|
|
|
const close = () => {
|
|
|
|
if (!ctl.isDisposed()) {
|
|
|
|
ctl.close();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-03-27 17:25:25 +00:00
|
|
|
const {hideArrow, hideDontShowTips, markAsSeen = true, onDispose, popupOptions} = options;
|
2022-12-20 02:06:39 +00:00
|
|
|
const {title, content} = GristBehavioralPrompts[prompt];
|
2023-01-31 16:35:46 +00:00
|
|
|
const ctl = showBehavioralPrompt(refElement, title(), content(), {
|
2022-12-20 02:06:39 +00:00
|
|
|
onClose: (dontShowTips) => {
|
|
|
|
if (dontShowTips) { this._dontShowTips(); }
|
2023-03-27 17:25:25 +00:00
|
|
|
if (markAsSeen) { this._markAsSeen(prompt); }
|
2022-12-20 02:06:39 +00:00
|
|
|
},
|
|
|
|
hideArrow,
|
|
|
|
popupOptions,
|
2023-03-27 17:25:25 +00:00
|
|
|
hideDontShowTips,
|
2022-12-20 02:06:39 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
ctl.onDispose(() => {
|
|
|
|
onDispose?.();
|
|
|
|
this._showNextQueuedTip();
|
|
|
|
});
|
|
|
|
dom.onElem(refElement, 'click', () => close());
|
|
|
|
dom.onDisposeElem(refElement, () => close());
|
|
|
|
}
|
|
|
|
|
|
|
|
private _showNextQueuedTip() {
|
|
|
|
this._queuedTips.shift();
|
|
|
|
if (this._queuedTips.length !== 0) {
|
|
|
|
const [nextTip] = this._queuedTips;
|
|
|
|
const {refElement, prompt, options} = nextTip;
|
|
|
|
this._showTip(refElement, prompt, options);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private _markAsSeen(prompt: BehavioralPrompt) {
|
|
|
|
const {dismissedTips} = this._prefs.get();
|
|
|
|
const newDismissedTips = new Set(dismissedTips);
|
|
|
|
newDismissedTips.add(prompt);
|
|
|
|
this._prefs.set({...this._prefs.get(), dismissedTips: [...newDismissedTips]});
|
|
|
|
}
|
|
|
|
|
|
|
|
private _dontShowTips() {
|
|
|
|
this._prefs.set({...this._prefs.get(), dontShowTips: true});
|
|
|
|
this._queuedTips = [];
|
|
|
|
}
|
|
|
|
}
|