diff --git a/app/Comment.php b/app/Comment.php index 34abdcf29..8588982e5 100644 --- a/app/Comment.php +++ b/app/Comment.php @@ -1,12 +1,12 @@ <?php namespace BookStack; -use Illuminate\Support\Facades\DB; class Comment extends Ownable { + public $sub_comments = []; protected $fillable = ['text', 'html', 'parent_id']; - protected $appends = ['created', 'updated']; + protected $appends = ['created', 'updated', 'sub_comments']; /** * Get the entity that this comment belongs to * @return \Illuminate\Database\Eloquent\Relations\MorphTo @@ -34,24 +34,38 @@ class Comment extends Ownable return $this->belongsTo(User::class); } - public function getCommentsByPage($pageId, $commentId, $pageNum = 0, $limit = 0) { - + public function getPageComments($pageId) { $query = static::newQuery(); $query->join('users AS u', 'comments.created_by', '=', 'u.id'); $query->leftJoin('users AS u1', 'comments.updated_by', '=', 'u1.id'); $query->leftJoin('images AS i', 'i.id', '=', 'u.image_id'); - $query->selectRaw('comments.id, text, html, comments.created_by, comments.updated_by, comments.created_at, comments.updated_at, ' + $query->selectRaw('comments.id, text, html, comments.created_by, comments.updated_by, ' + . 'comments.created_at, comments.updated_at, comments.parent_id, ' . 'u.name AS created_by_name, u1.name AS updated_by_name, ' - . '(SELECT count(c.id) FROM bookstack.comments c WHERE c.parent_id = comments.id AND page_id = ?) AS cnt_sub_comments, i.url AS avatar ', - [$pageId]); - - if (empty($commentId)) { - $query->whereRaw('page_id = ? AND parent_id IS NULL', [$pageId]); - } else { - $query->whereRaw('page_id = ? AND parent_id = ?', [$pageId, $commentId]); - } + . 'i.url AS avatar '); + $query->whereRaw('page_id = ?', [$pageId]); $query->orderBy('created_at'); - return $query; + return $query->get(); + } + + public function getAllPageComments($pageId) { + return self::where('page_id', '=', $pageId)->with(['createdBy' => function($query) { + $query->select('id', 'name', 'image_id'); + }, 'updatedBy' => function($query) { + $query->select('id', 'name'); + }, 'createdBy.avatar' => function ($query) { + $query->select('id', 'path', 'url'); + }])->get(); + } + + public function getCommentById($commentId) { + return self::where('id', '=', $commentId)->with(['createdBy' => function($query) { + $query->select('id', 'name', 'image_id'); + }, 'updatedBy' => function($query) { + $query->select('id', 'name'); + }, 'createdBy.avatar' => function ($query) { + $query->select('id', 'path', 'url'); + }])->first(); } public function getCreatedAttribute() { @@ -72,4 +86,8 @@ class Comment extends Ownable ]; return $updated; } + + public function getSubCommentsAttribute() { + return $this->sub_comments; + } } diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php index e1729bbee..29ccdf5a7 100644 --- a/app/Http/Controllers/CommentController.php +++ b/app/Http/Controllers/CommentController.php @@ -54,9 +54,12 @@ class CommentController extends Controller $respMsg = trans('entities.comment_updated'); } + $comment = $this->commentRepo->getCommentById($comment->id); + return response()->json([ 'status' => 'success', - 'message' => $respMsg + 'message' => $respMsg, + 'comment' => $comment ]); } @@ -64,11 +67,10 @@ class CommentController extends Controller public function destroy($id) { $comment = $this->comment->findOrFail($id); $this->checkOwnablePermission('comment-delete', $comment); - - // } - public function getCommentThread($pageId, $commentId = null) { + + public function getPageComments($pageId) { try { $page = $this->entityRepo->getById('page', $pageId, true); } catch (ModelNotFoundException $e) { @@ -85,12 +87,7 @@ class CommentController extends Controller $this->checkOwnablePermission('page-view', $page); - $comments = $this->commentRepo->getCommentsForPage($pageId, $commentId); - if (empty($commentId)) { - // requesting for parent level comments, send the total count as well. - $totalComments = $this->commentRepo->getCommentCount($pageId); - return response()->json(['success' => true, 'comments'=> $comments, 'total' => $totalComments]); - } - return response()->json(['success' => true, 'comments'=> $comments]); + $comments = $this->commentRepo->getPageComments($pageId); + return response()->json(['success' => true, 'comments'=> $comments['comments'], 'total' => $comments['total']]); } } diff --git a/app/Repos/CommentRepo.php b/app/Repos/CommentRepo.php index 7e4955d55..7d0c4ebd7 100644 --- a/app/Repos/CommentRepo.php +++ b/app/Repos/CommentRepo.php @@ -39,13 +39,48 @@ class CommentRepo { return $comment; } - public function getCommentsForPage($pageId, $commentId, $count = 20) { - // requesting parent comments - $query = $this->comment->getCommentsByPage($pageId, $commentId); - return $query->paginate($count); + public function getPageComments($pageId) { + $comments = $this->comment->getAllPageComments($pageId); + $index = []; + $totalComments = count($comments); + // normalizing the response. + foreach($comments as &$comment) { + $comment = $this->normalizeComment($comment); + $parentId = $comment->parent_id; + if (empty($parentId)) { + $index[$comment->id] = $comment; + continue; + } + + if (empty($index[$parentId])) { + // weird condition should not happen. + continue; + } + if (empty($index[$parentId]->sub_comments)) { + $index[$parentId]->sub_comments = []; + } + array_push($index[$parentId]->sub_comments, $comment); + $index[$comment->id] = $comment; + } + return [ + 'comments' => $comments, + 'total' => $totalComments + ]; } - public function getCommentCount($pageId) { - return $this->comment->where('page_id', '=', $pageId)->count(); + public function getCommentById($commentId) { + return $this->normalizeComment($this->comment->getCommentById($commentId)); + } + + private function normalizeComment($comment) { + if (empty($comment)) { + return; + } + $comment->createdBy->avatar_url = $comment->createdBy->getAvatar(50); + $comment->createdBy->profile_url = $comment->createdBy->getProfileUrl(); + if (!empty($comment->updatedBy)) { + $comment->updatedBy->profile_url = $comment->updatedBy->getProfileUrl(); + } + return $comment; } } \ No newline at end of file diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index 198089056..f64d7c038 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -714,10 +714,18 @@ module.exports = function (ngApp, events) { return events.emit('error', trans('error')); } if ($scope.isEdit) { - $scope.comment.html = commentHTML; + $scope.comment.html = resp.data.comment.html; + $scope.comment.text = resp.data.comment.text; + $scope.comment.updated = resp.data.comment.updated; + $scope.comment.updated_by = resp.data.comment.updated_by; $scope.$emit('evt.comment-success', $scope.comment.id); } else { $scope.comment.text = ''; + if ($scope.isReply === true && $scope.parent.sub_comments) { + $scope.parent.sub_comments.push(resp.data.comment); + } else { + $scope.$emit('evt.new-comment', resp.data.comment); + } $scope.$emit('evt.comment-success', null, true); } events.emit('success', trans(resp.data.message)); @@ -747,9 +755,14 @@ module.exports = function (ngApp, events) { $scope.errors = {}; // keep track of comment levels $scope.level = 1; - $scope.defaultAvatar = defaultAvatar; vm.totalCommentsStr = 'Loading...'; + $scope.$on('evt.new-comment', function (event, comment) { + // add the comment to the comment list. + vm.comments.push(comment); + event.stopPropagation(); + event.preventDefault(); + }); $timeout(function() { $http.get(window.baseUrl(`/ajax/page/${$scope.pageId}/comments/`)).then(resp => { @@ -757,7 +770,7 @@ module.exports = function (ngApp, events) { // TODO : Handle error return; } - vm.comments = resp.data.comments.data; + vm.comments = resp.data.comments; vm.totalComments = resp.data.total; // TODO : Fetch message from translate. if (vm.totalComments === 0) { @@ -770,21 +783,10 @@ module.exports = function (ngApp, events) { }, checkError('app')); }); - vm.loadSubComments = function(event, comment) { - event.preventDefault(); - $http.get(window.baseUrl(`/ajax/page/${$scope.pageId}/comments/${comment.id}/sub-comments`)).then(resp => { - if (!resp.data || resp.data.success !== true) { - return; - } - comment.is_loaded = true; - comment.comments = resp.data.comments.data; - }, checkError('app')); - }; - function checkError(errorGroupName) { $scope.errors[errorGroupName] = {}; return function(response) { - console.log(resp); + console.log(response); } } }]); diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 6c556acc9..ff0f93cfa 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -825,10 +825,12 @@ module.exports = function (ngApp, events) { templateUrl: 'comment-reply.html', scope: { pageId: '=', - parentId: '=' + parentId: '=', + parent: '=' }, link: function (scope, element) { scope.isReply = true; + element.find('textarea').focus(); scope.$on('evt.comment-success', function (event) { // no need for the event to do anything more. event.stopPropagation(); @@ -849,6 +851,7 @@ module.exports = function (ngApp, events) { }, link: function (scope, element) { scope.isEdit = true; + element.find('textarea').focus(); scope.$on('evt.comment-success', function (event, commentId) { // no need for the event to do anything more. event.stopPropagation(); @@ -892,7 +895,7 @@ module.exports = function (ngApp, events) { function compileHtml($container, scope, isReply) { let lnkFunc = null; if (isReply) { - lnkFunc = $compile('<comment-reply page-id="comment.pageId" parent-id="comment.id"></comment-reply>'); + lnkFunc = $compile('<comment-reply page-id="comment.pageId" parent-id="comment.id" parent="comment"></comment-reply>'); } else { lnkFunc = $compile('<comment-edit comment="comment"></comment-add>'); } diff --git a/resources/assets/sass/_comments.scss b/resources/assets/sass/_comments.scss index 7d7cb486a..0328341c3 100644 --- a/resources/assets/sass/_comments.scss +++ b/resources/assets/sass/_comments.scss @@ -4,12 +4,7 @@ } .comment-box:last-child { - border-bottom: none; - } - .load-more-comments { - font-size: 0.8em; - margin-top: -1px; - margin-bottom: 6px; + border-bottom: 0px; } } .page-comment { @@ -42,11 +37,11 @@ } } - .comment-actions.has-border { + .comment-actions { border-bottom: 1px solid #DDD; } - .comment-actions.has-border:last-child { + .comment-actions:last-child { border-bottom: 0px; } diff --git a/resources/views/comments/comments.blade.php b/resources/views/comments/comments.blade.php index 8c4cb9860..93e7ebc05 100644 --- a/resources/views/comments/comments.blade.php +++ b/resources/views/comments/comments.blade.php @@ -4,13 +4,13 @@ <script type="text/ng-template" id="comment-reply.html"> @include('comments/comment-reply', ['pageId' => $pageId]) </script> -<div ng-controller="CommentListController as vm" ng-init="pageId = <?= $page->id ?>" class="comments-list" ng-cloak> +<div ng-controller="CommentListController as vm" ng-init="pageId = <?= $page->id ?>" class="comments-list" ng-cloak> <h3>@{{vm.totalCommentsStr}}</h3> -<hr> - <div class="comment-box" ng-repeat="comment in vm.comments track by comment.id"> +<hr> + <div class="comment-box" ng-repeat="comment in vm.comments track by comment.id"> <div ng-include src="'comment-list-item.html'"> - + </div> </div> -</div> -@include('comments/comment-reply', ['pageId' => $pageId]) \ No newline at end of file + @include('comments/comment-reply', ['pageId' => $pageId]) +</div> \ No newline at end of file diff --git a/resources/views/comments/list-item.blade.php b/resources/views/comments/list-item.blade.php index ca8b58625..46af1a862 100644 --- a/resources/views/comments/list-item.blade.php +++ b/resources/views/comments/list-item.blade.php @@ -1,28 +1,24 @@ <div class='page-comment' id="comment-@{{::pageId}}-@{{::comment.id}}"> <div class="user-image"> - <img ng-src="@{{::defaultAvatar}}" alt="user avatar"> + <img ng-src="@{{::comment.created_by.avatar_url}}" alt="user avatar"> </div> <div class="comment-container"> <div class="comment-header"> - @{{ ::comment.created_by_name }} + <a href="@{{::comment.created_by.profile_url}}">@{{ ::comment.created_by.name }}</a> </div> <div ng-bind-html="comment.html" class="comment-body"> </div> - <div class="comment-actions" ng-class="{'has-border': comment.cnt_sub_comments === 0 || comment.is_loaded}"> + <div class="comment-actions"> <ul> <li ng-if="level < 3"><a href="#" comment-reply-link no-comment-reply-dupe="true" comment="comment" is-reply="true">Reply</a></li> <li><a href="#" comment-reply-link no-comment-reply-dupe="true" comment="comment">Edit</a></li> <li>Created <a title="@{{::comment.created.day_time_str}}" href="#comment-@{{::comment.id}}-@{{::pageId}}">@{{::comment.created.diff}}</a></li> - <li ng-if="comment.updated"><span title="@{{::comment.updated.day_time_str}}">Updated @{{::comment.updated.diff}}</span></li> + <li ng-if="comment.updated"><span title="@{{comment.updated.day_time_str}}">Updated @{{comment.updated.diff}} by + <a href="@{{comment.updated_by.profile_url}}">@{{comment.updated_by.name}}</a></span></li> </ul> </div> - <div class="load-more-comments" ng-if="comment.cnt_sub_comments > 0 && !comment.is_loaded"> - <a href="#" ng-click="vm.loadSubComments($event, comment, $index)"> - Load @{{::comment.cnt_sub_comments}} more comment(s) - </a> - </div> - <div class="comment-box" ng-repeat="comment in comments = comment.comments track by comment.id" ng-init="level = level + 1"> + <div class="comment-box" ng-repeat="comment in comments = comment.sub_comments track by comment.id" ng-init="level = level + 1"> <div ng-include src="'comment-list-item.html'"> </div> </div> diff --git a/routes/web.php b/routes/web.php index 076bc8110..463e4e77b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -123,8 +123,7 @@ Route::group(['middleware' => 'auth'], function () { Route::post('/ajax/page/{pageId}/comment/', 'CommentController@save'); Route::put('/ajax/page/{pageId}/comment/{commentId}', 'CommentController@save'); Route::delete('/ajax/comment/{id}', 'CommentController@destroy'); - Route::get('/ajax/page/{pageId}/comments/{commentId}/sub-comments', 'CommentController@getCommentThread'); - Route::get('/ajax/page/{pageId}/comments/', 'CommentController@getCommentThread'); + Route::get('/ajax/page/{pageId}/comments/', 'CommentController@getPageComments'); // Links Route::get('/link/{id}', 'PageController@redirectFromLink');