Skip to content

Commit d29b14e

Browse files
authored
Merge pull request #5584 from BookStackApp/content_comments
Content Comments
2 parents cdd446a + 32b29fc commit d29b14e

40 files changed

+1743
-520
lines changed

app/Activity/CommentRepo.php

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
use BookStack\Activity\Models\Comment;
66
use BookStack\Entities\Models\Entity;
7+
use BookStack\Exceptions\NotifyException;
8+
use BookStack\Exceptions\PrettyException;
79
use BookStack\Facades\Activity as ActivityService;
810
use BookStack\Util\HtmlDescriptionFilter;
911

@@ -20,7 +22,7 @@ public function getById(int $id): Comment
2022
/**
2123
* Create a new comment on an entity.
2224
*/
23-
public function create(Entity $entity, string $html, ?int $parent_id): Comment
25+
public function create(Entity $entity, string $html, ?int $parentId, string $contentRef): Comment
2426
{
2527
$userId = user()->id;
2628
$comment = new Comment();
@@ -29,7 +31,8 @@ public function create(Entity $entity, string $html, ?int $parent_id): Comment
2931
$comment->created_by = $userId;
3032
$comment->updated_by = $userId;
3133
$comment->local_id = $this->getNextLocalId($entity);
32-
$comment->parent_id = $parent_id;
34+
$comment->parent_id = $parentId;
35+
$comment->content_ref = preg_match('/^bkmrk-(.*?):\d+:(\d*-\d*)?$/', $contentRef) === 1 ? $contentRef : '';
3336

3437
$entity->comments()->save($comment);
3538
ActivityService::add(ActivityType::COMMENT_CREATE, $comment);
@@ -52,6 +55,41 @@ public function update(Comment $comment, string $html): Comment
5255
return $comment;
5356
}
5457

58+
59+
/**
60+
* Archive an existing comment.
61+
*/
62+
public function archive(Comment $comment): Comment
63+
{
64+
if ($comment->parent_id) {
65+
throw new NotifyException('Only top-level comments can be archived.', '/', 400);
66+
}
67+
68+
$comment->archived = true;
69+
$comment->save();
70+
71+
ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
72+
73+
return $comment;
74+
}
75+
76+
/**
77+
* Un-archive an existing comment.
78+
*/
79+
public function unarchive(Comment $comment): Comment
80+
{
81+
if ($comment->parent_id) {
82+
throw new NotifyException('Only top-level comments can be un-archived.', '/', 400);
83+
}
84+
85+
$comment->archived = false;
86+
$comment->save();
87+
88+
ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
89+
90+
return $comment;
91+
}
92+
5593
/**
5694
* Delete a comment from the system.
5795
*/

app/Activity/Controllers/CommentController.php

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace BookStack\Activity\Controllers;
44

55
use BookStack\Activity\CommentRepo;
6+
use BookStack\Activity\Tools\CommentTree;
7+
use BookStack\Activity\Tools\CommentTreeNode;
68
use BookStack\Entities\Queries\PageQueries;
79
use BookStack\Http\Controller;
810
use Illuminate\Http\Request;
@@ -26,6 +28,7 @@ public function savePageComment(Request $request, int $pageId)
2628
$input = $this->validate($request, [
2729
'html' => ['required', 'string'],
2830
'parent_id' => ['nullable', 'integer'],
31+
'content_ref' => ['string'],
2932
]);
3033

3134
$page = $this->pageQueries->findVisibleById($pageId);
@@ -40,14 +43,12 @@ public function savePageComment(Request $request, int $pageId)
4043

4144
// Create a new comment.
4245
$this->checkPermission('comment-create-all');
43-
$comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null);
46+
$contentRef = $input['content_ref'] ?? '';
47+
$comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null, $contentRef);
4448

4549
return view('comments.comment-branch', [
4650
'readOnly' => false,
47-
'branch' => [
48-
'comment' => $comment,
49-
'children' => [],
50-
]
51+
'branch' => new CommentTreeNode($comment, 0, []),
5152
]);
5253
}
5354

@@ -74,6 +75,46 @@ public function update(Request $request, int $commentId)
7475
]);
7576
}
7677

78+
/**
79+
* Mark a comment as archived.
80+
*/
81+
public function archive(int $id)
82+
{
83+
$comment = $this->commentRepo->getById($id);
84+
$this->checkOwnablePermission('page-view', $comment->entity);
85+
if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) {
86+
$this->showPermissionError();
87+
}
88+
89+
$this->commentRepo->archive($comment);
90+
91+
$tree = new CommentTree($comment->entity);
92+
return view('comments.comment-branch', [
93+
'readOnly' => false,
94+
'branch' => $tree->getCommentNodeForId($id),
95+
]);
96+
}
97+
98+
/**
99+
* Unmark a comment as archived.
100+
*/
101+
public function unarchive(int $id)
102+
{
103+
$comment = $this->commentRepo->getById($id);
104+
$this->checkOwnablePermission('page-view', $comment->entity);
105+
if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) {
106+
$this->showPermissionError();
107+
}
108+
109+
$this->commentRepo->unarchive($comment);
110+
111+
$tree = new CommentTree($comment->entity);
112+
return view('comments.comment-branch', [
113+
'readOnly' => false,
114+
'branch' => $tree->getCommentNodeForId($id),
115+
]);
116+
}
117+
77118
/**
78119
* Delete a comment from the system.
79120
*/

app/Activity/Models/Comment.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
* @property int $entity_id
2020
* @property int $created_by
2121
* @property int $updated_by
22+
* @property string $content_ref
23+
* @property bool $archived
2224
*/
2325
class Comment extends Model implements Loggable
2426
{

app/Activity/Tools/CommentTree.php

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class CommentTree
99
{
1010
/**
1111
* The built nested tree structure array.
12-
* @var array{comment: Comment, depth: int, children: array}[]
12+
* @var CommentTreeNode[]
1313
*/
1414
protected array $tree;
1515
protected array $comments;
@@ -28,17 +28,43 @@ public function enabled(): bool
2828

2929
public function empty(): bool
3030
{
31-
return count($this->tree) === 0;
31+
return count($this->getActive()) === 0;
3232
}
3333

3434
public function count(): int
3535
{
3636
return count($this->comments);
3737
}
3838

39-
public function get(): array
39+
public function getActive(): array
4040
{
41-
return $this->tree;
41+
return array_filter($this->tree, fn (CommentTreeNode $node) => !$node->comment->archived);
42+
}
43+
44+
public function activeThreadCount(): int
45+
{
46+
return count($this->getActive());
47+
}
48+
49+
public function getArchived(): array
50+
{
51+
return array_filter($this->tree, fn (CommentTreeNode $node) => $node->comment->archived);
52+
}
53+
54+
public function archivedThreadCount(): int
55+
{
56+
return count($this->getArchived());
57+
}
58+
59+
public function getCommentNodeForId(int $commentId): ?CommentTreeNode
60+
{
61+
foreach ($this->tree as $node) {
62+
if ($node->comment->id === $commentId) {
63+
return $node;
64+
}
65+
}
66+
67+
return null;
4268
}
4369

4470
public function canUpdateAny(): bool
@@ -54,6 +80,7 @@ public function canUpdateAny(): bool
5480

5581
/**
5682
* @param Comment[] $comments
83+
* @return CommentTreeNode[]
5784
*/
5885
protected function createTree(array $comments): array
5986
{
@@ -77,26 +104,22 @@ protected function createTree(array $comments): array
77104

78105
$tree = [];
79106
foreach ($childMap[0] ?? [] as $childId) {
80-
$tree[] = $this->createTreeForId($childId, 0, $byId, $childMap);
107+
$tree[] = $this->createTreeNodeForId($childId, 0, $byId, $childMap);
81108
}
82109

83110
return $tree;
84111
}
85112

86-
protected function createTreeForId(int $id, int $depth, array &$byId, array &$childMap): array
113+
protected function createTreeNodeForId(int $id, int $depth, array &$byId, array &$childMap): CommentTreeNode
87114
{
88115
$childIds = $childMap[$id] ?? [];
89116
$children = [];
90117

91118
foreach ($childIds as $childId) {
92-
$children[] = $this->createTreeForId($childId, $depth + 1, $byId, $childMap);
119+
$children[] = $this->createTreeNodeForId($childId, $depth + 1, $byId, $childMap);
93120
}
94121

95-
return [
96-
'comment' => $byId[$id],
97-
'depth' => $depth,
98-
'children' => $children,
99-
];
122+
return new CommentTreeNode($byId[$id], $depth, $children);
100123
}
101124

102125
protected function loadComments(): array
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace BookStack\Activity\Tools;
4+
5+
use BookStack\Activity\Models\Comment;
6+
7+
class CommentTreeNode
8+
{
9+
public Comment $comment;
10+
public int $depth;
11+
12+
/**
13+
* @var CommentTreeNode[]
14+
*/
15+
public array $children;
16+
17+
public function __construct(Comment $comment, int $depth, array $children)
18+
{
19+
$this->comment = $comment;
20+
$this->depth = $depth;
21+
$this->children = $children;
22+
}
23+
}

database/factories/Activity/Models/CommentFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public function definition()
2727
'html' => $html,
2828
'parent_id' => null,
2929
'local_id' => 1,
30+
'content_ref' => '',
31+
'archived' => false,
3032
];
3133
}
3234
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*/
12+
public function up(): void
13+
{
14+
Schema::table('comments', function (Blueprint $table) {
15+
$table->string('content_ref');
16+
$table->boolean('archived')->index();
17+
});
18+
}
19+
20+
/**
21+
* Reverse the migrations.
22+
*/
23+
public function down(): void
24+
{
25+
Schema::table('comments', function (Blueprint $table) {
26+
$table->dropColumn('content_ref');
27+
$table->dropColumn('archived');
28+
});
29+
}
30+
};

lang/en/common.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
'create' => 'Create',
3131
'update' => 'Update',
3232
'edit' => 'Edit',
33+
'archive' => 'Archive',
34+
'unarchive' => 'Un-Archive',
3335
'sort' => 'Sort',
3436
'move' => 'Move',
3537
'copy' => 'Copy',

lang/en/entities.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,8 +392,11 @@
392392
'comment' => 'Comment',
393393
'comments' => 'Comments',
394394
'comment_add' => 'Add Comment',
395+
'comment_none' => 'No comments to display',
395396
'comment_placeholder' => 'Leave a comment here',
396-
'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments',
397+
'comment_thread_count' => ':count Comment Thread|:count Comment Threads',
398+
'comment_archived_count' => ':count Archived',
399+
'comment_archived_threads' => 'Archived Threads',
397400
'comment_save' => 'Save Comment',
398401
'comment_new' => 'New Comment',
399402
'comment_created' => 'commented :createDiff',
@@ -402,8 +405,14 @@
402405
'comment_deleted_success' => 'Comment deleted',
403406
'comment_created_success' => 'Comment added',
404407
'comment_updated_success' => 'Comment updated',
408+
'comment_archive_success' => 'Comment archived',
409+
'comment_unarchive_success' => 'Comment un-archived',
410+
'comment_view' => 'View comment',
411+
'comment_jump_to_thread' => 'Jump to thread',
405412
'comment_delete_confirm' => 'Are you sure you want to delete this comment?',
406413
'comment_in_reply_to' => 'In reply to :commentId',
414+
'comment_reference' => 'Reference',
415+
'comment_reference_outdated' => '(Outdated)',
407416
'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',
408417

409418
// Revision

resources/icons/archive.svg

Lines changed: 1 addition & 0 deletions
Loading

0 commit comments

Comments
 (0)