1 import { InternalEventPublishStrategy, ServerTime, type InternalEventBus } from '@proton/docs-shared'
2 import type { DecryptedNode, DriveCompat } from '@proton/drive-store'
5 EventRecentDocumentStateUpdated,
7 RecentDocumentServiceState,
8 RecentDocumentsInterface,
9 RecentDocumentsSnapshot,
10 RecentDocumentsSnapshotData,
12 import { RecentDocumentStateUpdatedEvent } from './types'
13 import type { DocsApi } from '../../Api/DocsApi'
14 import type { LoggerInterface } from '@proton/utils/logs'
19 isOwnedByOthers: boolean
22 export class RecentDocumentsService implements RecentDocumentsInterface {
23 state: RecentDocumentServiceState = 'not_fetched'
24 recentDocuments: RecentDocument[] = []
25 record: Map<string, ResolvedItem> = new Map()
28 private eventBus: InternalEventBus,
29 private driveCompat: DriveCompat,
30 private docsApi: DocsApi,
31 private logger: LoggerInterface,
34 async notifyStateUpdated() {
35 const event: EventRecentDocumentStateUpdated = {
36 type: RecentDocumentStateUpdatedEvent,
40 await this.eventBus.publishSync(event, InternalEventPublishStrategy.SEQUENCE)
43 async setStatusAndNotify(status: RecentDocumentServiceState) {
45 await this.notifyStateUpdated()
49 await this.setStatusAndNotify('fetching')
51 const response = await this.docsApi.fetchRecentDocuments()
52 if (response.isFailed()) {
53 await this.setStatusAndNotify('not_fetched')
57 this.recentDocuments = response.getValue().RecentDocuments.map((item) => ({
59 shareId: item.ContextShareID,
60 lastViewed: new ServerTime(item.LastOpenTime),
63 await this.setStatusAndNotify('resolving')
65 const ids = this.recentDocuments.map((recentDocument) => ({
66 linkId: recentDocument.linkId,
67 shareId: recentDocument.shareId,
70 await Promise.all(ids.map((item) => this.resolveItem(item)))
72 await this.setStatusAndNotify('done')
75 async resolveItem(item: { linkId: string; shareId: string }) {
77 const [node, nodePath, isNodeAtIndexOwnedByOthers] = await Promise.all([
78 this.driveCompat.getNodes([item]).then((nodes) => nodes[0]),
79 this.driveCompat.getNodePaths([item]).then((paths) => paths[0]),
80 this.driveCompat.getNodesAreShared([item]).then((shared) => shared[0]),
83 const record: ResolvedItem = {
85 nodePath: nodePath.map((pathItem) => pathItem.name),
86 isOwnedByOthers: isNodeAtIndexOwnedByOthers,
89 this.record.set(item.linkId, record)
91 await this.notifyStateUpdated()
93 this.logger.error('Failed to resolve recent document', { error, item })
97 async trashDocument(recentDocument: RecentDocumentsSnapshotData): Promise<void> {
98 await this.setStatusAndNotify('fetching')
100 if (!recentDocument.parentLinkId) {
101 throw new Error('Node does not have parent link ID')
104 await this.driveCompat.trashDocument(
105 { linkId: recentDocument.linkId, volumeId: recentDocument.volumeId },
106 recentDocument.parentLinkId,
109 await this.setStatusAndNotify('done')
112 getSnapshot(): RecentDocumentsSnapshot {
113 const data: RecentDocumentsSnapshot['data'] = this.recentDocuments
114 .map((recentDocument) => {
115 const record = this.record.get(recentDocument.linkId)
121 name: record.node.name,
122 linkId: recentDocument.linkId,
123 volumeId: record.node.volumeId,
124 parentLinkId: record.node.parentNodeId,
125 location: record.isOwnedByOthers ? ['Shared with me'] : record.nodePath,
126 isSharedWithMe: record.isOwnedByOthers,
127 lastViewed: recentDocument.lastViewed,
128 createdBy: record.node.signatureAddress,
131 .filter((item) => !!item)