Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
use OCA\Whiteboard\Listener\BeforeTemplateRenderedListener;
use OCA\Whiteboard\Listener\LoadTextEditorListener;
use OCA\Whiteboard\Listener\LoadViewerListener;
use OCA\Whiteboard\Listener\RegisterDirectEditorListener;
use OCA\Whiteboard\Listener\RegisterTemplateCreatorListener;
use OCA\Whiteboard\Settings\SetupCheck;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\DirectEditing\RegisterDirectEditorEvent;
use OCP\Files\Template\ITemplateManager;
use OCP\Files\Template\RegisterTemplateCreatorEvent;
use OCP\IL10N;
Expand All @@ -48,6 +50,7 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(LoadViewer::class, LoadTextEditorListener::class);
$context->registerEventListener(RegisterTemplateCreatorEvent::class, RegisterTemplateCreatorListener::class);
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
$context->registerEventListener(RegisterDirectEditorEvent::class, RegisterDirectEditorListener::class);
$context->registerSetupCheck(SetupCheck::class);
}

Expand Down
43 changes: 43 additions & 0 deletions lib/DirectEditing/WhiteboardCreator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Whiteboard\DirectEditing;

use OCP\DirectEditing\ACreateEmpty;
use OCP\IL10N;

class WhiteboardCreator extends ACreateEmpty {

public const CREATOR_ID = 'whiteboard';

public function __construct(
private IL10N $l10n,
) {
}

#[\Override]
public function getId(): string {
return self::CREATOR_ID;
}

#[\Override]
public function getName(): string {
return $this->l10n->t('whiteboard');
}

#[\Override]
public function getExtension(): string {
return 'whiteboard';
}

#[\Override]
public function getMimetype(): string {
return 'application/vnd.excalidraw+json';
}
}
97 changes: 97 additions & 0 deletions lib/DirectEditing/WhiteboardDirectEditor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Whiteboard\DirectEditing;

use OCA\Whiteboard\AppInfo\Application;
use OCA\Whiteboard\Exception\UnauthorizedException;
use OCA\Whiteboard\Service\Authentication\AuthenticateUserServiceFactory;
use OCA\Whiteboard\Service\ConfigService;
use OCA\Whiteboard\Service\JWTService;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\DirectEditing\IEditor;
use OCP\DirectEditing\IToken;
use OCP\Files\InvalidPathException;
use OCP\Files\NotFoundException;
use OCP\IL10N;
use OCP\Util;

class WhiteboardDirectEditor implements IEditor {

/** @psalm-suppress PossiblyUnusedMethod */
public function __construct(
private IL10N $l10n,
private IInitialState $initialState,
private ConfigService $configService,
private JWTService $jwtService,
private AuthenticateUserServiceFactory $authenticateUserServiceFactory,
) {
}

#[\Override]
public function getId(): string {
return Application::APP_ID;
}

#[\Override]
public function getName(): string {
return $this->l10n->t('Whiteboard');
}

#[\Override]
public function getMimetypes(): array {
return [
'application/vnd.excalidraw+json',
];
}

#[\Override]
public function getMimetypesOptional(): array {
return [];
}

#[\Override]
public function getCreators(): array {
return [
new WhiteboardCreator($this->l10n),
];
}

#[\Override]
public function isSecure(): bool {
return false;
}

#[\Override]
public function open(IToken $token): Response {
$token->useTokenScope();

try {
$file = $token->getFile();

Util::addScript('whiteboard', 'whiteboard-main');
Util::addStyle('whiteboard', 'whiteboard-main');

$user = $this->authenticateUserServiceFactory->create(null)->authenticate();
$jwt = $this->jwtService->generateJWT($user, $file, false);

$this->initialState->provideInitialState('file_id', $file->getId());
$this->initialState->provideInitialState('directEditing', true);
$this->initialState->provideInitialState('jwt', $jwt);
$this->initialState->provideInitialState('collabBackendUrl', $this->configService->getCollabBackendUrl());

return new TemplateResponse(Application::APP_ID, 'directEditing', [], 'base');
} catch (InvalidPathException|NotFoundException|UnauthorizedException) {
return new NotFoundResponse();
}
}
}
37 changes: 37 additions & 0 deletions lib/Listener/RegisterDirectEditorListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Whiteboard\Listener;

use OCA\Whiteboard\DirectEditing\WhiteboardDirectEditor;
use OCP\DirectEditing\RegisterDirectEditorEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;

/** @template-implements IEventListener<Event|RegisterDirectEditorEvent> */
/**
* @psalm-suppress UndefinedClass
* @psalm-suppress MissingTemplateParam
*/
final class RegisterDirectEditorListener implements IEventListener {

/** @psalm-suppress PossiblyUnusedMethod */
public function __construct(
private WhiteboardDirectEditor $editor,
) {
}

#[\Override]
public function handle(Event $event): void {
if (!$event instanceof RegisterDirectEditorEvent) {
return;
}
$event->register($this->editor);
}
}
7 changes: 7 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { VotingSidebar } from './components/VotingSidebar'
import { useVoting } from './hooks/useVoting'
import { useContextMenuFilter } from './hooks/useContextMenuFilter'
import { useDisableExternalLibraries } from './hooks/useDisableExternalLibraries'
import { callMobileMessage } from './utils/mobileInterface'

const Excalidraw = memo(ExcalidrawComponent)

Expand Down Expand Up @@ -287,6 +288,12 @@ export default function App({
// Use the board data manager hook
const { saveOnUnmount, isLoading } = useBoardDataManager()

useEffect(() => {
if (!isLoading && loadState('whiteboard', 'directEditing', false)) {
callMobileMessage('loaded')
}
}, [isLoading])

// Effect to handle fileId changes - cleanup previous board data
useEffect(() => {
// Clear any existing Excalidraw data when fileId changes
Expand Down
46 changes: 46 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
renderWhiteboardView,
} from './utils/renderWhiteboardView'
import type { WhiteboardRootHandle } from './utils/renderWhiteboardView'
import { callMobileMessage } from './utils/mobileInterface'

declare global {
interface Window {
Expand All @@ -38,6 +39,12 @@ type PublicShareContext = {
sharingToken: string | null
}

type DirectEditingContext = {
fileId: number
collabBackendUrl: string
jwt: string
}

type ViewerContext = {
collabBackendUrl: string
resolveSharingToken: () => string | null
Expand All @@ -46,6 +53,7 @@ type ViewerContext = {
type RuntimeDescriptor =
| { type: 'recording'; context: RecordingContext }
| { type: 'public-share'; context: PublicShareContext }
| { type: 'direct-editing'; context: DirectEditingContext }
| { type: 'viewer'; context: ViewerContext }

const VIEWER_REGISTRATION_ATTEMPTS = 3
Expand All @@ -61,6 +69,9 @@ const bootstrapWhiteboardRuntime = (): void => {
case 'public-share':
runPublicShareRuntime(runtime.context)
return
case 'direct-editing':
runDirectEditingRuntime(runtime.context)
return
case 'viewer':
default:
runDefaultViewerRuntime(runtime.context)
Expand All @@ -84,6 +95,17 @@ const detectRuntime = (): RuntimeDescriptor => {
}
}

if (loadState('whiteboard', 'directEditing', false)) {
return {
type: 'direct-editing',
context: {
fileId,
collabBackendUrl,
jwt: loadState('whiteboard', 'jwt', ''),
},
}
}

if (isPublicShare()) {
return {
type: 'public-share',
Expand Down Expand Up @@ -126,6 +148,30 @@ function runRecordingRuntime(context: RecordingContext): void {
})
}

function runDirectEditingRuntime(context: DirectEditingContext): void {
runWhenDomReady(async () => {
await primeRecordingJwt(context.fileId, context.jwt)

const whiteboardElement = document.getElementById('whiteboard-app')
if (!whiteboardElement) {
logger.error('Direct editing mount element not found')
return
}

callMobileMessage('loading')

renderWhiteboardView(whiteboardElement, {
fileId: context.fileId,
isEmbedded: false,
fileName: '',
publicSharingToken: null,
collabBackendUrl: context.collabBackendUrl,
versionSource: null,
fileVersion: null,
})
})
}

function runPublicShareRuntime(context: PublicShareContext): void {
const viewerContext: ViewerContext = {
collabBackendUrl: context.collabBackendUrl,
Expand Down
51 changes: 51 additions & 0 deletions src/utils/mobileInterface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

declare global {
interface Window {
DirectEditingMobileInterface?: {
[key: string]: (arg?: string) => void
}
webkit?: {
messageHandlers?: {
DirectEditingMobileInterface?: {
postMessage: (message: unknown) => void
}
}
}
}
}

export function callMobileMessage(messageName: string, attributes?: unknown): void {
let message: unknown = messageName
if (typeof attributes !== 'undefined') {
message = {
MessageName: messageName,
Values: attributes,
}
}

let attributesString: string | null = null
try {
attributesString = JSON.stringify(attributes)
} catch {
attributesString = null
}

if (window.DirectEditingMobileInterface
&& typeof window.DirectEditingMobileInterface[messageName] === 'function') {
if (attributesString === null || typeof attributesString === 'undefined') {
window.DirectEditingMobileInterface[messageName]()
} else {
window.DirectEditingMobileInterface[messageName](attributesString)
}
}

if (window.webkit?.messageHandlers?.DirectEditingMobileInterface) {
window.webkit.messageHandlers.DirectEditingMobileInterface.postMessage(message)
}

window.postMessage(message)
}
30 changes: 30 additions & 0 deletions templates/directEditing.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
?>

<style>
body {
position: fixed;
background-color: var(--color-main-background);
}

#whiteboard-app {
width: 100%;
height: 100%;
position: fixed;
}

#body-public footer {
position: static;
left: auto;
bottom: auto;
transform: none;
width: auto;
max-width: none;
}
</style>

<div id="whiteboard-app" class="whiteboard"></div>
Loading