Browse Source

Adds live share presence support

main
Eric Amodio 5 years ago
parent
commit
751ad0ecfd
19 changed files with 241 additions and 22 deletions
  1. +4
    -0
      images/dark/icon-presence-away.svg
  2. +4
    -0
      images/dark/icon-presence-busy.svg
  3. +4
    -0
      images/dark/icon-presence-dnd.svg
  4. +4
    -0
      images/dark/icon-presence-offline.svg
  5. +4
    -0
      images/dark/icon-presence-online.svg
  6. +5
    -0
      images/dark/icon-vsls.svg
  7. +5
    -0
      images/light/icon-vsls.svg
  8. +25
    -2
      package.json
  9. +3
    -0
      src/annotations/annotations.ts
  10. +5
    -1
      src/annotations/blameAnnotationProvider.ts
  11. +1
    -0
      src/annotations/recentChangesAnnotationProvider.ts
  12. +1
    -0
      src/commands.ts
  13. +10
    -1
      src/commands/common.ts
  14. +43
    -0
      src/commands/inviteToLiveShare.ts
  15. +8
    -3
      src/constants.ts
  16. +41
    -8
      src/git/formatters/commitFormatter.ts
  17. +1
    -0
      src/hovers/lineHoverController.ts
  18. +12
    -6
      src/views/nodes/contributorNode.ts
  19. +61
    -1
      src/vsls/vsls.ts

+ 4
- 0
images/dark/icon-presence-away.svg View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="4" height="16" viewBox="0 0 4 16">
<circle cx="2" cy="14" r="2" fill="#cecece"/>
</svg>

+ 4
- 0
images/dark/icon-presence-busy.svg View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="4" height="16" viewBox="0 0 4 16">
<circle cx="2" cy="14" r="2" fill="#ca5628"/>
</svg>

+ 4
- 0
images/dark/icon-presence-dnd.svg View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="4" height="16" viewBox="0 0 4 16">
<circle cx="2" cy="14" r="2" fill="#ca5628"/>
</svg>

+ 4
- 0
images/dark/icon-presence-offline.svg View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="4" height="16" viewBox="0 0 4 16">
<circle cx="2" cy="14" r="2" fill="#cecece"/>
</svg>

+ 4
- 0
images/dark/icon-presence-online.svg View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="4" height="16" viewBox="0 0 4 16">
<circle cx="2" cy="14" r="2" fill="#28ca42"/>
</svg>

+ 5
- 0
images/dark/icon-vsls.svg View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 22">
<path fill="#89D185" d="M8 5l2.25 2.25L7 10.5 8.5 12l3.25-3.25L14 11V5H8z"/>
<path fill="#C5C5C5" d="M13 13h1v3c0 .55-.45 1-1 1H3c-.55 0-1-.45-1-1V6c0-.55.45-1 1-1h3v1H3v10h10v-3z"/>
</svg>

+ 5
- 0
images/light/icon-vsls.svg View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 22">
<path fill="#89D185" d="M8 5l2.25 2.25L7 10.5 8.5 12l3.25-3.25L14 11V5H8z"/>
<path fill="#424242" d="M13 13h1v3c0 .55-.45 1-1 1H3c-.55 0-1-.45-1-1V6c0-.55.45-1 1-1h3v1H3v10h10v-3z"/>
</svg>

+ 25
- 2
package.json View File

@ -593,7 +593,7 @@
},
"gitlens.hovers.detailsMarkdownFormat": {
"type": "string",
"default": "[${avatar} &nbsp;__${author}__](mailto:${email}), ${ago} &nbsp; _(${date})_ \n\n${message}\n\n${commands}",
"default": "${avatar} &nbsp;__${author}__, ${ago} &nbsp; _(${date})_ \n\n${message}\n\n${commands}",
"markdownDescription": "Specifies the format (in markdown) of the _commit details_ hover. See [_Commit Tokens_](https://github.com/eamodio/vscode-gitlens/wiki/Custom-Formatting#commit-tokens) in the GitLens docs",
"scope": "window"
},
@ -2291,6 +2291,15 @@
"category": "GitLens"
},
{
"command": "gitlens.inviteToLiveShare",
"title": "Invite to Live Share",
"category": "GitLens",
"icon": {
"dark": "images/dark/icon-vsls.svg",
"light": "images/light/icon-vsls.svg"
}
},
{
"command": "gitlens.views.exploreRepoRevision",
"title": "Explore Repository from Here",
"category": "GitLens"
@ -3219,6 +3228,10 @@
"when": "gitlens:enabled"
},
{
"command": "gitlens.inviteToLiveShare",
"when": "false"
},
{
"command": "gitlens.views.exploreRepoRevision",
"when": "false"
},
@ -4256,14 +4269,24 @@
"group": "8_gitlens_@1"
},
{
"command": "gitlens.inviteToLiveShare",
"when": "gitlens:vsls && gitlens:vsls != guest && viewItem =~ /gitlens:contributor\\b/",
"group": "inline@97"
},
{
"command": "gitlens.views.contributor.copyToClipboard",
"when": "viewItem =~ /gitlens:contributor\\b/",
"group": "inline@98"
},
{
"command": "gitlens.inviteToLiveShare",
"when": "gitlens:vsls && gitlens:vsls != guest && viewItem =~ /gitlens:contributor\\b/",
"group": "1_gitlens@1"
},
{
"command": "gitlens.views.contributor.copyToClipboard",
"when": "viewItem =~ /gitlens:contributor\\b/",
"group": "1_gitlens@1"
"group": "1_gitlens_1@1"
},
{
"command": "gitlens.views.contributor.addCoauthoredBy",

+ 3
- 0
src/annotations/annotations.ts View File

@ -23,6 +23,7 @@ import {
} from '../git/gitService';
import { Objects, Strings } from '../system';
import { toRgba } from '../webviews/apps/shared/colors';
import { ContactPresence } from '../vsls/vsls';
export interface ComputedHeatmap {
cold: boolean;
@ -85,6 +86,7 @@ export class Annotations {
static getHoverMessage(
commit: GitCommit,
dateFormat: string | null,
presence: ContactPresence | undefined,
remotes: GitRemote[],
annotationType?: FileAnnotationType,
line: number = 0
@ -99,6 +101,7 @@ export class Annotations {
dateFormat: dateFormat,
line: line,
markdown: true,
presence: presence,
remotes: remotes
})
);

+ 5
- 1
src/annotations/blameAnnotationProvider.ts View File

@ -41,7 +41,10 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase
}
clear() {
this._hoverProviderDisposable && this._hoverProviderDisposable.dispose();
if (this._hoverProviderDisposable !== undefined) {
this._hoverProviderDisposable.dispose();
this._hoverProviderDisposable = undefined;
}
super.clear();
}
@ -234,6 +237,7 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase
const message = Annotations.getHoverMessage(
logCommit || commit,
Container.config.defaultDateFormat,
await Container.vsls.getContactPresence(commit.email),
await Container.git.getRemotes(commit.repoPath),
this.annotationType,
editorLine

+ 1
- 0
src/annotations/recentChangesAnnotationProvider.ts View File

@ -64,6 +64,7 @@ export class RecentChangesAnnotationProvider extends AnnotationProviderBase {
hoverMessage: Annotations.getHoverMessage(
commit,
dateFormat,
await Container.vsls.getContactPresence(commit.email),
await Container.git.getRemotes(commit.repoPath),
this.annotationType,
count

+ 1
- 0
src/commands.ts View File

@ -17,6 +17,7 @@ export * from './commands/diffWithRef';
export * from './commands/diffWithRevision';
export * from './commands/diffWithWorking';
export * from './commands/externalDiff';
export * from './commands/inviteToLiveShare';
export * from './commands/openBranchesInRemote';
export * from './commands/openBranchInRemote';
export * from './commands/openChangedFiles';

+ 10
- 1
src/commands/common.ts View File

@ -16,7 +16,7 @@ import {
} from 'vscode';
import { BuiltInCommands, DocumentSchemes, ImageMimetypes } from '../constants';
import { Container } from '../container';
import { GitBranch, GitCommit, GitFile, GitRemote, GitUri, Repository } from '../git/gitService';
import { GitBranch, GitCommit, GitContributor, GitFile, GitRemote, GitUri, Repository } from '../git/gitService';
import { Logger } from '../logger';
import { CommandQuickPickItem, RepositoriesQuickPick } from '../quickpicks';
// import { Telemetry } from '../telemetry';
@ -53,6 +53,7 @@ export enum Commands {
DiffLineWithWorking = 'gitlens.diffLineWithWorking',
ExternalDiff = 'gitlens.externalDiff',
FetchRepositories = 'gitlens.fetchRepositories',
InviteToLiveShare = 'gitlens.inviteToLiveShare',
OpenChangedFiles = 'gitlens.openChangedFiles',
OpenBranchesInRemote = 'gitlens.openBranchesInRemote',
OpenBranchInRemote = 'gitlens.openBranchInRemote',
@ -244,6 +245,14 @@ export function isCommandViewContextWithCommit(
return (context.node as ViewNode & { commit: GitCommit }).commit instanceof GitCommit;
}
export function isCommandViewContextWithContributor(
context: CommandContext
): context is CommandViewItemContext & { node: ViewNode & { contributor: GitContributor } } {
if (context.type !== 'viewItem') return false;
return (context.node as ViewNode & { contributor: GitContributor }).contributor instanceof GitContributor;
}
export function isCommandViewContextWithFile(
context: CommandContext
): context is CommandViewItemContext & { node: ViewNode & { file: GitFile; repoPath: string } } {

+ 43
- 0
src/commands/inviteToLiveShare.ts View File

@ -0,0 +1,43 @@
'use strict';
import { command, Command, CommandContext, Commands, isCommandViewContextWithContributor } from './common';
import { Container } from '../container';
export interface InviteToLiveShareCommandArgs {
email?: string;
}
@command()
export class InviteToLiveShareCommand extends Command {
static getMarkdownCommandArgs(args: InviteToLiveShareCommandArgs): string;
static getMarkdownCommandArgs(email: string | undefined): string;
static getMarkdownCommandArgs(argsOrEmail: InviteToLiveShareCommandArgs | string | undefined): string {
const args =
argsOrEmail === undefined || typeof argsOrEmail === 'string' ? { email: argsOrEmail } : argsOrEmail;
return super.getMarkdownCommandArgsCore<InviteToLiveShareCommandArgs>(Commands.InviteToLiveShare, args);
}
constructor() {
super(Commands.InviteToLiveShare);
}
protected preExecute(context: CommandContext, args: InviteToLiveShareCommandArgs = {}) {
if (isCommandViewContextWithContributor(context)) {
args = { ...args };
args.email = context.node.contributor.email;
return this.execute(args);
}
return this.execute(args);
}
async execute(args: InviteToLiveShareCommandArgs = {}) {
if (args.email) {
const contact = await Container.vsls.getContact(args.email);
if (contact != null) {
return contact.invite();
}
}
return Container.vsls.startSession();
}
}

+ 8
- 3
src/constants.ts View File

@ -42,7 +42,8 @@ export enum CommandContext {
ViewsFileHistoryEditorFollowing = 'gitlens:views:fileHistory:editorFollowing',
ViewsLineHistoryEditorFollowing = 'gitlens:views:lineHistory:editorFollowing',
ViewsRepositoriesAutoRefresh = 'gitlens:views:repositories:autoRefresh',
ViewsSearchKeepResults = 'gitlens:views:search:keepResults'
ViewsSearchKeepResults = 'gitlens:views:search:keepResults',
Vsls = 'gitlens:vsls'
}
export function setCommandContext(key: CommandContext | string, value: any) {
@ -101,9 +102,13 @@ export enum GlyphChars {
Dot = '\u2022',
Ellipsis = '\u2026',
EnDash = '\u2013',
Envelope = '\u2709',
EqualsTriple = '\u2261',
Flag = '\u2691',
FlagHollow = '\u2690',
MiddleEllipsis = '\u22EF',
MuchGreaterThan = '\u226A',
MuchLessThan = '\u22D8',
MuchLessThan = '\u226A',
MuchGreaterThan = '\u226B',
Pencil = '\u270E',
Space = '\u00a0',
SpaceThin = '\u2009',

+ 41
- 8
src/git/formatters/commitFormatter.ts View File

@ -1,6 +1,7 @@
'use strict';
import {
DiffWithCommand,
InviteToLiveShareCommand,
OpenCommitInRemoteCommand,
OpenFileRevisionCommand,
ShowQuickCommitDetailsCommand,
@ -15,6 +16,7 @@ import { GitCommit, GitCommitType } from '../models/commit';
import { GitLogCommit, GitRemote } from '../models/models';
import { FormatOptions, Formatter } from './formatter';
import * as emojis from '../../emojis.json';
import { ContactPresence } from '../../vsls/vsls';
const emptyStr = '';
const emojiMap: { [key: string]: string } = emojis;
@ -29,6 +31,7 @@ export interface CommitFormatOptions extends FormatOptions {
dateStyle?: DateStyle;
line?: number;
markdown?: boolean;
presence?: ContactPresence;
remotes?: GitRemote[];
truncateMessageAtNewLine?: boolean;
@ -99,7 +102,12 @@ export class CommitFormatter extends Formatter {
}
get author() {
return this._padOrTruncate(this._item.author, this._options.tokenOptions.author);
const author = this._padOrTruncate(this._item.author, this._options.tokenOptions.author);
if (!this._options.markdown) {
return author;
}
return `[${author}](mailto:${this._item.email} "Email ${this._item.author} (${this._item.email})")`;
}
get authorAgo() {
@ -119,7 +127,21 @@ export class CommitFormatter extends Formatter {
return emptyStr;
}
return `![](${this._item.getGravatarUri(Container.config.defaultGravatarsStyle).toString(true)})`;
let avatar = `![](${this._item.getGravatarUri(Container.config.defaultGravatarsStyle).toString(true)})`;
const presence = this._options.presence;
if (presence != null) {
const title = `${this._item.author} ${this._item.author === 'You' ? 'are' : 'is'} ${
presence.status === 'dnd' ? 'in ' : ''
}${presence.statusText.toLocaleLowerCase()}`;
avatar += `![${title}](${encodeURI(
`file:///${Container.context.asAbsolutePath(`images/dark/icon-presence-${presence.status}.svg`)}`
)})`;
avatar = `[${avatar}](# "${title}")`;
}
return avatar;
}
get changes() {
@ -152,10 +174,12 @@ export class CommitFormatter extends Formatter {
let commands = `[\`${this.id}\`](${ShowQuickCommitDetailsCommand.getMarkdownCommandArgs(
this._item.sha
)} "Show Commit Details") [\`${GlyphChars.MuchGreaterThan}\`](${DiffWithCommand.getMarkdownCommandArgs(
)} "Show Commit Details") `;
commands += `**[\`${GlyphChars.MuchLessThan}\`](${DiffWithCommand.getMarkdownCommandArgs(
this._item,
this._options.line
)} "Open Changes") `;
)} "Open Changes")** `;
if (this._item.previousSha !== undefined) {
let annotationType = this._options.annotationType;
@ -168,17 +192,26 @@ export class CommitFormatter extends Formatter {
this._item.previousUri.fsPath,
this._item.repoPath
);
commands += `[\`${GlyphChars.SquareWithTopShadow}\`](${OpenFileRevisionCommand.getMarkdownCommandArgs(
commands += `**[\` ${GlyphChars.EqualsTriple} \`](${OpenFileRevisionCommand.getMarkdownCommandArgs(
uri,
annotationType || FileAnnotationType.Blame,
this._options.line
)} "Blame Previous Revision") `;
)} "Blame Previous Revision")** `;
}
if (this._options.remotes !== undefined && this._options.remotes.length !== 0) {
commands += `[\`${GlyphChars.ArrowUpRight}\`](${OpenCommitInRemoteCommand.getMarkdownCommandArgs(
commands += `**[\` ${GlyphChars.ArrowUpRight} \`](${OpenCommitInRemoteCommand.getMarkdownCommandArgs(
this._item.sha
)} "Open in Remote") `;
)} "Open in Remote")** `;
}
if (this._item.author !== 'You') {
const presence = this._options.presence;
if (presence != null) {
commands += `[\` ${GlyphChars.Envelope}+ \`](${InviteToLiveShareCommand.getMarkdownCommandArgs(
this._item.email
)} "Invite ${this._item.author} (${presence.statusText}) to a Live Share Session") `;
}
}
commands += `[\`${GlyphChars.MiddleEllipsis}\`](${ShowQuickCommitFileDetailsCommand.getMarkdownCommandArgs(

+ 1
- 0
src/hovers/lineHoverController.ts View File

@ -118,6 +118,7 @@ export class LineHoverController implements Disposable {
const message = Annotations.getHoverMessage(
logCommit || commit,
Container.config.defaultDateFormat,
await Container.vsls.getContactPresence(commit.email),
await Container.git.getRemotes(commit.repoPath),
fileAnnotations,
editorLine

+ 12
- 6
src/views/nodes/contributorNode.ts View File

@ -8,6 +8,7 @@ import { Container } from '../../container';
import { MessageNode, ShowMoreNode } from './common';
import { getBranchesAndTagTipsFn, insertDateMarkers } from './helpers';
import { CommitNode } from './commitNode';
import { GlyphChars } from '../../constants';
export class ContributorNode extends ViewNode<RepositoriesView> implements PageableViewNode {
readonly supportsPaging: boolean = true;
@ -47,15 +48,20 @@ export class ContributorNode extends ViewNode implements Pagea
return children;
}
getTreeItem(): TreeItem {
async getTreeItem(): Promise<TreeItem> {
const presence = await Container.vsls.getContactPresence(this.contributor.email);
const item = new TreeItem(this.contributor.name, TreeItemCollapsibleState.Collapsed);
item.id = this.id;
item.contextValue = ResourceType.Contributor;
item.description = this.contributor.email;
item.tooltip = `${this.contributor.name} <${this.contributor.email}>\n${Strings.pluralize(
'commit',
this.contributor.count
)}`;
item.description = `${
presence != null && presence.status !== 'offline'
? `${presence.statusText} ${GlyphChars.Space}${GlyphChars.Dot}${GlyphChars.Space} `
: ''
}${this.contributor.email}`;
item.tooltip = `${this.contributor.name}${presence != null ? ` (${presence.statusText})` : ''}\n${
this.contributor.email
}\n${Strings.pluralize('commit', this.contributor.count)}`;
if (this.view.config.avatars) {
item.iconPath = this.contributor.getGravatarUri(Container.config.defaultGravatarsStyle);

+ 61
- 1
src/vsls/vsls.ts View File

@ -10,6 +10,12 @@ import { VslsHostService } from './host';
export const vslsUriPrefixRegex = /^[/|\\]~(?:\d+?|external)(?:[/|\\]|$)/;
export const vslsUriRootRegex = /^[/|\\]~(?:\d+?|external)$/;
export interface ContactPresence {
status: ContactPresenceStatus;
statusText: string;
}
export type ContactPresenceStatus = 'online' | 'away' | 'busy' | 'dnd' | 'offline';
export class VslsController implements Disposable {
private _disposable: Disposable | undefined;
private _guest: VslsGuestService | undefined;
@ -18,6 +24,8 @@ export class VslsController implements Disposable {
private _onReady: (() => void) | undefined;
private _waitForReady: Promise<void> | undefined;
private _api: Promise<LiveShare | null> | undefined;
constructor() {
void this.initialize();
}
@ -44,16 +52,21 @@ export class VslsController implements Disposable {
this._waitForReady = new Promise(resolve => (this._onReady = resolve));
}
const api = await getApi();
this._api = getApi();
const api = await this._api;
if (api == null) {
setCommandContext(CommandContext.Vsls, false);
// Tear it down if we can't talk to live share
if (this._onReady !== undefined) {
this._onReady();
this._waitForReady = undefined;
}
return;
}
setCommandContext(CommandContext.Vsls, true);
this._disposable = Disposable.from(
api.onDidChangeSession(e => this.onLiveShareSessionChanged(api, e), this)
);
@ -67,6 +80,50 @@ export class VslsController implements Disposable {
return this._guest !== undefined || this._waitForReady !== undefined;
}
async getContact(email: string | undefined) {
if (email === undefined) return undefined;
const api = await this._api;
if (api == null) return undefined;
const contacts = await api.getContacts([email]);
return contacts.contacts[email];
}
async getContactPresence(email: string | undefined): Promise<ContactPresence | undefined> {
const contact = await this.getContact(email);
if (contact == null) return undefined;
switch (contact.status) {
case 'available':
return { status: 'online', statusText: 'Available' };
case 'away':
return { status: 'away', statusText: 'Away' };
case 'busy':
return { status: 'busy', statusText: 'Busy' };
case 'doNotDisturb':
return { status: 'dnd', statusText: 'DND' };
default:
return { status: 'offline', statusText: 'Offline' };
}
}
async invite(email: string | undefined) {
if (email == null) return undefined;
const contact = await this.getContact(email);
if (contact == null) return undefined;
return contact.invite();
}
async startSession() {
const api = await this._api;
if (api == null) return undefined;
return api.share();
}
async guest() {
if (this._waitForReady !== undefined) {
await this._waitForReady;
@ -92,17 +149,20 @@ export class VslsController implements Disposable {
switch (e.session.role) {
case Role.Host:
setCommandContext(CommandContext.Readonly, undefined);
setCommandContext(CommandContext.Vsls, 'host');
if (Container.config.liveshare.allowGuestAccess) {
this._host = await VslsHostService.share(api);
}
break;
case Role.Guest:
setCommandContext(CommandContext.Readonly, true);
setCommandContext(CommandContext.Vsls, 'guest');
this._guest = await VslsGuestService.connect(api);
break;
default:
setCommandContext(CommandContext.Readonly, undefined);
setCommandContext(CommandContext.Vsls, true);
break;
}

Loading…
Cancel
Save