From c658210cce8145f16af4853d78985623b1371838 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Mon, 11 Mar 2024 00:27:12 -0700 Subject: [PATCH] Feature - Surface video tutorials on Welcome Page (#207293) * Initial support for video tutorials * Experiment for surfacing video tutorials --- .../browser/gettingStarted.ts | 138 ++++++++++++++++-- 1 file changed, 122 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 725248d0031de..1695dfa8ce5ec 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'; @@ -148,6 +149,7 @@ export class GettingStartedPage extends EditorPane { private recentlyOpenedList?: GettingStartedIndexList; private startList?: GettingStartedIndexList; private gettingStartedList?: GettingStartedIndexList; + private videoList?: GettingStartedIndexList; private stepsSlide!: HTMLElement; private categoriesSlide!: HTMLElement; @@ -160,6 +162,7 @@ export class GettingStartedPage extends EditorPane { private detailsRenderer: GettingStartedDetailsRenderer; private categoriesSlideDisposables: DisposableStore; + private showFeaturedWalkthrough = true; constructor( group: IEditorGroup, @@ -185,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); @@ -345,7 +350,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 +444,12 @@ export class GettingStartedPage extends EditorPane { } break; } - case 'openExtensionPage': { - this.commandService.executeCommand('extension.open', argument); + case 'hideVideos': { + this.hideVideos(); break; } - case 'hideExtension': { - this.hideExtension(argument); + case 'openLink': { + this.openerService.open(argument); break; } default: { @@ -455,9 +466,9 @@ export class GettingStartedPage extends EditorPane { this.gettingStartedList?.rerender(); } - private hideExtension(extensionId: string) { - this.setHiddenCategories([...this.getHiddenCategories().add(extensionId)]); - this.registerDispatchListeners(); + private hideVideos() { + this.setHiddenCategories([...this.getHiddenCategories().add('getting-started-videos')]); + this.videoList?.setEntries(undefined); } private markAllStepsComplete() { @@ -807,6 +818,29 @@ 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 footer = $('.footer', {}, @@ -818,19 +852,27 @@ 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 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()); @@ -873,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'; @@ -1018,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)); } @@ -1026,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 @@ -1090,6 +1132,69 @@ export class GettingStartedPage extends EditorPane { return gettingStartedList; } + private buildVideosList(): GettingStartedIndexList { + + 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' + '.featured', + { + 'x-dispatch': 'openLink:' + entry.command, + 'title': entry.title + }, + featuredBadge, + $('.main-content', {}, + this.iconWidgetFor(entry), + titleContent, + $('a.codicon.codicon-close.hide-category-button', { + 'tabindex': 0, + 'x-dispatch': 'hideVideos', + 'title': localize('close', "Hide"), + 'role': 'button', + 'aria-label': localize('closeAriaLabel', "Hide"), + }), + ), + descriptionContent); + }; + + if (this.videoList) { + this.videoList.dispose(); + } + const videoList = this.videoList = new GettingStartedIndexList( + { + title: '', + klass: 'getting-started-videos', + limit: 1, + renderElement: renderFeaturedExtensions, + contextService: this.contextService, + }); + + if (this.getHiddenCategories().has('getting-started-videos')) { + return videoList; + } + + videoList.setEntries([{ + 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()); + + return videoList; + } + layout(size: Dimension) { this.detailsScrollbar?.scanDomNode(); @@ -1099,6 +1204,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();