Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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');
Comment thread
benjaminfrueh marked this conversation as resolved.
Outdated
}

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

#[\Override]
public function getMimetype(): string {
return 'application/vnd.excalidraw+json';
}
}
98 changes: 98 additions & 0 deletions lib/DirectEditing/WhiteboardDirectEditor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?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');
Util::addScript('text', 'text-editor');

$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