microsoft/vscode-react-native

Public

mirrored fromhttps://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
28ceac00278cb47a4302a559e4ccdd01a855b7f6

Branches

Tags

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

Clone

HTTPS

Download ZIP

src/extension/tipsNotificationsService/tipsNotificationService.ts

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