diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index ec5f1c66c14..3c03f349446 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -51,7 +51,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; -import { UserDataSyncAccountManager } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncAccount'; +import { UserDataSyncAccounts, AccountStatus } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncAccount'; const CONTEXT_CONFLICTS_SOURCES = new RawContextKey('conflictsSources', ''); @@ -85,13 +85,13 @@ const getActivityTitle = (label: string, userDataSyncService: IUserDataSyncServi } return label; }; -const getIdentityTitle = (label: string, userDataSyncAccountService: UserDataSyncAccountManager, authenticationService: IAuthenticationService) => { - const activeAccount = userDataSyncAccountService.activeAccount; - return activeAccount ? `${label} (${authenticationService.getDisplayName(activeAccount.providerId)}:${activeAccount.accountName})` : label; +const getIdentityTitle = (label: string, userDataSyncAccountService: UserDataSyncAccounts, authenticationService: IAuthenticationService) => { + const account = userDataSyncAccountService.current; + return account ? `${label} (${authenticationService.getDisplayName(account.providerId)}:${account.accountName})` : label; }; const turnOnSyncCommand = { id: 'workbench.userData.actions.syncStart', title: localize('turn on sync with category', "Preferences Sync: Turn on...") }; const signInCommand = { id: 'workbench.userData.actions.signin', title: localize('sign in', "Preferences Sync: Sign in to sync") }; -const stopSyncCommand = { id: 'workbench.userData.actions.stopSync', title(userDataSyncAccountService: UserDataSyncAccountManager, authenticationService: IAuthenticationService) { return getIdentityTitle(localize('stop sync', "Preferences Sync: Turn Off"), userDataSyncAccountService, authenticationService); } }; +const stopSyncCommand = { id: 'workbench.userData.actions.stopSync', title(userDataSyncAccountService: UserDataSyncAccounts, authenticationService: IAuthenticationService) { return getIdentityTitle(localize('stop sync', "Preferences Sync: Turn Off"), userDataSyncAccountService, authenticationService); } }; const resolveSettingsConflictsCommand = { id: 'workbench.userData.actions.resolveSettingsConflicts', title: localize('showConflicts', "Preferences Sync: Show Settings Conflicts") }; const resolveKeybindingsConflictsCommand = { id: 'workbench.userData.actions.resolveKeybindingsConflicts', title: localize('showKeybindingsConflicts', "Preferences Sync: Show Keybindings Conflicts") }; const resolveSnippetsConflictsCommand = { id: 'workbench.userData.actions.resolveSnippetsConflicts', title: localize('showSnippetsConflicts', "Preferences Sync: Show User Snippets Conflicts") }; @@ -103,21 +103,16 @@ const showSyncActivityCommand = { }; const showSyncSettingsCommand = { id: 'workbench.userData.actions.syncSettings', title: localize('sync settings', "Preferences Sync: Show Settings"), }; -const CONTEXT_ACTIVE_ACCOUNT_STATE = new RawContextKey('activeAccountStatus', SyncStatus.Uninitialized); -const enum ActiveAccountStatus { - Uninitialized = 'uninitialized', - Active = 'active', - Inactive = 'inactive' -} +const CONTEXT_ACCOUNT_STATE = new RawContextKey('userDataSyncAccountStatus', AccountStatus.Uninitialized); export class UserDataSyncWorkbenchContribution extends Disposable implements IWorkbenchContribution { private readonly syncEnablementContext: IContextKey; private readonly syncStatusContext: IContextKey; - private readonly activeAccountStatusContext: IContextKey; + private readonly accountStatusContext: IContextKey; private readonly conflictsSources: IContextKey; - private readonly userDataSyncAccountManager: UserDataSyncAccountManager; + private readonly userDataSyncAccounts: UserDataSyncAccounts; private readonly badgeDisposable = this._register(new MutableDisposable()); constructor( @@ -146,25 +141,25 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.syncEnablementContext = CONTEXT_SYNC_ENABLEMENT.bindTo(contextKeyService); this.syncStatusContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService); - this.activeAccountStatusContext = CONTEXT_ACTIVE_ACCOUNT_STATE.bindTo(contextKeyService); + this.accountStatusContext = CONTEXT_ACCOUNT_STATE.bindTo(contextKeyService); this.conflictsSources = CONTEXT_CONFLICTS_SOURCES.bindTo(contextKeyService); - this.userDataSyncAccountManager = instantiationService.createInstance(UserDataSyncAccountManager); + this.userDataSyncAccounts = instantiationService.createInstance(UserDataSyncAccounts); - if (this.userDataSyncAccountManager.userDataSyncAccountProvider) { + if (this.userDataSyncAccounts.accountProviderId) { registerConfiguration(); this.onDidChangeSyncStatus(this.userDataSyncService.status); this.onDidChangeConflicts(this.userDataSyncService.conflicts); this.onDidChangeEnablement(this.userDataSyncEnablementService.isEnabled()); - this.onDidChangeActiveAccount(); + this.onDidChangeAccountStatus(this.userDataSyncAccounts.status); this._register(Event.debounce(userDataSyncService.onDidChangeStatus, () => undefined, 500)(() => this.onDidChangeSyncStatus(this.userDataSyncService.status))); this._register(userDataSyncService.onDidChangeConflicts(() => this.onDidChangeConflicts(this.userDataSyncService.conflicts))); this._register(userDataSyncService.onSyncErrors(errors => this.onSyncErrors(errors))); this._register(this.userDataSyncEnablementService.onDidChangeEnablement(enabled => this.onDidChangeEnablement(enabled))); this._register(userDataAutoSyncService.onError(error => this.onAutoSyncError(error))); - this._register(this.userDataSyncAccountManager.onDidChangeActiveAccount(() => this.onDidChangeActiveAccount())); + this._register(this.userDataSyncAccounts.onDidChangeStatus(status => this.onDidChangeAccountStatus(status))); this.registerActions(); textModelResolverService.registerTextModelContentProvider(USER_DATA_SYNC_SCHEME, instantiationService.createInstance(UserDataRemoteContentProvider)); @@ -176,11 +171,12 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } - private onDidChangeActiveAccount(): void { - const activeAccount = this.userDataSyncAccountManager.activeAccount; - this.activeAccountStatusContext.set(activeAccount === undefined ? ActiveAccountStatus.Uninitialized - : activeAccount === null ? ActiveAccountStatus.Inactive : ActiveAccountStatus.Active); + private onDidChangeAccountStatus(status: AccountStatus): void { + this.accountStatusContext.set(status); this.updateBadge(); + if (status === AccountStatus.Unavailable) { + this.doTurnOff(false); + } } private onDidChangeSyncStatus(status: SyncStatus) { @@ -394,7 +390,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo let clazz: string | undefined; let priority: number | undefined = undefined; - if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.userDataSyncEnablementService.isEnabled() && this.userDataSyncAccountManager.activeAccount === null) { + if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.userDataSyncEnablementService.isEnabled() && this.userDataSyncAccounts.status === AccountStatus.Inactive) { badge = new NumberBadge(1, () => localize('sign in to sync', "Sign in to Sync")); } else if (this.userDataSyncService.conflicts.length) { badge = new NumberBadge(this.userDataSyncService.conflicts.reduce((result, syncResourceConflict) => { return result + syncResourceConflict.conflicts.length; }, 0), () => localize('has conflicts', "Preferences Sync: Conflicts Detected")); @@ -432,12 +428,13 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo quickPick.title = localize('turn on title', "Preferences Sync: Turn On"); quickPick.ok = false; quickPick.customButton = true; - if (this.userDataSyncAccountManager.activeAccount) { - quickPick.customLabel = localize('turn on', "Turn On"); - } else { - const displayName = this.authenticationService.getDisplayName(this.userDataSyncAccountManager.userDataSyncAccountProvider!); + const requiresLogin = this.userDataSyncAccounts.all.length === 0; + if (requiresLogin) { + const displayName = this.authenticationService.getDisplayName(this.userDataSyncAccounts.accountProviderId!); quickPick.description = localize('sign in and turn on sync detail', "Sign in with your {0} account to synchronize your data across devices.", displayName); quickPick.customLabel = localize('sign in and turn on sync', "Sign in & Turn on"); + } else { + quickPick.customLabel = localize('turn on', "Turn On"); } quickPick.placeholder = localize('configure sync placeholder', "Choose what to sync"); quickPick.canSelectMany = true; @@ -448,7 +445,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo disposables.add(Event.any(quickPick.onDidAccept, quickPick.onDidCustom)(async () => { if (quickPick.selectedItems.length) { this.updateConfiguration(items, quickPick.selectedItems); - this.doTurnOn().then(c, e); + this.doTurnOn(requiresLogin).then(c, e); quickPick.hide(); } })); @@ -457,17 +454,16 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); } - private async doTurnOn(): Promise { - // If this was not triggered by signing in from the accounts menu, show accounts list - if (this.userDataSyncAccountManager.activeAccount) { - await this.userDataSyncAccountManager.select(); + private async doTurnOn(requiresLogin: boolean): Promise { + if (requiresLogin) { + await this.userDataSyncAccounts.login(); } else { - await this.userDataSyncAccountManager.login(); + await this.userDataSyncAccounts.select(); } - // User did not pick an account or login failed, no need to continue - if (!this.userDataSyncAccountManager.activeAccount) { - return; + // User did not pick an account or login failed + if (this.userDataSyncAccounts.status !== AccountStatus.Active) { + throw new Error(localize('no account', "No account available")); } await this.handleFirstTimeSync(); @@ -575,16 +571,20 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } }); if (result.confirmed) { - if (result.checkboxChecked) { - this.telemetryService.publicLog2('sync/turnOffEveryWhere'); - await this.userDataSyncService.reset(); - } else { - await this.userDataSyncService.resetLocal(); - } - this.disableSync(); + return this.doTurnOff(!!result.checkboxChecked); } } + private async doTurnOff(turnOffEveryWhere: boolean): Promise { + if (turnOffEveryWhere) { + this.telemetryService.publicLog2('sync/turnOffEveryWhere'); + await this.userDataSyncService.reset(); + } else { + await this.userDataSyncService.resetLocal(); + } + this.disableSync(); + } + private disableSync(source?: SyncResource): void { if (source === undefined) { this.userDataSyncEnablementService.setEnablement(false); @@ -664,7 +664,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private registerTurnOnSyncAction(): void { - const turnOnSyncWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT.toNegated(), CONTEXT_ACTIVE_ACCOUNT_STATE.notEqualsTo(ActiveAccountStatus.Uninitialized)); + const turnOnSyncWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT.toNegated(), CONTEXT_ACCOUNT_STATE.notEqualsTo(AccountStatus.Uninitialized)); CommandsRegistry.registerCommand(turnOnSyncCommand.id, async () => { try { await this.turnOn(); @@ -715,14 +715,14 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo menu: { group: '5_sync', id: MenuId.GlobalActivity, - when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACTIVE_ACCOUNT_STATE.isEqualTo(ActiveAccountStatus.Inactive)), + when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Inactive)), order: 2 }, }); } async run(): Promise { try { - await that.userDataSyncAccountManager.login(); + await that.userDataSyncAccounts.login(); } catch (e) { that.notificationService.error(e); } @@ -816,7 +816,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private registerSyncStatusAction(): void { const that = this; - const when = ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACTIVE_ACCOUNT_STATE.isEqualTo(ActiveAccountStatus.Active), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)); + const when = ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Active), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)); this._register(registerAction2(class SyncStatusAction extends Action2 { constructor() { super({ @@ -871,7 +871,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo items.push({ id: showSyncSettingsCommand.id, label: showSyncSettingsCommand.title }); items.push({ id: showSyncActivityCommand.id, label: showSyncActivityCommand.title(that.userDataSyncService) }); items.push({ type: 'separator' }); - items.push({ id: stopSyncCommand.id, label: stopSyncCommand.title(that.userDataSyncAccountManager, that.authenticationService) }); + items.push({ id: stopSyncCommand.id, label: stopSyncCommand.title(that.userDataSyncAccounts, that.authenticationService) }); quickPick.items = items; disposables.add(quickPick.onDidAccept(() => { if (quickPick.selectedItems[0] && quickPick.selectedItems[0].id) { @@ -895,7 +895,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo constructor() { super({ id: stopSyncCommand.id, - title: stopSyncCommand.title(that.userDataSyncAccountManager, that.authenticationService), + title: stopSyncCommand.title(that.userDataSyncAccounts, that.authenticationService), menu: { id: MenuId.CommandPalette, when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT), diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncAccount.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncAccount.ts index 40347d24b0d..1922c15af24 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncAccount.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncAccount.ts @@ -7,15 +7,16 @@ import { IAuthenticationService } from 'vs/workbench/services/authentication/bro import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage'; import { localize } from 'vs/nls'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { AuthenticationSession } from 'vs/editor/common/modes'; +import { AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/editor/common/modes'; import { Event, Emitter } from 'vs/base/common/event'; import { getUserDataSyncStore, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { distinct } from 'vs/base/common/arrays'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { values } from 'vs/base/common/map'; +import { ILogService } from 'vs/platform/log/common/log'; type UserAccountClassification = { id: { classification: 'EndUserPseudonymizedInformation', purpose: 'BusinessInsight' }; @@ -26,23 +27,35 @@ type UserAccountEvent = { }; export interface IUserDataSyncAccount { - providerId: string; - sessionId: string; - accountName: string; + readonly providerId: string; + readonly sessionId: string; + readonly accountName: string; } -export class UserDataSyncAccountManager extends Disposable { +export const enum AccountStatus { + Uninitialized = 'uninitialized', + Unavailable = 'unavailable', + Inactive = 'inactive', + Active = 'active', +} - private static LAST_USED_SESSION_STORAGE_KEY = 'userDataSyncAccountPreference'; +export class UserDataSyncAccounts extends Disposable { + + private static CACHED_SESSION_STORAGE_KEY = 'userDataSyncAccountPreference'; _serviceBrand: any; - readonly userDataSyncAccountProvider: string | undefined; + readonly accountProviderId: string | undefined; - private _activeAccount: IUserDataSyncAccount | undefined | null; - get activeAccount(): IUserDataSyncAccount | undefined | null { return this._activeAccount; } - private readonly _onDidChangeActiveAccount = this._register(new Emitter<{ previous: IUserDataSyncAccount | undefined | null, current: IUserDataSyncAccount | null }>()); - readonly onDidChangeActiveAccount = this._onDidChangeActiveAccount.event; + private _status: AccountStatus = AccountStatus.Uninitialized; + get status(): AccountStatus { return this._status; } + private readonly _onDidChangeStatus = this._register(new Emitter()); + readonly onDidChangeStatus = this._onDidChangeStatus.event; + + private _all: IUserDataSyncAccount[] = []; + get all(): IUserDataSyncAccount[] { return this._all; } + + get current(): IUserDataSyncAccount | undefined { return this._all.filter(({ sessionId }) => sessionId === this.currentSessionId)[0]; } constructor( @IAuthenticationService private readonly authenticationService: IAuthenticationService, @@ -51,98 +64,130 @@ export class UserDataSyncAccountManager extends Disposable { @IStorageService private readonly storageService: IStorageService, @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @ILogService private readonly logService: ILogService, @IProductService productService: IProductService, @IConfigurationService configurationService: IConfigurationService, ) { super(); - this.userDataSyncAccountProvider = getUserDataSyncStore(productService, configurationService)?.authenticationProviderId; - if (this.userDataSyncAccountProvider) { - if (authenticationService.isAuthenticationProviderRegistered(this.userDataSyncAccountProvider)) { + this.accountProviderId = getUserDataSyncStore(productService, configurationService)?.authenticationProviderId; + if (this.accountProviderId) { + if (authenticationService.isAuthenticationProviderRegistered(this.accountProviderId)) { this.initialize(); } else { - this._register(Event.once(Event.filter(this.authenticationService.onDidRegisterAuthenticationProvider, providerId => providerId === this.userDataSyncAccountProvider))(() => this.initialize())); + this._register(Event.once(Event.filter(this.authenticationService.onDidRegisterAuthenticationProvider, providerId => providerId === this.accountProviderId))(() => this.initialize())); } } } private async initialize(): Promise { await this.update(); + this._register( Event.any( Event.filter( Event.any( this.authenticationService.onDidRegisterAuthenticationProvider, this.authenticationService.onDidUnregisterAuthenticationProvider, - Event.map(this.authenticationService.onDidChangeSessions, e => e.providerId) - ), providerId => providerId === this.userDataSyncAccountProvider), + ), providerId => providerId === this.accountProviderId), this.authenticationTokenService.onTokenFailed) (() => this.update())); + + this._register(Event.filter(this.authenticationService.onDidChangeSessions, e => e.providerId === this.accountProviderId)(({ event }) => this.onDidChangeSessions(event))); + this._register(this.storageService.onDidChangeStorage(e => this.onDidChangeStorage(e))); } private async update(): Promise { - if (!this.userDataSyncAccountProvider) { - return; - } - let activeSession: AuthenticationSession | undefined = undefined; - if (this.lastUsedSessionId) { - const sessions = await this.authenticationService.getSessions(this.userDataSyncAccountProvider); - if (sessions?.length) { - activeSession = sessions.find(session => session.id === this.lastUsedSessionId); + + let status = AccountStatus.Unavailable; + let allAccounts: Map = new Map(); + + if (this.accountProviderId) { + + let currentAccount: IUserDataSyncAccount | null = null; + let currentSession: AuthenticationSession | undefined = undefined; + + if (this.currentSessionId) { + const sessions = await this.authenticationService.getSessions(this.accountProviderId) || []; + for (const session of sessions) { + const account: IUserDataSyncAccount = { providerId: this.accountProviderId, sessionId: session.id, accountName: session.accountName }; + allAccounts.set(account.accountName, account); + if (session.id === this.currentSessionId) { + currentSession = session; + currentAccount = account; + } + } + } + + if (currentSession) { + status = AccountStatus.Inactive; + try { + const token = await currentSession.getAccessToken(); + await this.authenticationTokenService.setToken(token); + status = AccountStatus.Active; + } catch (e) { + // Ignore error + } + } + + if (currentAccount) { + // Always use current account if available + allAccounts.set(currentAccount.accountName, currentAccount); } } - let activeAccount: IUserDataSyncAccount | null = null; - if (activeSession) { - try { - const token = await activeSession.getAccessToken(); - await this.authenticationTokenService.setToken(token); - activeAccount = { - providerId: this.userDataSyncAccountProvider, - sessionId: activeSession.id, - accountName: activeSession.accountName - }; - } catch (e) { - // Ignore and log error - } + this._all = values(allAccounts); + + if (this._status !== status) { + this._status = status; + this.logService.debug('Sync account status changed', this._status, status); + this._onDidChangeStatus.fire(status); } - if (!this.areSameAccounts(activeAccount, this._activeAccount)) { - const previous = this._activeAccount; - this._activeAccount = activeAccount; - this._onDidChangeActiveAccount.fire({ previous, current: this._activeAccount }); - } } async login(): Promise { - if (this.userDataSyncAccountProvider) { - const session = await this.authenticationService.login(this.userDataSyncAccountProvider, ['https://management.core.windows.net/.default', 'offline_access']); - await this.switch(session.id); + if (this.accountProviderId) { + const session = await this.authenticationService.login(this.accountProviderId, ['https://management.core.windows.net/.default', 'offline_access']); + await this.switch(session.id, session.accountName); } } async select(): Promise { - if (!this.activeAccount) { + if (!this.accountProviderId) { + return; + } + if (!this.all.length) { throw new Error('Requires Login'); } await this.update(); - if (!this.activeAccount) { + if (!this.all.length) { throw new Error('Requires Login'); } - const { providerId, sessionId } = this.activeAccount; await new Promise(async (c, e) => { const disposables: DisposableStore = new DisposableStore(); - const quickPick = this.quickInputService.createQuickPick<{ label: string, session?: AuthenticationSession, detail?: string }>(); + const quickPick = this.quickInputService.createQuickPick<{ label: string, account?: IUserDataSyncAccount, detail?: string }>(); disposables.add(quickPick); - quickPick.title = localize('pick account', "{0}: Pick an account", this.authenticationService.getDisplayName(providerId)); + quickPick.title = localize('pick account', "{0}: Pick an account", this.authenticationService.getDisplayName(this.accountProviderId!)); quickPick.ok = false; quickPick.placeholder = localize('choose account placeholder', "Pick an account for syncing"); quickPick.ignoreFocusOut = true; + + const currentAccount = this.current; + const accounts = currentAccount + ? [currentAccount, ...this._all.filter(account => account.sessionId !== this.current!.sessionId)] + : this._all; + quickPick.items = [...accounts.map(account => ({ + label: account.accountName, + account: account, + detail: account.sessionId === this.current?.sessionId ? localize('last used', "Last Used") : undefined + })), { label: localize('choose another', "Use another account") }]; + disposables.add(quickPick.onDidAccept(async () => { const selected = quickPick.selectedItems[0]; if (selected) { - if (selected.session) { - await this.switch(selected.session.id); + if (selected.account) { + await this.switch(selected.account.sessionId, selected.account.accountName); } else { await this.login(); } @@ -152,66 +197,55 @@ export class UserDataSyncAccountManager extends Disposable { })); disposables.add(quickPick.onDidHide(() => disposables.dispose())); quickPick.show(); - - quickPick.busy = true; - quickPick.items = await this.getSessionQuickPickItems(providerId, sessionId); - quickPick.busy = false; - }); } - async switch(sessionId: string): Promise { - if (this.userDataSyncEnablementService.isEnabled() && (this.lastUsedSessionId && this.lastUsedSessionId !== sessionId)) { + private async switch(sessionId: string, accountName: string): Promise { + const currentAccount = this.current; + if (this.userDataSyncEnablementService.isEnabled() && (currentAccount && currentAccount.accountName !== accountName)) { // accounts are switched while sync is enabled. } - this.lastUsedSessionId = sessionId; + this.currentSessionId = sessionId; this.telemetryService.publicLog2('sync.userAccount', { id: sessionId.split('/')[1] }); await this.update(); } - private async getSessionQuickPickItems(providerId: string, sessionId: string): Promise<{ label: string, session?: AuthenticationSession, detail?: string }[]> { - const quickPickItems: { label: string, session?: AuthenticationSession, detail?: string }[] = []; - - let sessions = await this.authenticationService.getSessions(providerId) || []; - const lastUsedSession = sessions.filter(session => session.id === sessionId)[0]; - - if (lastUsedSession) { - sessions = sessions.filter(session => session.accountName !== lastUsedSession.accountName); - quickPickItems.push({ - label: lastUsedSession.accountName, - session: lastUsedSession, - detail: localize('previously used', "Last used") - }); + private onDidChangeSessions(e: AuthenticationSessionsChangeEvent): void { + if (this.currentSessionId && e.removed.includes(this.currentSessionId)) { + this.currentSessionId = undefined; } - - quickPickItems.push(...distinct(sessions, session => session.accountName).map(session => ({ label: session.accountName, session }))); - quickPickItems.push({ label: localize('choose another', "Use another account") }); - return quickPickItems; + this.update(); } - private get lastUsedSessionId(): string | undefined { - return this.storageService.get(UserDataSyncAccountManager.LAST_USED_SESSION_STORAGE_KEY, StorageScope.GLOBAL); - } - - private set lastUsedSessionId(lastUserSessionId: string | undefined) { - if (lastUserSessionId === undefined) { - this.storageService.remove(UserDataSyncAccountManager.LAST_USED_SESSION_STORAGE_KEY, StorageScope.GLOBAL); - } else { - this.storageService.store(UserDataSyncAccountManager.LAST_USED_SESSION_STORAGE_KEY, lastUserSessionId, StorageScope.GLOBAL); + private onDidChangeStorage(e: IWorkspaceStorageChangeEvent): void { + if (e.key === UserDataSyncAccounts.CACHED_SESSION_STORAGE_KEY && e.scope === StorageScope.GLOBAL + && this.currentSessionId !== this.getStoredCachedSessionId() /* This checks if current window changed the value or not */) { + this._cachedCurrentSessionId = null; + this.update(); } } - private areSameAccounts(a: IUserDataSyncAccount | undefined | null, b: IUserDataSyncAccount | undefined | null): boolean { - if (a === b) { - return true; + private _cachedCurrentSessionId: string | undefined | null = null; + private get currentSessionId(): string | undefined { + if (this._cachedCurrentSessionId === null) { + this._cachedCurrentSessionId = this.getStoredCachedSessionId(); } - if (a && b - && a.providerId === b.providerId - && a.sessionId === b.sessionId - ) { - return true; + return this._cachedCurrentSessionId; + } + + private set currentSessionId(cachedSessionId: string | undefined) { + if (this.currentSessionId !== cachedSessionId) { + this._cachedCurrentSessionId = cachedSessionId; + if (cachedSessionId === undefined) { + this.storageService.remove(UserDataSyncAccounts.CACHED_SESSION_STORAGE_KEY, StorageScope.GLOBAL); + } else { + this.storageService.store(UserDataSyncAccounts.CACHED_SESSION_STORAGE_KEY, cachedSessionId, StorageScope.GLOBAL); + } } - return false; + } + + private getStoredCachedSessionId(): string | undefined { + return this.storageService.get(UserDataSyncAccounts.CACHED_SESSION_STORAGE_KEY, StorageScope.GLOBAL); } }