diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index d03f3edf9a2..f5be21364a0 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -9054,6 +9054,31 @@ declare module 'vscode' { iconPath?: Uri; } + /** + * Reactions of a [comment](#Comment) + */ + export interface CommentReaction { + /** + * The human-readable label for the reaction + */ + readonly label: string; + + /** + * Icon for the reaction shown in UI. + */ + readonly iconPath: string | Uri; + + /** + * The number of users who have reacted to this reaction + */ + readonly count: number; + + /** + * Whether the [author](CommentAuthorInformation) of the comment has reacted to this reaction + */ + readonly authorHasReacted: boolean; + } + /** * A comment is displayed within the editor or the Comments Panel, depending on how it is provided. */ @@ -9093,6 +9118,11 @@ declare module 'vscode' { */ contextValue?: string; + /** + * Optional reactions of the [comment](#Comment) + */ + reactions?: CommentReaction[]; + /** * Optional label describing the [Comment](#Comment) * Label will be rendered next to authorName if exists. @@ -9157,6 +9187,11 @@ declare module 'vscode' { */ createCommentThread(uri: Uri, range: Range, comments: Comment[]): CommentThread; + /** + * Optional reaction handler for creating and deleting reactions on a [comment](#Comment). + */ + reactionHandler?: (comment: Comment, reaction: CommentReaction) => Promise; + /** * Dispose this comment controller. * diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 966803bd86c..317a06d6a5b 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -839,9 +839,6 @@ declare module 'vscode' { * Stay in proposed. */ interface CommentReaction { - readonly label?: string; - readonly iconPath?: string | Uri; - count?: number; readonly hasReacted?: boolean; } diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index 43bcbaa54e0..444ace69064 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -284,6 +284,9 @@ export class MainThreadCommentController { private readonly _threads: Map = new Map(); public activeCommentThread?: MainThreadCommentThread; + get features(): CommentProviderFeatures { + return this._features; + } constructor( private readonly _proxy: ExtHostCommentsShape, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 2403367f755..876d67ea636 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -130,6 +130,7 @@ export interface CommentProviderFeatures { finishDraftLabel?: string; reactionGroup?: modes.CommentReaction[]; commentThreadTemplate?: CommentThreadTemplate; + reactionHandler?: boolean; } export interface MainThreadCommentsShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index d10cb9a5325..adfbeb4f0d7 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -220,7 +220,7 @@ export class ExtHostComments implements ExtHostCommentsShape { const document = this._documents.getDocument(URI.revive(uri)); const commentController = this._commentControllers.get(commentControllerHandle); - if (!commentController || !commentController.reactionProvider || !commentController.reactionProvider.toggleReaction) { + if (!commentController || !((commentController.reactionProvider && commentController.reactionProvider.toggleReaction) || commentController.reactionHandler)) { return Promise.resolve(undefined); } @@ -229,9 +229,16 @@ export class ExtHostComments implements ExtHostCommentsShape { if (commentThread) { const vscodeComment = commentThread.getComment(comment.commentId); - if (commentController !== undefined && commentController.reactionProvider && commentController.reactionProvider.toggleReaction && vscodeComment) { - return commentController.reactionProvider.toggleReaction(document, vscodeComment, convertFromReaction(reaction)); + if (commentController !== undefined && vscodeComment) { + if (commentController.reactionHandler) { + return commentController.reactionHandler(vscodeComment, convertFromReaction(reaction)); + } + + if (commentController.reactionProvider && commentController.reactionProvider.toggleReaction) { + return commentController.reactionProvider.toggleReaction(document, vscodeComment, convertFromReaction(reaction)); + } } + } return Promise.resolve(undefined); @@ -647,7 +654,7 @@ export class ExtHostCommentThread implements vscode.CommentThread { } getComment(commentId: string): vscode.Comment | undefined { - const comments = this._comments.filter(comment => comment.commentId === commentId); + const comments = this._comments.filter(comment => (comment.commentId === commentId || comment.id === commentId)); if (comments && comments.length) { return comments[0]; @@ -719,6 +726,9 @@ export class ExtHostCommentInputBox implements vscode.CommentInputBox { this._value = input; } } + +type ReactionHandler = (comment: vscode.Comment, reaction: vscode.CommentReaction) => Promise; + class ExtHostCommentController implements vscode.CommentController { get id(): string { return this._id; @@ -752,6 +762,18 @@ class ExtHostCommentController implements vscode.CommentController { } } + private _reactionHandler?: ReactionHandler; + + get reactionHandler(): ReactionHandler | undefined { + return this._reactionHandler; + } + + set reactionHandler(handler: ReactionHandler | undefined) { + this._reactionHandler = handler; + + this._proxy.$updateCommentControllerFeatures(this.handle, { reactionHandler: !!handler }); + } + constructor( private _extension: IExtensionDescription, private _handle: number, @@ -880,9 +902,12 @@ function convertFromComment(comment: modes.Comment): vscode.Comment { isDraft: comment.isDraft, commentReactions: comment.commentReactions ? comment.commentReactions.map(reaction => { return { - label: reaction.label, - count: reaction.count, - hasReacted: reaction.hasReacted + label: reaction.label || '', + count: reaction.count || 0, + iconPath: reaction.iconPath ? URI.revive(reaction.iconPath) : '', + hasReacted: reaction.hasReacted, + authorHasReacted: reaction.hasReacted || false + }; }) : undefined, mode: comment.mode ? comment.mode : modes.CommentMode.Preview @@ -897,6 +922,7 @@ function convertToModeComment2(thread: ExtHostCommentThread, commentController: } const iconPath = vscodeComment.author && vscodeComment.author.iconPath ? vscodeComment.author.iconPath.toString() : undefined; + const reactions = vscodeComment.reactions || vscodeComment.commentReactions; return { commentId: vscodeComment.id || vscodeComment.commentId, @@ -911,7 +937,7 @@ function convertToModeComment2(thread: ExtHostCommentThread, commentController: editCommand: vscodeComment.editCommand ? commandsConverter.toInternal(vscodeComment.editCommand) : undefined, deleteCommand: vscodeComment.deleteCommand ? commandsConverter.toInternal(vscodeComment.deleteCommand) : undefined, label: vscodeComment.label, - commentReactions: vscodeComment.commentReactions ? vscodeComment.commentReactions.map(reaction => convertToReaction2(commentController.reactionProvider, reaction)) : undefined + commentReactions: reactions ? reactions.map(reaction => convertToReaction2(commentController.reactionProvider, reaction)) : undefined }; } @@ -958,9 +984,11 @@ function convertToReaction2(provider: vscode.CommentReactionProvider | undefined function convertFromReaction(reaction: modes.CommentReaction): vscode.CommentReaction { return { - label: reaction.label, - count: reaction.count, - hasReacted: reaction.hasReacted + label: reaction.label || '', + count: reaction.count || 0, + iconPath: reaction.iconPath ? URI.revive(reaction.iconPath) : '', + hasReacted: reaction.hasReacted, + authorHasReacted: reaction.hasReacted || false }; } diff --git a/src/vs/workbench/browser/web.simpleservices.ts b/src/vs/workbench/browser/web.simpleservices.ts index 073cc1fc3a2..cc5570593b5 100644 --- a/src/vs/workbench/browser/web.simpleservices.ts +++ b/src/vs/workbench/browser/web.simpleservices.ts @@ -540,6 +540,7 @@ export class SimpleCommentService implements ICommentService { addReaction: any; deleteReaction: any; getReactionGroup: any; + hasReactionHandler: any; toggleReaction: any; setActiveCommentThread: any; } diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 09e2426a914..63c9f7e5968 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -119,7 +119,7 @@ export class CommentNode extends Disposable { this._md = this.markdownRenderer.render(comment.body).element; this._body.appendChild(this._md); - if (this.comment.commentReactions && this.comment.commentReactions.length) { + if (this.comment.commentReactions && this.comment.commentReactions.length && this.comment.commentReactions.filter(reaction => !!reaction.count).length) { this.createReactionsContainer(this._commentDetailsContainer); } @@ -154,15 +154,23 @@ export class CommentNode extends Disposable { private createActionsToolbar() { const actions: IAction[] = []; - let reactionGroup = this.commentService.getReactionGroup(this.owner); - if (reactionGroup && reactionGroup.length) { - let commentThread = this.commentThread as modes.CommentThread2; - if (commentThread.commentThreadHandle !== undefined) { - let toggleReactionAction = this.createReactionPicker2(); - actions.push(toggleReactionAction); - } else { - let toggleReactionAction = this.createReactionPicker(); - actions.push(toggleReactionAction); + let hasReactionHandler = this.commentService.hasReactionHandler(this.owner); + + if (hasReactionHandler) { + let toggleReactionAction = this.createReactionPicker2(this.comment.commentReactions || []); + actions.push(toggleReactionAction); + } else { + let reactionGroup = this.commentService.getReactionGroup(this.owner); + if (reactionGroup && reactionGroup.length) { + let commentThread = this.commentThread as modes.CommentThread2; + if (commentThread.commentThreadHandle !== undefined) { + let reactionGroup = this.commentService.getReactionGroup(this.owner); + let toggleReactionAction = this.createReactionPicker2(reactionGroup || []); + actions.push(toggleReactionAction); + } else { + let toggleReactionAction = this.createReactionPicker(); + actions.push(toggleReactionAction); + } } } @@ -241,7 +249,7 @@ export class CommentNode extends Disposable { } } - private createReactionPicker2(): ToggleReactionsAction { + private createReactionPicker2(reactionGroup: modes.CommentReaction[]): ToggleReactionsAction { let toggleReactionActionViewItem: DropdownMenuActionViewItem; let toggleReactionAction = this._register(new ToggleReactionsAction(() => { if (toggleReactionActionViewItem) { @@ -250,7 +258,6 @@ export class CommentNode extends Disposable { }, nls.localize('commentToggleReaction', "Toggle Reaction"))); let reactionMenuActions: Action[] = []; - let reactionGroup = this.commentService.getReactionGroup(this.owner); if (reactionGroup && reactionGroup.length) { reactionMenuActions = reactionGroup.map((reaction) => { return new Action(`reaction.command.${reaction.label}`, `${reaction.label}`, '', true, async () => { @@ -356,8 +363,9 @@ export class CommentNode extends Disposable { }); this._register(this._reactionsActionBar); - this.comment.commentReactions!.map(reaction => { - let action = new ReactionAction(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted && reaction.canEdit ? 'active' : '', reaction.canEdit, async () => { + let hasReactionHandler = this.commentService.hasReactionHandler(this.owner); + this.comment.commentReactions!.filter(reaction => !!reaction.count).map(reaction => { + let action = new ReactionAction(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted && (reaction.canEdit || hasReactionHandler) ? 'active' : '', (reaction.canEdit || hasReactionHandler), async () => { try { let commentThread = this.commentThread as modes.CommentThread2; if (commentThread.commentThreadHandle !== undefined) { @@ -390,15 +398,20 @@ export class CommentNode extends Disposable { } }); - let reactionGroup = this.commentService.getReactionGroup(this.owner); - if (reactionGroup && reactionGroup.length) { - let commentThread = this.commentThread as modes.CommentThread2; - if (commentThread.commentThreadHandle !== undefined) { - let toggleReactionAction = this.createReactionPicker2(); - this._reactionsActionBar.push(toggleReactionAction, { label: false, icon: true }); - } else { - let toggleReactionAction = this.createReactionPicker(); - this._reactionsActionBar.push(toggleReactionAction, { label: false, icon: true }); + if (hasReactionHandler) { + let toggleReactionAction = this.createReactionPicker2(this.comment.commentReactions || []); + this._reactionsActionBar.push(toggleReactionAction, { label: false, icon: true }); + } else { + let reactionGroup = this.commentService.getReactionGroup(this.owner); + if (reactionGroup && reactionGroup.length) { + let commentThread = this.commentThread as modes.CommentThread2; + if (commentThread.commentThreadHandle !== undefined) { + let toggleReactionAction = this.createReactionPicker2(reactionGroup || []); + this._reactionsActionBar.push(toggleReactionAction, { label: false, icon: true }); + } else { + let toggleReactionAction = this.createReactionPicker(); + this._reactionsActionBar.push(toggleReactionAction, { label: false, icon: true }); + } } } } diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index 3ddfb902491..cc20345b198 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -70,6 +70,7 @@ export interface ICommentService { addReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise; deleteReaction(owner: string, resource: URI, comment: Comment, reaction: CommentReaction): Promise; getReactionGroup(owner: string): CommentReaction[] | undefined; + hasReactionHandler(owner: string): boolean; toggleReaction(owner: string, resource: URI, thread: CommentThread2, comment: Comment, reaction: CommentReaction): Promise; setActiveCommentThread(commentThread: CommentThread | null): void; } @@ -315,6 +316,16 @@ export class CommentService extends Disposable implements ICommentService { return undefined; } + hasReactionHandler(owner: string): boolean { + const commentProvider = this._commentControls.get(owner); + + if (commentProvider) { + return !!commentProvider.features.reactionHandler; + } + + return false; + } + getStartDraftLabel(owner: string): string | undefined { const commentProvider = this._commentProviders.get(owner);