microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
bump-glob-cli-fix

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/services/tipsNotificationsService/tipsNotificationService.ts

491lines · modecode

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