From 22009b9ba4c1a6c0fea970c5853b305851c2b035 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Wed, 14 Sep 2022 02:23:10 -0400 Subject: [PATCH] Adds lazy loading for avatars on the Graph --- package.json | 2 +- src/avatars.ts | 77 ++++++++++++------- src/env/node/git/localGitProvider.ts | 20 +++-- src/git/gitProviderService.ts | 6 +- src/git/models/commit.ts | 12 ++- src/git/models/graph.ts | 2 + src/plus/github/githubGitProvider.ts | 26 ++++++- src/plus/webviews/graph/graphWebview.ts | 66 +++++++++++++++- src/plus/webviews/graph/protocol.ts | 17 +++- src/webviews/apps/plus/graph/GraphWrapper.tsx | 13 +++- src/webviews/apps/plus/graph/graph.tsx | 26 +++++-- yarn.lock | 8 +- 12 files changed, 217 insertions(+), 58 deletions(-) diff --git a/package.json b/package.json index 214d2c1b655b6..10ce0beae4530 100644 --- a/package.json +++ b/package.json @@ -11680,7 +11680,7 @@ "vscode:prepublish": "yarn run bundle" }, "dependencies": { - "@gitkraken/gitkraken-components": "1.0.0-rc.13", + "@gitkraken/gitkraken-components": "1.0.0-rc.14", "@microsoft/fast-element": "^1.10.5", "@octokit/core": "4.0.5", "@vscode/codicons": "0.0.32", diff --git a/src/avatars.ts b/src/avatars.ts index 0f21b715137b7..53f85726c9d23 100644 --- a/src/avatars.ts +++ b/src/avatars.ts @@ -1,8 +1,8 @@ import { EventEmitter, Uri } from 'vscode'; import { GravatarDefaultStyle } from './config'; -import { configuration } from './configuration'; +import { ContextKeys } from './constants'; import { Container } from './container'; -import type { GitRevisionReference } from './git/models/reference'; +import { getContext } from './context'; import { getGitHubNoReplyAddressParts } from './git/remotes/github'; import type { StoredAvatar } from './storage'; import { debounce } from './system/function'; @@ -69,19 +69,41 @@ const retryDecay = [ export function getAvatarUri( email: string | undefined, - repoPathOrCommit: undefined, + repoPathOrCommit?: undefined, options?: { defaultStyle?: GravatarDefaultStyle; size?: number }, ): Uri; export function getAvatarUri( email: string | undefined, - repoPathOrCommit: string | GitRevisionReference, + repoPathOrCommit: string | { ref: string; repoPath: string }, options?: { defaultStyle?: GravatarDefaultStyle; size?: number }, ): Uri | Promise; export function getAvatarUri( email: string | undefined, - repoPathOrCommit: string | GitRevisionReference | undefined, + repoPathOrCommit: string | { ref: string; repoPath: string } | undefined, options?: { defaultStyle?: GravatarDefaultStyle; size?: number }, ): Uri | Promise { + return getAvatarUriCore(email, repoPathOrCommit, options); +} + +export function getCachedAvatarUri(email: string | undefined, options?: { size?: number }): Uri | undefined { + return getAvatarUriCore(email, undefined, { ...options, cached: true }); +} + +function getAvatarUriCore( + email: string | undefined, + repoPathOrCommit: string | { ref: string; repoPath: string } | undefined, + options?: { cached: true; defaultStyle?: GravatarDefaultStyle; size?: number }, +): Uri | undefined; +function getAvatarUriCore( + email: string | undefined, + repoPathOrCommit: string | { ref: string; repoPath: string } | undefined, + options?: { defaultStyle?: GravatarDefaultStyle; size?: number }, +): Uri | Promise; +function getAvatarUriCore( + email: string | undefined, + repoPathOrCommit: string | { ref: string; repoPath: string } | undefined, + options?: { cached?: boolean; defaultStyle?: GravatarDefaultStyle; size?: number }, +): Uri | Promise | undefined { ensureAvatarCache(avatarCache); // Double the size to avoid blurring on the retina screen @@ -104,20 +126,22 @@ export function getAvatarUri( const avatar = createOrUpdateAvatar(key, email, size, hash, options?.defaultStyle); if (avatar.uri != null) return avatar.uri; - let query = avatarQueue.get(key); - if (query == null && repoPathOrCommit != null && hasAvatarExpired(avatar)) { - query = getAvatarUriFromRemoteProvider(avatar, key, email, repoPathOrCommit, { size: size }).then( - uri => uri ?? avatar.uri ?? avatar.fallback!, - ); - avatarQueue.set( - key, - query.finally(() => avatarQueue.delete(key)), - ); - } + if (!options?.cached && repoPathOrCommit != null && getContext(ContextKeys.HasConnectedRemotes)) { + let query = avatarQueue.get(key); + if (query == null && hasAvatarExpired(avatar)) { + query = getAvatarUriFromRemoteProvider(avatar, key, email, repoPathOrCommit, { size: size }).then( + uri => uri ?? avatar.uri ?? avatar.fallback!, + ); + avatarQueue.set( + key, + query.finally(() => avatarQueue.delete(key)), + ); + } - if (query != null) return query; + return query ?? avatar.fallback!; + } - return avatar.uri ?? avatar.fallback!; + return options?.cached ? avatar.uri : avatar.uri ?? avatar.fallback!; } function createOrUpdateAvatar( @@ -183,23 +207,22 @@ async function getAvatarUriFromRemoteProvider( avatar: Avatar, key: string, email: string, - repoPathOrCommit: string | GitRevisionReference, + repoPathOrCommit: string | { ref: string; repoPath: string }, { size = 16 }: { size?: number } = {}, ) { ensureAvatarCache(avatarCache); try { let account; - if (configuration.get('integrations.enabled')) { - // if (typeof repoPathOrCommit === 'string') { - // const remote = await Container.instance.git.getRichRemoteProvider(repoPathOrCommit); - // account = await remote?.provider.getAccountForEmail(email, { avatarSize: size }); - // } else { - if (typeof repoPathOrCommit !== 'string') { - const remote = await Container.instance.git.getBestRemoteWithRichProvider(repoPathOrCommit.repoPath); - account = await remote?.provider.getAccountForCommit(repoPathOrCommit.ref, { avatarSize: size }); - } + // if (typeof repoPathOrCommit === 'string') { + // const remote = await Container.instance.git.getRichRemoteProvider(repoPathOrCommit); + // account = await remote?.provider.getAccountForEmail(email, { avatarSize: size }); + // } else { + if (typeof repoPathOrCommit !== 'string') { + const remote = await Container.instance.git.getBestRemoteWithRichProvider(repoPathOrCommit.repoPath); + account = await remote?.provider.getAccountForCommit(repoPathOrCommit.ref, { avatarSize: size }); } + if (account?.avatarUrl == null) { // If we have no account assume that won't change (without a reset), so set the timestamp to "never expire" avatar.uri = undefined; diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index b0335edc505bd..163756c5683c0 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -12,7 +12,7 @@ import type { Repository as BuiltInGitRepository, GitExtension, } from '../../../@types/vscode.git'; -import { getAvatarUri } from '../../../avatars'; +import { getCachedAvatarUri } from '../../../avatars'; import { configuration } from '../../../configuration'; import { CoreGitConfiguration, GlyphChars, Schemes } from '../../../constants'; import type { Container } from '../../../container'; @@ -1635,7 +1635,6 @@ export class LocalGitProvider implements GitProvider, Disposable { const remotes = getSettledValue(remotesResult); const remoteMap = remotes != null ? new Map(remotes.map(r => [r.name, r])) : new Map(); const selectSha = first(refParser.parse(getSettledValue(refResult) ?? '')); - const skipStashParents = new Set(); let stdin: string | undefined; // TODO@eamodio this is insanity -- there *HAS* to be a better way to get git log to return stashes @@ -1647,7 +1646,9 @@ export class LocalGitProvider implements GitProvider, Disposable { ); } + const avatars = new Map(); const ids = new Set(); + const skipStashParents = new Set(); let total = 0; let iterations = 0; @@ -1698,7 +1699,9 @@ export class LocalGitProvider implements GitProvider, Disposable { : data.indexOf(`\x00\x00${cursor.sha}\x00`); if (cursorIndex === -1) { // If we didn't find any new commits, we must have them all so return that we have everything - if (size === data.length) return { repoPath: repoPath, ids: ids, rows: [] }; + if (size === data.length) { + return { repoPath: repoPath, avatars: avatars, ids: ids, rows: [] }; + } size = data.length; nextPageLimit = (nextPageLimit === 0 ? defaultPageLimit : nextPageLimit) * 2; @@ -1723,7 +1726,7 @@ export class LocalGitProvider implements GitProvider, Disposable { } } - if (!data) return { repoPath: repoPath, ids: ids, rows: [] }; + if (!data) return { repoPath: repoPath, avatars: avatars, ids: ids, rows: [] }; log = data; if (limit !== 0) { @@ -1818,11 +1821,17 @@ export class LocalGitProvider implements GitProvider, Disposable { parents.splice(1, 2); } + if (!isStashCommit && !avatars.has(commit.authorEmail)) { + const uri = getCachedAvatarUri(commit.authorEmail); + if (uri != null) { + avatars.set(commit.authorEmail, uri.toString(true)); + } + } + rows.push({ sha: commit.sha, parents: parents, author: commit.author, - avatarUrl: !isStashCommit ? getAvatarUri(commit.authorEmail, undefined).toString(true) : undefined, email: commit.authorEmail ?? '', date: Number(ordering === 'author-date' ? commit.authorDate : commit.committerDate) * 1000, message: emojify(commit.message), @@ -1850,6 +1859,7 @@ export class LocalGitProvider implements GitProvider, Disposable { return { repoPath: repoPath, + avatars: avatars, ids: ids, rows: rows, sha: sha, diff --git a/src/git/gitProviderService.ts b/src/git/gitProviderService.ts index 49919d26a3c3c..2231667b80d8e 100644 --- a/src/git/gitProviderService.ts +++ b/src/git/gitProviderService.ts @@ -201,6 +201,10 @@ export class GitProviderService implements Disposable { if (configuration.changed(e, 'views.contributors.showAllBranches')) { this.resetCaches('contributors'); } + + if (e != null && configuration.changed(e, 'integrations.enabled')) { + this.updateContext(); + } } @debug() @@ -739,7 +743,7 @@ export class GitProviderService implements Disposable { let hasRemotes = false; let hasRichRemotes = false; let hasConnectedRemotes = false; - if (hasRepositories) { + if (hasRepositories && configuration.get('integrations.enabled')) { for (const repo of this._repositories.values()) { if (!hasConnectedRemotes) { hasConnectedRemotes = await repo.hasRichRemote(true); diff --git a/src/git/models/commit.ts b/src/git/models/commit.ts index 535b6c6d83a3c..085344bcf3e96 100644 --- a/src/git/models/commit.ts +++ b/src/git/models/commit.ts @@ -1,5 +1,5 @@ import { Uri } from 'vscode'; -import { getAvatarUri } from '../../avatars'; +import { getAvatarUri, getCachedAvatarUri } from '../../avatars'; import type { GravatarDefaultStyle } from '../../configuration'; import { DateSource, DateStyle } from '../../configuration'; import { GlyphChars } from '../../constants'; @@ -437,6 +437,10 @@ export class GitCommit implements GitRevisionReference { return this.author.getAvatarUri(this, options); } + getCachedAvatarUri(options?: { size?: number }): Uri | undefined { + return this.author.getCachedAvatarUri(options); + } + async getCommitForFile(file: string | GitFile): Promise { const path = typeof file === 'string' ? this.container.git.getRelativePath(file, this.repoPath) : file.path; const foundFile = await this.findFile(path); @@ -616,9 +620,11 @@ export class GitCommitIdentity implements GitCommitIdentityShape { commit: GitCommit, options?: { defaultStyle?: GravatarDefaultStyle; size?: number }, ): Uri | Promise { - if (this.avatarUrl != null) return Uri.parse(this.avatarUrl); + return this.avatarUrl != null ? Uri.parse(this.avatarUrl) : getAvatarUri(this.email, commit, options); + } - return getAvatarUri(this.email, commit, options); + getCachedAvatarUri(options?: { size?: number }): Uri | undefined { + return this.avatarUrl != null ? Uri.parse(this.avatarUrl) : getCachedAvatarUri(this.email, options); } } diff --git a/src/git/models/graph.ts b/src/git/models/graph.ts index 60ac2176ff723..bf5772515b404 100644 --- a/src/git/models/graph.ts +++ b/src/git/models/graph.ts @@ -21,6 +21,8 @@ export interface GitGraphRow extends GraphRow { export interface GitGraph { readonly repoPath: string; + /** A map of all avatar urls */ + readonly avatars: Map; /** A set of all "seen" commit ids */ readonly ids: Set; /** The rows for the set of commits requested */ diff --git a/src/plus/github/githubGitProvider.ts b/src/plus/github/githubGitProvider.ts index 8000606faef61..7fb092b0f50f7 100644 --- a/src/plus/github/githubGitProvider.ts +++ b/src/plus/github/githubGitProvider.ts @@ -1075,6 +1075,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { this.getTags(repoPath), ]); + const avatars = new Map(); const ids = new Set(); return this.getCommitsForGraphCore( @@ -1084,6 +1085,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { getSettledValue(branchResult), getSettledValue(remotesResult)?.[0], getSettledValue(tagsResult)?.values, + avatars, ids, options, ); @@ -1096,6 +1098,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { branch: GitBranch | undefined, remote: GitRemote | undefined, tags: GitTag[] | undefined, + avatars: Map, ids: Set, options?: { branch?: string; @@ -1107,6 +1110,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { if (log == null) { return { repoPath: repoPath, + avatars: avatars, ids: ids, rows: [], }; @@ -1116,6 +1120,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { if (commits == null) { return { repoPath: repoPath, + avatars: avatars, ids: ids, rows: [], }; @@ -1169,11 +1174,17 @@ export class GitHubGitProvider implements GitProvider, Disposable { refTags = []; } + if (commit.author.email && !avatars.has(commit.author.email)) { + const uri = commit.getCachedAvatarUri(); + if (uri != null) { + avatars.set(commit.author.email, uri.toString(true)); + } + } + rows.push({ sha: commit.sha, parents: commit.parents, author: commit.author.name, - avatarUrl: (await commit.getAvatarUri())?.toString(true), email: commit.author.email ?? '', date: commit.committer.date.getTime(), message: emojify(commit.message && String(commit.message).length ? commit.message : commit.summary), @@ -1193,6 +1204,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { return { repoPath: repoPath, + avatars: avatars, ids: ids, rows: rows, sha: options?.ref, @@ -1205,7 +1217,17 @@ export class GitHubGitProvider implements GitProvider, Disposable { }, more: async (limit: number | { until: string } | undefined): Promise => { const moreLog = await log.more?.(limit); - return this.getCommitsForGraphCore(repoPath, asWebviewUri, moreLog, branch, remote, tags, ids, options); + return this.getCommitsForGraphCore( + repoPath, + asWebviewUri, + moreLog, + branch, + remote, + tags, + avatars, + ids, + options, + ); }, }; } diff --git a/src/plus/webviews/graph/graphWebview.ts b/src/plus/webviews/graph/graphWebview.ts index 578ba294a29de..cd2a7b01244a8 100644 --- a/src/plus/webviews/graph/graphWebview.ts +++ b/src/plus/webviews/graph/graphWebview.ts @@ -1,5 +1,6 @@ import type { ColorTheme, ConfigurationChangeEvent, Disposable, Event, StatusBarItem } from 'vscode'; import { EventEmitter, MarkdownString, ProgressLocation, StatusBarAlignment, ViewColumn, window } from 'vscode'; +import { getAvatarUri } from '../../../avatars'; import { parseCommandContext } from '../../../commands/base'; import { GitActions } from '../../../commands/gitCommands.actions'; import type { GraphColumnConfig } from '../../../configuration'; @@ -28,12 +29,14 @@ import type { SubscriptionChangeEvent } from '../../subscription/subscriptionSer import { ensurePlusFeaturesEnabled } from '../../subscription/utils'; import type { DismissBannerParams, GraphCompositeConfig, GraphRepository, State } from './protocol'; import { + DidChangeAvatarsNotificationType, DidChangeCommitsNotificationType, DidChangeGraphConfigurationNotificationType, DidChangeNotificationType, DidChangeSelectionNotificationType, DidChangeSubscriptionNotificationType, DismissBannerCommandType, + GetMissingAvatarsCommandType, GetMoreCommitsCommandType, UpdateColumnCommandType, UpdateSelectedRepositoryCommandType, @@ -181,8 +184,11 @@ export class GraphWebview extends WebviewBase { case DismissBannerCommandType.method: onIpc(DismissBannerCommandType, e, params => this.dismissBanner(params.key)); break; + case GetMissingAvatarsCommandType.method: + onIpc(GetMissingAvatarsCommandType, e, params => this.onGetMissingAvatars(params.emails)); + break; case GetMoreCommitsCommandType.method: - onIpc(GetMoreCommitsCommandType, e, params => this.onGetMoreCommits(params.limit)); + onIpc(GetMoreCommitsCommandType, e, () => this.onGetMoreCommits()); break; case UpdateColumnCommandType.method: onIpc(UpdateColumnCommandType, e, params => this.onColumnUpdated(params.name, params.config)); @@ -308,8 +314,32 @@ export class GraphWebview extends WebviewBase { void this.notifyDidChangeGraphConfiguration(); } + private async onGetMissingAvatars(emails: { [email: string]: string }) { + if (this._graph == null) return; + + const repoPath = this._graph.repoPath; + + async function getAvatar(this: GraphWebview, email: string, sha: string) { + const uri = await getAvatarUri(email, { ref: sha, repoPath: repoPath }); + this._graph!.avatars.set(email, uri.toString(true)); + } + + const promises: Promise[] = []; + + for (const [email, sha] of Object.entries(emails)) { + if (this._graph.avatars.has(email)) continue; + + promises.push(getAvatar.call(this, email, sha)); + } + + if (promises.length) { + await Promise.allSettled(promises); + this.updateAvatars(); + } + } + @gate() - private async onGetMoreCommits(limit?: number) { + private async onGetMoreCommits() { if (this._graph?.more == null || this._repository?.etag !== this._etagRepository) { this.updateState(true); @@ -317,7 +347,7 @@ export class GraphWebview extends WebviewBase { } const { defaultItemLimit, pageItemLimit } = this.getConfig(); - const newGraph = await this._graph.more(limit ?? pageItemLimit ?? defaultItemLimit); + const newGraph = await this._graph.more(pageItemLimit ?? defaultItemLimit); if (newGraph != null) { this.setGraph(newGraph); } else { @@ -375,6 +405,24 @@ export class GraphWebview extends WebviewBase { this._notifyDidChangeStateDebounced(); } + private _notifyDidChangeAvatarsDebounced: Deferrable<() => void> | undefined = undefined; + + @debug() + private updateAvatars(immediate: boolean = false) { + if (!this.isReady || !this.visible) return; + + if (immediate) { + void this.notifyDidChangeAvatars(); + return; + } + + if (this._notifyDidChangeAvatarsDebounced == null) { + this._notifyDidChangeAvatarsDebounced = debounce(this.notifyDidChangeAvatars.bind(this), 100); + } + + this._notifyDidChangeAvatarsDebounced(); + } + @debug() private async notifyDidChangeState() { if (!this.isReady || !this.visible) return false; @@ -415,6 +463,16 @@ export class GraphWebview extends WebviewBase { }); } + @debug() + private async notifyDidChangeAvatars() { + if (!this.isReady || !this.visible) return false; + + const data = this._graph!; + return this.notify(DidChangeAvatarsNotificationType, { + avatars: Object.fromEntries(data.avatars), + }); + } + @debug() private async notifyDidChangeCommits() { if (!this.isReady || !this.visible) return false; @@ -422,6 +480,7 @@ export class GraphWebview extends WebviewBase { const data = this._graph!; return this.notify(DidChangeCommitsNotificationType, { rows: data.rows, + avatars: Object.fromEntries(data.avatars), paging: { startingCursor: data.paging?.startingCursor, more: data.paging?.more ?? false, @@ -486,6 +545,7 @@ export class GraphWebview extends WebviewBase { selectedVisibility: visibility, subscription: access.subscription.current, allowed: access.allowed, + avatars: Object.fromEntries(data.avatars), rows: data.rows, paging: { startingCursor: data.paging?.startingCursor, diff --git a/src/plus/webviews/graph/protocol.ts b/src/plus/webviews/graph/protocol.ts index 9f7223905aa21..391915b2e57fb 100644 --- a/src/plus/webviews/graph/protocol.ts +++ b/src/plus/webviews/graph/protocol.ts @@ -12,6 +12,7 @@ export interface State { selectedRows?: { [id: string]: true }; subscription?: Subscription; allowed?: boolean; + avatars?: { [email: string]: string }; rows?: GraphRow[]; paging?: GraphPaging; config?: GraphCompositeConfig; @@ -68,10 +69,12 @@ export interface DismissBannerParams { } export const DismissBannerCommandType = new IpcCommandType('graph/dismissBanner'); -export interface GetMoreCommitsParams { - limit?: number; +export interface GetMissingAvatarsParams { + emails: { [email: string]: string }; } -export const GetMoreCommitsCommandType = new IpcCommandType('graph/getMoreCommits'); +export const GetMissingAvatarsCommandType = new IpcCommandType('graph/getMissingAvatars'); + +export const GetMoreCommitsCommandType = new IpcCommandType('graph/getMoreCommits'); export interface UpdateColumnParams { name: string; @@ -112,8 +115,16 @@ export const DidChangeSubscriptionNotificationType = new IpcNotificationType( + 'graph/avatars/didChange', +); + export interface DidChangeCommitsParams { rows: GraphRow[]; + avatars: { [email: string]: string }; paging?: GraphPaging; } export const DidChangeCommitsNotificationType = new IpcNotificationType( diff --git a/src/webviews/apps/plus/graph/GraphWrapper.tsx b/src/webviews/apps/plus/graph/GraphWrapper.tsx index 53cc2ac315e24..29bf4b268b4b4 100644 --- a/src/webviews/apps/plus/graph/GraphWrapper.tsx +++ b/src/webviews/apps/plus/graph/GraphWrapper.tsx @@ -28,7 +28,8 @@ export interface GraphWrapperProps extends State { subscriber: (callback: CommitListCallback) => () => void; onSelectRepository?: (repository: GraphRepository) => void; onColumnChange?: (name: string, settings: GraphColumnConfig) => void; - onMoreCommits?: (limit?: number) => void; + onMissingAvatars?: (emails: { [email: string]: string }) => void; + onMoreCommits?: () => void; onDismissBanner?: (key: DismissBannerParams['key']) => void; onSelectionChange?: (selection: { id: string; type: GitGraphRowType }[]) => void; } @@ -140,10 +141,12 @@ export function GraphWrapper({ subscription, selectedVisibility, allowed, + avatars, config, paging, onSelectRepository, onColumnChange, + onMissingAvatars, onMoreCommits, onSelectionChange, nonce, @@ -153,6 +156,7 @@ export function GraphWrapper({ onDismissBanner, }: GraphWrapperProps) { const [graphList, setGraphList] = useState(rows); + const [graphAvatars, setAvatars] = useState(avatars); const [reposList, setReposList] = useState(repositories); const [currentRepository, setCurrentRepository] = useState( reposList.find(item => item.path === selectedRepository), @@ -201,6 +205,7 @@ export function GraphWrapper({ function transformData(state: State) { setGraphList(state.rows ?? []); + setAvatars(state.avatars ?? {}); setReposList(state.repositories ?? []); setCurrentRepository(reposList.find(item => item.path === state.selectedRepository)); setSelectedRows(state.selectedRows); @@ -233,6 +238,10 @@ export function GraphWrapper({ setRepoExpanded(!repoExpanded); }; + const handleMissingAvatars = (emails: { [email: string]: string }) => { + onMissingAvatars?.(emails); + }; + const handleMoreCommits = () => { setIsLoading(true); onMoreCommits?.(); @@ -432,6 +441,7 @@ export function GraphWrapper({ columnsSettings={graphColSettings} cssVariables={styleProps.cssVariables} getExternalIcon={getIconElementLibrary} + avatarUrlByEmail={graphAvatars} graphRows={graphList} height={mainHeight} isSelectedBySha={graphSelectedRows} @@ -440,6 +450,7 @@ export function GraphWrapper({ nonce={nonce} onColumnResized={handleOnColumnResized} onSelectGraphRows={handleSelectGraphRows} + onEmailsMissingAvatarUrls={handleMissingAvatars} onShowMoreCommits={handleMoreCommits} platform={clientPlatform} width={mainWidth} diff --git a/src/webviews/apps/plus/graph/graph.tsx b/src/webviews/apps/plus/graph/graph.tsx index 77f798e3016b6..d0e59bdefebcc 100644 --- a/src/webviews/apps/plus/graph/graph.tsx +++ b/src/webviews/apps/plus/graph/graph.tsx @@ -11,12 +11,14 @@ import type { State, } from '../../../../plus/webviews/graph/protocol'; import { + DidChangeAvatarsNotificationType, DidChangeCommitsNotificationType, DidChangeGraphConfigurationNotificationType, DidChangeNotificationType, DidChangeSelectionNotificationType, DidChangeSubscriptionNotificationType, DismissBannerCommandType, + GetMissingAvatarsCommandType, GetMoreCommitsCommandType, UpdateColumnCommandType, UpdateSelectedRepositoryCommandType as UpdateRepositorySelectionCommandType, @@ -68,6 +70,7 @@ export class GraphApp extends App { (path: GraphRepository) => this.onRepositorySelectionChanged(path), 250, )} + onMissingAvatars={(...params) => this.onGetMissingAvatars(...params)} onMoreCommits={(...params) => this.onGetMoreCommits(...params)} onSelectionChange={debounce( (selection: { id: string; type: GitGraphRowType }[]) => this.onSelectionChanged(selection), @@ -98,6 +101,13 @@ export class GraphApp extends App { }); break; + case DidChangeAvatarsNotificationType.method: + onIpc(DidChangeAvatarsNotificationType, msg, params => { + this.setState({ ...this.state, avatars: params.avatars }); + this.refresh(this.state); + }); + break; + case DidChangeCommitsNotificationType.method: onIpc(DidChangeCommitsNotificationType, msg, params => { let rows; @@ -164,6 +174,7 @@ export class GraphApp extends App { this.setState({ ...this.state, + avatars: params.avatars, rows: rows, paging: params.paging, }); @@ -180,10 +191,7 @@ export class GraphApp extends App { case DidChangeGraphConfigurationNotificationType.method: onIpc(DidChangeGraphConfigurationNotificationType, msg, params => { - this.setState({ - ...this.state, - config: params.config, - }); + this.setState({ ...this.state, config: params.config }); this.refresh(this.state); }); break; @@ -261,10 +269,12 @@ export class GraphApp extends App { }); } - private onGetMoreCommits(limit?: number) { - this.sendCommand(GetMoreCommitsCommandType, { - limit: limit, - }); + private onGetMissingAvatars(emails: { [email: string]: string }) { + this.sendCommand(GetMissingAvatarsCommandType, { emails: emails }); + } + + private onGetMoreCommits() { + this.sendCommand(GetMoreCommitsCommandType, undefined); } private onSelectionChanged(selection: { id: string; type: GitGraphRowType }[]) { diff --git a/yarn.lock b/yarn.lock index e2ba7af38c23e..28d6531c43b85 100644 --- a/yarn.lock +++ b/yarn.lock @@ -80,10 +80,10 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== -"@gitkraken/gitkraken-components@1.0.0-rc.13": - version "1.0.0-rc.13" - resolved "https://registry.yarnpkg.com/@gitkraken/gitkraken-components/-/gitkraken-components-1.0.0-rc.13.tgz#9bfb5872dfa027bbf69ac94874b31fbe223d4784" - integrity sha512-QaMe9FF9kUnt4KGYzEdhNm8JsLqguE8/yDJzEJD4y1Y5gbe7CmldnTjmrRZQehJiEMrWs/sfDuBygJOZG893eg== +"@gitkraken/gitkraken-components@1.0.0-rc.14": + version "1.0.0-rc.14" + resolved "https://registry.yarnpkg.com/@gitkraken/gitkraken-components/-/gitkraken-components-1.0.0-rc.14.tgz#2aa57a9c88654bc772e70a6305ab8f1f408e1b6b" + integrity sha512-pCtVtsU1uNiVEjS/bUoIBqMkDAM7apJGZUvZm9ynoERZM8ZRt883nol8MpfZkid8cMaw91FBdzu3MfxSDAFi1Q== dependencies: "@axosoft/react-virtualized" "9.22.3-gitkraken.3" classnames "2.2.3"