From 24675409fcabf1cba798fec226353bc7953ac019 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Tue, 15 Feb 2022 10:55:17 +0100 Subject: [PATCH] Comments view refresh (#140979) Fixes #142081 --- .../comments/browser/commentsTreeViewer.ts | 136 +++++++++++++----- .../contrib/comments/browser/commentsView.ts | 1 + .../contrib/comments/browser/media/panel.css | 43 +++++- .../contrib/comments/browser/timestamp.ts | 4 +- 4 files changed, 140 insertions(+), 44 deletions(-) diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 473b6f842cac1..e291c7d32dee3 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -18,16 +18,19 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { WorkbenchAsyncDataTree, IListService, IWorkbenchAsyncDataTreeOptions } from 'vs/platform/list/browser/listService'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IColorMapping } from 'vs/platform/theme/common/styler'; +import { TimestampWidget } from 'vs/workbench/contrib/comments/browser/timestamp'; +import { Codicon } from 'vs/base/common/codicons'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; export const COMMENTS_VIEW_ID = 'workbench.panel.comments'; export const COMMENTS_VIEW_TITLE = 'Comments'; export class CommentsAsyncDataSource implements IAsyncDataSource { hasChildren(element: any): boolean { - return element instanceof CommentsModel || element instanceof ResourceWithCommentThreads || (element instanceof CommentNode && !!element.replies.length); + return (element instanceof CommentsModel || element instanceof ResourceWithCommentThreads) && !(element instanceof CommentNode); } getChildren(element: any): any[] | Promise { @@ -37,9 +40,6 @@ export class CommentsAsyncDataSource implements IAsyncDataSource { if (element instanceof ResourceWithCommentThreads) { return Promise.resolve(element.commentThreads); } - if (element instanceof CommentNode) { - return Promise.resolve(element.replies); - } return Promise.resolve([]); } } @@ -49,9 +49,21 @@ interface IResourceTemplateData { } interface ICommentThreadTemplateData { - icon: HTMLImageElement; - userName: HTMLSpanElement; - commentText: HTMLElement; + threadMetadata: { + icon?: HTMLElement; + userNames: HTMLSpanElement; + timestamp: TimestampWidget; + separator: HTMLElement; + commentPreview: HTMLSpanElement; + }; + repliesMetadata: { + container: HTMLElement; + icon: HTMLElement; + count: HTMLSpanElement; + lastReplyDetail: HTMLSpanElement; + separator: HTMLElement; + timestamp: TimestampWidget; + }; disposables: IDisposable[]; } @@ -61,6 +73,9 @@ export class CommentsModelVirualDelegate implements IListVirtualDelegate { getHeight(element: any): number { + if ((element instanceof CommentNode) && element.hasReply()) { + return 44; + } return 22; } @@ -105,50 +120,97 @@ export class CommentNodeRenderer implements IListRenderer templateId: string = 'comment-node'; constructor( - @IOpenerService private readonly openerService: IOpenerService + @IOpenerService private readonly openerService: IOpenerService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { } renderTemplate(container: HTMLElement) { const data = Object.create(null); - const labelContainer = dom.append(container, dom.$('.comment-container')); - data.userName = dom.append(labelContainer, dom.$('.user')); - data.commentText = dom.append(labelContainer, dom.$('.text')); - data.disposables = []; + + const threadContainer = dom.append(container, dom.$('.comment-thread-container')); + const metadataContainer = dom.append(threadContainer, dom.$('.comment-metadata-container')); + data.threadMetadata = { + icon: dom.append(metadataContainer, dom.$('.icon')), + userNames: dom.append(metadataContainer, dom.$('.user')), + timestamp: new TimestampWidget(this.configurationService, dom.append(metadataContainer, dom.$('.timestamp-container'))), + separator: dom.append(metadataContainer, dom.$('.separator')), + commentPreview: dom.append(metadataContainer, dom.$('.text')) + }; + data.threadMetadata.separator.innerText = '\u00b7'; + + const snippetContainer = dom.append(threadContainer, dom.$('.comment-snippet-container')); + data.repliesMetadata = { + container: snippetContainer, + icon: dom.append(snippetContainer, dom.$('.icon')), + count: dom.append(snippetContainer, dom.$('.count')), + lastReplyDetail: dom.append(snippetContainer, dom.$('.reply-detail')), + separator: dom.append(snippetContainer, dom.$('.separator')), + timestamp: new TimestampWidget(this.configurationService, dom.append(snippetContainer, dom.$('.timestamp-container'))), + }; + data.repliesMetadata.separator.innerText = '\u00b7'; + data.repliesMetadata.icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.indent)); + data.disposables = [data.threadMetadata.timestamp, data.repliesMetadata.timestamp]; return data; } + private getCountString(commentCount: number): string { + if (commentCount > 1) { + return nls.localize('commentsCount', "{0} comments", commentCount); + } else { + return nls.localize('commentCount', "1 comment"); + } + } + + private getRenderedComment(commentBody: IMarkdownString, disposables: DisposableStore) { + const renderedComment = renderMarkdown(commentBody, { + inline: true, + actionHandler: { + callback: (content) => { + this.openerService.open(content, { allowCommands: commentBody.isTrusted }).catch(onUnexpectedError); + }, + disposables: disposables + } + }); + const images = renderedComment.element.getElementsByTagName('img'); + for (let i = 0; i < images.length; i++) { + const image = images[i]; + const textDescription = dom.$(''); + textDescription.textContent = image.alt ? nls.localize('imageWithLabel', "Image: {0}", image.alt) : nls.localize('image', "Image"); + image.parentNode!.replaceChild(textDescription, image); + } + return renderedComment; + } + renderElement(node: ITreeNode, index: number, templateData: ICommentThreadTemplateData, height: number | undefined): void { - templateData.userName.textContent = node.element.comment.userName; - templateData.commentText.innerText = ''; - if (typeof node.element.comment.body === 'string') { - templateData.commentText.innerText = node.element.comment.body; + const commentCount = node.element.replies.length + 1; + templateData.threadMetadata.icon?.classList.add(...ThemeIcon.asClassNameArray((commentCount === 1) ? Codicon.comment : Codicon.commentDiscussion)); + templateData.threadMetadata.userNames.textContent = node.element.comment.userName; + templateData.threadMetadata.timestamp.setTimestamp(node.element.comment.timestamp ? new Date(node.element.comment.timestamp) : undefined); + const originalComment = node.element; + + templateData.threadMetadata.commentPreview.innerText = ''; + if (typeof originalComment.comment.body === 'string') { + templateData.threadMetadata.commentPreview.innerText = originalComment.comment.body; } else { - const commentBody = node.element.comment.body; const disposables = new DisposableStore(); templateData.disposables.push(disposables); - const renderedComment = renderMarkdown(commentBody, { - inline: true, - actionHandler: { - callback: (content) => { - this.openerService.open(content, { allowCommands: commentBody.isTrusted }).catch(onUnexpectedError); - }, - disposables: disposables - } - }); + const renderedComment = this.getRenderedComment(originalComment.comment.body, disposables); templateData.disposables.push(renderedComment); + templateData.threadMetadata.commentPreview.appendChild(renderedComment.element); + templateData.threadMetadata.commentPreview.title = renderedComment.element.textContent ?? ''; + } - const images = renderedComment.element.getElementsByTagName('img'); - for (let i = 0; i < images.length; i++) { - const image = images[i]; - const textDescription = dom.$(''); - textDescription.textContent = image.alt ? nls.localize('imageWithLabel', "Image: {0}", image.alt) : nls.localize('image', "Image"); - image.parentNode!.replaceChild(textDescription, image); - } - - templateData.commentText.appendChild(renderedComment.element); - templateData.commentText.title = renderedComment.element.textContent ?? ''; + if (!node.element.hasReply()) { + templateData.repliesMetadata.container.style.display = 'none'; + return; } + + templateData.repliesMetadata.container.style.display = ''; + templateData.repliesMetadata.count.textContent = this.getCountString(commentCount); + templateData.repliesMetadata.lastReplyDetail.textContent = nls.localize('lastReplyFrom', "Last reply from {0}", node.element.replies[node.element.replies.length - 1].comment.userName); + templateData.repliesMetadata.timestamp.setTimestamp(originalComment.comment.timestamp ? new Date(originalComment.comment.timestamp) : undefined); + } disposeTemplate(templateData: ICommentThreadTemplateData): void { diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index 9e582d4a0577a..de0206fb60cb7 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -68,6 +68,7 @@ export class CommentsPanel extends ViewPane { let domContainer = dom.append(container, dom.$('.comments-panel-container')); this.treeContainer = dom.append(domContainer, dom.$('.tree-container')); + this.treeContainer.classList.add('file-icon-themable-tree', 'show-file-icons'); this.commentsModel = new CommentsModel(); this.createTree(); diff --git a/src/vs/workbench/contrib/comments/browser/media/panel.css b/src/vs/workbench/contrib/comments/browser/media/panel.css index b931b6cdb357e..fbd5f7e9cc75c 100644 --- a/src/vs/workbench/contrib/comments/browser/media/panel.css +++ b/src/vs/workbench/contrib/comments/browser/media/panel.css @@ -20,31 +20,57 @@ visibility: hidden; } +.comments-panel .comments-panel-container .tree-container .comment-thread-container { + display: block; +} + .comments-panel .comments-panel-container .tree-container .resource-container, -.comments-panel .comments-panel-container .tree-container .comment-container { +.comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-metadata-container, +.comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-snippet-container { display: flex; } +.comments-panel .count, .comments-panel .user { padding-right: 5px; - opacity: 0.5; } -.comments-panel .comments-panel-container .tree-container .comment-container .text { +.comments-panel .comments-panel-container .tree-container .comment-thread-container .icon { + padding-top: 4px; + padding-right: 5px; +} + +.comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-metadata-container .count, +.comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-snippet-container .text { + display: flex; flex: 1; min-width: 0; } -.comments-panel .comments-panel-container .tree-container .comment-container .text * { +.comments-panel .comments-panel-container .tree-container .comment-thread-container .reply-detail, +.comments-panel .comments-panel-container .tree-container .comment-thread-container .timestamp { + display:flex; + font-size: 0.9em; + padding-right: 5px; + opacity: 0.8; +} + +.comments-panel .comments-panel-container .tree-container .comment-thread-container .text * { margin: 0; text-overflow: ellipsis; + max-width: 500px; overflow: hidden; } -.comments-panel .comments-panel-container .tree-container .comment-container .text code { +.comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-snippet-container .text code { font-family: var(--monaco-monospace-font); } +.comments-panel .comments-panel-container .tree-container .comment-thread-container .separator { + padding-right: 5px; + opacity: 0.8; +} + .comments-panel .comments-panel-container .message-box-container { line-height: 22px; padding-left: 20px; @@ -55,7 +81,12 @@ margin-left: 10px; } -.comments-panel .comments-panel-container .tree-container .comment-container { +.comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-metadata-container, +.comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-snippet-container { line-height: 22px; margin-right: 5px; } + +.comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-snippet-container { + padding-left: 16px; +} diff --git a/src/vs/workbench/contrib/comments/browser/timestamp.ts b/src/vs/workbench/contrib/comments/browser/timestamp.ts index 802cf8cb698a9..ad7ae7fbd38c2 100644 --- a/src/vs/workbench/contrib/comments/browser/timestamp.ts +++ b/src/vs/workbench/contrib/comments/browser/timestamp.ts @@ -18,6 +18,7 @@ export class TimestampWidget extends Disposable { constructor(private configurationService: IConfigurationService, container: HTMLElement, timeStamp?: Date) { super(); this._date = dom.append(container, dom.$('span.timestamp')); + this._date.style.display = 'none'; this._useRelativeTime = this.useRelativeTimeSetting; this.setTimestamp(timeStamp); } @@ -37,9 +38,10 @@ export class TimestampWidget extends Disposable { private updateDate(timestamp?: Date) { if (!timestamp) { this._date.textContent = ''; + this._date.style.display = 'none'; } else if ((timestamp !== this._timestamp) || (this.useRelativeTimeSetting !== this._useRelativeTime)) { - + this._date.style.display = ''; let textContent: string; let tooltip: string | undefined; if (this.useRelativeTimeSetting) {