From cd3f593c9d1336d613eb7fafe195e679c75f402d Mon Sep 17 00:00:00 2001 From: Hoang Pham Date: Wed, 17 Dec 2025 21:15:47 +0700 Subject: [PATCH] fix: use Viewer for public whiteboard shares on NC29 Signed-off-by: Hoang Pham --- .../BeforeTemplateRenderedListener.php | 4 + src/main.ts | 120 +++++++++++++----- src/styles/index.scss | 1 + src/styles/modes/_public-share.scss | 64 ++++++++++ 4 files changed, 155 insertions(+), 34 deletions(-) create mode 100644 src/styles/modes/_public-share.scss diff --git a/lib/Listener/BeforeTemplateRenderedListener.php b/lib/Listener/BeforeTemplateRenderedListener.php index 5ec35ec1..8eb4a881 100644 --- a/lib/Listener/BeforeTemplateRenderedListener.php +++ b/lib/Listener/BeforeTemplateRenderedListener.php @@ -37,6 +37,10 @@ public function handle(Event $event): void { return; } + if ($event->getScope() === BeforeTemplateRenderedEvent::SCOPE_PUBLIC_SHARE_AUTH) { + return; + } + try { $node = $event->getShare()->getNode(); } catch (NotFoundException) { diff --git a/src/main.ts b/src/main.ts index 91ec0e5c..50226e4c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -122,31 +122,66 @@ function runRecordingRuntime(context: RecordingContext): void { } function runPublicShareRuntime(context: PublicShareContext): void { - let hasRegisteredViewer = false + document.body.classList.add('whiteboard-public-share') + + // On NC29/30, there's a hidden input with id="mimetype" that we can check. + // On NC31+, this element doesn't exist. Since this script is only loaded + // for whiteboard files (via BeforeTemplateRenderedListener), we can safely + // skip this check if the element doesn't exist. + const mimetypeElmt = document.getElementById('mimetype') as HTMLInputElement | null + if (mimetypeElmt && mimetypeElmt.value !== 'application/vnd.excalidraw+json') { + return + } + + const viewerContext: ViewerContext = { + collabBackendUrl: context.collabBackendUrl, + resolveSharingToken: () => context.sharingToken, + } - const ensureViewerRegistered = () => { - if (hasRegisteredViewer) { + let hasOpenedInViewer = false + const openInViewer = (): void => { + if (hasOpenedInViewer) { return } - hasRegisteredViewer = true - runDefaultViewerRuntime({ - collabBackendUrl: context.collabBackendUrl, - resolveSharingToken: () => context.sharingToken, - }) - } + const viewerApi = getViewerApi() + if (!viewerApi) { + return + } - const tryBootstrap = (): boolean => { - const previewHost = document.getElementById('preview') || document.getElementById('imgframe') + if (typeof viewerApi.openWith !== 'function' && typeof viewerApi.open !== 'function') { + return + } - if (!previewHost) { - return false + hasOpenedInViewer = true + + try { + viewerApi.setRootElement?.(null) + } catch { + // ignore + } + + try { + if (typeof viewerApi.openWith === 'function') { + viewerApi.openWith('whiteboard', { path: '/', enableSidebar: false, canLoop: false }) + return + } + + viewerApi.open?.({ path: '/', enableSidebar: false, canLoop: false }) + } catch (error) { + hasOpenedInViewer = false + logger.error('Could not open public share in viewer', { error }) + } + } + + const openEmbeddedFallback = (): void => { + if (hasOpenedInViewer) { + return } - const mimetypeElmt = document.getElementById('mimetype') as HTMLInputElement | null - const isWhiteboard = mimetypeElmt?.value === 'application/vnd.excalidraw+json' - if (isPublicShare() && !isWhiteboard) { - return true + const previewHost = document.getElementById('preview') || document.getElementById('imgframe') + if (!previewHost) { + return } previewHost.innerHTML = '' @@ -163,25 +198,14 @@ function runPublicShareRuntime(context: PublicShareContext): void { versionSource: null, fileVersion: null, }) - return true } runWhenDomReady(() => { - ensureViewerRegistered() - - if (tryBootstrap()) { - return - } - - const observer = new MutationObserver(() => { - ensureViewerRegistered() - if (tryBootstrap()) { - observer.disconnect() - } - }) + registerViewerHandler(createWhiteboardComponent(viewerContext), 0, openInViewer) - observer.observe(document.body, { childList: true, subtree: true }) - window.setTimeout(() => observer.disconnect(), 20000) + window.setTimeout(() => { + openEmbeddedFallback() + }, 2500) }) } @@ -221,6 +245,29 @@ type ViewerHandlerRegistration = { type ViewerApi = { registerHandler?: (handler: ViewerHandlerRegistration) => void + open?: (options?: { + path?: string + fileInfo?: unknown + list?: unknown[] + enableSidebar?: boolean + loadMore?: () => unknown[] + canLoop?: boolean + onPrev?: () => void + onNext?: () => void + onClose?: () => void + }) => void + openWith?: (handlerId: string, options?: { + path?: string + fileInfo?: unknown + list?: unknown[] + enableSidebar?: boolean + loadMore?: () => unknown[] + canLoop?: boolean + onPrev?: () => void + onNext?: () => void + onClose?: () => void + }) => void + setRootElement?: (el?: string | null) => void compareFileInfo?: unknown } @@ -302,7 +349,11 @@ const createWhiteboardComponent = (options: ViewerComponentOptions): VueComponen data: (): WhiteboardComponentData => ({ root: null }), }) -const registerViewerHandler = (component: VueComponentDefinition, attempt = 0): void => { +const registerViewerHandler = ( + component: VueComponentDefinition, + attempt = 0, + afterRegister?: () => void, +): void => { const viewerApi = getViewerApi() if (viewerApi?.registerHandler) { @@ -314,6 +365,7 @@ const registerViewerHandler = (component: VueComponentDefinition, attempt = 0): theme: 'default', canCompare: true, }) + afterRegister?.() return } @@ -323,7 +375,7 @@ const registerViewerHandler = (component: VueComponentDefinition, attempt = 0): } window.setTimeout( - () => registerViewerHandler(component, attempt + 1), + () => registerViewerHandler(component, attempt + 1, afterRegister), VIEWER_REGISTRATION_DELAY_MS, ) } diff --git a/src/styles/index.scss b/src/styles/index.scss index acf2bb93..05961fa6 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -6,3 +6,4 @@ @use './globals/layout'; @use './overrides/excalidraw'; @use './modes/viewer'; +@use './modes/public-share'; diff --git a/src/styles/modes/_public-share.scss b/src/styles/modes/_public-share.scss new file mode 100644 index 00000000..e8149f5c --- /dev/null +++ b/src/styles/modes/_public-share.scss @@ -0,0 +1,64 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#body-public.whiteboard-public-share { + --header-height: 0px; + --footer-height: 0px; + --whiteboard-public-share-main-text: var(--color-main-text); +} + +#body-public.whiteboard-public-share #header { + display: none; +} + +#body-public.whiteboard-public-share #content.app-files_sharing { + --body-container-margin: 0px; + --body-container-radius: 0px; + --footer-height: 0px; + --body-height: calc(100% - env(safe-area-inset-bottom)); + margin: 0 !important; +} + +#body-public.whiteboard-public-share #content.app-files_sharing + footer { + display: none; +} + +#body-public.whiteboard-public-share #content.app-files_sharing #app-content { + overflow: hidden; +} + +#body-public.whiteboard-public-share #content.app-files_sharing #files-public-content, +#body-public.whiteboard-public-share #content.app-files_sharing #preview { + height: 100%; +} + +#body-public.whiteboard-public-share #content.app-files_sharing #preview { + position: relative; +} + +#body-public.whiteboard-public-share #content.app-files_sharing .whiteboard { + height: 100%; + width: 100%; + inset: 0; +} + +#body-public.whiteboard-public-share .network-status, +#body-public.whiteboard-public-share .grid-toggle-button { + display: none; +} + +#body-public.whiteboard-public-share .excalidraw .sidebar-trigger__label { + line-height: 1.2; + height: auto; +} + +#body-public.whiteboard-public-share #viewer .modal-header .button-vue { + color: var(--whiteboard-public-share-main-text) !important; +} + +#body-public.whiteboard-public-share #viewer .whiteboard { + top: 50px; + height: calc(100% - 50px); +}