diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 6126701924d..71975132f28 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -9,7 +9,7 @@ import { Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; import { isObject } from 'vs/base/common/types'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; @@ -1234,6 +1234,8 @@ export interface NewCommentAction { */ export interface CommentReaction { readonly label?: string; + readonly iconPath?: UriComponents; + readonly count?: number; readonly hasReacted?: boolean; readonly canEdit?: boolean; } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 9159859427b..d0a90d17e21 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -873,6 +873,8 @@ declare module 'vscode' { interface CommentReaction { readonly label?: string; + readonly iconPath?: string | Uri; + count?: number; readonly hasReacted?: boolean; } diff --git a/src/vs/workbench/api/node/extHostComments.ts b/src/vs/workbench/api/node/extHostComments.ts index dbaa39bf37a..7b79f1810f2 100644 --- a/src/vs/workbench/api/node/extHostComments.ts +++ b/src/vs/workbench/api/node/extHostComments.ts @@ -250,7 +250,13 @@ function convertFromComment(comment: modes.Comment): vscode.Comment { canEdit: comment.canEdit, canDelete: comment.canDelete, isDraft: comment.isDraft, - commentReactions: comment.commentReactions + commentReactions: comment.commentReactions ? comment.commentReactions.map(reaction => { + return { + label: reaction.label, + count: reaction.count, + hasReacted: reaction.hasReacted + }; + }) : undefined }; } @@ -273,6 +279,8 @@ function convertToComment(provider: vscode.DocumentCommentProvider | vscode.Work commentReactions: vscodeComment.commentReactions ? vscodeComment.commentReactions.map(reaction => { return { label: reaction.label, + iconPath: reaction.iconPath ? extHostTypeConverter.pathOrURIToURI(reaction.iconPath) : undefined, + count: reaction.count, hasReacted: reaction.hasReacted, canEdit: (reaction.hasReacted && providerCanDeleteReaction) || (!reaction.hasReacted && providerCanAddReaction) }; diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index 00c9c120a02..43e16f8e559 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -311,7 +311,7 @@ export function fromRangeOrRangeWithMessage(ranges: vscode.Range[] | vscode.Deco } } -function pathOrURIToURI(value: string | URI): URI { +export function pathOrURIToURI(value: string | URI): URI { if (typeof value === 'undefined') { return value; } diff --git a/src/vs/workbench/contrib/comments/electron-browser/commentNode.ts b/src/vs/workbench/contrib/comments/electron-browser/commentNode.ts index 85161998a60..5d5264b7570 100644 --- a/src/vs/workbench/contrib/comments/electron-browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/electron-browser/commentNode.ts @@ -9,7 +9,7 @@ import * as modes from 'vs/editor/common/modes'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionsOrientation, ActionItem, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Button } from 'vs/base/browser/ui/button/button'; -import { Action, IActionRunner } from 'vs/base/common/actions'; +import { Action, IActionRunner, IAction } from 'vs/base/common/actions'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ITextModel } from 'vs/editor/common/model'; @@ -32,10 +32,41 @@ import { assign } from 'vs/base/common/objects'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { DropdownMenuActionItem } from 'vs/base/browser/ui/dropdown/dropdown'; +import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; const UPDATE_COMMENT_LABEL = nls.localize('label.updateComment', "Update comment"); const UPDATE_IN_PROGRESS_LABEL = nls.localize('label.updatingComment', "Updating comment..."); +class ToggleReactionsAction extends Action { + + static readonly ID = 'toolbar.toggle.pickReactions'; + + private _menuActions: IAction[]; + private toggleDropdownMenu: () => void; + + constructor(toggleDropdownMenu: () => void, title?: string) { + title = title || nls.localize('pickReactions', "Pick Reactions..."); + super(ToggleReactionsAction.ID, title, 'toggle-reactions', true); + + this.toggleDropdownMenu = toggleDropdownMenu; + } + + run(): Promise { + this.toggleDropdownMenu(); + + return Promise.resolve(true); + } + + get menuActions() { + return this._menuActions; + } + + set menuActions(actions: IAction[]) { + this._menuActions = actions; + } +} + export class CommentNode extends Disposable { private _domNode: HTMLElement; private _body: HTMLElement; @@ -96,7 +127,7 @@ export class CommentNode extends Disposable { this._body.appendChild(this._md); if (this.comment.commentReactions && this.comment.commentReactions.length) { - this.createReactions(this._commentDetailsContainer); + this.createReactionsContainer(this._commentDetailsContainer); } this._domNode.setAttribute('aria-label', `${comment.userName}, ${comment.body.value}`); @@ -120,6 +151,13 @@ export class CommentNode extends Disposable { } const actions: Action[] = []; + + let reactionGroup = this.commentService.getReactionGroup(this.owner); + if (reactionGroup && reactionGroup.length) { + let toggleReactionAction = this.createReactionPicker(); + actions.push(toggleReactionAction); + } + if (this.comment.canEdit) { this._editAction = this.createEditAction(commentDetailsContainer); actions.push(this._editAction); @@ -134,37 +172,35 @@ export class CommentNode extends Disposable { const actionsContainer = dom.append(header, dom.$('.comment-actions.hidden')); this.toolbar = new ToolBar(actionsContainer, this.contextMenuService, { - actionItemProvider: action => this.actionItemProvider(action as Action), + actionItemProvider: action => { + if (action.id === ToggleReactionsAction.ID) { + return new DropdownMenuActionItem( + action, + (action).menuActions, + this.contextMenuService, + action => { + return this.actionItemProvider(action as Action); + }, + this.actionRunner, + undefined, + 'toolbar-toggle-pickReactions', + () => { return AnchorAlignment.RIGHT; } + ); + } + return this.actionItemProvider(action as Action); + }, orientation: ActionsOrientation.HORIZONTAL }); this.registerActionBarListeners(actionsContainer); - - let reactionActions: Action[] = []; - let reactionGroup = this.commentService.getReactionGroup(this.owner); - if (reactionGroup && reactionGroup.length) { - reactionActions = reactionGroup.map((reaction) => { - return new Action(`reaction.command.${reaction.label}`, `${reaction.label}`, '', true, async () => { - try { - await this.commentService.addReaction(this.owner, this.resource, this.comment, reaction); - } catch (e) { - const error = e.message - ? nls.localize('commentAddReactionError', "Deleting the comment reaction failed: {0}.", e.message) - : nls.localize('commentAddReactionDefaultError', "Deleting the comment reaction failed"); - this.notificationService.error(error); - } - }); - }); - } - - this.toolbar.setActions(actions, reactionActions)(); + this.toolbar.setActions(actions, [])(); this._toDispose.push(this.toolbar); } } actionItemProvider(action: Action) { let options = {}; - if (action.id === 'comment.delete' || action.id === 'comment.edit') { + if (action.id === 'comment.delete' || action.id === 'comment.edit' || action.id === ToggleReactionsAction.ID) { options = { label: false, icon: true }; } else { options = { label: true, icon: true }; @@ -174,13 +210,77 @@ export class CommentNode extends Disposable { return item; } - private createReactions(commentDetailsContainer: HTMLElement): void { + private createReactionPicker(): ToggleReactionsAction { + let toggleReactionActionItem: DropdownMenuActionItem; + let toggleReactionAction = this._register(new ToggleReactionsAction(() => { + if (toggleReactionActionItem) { + toggleReactionActionItem.show(); + } + }, 'Pick')); + + 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 () => { + try { + await this.commentService.addReaction(this.owner, this.resource, this.comment, reaction); + } catch (e) { + const error = e.message + ? nls.localize('commentAddReactionError', "Deleting the comment reaction failed: {0}.", e.message) + : nls.localize('commentAddReactionDefaultError', "Deleting the comment reaction failed"); + this.notificationService.error(error); + } + }); + }); + } + + toggleReactionAction.menuActions = reactionMenuActions; + + toggleReactionActionItem = new DropdownMenuActionItem( + toggleReactionAction, + (toggleReactionAction).menuActions, + this.contextMenuService, + action => { + if (action.id === ToggleReactionsAction.ID) { + return toggleReactionActionItem; + } + return this.actionItemProvider(action as Action); + }, + this.actionRunner, + undefined, + 'toolbar-toggle-pickReactions', + () => { return AnchorAlignment.RIGHT; } + ); + + return toggleReactionAction; + } + + private createReactionsContainer(commentDetailsContainer: HTMLElement): void { this._actionsContainer = dom.append(commentDetailsContainer, dom.$('div.comment-reactions')); - this._reactionsActionBar = new ActionBar(this._actionsContainer, {}); + this._reactionsActionBar = new ActionBar(this._actionsContainer, { + actionItemProvider: action => { + if (action.id === ToggleReactionsAction.ID) { + return new DropdownMenuActionItem( + action, + (action).menuActions, + this.contextMenuService, + action => { + return this.actionItemProvider(action as Action); + }, + this.actionRunner, + undefined, + 'toolbar-toggle-pickReactions', + () => { return AnchorAlignment.RIGHT; } + ); + } + return this.actionItemProvider(action as Action); + } + }); this._toDispose.push(this._reactionsActionBar); - let reactionActions = this.comment.commentReactions!.map(reaction => { - return new Action(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted && reaction.canEdit ? 'active' : '', reaction.canEdit, async () => { + this.comment.commentReactions!.map(reaction => { + let action = new Action(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted && reaction.canEdit ? 'active' : '', reaction.canEdit, async () => { try { if (reaction.hasReacted) { await this.commentService.deleteReaction(this.owner, this.resource, this.comment, reaction); @@ -202,9 +302,15 @@ export class CommentNode extends Disposable { this.notificationService.error(error); } }); + + this._reactionsActionBar.push(action, { label: true, icon: true }); }); - reactionActions.forEach(action => this._reactionsActionBar!.push(action, { label: true, icon: true })); + let reactionGroup = this.commentService.getReactionGroup(this.owner); + if (reactionGroup && reactionGroup.length) { + let toggleReactionAction = this.createReactionPicker(); + this._reactionsActionBar.push(toggleReactionAction, { label: false, icon: true }); + } } private createCommentEditor(): void { @@ -383,7 +489,7 @@ export class CommentNode extends Disposable { } if (this.comment.commentReactions && this.comment.commentReactions.length) { - this.createReactions(this._commentDetailsContainer); + this.createReactionsContainer(this._commentDetailsContainer); } } diff --git a/src/vs/workbench/contrib/comments/electron-browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/electron-browser/commentsEditorContribution.ts index 6b67c55dace..da9d0b0e6e7 100644 --- a/src/vs/workbench/contrib/comments/electron-browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/electron-browser/commentsEditorContribution.ts @@ -748,13 +748,11 @@ registerThemingParticipant((theme, collector) => { const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND); if (statusBarItemHoverBackground) { - collector.addRule(`.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.active:hover { background-color: ${statusBarItemHoverBackground}; border: 1px solid grey; - border-radius: 3px; }`); + collector.addRule(`.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.active:hover { background-color: ${statusBarItemHoverBackground}; border: 1px solid grey;}`); } const statusBarItemActiveBackground = theme.getColor(STATUS_BAR_ITEM_ACTIVE_BACKGROUND); if (statusBarItemActiveBackground) { - collector.addRule(`.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.active:active { background-color: ${statusBarItemActiveBackground}; border: 1px solid grey; - border-radius: 3px;}`); + collector.addRule(`.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.active { background-color: ${statusBarItemActiveBackground}; border: 1px solid grey;}`); } }); diff --git a/src/vs/workbench/contrib/comments/electron-browser/media/reaction-dark.svg b/src/vs/workbench/contrib/comments/electron-browser/media/reaction-dark.svg new file mode 100644 index 00000000000..b51dfbbd9b8 --- /dev/null +++ b/src/vs/workbench/contrib/comments/electron-browser/media/reaction-dark.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/vs/workbench/contrib/comments/electron-browser/media/reaction-hc.svg b/src/vs/workbench/contrib/comments/electron-browser/media/reaction-hc.svg new file mode 100644 index 00000000000..2c7fa126dd9 --- /dev/null +++ b/src/vs/workbench/contrib/comments/electron-browser/media/reaction-hc.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/vs/workbench/contrib/comments/electron-browser/media/reaction.svg b/src/vs/workbench/contrib/comments/electron-browser/media/reaction.svg new file mode 100644 index 00000000000..8696b8f2bab --- /dev/null +++ b/src/vs/workbench/contrib/comments/electron-browser/media/reaction.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/vs/workbench/contrib/comments/electron-browser/media/review.css b/src/vs/workbench/contrib/comments/electron-browser/media/review.css index fe06ac65a16..fcbe1d78620 100644 --- a/src/vs/workbench/contrib/comments/electron-browser/media/review.css +++ b/src/vs/workbench/contrib/comments/electron-browser/media/review.css @@ -153,7 +153,39 @@ .monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.active { border: 1px solid grey; - border-radius: 3px; +} + +.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.toolbar-toggle-pickReactions { + background-image: url(./reaction.svg); + width: 16px; + height: 16px; + background-repeat: no-repeat; + background-position: center; +} + +.monaco-editor.vs-dark .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.toolbar-toggle-pickReactions { + background-image: url(./reaction-dark.svg); +} + +.monaco-editor.hc-black .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.toolbar-toggle-pickReactions { + background-image: url(./reaction-hc.svg); +} + +.monaco-editor .review-widget .body .review-comment .comment-title .action-label.toolbar-toggle-pickReactions { + background-image: url(./reaction.svg); + width: 22px; + height: 18px; + background-size: 100% auto; + background-position: center; + background-repeat: no-repeat; +} + +.monaco-editor.vs-dark .review-widget .body .review-comment .comment-title .action-label.toolbar-toggle-pickReactions { + background-image: url(./reaction-dark.svg); +} + +.monaco-editor.hc-black .review-widget .body .review-comment .comment-title .action-label.toolbar-toggle-pickReactions { + background-image: url(./reaction-hc.svg); } .monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.disabled {