diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index 5867366c02..e431404535 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -158,7 +158,7 @@ services: limits: memory: 1024M print: - image: hardcoreeng/uberflow-print + image: hardcoreeng/print restart: unless-stopped ports: - 4005:4005 @@ -171,7 +171,7 @@ services: limits: memory: 300M sign: - image: hardcoreeng/uberflow-sign + image: hardcoreeng/sign restart: unless-stopped ports: - 4006:4006 @@ -193,7 +193,7 @@ services: limits: memory: 300M analytics: - image: hardcoreeng/uberflow-analytics-collector + image: hardcoreeng/analytics-collector restart: unless-stopped ports: - 4077:4007 diff --git a/packages/ui/src/components/Scroller.svelte b/packages/ui/src/components/Scroller.svelte index ed08e67538..05372851e9 100644 --- a/packages/ui/src/components/Scroller.svelte +++ b/packages/ui/src/components/Scroller.svelte @@ -43,6 +43,7 @@ export let checkForHeaders: boolean = false export let stickedScrollBars: boolean = false export let thinScrollBars: boolean = false + export let disableOverscroll = false export let onScroll: ((params: ScrollParams) => void) | undefined = undefined export let onResize: (() => void) | undefined = undefined @@ -542,6 +543,7 @@ onResize?.() }} class="scroll relative flex-shrink" + class:disableOverscroll style:overflow-x={horizontal ? 'auto' : 'hidden'} on:scroll={() => { if (onScroll) { @@ -838,6 +840,9 @@ height: 100%; overflow-y: auto; + &.disableOverscroll { + overscroll-behavior: none; + } &::-webkit-scrollbar:vertical { width: 0; } diff --git a/plugins/chunter-resources/src/channelDataProvider.ts b/plugins/chunter-resources/src/channelDataProvider.ts index 2c5a749f7f..23a6601ba2 100644 --- a/plugins/chunter-resources/src/channelDataProvider.ts +++ b/plugins/chunter-resources/src/channelDataProvider.ts @@ -60,7 +60,6 @@ interface IChannelDataProvider { datesStore: Readable metadataStore: Readable - loadMore: (mode: LoadMode, loadAfter: Timestamp) => Promise canLoadMore: (mode: LoadMode, loadAfter: Timestamp) => boolean jumpToDate: (date: Timestamp) => Promise } @@ -95,10 +94,27 @@ export class ChannelDataProvider implements IChannelDataProvider { ([initialLoaded, tailLoading]) => !initialLoaded || tailLoading ) + private readonly backwardNextStore = writable(undefined) + private readonly forwardNextStore = writable(undefined) + + private backwardNextPromise: Promise | undefined = undefined + private forwardNextPromise: Promise | undefined = undefined + + private readonly isBackwardLoading = writable(false) + private readonly isForwardLoading = writable(false) + + private nextChunkAdding = false + public messagesStore = derived([this.chunksStore, this.tailStore], ([chunks, tail]) => { return [...chunks.map(({ data }) => data).flat(), ...tail] }) + public canLoadNextForwardStore = derived([this.messagesStore, this.forwardNextStore], ([messages, forwardNext]) => { + if (forwardNext !== undefined) return false + + return this.canLoadMore('forward', messages[messages.length - 1]?.createdOn) + }) + constructor ( chatId: Ref, _class: Ref>, @@ -139,20 +155,15 @@ export class ChannelDataProvider implements IChannelDataProvider { private clearData (): void { this.metadataStore.set([]) - this.tailStore.set([]) - this.chunksStore.set([]) - this.isInitialLoadingStore.set(false) - this.isInitialLoadedStore.set(false) this.isTailLoading.set(false) - this.datesStore.set([]) this.newTimestampStore.set(undefined) this.isLoadingMoreStore.set(false) - - this.tailStart = undefined this.chatId = undefined this.selectedMsgId = undefined + + this.clearMessages() } private async loadData (loadAll = false): Promise { @@ -211,9 +222,13 @@ export class ChannelDataProvider implements IChannelDataProvider { this.isTailLoading.set(true) const tailStart = metadata[startIndex]?.createdOn this.loadTail(tailStart) + this.backwardNextPromise = this.loadNext('backward', metadata[startIndex]?.createdOn, this.limit) } else { const newStart = Math.max(startPosition - this.limit / 2, 0) await this.loadMore('forward', metadata[newStart]?.createdOn, this.limit) + if (newStart > 0) { + this.backwardNextPromise = this.loadNext('backward', metadata[newStart]?.createdOn, this.limit) + } } this.isInitialLoadingStore.set(false) @@ -260,41 +275,28 @@ export class ChannelDataProvider implements IChannelDataProvider { ) } - public async loadMore (mode: LoadMode, loadAfter?: Timestamp, limit?: number): Promise { - if (this.chatId === undefined || loadAfter === undefined) { - return - } + isNextLoading (mode: LoadMode): boolean { + return mode === 'forward' ? get(this.isForwardLoading) : get(this.isBackwardLoading) + } - if (!this.canLoadMore(mode, loadAfter) || get(this.isLoadingMoreStore)) { - return - } + isNextLoaded (mode: LoadMode): boolean { + return mode === 'forward' ? get(this.forwardNextStore) !== undefined : get(this.backwardNextStore) !== undefined + } - this.isLoadingMoreStore.set(true) + setNextLoading (mode: LoadMode, value: boolean): void { + mode === 'forward' ? this.isForwardLoading.set(value) : this.isBackwardLoading.set(value) + } - const isBackward = mode === 'backward' - const isForward = mode === 'forward' + getTailStartIndex (metadata: MessageMetadata[], loadAfter: Timestamp): number { + const index = metadata.slice(-this.limit - 1).findIndex(({ createdOn }) => createdOn === loadAfter) - const chunks = get(this.chunksStore) - const tail = get(this.tailStore) - const lastChunk: Chunk | undefined = isBackward ? chunks[0] : chunks[chunks.length - 1] - const skipIds = (lastChunk?.data ?? []) - .concat(tail) - .filter(({ createdOn }) => createdOn === loadAfter) - .map(({ _id }) => _id) as Array> - - if (isForward) { - const metadata = get(this.metadataStore) - const metaIndex = metadata.findIndex(({ createdOn }) => createdOn === loadAfter) - const shouldLoadTail = metaIndex >= 0 && metaIndex + this.limit >= metadata.length - - if (shouldLoadTail) { - this.loadTail(metadata[metaIndex + 1]?.createdOn, undefined, { _id: { $nin: skipIds } }) - this.isLoadingMoreStore.set(false) - return - } - } + return index !== -1 ? metadata.length - index : -1 + } + async loadChunk (isBackward: boolean, loadAfter: Timestamp, limit?: number): Promise { const client = getClient() + const skipIds = this.getChunkSkipIds(loadAfter) + const messages = await client.findAll( chunter.class.ChatMessage, { @@ -312,20 +314,149 @@ export class ChannelDataProvider implements IChannelDataProvider { ) if (messages.length === 0) { - this.isLoadingMoreStore.set(false) return } const from = isBackward ? messages[0] : messages[messages.length - 1] const to = isBackward ? messages[messages.length - 1] : messages[0] - const chunk: Chunk = { + return { from: from.createdOn ?? from.modifiedOn, to: to.createdOn ?? to.modifiedOn, data: isBackward ? messages.reverse() : messages } + } + + getChunkSkipIds (after: Timestamp, loadTail = false): Array> { + const chunks = get(this.chunksStore) + const metadata = get(this.metadataStore) + const tail = get(this.tailStore) + const tailData = tail.length > 0 ? get(this.tailStore) : metadata.slice(-this.limit) + + return chunks + .filter(({ to, from }) => from >= after || to <= after) + .map(({ data }) => data as MessageMetadata[]) + .flat() + .concat(loadTail ? [] : tailData) + .filter(({ createdOn }) => createdOn === after) + .map(({ _id }) => _id) as Array> + } + + async loadNext (mode: LoadMode, loadAfter?: Timestamp, limit?: number): Promise { + if (this.chatId === undefined || loadAfter === undefined) { + return + } + + if (this.isNextLoading(mode) || this.isNextLoaded(mode)) { + return + } + + if (!this.canLoadMore(mode, loadAfter)) { + return + } + + this.setNextLoading(mode, true) + + const isBackward = mode === 'backward' + const isForward = mode === 'forward' + + const metadata = get(this.metadataStore) + + if (isForward && this.getTailStartIndex(metadata, loadAfter) !== -1) { + this.setNextLoading(mode, false) + return + } + + const chunk = await this.loadChunk(isBackward, loadAfter, limit) + + if (chunk !== undefined && isBackward) { + this.backwardNextStore.set(chunk) + } + if (chunk !== undefined && isForward) { + this.forwardNextStore.set(chunk) + } + + this.setNextLoading(mode, false) + } + + public async addNextChunk (mode: LoadMode, loadAfter?: Timestamp, limit?: number): Promise { + if (loadAfter === undefined || this.nextChunkAdding) { + return + } + + this.nextChunkAdding = true + + if (this.forwardNextPromise instanceof Promise && mode === 'forward') { + await this.forwardNextPromise + this.forwardNextPromise = undefined + } + + if (this.backwardNextPromise instanceof Promise && mode === 'backward') { + await this.backwardNextPromise + this.backwardNextPromise = undefined + } + + if (this.isNextLoaded(mode)) { + const next = mode === 'forward' ? get(this.forwardNextStore) : get(this.backwardNextStore) + if (next !== undefined) { + if (mode === 'forward') { + this.forwardNextStore.set(undefined) + this.chunksStore.set([...get(this.chunksStore), next]) + this.forwardNextPromise = this.loadNext('forward', next.from, limit) + } else { + this.backwardNextStore.set(undefined) + this.chunksStore.set([next, ...get(this.chunksStore)]) + this.backwardNextPromise = this.loadNext('backward', next.to, limit) + } + } + } else { + await this.loadMore(mode, loadAfter, limit) + } + + this.nextChunkAdding = false + } + + private async loadMore (mode: LoadMode, loadAfter?: Timestamp, limit?: number): Promise { + if (get(this.isLoadingMoreStore) || loadAfter === undefined) { + return + } + + if (!this.canLoadMore(mode, loadAfter)) { + return + } + + this.isLoadingMoreStore.set(true) + + const isBackward = mode === 'backward' + const isForward = mode === 'forward' + + const chunks = get(this.chunksStore) + const metadata = get(this.metadataStore) + + if (isForward) { + const index = this.getTailStartIndex(metadata, loadAfter) + const tailAfter = metadata[index]?.createdOn + + if (tailAfter !== undefined) { + const skipIds = chunks[chunks.length - 1]?.data.map(({ _id }) => _id) ?? [] + this.loadTail(tailAfter, undefined, { _id: { $nin: skipIds } }) + this.isLoadingMoreStore.set(false) + return + } + } + + const chunk = await this.loadChunk(isBackward, loadAfter, limit) + + if (chunk !== undefined) { + this.chunksStore.set(isBackward ? [chunk, ...chunks] : [...chunks, chunk]) + + if (isBackward) { + this.forwardNextPromise = this.loadNext('backward', chunk.to, limit) + } else { + this.forwardNextPromise = this.loadNext('forward', chunk.from, limit) + } + } - this.chunksStore.set(isBackward ? [chunk, ...chunks] : [...chunks, chunk]) this.isLoadingMoreStore.set(false) } @@ -426,6 +557,12 @@ export class ChannelDataProvider implements IChannelDataProvider { this.isInitialLoadedStore.set(false) this.tailQuery.unsubscribe() this.tailStart = undefined + this.backwardNextPromise = undefined + this.forwardNextPromise = undefined + this.forwardNextStore.set(undefined) + this.backwardNextStore.set(undefined) + this.isBackwardLoading.set(false) + this.isForwardLoading.set(false) } public async jumpToDate (date: Timestamp): Promise { diff --git a/plugins/chunter-resources/src/components/ChannelScrollView.svelte b/plugins/chunter-resources/src/components/ChannelScrollView.svelte index 7b4de7f091..e9f09049f2 100644 --- a/plugins/chunter-resources/src/components/ChannelScrollView.svelte +++ b/plugins/chunter-resources/src/components/ChannelScrollView.svelte @@ -67,6 +67,7 @@ const dateSelectorHeight = 30 const headerHeight = 52 const minMsgHeightRem = 2 + const loadMoreThreshold = 40 const client = getClient() const hierarchy = client.getHierarchy() @@ -244,7 +245,7 @@ return false } - return scrollElement.scrollTop === 0 + return scrollElement.scrollTop <= loadMoreThreshold } function shouldLoadMoreDown (): boolean { @@ -254,10 +255,11 @@ const { scrollHeight, scrollTop, clientHeight } = scrollElement - return scrollHeight - Math.ceil(scrollTop + clientHeight) <= 0 + return scrollHeight - Math.ceil(scrollTop + clientHeight) <= loadMoreThreshold } let scrollToRestore = 0 + let backwardRequested = false function loadMore (): void { if (!loadMoreAllowed || $isLoadingMoreStore || !scrollElement || isInitialScrolling) { @@ -268,18 +270,24 @@ const maxMsgPerScreen = Math.ceil(scrollElement.clientHeight / minMsgHeightPx) const limit = Math.max(maxMsgPerScreen, provider.limit) - if (shouldLoadMoreUp() && scrollElement && provider.canLoadMore('backward', messages[0]?.createdOn)) { + if (!shouldLoadMoreUp()) { + backwardRequested = false + } + + if (shouldLoadMoreUp() && !backwardRequested) { shouldScrollToNew = false - scrollToRestore = scrollElement.scrollHeight - void provider.loadMore('backward', messages[0]?.createdOn, limit) - } else if (shouldLoadMoreDown() && provider.canLoadMore('forward', messages[messages.length - 1]?.createdOn)) { + scrollToRestore = scrollElement?.scrollHeight ?? 0 + provider.addNextChunk('backward', messages[0]?.createdOn, limit) + backwardRequested = true + } else if (shouldLoadMoreDown()) { + scrollToRestore = 0 shouldScrollToNew = false - void provider.loadMore('forward', messages[messages.length - 1]?.createdOn, limit) isScrollAtBottom = false + provider.addNextChunk('forward', messages[messages.length - 1]?.createdOn, limit) } } - function handleScroll ({ autoScrolling }: ScrollParams): void { + async function handleScroll ({ autoScrolling }: ScrollParams): Promise { saveScrollPosition() updateDownButtonVisibility($metadataStore, displayMessages, scrollElement) if (autoScrolling) { @@ -668,6 +676,8 @@ scrollToBottom() } } + + const canLoadNextForwardStore = provider.canLoadNextForwardStore {#if isLoading} @@ -694,10 +704,11 @@ bind:divScroll={scrollElement} bind:divBox={scrollContentBox} noStretch={false} + disableOverscroll onScroll={handleScroll} onResize={handleResize} > - {#if loadMoreAllowed && provider.canLoadMore('backward', messages[0]?.createdOn)} + {#if loadMoreAllowed} {/if} @@ -736,7 +747,7 @@ /> {/each} - {#if loadMoreAllowed && provider.canLoadMore('forward', messages[messages.length - 1]?.createdOn)} + {#if loadMoreAllowed && $canLoadNextForwardStore} {/if} diff --git a/plugins/drive-assets/lang/en.json b/plugins/drive-assets/lang/en.json index bbb0b4c9b1..f78383395d 100644 --- a/plugins/drive-assets/lang/en.json +++ b/plugins/drive-assets/lang/en.json @@ -20,8 +20,7 @@ "Upload": "Upload", "CreateDrive": "Create Drive", "CreateFolder": "Create Folder", - "UploadFile": "Upload File", - "UploadFolder": "Upload Folder", + "UploadFile": "Upload Files", "EditDrive": "Edit Drive", "Rename": "Rename", "Restore": "Restore", diff --git a/plugins/drive-assets/lang/es.json b/plugins/drive-assets/lang/es.json index a79eeeefd3..a06ae13a75 100644 --- a/plugins/drive-assets/lang/es.json +++ b/plugins/drive-assets/lang/es.json @@ -20,8 +20,7 @@ "Upload": "Subir", "CreateDrive": "Crear unidad", "CreateFolder": "Crear carpeta", - "UploadFile": "Subir archivo", - "UploadFolder": "Subir carpeta", + "UploadFile": "Subir archivos", "EditDrive": "Editar unidad", "Rename": "Renombrar", "Restore": "Restaurar", diff --git a/plugins/drive-assets/lang/fr.json b/plugins/drive-assets/lang/fr.json index 61a79d2642..7daf2d616e 100644 --- a/plugins/drive-assets/lang/fr.json +++ b/plugins/drive-assets/lang/fr.json @@ -20,8 +20,7 @@ "Upload": "Téléverser", "CreateDrive": "Créer un disque", "CreateFolder": "Créer un dossier", - "UploadFile": "Télécharger un fichier", - "UploadFolder": "Télécharger un dossier", + "UploadFile": "Télécharger des fichiers", "EditDrive": "Modifier le disque", "Rename": "Renommer", "Restore": "Restaurer", diff --git a/plugins/drive-assets/lang/pt.json b/plugins/drive-assets/lang/pt.json index 9822a424ee..fc063c799f 100644 --- a/plugins/drive-assets/lang/pt.json +++ b/plugins/drive-assets/lang/pt.json @@ -20,8 +20,7 @@ "Upload": "Carregar", "CreateDrive": "Criar unidade", "CreateFolder": "Criar pasta", - "UploadFile": "Carregar ficheiro", - "UploadFolder": "Carregar pasta", + "UploadFile": "Carregar ficheiros", "EditDrive": "Editar unidade", "Rename": "Renomear", "Restore": "Restaurar", diff --git a/plugins/drive-assets/lang/ru.json b/plugins/drive-assets/lang/ru.json index 9d7c4ea9cc..825892ec0d 100644 --- a/plugins/drive-assets/lang/ru.json +++ b/plugins/drive-assets/lang/ru.json @@ -20,8 +20,7 @@ "Upload": "Загрузить", "CreateDrive": "Создать диск", "CreateFolder": "Создать папку", - "UploadFile": "Загрузить файл", - "UploadFolder": "Загрузить папку", + "UploadFile": "Загрузить файлы", "EditDrive": "Редактировать", "Rename": "Переименовать", "Restore": "Восстановить", diff --git a/plugins/drive-assets/lang/zh.json b/plugins/drive-assets/lang/zh.json index 1359f4c65f..0ac452d5ac 100644 --- a/plugins/drive-assets/lang/zh.json +++ b/plugins/drive-assets/lang/zh.json @@ -21,7 +21,6 @@ "CreateDrive": "创建磁盘", "CreateFolder": "创建文件夹", "UploadFile": "上传文件", - "UploadFolder": "上传文件夹", "EditDrive": "编辑磁盘", "Rename": "重命名", "Restore": "恢复", diff --git a/plugins/drive-resources/src/components/DriveSpaceHeader.svelte b/plugins/drive-resources/src/components/DriveSpaceHeader.svelte index f638ee2937..8054b62c46 100644 --- a/plugins/drive-resources/src/components/DriveSpaceHeader.svelte +++ b/plugins/drive-resources/src/components/DriveSpaceHeader.svelte @@ -14,22 +14,19 @@ --> diff --git a/plugins/drive-resources/src/components/FilePanel.svelte b/plugins/drive-resources/src/components/FilePanel.svelte index 8481c0b60b..bbbd74b924 100644 --- a/plugins/drive-resources/src/components/FilePanel.svelte +++ b/plugins/drive-resources/src/components/FilePanel.svelte @@ -73,6 +73,7 @@ maxNumberOfFiles: 1, hideProgress: true }, + {}, async (uuid, name, file, path, metadata) => { const data = { file: uuid, diff --git a/plugins/drive-resources/src/plugin.ts b/plugins/drive-resources/src/plugin.ts index 4512ce0fbf..661209ad96 100644 --- a/plugins/drive-resources/src/plugin.ts +++ b/plugins/drive-resources/src/plugin.ts @@ -21,7 +21,6 @@ export default mergeIds(driveId, drive, { CreateDrive: '' as IntlString, CreateFolder: '' as IntlString, UploadFile: '' as IntlString, - UploadFolder: '' as IntlString, Download: '' as IntlString, Upload: '' as IntlString, EditDrive: '' as IntlString, diff --git a/plugins/drive-resources/src/utils.ts b/plugins/drive-resources/src/utils.ts index 51121f5da0..269b530928 100644 --- a/plugins/drive-resources/src/utils.ts +++ b/plugins/drive-resources/src/utils.ts @@ -19,7 +19,12 @@ import drive, { createFile } from '@hcengineering/drive' import { type Asset, setPlatformStatus, unknownError } from '@hcengineering/platform' import { getClient } from '@hcengineering/presentation' import { type AnySvelteComponent, showPopup } from '@hcengineering/ui' -import { uploadFiles } from '@hcengineering/uploader' +import { + type FileUploadCallback, + getDataTransferFiles, + showFilesUploadPopup, + uploadFiles +} from '@hcengineering/uploader' import { openDoc } from '@hcengineering/view-resources' import CreateDrive from './components/CreateDrive.svelte' @@ -154,7 +159,38 @@ export async function resolveParents (object: Resource): Promise { return parents.reverse() } -export async function uploadFilesToDrive (files: DataTransfer, space: Ref, parent: Ref): Promise { +export async function uploadFilesToDrive (dt: DataTransfer, space: Ref, parent: Ref): Promise { + const files = await getDataTransferFiles(dt) + + const onFileUploaded = await fileUploadCallback(space, parent) + + const target = + parent !== drive.ids.Root + ? { objectId: parent, objectClass: drive.class.Folder } + : { objectId: space, objectClass: drive.class.Drive } + + await uploadFiles(files, target, {}, onFileUploaded) +} + +export async function uploadFilesToDrivePopup (space: Ref, parent: Ref): Promise { + const onFileUploaded = await fileUploadCallback(space, parent) + + const target = + parent !== drive.ids.Root + ? { objectId: parent, objectClass: drive.class.Folder } + : { objectId: space, objectClass: drive.class.Drive } + + await showFilesUploadPopup( + target, + {}, + { + fileManagerSelectionType: 'both' + }, + onFileUploaded + ) +} + +async function fileUploadCallback (space: Ref, parent: Ref): Promise { const client = getClient() const query = parent !== drive.ids.Root ? { space, path: parent } : { space } @@ -190,12 +226,7 @@ export async function uploadFilesToDrive (files: DataTransfer, space: Ref return current } - const target = - parent !== drive.ids.Root - ? { objectId: parent, objectClass: drive.class.Folder } - : { objectId: space, objectClass: drive.class.Drive } - - await uploadFiles(files, target, {}, async (uuid, name, file, path, metadata) => { + const callback: FileUploadCallback = async (uuid, name, file, path, metadata) => { const folder = await findParent(path) try { const data = { @@ -211,5 +242,7 @@ export async function uploadFilesToDrive (files: DataTransfer, space: Ref } catch (err) { void setPlatformStatus(unknownError(err)) } - }) + } + + return callback } diff --git a/plugins/uploader-resources/src/components/FileUploadPopup.svelte b/plugins/uploader-resources/src/components/FileUploadPopup.svelte index 3f83c5213b..1791b70c1a 100644 --- a/plugins/uploader-resources/src/components/FileUploadPopup.svelte +++ b/plugins/uploader-resources/src/components/FileUploadPopup.svelte @@ -14,6 +14,7 @@ -->