Skip to content

Commit d7accc5

Browse files
committed
feat: collaborative description editing
Signed-off-by: grnd-alt <git@belakkaf.net>
1 parent 49ed7c3 commit d7accc5

5 files changed

Lines changed: 195 additions & 3 deletions

File tree

lib/AppInfo/Application.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use OCA\Deck\Event\CardCreatedEvent;
2525
use OCA\Deck\Event\CardDeletedEvent;
2626
use OCA\Deck\Event\CardUpdatedEvent;
27+
use OCA\Deck\Event\RegisterTextContextEventListener;
2728
use OCA\Deck\Event\SessionClosedEvent;
2829
use OCA\Deck\Event\SessionCreatedEvent;
2930
use OCA\Deck\Federation\DeckFederationProvider;
@@ -52,6 +53,7 @@
5253
use OCA\Deck\Teams\DeckTeamResourceProvider;
5354
use OCA\Deck\UserMigration\DeckMigrator;
5455
use OCA\Text\Event\LoadEditor;
56+
use OCA\Text\Event\RegisterContextEvent;
5557
use OCP\AppFramework\App;
5658
use OCP\AppFramework\Bootstrap\IBootContext;
5759
use OCP\AppFramework\Bootstrap\IBootstrap;
@@ -189,6 +191,8 @@ public function register(IRegistrationContext $context): void {
189191
$context->registerTeamResourceProvider(DeckTeamResourceProvider::class);
190192

191193
$context->registerUserMigrator(DeckMigrator::class);
194+
195+
$context->registerEventListener(RegisterContextEvent::class, RegisterTextContextEventListener::class);
192196
}
193197

194198
public function registerCommentsEntity(IEventDispatcher $eventDispatcher): void {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace OCA\Deck\Event;
11+
12+
use OCA\Deck\Provider\TextContextProviderFactory;
13+
use OCA\Text\Event\RegisterContextEvent;
14+
use OCP\EventDispatcher\Event;
15+
use OCP\EventDispatcher\IEventListener;
16+
17+
class RegisterTextContextEventListener implements IEventListener{
18+
public function __construct(
19+
private readonly TextContextProviderFactory $textContextProviderFactory,
20+
){}
21+
public function handle(Event $event): void{
22+
if (!$event instanceof RegisterContextEvent) {
23+
return;
24+
}
25+
26+
$event->getContextManager()->registerContext(
27+
'deck_card',
28+
TextContextProviderFactory::class
29+
);
30+
}
31+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCA\Deck\Provider;
10+
11+
use Co\Http\Server;
12+
use OC\Security\SecureRandom;
13+
use OCA\Deck\Db\Acl;
14+
use OCA\Deck\Db\CardMapper;
15+
use OCA\Deck\Service\CardService;
16+
use OCA\Deck\Service\PermissionService;
17+
use OCA\Text\Context\DocumentData;
18+
use OCA\Text\Context\IContext;
19+
use OCA\Text\Context\SessionInfo;
20+
use OCA\Text\Db\Document;
21+
use OCA\Text\Exception\DocumentSaveConflictException;
22+
use OCP\Files\File;
23+
24+
class TextContextProvider implements IContext {
25+
public function __construct(
26+
private readonly SecureRandom $secureRandom,
27+
private readonly CardService $cardService,
28+
private readonly CardMapper $cardMapper,
29+
private readonly PermissionService $permissionService,
30+
private readonly string $userId,
31+
private readonly int $cardId,
32+
) {
33+
}
34+
public function getId(): int {
35+
return $this->cardId;
36+
}
37+
38+
public function getType(): string {
39+
return 'deck_card';
40+
}
41+
42+
public function toString(): string {
43+
return $this->getType() . ' (' . $this->getId() . ')';
44+
}
45+
46+
public function buildDocument(): Document{
47+
$document = new Document();
48+
$document->setContextType($this->getType());
49+
$document->setContextId($this->getId());
50+
$document->setLastSavedVersion(0);
51+
$document->setLastSavedVersionTime(1);
52+
$document->setLastSavedVersionEtag($this->secureRandom->generate(6));
53+
54+
$document->setChecksum($this->computeChecksum());
55+
$document->setBaseVersionEtag(uniqid());
56+
return $document;
57+
}
58+
59+
public function prepareSession(DocumentData $documentData): SessionInfo {
60+
if ($documentData->documentState === null) {
61+
$content = null;
62+
} else {
63+
$content = $this->cardService->find($this->cardId)->getDescription();
64+
}
65+
$readonly = !$this->permissionService->checkPermission($this->cardMapper, $this->cardId, Acl::PERMISSION_EDIT, $this->userId);
66+
return new SessionInfo(
67+
content: $content,
68+
readOnly: $readonly,
69+
lock: null,
70+
hasOwner: true,
71+
);
72+
}
73+
74+
public function isReadOnly(): bool {
75+
return !$this->permissionService->checkPermission($this->cardMapper, $this->cardId, Acl::PERMISSION_EDIT, $this->userId);
76+
}
77+
78+
public function updateDocument(Document $document): ?Document {
79+
if ($this->computeChecksum() !== $document->getChecksum()) {
80+
$card = $this->cardService->find($this->cardId);
81+
throw new DocumentSaveConflictException($card->getDescription());
82+
}
83+
return null;
84+
}
85+
86+
public function getFile(): ?File{
87+
return null;
88+
}
89+
90+
public function loadContent(): ?string {
91+
$card = $this->cardService->find($this->cardId);
92+
if ($card === null) {
93+
return null;
94+
}
95+
return $card->getDescription();
96+
}
97+
98+
public function saveWithLock(string $content, callable $doWhileLocked): void {
99+
$card = $this->cardService->find($this->cardId);
100+
if ($card === null) {
101+
return;
102+
}
103+
$card->setDescription($content);
104+
$this->cardService->update($card->getId(), $card->getTitle(), $card->getStackId(), $card->getType(), $card->getOwner(), $card->getDescription(), $card->getOrder());
105+
}
106+
107+
public function cleanup(): void {
108+
}
109+
110+
private function computeChecksum(): string {
111+
$card = $this->cardService->find($this->cardId);
112+
if ($card === null) {
113+
return '';
114+
}
115+
return hash('crc32', $card->getDescription());
116+
}
117+
118+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
namespace OCA\Deck\Provider;
9+
10+
use OC\Security\SecureRandom;
11+
use OCA\Deck\Db\CardMapper;
12+
use OCA\Deck\Service\CardService;
13+
use OCA\Deck\Service\PermissionService;
14+
use OCA\Text\Context\IContext;
15+
use OCA\Text\Context\IContextFactory;
16+
use OCP\IUser;
17+
use OCP\Share\IShare;
18+
19+
class TextContextProviderFactory implements IContextFactory{
20+
public function __construct(
21+
private readonly SecureRandom $secureRandom,
22+
private readonly CardService $cardService,
23+
private readonly CardMapper $cardMapper,
24+
private readonly PermissionService $permissionService,
25+
private readonly string $userId,
26+
) {
27+
}
28+
29+
public function build(IUser|IShare $auth, string $type, int $id): IContext {
30+
return new TextContextProvider(
31+
secureRandom: $this->secureRandom,
32+
cardService: $this->cardService,
33+
cardMapper: $this->cardMapper,
34+
permissionService: $this->permissionService,
35+
userId: $this->userId,
36+
cardId: $id,
37+
);
38+
}
39+
}

src/components/card/Description.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export default {
117117
},
118118
data() {
119119
return {
120-
textAppAvailable: !!window.OCA?.Text?.createEditor,
120+
textAppAvailable: !!window.OCA?.Text?.createCollaborativeEditor,
121121
editor: null,
122122
keyExitState: 0,
123123
descriptionOld: '',
@@ -205,9 +205,9 @@ export default {
205205
this.descriptionLastEdit = 0
206206
this.descriptionOld = this.card.description
207207
this.description = this.card.description
208-
this.editor = await window.OCA.Text.createEditor({
208+
this.editor = await window.OCA.Text.createCollaborativeEditor({
209+
context: { type: 'deck_card', id: this.card.id },
209210
el: this.$refs.editor,
210-
content: this.card.description,
211211
readOnly: !this.canEdit,
212212
onLoaded: () => {
213213
this.descriptionLastEdit = 0

0 commit comments

Comments
 (0)