From dffefeb4116554d7d36e6f1537d9090266c19e01 Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Wed, 31 May 2023 14:58:08 -0400 Subject: [PATCH] Adds new Account view & brand new Home experience --- package.json | 48 ++- src/commands/showView.ts | 2 + src/constants.ts | 3 +- src/container.ts | 7 + src/plus/subscription/subscriptionService.ts | 14 +- src/plus/webviews/account/accountWebview.ts | 120 ++++++ src/plus/webviews/account/protocol.ts | 18 + src/plus/webviews/account/registration.ts | 22 ++ src/telemetry/usageTracker.ts | 3 +- src/webviews/apps/home/components/card-section.ts | 109 ------ src/webviews/apps/home/components/header-card.ts | 412 -------------------- src/webviews/apps/home/components/plus-banner.ts | 266 ------------- src/webviews/apps/home/components/plus-content.ts | 129 ------- .../apps/home/components/stepped-section.ts | 134 ------- src/webviews/apps/home/home.html | 387 ++++++------------- src/webviews/apps/home/home.scss | 421 ++++----------------- src/webviews/apps/home/home.ts | 294 +------------- src/webviews/apps/plus/account/account.html | 21 + src/webviews/apps/plus/account/account.scss | 121 ++++++ src/webviews/apps/plus/account/account.ts | 105 +++++ .../apps/plus/account/components/header-card.ts | 378 ++++++++++++++++++ .../apps/plus/account/components/plus-content.ts | 129 +++++++ .../shared/components/account/account-badge.ts | 6 +- src/webviews/home/homeWebview.ts | 289 +------------- src/webviews/home/protocol.ts | 55 +-- webpack.config.js | 2 + 26 files changed, 1173 insertions(+), 2322 deletions(-) create mode 100644 src/plus/webviews/account/accountWebview.ts create mode 100644 src/plus/webviews/account/protocol.ts create mode 100644 src/plus/webviews/account/registration.ts delete mode 100644 src/webviews/apps/home/components/card-section.ts delete mode 100644 src/webviews/apps/home/components/header-card.ts delete mode 100644 src/webviews/apps/home/components/plus-banner.ts delete mode 100644 src/webviews/apps/home/components/plus-content.ts delete mode 100644 src/webviews/apps/home/components/stepped-section.ts create mode 100644 src/webviews/apps/plus/account/account.html create mode 100644 src/webviews/apps/plus/account/account.scss create mode 100644 src/webviews/apps/plus/account/account.ts create mode 100644 src/webviews/apps/plus/account/components/header-card.ts create mode 100644 src/webviews/apps/plus/account/components/plus-content.ts diff --git a/package.json b/package.json index a8d6b2c..8c7e1aa 100644 --- a/package.json +++ b/package.json @@ -4606,6 +4606,11 @@ "category": "GitLens" }, { + "command": "gitlens.showAccountView", + "title": "Show Account View", + "category": "GitLens" + }, + { "command": "gitlens.showInCommitGraph", "title": "Open in Commit Graph", "category": "GitLens+", @@ -6381,6 +6386,12 @@ "icon": "$(refresh)" }, { + "command": "gitlens.views.account.refresh", + "title": "Refresh", + "category": "GitLens", + "icon": "$(refresh)" + }, + { "command": "gitlens.views.lineHistory.changeBase", "title": "Change Base...", "category": "GitLens", @@ -6832,12 +6843,6 @@ "category": "GitLens" }, { - "command": "gitlens.views.timeline.openInTab", - "title": "Open in Editor Area", - "category": "GitLens", - "icon": "$(link-external)" - }, - { "command": "gitlens.views.timeline.refresh", "title": "Refresh", "category": "GitLens", @@ -7721,6 +7726,10 @@ "when": "gitlens:enabled" }, { + "command": "gitlens.showAccountView", + "when": "gitlens:enabled" + }, + { "command": "gitlens.showInCommitGraph", "when": "false" }, @@ -8905,6 +8914,10 @@ "when": "false" }, { + "command": "gitlens.views.account.refresh", + "when": "false" + }, + { "command": "gitlens.views.lineHistory.changeBase", "when": "false" }, @@ -9233,10 +9246,6 @@ "when": "false" }, { - "command": "gitlens.views.timeline.openInTab", - "when": "false" - }, - { "command": "gitlens.views.timeline.refresh", "when": "false" }, @@ -10343,6 +10352,11 @@ "group": "navigation@99" }, { + "command": "gitlens.views.account.refresh", + "when": "view =~ /^gitlens\\.views\\.account/", + "group": "navigation@99" + }, + { "command": "gitlens.showLineHistoryView", "when": "!gitlens:hasVirtualFolders && view =~ /^gitlens\\.views\\.fileHistory/", "group": "8_gitlens_toggles@0" @@ -10618,11 +10632,6 @@ "group": "5_gitlens@0" }, { - "command": "gitlens.views.timeline.openInTab", - "when": "view =~ /^gitlens\\.views\\.timeline/", - "group": "navigation@98" - }, - { "command": "gitlens.views.timeline.refresh", "when": "view =~ /^gitlens\\.views\\.timeline/", "group": "navigation@99" @@ -13701,6 +13710,15 @@ "icon": "$(gitlens-contributors-view)", "initialSize": 1, "visibility": "collapsed" + }, + { + "type": "webview", + "id": "gitlens.views.account", + "name": "GitKraken Account", + "contextualTitle": "GitLens", + "icon": "$(gitlens-gitlens)", + "initialSize": 1, + "visibility": "collapsed" } ], "gitlensInspect": [ diff --git a/src/commands/showView.ts b/src/commands/showView.ts index 7f78a0a..1a7ffc0 100644 --- a/src/commands/showView.ts +++ b/src/commands/showView.ts @@ -44,6 +44,8 @@ export class ShowViewCommand extends Command { return this.container.fileHistoryView.show(); case Commands.ShowHomeView: return this.container.homeView.show(); + case Commands.ShowAccountView: + return this.container.accountView.show(); case Commands.ShowGraphView: return this.container.graphView.show(); case Commands.ShowLineHistoryView: diff --git a/src/constants.ts b/src/constants.ts index 969078a..43729e5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -239,6 +239,7 @@ export const enum Commands { ShowGraphPage = 'gitlens.showGraphPage', ShowGraphView = 'gitlens.showGraphView', ShowHomeView = 'gitlens.showHomeView', + ShowAccountView = 'gitlens.showAccountView', ShowInCommitGraph = 'gitlens.showInCommitGraph', ShowInDetailsView = 'gitlens.showInDetailsView', ShowLastQuickPick = 'gitlens.showLastQuickPick', @@ -311,7 +312,7 @@ export const enum Commands { export type CustomEditorIds = 'rebase'; export type WebviewIds = 'graph' | 'settings' | 'timeline' | 'welcome' | 'focus'; -export type WebviewViewIds = 'commitDetails' | 'graph' | 'graphDetails' | 'home' | 'timeline'; +export type WebviewViewIds = 'commitDetails' | 'graph' | 'graphDetails' | 'home' | 'timeline' | 'account'; export type ContextKeys = | `${typeof extensionPrefix}:action:${string}` diff --git a/src/container.ts b/src/container.ts index 98cdb3c..c3731ae 100644 --- a/src/container.ts +++ b/src/container.ts @@ -23,6 +23,7 @@ import { IntegrationAuthenticationService } from './plus/integrationAuthenticati import { SubscriptionAuthenticationProvider } from './plus/subscription/authenticationProvider'; import { ServerConnection } from './plus/subscription/serverConnection'; import { SubscriptionService } from './plus/subscription/subscriptionService'; +import { registerAccountWebviewView } from './plus/webviews/account/registration'; import { registerFocusWebviewPanel } from './plus/webviews/focus/registration'; import { registerGraphWebviewCommands, @@ -250,6 +251,7 @@ export class Container { this._disposables.push((this._searchAndCompareView = new SearchAndCompareView(this))); this._disposables.push((this._homeView = registerHomeWebviewView(this._webviews))); + this._disposables.push((this._accountView = registerAccountWebviewView(this._webviews))); if (configuration.get('terminalLinks.enabled')) { this._disposables.push((this._terminalLinks = new GitTerminalLinkProvider(this))); @@ -484,6 +486,11 @@ export class Container { return this._homeView; } + private readonly _accountView: WebviewViewProxy; + get accountView() { + return this._accountView; + } + @memoize() get id() { return this._context.extension.id; diff --git a/src/plus/subscription/subscriptionService.ts b/src/plus/subscription/subscriptionService.ts index 00a46be..1821f49 100644 --- a/src/plus/subscription/subscriptionService.ts +++ b/src/plus/subscription/subscriptionService.ts @@ -239,7 +239,7 @@ export class SubscriptionService implements Disposable { // Abort any waiting authentication to ensure we can start a new flow await this.container.subscriptionAuthentication.abort(); - void this.showHomeView(); + void this.showAccountView(); const session = await this.ensureSession(true); const loggedIn = Boolean(session); @@ -366,7 +366,7 @@ export class SubscriptionService implements Disposable { Uri.joinPath(this.baseAccountUri, 'subscription').with({ query: 'product=gitlens&license=PRO' }), ); } - await this.showHomeView(); + await this.showAccountView(); } @gate() @@ -376,7 +376,7 @@ export class SubscriptionService implements Disposable { const scope = getLogScope(); - void this.showHomeView(true); + void this.showAccountView(true); const session = await this.ensureSession(false); if (session == null) return false; @@ -429,11 +429,11 @@ export class SubscriptionService implements Disposable { } @log() - async showHomeView(silent: boolean = false): Promise { + async showAccountView(silent: boolean = false): Promise { if (silent && !configuration.get('plusFeatures.enabled', undefined, true)) return; - if (!this.container.homeView.visible) { - await executeCommand(Commands.ShowHomeView); + if (!this.container.accountView.visible) { + await executeCommand(Commands.ShowAccountView); } } @@ -448,7 +448,7 @@ export class SubscriptionService implements Disposable { let { plan, previewTrial } = this._subscription; if (previewTrial != null) { - void this.showHomeView(); + void this.showAccountView(); if (!silent && plan.effective.id === SubscriptionPlanId.Free) { const confirm: MessageItem = { title: 'Extend Your Trial', isCloseAffordance: true }; diff --git a/src/plus/webviews/account/accountWebview.ts b/src/plus/webviews/account/accountWebview.ts new file mode 100644 index 0000000..1c7c74a --- /dev/null +++ b/src/plus/webviews/account/accountWebview.ts @@ -0,0 +1,120 @@ +import { Disposable, window } from 'vscode'; +import { getAvatarUriFromGravatarEmail } from '../../../avatars'; +import type { Container } from '../../../container'; +import type { RepositoriesVisibility } from '../../../git/gitProviderService'; +import type { Subscription } from '../../../subscription'; +import { registerCommand } from '../../../system/command'; +import type { Deferrable } from '../../../system/function'; +import { debounce } from '../../../system/function'; +import type { WebviewController, WebviewProvider } from '../../../webviews/webviewController'; +import type { SubscriptionChangeEvent } from '../../subscription/subscriptionService'; +import type { State } from './protocol'; +import { DidChangeSubscriptionNotificationType } from './protocol'; + +export class AccountWebviewProvider implements WebviewProvider { + private readonly _disposable: Disposable; + + constructor(private readonly container: Container, private readonly host: WebviewController) { + this._disposable = Disposable.from(this.container.subscription.onDidChange(this.onSubscriptionChanged, this)); + } + + dispose() { + this._disposable.dispose(); + } + + private onSubscriptionChanged(e: SubscriptionChangeEvent) { + void this.notifyDidChangeData(e.current); + } + + onVisibilityChanged(visible: boolean): void { + if (!visible) { + this._validateSubscriptionDebounced?.cancel(); + return; + } + + queueMicrotask(() => void this.validateSubscription()); + } + + onWindowFocusChanged(focused: boolean): void { + if (!focused || !this.host.visible) { + this._validateSubscriptionDebounced?.cancel(); + return; + } + + queueMicrotask(() => void this.validateSubscription()); + } + + registerCommands(): Disposable[] { + return [registerCommand(`${this.host.id}.refresh`, () => this.host.refresh(true), this)]; + } + + includeBootstrap(): Promise { + return this.getState(); + } + + private async getRepoVisibility(): Promise { + const visibility = await this.container.git.visibility(); + return visibility; + } + + private async getSubscription(subscription?: Subscription) { + const sub = subscription ?? (await this.container.subscription.getSubscription(true)); + + let avatar; + if (sub.account?.email) { + avatar = getAvatarUriFromGravatarEmail(sub.account.email, 34).toString(); + } else { + avatar = `${this.host.getWebRoot() ?? ''}/media/gitlens-logo.webp`; + } + + return { + subscription: sub, + avatar: avatar, + }; + } + + private async getState(subscription?: Subscription): Promise { + const subscriptionResult = await this.getSubscription(subscription); + + return { + timestamp: Date.now(), + webroot: this.host.getWebRoot(), + subscription: subscriptionResult.subscription, + avatar: subscriptionResult.avatar, + }; + } + + private notifyDidChangeData(subscription?: Subscription) { + if (!this.host.ready) return false; + + return window.withProgress({ location: { viewId: this.host.id } }, async () => { + const sub = await this.getSubscription(subscription); + return this.host.notify(DidChangeSubscriptionNotificationType, { + ...sub, + }); + }); + } + + private _validateSubscriptionDebounced: Deferrable | undefined = + undefined; + + private async validateSubscription(): Promise { + if (this._validateSubscriptionDebounced == null) { + this._validateSubscriptionDebounced = debounce(this.validateSubscriptionCore, 1000); + } + + await this._validateSubscriptionDebounced(); + } + + private _validating: Promise | undefined; + private async validateSubscriptionCore() { + if (this._validating == null) { + this._validating = this.container.subscription.validate(); + try { + await this._validating; + } finally { + this._validating = undefined; + } + } + } +} diff --git a/src/plus/webviews/account/protocol.ts b/src/plus/webviews/account/protocol.ts new file mode 100644 index 0000000..6919fc2 --- /dev/null +++ b/src/plus/webviews/account/protocol.ts @@ -0,0 +1,18 @@ +import type { Subscription } from '../../../subscription'; +import { IpcNotificationType } from '../../../webviews/protocol'; + +export interface State { + timestamp: number; + + webroot?: string; + subscription: Subscription; + avatar?: string; +} + +export interface DidChangeSubscriptionParams { + subscription: Subscription; + avatar?: string; +} +export const DidChangeSubscriptionNotificationType = new IpcNotificationType( + 'subscription/didChange', +); diff --git a/src/plus/webviews/account/registration.ts b/src/plus/webviews/account/registration.ts new file mode 100644 index 0000000..8bdac46 --- /dev/null +++ b/src/plus/webviews/account/registration.ts @@ -0,0 +1,22 @@ +import type { WebviewsController } from '../../../webviews/webviewsController'; +import type { State } from './protocol'; + +export function registerAccountWebviewView(controller: WebviewsController) { + return controller.registerWebviewView( + { + id: 'gitlens.views.account', + fileName: 'account.html', + title: 'GitKraken Account', + contextKeyPrefix: `gitlens:webviewView:account`, + trackingFeature: 'accountView', + plusFeature: false, + webviewHostOptions: { + retainContextWhenHidden: false, + }, + }, + async (container, host) => { + const { AccountWebviewProvider } = await import(/* webpackChunkName: "account" */ './accountWebview'); + return new AccountWebviewProvider(container, host); + }, + ); +} diff --git a/src/telemetry/usageTracker.ts b/src/telemetry/usageTracker.ts index 49b2674..290354a 100644 --- a/src/telemetry/usageTracker.ts +++ b/src/telemetry/usageTracker.ts @@ -31,7 +31,8 @@ export type TrackedUsageFeatures = | 'timelineView' | 'welcomeWebview' | 'workspaceView' - | 'focusWebview'; + | 'focusWebview' + | 'accountView'; export type TrackedUsageKeys = `${TrackedUsageFeatures}:shown`; export type UsageChangeEvent = { diff --git a/src/webviews/apps/home/components/card-section.ts b/src/webviews/apps/home/components/card-section.ts deleted file mode 100644 index 5206ca4..0000000 --- a/src/webviews/apps/home/components/card-section.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { attr, css, customElement, FASTElement, html, when } from '@microsoft/fast-element'; -import { numberConverter } from '../../shared/components/converters/number-converter'; -import '../../shared/components/code-icon'; - -const template = html``; - -const styles = css` - * { - box-sizing: border-box; - } - - :host { - display: block; - padding: 1.2rem; - background-color: var(--card-background); - margin-bottom: 1rem; - border-radius: 0.4rem; - background-repeat: no-repeat; - background-size: cover; - transition: aspect-ratio linear 100ms, background-color linear 100ms; - } - - :host(:hover) { - background-color: var(--card-hover-background); - } - - header { - display: flex; - flex-direction: row; - justify-content: space-between; - gap: 0.4rem; - margin-bottom: 1rem; - } - - .dismiss { - width: 2rem; - height: 2rem; - padding: 0; - font-size: var(--vscode-editor-font-size); - line-height: 2rem; - font-family: inherit; - border: none; - color: inherit; - background: none; - text-align: left; - cursor: pointer; - opacity: 0.5; - flex: none; - text-align: center; - } - - .dismiss:focus { - outline: 1px solid var(--vscode-focusBorder); - outline-offset: 0.2rem; - } - - .heading { - text-transform: uppercase; - } - - .description { - margin-left: 0.2rem; - text-transform: none; - /* color needs to come from some sort property */ - color: #b68cd8; - } -`; - -@customElement({ name: 'card-section', template: template, styles: styles }) -export class CardSection extends FASTElement { - @attr({ attribute: 'no-heading', mode: 'boolean' }) - noHeading = false; - - @attr({ attribute: 'heading-level', converter: numberConverter }) - headingLevel = 2; - - @attr({ mode: 'boolean' }) - dismissable = false; - - @attr({ mode: 'boolean' }) - expanded = true; - - handleDismiss(_e: Event) { - this.$emit('dismiss'); - } -} diff --git a/src/webviews/apps/home/components/header-card.ts b/src/webviews/apps/home/components/header-card.ts deleted file mode 100644 index 45d248f..0000000 --- a/src/webviews/apps/home/components/header-card.ts +++ /dev/null @@ -1,412 +0,0 @@ -import { attr, css, customElement, FASTElement, html, ref, volatile, when } from '@microsoft/fast-element'; -import { SubscriptionState } from '../../../../subscription'; -import { pluralize } from '../../../../system/string'; -import { numberConverter } from '../../shared/components/converters/number-converter'; -import '../../shared/components/code-icon'; -import '../../shared/components/overlays/pop-over'; - -const template = html` -
GitLens Logo
-

- ${when(x => x.name === '', html`GitLens 13`)} - ${when(x => x.name !== '', html`${x => x.name}`)} -

- -
-
-
- - ${when( - x => x.state === SubscriptionState.FreePreviewTrialExpired, - html`Extend Pro Trial`, - )} - ${when( - x => - x.state === SubscriptionState.FreeInPreviewTrial || - x.state === SubscriptionState.FreePlusInTrial || - x.state === SubscriptionState.FreePlusTrialExpired, - html`Upgrade to Pro`, - )} - ${when( - x => x.state === SubscriptionState.VerificationRequired, - html` - Verify Refresh - `, - )} - -`; - -const styles = css` - * { - box-sizing: border-box; - } - - :host { - position: relative; - display: grid; - /* - padding: 1rem 1rem 1.2rem; - background-color: var(--card-background); - border-radius: 0.4rem; - */ - padding: 1rem 0 1.2rem; - gap: 0 0.8rem; - grid-template-columns: 3.4rem auto; - grid-auto-flow: column; - } - - a { - color: var(--vscode-textLink-foreground); - text-decoration: none; - } - a:focus { - outline-color: var(--focus-border); - } - a:hover { - text-decoration: underline; - } - - .header-card__media { - grid-column: 1; - grid-row: 1 / span 2; - display: flex; - align-items: center; - } - - .header-card__image { - width: 100%; - aspect-ratio: 1 / 1; - border-radius: 50%; - } - - .header-card__title { - font-size: var(--vscode-font-size); - font-weight: 600; - margin: 0; - } - - .header-card__title.logo { - font-family: 'Segoe UI Semibold', var(--font-family); - font-size: 1.5rem; - } - - .header-card__account { - position: relative; - margin: 0; - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 0 0.4rem; - } - - .progress { - width: 100%; - overflow: hidden; - } - - :host-context(.vscode-high-contrast) .progress, - :host-context(.vscode-dark) .progress { - background-color: var(--color-background--lighten-15); - } - - :host-context(.vscode-high-contrast-light) .progress, - :host-context(.vscode-light) .progress { - background-color: var(--color-background--darken-15); - } - - .progress__indicator { - height: 4px; - background-color: var(--vscode-progressBar-background); - } - - .header-card__progress { - position: absolute; - bottom: 0; - left: 0; - /* - border-bottom-left-radius: 0.4rem; - border-bottom-right-radius: 0.4rem; - */ - } - - .brand { - color: var(--gitlens-brand-color-2); - } - .status { - color: var(--color-foreground--65); - } - - .status-label { - cursor: help; - } - - .status pop-over { - top: 1.6em; - left: 0; - } - .status-label:not(:hover) + pop-over:not(.is-pinned) { - display: none; - } - - .repo-access { - font-size: 1.1em; - margin-right: 0.2rem; - } - .repo-access:not(.is-pro) { - filter: grayscale(1) brightness(0.7); - } - - .actions { - position: absolute; - right: 0.1rem; - top: 0.1rem; - } - - .action { - display: inline-block; - padding: 0.2rem 0.6rem; - border-radius: 0.3rem; - color: var(--color-foreground--75); - } - :host-context(.vscode-high-contrast) .action.is-primary, - :host-context(.vscode-dark) .action.is-primary { - border: 1px solid var(--color-background--lighten-15); - } - - :host-context(.vscode-high-contrast-light) .action.is-primary, - :host-context(.vscode-light) .action.is-primary { - border: 1px solid var(--color-background--darken-15); - } - - .action.is-icon { - display: inline-flex; - justify-content: center; - align-items: center; - width: 2.2rem; - height: 2.2rem; - padding: 0; - } - .action:hover { - text-decoration: none; - color: var(--color-foreground); - } - - :host-context(.vscode-high-contrast) .action:hover, - :host-context(.vscode-dark) .action:hover { - background-color: var(--color-background--lighten-10); - } - - :host-context(.vscode-high-contrast-light) .action:hover, - :host-context(.vscode-light) .action:hover { - background-color: var(--color-background--darken-10); - } - - pop-over .action { - margin-right: -0.2rem; - } - - .link-inline { - color: inherit; - text-decoration: underline; - } - .link-inline:hover { - color: var(--vscode-textLink-foreground); - } -`; - -@customElement({ name: 'header-card', template: template, styles: styles }) -export class HeaderCard extends FASTElement { - @attr - image = ''; - - @attr - name = ''; - - @attr({ converter: numberConverter }) - days = 0; - - @attr({ converter: numberConverter }) - steps = 4; - - @attr({ converter: numberConverter }) - completed = 0; - - @attr({ converter: numberConverter }) - state: SubscriptionState = SubscriptionState.Free; - - @attr - plan = ''; - - @attr({ attribute: 'pin-status', mode: 'boolean' }) - pinStatus = true; - - progressNode!: HTMLElement; - statusNode!: HTMLElement; - - override attributeChangedCallback(name: string, oldValue: string, newValue: string): void { - super.attributeChangedCallback(name, oldValue, newValue); - - if (oldValue === newValue || this.progressNode == null) { - return; - } - this.updateProgressWidth(); - } - - get daysRemaining() { - if (this.days < 1) { - return '<1 day'; - } - return pluralize('day', this.days); - } - - get progressNow() { - return this.completed + 1; - } - - get progressMax() { - return this.steps + 1; - } - - @volatile - get progress() { - return `${(this.progressNow / this.progressMax) * 100}%`; - } - - @volatile - get planName() { - switch (this.state) { - case SubscriptionState.Free: - case SubscriptionState.FreePreviewTrialExpired: - case SubscriptionState.FreePlusTrialExpired: - return 'GitLens Free'; - case SubscriptionState.FreeInPreviewTrial: - case SubscriptionState.FreePlusInTrial: - return 'GitLens Pro (Trial)'; - case SubscriptionState.VerificationRequired: - return `${this.plan} (Unverified)`; - default: - return this.plan; - } - } - - @volatile - get daysLeft() { - switch (this.state) { - case SubscriptionState.FreeInPreviewTrial: - case SubscriptionState.FreePlusInTrial: - return `, ${this.daysRemaining} left`; - default: - return ''; - } - } - - get hasAccount() { - switch (this.state) { - case SubscriptionState.Free: - case SubscriptionState.FreePreviewTrialExpired: - case SubscriptionState.FreeInPreviewTrial: - return false; - } - return true; - } - - get isPro() { - switch (this.state) { - case SubscriptionState.Free: - case SubscriptionState.FreePreviewTrialExpired: - case SubscriptionState.FreePlusTrialExpired: - case SubscriptionState.VerificationRequired: - return false; - } - return true; - } - - updateProgressWidth() { - this.progressNode.style.width = this.progress; - } - - dismissStatus(_e: MouseEvent) { - this.pinStatus = false; - this.$emit('dismiss-status'); - - window.requestAnimationFrame(() => { - this.statusNode?.focus(); - }); - } -} diff --git a/src/webviews/apps/home/components/plus-banner.ts b/src/webviews/apps/home/components/plus-banner.ts deleted file mode 100644 index 0578b83..0000000 --- a/src/webviews/apps/home/components/plus-banner.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { attr, css, customElement, FASTElement, html, observable, volatile, when } from '@microsoft/fast-element'; -import { SubscriptionState } from '../../../../subscription'; -import { pluralize } from '../../../../system/string'; -import { numberConverter } from '../../shared/components/converters/number-converter'; -import '../../shared/components/code-icon'; - -const template = html` - ${when( - x => x.state === SubscriptionState.Free, - html` -

- Powerful, additional features - that enhance your GitLens experience. -

- - ${when( - y => y.hasRepositories, - html` -

- Try GitLens+ features on private repos -

- `, - )} - `, - )} - ${when( - x => x.state === SubscriptionState.Paid, - html` -

Welcome to ${x => x.planName}!

-

- You have access to - GitLens+ features - on any repo. -

- `, - )} - ${when( - x => x.state === SubscriptionState.FreeInPreviewTrial, - html` -

GitLens Pro Trial

-

- You have ${x => x.daysRemaining} left in your 3-day GitLens Pro trial. Don't worry if you need more - time, you can extend your trial for an additional free 7-days of - GitLens+ features on - private repos. -

- ${when( - y => y.hasRepositories, - html` -

- Upgrade to Pro -

- `, - )} - `, - )} - ${when( - x => x.state === SubscriptionState.FreePlusInTrial, - html` -

GitLens Pro Trial

-

- You have ${x => x.daysRemaining} left in your GitLens Pro trial. Once your trial ends, you'll continue - to have access to - GitLens+ features on - local and public repos, while upgrading to GitLens Pro gives you access on private repos. -

- `, - )} - ${when( - x => x.state === SubscriptionState.FreePreviewTrialExpired, - html` -

Extend Your GitLens Pro Trial

-

- Your free 3-day GitLens Pro trial has ended, extend your trial to get an additional free 7-days of - GitLens+ features on private repos. -

- ${when( - y => y.hasRepositories, - html` -

- Extend Pro Trial -

- `, - )} - `, - )} - ${when( - x => x.state === SubscriptionState.FreePlusTrialExpired, - html` -

GitLens Pro Trial Expired

-

- Your GitLens Pro trial has ended, please upgrade to GitLens Pro to continue to use GitLens+ features on - private repos. -

- ${when( - y => y.hasRepositories, - html` -

- Upgrade to Pro -

- `, - )} - `, - )} - ${when( - x => x.state === SubscriptionState.VerificationRequired, - html` -

Please verify your email

-

- Before you can also use GitLens+ features on private repos, please verify your email address. -

-

- Resend Verification Email -

-

- Refresh Verification Status -

- `, - )} - ${when( - x => - [ - SubscriptionState.Free, - SubscriptionState.FreePreviewTrialExpired, - SubscriptionState.FreePlusTrialExpired, - ].includes(x.state) && x.hasRepositories, - html` -

- ${when( - x => x.plus, - html`Hide GitLens+ features`, - )} - ${when( - x => !x.plus, - html`Restore GitLens+ features`, - )} -

- `, - )} - ${when( - x => !x.hasRepositories, - html` -

- To use GitLens+, open a folder containing a git repository or clone from a URL from the Explorer. -

- `, - )} -`; - -const styles = css` - * { - box-sizing: border-box; - } - - :host { - display: block; - text-align: center; - } - - a { - color: var(--vscode-textLink-foreground); - text-decoration: none; - } - a:focus { - outline-color: var(--focus-border); - } - a:hover { - text-decoration: underline; - } - - h3, - p { - margin-top: 0; - } - - h3 a { - color: inherit; - text-decoration: underline; - text-decoration-color: var(--color-foreground--50); - } - - h3 a:hover { - text-decoration-color: inherit; - } - - .mb-1 { - margin-bottom: 0.4rem; - } - .mb-0 { - margin-bottom: 0; - } - - .minimal { - color: var(--color-foreground--50); - font-size: 1rem; - position: relative; - top: -0.2rem; - } -`; - -@customElement({ name: 'plus-banner', template: template, styles: styles }) -export class PlusBanner extends FASTElement { - @attr({ converter: numberConverter }) - days = 0; - - @attr({ converter: numberConverter }) - state: SubscriptionState = SubscriptionState.Free; - - @attr - plan = ''; - - @attr - visibility: 'local' | 'public' | 'mixed' | 'private' = 'public'; - - @attr({ mode: 'boolean' }) - plus = true; - - @observable - hasRepositories = false; - - get daysRemaining() { - if (this.days < 1) { - return 'less than one day'; - } - return pluralize('day', this.days); - } - - get isFree() { - return ['local', 'public'].includes(this.visibility); - } - - @volatile - get planName() { - switch (this.state) { - case SubscriptionState.Free: - case SubscriptionState.FreePreviewTrialExpired: - case SubscriptionState.FreePlusTrialExpired: - return 'GitLens Free'; - case SubscriptionState.FreeInPreviewTrial: - case SubscriptionState.FreePlusInTrial: - return 'GitLens Pro (Trial)'; - case SubscriptionState.VerificationRequired: - return `${this.plan} (Unverified)`; - default: - return this.plan; - } - } - - fireAction(command: string) { - this.$emit('action', command); - } -} diff --git a/src/webviews/apps/home/components/plus-content.ts b/src/webviews/apps/home/components/plus-content.ts deleted file mode 100644 index 811d1a3..0000000 --- a/src/webviews/apps/home/components/plus-content.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { attr, css, customElement, FASTElement, html, volatile, when } from '@microsoft/fast-element'; -import { SubscriptionState } from '../../../../subscription'; -import { pluralize } from '../../../../system/string'; -import { numberConverter } from '../../shared/components/converters/number-converter'; -import '../../shared/components/code-icon'; - -const template = html` -
-
- ${when( - x => x.state === SubscriptionState.Free, - html` -

- GitLens+ features - are free for local and public repos, no account required, while upgrading to GitLens Pro gives you - access on private repos. -

-

All other GitLens features can always be used on any repo.

- `, - )} - ${when( - x => x.state !== SubscriptionState.Free, - html`

All other GitLens features can always be used on any repo

`, - )} -
-`; - -const styles = css` - * { - box-sizing: border-box; - } - - :host { - display: flex; - flex-direction: row; - padding: 0.8rem 1.2rem; - background-color: var(--color-alert-neutralBackground); - border-left: 0.3rem solid var(--color-foreground--50); - color: var(--color-alert-foreground); - } - - a { - color: var(--vscode-textLink-foreground); - text-decoration: none; - } - a:focus { - outline-color: var(--focus-border); - } - a:hover { - text-decoration: underline; - } - - p { - margin-top: 0; - } - - .icon { - display: none; - flex: none; - margin-right: 0.4rem; - } - - .icon code-icon { - font-size: 2.4rem; - margin-top: 0.2rem; - } - - .content { - font-size: 1.2rem; - line-height: 1.2; - text-align: left; - } - - .mb-1 { - margin-bottom: 0.8rem; - } - .mb-0 { - margin-bottom: 0; - } -`; - -@customElement({ name: 'plus-content', template: template, styles: styles }) -export class PlusContent extends FASTElement { - @attr({ converter: numberConverter }) - days = 0; - - @attr({ converter: numberConverter }) - state: SubscriptionState = SubscriptionState.Free; - - @attr - plan = ''; - - @attr - visibility: 'local' | 'public' | 'mixed' | 'private' = 'public'; - - get daysRemaining() { - if (this.days < 1) { - return 'less than one day'; - } - return pluralize('day', this.days); - } - - get isFree() { - return ['local', 'public'].includes(this.visibility); - } - - @volatile - get planName() { - switch (this.state) { - case SubscriptionState.Free: - case SubscriptionState.FreePreviewTrialExpired: - case SubscriptionState.FreePlusTrialExpired: - return 'GitLens Free'; - case SubscriptionState.FreeInPreviewTrial: - case SubscriptionState.FreePlusInTrial: - return 'GitLens Pro (Trial)'; - case SubscriptionState.VerificationRequired: - return `${this.plan} (Unverified)`; - default: - return this.plan; - } - } - - fireAction(command: string) { - this.$emit('action', command); - } -} diff --git a/src/webviews/apps/home/components/stepped-section.ts b/src/webviews/apps/home/components/stepped-section.ts deleted file mode 100644 index 6619480..0000000 --- a/src/webviews/apps/home/components/stepped-section.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { attr, css, customElement, FASTElement, html } from '@microsoft/fast-element'; -import { numberConverter } from '../../shared/components/converters/number-converter'; -import '../../shared/components/code-icon'; - -const template = html``; - -const styles = css` - * { - box-sizing: border-box; - } - - :host { - display: grid; - gap: 0 0.8rem; - grid-template-columns: 16px auto; - grid-auto-flow: column; - margin-bottom: 2.4rem; - } - - .button { - width: 100%; - padding: 0.1rem 0 0 0; - font-size: var(--vscode-editor-font-size); - line-height: 1.6rem; - font-family: inherit; - border: none; - color: inherit; - background: none; - text-align: left; - text-transform: uppercase; - cursor: pointer; - } - - .button:focus { - outline: 1px solid var(--vscode-focusBorder); - outline-offset: 0.2rem; - } - - .checkline { - position: relative; - grid-column: 1; - grid-row: 1 / span 2; - color: var(--vscode-textLink-foreground); - } - - :host(:not(:last-of-type)) .checkline:after { - content: ''; - position: absolute; - border-left: 0.1rem solid currentColor; - width: 0; - top: 1.6rem; - bottom: -2.4rem; - left: 50%; - transform: translateX(-50%); - opacity: 0.3; - } - - .checkbox { - cursor: pointer; - } - .checkbox code-icon { - pointer-events: none; - } - - .heading:hover ~ .checkline .check-icon, - .checkbox:hover .check-icon { - display: none; - } - - .check-hover-icon { - display: none; - } - .heading:hover ~ .checkline .check-hover-icon, - .checkbox:hover .check-hover-icon { - display: unset; - } - - .content { - margin-top: 1rem; - } - - .content.is-hidden { - display: none; - } - - .description { - margin-left: 0.6rem; - text-transform: none; - opacity: 0.5; - } -`; - -@customElement({ name: 'stepped-section', template: template, styles: styles }) -export class SteppedSection extends FASTElement { - @attr({ attribute: 'heading-level', converter: numberConverter }) - headingLevel = 2; - - @attr({ mode: 'boolean' }) - completed = false; - - handleClick(_e: Event) { - this.completed = !this.completed; - this.$emit('complete', this.completed); - } -} diff --git a/src/webviews/apps/home/home.html b/src/webviews/apps/home/home.html index e69d37e..9aed034 100644 --- a/src/webviews/apps/home/home.html +++ b/src/webviews/apps/home/home.html @@ -15,21 +15,21 @@ href="https://help.gitkraken.com/gitlens/gitlens-release-notes-current/" aria-label="What's New" title="What's New" - >What's NewWhat's New HelpHelp FeedbackFeedback
@@ -38,33 +38,33 @@ href="https://github.com/gitkraken/vscode-gitlens/discussions" aria-label="GitHub Discussions" title="GitHub Discussions" - >
-
+
- -
- - Welcome to GitLens 13 -

- GitLens supercharges Git inside VS Code and unlocks the untapped knowledge within each - repository. -

- - Get Started Tutorial Video - - -
- - Features - always free and accessible -

- GitLens is deeply integrated into many areas and aspects of VS Code, especially editors and - views. Learn more in the Feature Walkthrough. -

- -
-
-

- Find many features by opening the - Source Control Side Bar. -

-

- Click on - a layout option to set the location of your GitLens views. -

-
- -
-
- - GitLens+ Features - want even more from GitLens? - -
- -
-
- -
- - Integrations -

GitLens provides issue and pull request auto-linking with many Git hosting services.

-

- Rich integrations with GitHub & GitLab provide more detailed hover information for auto-linked - issues and pull requests, pull requests associated with branches and commits, and avatars. -

-
-
- -
- - Focus View ✨ (preview) - Focus view Screenshot -

- The - Focus View - provides you with a comprehensive list of all your most important work across your connected - GitHub repos. -

-
- - Commit Graph ✨ - Commit Graph illustration -

- The - Commit Graph - helps you easily visualize and keep track of all work in progress. -

-

- Use the rich commit search to find exactly what you're looking for. It's powerful filters allow - you to search by a specific commit, message, author, a changed file or files, or even a specific - code change. -

-
- - Visual File History ✨ - Visual File History illustration -

- The - Visual File History - allows you to quickly see the evolution of a file, including when changes were made, how large - they were, and who made them. -

-

- Use it to quickly find when the most impactful changes were made to a file or who best to talk - to about file changes and more. -

-
- - Worktrees ✨ - Worktrees illustration -

- Worktrees - help you multitask by minimizing the context switching between branches, allowing you to easily - work on different branches of a repository simultaneously. -

-

- Avoid interrupting your work in progress when needing to review a pull request. Simply create a - new worktree and open it in a new VS Code window, all without impacting your other work -

-
-
-
- Close + + Commit Graph ✨ - Restore Home view stateCommit Details view -
+ Focus ✨ + Visual File History ✨ + + +
#{endOfBody} diff --git a/src/webviews/apps/home/home.scss b/src/webviews/apps/home/home.scss index a1692a2..d1a5568 100644 --- a/src/webviews/apps/home/home.scss +++ b/src/webviews/apps/home/home.scss @@ -1,3 +1,5 @@ +@use '../shared/styles/properties'; + :root { --gitlens-z-inline: 1000; --gitlens-z-sticky: 1100; @@ -121,81 +123,6 @@ body { white-space: nowrap; } -.home { - padding: 0; - height: 100%; - display: flex; - flex-direction: column; - gap: 0.4rem; - overflow: hidden; - - &__header { - flex: none; - padding: 0 2rem; - position: relative; - - [aria-hidden='false'] ~ header-card { - display: none; - } - } - &__main { - flex: 1; - overflow: auto; - padding: 2rem 2rem 0.4rem; - - background: linear-gradient(var(--color-view-background) 33%, var(--color-view-background)), - linear-gradient(var(--color-view-background), var(--color-view-background) 66%) 0 100%, - linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)), - linear-gradient(to top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 0 100%; - background-color: var(--color-view-background); - background-repeat: no-repeat; - background-attachment: local, local, scroll, scroll; - background-size: 100% 12px, 100% 12px, 100% 6px, 100% 6px; - } - &__nav { - flex: none; - padding: 0 2rem; - margin-bottom: 0.6rem; - } -} - -.popover { - background-color: var(--color-background--lighten-15); - position: absolute; - top: 100%; - left: 5.2rem; - transform: translateY(0.8rem); - max-width: 30rem; - padding: 0.8rem 1.2rem 1.2rem; - z-index: 10; - - display: flex; - flex-direction: column; - gap: 0.4rem; - - &__top { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - opacity: 0.5; - } - - &__heading { - font-weight: 600; - } - - &__caret { - position: absolute; - bottom: 100%; - width: 0; - height: 0; - border-left: 0.8rem solid transparent; - border-right: 0.8rem solid transparent; - border-bottom: 0.8rem solid var(--color-background--lighten-15); - } -} - h3 { border: none; color: var(--color-view-header-foreground); @@ -236,38 +163,41 @@ ul { padding-left: 1.2em; } -.unlist { - list-style: none; - padding-left: 0; -} - -.icon-list { - list-style: none; - padding-left: 0; +.home { + padding: 0; + height: 100%; + display: flex; + flex-direction: column; + gap: 0.4rem; + overflow: hidden; - li { + &__header { + flex: none; + padding: 0 2rem; position: relative; - padding-left: 2.2rem; - - > code-icon:first-child { - position: absolute; - left: 0; - top: 0.1rem; - font-size: 1.6rem; - color: var(--color-foreground--50); + + [aria-hidden='false'] ~ header-card { + display: none; } } -} - -.button-container { - display: flex; - flex-direction: column; - margin-bottom: 1rem; -} + &__main { + flex: 1; + overflow: auto; + padding: 0.8rem 2rem 0.4rem; -.button-link { - code-icon { - margin-right: 0.4rem; + background: linear-gradient(var(--color-view-background) 33%, var(--color-view-background)), + linear-gradient(var(--color-view-background), var(--color-view-background) 66%) 0 100%, + linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)), + linear-gradient(to top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)) 0 100%; + background-color: var(--color-view-background); + background-repeat: no-repeat; + background-attachment: local, local, scroll, scroll; + background-size: 100% 12px, 100% 12px, 100% 6px, 100% 6px; + } + &__nav { + flex: none; + padding: 0 2rem; + margin-bottom: 0.6rem; } } @@ -303,7 +233,7 @@ ul { color: inherit; border-radius: 0.3rem; - .codicon { + .code-icon { line-height: 1.6rem; } @@ -344,27 +274,6 @@ ul { } } -.gl-plus-banner { - background-color: transparent; - background-position: left -30vw center; - background-size: 80vw; -} - -.plus-banner-text { - text-shadow: 0.1rem 0.1rem 0 var(--color-background), 0.1rem 0.1rem 0.2rem var(--color-background); -} - -.logo { - font-size: 1.8rem; - color: var(--gitlens-brand-color-2); - font-weight: 500; -} - -.description { - color: #b68cd8; - opacity: 0.6; -} - .alert { padding: 0.8rem 1.2rem; line-height: 1.2; @@ -399,250 +308,84 @@ ul { } } -.activitybar-banner { - display: flex; - flex-direction: row-reverse; - justify-content: flex-end; - align-items: stretch; - gap: 1.6rem; - - @media (max-width: 280px) { - flex-direction: column; - align-items: center; - } - - ul { - display: flex; - flex-direction: column; - justify-content: center; - gap: clamp(0.1rem, 2vw, 1.2rem); - margin-bottom: 0; - } - - &__content { - // padding-top: 1.6rem; - display: flex; - flex-direction: column; - justify-content: center; - - > :last-child { - margin-bottom: 0; - } - } - - &__media { - position: relative; - flex: none; - width: 9.2rem; - } - - &__nav { - position: absolute; - top: 0; - left: 0.4rem; - width: 4.8rem; - height: 12.3rem; - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-start; - gap: 1.6rem; - - &-item { - position: absolute; - left: 0.5rem; - width: 4.6rem; - height: 3.2rem; - // background-color: #ff000066; - - &:first-of-type { - top: 2.2rem; - } - - &:last-of-type { - top: 7rem; - } - } - } +gk-button { + max-width: 300px; + width: 100%; - #no-repo[aria-hidden='false'] ~ & { - display: none; + & + & { + margin-top: 1rem; } } -#no-repo { +.mb-0 { margin-bottom: 0; +} - &[aria-hidden='true'] { +@media (max-width: 280px) { + .not-small { display: none; } } - -.video-banner { - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-end; - margin-bottom: 0.8rem; - background: no-repeat var(--video-banner-play) 1.2rem center, no-repeat var(--video-banner-bg) left center; - background-color: var(--card-background); - background-size: clamp(2.9rem, 8%, 6rem), cover; - background-blend-mode: normal, overlay; - aspect-ratio: var(--video-banner-ratio, 354 / 54); - padding: 0.4rem 1.2rem; - color: inherit; - line-height: 1.2; - font-size: clamp(var(--vscode-font-size), 4vw, 2.4rem); - transition: aspect-ratio linear 100ms, background-color linear 100ms, background-position linear 200ms; - border-radius: 0.4rem; - - @media (min-width: 277px) { - background-blend-mode: normal, normal; - background-position: center center, left center; - } - - @media (min-width: 564px) { - aspect-ratio: var(--video-banner-ratio, 354 / 40); - } - - &:hover { - background-color: var(--card-hover-background); - text-decoration: none; - color: inherit; - } - - small { - color: #8d778d; +@media (min-width: 281px) { + .only-small { + display: none; } } -.link-minimal { - color: var(--color-foreground--50); - font-size: 1rem; - text-align: center; - position: relative; - top: 0.6rem; - - &:hover { +.t { + &-eyebrow { + text-transform: uppercase; + font-size: 1rem; + font-weight: 600; color: var(--color-foreground--50); + margin: 0; } } -vscode-button { - max-width: 300px; - width: 100%; - - & + & { - margin-top: 1rem; - } -} - -.link-minimal, -vscode-button { - align-self: center; - - @media (min-width: 640px) { - align-self: flex-start; +.nav-list { + margin: { + left: -2rem; + right: -2rem; } -} - -@import '../shared/codicons'; - -// .codicon { -// position: relative; -// top: -2px; -// } - -.type-tight { - line-height: 1.2; -} - -.mb-1 { - margin-bottom: 0.4rem; -} -.mb-0 { - margin-bottom: 0; -} - -.hide { - display: none; -} - -.svg { - width: 100%; - height: auto; - - &__outline { - transition: all ease 250ms; - - .vscode-light &, - .vscode-high-contrast-light & { - stop-color: var(--color-background--darken-15); - } + display: flex; + flex-direction: column; + gap: 0.1rem; + align-items: stretch; + margin-bottom: 1.6rem; - .vscode-dark &, - .vscode-high-contrast & { - stop-color: var(--color-background--lighten-15); - } - } + &__item { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.8rem; + padding: 0.4rem 2rem; + color: inherit; - &:hover &__outline, - .activitybar-banner__nav-item:focus ~ & &__outline, - .activitybar-banner__nav-item:hover ~ & &__outline { - .vscode-light &, - .vscode-high-contrast-light & { - stop-color: var(--color-background--darken-50); + &:hover { + background-color: var(--vscode-list-hoverBackground); + color: var(--vscode-list-hoverForeground); + text-decoration: none; } - .vscode-dark &, - .vscode-high-contrast & { - stop-color: var(--color-background--lighten-50); + &:active { + background-color: var(--vscode-list-activeSelectionBackground); + color: var(--vscode-list-activeSelectionForeground); } } - &__bar { - fill: var(--vscode-activityBar-background); - } - - &__indicator { - fill: transparent; - &.is-active { - fill: var(--vscode-activityBar-activeBorder); - } - } &__icon { - transition: all ease 100ms; - fill: var(--vscode-activityBar-inactiveForeground); - &.is-active { - fill: var(--vscode-activityBar-foreground); - } - } - &__arrow { - fill: transparent; - &.is-active { - fill: var(--vscode-textLink-foreground); - } + opacity: 0.5; } - .activitybar-banner__nav-item:first-of-type:focus ~ & &__icon:last-of-type, - .activitybar-banner__nav-item:first-of-type:hover ~ & &__icon:last-of-type, - .activitybar-banner__nav-item:last-of-type:focus ~ & &__icon:first-of-type, - .activitybar-banner__nav-item:last-of-type:hover ~ & &__icon:first-of-type { - fill: var(--vscode-activityBar-foreground); + &__label { + font-weight: 600; } -} -.plus-section-thumb { - border-radius: 0.6rem; -} - -@media (max-width: 280px) { - .not-small { - display: none; + &__item:hover &__label { + text-decoration: underline; } -} -@media (min-width: 281px) { - .only-small { - display: none; + + &__title { + padding: 0 2rem; } } diff --git a/src/webviews/apps/home/home.ts b/src/webviews/apps/home/home.ts index f3ddae3..94316b8 100644 --- a/src/webviews/apps/home/home.ts +++ b/src/webviews/apps/home/home.ts @@ -1,49 +1,21 @@ /*global*/ import './home.scss'; -import { provideVSCodeDesignSystem, vsCodeButton } from '@vscode/webview-ui-toolkit'; import type { Disposable } from 'vscode'; -import { getSubscriptionTimeRemaining, SubscriptionState } from '../../../subscription'; import type { State } from '../../home/protocol'; -import { - CompleteStepCommandType, - DidChangeConfigurationType, - DidChangeLayoutType, - DidChangeRepositoriesType, - DidChangeSubscriptionNotificationType, - DismissBannerCommandType, - DismissSectionCommandType, - DismissStatusCommandType, -} from '../../home/protocol'; +import { DidChangeRepositoriesType } from '../../home/protocol'; import type { IpcMessage } from '../../protocol'; import { ExecuteCommandType, onIpc } from '../../protocol'; import { App } from '../shared/appBase'; import { DOM } from '../shared/dom'; -import type { CardSection } from './components/card-section'; -import type { HeaderCard } from './components/header-card'; -import type { PlusBanner } from './components/plus-banner'; -import type { SteppedSection } from './components/stepped-section'; +import '../shared/components/button'; import '../shared/components/code-icon'; -import '../shared/components/overlays/pop-over'; -import './components/card-section'; -import './components/header-card'; -import './components/plus-banner'; -import './components/plus-content'; -import './components/stepped-section'; export class HomeApp extends App { - private $steps!: SteppedSection[]; - private $cards!: CardSection[]; - constructor() { super('HomeApp'); } protected override onInitialize() { - provideVSCodeDesignSystem().register(vsCodeButton()); - - this.$steps = [...document.querySelectorAll('stepped-section[id]')]; - this.$cards = [...document.querySelectorAll('card-section[id]')]; - this.state = this.getState() ?? this.state; this.updateState(); } @@ -54,29 +26,6 @@ export class HomeApp extends App { disposables.push( DOM.on('[data-action]', 'click', (e, target: HTMLElement) => this.onDataActionClicked(e, target)), ); - disposables.push( - DOM.on('plus-banner', 'action', (e, target: HTMLElement) => - this.onPlusActionClicked(e, target), - ), - ); - disposables.push( - DOM.on('stepped-section', 'complete', (e, target: HTMLElement) => - this.onStepComplete(e, target), - ), - ); - disposables.push( - DOM.on('card-section', 'dismiss', (e, target: HTMLElement) => - this.onCardDismissed(e, target), - ), - ); - disposables.push( - DOM.on('header-card', 'dismiss-status', (e, target: HTMLElement) => - this.onStatusDismissed(e, target), - ), - ); - disposables.push( - DOM.on('[data-banner-dismiss]', 'click', (e, target: HTMLElement) => this.onBannerDismissed(e, target)), - ); return disposables; } @@ -85,18 +34,6 @@ export class HomeApp extends App { const msg = e.data as IpcMessage; switch (msg.method) { - case DidChangeSubscriptionNotificationType.method: - this.log(`onMessageReceived(${msg.id}): name=${msg.method}`); - - onIpc(DidChangeSubscriptionNotificationType, msg, params => { - this.state.subscription = params.subscription; - this.state.completedActions = params.completedActions; - this.state.avatar = params.avatar; - this.state.pinStatus = params.pinStatus; - this.setState(this.state); - this.updateState(); - }); - break; case DidChangeRepositoriesType.method: this.log(`onMessageReceived(${msg.id}): name=${msg.method}`); @@ -106,154 +43,32 @@ export class HomeApp extends App { this.updateNoRepo(); }); break; - case DidChangeConfigurationType.method: - this.log(`onMessageReceived(${msg.id}): name=${msg.method}`); - - onIpc(DidChangeConfigurationType, msg, params => { - this.state.plusEnabled = params.plusEnabled; - this.setState(this.state); - this.updatePlusContent(); - }); - break; - case DidChangeLayoutType.method: - this.log(`onMessageReceived(${msg.id}): name=${msg.method}`); - - onIpc(DidChangeLayoutType, msg, params => { - this.state.layout = params.layout; - this.setState(this.state); - this.updateLayout(); - }); - break; - default: super.onMessageReceived?.(e); break; } } - private onStepComplete(e: CustomEvent, target: HTMLElement) { - const id = target.id; - const isComplete = e.detail ?? false; - this.state.completedSteps = toggleArrayItem(this.state.completedSteps, id, isComplete); - this.sendCommand(CompleteStepCommandType, { id: id, completed: isComplete }); - this.updateState(); - } - - private onCardDismissed(e: CustomEvent, target: HTMLElement) { - const id = target.id; - this.state.dismissedSections = toggleArrayItem(this.state.dismissedSections, id); - this.sendCommand(DismissSectionCommandType, { id: id }); - this.updateState(); - } - - private onStatusDismissed(_e: CustomEvent, _target: HTMLElement) { - this.state.pinStatus = false; - this.sendCommand(DismissStatusCommandType, undefined); - this.updateHeader(); - } - - private onBannerDismissed(_e: MouseEvent, target: HTMLElement) { - const key = target.getAttribute('data-banner-dismiss'); - if (key == null || this.state.dismissedBanners?.includes(key)) { - return; - } - this.state.dismissedBanners = this.state.dismissedBanners ?? []; - this.state.dismissedBanners.push(key); - this.sendCommand(DismissBannerCommandType, { id: key }); - this.updateBanners(); - } - private onDataActionClicked(_e: MouseEvent, target: HTMLElement) { const action = target.dataset.action; this.onActionClickedCore(action); } - private onPlusActionClicked(e: CustomEvent, _target: HTMLElement) { - this.onActionClickedCore(e.detail); - } - private onActionClickedCore(action?: string) { if (action?.startsWith('command:')) { this.sendCommand(ExecuteCommandType, { command: action.slice(8) }); } } - private getDaysRemaining() { - if ( - ![SubscriptionState.FreeInPreviewTrial, SubscriptionState.FreePlusInTrial].includes( - this.state.subscription.state, - ) - ) { - return 0; - } - - return getSubscriptionTimeRemaining(this.state.subscription, 'days') ?? 0; - } - - private forceShowPlus() { - return [ - SubscriptionState.FreePreviewTrialExpired, - SubscriptionState.FreePlusTrialExpired, - SubscriptionState.VerificationRequired, - ].includes(this.state.subscription.state); - } - - private updateHeader(days = this.getDaysRemaining(), forceShowPlus = this.forceShowPlus()) { - const { subscription, completedSteps, avatar, pinStatus } = this.state; - - const $headerContent = document.getElementById('header-card') as HeaderCard; - if ($headerContent) { - if (avatar) { - $headerContent.setAttribute('image', avatar); - } - $headerContent.setAttribute('name', subscription.account?.name ?? ''); - - const steps = this.$steps?.length ?? 0; - let completed = completedSteps?.length ?? 0; - if (steps > 0 && completed > 0) { - const stepIds = this.$steps.map(el => el.id); - const availableCompleted = completedSteps!.filter(name => stepIds.includes(name)); - completed = availableCompleted.length; - - if (forceShowPlus && availableCompleted.includes('plus')) { - completed -= 1; - } - } - - $headerContent.setAttribute('steps', steps.toString()); - $headerContent.setAttribute('completed', completed.toString()); - $headerContent.setAttribute('state', subscription.state.toString()); - $headerContent.setAttribute('plan', subscription.plan.effective.name); - $headerContent.setAttribute('days', days.toString()); - $headerContent.pinStatus = pinStatus; - } - } - - private updateBanners() { - const $banners = [...document.querySelectorAll('[data-banner]')]; - if (!$banners.length) { - return; - } - - const { dismissedBanners } = this.state; - $banners.forEach($el => { - const key = $el.getAttribute('data-banner'); - if (key !== null && dismissedBanners?.includes(key)) { - $el.setAttribute('hidden', 'true'); - } else { - $el.removeAttribute('hidden'); - } - }); - } - private updateNoRepo() { const { repositories: { openCount, hasUnsafe, trusted }, } = this.state; + const header = document.getElementById('header')!; if (!trusted) { + header.hidden = false; setElementVisibility('untrusted-alert', true); - setElementVisibility('no-repo', false); setElementVisibility('no-repo-alert', false); setElementVisibility('unsafe-repo-alert', false); @@ -263,101 +78,13 @@ export class HomeApp extends App { setElementVisibility('untrusted-alert', false); const noRepos = openCount === 0; - - setElementVisibility('no-repo', noRepos); setElementVisibility('no-repo-alert', noRepos && !hasUnsafe); setElementVisibility('unsafe-repo-alert', hasUnsafe); - } - - private updateLayout() { - const { layout } = this.state; - - const $els = [...document.querySelectorAll('[data-gitlens-layout]')]; - $els.forEach(el => { - const attr = el.getAttribute('data-gitlens-layout'); - el.classList.toggle('is-active', attr === layout); - }); - } - - private updatePlusContent(days = this.getDaysRemaining()) { - const { subscription, visibility, plusEnabled } = this.state; - - let $plusContent = document.getElementById('plus-banner'); - if ($plusContent) { - $plusContent.setAttribute('days', days.toString()); - $plusContent.setAttribute('state', subscription.state.toString()); - $plusContent.setAttribute('visibility', visibility); - $plusContent.setAttribute('plan', subscription.plan.effective.name); - $plusContent.setAttribute('plus', plusEnabled.toString()); - ($plusContent as PlusBanner).hasRepositories = this.state.repositories.count > 0; - } - - $plusContent = document.getElementById('plus-content'); - if ($plusContent) { - $plusContent.setAttribute('days', days.toString()); - $plusContent.setAttribute('state', subscription.state.toString()); - $plusContent.setAttribute('visibility', visibility); - $plusContent.setAttribute('plan', subscription.plan.effective.name); - } - } - - private updateSteps(forceShowPlus = this.forceShowPlus()) { - if ( - this.$steps == null || - this.$steps.length === 0 || - this.state.completedSteps == null || - this.state.completedSteps.length === 0 - ) { - return; - } - - this.$steps.forEach(el => { - el.setAttribute( - 'completed', - (el.id === 'plus' && forceShowPlus) || this.state.completedSteps?.includes(el.id) !== true - ? 'false' - : 'true', - ); - }); - } - - private updateSections() { - if ( - this.$cards == null || - this.$cards.length === 0 || - this.state.dismissedSections == null || - this.state.dismissedSections.length === 0 - ) { - return; - } - - this.state.dismissedSections.forEach(id => { - const found = this.$cards.findIndex(el => el.id === id); - if (found > -1) { - this.$cards[found].remove(); - this.$cards.splice(found, 1); - } - }); + header.hidden = !noRepos && !hasUnsafe; } private updateState() { - const { completedSteps, dismissedSections } = this.state; - this.updateNoRepo(); - this.updateLayout(); - - const showRestoreWelcome = completedSteps?.length || dismissedSections?.length; - document.getElementById('restore-welcome')?.classList.toggle('hide', !showRestoreWelcome); - - const forceShowPlus = this.forceShowPlus(); - const days = this.getDaysRemaining(); - this.updateHeader(days, forceShowPlus); - this.updatePlusContent(days); - - this.updateSteps(forceShowPlus); - - this.updateSections(); - this.updateBanners(); } } @@ -379,15 +106,4 @@ function setElementVisibility(elementOrId: string | HTMLElement | null | undefin } } -function toggleArrayItem(list: string[] = [], item: string, add = true) { - const hasStep = list.includes(item); - if (!hasStep && add) { - list.push(item); - } else if (hasStep && !add) { - list.splice(list.indexOf(item), 1); - } - - return list; -} - new HomeApp(); diff --git a/src/webviews/apps/plus/account/account.html b/src/webviews/apps/plus/account/account.html new file mode 100644 index 0000000..7a52944 --- /dev/null +++ b/src/webviews/apps/plus/account/account.html @@ -0,0 +1,21 @@ + + + + + + + + + + #{endOfBody} + + + diff --git a/src/webviews/apps/plus/account/account.scss b/src/webviews/apps/plus/account/account.scss new file mode 100644 index 0000000..37c134b --- /dev/null +++ b/src/webviews/apps/plus/account/account.scss @@ -0,0 +1,121 @@ +:root { + --gitlens-z-inline: 1000; + --gitlens-z-sticky: 1100; + --gitlens-z-popover: 1200; + --gitlens-z-cover: 1300; + --gitlens-z-dialog: 1400; + --gitlens-z-modal: 1500; + --gitlens-brand-color: #914db3; + --gitlens-brand-color-2: #a16dc4; +} + +.vscode-high-contrast, +.vscode-dark { + --progress-bar-color: var(--color-background--lighten-15); + --card-background: var(--color-background--lighten-075); + --card-hover-background: var(--color-background--lighten-10); + --popover-bg: var(--color-background--lighten-15); +} + +.vscode-high-contrast-light, +.vscode-light { + --progress-bar-color: var(--color-background--darken-15); + --card-background: var(--color-background--darken-075); + --card-hover-background: var(--color-background--darken-10); + --popover-bg: var(--color-background--darken-15); +} + +* { + box-sizing: border-box; +} + +// avoids FOUC for elements not yet called with `define()` +:not(:defined) { + visibility: hidden; +} + +[hidden] { + display: none !important; +} + +html { + height: 100%; + font-size: 62.5%; + text-size-adjust: 100%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + background-color: var(--color-view-background); + color: var(--color-view-foreground); + font-family: var(--font-family); + min-height: 100%; + line-height: 1.4; + font-size: var(--vscode-font-size); + + &.scrollable, + .scrollable { + border-color: transparent; + transition: border-color 1s linear; + } + + &:hover, + &:focus-within { + &.scrollable, + .scrollable { + border-color: var(--vscode-scrollbarSlider-background); + transition: none; + } + } + + &.preload { + &.scrollable, + .scrollable { + transition: none; + } + } +} + +::-webkit-scrollbar-corner { + background-color: transparent !important; +} + +::-webkit-scrollbar-thumb { + background-color: transparent; + border-color: inherit; + border-right-style: inset; + border-right-width: calc(100vw + 100vh); + border-radius: unset !important; + + &:hover { + border-color: var(--vscode-scrollbarSlider-hoverBackground); + } + + &:active { + border-color: var(--vscode-scrollbarSlider-activeBackground); + } +} + +:focus { + outline-color: var(--vscode-focusBorder); +} + +.account { + padding: 0; + height: 100%; + display: flex; + flex-direction: column; + gap: 0.4rem; + overflow: hidden; + + &__header { + flex: none; + padding: 0 2rem; + position: relative; + + [aria-hidden='false'] ~ header-card { + display: none; + } + } +} diff --git a/src/webviews/apps/plus/account/account.ts b/src/webviews/apps/plus/account/account.ts new file mode 100644 index 0000000..ffadcab --- /dev/null +++ b/src/webviews/apps/plus/account/account.ts @@ -0,0 +1,105 @@ +/*global*/ +import './account.scss'; +import type { Disposable } from 'vscode'; +import type { State } from '../../../../plus/webviews/account/protocol'; +import { DidChangeSubscriptionNotificationType } from '../../../../plus/webviews/account/protocol'; +import { getSubscriptionTimeRemaining, SubscriptionState } from '../../../../subscription'; +import type { IpcMessage } from '../../../protocol'; +import { ExecuteCommandType, onIpc } from '../../../protocol'; +import { App } from '../../shared/appBase'; +import { DOM } from '../../shared/dom'; +import type { HeaderCard } from './components/header-card'; +import '../../shared/components/code-icon'; +import './components/header-card'; + +export class AccountApp extends App { + constructor() { + super('AccountApp'); + } + + protected override onInitialize() { + this.state = this.getState() ?? this.state; + this.updateState(); + } + + protected override onBind(): Disposable[] { + const disposables = super.onBind?.() ?? []; + + disposables.push( + DOM.on('[data-action]', 'click', (e, target: HTMLElement) => this.onDataActionClicked(e, target)), + ); + + return disposables; + } + + protected override onMessageReceived(e: MessageEvent) { + const msg = e.data as IpcMessage; + + switch (msg.method) { + case DidChangeSubscriptionNotificationType.method: + this.log(`onMessageReceived(${msg.id}): name=${msg.method}`); + + onIpc(DidChangeSubscriptionNotificationType, msg, params => { + this.state.subscription = params.subscription; + this.state.avatar = params.avatar; + this.setState(this.state); + this.updateState(); + }); + break; + + default: + super.onMessageReceived?.(e); + break; + } + } + + private onDataActionClicked(_e: MouseEvent, target: HTMLElement) { + const action = target.dataset.action; + this.onActionClickedCore(action); + } + + private onActionClickedCore(action?: string) { + if (action?.startsWith('command:')) { + this.sendCommand(ExecuteCommandType, { command: action.slice(8) }); + } + } + + private getDaysRemaining() { + if ( + ![SubscriptionState.FreeInPreviewTrial, SubscriptionState.FreePlusInTrial].includes( + this.state.subscription.state, + ) + ) { + return 0; + } + + return getSubscriptionTimeRemaining(this.state.subscription, 'days') ?? 0; + } + + private updateHeader(days = this.getDaysRemaining()) { + const { subscription, avatar } = this.state; + + const $headerContent = document.getElementById('header-card')! as HeaderCard; + if (avatar) { + $headerContent.setAttribute('image', avatar); + } + $headerContent.setAttribute('name', subscription.account?.name ?? ''); + + // TODO: remove + const steps = 0; + const completed = 0; + + $headerContent.setAttribute('steps', steps.toString()); + $headerContent.setAttribute('completed', completed.toString()); + $headerContent.setAttribute('state', subscription.state.toString()); + $headerContent.setAttribute('plan', subscription.plan.effective.name); + $headerContent.setAttribute('days', days.toString()); + } + + private updateState() { + const days = this.getDaysRemaining(); + this.updateHeader(days); + } +} + +new AccountApp(); diff --git a/src/webviews/apps/plus/account/components/header-card.ts b/src/webviews/apps/plus/account/components/header-card.ts new file mode 100644 index 0000000..5d04f8b --- /dev/null +++ b/src/webviews/apps/plus/account/components/header-card.ts @@ -0,0 +1,378 @@ +import { attr, css, customElement, FASTElement, html, ref, volatile, when } from '@microsoft/fast-element'; +import { SubscriptionState } from '../../../../../subscription'; +import { pluralize } from '../../../../../system/string'; +import { numberConverter } from '../../../shared/components/converters/number-converter'; +import '../../../shared/components/code-icon'; + +const template = html` +
GitLens Logo
+

+ ${when(x => x.name === '', html`GitLens 13`)} + ${when(x => x.name !== '', html`${x => x.name}`)} +

+ +

+ ${x => + x.isPro + ? 'You have access to all GitLens features on any repo.' + : 'You have access to ✨ features on local & public repos, and all other GitLens features on any repo.'} +

+ ✨ indicates a subscription is required to use this feature on privately hosted repos. + learn more +

+ + + ${when( + x => x.state === SubscriptionState.FreePreviewTrialExpired, + html`Extend Pro Trial`, + )} + ${when( + x => + x.state === SubscriptionState.FreeInPreviewTrial || + x.state === SubscriptionState.FreePlusInTrial || + x.state === SubscriptionState.FreePlusTrialExpired, + html`Upgrade to Pro`, + )} + ${when( + x => x.state === SubscriptionState.VerificationRequired, + html` + Verify Refresh + `, + )} + +`; + +const styles = css` + * { + box-sizing: border-box; + } + + :host { + position: relative; + display: grid; + padding: 1rem 0 1.2rem; + gap: 0 0.8rem; + grid-template-columns: 3.4rem auto; + grid-auto-flow: column; + } + + a { + color: var(--vscode-textLink-foreground); + text-decoration: none; + } + a:focus { + outline-color: var(--focus-border); + } + a:hover { + text-decoration: underline; + } + + .header-card__media { + grid-column: 1; + grid-row: 1 / span 2; + display: flex; + align-items: center; + } + + .header-card__image { + width: 100%; + aspect-ratio: 1 / 1; + border-radius: 50%; + } + + .header-card__title { + font-size: var(--vscode-font-size); + font-weight: 600; + margin: 0; + } + + .header-card__title.logo { + font-family: 'Segoe UI Semibold', var(--font-family); + font-size: 1.5rem; + } + + .header-card__account { + position: relative; + margin: 0; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 0 0.4rem; + } + + .features { + grid-column: 1 / 3; + grid-row: 3; + } + + .progress { + width: 100%; + overflow: hidden; + } + + :host-context(.vscode-high-contrast) .progress, + :host-context(.vscode-dark) .progress { + background-color: var(--color-background--lighten-15); + } + + :host-context(.vscode-high-contrast-light) .progress, + :host-context(.vscode-light) .progress { + background-color: var(--color-background--darken-15); + } + + .progress__indicator { + height: 4px; + background-color: var(--vscode-progressBar-background); + } + + .header-card__progress { + position: absolute; + bottom: 0; + left: 0; + } + + .brand { + color: var(--gitlens-brand-color-2); + } + .status { + color: var(--color-foreground--65); + } + + .repo-access { + font-size: 1.1em; + margin-right: 0.2rem; + } + .repo-access:not(.is-pro) { + filter: grayscale(1) brightness(0.7); + } + + .actions { + position: absolute; + right: 0.1rem; + top: 0.1rem; + } + + .action { + display: inline-block; + padding: 0.2rem 0.6rem; + border-radius: 0.3rem; + color: var(--color-foreground--75); + } + :host-context(.vscode-high-contrast) .action.is-primary, + :host-context(.vscode-dark) .action.is-primary { + border: 1px solid var(--color-background--lighten-15); + } + + :host-context(.vscode-high-contrast-light) .action.is-primary, + :host-context(.vscode-light) .action.is-primary { + border: 1px solid var(--color-background--darken-15); + } + + .action.is-icon { + display: inline-flex; + justify-content: center; + align-items: center; + width: 2.2rem; + height: 2.2rem; + padding: 0; + } + .action:hover { + text-decoration: none; + color: var(--color-foreground); + } + + :host-context(.vscode-high-contrast) .action:hover, + :host-context(.vscode-dark) .action:hover { + background-color: var(--color-background--lighten-10); + } + + :host-context(.vscode-high-contrast-light) .action:hover, + :host-context(.vscode-light) .action:hover { + background-color: var(--color-background--darken-10); + } + + .link-inline { + color: inherit; + text-decoration: underline; + } + .link-inline:hover { + color: var(--vscode-textLink-foreground); + } +`; + +@customElement({ name: 'header-card', template: template, styles: styles }) +export class HeaderCard extends FASTElement { + @attr + image = ''; + + @attr + name = ''; + + @attr({ converter: numberConverter }) + days = 0; + + @attr({ converter: numberConverter }) + steps = 4; + + @attr({ converter: numberConverter }) + completed = 0; + + @attr({ converter: numberConverter }) + state: SubscriptionState = SubscriptionState.Free; + + @attr + plan = ''; + + @attr({ attribute: 'pin-status', mode: 'boolean' }) + pinStatus = true; + + progressNode!: HTMLElement; + statusNode!: HTMLElement; + + override attributeChangedCallback(name: string, oldValue: string, newValue: string): void { + super.attributeChangedCallback(name, oldValue, newValue); + + if (oldValue === newValue || this.progressNode == null) { + return; + } + this.updateProgressWidth(); + } + + get daysRemaining() { + if (this.days < 1) { + return '<1 day'; + } + return pluralize('day', this.days); + } + + get progressNow() { + return this.completed + 1; + } + + get progressMax() { + return this.steps + 1; + } + + @volatile + get progress() { + return `${(this.progressNow / this.progressMax) * 100}%`; + } + + @volatile + get planName() { + switch (this.state) { + case SubscriptionState.Free: + case SubscriptionState.FreePreviewTrialExpired: + case SubscriptionState.FreePlusTrialExpired: + return 'GitLens Free'; + case SubscriptionState.FreeInPreviewTrial: + case SubscriptionState.FreePlusInTrial: + return 'GitLens Pro (Trial)'; + case SubscriptionState.VerificationRequired: + return `${this.plan} (Unverified)`; + default: + return this.plan; + } + } + + @volatile + get daysLeft() { + switch (this.state) { + case SubscriptionState.FreeInPreviewTrial: + case SubscriptionState.FreePlusInTrial: + return `, ${this.daysRemaining} left`; + default: + return ''; + } + } + + get hasAccount() { + switch (this.state) { + case SubscriptionState.Free: + case SubscriptionState.FreePreviewTrialExpired: + case SubscriptionState.FreeInPreviewTrial: + return false; + } + return true; + } + + get isPro() { + switch (this.state) { + case SubscriptionState.Free: + case SubscriptionState.FreePreviewTrialExpired: + case SubscriptionState.FreePlusTrialExpired: + case SubscriptionState.VerificationRequired: + return false; + } + return true; + } + + updateProgressWidth() { + this.progressNode.style.width = this.progress; + } + + dismissStatus(_e: MouseEvent) { + this.pinStatus = false; + this.$emit('dismiss-status'); + + window.requestAnimationFrame(() => { + this.statusNode?.focus(); + }); + } +} diff --git a/src/webviews/apps/plus/account/components/plus-content.ts b/src/webviews/apps/plus/account/components/plus-content.ts new file mode 100644 index 0000000..bcce544 --- /dev/null +++ b/src/webviews/apps/plus/account/components/plus-content.ts @@ -0,0 +1,129 @@ +import { attr, css, customElement, FASTElement, html, volatile, when } from '@microsoft/fast-element'; +import { SubscriptionState } from '../../../../../subscription'; +import { pluralize } from '../../../../../system/string'; +import { numberConverter } from '../../../shared/components/converters/number-converter'; +import '../../../shared/components/code-icon'; + +const template = html` +
+
+ ${when( + x => x.state === SubscriptionState.Free, + html` +

+ GitLens+ features + are free for local and public repos, no account required, while upgrading to GitLens Pro gives you + access on private repos. +

+

All other GitLens features can always be used on any repo.

+ `, + )} + ${when( + x => x.state !== SubscriptionState.Free, + html`

All other GitLens features can always be used on any repo

`, + )} +
+`; + +const styles = css` + * { + box-sizing: border-box; + } + + :host { + display: flex; + flex-direction: row; + padding: 0.8rem 1.2rem; + background-color: var(--color-alert-neutralBackground); + border-left: 0.3rem solid var(--color-foreground--50); + color: var(--color-alert-foreground); + } + + a { + color: var(--vscode-textLink-foreground); + text-decoration: none; + } + a:focus { + outline-color: var(--focus-border); + } + a:hover { + text-decoration: underline; + } + + p { + margin-top: 0; + } + + .icon { + display: none; + flex: none; + margin-right: 0.4rem; + } + + .icon code-icon { + font-size: 2.4rem; + margin-top: 0.2rem; + } + + .content { + font-size: 1.2rem; + line-height: 1.2; + text-align: left; + } + + .mb-1 { + margin-bottom: 0.8rem; + } + .mb-0 { + margin-bottom: 0; + } +`; + +@customElement({ name: 'plus-content', template: template, styles: styles }) +export class PlusContent extends FASTElement { + @attr({ converter: numberConverter }) + days = 0; + + @attr({ converter: numberConverter }) + state: SubscriptionState = SubscriptionState.Free; + + @attr + plan = ''; + + @attr + visibility: 'local' | 'public' | 'mixed' | 'private' = 'public'; + + get daysRemaining() { + if (this.days < 1) { + return 'less than one day'; + } + return pluralize('day', this.days); + } + + get isFree() { + return ['local', 'public'].includes(this.visibility); + } + + @volatile + get planName() { + switch (this.state) { + case SubscriptionState.Free: + case SubscriptionState.FreePreviewTrialExpired: + case SubscriptionState.FreePlusTrialExpired: + return 'GitLens Free'; + case SubscriptionState.FreeInPreviewTrial: + case SubscriptionState.FreePlusInTrial: + return 'GitLens Pro (Trial)'; + case SubscriptionState.VerificationRequired: + return `${this.plan} (Unverified)`; + default: + return this.plan; + } + } + + fireAction(command: string) { + this.$emit('action', command); + } +} diff --git a/src/webviews/apps/shared/components/account/account-badge.ts b/src/webviews/apps/shared/components/account/account-badge.ts index 320802e..026ef5e 100644 --- a/src/webviews/apps/shared/components/account/account-badge.ts +++ b/src/webviews/apps/shared/components/account/account-badge.ts @@ -15,7 +15,7 @@ const template = html` ${x => x.popoverText}

- ✨ indicates GitLens+ features + ✨ indicates a subscription is required to use this feature on privately hosted repos.
`; @@ -138,7 +138,7 @@ export class AccountBadge extends FASTElement { @volatile get popoverText() { return this.isPro - ? 'You have access to all GitLens and GitLens+ features on any repo.' - : 'You have access to GitLens+ features on local & public repos, and all other GitLens features on any repo.'; + ? 'You have access to all GitLens features on any repo.' + : 'You have access to ✨ features on local & public repos, and all other GitLens features on any repo.'; } } diff --git a/src/webviews/home/homeWebview.ts b/src/webviews/home/homeWebview.ts index 01b87b3..5425d36 100644 --- a/src/webviews/home/homeWebview.ts +++ b/src/webviews/home/homeWebview.ts @@ -1,38 +1,9 @@ -import type { ConfigurationChangeEvent } from 'vscode'; -import { Disposable, window, workspace } from 'vscode'; -import { getAvatarUriFromGravatarEmail } from '../../avatars'; -import { ViewsLayout } from '../../commands/setViewsLayout'; +import { Disposable, workspace } from 'vscode'; import type { Container } from '../../container'; -import type { RepositoriesVisibility } from '../../git/gitProviderService'; -import type { SubscriptionChangeEvent } from '../../plus/subscription/subscriptionService'; -import type { Subscription } from '../../subscription'; -import { executeCoreCommand, registerCommand } from '../../system/command'; -import { configuration } from '../../system/configuration'; -import type { Deferrable } from '../../system/function'; -import { debounce } from '../../system/function'; -import { getSettledValue } from '../../system/promise'; -import type { StorageChangeEvent } from '../../system/storage'; -import type { IpcMessage } from '../protocol'; -import { onIpc } from '../protocol'; +import { registerCommand } from '../../system/command'; import type { WebviewController, WebviewProvider } from '../webviewController'; -import type { - CompleteStepParams, - DidChangeRepositoriesParams, - DismissBannerParams, - DismissSectionParams, - State, -} from './protocol'; -import { - CompletedActions, - CompleteStepCommandType, - DidChangeConfigurationType, - DidChangeLayoutType, - DidChangeRepositoriesType, - DidChangeSubscriptionNotificationType, - DismissBannerCommandType, - DismissSectionCommandType, - DismissStatusCommandType, -} from './protocol'; +import type { DidChangeRepositoriesParams, State } from './protocol'; +import { DidChangeRepositoriesType } from './protocol'; const emptyDisposable = Object.freeze({ dispose: () => { @@ -45,10 +16,7 @@ export class HomeWebviewProvider implements WebviewProvider { constructor(private readonly container: Container, private readonly host: WebviewController) { this._disposable = Disposable.from( - this.container.subscription.onDidChange(this.onSubscriptionChanged, this), this.container.git.onDidChangeRepositories(this.onRepositoriesChanged, this), - configuration.onDidChange(this.onConfigurationChanged, this), - this.container.storage.onDidChange(this.onStorageChanged, this), !workspace.isTrusted ? workspace.onDidGrantWorkspaceTrust(this.notifyDidChangeRepositories, this) : emptyDisposable, @@ -59,220 +27,23 @@ export class HomeWebviewProvider implements WebviewProvider { this._disposable.dispose(); } - private onConfigurationChanged(e: ConfigurationChangeEvent) { - if (!configuration.changed(e, 'plusFeatures.enabled')) { - return; - } - - this.notifyDidChangeConfiguration(); - } - private onRepositoriesChanged() { this.notifyDidChangeRepositories(); } - private onStorageChanged(e: StorageChangeEvent) { - if (e.key !== 'views:layout') return; - - this.notifyDidChangeLayout(); - } - - private async onSubscriptionChanged(e: SubscriptionChangeEvent) { - await this.container.storage.store('home:status:pinned', true); - void this.notifyDidChangeData(e.current); - } - - onVisibilityChanged(visible: boolean): void { - if (!visible) { - this._validateSubscriptionDebounced?.cancel(); - return; - } - - queueMicrotask(() => void this.validateSubscription()); - } - - onWindowFocusChanged(focused: boolean): void { - if (!focused || !this.host.visible) { - this._validateSubscriptionDebounced?.cancel(); - return; - } - - queueMicrotask(() => void this.validateSubscription()); - } - registerCommands(): Disposable[] { - return [ - registerCommand(`${this.host.id}.refresh`, () => this.host.refresh(true), this), - registerCommand('gitlens.home.toggleWelcome', async () => { - const welcomeVisible = !this.welcomeVisible; - await this.container.storage.store('views:welcome:visible', welcomeVisible); - if (welcomeVisible) { - await Promise.allSettled([ - this.container.storage.store('home:actions:completed', []), - this.container.storage.store('home:steps:completed', []), - this.container.storage.store('home:sections:dismissed', []), - ]); - } - - void this.host.refresh(); - }), - registerCommand('gitlens.home.restoreWelcome', async () => { - await Promise.allSettled([ - this.container.storage.store('home:steps:completed', []), - this.container.storage.store('home:sections:dismissed', []), - ]); - - void this.host.refresh(); - }), - - registerCommand('gitlens.home.showSCM', async () => { - const completedActions = this.container.storage.get('home:actions:completed', []); - if (!completedActions.includes(CompletedActions.OpenedSCM)) { - completedActions.push(CompletedActions.OpenedSCM); - await this.container.storage.store('home:actions:completed', completedActions); - - void this.notifyDidChangeData(); - } - - await executeCoreCommand('workbench.view.scm'); - }), - ]; - } - - onMessageReceived(e: IpcMessage) { - switch (e.method) { - case CompleteStepCommandType.method: - onIpc(CompleteStepCommandType, e, params => this.completeStep(params)); - break; - case DismissSectionCommandType.method: - onIpc(DismissSectionCommandType, e, params => this.dismissSection(params)); - break; - case DismissStatusCommandType.method: - onIpc(DismissStatusCommandType, e, _params => this.dismissPinStatus()); - break; - case DismissBannerCommandType.method: - onIpc(DismissBannerCommandType, e, params => this.dismissBanner(params)); - break; - } - } - - private async completeStep({ id, completed = false }: CompleteStepParams) { - const steps = this.container.storage.get('home:steps:completed', []); - - const hasStep = steps.includes(id); - if (!hasStep && completed) { - steps.push(id); - } else if (hasStep && !completed) { - steps.splice(steps.indexOf(id), 1); - } - - await this.container.storage.store('home:steps:completed', steps); - void this.notifyDidChangeData(); - } - - private async dismissSection(params: DismissSectionParams) { - const sections = this.container.storage.get('home:sections:dismissed', []); - if (sections.includes(params.id)) return; - - sections.push(params.id); - - await this.container.storage.store('home:sections:dismissed', sections); - void this.notifyDidChangeData(); - } - - private async dismissBanner(params: DismissBannerParams) { - const banners = this.container.storage.get('home:banners:dismissed', []); - - if (!banners.includes(params.id)) { - banners.push(params.id); - } - - await this.container.storage.store('home:banners:dismissed', banners); - void this.notifyDidChangeData(); - } - - private async dismissPinStatus() { - await this.container.storage.store('home:status:pinned', false); - void this.notifyDidChangeData(); + return [registerCommand(`${this.host.id}.refresh`, () => this.host.refresh(true), this)]; } includeBootstrap(): Promise { return this.getState(); } - private get welcomeVisible(): boolean { - return this.container.storage.get('views:welcome:visible', true); - } - - private async getRepoVisibility(): Promise { - const visibility = await this.container.git.visibility(); - return visibility; - } - - private async getSubscription(subscription?: Subscription) { - // Make sure to make a copy of the array otherwise it will be live to the storage value - const completedActions = [...this.container.storage.get('home:actions:completed', [])]; - if (!this.welcomeVisible) { - completedActions.push(CompletedActions.DismissedWelcome); - } - - const sub = subscription ?? (await this.container.subscription.getSubscription(true)); - - let avatar; - if (sub.account?.email) { - avatar = getAvatarUriFromGravatarEmail(sub.account.email, 34).toString(); - } else { - avatar = `${this.host.getWebRoot() ?? ''}/media/gitlens-logo.webp`; - } - - return { - subscription: sub, - completedActions: completedActions, - avatar: avatar, - }; - } - - private getPinStatus() { - return this.container.storage.get('home:status:pinned') ?? true; - } - - private async getState(subscription?: Subscription): Promise { - const [visibilityResult, subscriptionResult] = await Promise.allSettled([ - this.getRepoVisibility(), - this.getSubscription(subscription), - ]); - - const sub = getSettledValue(subscriptionResult)!; - const steps = this.container.storage.get('home:steps:completed', []); - const sections = this.container.storage.get('home:sections:dismissed', []); - const dismissedBanners = this.container.storage.get('home:banners:dismissed', []); - - return { + private async getState(): Promise { + return Promise.resolve({ timestamp: Date.now(), repositories: this.getRepositoriesState(), webroot: this.host.getWebRoot(), - subscription: sub.subscription, - completedActions: sub.completedActions, - plusEnabled: this.getPlusEnabled(), - visibility: getSettledValue(visibilityResult)!, - completedSteps: steps, - dismissedSections: sections, - avatar: sub.avatar, - layout: this.getLayout(), - pinStatus: this.getPinStatus(), - dismissedBanners: dismissedBanners, - }; - } - - private notifyDidChangeData(subscription?: Subscription) { - if (!this.host.ready) return false; - - return window.withProgress({ location: { viewId: this.host.id } }, async () => { - const sub = await this.getSubscription(subscription); - return this.host.notify(DidChangeSubscriptionNotificationType, { - ...sub, - pinStatus: this.getPinStatus(), - }); }); } @@ -290,50 +61,4 @@ export class HomeWebviewProvider implements WebviewProvider { void this.host.notify(DidChangeRepositoriesType, this.getRepositoriesState()); } - - private getPlusEnabled() { - return configuration.get('plusFeatures.enabled'); - } - - private notifyDidChangeConfiguration() { - if (!this.host.ready) return; - - void this.host.notify(DidChangeConfigurationType, { - plusEnabled: this.getPlusEnabled(), - }); - } - - private getLayout() { - const layout = this.container.storage.get('views:layout'); - return layout != null ? (layout as ViewsLayout) : ViewsLayout.SourceControl; - } - - private notifyDidChangeLayout() { - if (!this.host.ready) return; - - void this.host.notify(DidChangeLayoutType, { layout: this.getLayout() }); - } - - private _validateSubscriptionDebounced: Deferrable | undefined = - undefined; - - private async validateSubscription(): Promise { - if (this._validateSubscriptionDebounced == null) { - this._validateSubscriptionDebounced = debounce(this.validateSubscriptionCore, 1000); - } - - await this._validateSubscriptionDebounced(); - } - - private _validating: Promise | undefined; - private async validateSubscriptionCore() { - if (this._validating == null) { - this._validating = this.container.subscription.validate(); - try { - await this._validating; - } finally { - this._validating = undefined; - } - } - } } diff --git a/src/webviews/home/protocol.ts b/src/webviews/home/protocol.ts index 050fbf6..5c0e202 100644 --- a/src/webviews/home/protocol.ts +++ b/src/webviews/home/protocol.ts @@ -1,7 +1,4 @@ -import type { ViewsLayout } from '../../commands/setViewsLayout'; -import type { RepositoriesVisibility } from '../../git/gitProviderService'; -import type { Subscription } from '../../subscription'; -import { IpcCommandType, IpcNotificationType } from '../protocol'; +import { IpcNotificationType } from '../protocol'; export const enum CompletedActions { DismissedWelcome = 'dismissed:welcome', @@ -13,46 +10,8 @@ export interface State { repositories: DidChangeRepositoriesParams; webroot?: string; - subscription: Subscription; - completedActions: CompletedActions[]; - completedSteps?: string[]; - dismissedBanners?: string[]; - dismissedSections?: string[]; - plusEnabled: boolean; - visibility: RepositoriesVisibility; - avatar?: string; - layout: ViewsLayout; - pinStatus: boolean; } -export interface CompleteStepParams { - id: string; - completed: boolean; -} -export const CompleteStepCommandType = new IpcCommandType('home/step/complete'); - -export interface DismissSectionParams { - id: string; -} -export const DismissSectionCommandType = new IpcCommandType('home/section/dismiss'); - -export const DismissStatusCommandType = new IpcCommandType('home/status/dismiss'); - -export interface DismissBannerParams { - id: string; -} -export const DismissBannerCommandType = new IpcCommandType('home/banner/dismiss'); - -export interface DidChangeSubscriptionParams { - subscription: Subscription; - completedActions: CompletedActions[]; - avatar?: string; - pinStatus: boolean; -} -export const DidChangeSubscriptionNotificationType = new IpcNotificationType( - 'subscription/didChange', -); - export interface DidChangeRepositoriesParams { count: number; openCount: number; @@ -60,15 +19,3 @@ export interface DidChangeRepositoriesParams { trusted: boolean; } export const DidChangeRepositoriesType = new IpcNotificationType('repositories/didChange'); - -export interface DidChangeConfigurationParams { - plusEnabled: boolean; -} -export const DidChangeConfigurationType = new IpcNotificationType( - 'configuration/didChange', -); - -export interface DidChangeLayoutParams { - layout: ViewsLayout; -} -export const DidChangeLayoutType = new IpcNotificationType('layout/didChange'); diff --git a/webpack.config.js b/webpack.config.js index a9aabbb..aa2aaf6 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -329,6 +329,7 @@ function getWebviewsConfig(mode, env) { getHtmlPlugin('timeline', true, mode, env), getHtmlPlugin('welcome', false, mode, env), getHtmlPlugin('focus', true, mode, env), + getHtmlPlugin('account', true, mode, env), getCspHtmlPlugin(mode, env), new InlineChunkHtmlPlugin(HtmlPlugin, mode === 'production' ? ['\\.css$'] : []), new CopyPlugin({ @@ -392,6 +393,7 @@ function getWebviewsConfig(mode, env) { timeline: './plus/timeline/timeline.ts', welcome: './welcome/welcome.ts', focus: './plus/focus/focus.ts', + account: './plus/account/account.ts', }, mode: mode, target: 'web',