Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions extensions/github-authentication/src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
const sessionPromises = sessionData.map(async (session: SessionData): Promise<vscode.AuthenticationSession | undefined> => {
// For GitHub scope list, order doesn't matter so we immediately sort the scopes
const scopesStr = [...session.scopes].sort().join(' ');
let userInfo: { id: string; accountName: string } | undefined;
let userInfo: { id: string; accountName: string; avatarUrl?: string } | undefined;
if (!session.account) {
try {
userInfo = await this._githubServer.getUserInfo(session.accessToken);
Expand Down Expand Up @@ -314,7 +314,8 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
label: session.account
? session.account.label ?? session.account.displayName ?? '<unknown>'
: userInfo?.accountName ?? '<unknown>',
id: accountId
id: accountId,
avatarUrl: userInfo?.avatarUrl ?? `https://avatars.githubusercontent.com/u/${accountId}?v=4`
},
// we set this to session.scopes to maintain the original order of the scopes requested
// by the extension that called getSession()
Expand Down Expand Up @@ -412,7 +413,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
return {
id: crypto.getRandomValues(new Uint32Array(2)).reduce((prev, curr) => prev += curr.toString(16), ''),
accessToken: token,
account: { label: userInfo.accountName, id: userInfo.id },
account: { label: userInfo.accountName, id: userInfo.id, avatarUrl: userInfo.avatarUrl },
scopes
};
}
Expand Down
8 changes: 4 additions & 4 deletions extensions/github-authentication/src/githubServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const REDIRECT_URL_INSIDERS = 'https://insiders.vscode.dev/redirect';
export interface IGitHubServer {
login(scopes: string, signInProvider?: GitHubSocialSignInProvider, extraAuthorizeParameters?: Record<string, string>, existingLogin?: string): Promise<string>;
logout(session: vscode.AuthenticationSession): Promise<void>;
getUserInfo(token: string): Promise<{ id: string; accountName: string }>;
getUserInfo(token: string): Promise<{ id: string; accountName: string; avatarUrl?: string }>;
sendAdditionalTelemetryInfo(session: vscode.AuthenticationSession): Promise<void>;
friendlyName: string;
}
Expand Down Expand Up @@ -217,7 +217,7 @@ export class GitHubServer implements IGitHubServer {
return vscode.Uri.parse(`${apiUri.scheme}://${apiUri.authority}/api/v3${path}`);
}

public async getUserInfo(token: string): Promise<{ id: string; accountName: string }> {
public async getUserInfo(token: string): Promise<{ id: string; accountName: string; avatarUrl?: string }> {
let result;
try {
this._logger.info('Getting user info...');
Expand All @@ -237,9 +237,9 @@ export class GitHubServer implements IGitHubServer {

if (result.ok) {
try {
const json = await result.json() as { id: number; login: string };
const json = await result.json() as { id: number; login: string; avatar_url?: string };
this._logger.info('Got account info!');
return { id: `${json.id}`, accountName: json.login };
return { id: `${json.id}`, accountName: json.login, avatarUrl: json.avatar_url };
} catch (e) {
this._logger.error(`Unexpected error parsing response from GitHub: ${e.message ?? e}`);
throw e;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,26 @@
width: 100%;
height: 35px;
}

/* Avatar on accounts icon - replaces the codicon glyph with a circular profile picture */
.action-label.has-avatar {
position: relative;
}

.action-label.has-avatar::before {
visibility: hidden;
}

.action-label.has-avatar .accounts-avatar {
position: absolute;
top: 50%;
left: 50%;
width: 22px;
height: 22px;
transform: translate(-50%, -50%);
border-radius: 50%;
object-fit: cover;
pointer-events: none;
user-select: none;
-webkit-user-drag: none;
}
61 changes: 61 additions & 0 deletions src/vs/workbench/browser/parts/globalCompositeBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,14 +298,73 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction
this.initialize();
}

private currentAvatarUrl: string | undefined;
private avatarElement: HTMLImageElement | undefined;

override render(container: HTMLElement): void {
super.render(container);
this.updateAvatar();
}

protected override updateLabel(): void {
super.updateLabel();
// Re-apply avatar styling after the parent resets the label classes
if (this.currentAvatarUrl && this.label) {
this.applyAvatarToLabel(this.currentAvatarUrl);
}
}

private updateAvatar(): void {
// Find the first account with an avatarUrl
let avatarUrl: string | undefined;
for (const accounts of this.groupedAccounts.values()) {
for (const account of accounts) {
if (account.avatarUrl) {
avatarUrl = account.avatarUrl;
break;
}
}
if (avatarUrl) {
break;
}
}

if (avatarUrl && this.label) {
this.currentAvatarUrl = avatarUrl;
this.applyAvatarToLabel(avatarUrl);
} else if (this.currentAvatarUrl && this.label) {
this.currentAvatarUrl = undefined;
this.label.classList.remove('has-avatar');
this.avatarElement?.remove();
}
}

private applyAvatarToLabel(avatarUrl: string): void {
this.label.classList.add('has-avatar');
if (!this.avatarElement) {
this.avatarElement = document.createElement('img');
this.avatarElement.className = 'accounts-avatar';
this.avatarElement.alt = '';
this.avatarElement.draggable = false;
}
if (this.avatarElement.src !== avatarUrl) {
this.avatarElement.src = avatarUrl;
}
if (!this.label.contains(this.avatarElement)) {
this.label.appendChild(this.avatarElement);
}
}

private registerListeners(): void {
this._register(this.authenticationService.onDidRegisterAuthenticationProvider(async (e) => {
await this.addAccountsFromProvider(e.id);
this.updateAvatar();
}));

this._register(this.authenticationService.onDidUnregisterAuthenticationProvider((e) => {
this.groupedAccounts.delete(e.id);
this.problematicProviders.delete(e.id);
this.updateAvatar();
}));

this._register(this.authenticationService.onDidChangeSessions(async e => {
Expand All @@ -321,6 +380,7 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction
this.logService.error(e);
}
}
this.updateAvatar();
}));
}

Expand Down Expand Up @@ -351,6 +411,7 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction
}

this.initialized = true;
this.updateAvatar();
}

//#region overrides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const INTERNAL_AUTH_PROVIDER_PREFIX = '__';
export interface AuthenticationSessionAccount {
label: string;
id: string;
avatarUrl?: string;
}

export interface AuthenticationSession {
Expand Down
5 changes: 5 additions & 0 deletions src/vscode-dts/vscode.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17852,6 +17852,11 @@ declare module 'vscode' {
* The human-readable name of the account.
*/
readonly label: string;

/**
* The optional avatar URL of the account.
*/
readonly avatarUrl?: string;
}

/**
Expand Down