Skip to content

Commit e03f9bd

Browse files
author
Jackson Kearl
committed
Close #122702
1 parent 727146d commit e03f9bd

File tree

4 files changed

+157
-26
lines changed

4 files changed

+157
-26
lines changed

src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.css

+52-7
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@
164164
margin: 4px 0 4px;
165165
font-size: 14px;
166166
font-weight: 500;
167+
text-align: left;
168+
flex-grow: 1;
167169
}
168170

169171
.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlideCategories .category-progress {
@@ -174,7 +176,7 @@
174176
}
175177

176178
.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category.no-progress {
177-
padding: 3px 12px;
179+
padding: 3px 6px;
178180
}
179181

180182
.monaco-workbench .part.editor>.content .gettingStartedContainer .getting-started-category.no-progress .category-progress {
@@ -232,10 +234,9 @@
232234
box-sizing: border-box;
233235
line-height: normal;
234236
margin: 8px 8px 12px;
235-
padding: 3px 12px 6px;
237+
padding: 3px 6px 6px;
236238
left: 1px;
237239
text-align: left;
238-
display: flex;
239240
}
240241

241242
.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlideCategories .getting-started-category {
@@ -248,12 +249,56 @@
248249
font-size: 20px;
249250
}
250251

251-
.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category .codicon.hide-category-button {
252+
253+
.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category .main-content {
254+
display: flex;
255+
align-items: center;
256+
justify-content: flex-start;
257+
width: 100%;
258+
}
259+
260+
.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category .description-content {
261+
text-align: left;
262+
margin-left: 28px;
263+
}
264+
265+
.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category .new-badge {
266+
justify-self: flex-end;
267+
border-radius: 4px;
268+
padding: 0 2px;
269+
}
270+
271+
.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category .featured-badge {
272+
position: relative;
273+
top: -4px;
274+
left: -8px;
275+
}
276+
277+
.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category .featured {
278+
border-top: 30px solid red;
279+
width: 30px;
280+
box-sizing: border-box;
281+
height: 20px;
282+
border-right: 40px solid transparent;
252283
position: absolute;
253-
top: 8px;
254-
right: 8px;
284+
}
285+
286+
.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category .featured .featured-icon {
287+
top: -30px;
288+
left: 4px;
289+
font-size: 12pt;
290+
}
291+
292+
.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category .codicon.hide-category-button {
293+
position: relative;
294+
top: 0px;
295+
align-self: start;
296+
left: 8px;
255297
font-size: 16px;
256-
padding-right: 0;
298+
}
299+
300+
.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category.featured .icon-widget {
301+
visibility: hidden;
257302
}
258303

259304
.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlideCategories .getting-started-category img.category-icon {

src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts

+48-14
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { IProductService } from 'vs/platform/product/common/productService';
1515
import { hiddenEntriesConfigurationKey, IGettingStartedCategory, IGettingStartedCategoryWithProgress, IGettingStartedService } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService';
1616
import { IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService';
1717
import { welcomePageBackground, welcomePageProgressBackground, welcomePageProgressForeground, welcomePageTileBackground, welcomePageTileHoverBackground, welcomePageTileShadow } from 'vs/workbench/contrib/welcome/page/browser/welcomePageColors';
18-
import { activeContrastBorder, buttonBackground, buttonForeground, buttonHoverBackground, contrastBorder, descriptionForeground, focusBorder, foreground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry';
18+
import { activeContrastBorder, badgeBackground, buttonBackground, buttonForeground, buttonHoverBackground, contrastBorder, descriptionForeground, focusBorder, foreground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry';
1919
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
2020
import { firstSessionDateStorageKey, ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
2121
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
@@ -61,6 +61,7 @@ import { Schemas } from 'vs/base/common/network';
6161
import { IEditorOptions } from 'vs/platform/editor/common/editor';
6262
import { coalesce, flatten } from 'vs/base/common/arrays';
6363
import { ThemeSettings } from 'vs/workbench/services/themes/common/workbenchThemeService';
64+
import { ACTIVITY_BAR_BADGE_FOREGROUND } from 'vs/workbench/common/theme';
6465

6566
const SLIDE_TRANSITION_TIME_MS = 250;
6667
const configurationKey = 'workbench.startupEditor';
@@ -107,7 +108,7 @@ export class GettingStartedPage extends EditorPane {
107108
private hasScrolledToFirstCategory = false;
108109
private recentlyOpenedList?: GettingStartedIndexList<IRecentFolder | IRecentWorkspace>;
109110
private startList?: GettingStartedIndexList<IGettingStartedCategory>;
110-
private gettingStartedList?: GettingStartedIndexList<IGettingStartedCategory>;
111+
private gettingStartedList?: GettingStartedIndexList<IGettingStartedCategoryWithProgress>;
111112

112113
private stepsSlide!: HTMLElement;
113114
private categoriesSlide!: HTMLElement;
@@ -906,39 +907,58 @@ export class GettingStartedPage extends EditorPane {
906907
return startList;
907908
}
908909

909-
private buildGettingStartedWalkthroughsList(): GettingStartedIndexList<IGettingStartedCategory> {
910+
private buildGettingStartedWalkthroughsList(): GettingStartedIndexList<IGettingStartedCategoryWithProgress> {
910911

911-
const renderGetttingStaredWalkthrough = (category: IGettingStartedCategory) => {
912+
const renderGetttingStaredWalkthrough = (category: IGettingStartedCategoryWithProgress) => {
912913
const hiddenCategories = this.getHiddenCategories();
913914

914915
if (category.content.type !== 'steps' || hiddenCategories.has(category.id)) {
915916
return undefined;
916917
}
917918

918-
return $('button.getting-started-category',
919+
const newBadge = $('.new-badge', {});
920+
if (category.content.accolades === 'newCategory') {
921+
reset(newBadge, $('.new-category', {}, localize('new', "New")));
922+
} else if (category.content.accolades === 'newContent') {
923+
reset(newBadge, $('.new-items', {}, localize('newItems', "New Items")));
924+
}
925+
926+
const featuredBadge = $('.featured-badge', {});
927+
const descriptionContent = $('.description-content', {},);
928+
929+
if (category.content.accolades === 'featured') {
930+
reset(featuredBadge, $('.featured', {}, $('span.featured-icon.codicon.codicon-star-empty')));
931+
reset(descriptionContent, category.description);
932+
}
933+
934+
return $('button.getting-started-category' + (category.content.accolades === 'featured' ? '.featured' : ''),
919935
{
920936
'x-dispatch': 'selectCategory:' + category.id,
921937
'role': 'listitem',
922938
'title': category.description
923939
},
924-
this.iconWidgetFor(category),
925-
$('a.codicon.codicon-close.hide-category-button', {
926-
'x-dispatch': 'hideCategory:' + category.id,
927-
'title': localize('close', "Hide"),
928-
}),
929-
$('h3.category-title.max-lines-3', { 'x-category-title-for': category.id }, category.title),
940+
featuredBadge,
941+
$('.main-content', {},
942+
this.iconWidgetFor(category),
943+
$('h3.category-title.max-lines-3', { 'x-category-title-for': category.id }, category.title,),
944+
newBadge,
945+
$('a.codicon.codicon-close.hide-category-button', {
946+
'x-dispatch': 'hideCategory:' + category.id,
947+
'title': localize('close', "Hide"),
948+
}),
949+
),
950+
descriptionContent,
930951
$('.category-progress', { 'x-data-category-id': category.id, },
931952
$('.progress-bar-outer', { 'role': 'progressbar' },
932953
$('.progress-bar-inner'))));
933954
};
934955

935956
if (this.gettingStartedList) { this.gettingStartedList.dispose(); }
936957

937-
const limit = Math.min(this.gettingStartedCategories.filter(c => c.priority > 0).length, 5);
938958
const gettingStartedList = this.gettingStartedList = new GettingStartedIndexList(
939959
localize('gettingStarted', "Getting Started"),
940960
'getting-started',
941-
limit,
961+
5,
942962
undefined,
943963
undefined,
944964
$('button.button-link.see-all-walkthroughs', { 'tabindex': '0', 'x-dispatch': 'seeAllWalkthroughs' }, localize('seeMore', "See More")),
@@ -1018,7 +1038,9 @@ export class GettingStartedPage extends EditorPane {
10181038
}
10191039

10201040
private iconWidgetFor(category: IGettingStartedCategory) {
1021-
return category.icon.type === 'icon' ? $(ThemeIcon.asCSSSelector(category.icon.icon)) : $('img.category-icon', { src: category.icon.path });
1041+
const widget = category.icon.type === 'icon' ? $(ThemeIcon.asCSSSelector(category.icon.icon)) : $('img.category-icon', { src: category.icon.path });
1042+
widget.classList.add('icon-widget');
1043+
return widget;
10221044
}
10231045

10241046
private buildStepMarkdownDescription(container: HTMLElement, text: LinkedText[]) {
@@ -1474,4 +1496,16 @@ registerThemingParticipant((theme, collector) => {
14741496
if (progressForeground) {
14751497
collector.addRule(`.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideCategories .progress-bar-inner { background-color: ${progressForeground}; }`);
14761498
}
1499+
1500+
const newBadgeForeground = theme.getColor(ACTIVITY_BAR_BADGE_FOREGROUND);
1501+
if (newBadgeForeground) {
1502+
collector.addRule(`.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category .new-badge { color: ${newBadgeForeground}; }`);
1503+
collector.addRule(`.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category .featured .featured-icon { color: ${newBadgeForeground}; }`);
1504+
}
1505+
1506+
const newBadgeBackground = theme.getColor(badgeBackground);
1507+
if (newBadgeBackground) {
1508+
collector.addRule(`.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category .new-badge { background-color: ${newBadgeBackground}; }`);
1509+
collector.addRule(`.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category .featured { border-top-color: ${newBadgeBackground}; }`);
1510+
}
14771511
});

src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts

+52-5
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,12 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
3838
export const WorkspacePlatform = new RawContextKey<'mac' | 'linux' | 'windows' | undefined>('workspacePlatform', undefined, localize('workspacePlatform', "The platform of the current workspace, which in remote contexts may be different from the platform of the UI"));
3939

4040
export const IGettingStartedService = createDecorator<IGettingStartedService>('gettingStartedService');
41+
4142
export const hiddenEntriesConfigurationKey = 'workbench.welcomePage.hiddenCategories';
4243

44+
export const walkthroughMetadataConfigurationKey = 'workbench.welcomePage.walkthroughMetadata';
45+
export type WalkthroughMetaDataType = Map<string, { firstSeen: number; stepIDs: string[]; }>;
46+
4347
export const enum GettingStartedCategory {
4448
Beginner = 'Beginner',
4549
Intermediate = 'Intermediate',
@@ -69,6 +73,7 @@ export interface IGettingStartedWalkthroughDescriptor {
6973
id: GettingStartedCategory | string
7074
title: string
7175
description: string
76+
isFeatured: boolean
7277
order: number
7378
next?: string
7479
icon:
@@ -115,6 +120,7 @@ export interface IGettingStartedCategory {
115120
id: GettingStartedCategory | string
116121
title: string
117122
description: string
123+
isFeatured: boolean
118124
order: number
119125
next?: string
120126
icon:
@@ -135,6 +141,7 @@ export interface IGettingStartedCategoryWithProgress extends Omit<IGettingStarte
135141
| {
136142
type: 'steps',
137143
steps: IGettingStartedStepWithProgress[],
144+
accolades: 'newCategory' | 'newContent' | 'featured' | undefined
138145
done: boolean;
139146
stepsComplete: number
140147
stepsTotal: number
@@ -166,6 +173,9 @@ export interface IGettingStartedService {
166173
installedExtensionsRegistered: Promise<void>;
167174
}
168175

176+
// Show walkthrough as "new" for 7 days after first install
177+
const NEW_WALKTHROUGH_TIME = 7 * 24 * 60 * 60 * 1000;
178+
169179
export class GettingStartedService extends Disposable implements IGettingStartedService {
170180
declare readonly _serviceBrand: undefined;
171181

@@ -208,6 +218,8 @@ export class GettingStartedService extends Disposable implements IGettingStarted
208218
private triggerInstalledExtensionsRegistered!: () => void;
209219
installedExtensionsRegistered: Promise<void>;
210220

221+
private metadata: WalkthroughMetaDataType;
222+
211223
constructor(
212224
@IStorageService private readonly storageService: IStorageService,
213225
@ICommandService private readonly commandService: ICommandService,
@@ -227,6 +239,9 @@ export class GettingStartedService extends Disposable implements IGettingStarted
227239

228240
this.tasExperimentService = tasExperimentService;
229241

242+
this.metadata = new Map(
243+
JSON.parse(
244+
this.storageService.get(walkthroughMetadataConfigurationKey, StorageScope.GLOBAL, '[]')));
230245

231246
const builtinNewMenuItems = [
232247
{
@@ -528,6 +543,11 @@ export class GettingStartedService extends Disposable implements IGettingStarted
528543
await Promise.all(extension.contributes?.walkthroughs?.map(async (walkthrough, index) => {
529544
const categoryID = extension.identifier.value + '#' + walkthrough.id;
530545

546+
const isNewlyInstalled = !this.metadata.get(categoryID);
547+
if (isNewlyInstalled) {
548+
this.metadata.set(categoryID, { firstSeen: +new Date(), stepIDs: walkthrough.steps.map(s => s.id) });
549+
}
550+
531551
const override = await Promise.race([
532552
this.tasExperimentService?.getTreatment<string>(`gettingStarted.overrideCategory.${categoryID}.when`),
533553
new Promise<string | undefined>(resolve => setTimeout(() => resolve(walkthrough.when), 5000))
@@ -538,17 +558,18 @@ export class GettingStartedService extends Disposable implements IGettingStarted
538558
&& this.contextService.contextMatchesRules(ContextKeyExpr.deserialize(override ?? walkthrough.when) ?? ContextKeyExpr.true())
539559
) {
540560
this.sessionInstalledExtensions.delete(extension.identifier.value.toLowerCase());
541-
if (index < sectionToOpenIndex) {
561+
if (index < sectionToOpenIndex && isNewlyInstalled) {
542562
sectionToOpen = categoryID;
543563
sectionToOpenIndex = index;
544564
}
545565
}
546566

547-
const walkthoughDescriptior = {
567+
const walkthoughDescriptior: IGettingStartedWalkthroughDescriptor = {
548568
content: { type: 'steps' },
549569
description: walkthrough.description,
550570
title: walkthrough.title,
551571
id: categoryID,
572+
isFeatured: false,
552573
order: Math.min(),
553574
icon: {
554575
type: 'image',
@@ -619,6 +640,8 @@ export class GettingStartedService extends Disposable implements IGettingStarted
619640
this.registerWalkthrough(walkthoughDescriptior, steps);
620641
}));
621642

643+
this.storageService.store(walkthroughMetadataConfigurationKey, JSON.stringify([...this.metadata.entries()]), StorageScope.GLOBAL, StorageTarget.USER);
644+
622645
this.triggerInstalledExtensionsRegistered();
623646

624647
if (sectionToOpen && this.configurationService.getValue<string>('workbench.welcomePage.walkthroughs.openOnInstall')) {
@@ -753,7 +776,7 @@ export class GettingStartedService extends Disposable implements IGettingStarted
753776
})
754777
.filter(category => category.content.type !== 'steps' || category.content.steps.length)
755778
.map(category => this.getCategoryProgress(category))
756-
.sort((a, b) => a.priority - b.priority);
779+
.sort((a, b) => b.priority - a.priority);
757780
return categoriesWithCompletion;
758781
}
759782

@@ -764,12 +787,35 @@ export class GettingStartedService extends Disposable implements IGettingStarted
764787
const stepsWithProgress = category.content.steps.map(step => this.getStepProgress(step));
765788
const stepsComplete = stepsWithProgress.filter(step => step.done);
766789

790+
const isFeatured = category.isFeatured;
791+
792+
const firstSeenDate = this.metadata.get(category.id)?.firstSeen;
793+
const isNew = firstSeenDate && firstSeenDate > (+new Date() - NEW_WALKTHROUGH_TIME);
794+
795+
const lastStepIDs = this.metadata.get(category.id)?.stepIDs;
796+
const hasNewSteps = lastStepIDs && category.content.steps.length === lastStepIDs.length && category.content.steps.every(({ id }, index) => id === lastStepIDs[index]);
797+
798+
let priority = 0;
799+
800+
if (isFeatured) {
801+
priority += 20;
802+
}
803+
804+
if (isNew && firstSeenDate) {
805+
priority += 10 + (NEW_WALKTHROUGH_TIME - (+new Date() - firstSeenDate)) / (24 * 60 * 60 * 1000);
806+
}
807+
808+
if (hasNewSteps) {
809+
priority += 1;
810+
}
767811
return {
768812
...category,
769-
priority: 1 - (stepsComplete.length / stepsWithProgress.length),
813+
priority,
770814
content: {
771815
type: 'steps',
772816
steps: stepsWithProgress,
817+
accolades: isFeatured ? 'featured' : isNew ? 'newCategory' : hasNewSteps ? 'newContent' : undefined,
818+
// accolades: Math.random() < 0.333 ? 'featured' : Math.random() < 0.5 ? 'newCategory' : 'newContent',
773819
stepsComplete: stepsComplete.length,
774820
stepsTotal: stepsWithProgress.length,
775821
done: stepsComplete.length === stepsWithProgress.length,
@@ -816,7 +862,7 @@ export class GettingStartedService extends Disposable implements IGettingStarted
816862
return;
817863
}
818864

819-
const category: IGettingStartedCategory = { ...categoryDescriptor };
865+
const category: IGettingStartedCategory = { ...categoryDescriptor, isFeatured: false };
820866

821867
this.gettingStartedContributions.set(categoryDescriptor.id, category);
822868
this._onDidAddCategory.fire();
@@ -830,6 +876,7 @@ export class GettingStartedService extends Disposable implements IGettingStarted
830876
}
831877

832878
const category: IGettingStartedCategory = { ...categoryDescriptor, content: { type: 'steps', steps } };
879+
833880
this.gettingStartedContributions.set(categoryDescriptor.id, category);
834881
steps.forEach(step => {
835882
if (this.steps.has(step.id)) { throw Error('Attempting to register step with id ' + step.id + ' twice. Second is dropped.'); }

0 commit comments

Comments
 (0)