microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.8.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/tipsNotificationsService/tipsNotificationService.ts

503lines · 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
4import * as vscode from "vscode";
5import { IConfig, retryDownloadConfig } from "../remoteConfigHelper";
6import { TelemetryHelper } from "../../common/telemetryHelper";
7import { Telemetry } from "../../common/telemetry";
8import { ExtensionConfigManager } from "../extensionConfigManager";
9import tipsStorage from "./tipsStorage";
10import { findFileInFolderHierarchy } from "../../common/extensionHelper";
11import { SettingsHelper } from "../../extension/settingsHelper";
12import { OutputChannelLogger } from "../log/OutputChannelLogger";
13import * as path from "path";
14
15enum TipNotificationAction {
16GET_MORE_INFO = "tipsMoreInfo",
17DO_NOT_SHOW_AGAIN = "tipsDoNotShow",
18SHOWN = "tipShown",
19}
20
21export interface TipNotificationConfig extends IConfig {
22firstTimeMinDaysToRemind: number;
23firstTimeMaxDaysToRemind: number;
24minDaysToRemind: number;
25maxDaysToRemind: number;
26daysAfterLastUsage: number;
27}
28
29export interface TipInfo {
30knownDate?: Date;
31shownDate?: Date;
32}
33
34export interface Tips {
35[tipId: string]: TipInfo;
36}
37
38export interface AllTips {
39generalTips: Tips;
40specificTips: Tips;
41}
42
43export interface TipsConfig extends TipNotificationConfig {
44daysLeftBeforeGeneralTip: number;
45lastExtensionUsageDate?: Date;
46allTipsShownFirstly: boolean;
47tips: AllTips;
48}
49
50export interface GeneratedTipResponse {
51selection: string | undefined;
52tipKey: string;
53}
54
55export class TipNotificationService implements vscode.Disposable {
56private static instance: TipNotificationService;
57
58private readonly TIPS_NOTIFICATIONS_LOG_CHANNEL_NAME: string;
59private readonly TIPS_CONFIG_NAME: string;
60private readonly endpointURL: string;
61private readonly downloadConfigRequest: Promise<TipNotificationConfig>;
62private readonly getMoreInfoButtonText: string;
63private readonly doNotShowTipsAgainButtonText: string;
64
65private cancellationTokenSource: vscode.CancellationTokenSource;
66private _tipsConfig: TipsConfig | null;
67private logger: OutputChannelLogger;
68private showTips: boolean;
69
70public static getInstance(): TipNotificationService {
71if (!TipNotificationService.instance) {
72TipNotificationService.instance = new TipNotificationService();
73}
74
75return TipNotificationService.instance;
76}
77
78public dispose(): void {
79this.cancellationTokenSource.cancel();
80this.cancellationTokenSource.dispose();
81}
82
83private constructor() {
84this.endpointURL =
85"https://microsoft.github.io/vscode-react-native/tipsNotifications/tipsNotificationsConfig.json";
86this.TIPS_NOTIFICATIONS_LOG_CHANNEL_NAME = "Tips Notifications";
87this.TIPS_CONFIG_NAME = "tipsConfig";
88this.getMoreInfoButtonText = "Get more info";
89this.doNotShowTipsAgainButtonText = "Don't show tips again";
90
91this.cancellationTokenSource = new vscode.CancellationTokenSource();
92this._tipsConfig = null;
93this.downloadConfigRequest = retryDownloadConfig<TipNotificationConfig>(
94this.endpointURL,
95this.cancellationTokenSource,
96);
97this.showTips = SettingsHelper.getShowTips();
b49f7119etatanova4 years ago98this.logger = OutputChannelLogger.getChannel(
99this.TIPS_NOTIFICATIONS_LOG_CHANNEL_NAME,
100true,
101);
f338085detatanova4 years ago102}
103
104public async showTipNotification(
105isGeneralTip: boolean = true,
106specificTipKey?: string,
107): Promise<void> {
108if (!isGeneralTip && !specificTipKey) {
109this.logger.debug("The specific tip key parameter isn't passed for a specific tip");
110return;
111}
112
113await this.initializeTipsConfig();
114
115if (!this.showTips) {
116return;
117}
118
119const curDate: Date = new Date();
120let tipResponse: GeneratedTipResponse | undefined;
121
122if (isGeneralTip) {
123this.deleteOutdatedKnownDate();
124if (this.tipsConfig.daysLeftBeforeGeneralTip === 0) {
125tipResponse = await this.showRandomGeneralTipNotification();
126} else {
127if (
128this.tipsConfig.lastExtensionUsageDate &&
129!this.areSameDates(curDate, this.tipsConfig.lastExtensionUsageDate)
130) {
131this.tipsConfig.daysLeftBeforeGeneralTip--;
132}
133}
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 {
186for (let key in configTips) {
187if (!(key in storageTips)) {
188delete configTips[key];
189}
190}
191
192for (let key in storageTips) {
193if (!(key in configTips)) {
194configTips[key] = {};
195}
196}
197
198return configTips;
199}
200
f338085detatanova4 years ago201private get tipsConfig(): TipsConfig {
202if (!this._tipsConfig) {
203if (!ExtensionConfigManager.config.has(this.TIPS_CONFIG_NAME)) {
204throw new Error("Could not find Tips config in the config store.");
205} else {
206this._tipsConfig = this.parseDatesInRawConfig(
207ExtensionConfigManager.config.get(this.TIPS_CONFIG_NAME),
208);
209}
210}
211return this._tipsConfig;
212}
213
214private async handleUserActionOnTip(
215tipResponse: GeneratedTipResponse,
216isGeneralTip: boolean,
217): Promise<void> {
218const { selection, tipKey } = tipResponse;
219
220if (selection === this.getMoreInfoButtonText) {
221this.sendTipNotificationActionTelemetry(tipKey, TipNotificationAction.GET_MORE_INFO);
222
223const readmeFile: string | null = findFileInFolderHierarchy(__dirname, "README.md");
224
225if (readmeFile) {
226const anchorLink: string = isGeneralTip
227? this.getGeneralTipNotificationAnchorLinkByKey(tipKey)
228: this.getSpecificTipNotificationAnchorLinkByKey(tipKey);
229
230const uriFile = vscode.Uri.parse(
231path.normalize(`file://${readmeFile}${anchorLink}`),
232);
233
234vscode.commands.executeCommand("markdown.showPreview", uriFile);
235}
236}
237
238if (selection === this.doNotShowTipsAgainButtonText) {
239this.sendTipNotificationActionTelemetry(
240tipKey,
241TipNotificationAction.DO_NOT_SHOW_AGAIN,
242);
243this.showTips = false;
244await SettingsHelper.setShowTips(this.showTips);
245}
246}
247
248private async initializeTipsConfig(): Promise<void> {
249this.showTips = SettingsHelper.getShowTips();
250if (this._tipsConfig) {
251return;
252}
253
254let tipsConfig: TipsConfig;
255if (!ExtensionConfigManager.config.has(this.TIPS_CONFIG_NAME)) {
256tipsConfig = {
257daysLeftBeforeGeneralTip: 0,
258firstTimeMinDaysToRemind: 3,
259firstTimeMaxDaysToRemind: 6,
260minDaysToRemind: 6,
261maxDaysToRemind: 10,
262daysAfterLastUsage: 30,
263allTipsShownFirstly: false,
264tips: {
265generalTips: {},
266specificTips: {},
267},
268};
269
270tipsConfig = await this.mergeRemoteConfigToLocal(tipsConfig);
271
272Object.keys(tipsStorage.generalTips).forEach(key => {
273tipsConfig.tips.generalTips[key] = {};
274});
275
276Object.keys(tipsStorage.specificTips).forEach(key => {
277tipsConfig.tips.specificTips[key] = {};
278});
279
280ExtensionConfigManager.config.set(this.TIPS_CONFIG_NAME, tipsConfig);
281} else {
282tipsConfig = this.parseDatesInRawConfig(
283ExtensionConfigManager.config.get(this.TIPS_CONFIG_NAME),
284);
285}
286
287this._tipsConfig = tipsConfig;
288}
289
290private async showRandomGeneralTipNotification(): Promise<GeneratedTipResponse> {
291let generalTipsForRandom: Array<string>;
292const generalTips: Tips = this.tipsConfig.tips.generalTips;
293const generalTipsKeys: Array<string> = Object.keys(this.tipsConfig.tips.generalTips);
294
295if (!this.tipsConfig.allTipsShownFirstly) {
296generalTipsForRandom = generalTipsKeys.filter(
297tipId => !generalTips[tipId].knownDate && !generalTips[tipId].shownDate,
298);
299if (generalTipsForRandom.length === 1) {
300this.tipsConfig.allTipsShownFirstly = true;
301}
302} else {
303generalTipsForRandom = generalTipsKeys.sort((tipId1, tipId2) => {
304return (
008d88e5RedMickey4 years ago305// According to ECMAScript standard: The exact moment of midnight at the beginning of
306// 01 January, 1970 UTC is represented by the value +0.
307(generalTips[tipId2].shownDate ?? new Date(+0)).getTime() -
308(generalTips[tipId1].shownDate ?? new Date(+0)).getTime()
f338085detatanova4 years ago309);
310});
311}
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
331const randIndex: number = this.getRandomIntInclusive(
332leftIndex,
333generalTipsForRandom.length - 1,
334);
335const selectedGeneralTipKey: string = generalTipsForRandom[randIndex];
336const tipNotificationText = this.getGeneralTipNotificationTextByKey(selectedGeneralTipKey);
337
338this.tipsConfig.tips.generalTips[selectedGeneralTipKey].shownDate = new Date();
339
340this._tipsConfig = await this.mergeRemoteConfigToLocal(this.tipsConfig);
341const daysBeforeNextTip: number = this.tipsConfig.allTipsShownFirstly
342? this.getRandomIntInclusive(
343this.tipsConfig.minDaysToRemind,
344this.tipsConfig.maxDaysToRemind,
345)
346: this.getRandomIntInclusive(
347this.tipsConfig.firstTimeMinDaysToRemind,
348this.tipsConfig.firstTimeMaxDaysToRemind,
349);
350
351this.tipsConfig.daysLeftBeforeGeneralTip = daysBeforeNextTip;
352
353ExtensionConfigManager.config.set(this.TIPS_CONFIG_NAME, this.tipsConfig);
354
355this.sendShowTipNotificationTelemetry(selectedGeneralTipKey);
356
357return {
358selection: await vscode.window.showInformationMessage(
359tipNotificationText,
360...[this.getMoreInfoButtonText, this.doNotShowTipsAgainButtonText],
361),
362tipKey: selectedGeneralTipKey,
363};
364}
365
366private async showSpecificTipNotification(
367tipKey: string,
368): Promise<GeneratedTipResponse | undefined> {
369if (this.tipsConfig.tips.specificTips[tipKey].shownDate) {
370return;
371}
372
373const tipNotificationText = this.getSpecificTipNotificationTextByKey(tipKey);
374
375this.tipsConfig.tips.specificTips[tipKey].shownDate = new Date();
376ExtensionConfigManager.config.set(this.TIPS_CONFIG_NAME, this.tipsConfig);
377
378this.sendShowTipNotificationTelemetry(tipKey);
379
380return {
381selection: await vscode.window.showInformationMessage(
382tipNotificationText,
383...[this.getMoreInfoButtonText, this.doNotShowTipsAgainButtonText],
384),
385tipKey,
386};
387}
388
389private async mergeRemoteConfigToLocal(tipsConfig: TipsConfig): Promise<TipsConfig> {
390const remoteConfig = await this.downloadConfigRequest;
391tipsConfig.firstTimeMinDaysToRemind = remoteConfig.firstTimeMinDaysToRemind;
392tipsConfig.firstTimeMaxDaysToRemind = remoteConfig.firstTimeMaxDaysToRemind;
393tipsConfig.minDaysToRemind = remoteConfig.minDaysToRemind;
394tipsConfig.maxDaysToRemind = remoteConfig.maxDaysToRemind;
395tipsConfig.daysAfterLastUsage = remoteConfig.daysAfterLastUsage;
396return tipsConfig;
397}
398
399private getGeneralTipNotificationTextByKey(key: string): string {
400return tipsStorage.generalTips[key].text;
401}
402
403private getSpecificTipNotificationTextByKey(key: string): string {
404return tipsStorage.specificTips[key].text;
405}
406
407private getGeneralTipNotificationAnchorLinkByKey(key: string): string {
408return tipsStorage.generalTips[key].anchorLink;
409}
410
411private getSpecificTipNotificationAnchorLinkByKey(key: string): string {
412return tipsStorage.specificTips[key].anchorLink;
413}
414
415private deleteOutdatedKnownDate(): void {
416const dateNow: Date = new Date();
417const generalTips: Tips = this.tipsConfig.tips.generalTips;
418const generalTipsKeys: Array<string> = Object.keys(this.tipsConfig.tips.generalTips);
419
420generalTipsKeys
421.filter(tipKey => {
422const knownDate = generalTips[tipKey].knownDate ?? new Date();
423return (
424generalTips[tipKey].knownDate &&
425this.getDifferenceInDays(knownDate, dateNow) >
426this.tipsConfig.daysAfterLastUsage
427);
428})
429.forEach(tipKey => {
430delete generalTips[tipKey].knownDate;
431});
432}
433
434private areSameDates(date1: Date, date2: Date): boolean {
435return (
436date1.getFullYear() === date2.getFullYear() &&
437date1.getMonth() === date2.getMonth() &&
438date1.getDate() === date2.getDate()
439);
440}
441
442private getDifferenceInDays(date1: Date, date2: Date): number {
443const diffInMs = Math.abs(date2.getTime() - date1.getTime());
444return diffInMs / (1000 * 60 * 60 * 24);
445}
446
447private getRandomIntInclusive(min: number, max: number): number {
448min = Math.ceil(min);
449max = Math.floor(max);
450return Math.floor(Math.random() * (max - min + 1)) + min;
451}
452
453private parseDatesInRawConfig(rawTipsConfig: TipsConfig): TipsConfig {
454if (rawTipsConfig.lastExtensionUsageDate) {
455rawTipsConfig.lastExtensionUsageDate = new Date(rawTipsConfig.lastExtensionUsageDate);
456}
457
458const parseDatesInTips = (tipsKeys: string[], tipsType: "generalTips" | "specificTips") => {
459tipsKeys.forEach(tipKey => {
460let tip = rawTipsConfig.tips[tipsType][tipKey];
461if (tip.knownDate) {
462rawTipsConfig.tips[tipsType][tipKey].knownDate = new Date(tip.knownDate);
463}
464if (tip.shownDate) {
465if (tip.shownDate) {
466rawTipsConfig.tips[tipsType][tipKey].shownDate = new Date(tip.shownDate);
467}
468}
469});
470};
471
472parseDatesInTips(Object.keys(rawTipsConfig.tips.specificTips), "specificTips");
473parseDatesInTips(Object.keys(rawTipsConfig.tips.generalTips), "generalTips");
474
475return rawTipsConfig;
476}
477
478private sendShowTipNotificationTelemetry(tipKey: string): void {
479const showTipNotificationEvent = TelemetryHelper.createTelemetryEvent(
480"showTipNotification",
481{
482tipKey,
483},
484);
485
486Telemetry.send(showTipNotificationEvent);
487}
488
489private sendTipNotificationActionTelemetry(
490tipKey: string,
491tipNotificationAction: TipNotificationAction,
492): void {
493const tipNotificationActionEvent = TelemetryHelper.createTelemetryEvent(
494"tipNotificationAction",
495{
496tipKey,
497tipNotificationAction,
498},
499);
500
501Telemetry.send(tipNotificationActionEvent);
502}
503}