microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
prepare-for-1.14.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/services/tipsNotificationsService/tipsNotificationService.ts

487lines · 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 {
444ec9f8ConnorQi011 months ago57private static instance: TipNotificationService | null;
f338085detatanova4 years ago58
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();
444ec9f8ConnorQi011 months ago82TipNotificationService.instance = null;
f338085detatanova4 years ago83}
84
85private constructor() {
86this.endpointURL =
87"https://microsoft.github.io/vscode-react-native/tipsNotifications/tipsNotificationsConfig.json";
88this.TIPS_NOTIFICATIONS_LOG_CHANNEL_NAME = "Tips Notifications";
89this.TIPS_CONFIG_NAME = "tipsConfig";
90this.getMoreInfoButtonText = "Get more info";
91this.doNotShowTipsAgainButtonText = "Don't show tips again";
92
93this.cancellationTokenSource = new vscode.CancellationTokenSource();
94this._tipsConfig = null;
95this.downloadConfigRequest = retryDownloadConfig<TipNotificationConfig>(
96this.endpointURL,
97this.cancellationTokenSource,
98);
99this.showTips = SettingsHelper.getShowTips();
b49f7119etatanova4 years ago100this.logger = OutputChannelLogger.getChannel(
101this.TIPS_NOTIFICATIONS_LOG_CHANNEL_NAME,
102true,
103);
f338085detatanova4 years ago104}
105
106public async showTipNotification(
107isGeneralTip: boolean = true,
108specificTipKey?: string,
109): Promise<void> {
110if (!isGeneralTip && !specificTipKey) {
111this.logger.debug("The specific tip key parameter isn't passed for a specific tip");
112return;
113}
114
115await this.initializeTipsConfig();
116
117if (!this.showTips) {
118return;
119}
120
121const curDate: Date = new Date();
122let tipResponse: GeneratedTipResponse | undefined;
123
124if (isGeneralTip) {
125this.deleteOutdatedKnownDate();
126if (this.tipsConfig.daysLeftBeforeGeneralTip === 0) {
127tipResponse = await this.showRandomGeneralTipNotification();
09f6024fHeniker4 years ago128} else if (
129this.tipsConfig.lastExtensionUsageDate &&
130!areSameDates(curDate, this.tipsConfig.lastExtensionUsageDate)
131) {
132this.tipsConfig.daysLeftBeforeGeneralTip--;
f338085detatanova4 years ago133}
134} else {
135tipResponse = await this.showSpecificTipNotification(<string>specificTipKey);
136}
137
138if (tipResponse) {
139await this.handleUserActionOnTip(tipResponse, isGeneralTip);
140}
141
142this.tipsConfig.lastExtensionUsageDate = curDate;
143ExtensionConfigManager.config.set(this.TIPS_CONFIG_NAME, this.tipsConfig);
144}
145
146public async setKnownDateForFeatureById(
147key: string,
148isGeneralTip: boolean = true,
149): Promise<void> {
150await this.initializeTipsConfig();
151
152if (isGeneralTip) {
153this.tipsConfig.tips.generalTips[key].knownDate = new Date();
154} else {
155this.tipsConfig.tips.specificTips[key].knownDate = new Date();
156}
157
158ExtensionConfigManager.config.set(this.TIPS_CONFIG_NAME, this.tipsConfig);
159}
160
b49f7119etatanova4 years ago161public updateTipsConfig(): void {
162if (!ExtensionConfigManager.config.has(this.TIPS_CONFIG_NAME)) {
163return;
164}
165
166const tipsConfig = this.tipsConfig;
167
168tipsConfig.tips.generalTips = this.updateConfigTipsFromStorage(
169tipsStorage.generalTips,
170tipsConfig.tips.generalTips,
171);
172
173tipsConfig.tips.specificTips = this.updateConfigTipsFromStorage(
174tipsStorage.specificTips,
175tipsConfig.tips.specificTips,
176);
177
178this._tipsConfig = tipsConfig;
179ExtensionConfigManager.config.set(this.TIPS_CONFIG_NAME, tipsConfig);
180}
181
182private updateConfigTipsFromStorage(
183storageTips: Record<string, unknown>,
184configTips: Tips,
185): Tips {
09f6024fHeniker4 years ago186// eslint-disable-next-line no-restricted-syntax
187for (const key in configTips) {
b49f7119etatanova4 years ago188if (!(key in storageTips)) {
189delete configTips[key];
190}
191}
192
09f6024fHeniker4 years ago193// eslint-disable-next-line no-restricted-syntax
194for (const key in storageTips) {
b49f7119etatanova4 years ago195if (!(key in configTips)) {
196configTips[key] = {};
197}
198}
199
200return configTips;
201}
202
f338085detatanova4 years ago203private get tipsConfig(): TipsConfig {
204if (!this._tipsConfig) {
205if (!ExtensionConfigManager.config.has(this.TIPS_CONFIG_NAME)) {
206throw new Error("Could not find Tips config in the config store.");
207} else {
208this._tipsConfig = this.parseDatesInRawConfig(
209ExtensionConfigManager.config.get(this.TIPS_CONFIG_NAME),
210);
211}
212}
213return this._tipsConfig;
214}
215
216private async handleUserActionOnTip(
217tipResponse: GeneratedTipResponse,
218isGeneralTip: boolean,
219): Promise<void> {
220const { selection, tipKey } = tipResponse;
221
222if (selection === this.getMoreInfoButtonText) {
223this.sendTipNotificationActionTelemetry(tipKey, TipNotificationAction.GET_MORE_INFO);
224
225const readmeFile: string | null = findFileInFolderHierarchy(__dirname, "README.md");
226
227if (readmeFile) {
228const anchorLink: string = isGeneralTip
229? this.getGeneralTipNotificationAnchorLinkByKey(tipKey)
230: this.getSpecificTipNotificationAnchorLinkByKey(tipKey);
231
232const uriFile = vscode.Uri.parse(
233path.normalize(`file://${readmeFile}${anchorLink}`),
234);
235
09f6024fHeniker4 years ago236void vscode.commands.executeCommand("markdown.showPreview", uriFile);
f338085detatanova4 years ago237}
238}
239
240if (selection === this.doNotShowTipsAgainButtonText) {
241this.sendTipNotificationActionTelemetry(
242tipKey,
243TipNotificationAction.DO_NOT_SHOW_AGAIN,
244);
245this.showTips = false;
246await SettingsHelper.setShowTips(this.showTips);
247}
248}
249
250private async initializeTipsConfig(): Promise<void> {
251this.showTips = SettingsHelper.getShowTips();
252if (this._tipsConfig) {
253return;
254}
255
256let tipsConfig: TipsConfig;
257if (!ExtensionConfigManager.config.has(this.TIPS_CONFIG_NAME)) {
258tipsConfig = {
259daysLeftBeforeGeneralTip: 0,
260firstTimeMinDaysToRemind: 3,
261firstTimeMaxDaysToRemind: 6,
262minDaysToRemind: 6,
263maxDaysToRemind: 10,
264daysAfterLastUsage: 30,
265allTipsShownFirstly: false,
266tips: {
267generalTips: {},
268specificTips: {},
269},
270};
271
272tipsConfig = await this.mergeRemoteConfigToLocal(tipsConfig);
273
274Object.keys(tipsStorage.generalTips).forEach(key => {
275tipsConfig.tips.generalTips[key] = {};
276});
277
278Object.keys(tipsStorage.specificTips).forEach(key => {
279tipsConfig.tips.specificTips[key] = {};
280});
281
282ExtensionConfigManager.config.set(this.TIPS_CONFIG_NAME, tipsConfig);
283} else {
284tipsConfig = this.parseDatesInRawConfig(
285ExtensionConfigManager.config.get(this.TIPS_CONFIG_NAME),
286);
287}
288
289this._tipsConfig = tipsConfig;
290}
291
292private async showRandomGeneralTipNotification(): Promise<GeneratedTipResponse> {
293let generalTipsForRandom: Array<string>;
294const generalTips: Tips = this.tipsConfig.tips.generalTips;
295const generalTipsKeys: Array<string> = Object.keys(this.tipsConfig.tips.generalTips);
296
297if (!this.tipsConfig.allTipsShownFirstly) {
298generalTipsForRandom = generalTipsKeys.filter(
299tipId => !generalTips[tipId].knownDate && !generalTips[tipId].shownDate,
300);
301if (generalTipsForRandom.length === 1) {
302this.tipsConfig.allTipsShownFirstly = true;
303}
304} else {
09f6024fHeniker4 years ago305generalTipsForRandom = generalTipsKeys.sort(
306(tipId1, tipId2) =>
008d88e5RedMickey4 years ago307// According to ECMAScript standard: The exact moment of midnight at the beginning of
308// 01 January, 1970 UTC is represented by the value +0.
309(generalTips[tipId2].shownDate ?? new Date(+0)).getTime() -
09f6024fHeniker4 years ago310(generalTips[tipId1].shownDate ?? new Date(+0)).getTime(),
311);
f338085detatanova4 years ago312}
313
314let leftIndex: number;
315
316switch (generalTipsForRandom.length) {
317case 0:
318return {
319selection: undefined,
320tipKey: "",
321};
322case 1:
323leftIndex = 0;
324break;
325case 2:
326leftIndex = 1;
327break;
328default:
329leftIndex = 2;
330}
331
ab0238b7RedMickey4 years ago332const randIndex: number = getRandomIntInclusive(leftIndex, generalTipsForRandom.length - 1);
f338085detatanova4 years ago333const selectedGeneralTipKey: string = generalTipsForRandom[randIndex];
334const tipNotificationText = this.getGeneralTipNotificationTextByKey(selectedGeneralTipKey);
335
336this.tipsConfig.tips.generalTips[selectedGeneralTipKey].shownDate = new Date();
337
338this._tipsConfig = await this.mergeRemoteConfigToLocal(this.tipsConfig);
339const daysBeforeNextTip: number = this.tipsConfig.allTipsShownFirstly
ab0238b7RedMickey4 years ago340? getRandomIntInclusive(
f338085detatanova4 years ago341this.tipsConfig.minDaysToRemind,
342this.tipsConfig.maxDaysToRemind,
343)
ab0238b7RedMickey4 years ago344: getRandomIntInclusive(
f338085detatanova4 years ago345this.tipsConfig.firstTimeMinDaysToRemind,
346this.tipsConfig.firstTimeMaxDaysToRemind,
347);
348
349this.tipsConfig.daysLeftBeforeGeneralTip = daysBeforeNextTip;
350
351ExtensionConfigManager.config.set(this.TIPS_CONFIG_NAME, this.tipsConfig);
352
353this.sendShowTipNotificationTelemetry(selectedGeneralTipKey);
354
355return {
356selection: await vscode.window.showInformationMessage(
357tipNotificationText,
358...[this.getMoreInfoButtonText, this.doNotShowTipsAgainButtonText],
359),
360tipKey: selectedGeneralTipKey,
361};
362}
363
364private async showSpecificTipNotification(
365tipKey: string,
366): Promise<GeneratedTipResponse | undefined> {
367if (this.tipsConfig.tips.specificTips[tipKey].shownDate) {
368return;
369}
370
371const tipNotificationText = this.getSpecificTipNotificationTextByKey(tipKey);
372
373this.tipsConfig.tips.specificTips[tipKey].shownDate = new Date();
374ExtensionConfigManager.config.set(this.TIPS_CONFIG_NAME, this.tipsConfig);
375
376this.sendShowTipNotificationTelemetry(tipKey);
377
378return {
379selection: await vscode.window.showInformationMessage(
380tipNotificationText,
381...[this.getMoreInfoButtonText, this.doNotShowTipsAgainButtonText],
382),
383tipKey,
384};
385}
386
387private async mergeRemoteConfigToLocal(tipsConfig: TipsConfig): Promise<TipsConfig> {
388const remoteConfig = await this.downloadConfigRequest;
389tipsConfig.firstTimeMinDaysToRemind = remoteConfig.firstTimeMinDaysToRemind;
390tipsConfig.firstTimeMaxDaysToRemind = remoteConfig.firstTimeMaxDaysToRemind;
391tipsConfig.minDaysToRemind = remoteConfig.minDaysToRemind;
392tipsConfig.maxDaysToRemind = remoteConfig.maxDaysToRemind;
393tipsConfig.daysAfterLastUsage = remoteConfig.daysAfterLastUsage;
394return tipsConfig;
395}
396
397private getGeneralTipNotificationTextByKey(key: string): string {
5b09cf97Zhen Zhen Yuan (BEYONDSOFT CONSULTING INC)7 months ago398return (tipsStorage.generalTips as any)[key].text;
f338085detatanova4 years ago399}
400
401private getSpecificTipNotificationTextByKey(key: string): string {
5b09cf97Zhen Zhen Yuan (BEYONDSOFT CONSULTING INC)7 months ago402return (tipsStorage.specificTips as any)[key].text;
f338085detatanova4 years ago403}
404
405private getGeneralTipNotificationAnchorLinkByKey(key: string): string {
5b09cf97Zhen Zhen Yuan (BEYONDSOFT CONSULTING INC)7 months ago406return (tipsStorage.generalTips as any)[key].anchorLink;
f338085detatanova4 years ago407}
408
409private getSpecificTipNotificationAnchorLinkByKey(key: string): string {
5b09cf97Zhen Zhen Yuan (BEYONDSOFT CONSULTING INC)7 months ago410return (tipsStorage.specificTips as any)[key].anchorLink;
f338085detatanova4 years ago411}
412
413private deleteOutdatedKnownDate(): void {
414const dateNow: Date = new Date();
415const generalTips: Tips = this.tipsConfig.tips.generalTips;
416const generalTipsKeys: Array<string> = Object.keys(this.tipsConfig.tips.generalTips);
417
418generalTipsKeys
419.filter(tipKey => {
420const knownDate = generalTips[tipKey].knownDate ?? new Date();
421return (
422generalTips[tipKey].knownDate &&
423this.getDifferenceInDays(knownDate, dateNow) >
424this.tipsConfig.daysAfterLastUsage
425);
426})
427.forEach(tipKey => {
428delete generalTips[tipKey].knownDate;
429});
430}
431
432private getDifferenceInDays(date1: Date, date2: Date): number {
433const diffInMs = Math.abs(date2.getTime() - date1.getTime());
434return diffInMs / (1000 * 60 * 60 * 24);
435}
436
437private parseDatesInRawConfig(rawTipsConfig: TipsConfig): TipsConfig {
438if (rawTipsConfig.lastExtensionUsageDate) {
439rawTipsConfig.lastExtensionUsageDate = new Date(rawTipsConfig.lastExtensionUsageDate);
440}
441
442const parseDatesInTips = (tipsKeys: string[], tipsType: "generalTips" | "specificTips") => {
443tipsKeys.forEach(tipKey => {
09f6024fHeniker4 years ago444const tip = rawTipsConfig.tips[tipsType][tipKey];
f338085detatanova4 years ago445if (tip.knownDate) {
446rawTipsConfig.tips[tipsType][tipKey].knownDate = new Date(tip.knownDate);
447}
448if (tip.shownDate) {
449if (tip.shownDate) {
450rawTipsConfig.tips[tipsType][tipKey].shownDate = new Date(tip.shownDate);
451}
452}
453});
454};
455
456parseDatesInTips(Object.keys(rawTipsConfig.tips.specificTips), "specificTips");
457parseDatesInTips(Object.keys(rawTipsConfig.tips.generalTips), "generalTips");
458
459return rawTipsConfig;
460}
461
462private sendShowTipNotificationTelemetry(tipKey: string): void {
463const showTipNotificationEvent = TelemetryHelper.createTelemetryEvent(
464"showTipNotification",
465{
466tipKey,
467},
468);
469
470Telemetry.send(showTipNotificationEvent);
471}
472
473private sendTipNotificationActionTelemetry(
474tipKey: string,
475tipNotificationAction: TipNotificationAction,
476): void {
477const tipNotificationActionEvent = TelemetryHelper.createTelemetryEvent(
478"tipNotificationAction",
479{
480tipKey,
481tipNotificationAction,
482},
483);
484
485Telemetry.send(tipNotificationActionEvent);
486}
487}