Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions dev/prod/public/config-dev.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"ACCOUNTS_URL":"https://account.hc.engineering",
"UPLOAD_URL":"/files",
"UPLOAD_URL": "https://datalake.hc.engineering/upload/form-data/:workspace",
"FILES_URL": "https://datalake.hc.engineering/blob/:workspace/:blobId/:filename",
"MODEL_VERSION": null,
"TELEGRAM_URL": "https://telegram.hc.engineering",
"GMAIL_URL": "https://gmail.hc.engineering",
Expand All @@ -11,5 +12,7 @@
"PRESENCE_URL": "wss://presence.hc.engineering",
"PUBLIC_SCHEDULE_URL": "https://schedule.hc.engineering",
"CALDAV_SERVER_URL": "https://caldav.hc.engineering",
"BACKUP_URL": "https://front.hc.engineering/api/backup"
"BACKUP_URL": "https://front.hc.engineering/api/backup",
"PREVIEW_CONFIG": "image|https://datalake.hc.engineering/image/fit=scale-down,width=:width,height=:height,dpr=:dpr/:workspace/:blobId;video|https://datalake.hc.engineering/video/:workspace/:blobId/meta",
"COMMUNICATION_API_ENABLED": "true"
}
64 changes: 51 additions & 13 deletions packages/presentation/src/components/Image.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
-->
<script lang="ts">
import type { Blob, Ref } from '@hcengineering/core'
import { Image } from '@hcengineering/ui'
import { Image, lazyObserverContinuous } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'

import { getBlobRef } from '../preview'

export let blob: Ref<Blob>
Expand All @@ -25,22 +27,58 @@
export let responsive: boolean = false
export let loading: 'lazy' | 'eager' = 'eager'

const dispatch = createEventDispatcher()

let blobSrc: { src: string, srcset: string } | undefined

$: void getBlobRef(blob, alt, width, height).then((val) => {
blobSrc = val
})

let visible = true
let loaded = false

function trackVisible (node: Element): any {
return lazyObserverContinuous(
node,
(val) => {
visible = val
},
'10%'
)
}

$: src = visible || loaded ? blobSrc?.src : undefined
$: srcset = visible || loaded ? blobSrc?.srcset : undefined

function handleLoad (): void {
loaded = true
dispatch('load')
}

function handleLoadStart (): void {
loaded = false
dispatch('loadstart')
}

function handleError (): void {
loaded = false
dispatch('error')
}
</script>

<Image
src={blobSrc?.src}
srcset={blobSrc?.srcset}
{alt}
width={responsive ? '100%' : width}
height={responsive ? '100%' : height}
{loading}
{fit}
on:load
on:error
on:loadstart
/>
<div use:trackVisible>
<Image
{src}
{srcset}
{alt}
width={responsive ? '100%' : width}
height={responsive ? '100%' : height}
{loading}
{fit}
on:load={handleLoad}
on:loadstart={handleLoadStart}
on:error={handleError}
on:loadstart
/>
</div>
108 changes: 91 additions & 17 deletions packages/ui/src/lazy.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { DelayedCaller } from './utils'

type ObserverMode = 'once' | 'continuous'

interface ObserverEntry {
callback: (isIntersecting: boolean) => void
mode: ObserverMode
}

const observers = new Map<string, IntersectionObserver>()
const entryMap = new WeakMap<Element, { callback: (isIntersecting: boolean) => void }>()
const entryMap = new WeakMap<Element, ObserverEntry>()

const delayedCaller = new DelayedCaller(5)

function makeObserver (rootMargin: string): IntersectionObserver {
const entriesPending = new Map<Element, { isIntersecting: boolean }>()

const notifyObservers = (observer: IntersectionObserver): void => {
for (const [target, entry] of entriesPending.entries()) {
const entryData = entryMap.get(target)
Expand All @@ -15,13 +24,16 @@ function makeObserver (rootMargin: string): IntersectionObserver {
}

entryData.callback(entry.isIntersecting)
if (entry.isIntersecting) {

// Only unobserve if mode is 'once' and element became visible
if (entry.isIntersecting && entryData.mode === 'once') {
entryMap.delete(target)
observer.unobserve(target)
}
}
entriesPending.clear()
}

const observer = new IntersectionObserver(
(entries, observer) => {
for (const entry of entries) {
Expand All @@ -36,15 +48,21 @@ function makeObserver (rootMargin: string): IntersectionObserver {
return observer
}

function listen (rootMargin: string, element: Element, callback: (isIntersecting: boolean) => void): () => void {
function listen (
rootMargin: string,
element: Element,
callback: (isIntersecting: boolean) => void,
mode: ObserverMode = 'once'
): () => void {
let observer = observers.get(rootMargin)
if (observer == null) {
observer = makeObserver(rootMargin)
observers.set(rootMargin, observer)
}

entryMap.set(element, { callback })
entryMap.set(element, { callback, mode })
observer.observe(element)

return () => {
observer?.unobserve(element)
entryMap.delete(element)
Expand All @@ -56,33 +74,73 @@ function listen (rootMargin: string, element: Element, callback: (isIntersecting
*/
export const isLazyEnabled = (): boolean => (localStorage.getItem('#platform.lazy.loading') ?? 'true') === 'true'

export function lazyObserver (node: Element, onVisible: (value: boolean, unsubscribe?: () => void) => void): any {
interface LazyObserverOptions {
mode?: ObserverMode
rootMargin?: string
}

export function lazyObserver (
node: Element,
onVisible: (value: boolean, unsubscribe?: () => void) => void,
options: LazyObserverOptions = {}
): any {
const { mode = 'once', rootMargin = '20%' } = options

let visible = false
let destroy = (): void => {}

const lazyEnabled = isLazyEnabled()
if (!lazyEnabled) {

// Special case: 'once' mode with lazy disabled - immediately report visible
if (!lazyEnabled && mode === 'once') {
visible = true
onVisible(visible)
return {
destroy: () => {},
update: () => {}
}
}
if (visible) {
onVisible(visible)
return {}

// For continuous mode, we set up once and don't need the update logic
if (mode === 'continuous') {
destroy = listen(
rootMargin,
node,
(isIntersecting) => {
if (visible !== isIntersecting) {
visible = isIntersecting
onVisible(visible, destroy)
}
},
mode
)

return {
destroy,
update: () => {} // No-op for continuous mode
}
}

// For 'once' mode with lazy enabled
let needsUpdate = true
let destroy = (): void => {}
// we need this update function to re-trigger observer for moved elements
// moved elements are relevant because onVisible can have side effects

const update = (): void => {
if (!needsUpdate) {
return
}
needsUpdate = false
destroy()
destroy = listen('20%', node, (isIntersecting) => {
visible = isIntersecting
needsUpdate = visible
onVisible(visible, destroy)
})

destroy = listen(
rootMargin,
node,
(isIntersecting) => {
visible = isIntersecting
needsUpdate = visible
onVisible(visible, destroy)
},
mode
)
}
update()

Expand All @@ -91,3 +149,19 @@ export function lazyObserver (node: Element, onVisible: (value: boolean, unsubsc
update
}
}

export function lazyObserverOnce (
node: Element,
onVisible: (value: boolean, unsubscribe?: () => void) => void,
rootMargin = '20%'
): any {
return lazyObserver(node, onVisible, { mode: 'once', rootMargin })
}

export function lazyObserverContinuous (
node: Element,
onVisible: (value: boolean, unsubscribe?: () => void) => void,
rootMargin = '20%'
): any {
return lazyObserver(node, onVisible, { mode: 'continuous', rootMargin })
}
Loading