diff --git a/resources/icons/gutter-comment-unresolved.svg b/resources/icons/gutter-comment-unresolved.svg new file mode 100644 index 0000000..36c1219 --- /dev/null +++ b/resources/icons/gutter-comment-unresolved.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/gutter-edit.svg b/resources/icons/gutter-edit.svg new file mode 100644 index 0000000..552fbf9 --- /dev/null +++ b/resources/icons/gutter-edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/gutter-pass.svg b/resources/icons/gutter-pass.svg new file mode 100644 index 0000000..6a6c314 --- /dev/null +++ b/resources/icons/gutter-pass.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/api/extendedBase.ts b/src/api/extendedBase.ts index 1da9b38..6d395fa 100644 --- a/src/api/extendedBase.ts +++ b/src/api/extendedBase.ts @@ -12,6 +12,14 @@ export interface UrlLinkedFileProvider { url: string, } +export interface CommentThreadSchema { + messages: CommentThreadMessageSchema[], + resolved?: boolean, + resolved_at?: string, //ISO format date string + resolved_by_user_id?: string, + resolved_by_user?: UserInfoSchema, +} + export interface CommentThreadMessageSchema { id: string, content: string, @@ -22,32 +30,28 @@ export interface CommentThreadMessageSchema { user: UserInfoSchema, } +export interface DocumentReviewSchema { + id: string, + metadata: {user_id:string, ts:string}, //"ts" is ISO format date string +} + +export interface DocumentReviewChangeSchema extends DocumentReviewSchema { + op: {p:number, i?:string, d?:string}, +} + +export interface DocumentReviewCommentSchema { + op: {p:number, c:string, t:string}, // "c" for quoted text, "t" for thread_id + thread?: CommentThreadSchema, // to be filled in via API call +} + export interface DocumentRangesSchema { - changes: { - id: string, - op: {p:number, i?:string, d?:string}, - metadata: {user_id:string, ts:string}, //"ts" is ISO format date string - }[], - comments: { - id: string, - // "c" for quoted text, "t" for threadId - op: {p:number, c:string, t:string}, - metadata: {user_id:string, ts:string}, //"ts" is ISO format date string - }[], + changes: DocumentReviewChangeSchema[], + comments: DocumentReviewCommentSchema[], } export interface ExtendedResponseSchema extends ResponseSchema { - threads: {[threadId:string]: { - messages: CommentThreadMessageSchema[], - resolved?: boolean, - resolved_at?: string, //ISO format date string - resolved_by_user_id?: string, - resolved_by_user?: UserInfoSchema, - }}, - ranges: { - id: string,//document id - ranges: DocumentRangesSchema, - }[], + threads: {[threadId:string]: CommentThreadSchema}, + ranges: {[docId:string]: DocumentRangesSchema}, } export class ExtendedBaseAPI extends BaseAPI { @@ -78,7 +82,8 @@ export class ExtendedBaseAPI extends BaseAPI { async getAllDocumentRanges(identity: Identity, project_id: string) { this.setIdentity(identity); return await this.request('GET', `project/${project_id}/ranges`, undefined, (res) => { - const ranges = JSON.parse(res!); + const rangeList = JSON.parse(res!) as {id:string, ranges:DocumentRangesSchema}[]; + const ranges = Object.assign({}, ...rangeList.map((r) => ({[r.id]: r.ranges}))); return {ranges}; }) as ExtendedResponseSchema; } diff --git a/src/collaboration/clientManager.ts b/src/collaboration/clientManager.ts index c108ca9..fd8561c 100644 --- a/src/collaboration/clientManager.ts +++ b/src/collaboration/clientManager.ts @@ -5,6 +5,7 @@ import { SocketIOAPI, UpdateUserSchema } from '../api/socketio'; import { VirtualFileSystem } from '../core/remoteFileSystemProvider'; import { ChatViewProvider } from './chatViewProvider'; import { LocalReplicaSCMProvider } from '../scm/localReplicaSCM'; +import { ReviewPanelProvider } from './reviewPanelProvider'; interface ExtendedUpdateUserSchema extends UpdateUserSchema { selection?: { @@ -59,6 +60,7 @@ export class ClientManager { private readonly onlineUsers: {[K:string]:ExtendedUpdateUserSchema} = {}; private connectedFlag: boolean = true; private readonly chatViewer: ChatViewProvider; + private readonly reviewPanel: ReviewPanelProvider; constructor( private readonly vfs: VirtualFileSystem, @@ -101,6 +103,7 @@ export class ClientManager { }); this.chatViewer = new ChatViewProvider(this.vfs, this.publicId, this.context.extensionUri, this.socket); + this.reviewPanel = new ReviewPanelProvider(this.vfs, this.context, this.socket); this.status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 0); this.updateStatus(); } @@ -376,6 +379,8 @@ export class ClientManager { }), // register chat view provider ...this.chatViewer.triggers, + // register review panel provider + ...this.reviewPanel.triggers, // update this client's position vscode.window.onDidChangeTextEditorSelection(async e => { if (e.kind===undefined) { return; } diff --git a/src/collaboration/reviewPanelProvider.ts b/src/collaboration/reviewPanelProvider.ts new file mode 100644 index 0000000..537fd3a --- /dev/null +++ b/src/collaboration/reviewPanelProvider.ts @@ -0,0 +1,62 @@ +import * as vscode from 'vscode'; +import { VirtualFileSystem } from '../core/remoteFileSystemProvider'; +import { SocketIOAPI } from '../api/socketio'; +import { DocumentRangesSchema } from '../api/extendedBase'; + +const reviewOpenCommentDecorationOption: vscode.DecorationRenderOptions = { + // backgroundColor: 'rgba(243, 177, 17, 0.3)', + backgroundColor: new vscode.ThemeColor('editor.hoverHighlightBackground'), + gutterIconPath: 'resources/gutter-comment-unresolved.svg', +}; + +const reviewResolvedCommentDecorationOption: vscode.DecorationRenderOptions = { + gutterIconPath: 'resources/gutter-pass.svg', +}; + +const reviewInsertChangeDecorationOption: vscode.DecorationRenderOptions = { + // color: 'rgba(44, 142, 48, 0.3)', + backgroundColor: new vscode.ThemeColor('diffEditor.insertedTextBackground'), + gutterIconPath: 'resources/gutter-edit.svg', +}; + +const reviewDeleteChangeDecorationOption: vscode.DecorationRenderOptions = { + // color: 'rgba(197, 6, 11, 1.0)', + fontStyle: 'text-decoration: line-through;', + backgroundColor: new vscode.ThemeColor('diffEditor.removedTextBackground'), + gutterIconPath: 'resources/gutter-edit.svg', +}; + +function fixGutterIconAbsolutePaths(context: vscode.ExtensionContext) { + for (const option of [ + reviewOpenCommentDecorationOption, + reviewResolvedCommentDecorationOption, + reviewInsertChangeDecorationOption, + reviewDeleteChangeDecorationOption + ]) { + if (option.gutterIconPath) { + option.gutterIconPath = context.asAbsolutePath(option.gutterIconPath as string); + } + } +} + +export class ReviewPanelProvider { + private reviewRecords?: {[docId:string]: DocumentRangesSchema}; + + constructor( + private readonly vfs: VirtualFileSystem, + private readonly context: vscode.ExtensionContext, + private readonly socket: SocketIOAPI, + ) { + // init review records + } + + private updateReviewDecorations() { + + } + + get triggers() { + return [ + + ]; + } +} diff --git a/src/core/remoteFileSystemProvider.ts b/src/core/remoteFileSystemProvider.ts index 2567a97..875a7d2 100644 --- a/src/core/remoteFileSystemProvider.ts +++ b/src/core/remoteFileSystemProvider.ts @@ -1178,6 +1178,24 @@ export class VirtualFileSystem extends vscode.Disposable { return false; } } + + async getAllDocumentReviews() { + const identity = await GlobalStateManager.authenticate(this.context, this.serverName); + const rangesRes = await (this.api as ExtendedBaseAPI).getAllDocumentRanges(identity, this.projectId); + if (rangesRes.type==='success') { + const threadsRes = await (this.api as ExtendedBaseAPI).getAllCommentThreads(identity, this.projectId); + if (threadsRes.type==='success') { + for (const range of Object.values(rangesRes.ranges)) { + for (const comment of range.comments) { + comment.thread = threadsRes.threads[comment.op.t]; + } + } + return rangesRes.ranges; + } + } + + return undefined; + } } export class RemoteFileSystemProvider implements vscode.FileSystemProvider {