Commit 0c187ef7 authored by Yassine Doghri's avatar Yassine Doghri
Browse files

feat(comments): add like / undo like to comment + add comment page

parent bb4752c3
......@@ -771,12 +771,24 @@ $routes->group('@(:podcastHandle)', function ($routes): void {
'controller-method' => 'EpisodeController::comments/$1/$2',
],
]);
$routes->get('comments/(:uuid)', 'EpisodeController::comment/$1/$2/$3', [
$routes->get('comments/(:uuid)', 'EpisodeCommentController::view/$1/$2/$3', [
'as' => 'comment',
'application/activity+json' => [
'controller-method' => 'EpisodeController::commentObject/$1/$2',
],
'application/podcast-activity+json' => [
'controller-method' => 'EpisodeController::commentObject/$1/$2',
],
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'controller-method' => 'EpisodeController::commentObject/$1/$2',
],
]);
$routes->get('comments/(:uuid)/replies', 'EpisodeController::commentReplies/$1/$2/$3', [
$routes->get('comments/(:uuid)/replies', 'EpisodeCommentController::replies/$1/$2/$3', [
'as' => 'comment-replies',
]);
$routes->post('comments/(:uuid)/like', 'EpisodeCommentController::attemptLike/$1/$2/$3', [
'as' => 'comment-attempt-like',
]);
$routes->get('oembed.json', 'EpisodeController::oembedJSON/$1/$2', [
'as' => 'episode-oembed-json',
]);
......
......@@ -10,13 +10,13 @@ declare(strict_types=1);
namespace App\Controllers\Admin;
use App\Entities\Comment;
use App\Entities\Episode;
use App\Entities\EpisodeComment;
use App\Entities\Image;
use App\Entities\Location;
use App\Entities\Podcast;
use App\Entities\Post;
use App\Models\CommentModel;
use App\Models\EpisodeCommentModel;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use App\Models\PostModel;
......@@ -800,7 +800,7 @@ class EpisodeController extends BaseController
$message = $this->request->getPost('message');
$newComment = new Comment([
$newComment = new EpisodeComment([
'actor_id' => interact_as_actor_id(),
'episode_id' => $this->episode->id,
'message' => $message,
......@@ -808,7 +808,7 @@ class EpisodeController extends BaseController
'created_by' => user_id(),
]);
$commentModel = new CommentModel();
$commentModel = new EpisodeCommentModel();
if (
! $commentModel->addComment($newComment, true)
) {
......
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use ActivityPub\Entities\Actor;
use ActivityPub\Objects\OrderedCollectionObject;
use ActivityPub\Objects\OrderedCollectionPage;
use Analytics\AnalyticsTrait;
use App\Controllers\Admin\BaseController;
use App\Entities\Episode;
use App\Entities\EpisodeComment;
use App\Entities\Podcast;
use App\Libraries\CommentObject;
use App\Models\EpisodeCommentModel;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\Response;
class EpisodeCommentController extends BaseController
{
use AnalyticsTrait;
protected Podcast $podcast;
protected Actor $actor;
protected Episode $episode;
protected EpisodeComment $comment;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) < 3) {
throw PageNotFoundException::forPageNotFound();
}
if (
($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) === null
) {
throw PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
$this->actor = $podcast->actor;
if (
($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) === null
) {
throw PageNotFoundException::forPageNotFound();
}
$this->episode = $episode;
if (
($comment = (new EpisodeCommentModel())->getCommentById($params[2])) === null
) {
throw PageNotFoundException::forPageNotFound();
}
$this->comment = $comment;
unset($params[2]);
unset($params[1]);
unset($params[0]);
return $this->{$method}(...$params);
}
public function view(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$cacheName = implode(
'_',
array_filter([
'page',
"comment#{$this->comment->id}",
service('request')
->getLocale(),
can_user_interact() ? '_authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'episode' => $this->episode,
'comment' => $this->comment,
];
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
helper('form');
return view('podcast/comment_authenticated', $data);
}
return view('podcast/comment', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
/**
* @noRector ReturnTypeDeclarationRector
*/
public function commentObject(): Response
{
$commentObject = new CommentObject($this->comment);
return $this->response
->setContentType('application/json')
->setBody($commentObject->toJSON());
}
public function replies(): Response
{
/**
* get comment replies
*/
$commentReplies = model('CommentModel', false)
->where('in_reply_to_id', service('uuid')->fromString($this->comment->id)->getBytes())
->orderBy('created_at', 'ASC');
$pageNumber = (int) $this->request->getGet('page');
if ($pageNumber < 1) {
$commentReplies->paginate(12);
$pager = $commentReplies->pager;
$collection = new OrderedCollectionObject(null, $pager);
} else {
$paginatedReplies = $commentReplies->paginate(12, 'default', $pageNumber);
$pager = $commentReplies->pager;
$orderedItems = [];
if ($paginatedReplies !== null) {
foreach ($paginatedReplies as $reply) {
$replyObject = new CommentObject($reply);
$orderedItems[] = $replyObject;
}
}
$collection = new OrderedCollectionPage($pager, $orderedItems);
}
return $this->response
->setContentType('application/activity+json')
->setBody($collection->toJSON());
}
public function attemptLike(): RedirectResponse
{
model('LikeModel')
->toggleLike(interact_as_actor(), $this->comment);
return redirect()->back();
}
}
......@@ -15,10 +15,8 @@ use ActivityPub\Objects\OrderedCollectionPage;
use Analytics\AnalyticsTrait;
use App\Entities\Episode;
use App\Entities\Podcast;
use App\Libraries\CommentObject;
use App\Libraries\NoteObject;
use App\Libraries\PodcastEpisode;
use App\Models\CommentModel;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Database\BaseBuilder;
......@@ -256,57 +254,4 @@ class EpisodeController extends BaseController
->setHeader('Access-Control-Allow-Origin', '*')
->setBody($collection->toJSON());
}
/**
* @noRector ReturnTypeDeclarationRector
*/
public function comment(string $commentId): Response
{
if (
($comment = (new CommentModel())->getCommentById($commentId)) === null
) {
throw PageNotFoundException::forPageNotFound();
}
$commentObject = new CommentObject($comment);
return $this->response
->setContentType('application/json')
->setBody($commentObject->toJSON());
}
public function commentReplies(string $commentId): Response
{
/**
* get comment replies
*/
$commentReplies = model('CommentModel', false)
->where('in_reply_to_id', service('uuid')->fromString($commentId)->getBytes())
->orderBy('created_at', 'ASC');
$pageNumber = (int) $this->request->getGet('page');
if ($pageNumber < 1) {
$commentReplies->paginate(12);
$pager = $commentReplies->pager;
$collection = new OrderedCollectionObject(null, $pager);
} else {
$paginatedReplies = $commentReplies->paginate(12, 'default', $pageNumber);
$pager = $commentReplies->pager;
$orderedItems = [];
if ($paginatedReplies !== null) {
foreach ($paginatedReplies as $reply) {
$replyObject = new CommentObject($reply);
$orderedItems[] = $replyObject;
}
}
$collection = new OrderedCollectionPage($pager, $orderedItems);
}
return $this->response
->setContentType('application/activity+json')
->setBody($collection->toJSON());
}
}
......@@ -3,9 +3,9 @@
declare(strict_types=1);
/**
* Class AddComments creates comments table in database
* Class AddEpisodeComments creates episode_comments table in database
*
* @copyright 2020 Podlibre
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
......@@ -14,7 +14,7 @@ namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddComments extends Migration
class AddEpisodeComments extends Migration
{
public function up(): void
{
......@@ -42,22 +42,16 @@ class AddComments extends Migration
],
'message' => [
'type' => 'VARCHAR',
'constraint' => 500,
'null' => true,
'constraint' => 5000,
],
'message_html' => [
'type' => 'VARCHAR',
'constraint' => 600,
'null' => true,
'constraint' => 6000,
],
'likes_count' => [
'type' => 'INT',
'unsigned' => true,
],
'dislikes_count' => [
'type' => 'INT',
'unsigned' => true,
],
'replies_count' => [
'type' => 'INT',
'unsigned' => true,
......@@ -75,11 +69,11 @@ class AddComments extends Migration
$this->forge->addForeignKey('episode_id', 'episodes', 'id', '', 'CASCADE');
$this->forge->addForeignKey('actor_id', 'activitypub_actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->createTable('comments');
$this->forge->createTable('episode_comments');
}
public function down(): void
{
$this->forge->dropTable('comments');
$this->forge->dropTable('episode_comments');
}
}
<?php
declare(strict_types=1);
/**
* Class AddLikes Creates likes table in database
*
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace ActivityPub\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddLikes extends Migration
{
public function up(): void
{
$this->forge->addField([
'actor_id' => [
'type' => 'INT',
'unsigned' => true,
],
'comment_id' => [
'type' => 'BINARY',
'constraint' => 16,
],
]);
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT current_timestamp()');
$this->forge->addPrimaryKey(['actor_id', 'comment_id']);
$this->forge->addForeignKey('actor_id', 'activitypub_actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('comment_id', 'episode_comments', 'id', '', 'CASCADE');
$this->forge->createTable('likes');
}
public function down(): void
{
$this->forge->dropTable('likes');
}
}
......@@ -11,7 +11,7 @@ declare(strict_types=1);
namespace App\Entities;
use App\Libraries\SimpleRSSElement;
use App\Models\CommentModel;
use App\Models\EpisodeCommentModel;
use App\Models\PersonModel;
use App\Models\PodcastModel;
use App\Models\PostModel;
......@@ -122,7 +122,7 @@ class Episode extends Entity
protected ?array $posts = null;
/**
* @var Comment[]|null
* @var EpisodeComment[]|null
*/
protected ?array $comments = null;
......@@ -402,7 +402,7 @@ class Episode extends Entity
}
/**
* @return Comment[]
* @return EpisodeComment[]
*/
public function getComments(): array
{
......@@ -411,7 +411,7 @@ class Episode extends Entity
}
if ($this->comments === null) {
$this->comments = (new CommentModel())->getEpisodeComments($this->id);
$this->comments = (new EpisodeCommentModel())->getEpisodeComments($this->id);
}
return $this->comments;
......
......@@ -10,6 +10,7 @@ declare(strict_types=1);
namespace App\Entities;
use App\Models\EpisodeCommentModel;
use App\Models\EpisodeModel;
use CodeIgniter\I18n\Time;
use Michalsn\Uuid\UuidEntity;
......@@ -23,22 +24,28 @@ use RuntimeException;
* @property int $actor_id
* @property Actor|null $actor
* @property string $in_reply_to_id
* @property Comment|null $reply_to_comment
* @property EpisodeComment|null $reply_to_comment
* @property string $message
* @property string $message_html
* @property int $likes_count
* @property int $dislikes_count
* @property int $replies_count
* @property Time $created_at
* @property int $created_by
*
* @property EpisodeComment[] $replies
*/
class Comment extends UuidEntity
class EpisodeComment extends UuidEntity
{
protected ?Episode $episode = null;
protected ?Actor $actor = null;
protected ?Comment $reply_to_comment = null;
protected ?EpisodeComment $reply_to_comment = null;
/**
* @var EpisodeComment[]|null
*/
protected ?array $replies = null;
/**
* @var string[]
......@@ -57,7 +64,6 @@ class Comment extends UuidEntity
'message' => 'string',
'message_html' => 'string',
'likes_count' => 'integer',
'dislikes_count' => 'integer',
'replies_count' => 'integer',
'created_by' => 'integer',
'is_from_post' => 'boolean',
......@@ -96,6 +102,22 @@ class Comment extends UuidEntity
return $this->actor;
}
/**
* @return EpisodeComment[]
*/
public function getReplies(): array
{
if ($this->id === null) {
throw new RuntimeException('Comment must be created before getting replies.');
}
if ($this->replies === null) {
$this->replies = (new EpisodeCommentModel())->getCommentReplies($this->id);
}
return $this->replies;
}
public function setMessage(string $message): static
{
helper('activitypub');
......
<?php
declare(strict_types=1);
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use Michalsn\Uuid\UuidEntity;
/**
* @property int $actor_id
* @property string $comment_id
*/
class Like extends UuidEntity
{
/**
* @var string[]
*/
protected $uuids = ['comment_id'];
/**
* @var array<string, string>
*/
protected $casts = [
'actor_id' => 'integer',
'comment_id' => 'string',
];
}
......@@ -16,11 +16,8 @@ return [
'submit_reply' => 'Reply',
],
'like' => 'Like',
'dislike' => 'Dislike',
'replies' => '{numberOfReplies, plural,
one {# reply}
other {# replies}
}',
'reply' => 'Reply',
'view_replies' => 'View replies ({numberOfReplies})',
'block_actor' => 'Block user @{actorUsername}',
'block_domain' => 'Block domain @{actorDomain}',
'delete' => 'Delete comment',
......
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
return [
'form' => [
'episode_message_placeholder' => 'Saisissez un commentaire...',
'reply_to_placeholder' => 'Répondre à @{actorUsername}',
'submit' => 'Envoyer !',
'submit_reply' => 'Répondre',
],
'like' => 'J’aime',
'reply' => 'Répondre',
'replies' => '{numberOfReplies, plural,
one {# réponse}
other {# réponses}
}',
'block_actor' => 'Bloquer l’utilisateur @{actorUsername}',
'block_domain' => 'Bloquer le domaine @{actorDomain}',
'delete' => 'Supprimer le commentaire',
];
......@@ -19,7 +19,7 @@ return [
'Écrivez votre message pour l’épisode...',
'episode_url_placeholder' => 'URL de l’épisode',
'reply_to_placeholder' => 'Répondre à @{actorUsername}',
'submit' => 'Envoyer!',
'submit' => 'Envoyer!',
'submit_reply' => 'Répondre',
],
'favourites' => '{numberOfFavourites, plural,
......
......@@ -100,14 +100,12 @@ class FavouriteModel extends UuidModel
->where('id', service('uuid') ->fromString($post->id) ->getBytes())
->decrement('favourites_count');
$this->db
->table('activitypub_favourites')
->where([
'actor_id' => $actor->id,
'post_id' => service('uuid')
->fromString($post->id)
->getBytes(),