From ca46da30056cf1c0ad30bc97f7d5d0b21aa212b7 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Tue, 5 Mar 2024 16:16:47 -0800 Subject: [PATCH 1/3] Initial support for video tutorials --- .../browser/gettingStarted.ts | 106 ++++++++++++++++-- 1 file changed, 95 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 725248d0031de..1cfe3bea87270 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -148,6 +148,7 @@ export class GettingStartedPage extends EditorPane { private recentlyOpenedList?: GettingStartedIndexList; private startList?: GettingStartedIndexList; private gettingStartedList?: GettingStartedIndexList; + private videoList?: GettingStartedIndexList; private stepsSlide!: HTMLElement; private categoriesSlide!: HTMLElement; @@ -345,7 +346,13 @@ export class GettingStartedPage extends EditorPane { this.dispatchListeners.clear(); this.container.querySelectorAll('[x-dispatch]').forEach(element => { - const [command, argument] = (element.getAttribute('x-dispatch') ?? '').split(':'); + const dispatch = element.getAttribute('x-dispatch') ?? ''; + let command, argument; + if (dispatch.startsWith('openLink:https')) { + [command, argument] = ['openLink', dispatch.replace('openLink:', '')]; + } else { + [command, argument] = dispatch.split(':'); + } if (command) { this.dispatchListeners.add(addDisposableListener(element, 'click', (e) => { e.stopPropagation(); @@ -433,12 +440,12 @@ export class GettingStartedPage extends EditorPane { } break; } - case 'openExtensionPage': { - this.commandService.executeCommand('extension.open', argument); + case 'hideVideosContent': { + this.hideVideosContent(); break; } - case 'hideExtension': { - this.hideExtension(argument); + case 'openLink': { + this.openerService.open(argument); break; } default: { @@ -455,9 +462,9 @@ export class GettingStartedPage extends EditorPane { this.gettingStartedList?.rerender(); } - private hideExtension(extensionId: string) { - this.setHiddenCategories([...this.getHiddenCategories().add(extensionId)]); - this.registerDispatchListeners(); + private hideVideosContent() { + this.setHiddenCategories([...this.getHiddenCategories().add('videos')]); + this.videoList?.setEntries(undefined); } private markAllStepsComplete() { @@ -808,6 +815,7 @@ export class GettingStartedPage extends EditorPane { const startList = this.buildStartList(); const recentList = this.buildRecentlyOpenedList(); const gettingStartedList = this.buildGettingStartedWalkthroughsList(); + const videoList = this.buildVideosList(); const footer = $('.footer', {}, $('p.showOnStartup', {}, @@ -818,19 +826,38 @@ export class GettingStartedPage extends EditorPane { const layoutLists = () => { if (gettingStartedList.itemCount) { this.container.classList.remove('noWalkthroughs'); - reset(rightColumn, gettingStartedList.getDomElement()); + if (videoList.itemCount > 0) { + reset(rightColumn, videoList.getDomElement(), gettingStartedList.getDomElement()); + } else { + reset(rightColumn, gettingStartedList.getDomElement()); + } } else { this.container.classList.add('noWalkthroughs'); - reset(rightColumn); + if (videoList.itemCount > 0) { + reset(rightColumn, videoList.getDomElement()); + } + else { + reset(rightColumn); + } + } + setTimeout(() => this.categoriesPageScrollbar?.scanDomNode(), 50); + layoutRecentList(); + }; + const layoutVideos = () => { + if (videoList.itemCount > 0) { + reset(rightColumn, videoList.getDomElement(), gettingStartedList.getDomElement()); + } + else { + reset(rightColumn, gettingStartedList.getDomElement()); } setTimeout(() => this.categoriesPageScrollbar?.scanDomNode(), 50); layoutRecentList(); }; const layoutRecentList = () => { - if (this.container.classList.contains('noWalkthroughs')) { + if (this.container.classList.contains('noWalkthroughs') && videoList.itemCount === 0) { recentList.setLimit(10); reset(leftColumn, startList.getDomElement()); reset(rightColumn, recentList.getDomElement()); @@ -840,6 +867,7 @@ export class GettingStartedPage extends EditorPane { } }; + videoList.onDidChange(layoutVideos); gettingStartedList.onDidChange(layoutLists); layoutLists(); @@ -1090,6 +1118,61 @@ export class GettingStartedPage extends EditorPane { return gettingStartedList; } + private buildVideosList(): GettingStartedIndexList { + + const renderFeaturedExtensions = (entry: IWelcomePageStartEntry): HTMLElement => { + + const descriptionContent = $('.description-content', {},); + + reset(descriptionContent, ...renderLabelWithIcons(entry.description)); + + const titleContent = $('h3.category-title.max-lines-3', { 'x-category-title-for': entry.id }); + reset(titleContent, ...renderLabelWithIcons(entry.title)); + + return $('button.getting-started-category', + { + 'x-dispatch': 'openLink:' + entry.command, + 'title': entry.description + }, + $('.main-content', {}, + this.iconWidgetFor(entry), + titleContent, + $('a.codicon.codicon-close.hide-category-button', { + 'tabindex': 0, + 'x-dispatch': 'hideVideosContent', + 'title': localize('close', "Hide"), + 'role': 'button', + 'aria-label': localize('closeAriaLabel', "Hide"), + }), + )); + }; + + if (this.videoList) { + this.videoList.dispose(); + } + const videoList = this.videoList = new GettingStartedIndexList( + { + title: '', + klass: 'getting-started-videos', + limit: 1, + renderElement: renderFeaturedExtensions, + contextService: this.contextService, + }); + + videoList.setEntries([{ + id: 'videos', + title: 'Watch Tutorials', + description: 'Learn VS Code\'s must-have features in short and practical tutorials', + icon: { type: 'icon', icon: Codicon.play }, + command: 'https://www.youtube.com/watch?v=B-s71n0dHUk&list=PLj6YeMhvp2S5UgiQnBfvD7XgOMKs3O_G6&index=1', + order: 0, + when: ContextKeyExpr.true(), + }]); + videoList.onDidChange(() => this.registerDispatchListeners()); + + return videoList; + } + layout(size: Dimension) { this.detailsScrollbar?.scanDomNode(); @@ -1099,6 +1182,7 @@ export class GettingStartedPage extends EditorPane { this.startList?.layout(size); this.gettingStartedList?.layout(size); this.recentlyOpenedList?.layout(size); + this.videoList?.layout(size); if (this.editorInput?.selectedStep && this.currentMediaType) { this.mediaDisposables.clear(); From f179ec34a27bef361bb47d3c97080ed1114c8222 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Sun, 10 Mar 2024 18:48:45 -0700 Subject: [PATCH 2/3] Experiment for surfacing video tutorials --- .../browser/gettingStarted.ts | 92 ++++++++++++------- 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 1cfe3bea87270..ec29e5f474473 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -70,6 +70,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ThemeSettings } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { GettingStartedIndexList } from './gettingStartedList'; +import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; const SLIDE_TRANSITION_TIME_MS = 250; const configurationKey = 'workbench.startupEditor'; @@ -161,6 +162,7 @@ export class GettingStartedPage extends EditorPane { private detailsRenderer: GettingStartedDetailsRenderer; private categoriesSlideDisposables: DisposableStore; + private showFeaturedWalkthrough: boolean = true; constructor( group: IEditorGroup, @@ -186,7 +188,9 @@ export class GettingStartedPage extends EditorPane { @IHostService private readonly hostService: IHostService, @IWebviewService private readonly webviewService: IWebviewService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService) { + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, + @IWorkbenchAssignmentService private readonly tasExperimentService: IWorkbenchAssignmentService + ) { super(GettingStartedPage.ID, group, telemetryService, themeService, storageService); @@ -440,8 +444,8 @@ export class GettingStartedPage extends EditorPane { } break; } - case 'hideVideosContent': { - this.hideVideosContent(); + case 'hideVideos': { + this.hideVideos(); break; } case 'openLink': { @@ -462,8 +466,8 @@ export class GettingStartedPage extends EditorPane { this.gettingStartedList?.rerender(); } - private hideVideosContent() { - this.setHiddenCategories([...this.getHiddenCategories().add('videos')]); + private hideVideos() { + this.setHiddenCategories([...this.getHiddenCategories().add('getting-started-videos')]); this.videoList?.setEntries(undefined); } @@ -814,8 +818,30 @@ export class GettingStartedPage extends EditorPane { const startList = this.buildStartList(); const recentList = this.buildRecentlyOpenedList(); + + const showVideoTutorials = await Promise.race([ + this.tasExperimentService?.getTreatment('gettingStarted.showVideoTutorials'), + new Promise(resolve => setTimeout(() => resolve(false), 200)) + ]); + + let videoList: GettingStartedIndexList; + if (showVideoTutorials === true) { + this.showFeaturedWalkthrough = false; + videoList = this.buildVideosList(); + const layoutVideos = () => { + if (videoList?.itemCount > 0) { + reset(rightColumn, videoList?.getDomElement(), gettingStartedList.getDomElement()); + } + else { + reset(rightColumn, gettingStartedList.getDomElement()); + } + setTimeout(() => this.categoriesPageScrollbar?.scanDomNode(), 50); + layoutRecentList(); + }; + videoList.onDidChange(layoutVideos); + } + const gettingStartedList = this.buildGettingStartedWalkthroughsList(); - const videoList = this.buildVideosList(); const footer = $('.footer', {}, $('p.showOnStartup', {}, @@ -826,16 +852,16 @@ export class GettingStartedPage extends EditorPane { const layoutLists = () => { if (gettingStartedList.itemCount) { this.container.classList.remove('noWalkthroughs'); - if (videoList.itemCount > 0) { - reset(rightColumn, videoList.getDomElement(), gettingStartedList.getDomElement()); + if (videoList?.itemCount > 0) { + reset(rightColumn, videoList?.getDomElement(), gettingStartedList.getDomElement()); } else { reset(rightColumn, gettingStartedList.getDomElement()); } } else { this.container.classList.add('noWalkthroughs'); - if (videoList.itemCount > 0) { - reset(rightColumn, videoList.getDomElement()); + if (videoList?.itemCount > 0) { + reset(rightColumn, videoList?.getDomElement()); } else { reset(rightColumn); @@ -845,19 +871,8 @@ export class GettingStartedPage extends EditorPane { layoutRecentList(); }; - const layoutVideos = () => { - if (videoList.itemCount > 0) { - reset(rightColumn, videoList.getDomElement(), gettingStartedList.getDomElement()); - } - else { - reset(rightColumn, gettingStartedList.getDomElement()); - } - setTimeout(() => this.categoriesPageScrollbar?.scanDomNode(), 50); - layoutRecentList(); - }; - const layoutRecentList = () => { - if (this.container.classList.contains('noWalkthroughs') && videoList.itemCount === 0) { + if (this.container.classList.contains('noWalkthroughs') && videoList?.itemCount === 0) { recentList.setLimit(10); reset(leftColumn, startList.getDomElement()); reset(rightColumn, recentList.getDomElement()); @@ -867,7 +882,6 @@ export class GettingStartedPage extends EditorPane { } }; - videoList.onDidChange(layoutVideos); gettingStartedList.onDidChange(layoutLists); layoutLists(); @@ -901,7 +915,7 @@ export class GettingStartedPage extends EditorPane { const telemetryNotice = $('p.telemetry-notice'); this.buildTelemetryFooter(telemetryNotice); footer.appendChild(telemetryNotice); - } else if (!this.productService.openToWelcomeMainPage && !someStepsComplete && !this.hasScrolledToFirstCategory) { + } else if (!this.productService.openToWelcomeMainPage && !someStepsComplete && !this.hasScrolledToFirstCategory && !this.showFeaturedWalkthrough) { const firstSessionDateString = this.storageService.get(firstSessionDateStorageKey, StorageScope.APPLICATION) || new Date().toUTCString(); const daysSinceFirstSession = ((+new Date()) - (+new Date(firstSessionDateString))) / 1000 / 60 / 60 / 24; const fistContentBehaviour = daysSinceFirstSession < 1 ? 'openToFirstCategory' : 'index'; @@ -1046,7 +1060,7 @@ export class GettingStartedPage extends EditorPane { const featuredBadge = $('.featured-badge', {}); const descriptionContent = $('.description-content', {},); - if (category.isFeatured) { + if (category.isFeatured && this.showFeaturedWalkthrough) { reset(featuredBadge, $('.featured', {}, $('span.featured-icon.codicon.codicon-star-full'))); reset(descriptionContent, ...renderLabelWithIcons(category.description)); } @@ -1054,7 +1068,7 @@ export class GettingStartedPage extends EditorPane { const titleContent = $('h3.category-title.max-lines-3', { 'x-category-title-for': category.id }); reset(titleContent, ...renderLabelWithIcons(category.title)); - return $('button.getting-started-category' + (category.isFeatured ? '.featured' : ''), + return $('button.getting-started-category' + (category.isFeatured && this.showFeaturedWalkthrough ? '.featured' : ''), { 'x-dispatch': 'selectCategory:' + category.id, 'title': category.description @@ -1122,29 +1136,33 @@ export class GettingStartedPage extends EditorPane { const renderFeaturedExtensions = (entry: IWelcomePageStartEntry): HTMLElement => { + const featuredBadge = $('.featured-badge', {}); const descriptionContent = $('.description-content', {},); + reset(featuredBadge, $('.featured', {}, $('span.featured-icon.codicon.codicon-star-full'))); reset(descriptionContent, ...renderLabelWithIcons(entry.description)); const titleContent = $('h3.category-title.max-lines-3', { 'x-category-title-for': entry.id }); reset(titleContent, ...renderLabelWithIcons(entry.title)); - return $('button.getting-started-category', + return $('button.getting-started-category' + '.featured', { 'x-dispatch': 'openLink:' + entry.command, - 'title': entry.description + 'title': entry.title }, + featuredBadge, $('.main-content', {}, this.iconWidgetFor(entry), titleContent, $('a.codicon.codicon-close.hide-category-button', { 'tabindex': 0, - 'x-dispatch': 'hideVideosContent', + 'x-dispatch': 'hideVideos', 'title': localize('close', "Hide"), 'role': 'button', 'aria-label': localize('closeAriaLabel', "Hide"), }), - )); + ), + descriptionContent); }; if (this.videoList) { @@ -1159,13 +1177,17 @@ export class GettingStartedPage extends EditorPane { contextService: this.contextService, }); + if (this.getHiddenCategories().has('getting-started-videos')) { + return videoList; + } + videoList.setEntries([{ - id: 'videos', - title: 'Watch Tutorials', - description: 'Learn VS Code\'s must-have features in short and practical tutorials', - icon: { type: 'icon', icon: Codicon.play }, - command: 'https://www.youtube.com/watch?v=B-s71n0dHUk&list=PLj6YeMhvp2S5UgiQnBfvD7XgOMKs3O_G6&index=1', + id: 'getting-started-videos', + title: localize('videos-title', 'Discover Getting Started Tutorials'), + description: localize('videos-description', 'Learn VS Code\'s must-have features in short and practical videos'), + command: 'https://aka.ms/vscode-getting-started-tutorials', order: 0, + icon: { type: 'icon', icon: Codicon.play }, when: ContextKeyExpr.true(), }]); videoList.onDidChange(() => this.registerDispatchListeners()); From c00edbdfa7b58463a616c1931cc5fdbc13897051 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Sun, 10 Mar 2024 20:58:49 -0700 Subject: [PATCH 3/3] Fix logic for showing walkthrough --- .../contrib/welcomeGettingStarted/browser/gettingStarted.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index ec29e5f474473..1695dfa8ce5ec 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -162,7 +162,7 @@ export class GettingStartedPage extends EditorPane { private detailsRenderer: GettingStartedDetailsRenderer; private categoriesSlideDisposables: DisposableStore; - private showFeaturedWalkthrough: boolean = true; + private showFeaturedWalkthrough = true; constructor( group: IEditorGroup, @@ -915,7 +915,7 @@ export class GettingStartedPage extends EditorPane { const telemetryNotice = $('p.telemetry-notice'); this.buildTelemetryFooter(telemetryNotice); footer.appendChild(telemetryNotice); - } else if (!this.productService.openToWelcomeMainPage && !someStepsComplete && !this.hasScrolledToFirstCategory && !this.showFeaturedWalkthrough) { + } else if (!this.productService.openToWelcomeMainPage && !someStepsComplete && !this.hasScrolledToFirstCategory && this.showFeaturedWalkthrough) { const firstSessionDateString = this.storageService.get(firstSessionDateStorageKey, StorageScope.APPLICATION) || new Date().toUTCString(); const daysSinceFirstSession = ((+new Date()) - (+new Date(firstSessionDateString))) / 1000 / 60 / 60 / 24; const fistContentBehaviour = daysSinceFirstSession < 1 ? 'openToFirstCategory' : 'index';