Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / docs-core / lib / UseCase / CreateThread.ts
blob45419b1a0f91d9f1410a1dbcb26ec580506db22c
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'
17 /**
18  * Creates a new comment thread with the API, supplying and encrypting an initial comment.
19  */
20 export class CreateThread implements UseCaseInterface<CommentThreadInterface> {
21   constructor(
22     private api: DocsApi,
23     private encryptComment: EncryptComment,
24     private decryptComment: DecryptComment,
25     private eventBus: InternalEventBusInterface,
26     private logger: LoggerInterface,
27   ) {}
29   async execute(dto: {
30     text: string
31     entitlements: PublicDocumentEntitlements | DocumentEntitlements
32     commentsState: LocalCommentsState
33     type: CommentThreadType
34     decryptedDocumentName: string | null
35     markID?: string
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(
43       GenerateUUID(),
44       ServerTime.now(),
45       ServerTime.now(),
46       dto.text,
47       null,
48       isPrivateDocumentKeys(dto.entitlements.keys) ? dto.entitlements.keys.userOwnAddress : undefined,
49       [],
50       false,
51       commentType,
52     )
54     const localID = GenerateUUID()
56     const localThread = new CommentThread(
57       localID,
58       ServerTime.now(),
59       ServerTime.now(),
60       markID,
61       [comment],
62       true,
63       CommentThreadState.Active,
64       dto.type,
65       localID,
66     )
68     dto.commentsState.addThread(localThread)
70     const shouldCreateMark = dto.createMarkNode ?? true
72     if (shouldCreateMark) {
73       this.eventBus.publish<CommentMarkNodeChangeData>({
74         type: CommentsEvent.CreateMarkNode,
75         payload: {
76           markID,
77         },
78       })
79     }
81     const onFail = () => {
82       dto.commentsState.deleteThread(localThread.id)
84       if (shouldCreateMark) {
85         this.eventBus.publish<CommentMarkNodeChangeData>({
86           type: CommentsEvent.RemoveMarkNode,
87           payload: {
88             markID,
89           },
90         })
91       }
92     }
94     const commentEncryptionResult = await this.encryptComment.execute(dto.text, markID, dto.entitlements.keys)
95     if (commentEncryptionResult.isFailed()) {
96       onFail()
97       return Result.fail(commentEncryptionResult.getError())
98     }
100     const encryptedCommentContent = commentEncryptionResult.getValue()
102     const result = await this.api.createThread(
103       {
104         markId: markID,
105         encryptedMainCommentContent: encryptedCommentContent,
106         type: dto.type,
107         commentType,
108         decryptedDocumentName: dto.decryptedDocumentName,
109       },
110       dto.entitlements,
111     )
113     if (result.isFailed()) {
114       metrics.docs_comments_error_total.increment({
115         reason: 'server_error',
116       })
118       onFail()
120       return Result.fail(result.getError().message)
121     }
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)
128         return result
129       }),
130     )
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()}`)
135     }
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()),
145       false,
146       response.CommentThread.State,
147       response.CommentThread.Type,
148       localID,
149     )
151     dto.commentsState.replacePlaceholderThread(localThread.id, thread)
153     return Result.ok(thread)
154   }