1 import type { UseCaseInterface } from '../Domain/UseCase/UseCaseInterface'
2 import { Result } from '@proton/docs-shared'
3 import { Comment, CommentThread } from '../Models'
4 import type { CommentMarkNodeChangeData, CommentThreadInterface, InternalEventBusInterface } from '@proton/docs-shared'
5 import { CommentThreadState, CommentType, CommentsEvent, ServerTime } from '@proton/docs-shared'
6 import { GenerateUUID } from '../Util/GenerateUuid'
7 import type { EncryptComment } from './EncryptComment'
8 import type { DecryptComment } from './DecryptComment'
9 import type { LocalCommentsState } from '../Services/Comments/LocalCommentsState'
10 import type { DocsApi } from '../Api/DocsApi'
11 import metrics from '@proton/metrics'
12 import { CommentThreadType } from '@proton/docs-shared'
13 import type { LoggerInterface } from '@proton/utils/logs'
14 import type { DocumentEntitlements, PublicDocumentEntitlements } from '../Types/DocumentEntitlements'
15 import { isPrivateDocumentKeys } from '../Types/DocumentEntitlements'
18 * Creates a new comment thread with the API, supplying and encrypting an initial comment.
20 export class CreateThread implements UseCaseInterface<CommentThreadInterface> {
23 private encryptComment: EncryptComment,
24 private decryptComment: DecryptComment,
25 private eventBus: InternalEventBusInterface,
26 private logger: LoggerInterface,
31 entitlements: PublicDocumentEntitlements | DocumentEntitlements
32 commentsState: LocalCommentsState
33 type: CommentThreadType
34 decryptedDocumentName: string | null
36 createMarkNode?: boolean
37 }): Promise<Result<CommentThreadInterface>> {
38 const markID = dto.markID ?? GenerateUUID()
40 const commentType = dto.type === CommentThreadType.Suggestion ? CommentType.Suggestion : CommentType.Comment
42 const comment = new Comment(
48 isPrivateDocumentKeys(dto.entitlements.keys) ? dto.entitlements.keys.userOwnAddress : undefined,
54 const localID = GenerateUUID()
56 const localThread = new CommentThread(
63 CommentThreadState.Active,
68 dto.commentsState.addThread(localThread)
70 const shouldCreateMark = dto.createMarkNode ?? true
72 if (shouldCreateMark) {
73 this.eventBus.publish<CommentMarkNodeChangeData>({
74 type: CommentsEvent.CreateMarkNode,
81 const onFail = () => {
82 dto.commentsState.deleteThread(localThread.id)
84 if (shouldCreateMark) {
85 this.eventBus.publish<CommentMarkNodeChangeData>({
86 type: CommentsEvent.RemoveMarkNode,
94 const commentEncryptionResult = await this.encryptComment.execute(dto.text, markID, dto.entitlements.keys)
95 if (commentEncryptionResult.isFailed()) {
97 return Result.fail(commentEncryptionResult.getError())
100 const encryptedCommentContent = commentEncryptionResult.getValue()
102 const result = await this.api.createThread(
105 encryptedMainCommentContent: encryptedCommentContent,
108 decryptedDocumentName: dto.decryptedDocumentName,
113 if (result.isFailed()) {
114 metrics.docs_comments_error_total.increment({
115 reason: 'server_error',
120 return Result.fail(result.getError().message)
123 const response = result.getValue()
125 const comments = await Promise.all(
126 response.CommentThread.Comments.map(async (commentDto) => {
127 const result = await this.decryptComment.execute(commentDto, response.CommentThread.Mark, dto.entitlements.keys)
132 const failedComments = comments.filter((result) => result.isFailed())
133 for (const failed of failedComments) {
134 this.logger.error(`[CreateThread] Failed to decrypt comment: ${failed.getError()}`)
137 const successfulComments = comments.filter((result) => !result.isFailed())
139 const thread = new CommentThread(
140 response.CommentThread.CommentThreadID,
141 new ServerTime(response.CommentThread.CreateTime),
142 new ServerTime(response.CommentThread.ModifyTime),
143 response.CommentThread.Mark,
144 successfulComments.map((result) => result.getValue()),
146 response.CommentThread.State,
147 response.CommentThread.Type,
151 dto.commentsState.replacePlaceholderThread(localThread.id, thread)
153 return Result.ok(thread)