microsoft/vscode-react-native

Public

mirrored from https://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.9.2

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/extension/services/tipsNotificationsService/tipsNotificationService.ts

486lines · modeblame

f338085detatanova4 years ago1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT license. See LICENSE file in the project root for details.
3
09f6024fHeniker4 years ago4import * as path from "path";
f338085detatanova4 years ago5import * as vscode from "vscode";
6import { IConfig, retryDownloadConfig } from "../remoteConfigHelper";
ab0238b7RedMickey4 years ago7import { TelemetryHelper } from "../../../common/telemetryHelper";
8import { Telemetry } from "../../../common/telemetry";
9import { ExtensionConfigManager } from "../../extensionConfigManager";
10import { findFileInFolderHierarchy } from "../../../common/extensionHelper";
11import { SettingsHelper } from "../../settingsHelper";
12import { OutputChannelLogger } from "../../log/OutputChannelLogger";
13import { areSameDates, getRandomIntInclusive } from "../../../common/utils";
09f6024fHeniker4 years ago14import tipsStorage from "./tipsStorage";
f338085detatanova4 years ago15
16enum TipNotificationAction {
17GET_MORE_INFO = "tipsMoreInfo",
18DO_NOT_SHOW_AGAIN = "tipsDoNotShow",
19SHOWN = "tipShown",
20}
21
22export interface TipNotificationConfig extends IConfig {
23firstTimeMinDaysToRemind: number;
24firstTimeMaxDaysToRemind: number;
25minDaysToRemind: number;
26maxDaysToRemind: number;
27daysAfterLastUsage: number;
28}
29
30export interface TipInfo {
31knownDate?: Date;
32shownDate?: Date;
33}
34
35export interface Tips {
36[tipId: string]: TipInfo;
37}
38
39export interface AllTips {
40generalTips: Tips;
41specificTips: Tips;
42}
43
44export interface TipsConfig extends TipNotificationConfig {
45daysLeftBeforeGeneralTip: number;
46lastExtensionUsageDate?: Date;
47allTipsShownFirstly: boolean;
48tips: AllTips;
49}
50
51export interface GeneratedTipResponse {
52selection: string | undefined;
53tipKey: string;
54}
55
56export class TipNotificationService implements vscode.Disposable {
57private static instance: TipNotificationService;
58
59private readonly TIPS_NOTIFICATIONS_LOG_CHANNEL_NAME: string;
60private readonly TIPS_CONFIG_NAME: string;
61private readonly endpointURL: string;
62private readonly downloadConfigRequest: Promise<TipNotificationConfig>;
63private readonly getMoreInfoButtonText: string;
64private readonly doNotShowTipsAgainButtonText: string;
65
66private cancellationTokenSource: vscode.CancellationTokenSource;
67private _tipsConfig: TipsConfig | null;
68private logger: OutputChannelLogger;
69private showTips: boolean;
70
71public static getInstance(): TipNotificationService {
72if (!TipNotificationService.instance) {
73TipNotificationService.instance = new TipNotificationService();
74}
75
76return TipNotificationService.instance;
77}
78
79public dispose(): void {
80this.cancellationTokenSource.cancel();
81this.cancellationTokenSource.dispose();
82}
83
84private constructor() {
85this.endpointURL =
86"https://microsoft.github.io/vscode-react-native/tipsNotifications/tipsNotificationsConfig.json";
87this.TIPS_NOTIFICATIONS_LOG_CHANNEL_NAME = "Tips Notifications";
88this.TIPS_CONFIG_NAME = "tipsConfig";
89this.getMoreInfoButtonText = "Get more info";
90this.doNotShowTipsAgainButtonText = "Don't show tips again";
91
92this.cancellationTokenSource = new vscode.CancellationTokenSource();
93this._tipsConfig = null;
94this.downloadConfigRequest = retryDownloadConfig<TipNotificationConfig>(
95this.endpointURL,
96this.cancellationTokenSource,
97);
98this.showTips = SettingsHelper.getShowTips();
b49f7119etatanova4 years ago99this.logger = OutputChannelLogger.getChannel(
100this.TIPS_NOTIFICATIONS_LOG_CHANNEL_NAME,
101true,
102);
f338085detatanova4 years ago103}
104
105public async showTipNotification(
106isGeneralTip: boolean = true,
107specificTipKey?: string,
108): Promise<void> {
109if (!isGeneralTip && !specificTipKey) {
110this.logger.debug("The specific tip key parameter isn't passed for a specific tip");
111return;
112}
113
114await this.initializeTipsConfig();
115
116if (!this.showTips) {
117return;
118}
119
120const curDate: Date = new Date();
121let tipResponse: GeneratedTipResponse | undefined;
122
123if (isGeneralTip) {
124this.deleteOutdatedKnownDate();
125if (this.tipsConfig.daysLeftBeforeGeneralTip === 0) {
126tipResponse = await this.showRandomGeneralTipNotification();
09f6024fHeniker4 years ago127} else if (
128this.tipsConfig.lastExtensionUsageDate &&
129!areSameDates(curDate, this.tipsConfig.lastExtensionUsageDate)
130) {
131this.tipsConfig.daysLeftBeforeGeneralTip--;
f338085detatanova4 years ago132}
133} else {
134tipResponse = await this.showSpecificTipNotification(<string>specificTipKey);
135}
136
137if (tipResponse) {
138await this.handleUserActionOnTip(tipResponse, isGeneralTip);
139}
140
141this.tipsConfig.lastExtensionUsageDate = curDate;
142ExtensionConfigManager.config.set(this.TIPS_CONFIG_NAME, this.tipsConfig);
143}
144
145public async setKnownDateForFeatureById(
146key: string,
147isGeneralTip: boolean = true,
148): Promise<void> {
149await this.initializeTipsConfig();
150
151if (isGeneralTip) {
152this.tipsConfig.tips.generalTips[key].knownDate = new Date();
153} else {
154this.tipsConfig.tips.specificTips[key].knownDate = new Date();
155}
156
157ExtensionConfigManager.config.set(this.TIPS_CONFIG_NAME, this.tipsConfig);
158}
159
b49f7119etatanova4 years ago160public updateTipsConfig(): void {
161if (!ExtensionConfigManager.config.has(this.TIPS_CONFIG_NAME)) {
162return;
163}
164
165const tipsConfig = this.tipsConfig;
166
167tipsConfig.tips.generalTips = this.updateConfigTipsFromStorage(
168tipsStorage.generalTips,
169tipsConfig.tips.generalTips,
170);
171
172tipsConfig.tips.specificTips = this.updateConfigTipsFromStorage(
173tipsStorage.specificTips,
174tipsConfig.tips.specificTips,
175);
176
177this._tipsConfig = tipsConfig;
178ExtensionConfigManager.config.set(this.TIPS_CONFIG_NAME, tipsConfig);
179}
180
181private updateConfigTipsFromStorage(
182storageTips: Record<string, unknown>,
183configTips: Tips,
184): Tips {
09f6024fHeniker4 years ago185// eslint-disable-next-line no-restricted-syntax
186for (const key in configTips) {
b49f7119etatanova4 years ago187if (!(key in storageTips)) {
188delete configTips[key];
189}
190}
191
09f6024fHeniker4 years ago192// eslint-disable-next-line no-restricted-syntax
193for (const key in storageTips) {
b49f7119etatanova4 years ago194if (!(key in configTips)) {
195configTips[key] = {};
196}
197}
198
199return configTips;
200}
201
f338085detatanova4 years ago202private get tipsConfig(): TipsConfig {
203if (!this._tipsConfig) {
204if (!ExtensionConfigManager.config.has(this.TIPS_CONFIG_NAME)) {
205throw new Error("Could not find Tips config in the config store.");
206} else {
207this._tipsConfig = this.parseDatesInRawConfig(
208ExtensionConfigManager.config.get(this.TIPS_CONFIG_NAME),
209);
210}
211}
212return this._tipsConfig;
213}
214
215private async handleUserActionOnTip(
216tipResponse: GeneratedTipResponse,
217isGeneralTip: boolean,
218): Promise<void> {
219const { selection, tipKey } = tipResponse;
220
221if (selection === this.getMoreInfoButtonText) {
222this.sendTipNotificationActionTelemetry(tipKey, TipNotificationAction.GET_MORE_INFO);
223
224const readmeFile: string | null = findFileInFolderHierarchy(__dirname, "README.md");
225
226if (readmeFile) {
227const anchorLink: string = isGeneralTip
228? this.getGeneralTipNotificationAnchorLinkByKey(tipKey)
229: this.getSpecificTipNotificationAnchorLinkByKey(tipKey);
230
231const uriFile = vscode.Uri.parse(
232path.normalize(`file://${readmeFile}${anchorLink}`),
233);
234
09f6024fHeniker4 years ago235void vscode.commands.executeCommand("markdown.showPreview", uriFile);
f338085detatanova4 years ago236}
237}
238
239if (selection === this.doNotShowTipsAgainButtonText) {
240this.sendTipNotificationActionTelemetry(
241tipKey,
242TipNotificationAction.DO_NOT_SHOW_AGAIN,
243);
244this.showTips = false;
245await SettingsHelper.setShowTips(this.showTips);
246}
247}
248
249private async initializeTipsConfig(): Promise<void> {
250this.showTips = SettingsHelper.getShowTips();
251if (this._tipsConfig) {
252return;
253}
254
255let tipsConfig: TipsConfig;
256if (!ExtensionConfigManager.config.has(this.TIPS_CONFIG_NAME)) {
257tipsConfig = {
258daysLeftBeforeGeneralTip: 0,
259firstTimeMinDaysToRemind: 3,
260firstTimeMaxDaysToRemind: 6,
261minDaysToRemind: 6,
262maxDaysToRemind: 10,
263daysAfterLastUsage: 30,
264allTipsShownFirstly: false,
265tips: {
266generalTips: {},
267specificTips: {},
268},
269};
270
271tipsConfig = await this.mergeRemoteConfigToLocal(tipsConfig);
272
273Object.keys(tipsStorage.generalTips).forEach(key => {
274tipsConfig.tips.generalTips[key] = {};
275});
276
277Object.keys(tipsStorage.specificTips).forEach(key => {
278tipsConfig.tips.specificTips[key] = {};
279});
280
281ExtensionConfigManager.config.set(this.TIPS_CONFIG_NAME, tipsConfig);
282} else {
283tipsConfig = this.parseDatesInRawConfig(
284ExtensionConfigManager.config.get(this.TIPS_CONFIG_NAME),
285);
286}
287
288this._tipsConfig = tipsConfig;
289}
290
291private async showRandomGeneralTipNotification(): Promise<GeneratedTipResponse> {
292let generalTipsForRandom: Array<string>;
293const generalTips: Tips = this.tipsConfig.tips.generalTips;
294const generalTipsKeys: Array<string> = Object.keys(this.tipsConfig.tips.generalTips);
295
296if (!this.tipsConfig.allTipsShownFirstly) {
297generalTipsForRandom = generalTipsKeys.filter(
298tipId => !generalTips[tipId].knownDate && !generalTips[tipId].shownDate,
299);
300if (generalTipsForRandom.length === 1) {
301this.tipsConfig.allTipsShownFirstly = true;
302}
303} else {
09f6024fHeniker4 years ago304generalTipsForRandom = generalTipsKeys.sort(
305(tipId1, tipId2) =>
008d88e5RedMickey4 years ago306// According to ECMAScript standard: The exact moment of midnight at the beginning of
307// 01 January, 1970 UTC is represented by the value +0.
308(generalTips[tipId2].shownDate ?? new Date(+0)).getTime() -
09f6024fHeniker4 years ago309(generalTips[tipId1].shownDate ?? new Date(+0)).getTime(),
310);
f338085detatanova4 years ago311}
312
313let leftIndex: number;
314
315switch (generalTipsForRandom.length) {
316case 0:
317return {
318selection: undefined,
319tipKey: "",
320};
321case 1:
322leftIndex = 0;
323break;
324case 2:
325leftIndex = 1;
326break;
327default:
328leftIndex = 2;
329}
330
ab0238b7RedMickey4 years ago331const randIndex: number = getRandomIntInclusive(leftIndex, generalTipsForRandom.length - 1);
f338085detatanova4 years ago332const selectedGeneralTipKey: string = generalTipsForRandom[randIndex];
333const tipNotificationText = this.getGeneralTipNotificationTextByKey(selectedGeneralTipKey);
334
335this.tipsConfig.tips.generalTips[selectedGeneralTipKey].shownDate = new Date();
336
337this._tipsConfig = await this.mergeRemoteConfigToLocal(this.tipsConfig);
338const daysBeforeNextTip: number = this.tipsConfig.allTipsShownFirstly
ab0238b7RedMickey4 years ago339? getRandomIntInclusive(
f338085detatanova4 years ago340this.tipsConfig.minDaysToRemind,
341this.tipsConfig.maxDaysToRemind,
342)
ab0238b7RedMickey4 years ago343: getRandomIntInclusive(
f338085detatanova4 years ago344this.tipsConfig.firstTimeMinDaysToRemind,
345this.tipsConfig.firstTimeMaxDaysToRemind,
346);
347
348this.tipsConfig.daysLeftBeforeGeneralTip = daysBeforeNextTip;
349
350ExtensionConfigManager.config.set(this.TIPS_CONFIG_NAME, this.tipsConfig);
351
352this.sendShowTipNotificationTelemetry(selectedGeneralTipKey);
353
354return {
355selection: await vscode.window.showInformationMessage(
356tipNotificationText,
357...[this.getMoreInfoButtonText, this.doNotShowTipsAgainButtonText],
358),
359tipKey: selectedGeneralTipKey,
360};
361}
362
363private async showSpecificTipNotification(
364tipKey: string,
365): Promise<GeneratedTipResponse | undefined> {
366if (this.tipsConfig.tips.specificTips[tipKey].shownDate) {
367return;
368}
369
370const tipNotificationText = this.getSpecificTipNotificationTextByKey(tipKey);
371
372this.tipsConfig.tips.specificTips[tipKey].shownDate = new Date();
373ExtensionConfigManager.config.set(this.TIPS_CONFIG_NAME, this.tipsConfig);
374
375this.sendShowTipNotificationTelemetry(tipKey);
376
377return {
378selection: await vscode.window.showInformationMessage(
379tipNotificationText,
380...[this.getMoreInfoButtonText, this.doNotShowTipsAgainButtonText],
381),
382tipKey,
383};
384}
385
386private async mergeRemoteConfigToLocal(tipsConfig: TipsConfig): Promise<TipsConfig> {
387const remoteConfig = await this.downloadConfigRequest;
388tipsConfig.firstTimeMinDaysToRemind = remoteConfig.firstTimeMinDaysToRemind;
389tipsConfig.firstTimeMaxDaysToRemind = remoteConfig.firstTimeMaxDaysToRemind;
390tipsConfig.minDaysToRemind = remoteConfig.minDaysToRemind;
391tipsConfig.maxDaysToRemind = remoteConfig.maxDaysToRemind;
392tipsConfig.daysAfterLastUsage = remoteConfig.daysAfterLastUsage;
393return tipsConfig;
394}
395
396private getGeneralTipNotificationTextByKey(key: string): string {
397return tipsStorage.generalTips[key].text;
398}
399
400private getSpecificTipNotificationTextByKey(key: string): string {
401return tipsStorage.specificTips[key].text;
402}
403
404private getGeneralTipNotificationAnchorLinkByKey(key: string): string {
405return tipsStorage.generalTips[key].anchorLink;
406}
407
408private getSpecificTipNotificationAnchorLinkByKey(key: string): string {
409return tipsStorage.specificTips[key].anchorLink;
410}
411
412private deleteOutdatedKnownDate(): void {
413const dateNow: Date = new Date();
414const generalTips: Tips = this.tipsConfig.tips.generalTips;
415const generalTipsKeys: Array<string> = Object.keys(this.tipsConfig.tips.generalTips);
416
417generalTipsKeys
418.filter(tipKey => {
419const knownDate = generalTips[tipKey].knownDate ?? new Date();
420return (
421generalTips[tipKey].knownDate &&
422this.getDifferenceInDays(knownDate, dateNow) >
423this.tipsConfig.daysAfterLastUsage
424);
425})
426.forEach(tipKey => {
427delete generalTips[tipKey].knownDate;
428});
429}
430
431private getDifferenceInDays(date1: Date, date2: Date): number {
432const diffInMs = Math.abs(date2.getTime() - date1.getTime());
433return diffInMs / (1000 * 60 * 60 * 24);
434}
435
436private parseDatesInRawConfig(rawTipsConfig: TipsConfig): TipsConfig {
437if (rawTipsConfig.lastExtensionUsageDate) {
438rawTipsConfig.lastExtensionUsageDate = new Date(rawTipsConfig.lastExtensionUsageDate);
439}
440
441const parseDatesInTips = (tipsKeys: string[], tipsType: "generalTips" | "specificTips") => {
442tipsKeys.forEach(tipKey => {
09f6024fHeniker4 years ago443const tip = rawTipsConfig.tips[tipsType][tipKey];
f338085detatanova4 years ago444if (tip.knownDate) {
445rawTipsConfig.tips[tipsType][tipKey].knownDate = new Date(tip.knownDate);
446}
447if (tip.shownDate) {
448if (tip.shownDate) {
449rawTipsConfig.tips[tipsType][tipKey].shownDate = new Date(tip.shownDate);
450}
451}
452});
453};
454
455parseDatesInTips(Object.keys(rawTipsConfig.tips.specificTips), "specificTips");
456parseDatesInTips(Object.keys(rawTipsConfig.tips.generalTips), "generalTips");
457
458return rawTipsConfig;
459}
460
461private sendShowTipNotificationTelemetry(tipKey: string): void {
462const showTipNotificationEvent = TelemetryHelper.createTelemetryEvent(
463"showTipNotification",
464{
465tipKey,
466},
467);
468
469Telemetry.send(showTipNotificationEvent);
470}
471
472private sendTipNotificationActionTelemetry(
473tipKey: string,
474tipNotificationAction: TipNotificationAction,
475): void {
476const tipNotificationActionEvent = TelemetryHelper.createTelemetryEvent(
477"tipNotificationAction",
478{
479tipKey,
480tipNotificationAction,
481},
482);
483
484Telemetry.send(tipNotificationActionEvent);
485}
486}