Skip to content
Snippets Groups Projects
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
No related branches found
No related tags found
No related merge requests found
Showing
with 559 additions and 148 deletions
...@@ -771,12 +771,24 @@ $routes->group('@(:podcastHandle)', function ($routes): void { ...@@ -771,12 +771,24 @@ $routes->group('@(:podcastHandle)', function ($routes): void {
'controller-method' => 'EpisodeController::comments/$1/$2', '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', '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', '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', [ $routes->get('oembed.json', 'EpisodeController::oembedJSON/$1/$2', [
'as' => 'episode-oembed-json', 'as' => 'episode-oembed-json',
]); ]);
......
...@@ -10,13 +10,13 @@ declare(strict_types=1); ...@@ -10,13 +10,13 @@ declare(strict_types=1);
namespace App\Controllers\Admin; namespace App\Controllers\Admin;
use App\Entities\Comment;
use App\Entities\Episode; use App\Entities\Episode;
use App\Entities\EpisodeComment;
use App\Entities\Image; use App\Entities\Image;
use App\Entities\Location; use App\Entities\Location;
use App\Entities\Podcast; use App\Entities\Podcast;
use App\Entities\Post; use App\Entities\Post;
use App\Models\CommentModel; use App\Models\EpisodeCommentModel;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use App\Models\PostModel; use App\Models\PostModel;
...@@ -800,7 +800,7 @@ class EpisodeController extends BaseController ...@@ -800,7 +800,7 @@ class EpisodeController extends BaseController
$message = $this->request->getPost('message'); $message = $this->request->getPost('message');
$newComment = new Comment([ $newComment = new EpisodeComment([
'actor_id' => interact_as_actor_id(), 'actor_id' => interact_as_actor_id(),
'episode_id' => $this->episode->id, 'episode_id' => $this->episode->id,
'message' => $message, 'message' => $message,
...@@ -808,7 +808,7 @@ class EpisodeController extends BaseController ...@@ -808,7 +808,7 @@ class EpisodeController extends BaseController
'created_by' => user_id(), 'created_by' => user_id(),
]); ]);
$commentModel = new CommentModel(); $commentModel = new EpisodeCommentModel();
if ( if (
! $commentModel->addComment($newComment, true) ! $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; ...@@ -15,10 +15,8 @@ use ActivityPub\Objects\OrderedCollectionPage;
use Analytics\AnalyticsTrait; use Analytics\AnalyticsTrait;
use App\Entities\Episode; use App\Entities\Episode;
use App\Entities\Podcast; use App\Entities\Podcast;
use App\Libraries\CommentObject;
use App\Libraries\NoteObject; use App\Libraries\NoteObject;
use App\Libraries\PodcastEpisode; use App\Libraries\PodcastEpisode;
use App\Models\CommentModel;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\BaseBuilder;
...@@ -256,57 +254,4 @@ class EpisodeController extends BaseController ...@@ -256,57 +254,4 @@ class EpisodeController extends BaseController
->setHeader('Access-Control-Allow-Origin', '*') ->setHeader('Access-Control-Allow-Origin', '*')
->setBody($collection->toJSON()); ->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 @@ ...@@ -3,9 +3,9 @@
declare(strict_types=1); 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 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
...@@ -14,7 +14,7 @@ namespace App\Database\Migrations; ...@@ -14,7 +14,7 @@ namespace App\Database\Migrations;
use CodeIgniter\Database\Migration; use CodeIgniter\Database\Migration;
class AddComments extends Migration class AddEpisodeComments extends Migration
{ {
public function up(): void public function up(): void
{ {
...@@ -42,22 +42,16 @@ class AddComments extends Migration ...@@ -42,22 +42,16 @@ class AddComments extends Migration
], ],
'message' => [ 'message' => [
'type' => 'VARCHAR', 'type' => 'VARCHAR',
'constraint' => 500, 'constraint' => 5000,
'null' => true,
], ],
'message_html' => [ 'message_html' => [
'type' => 'VARCHAR', 'type' => 'VARCHAR',
'constraint' => 600, 'constraint' => 6000,
'null' => true,
], ],
'likes_count' => [ 'likes_count' => [
'type' => 'INT', 'type' => 'INT',
'unsigned' => true, 'unsigned' => true,
], ],
'dislikes_count' => [
'type' => 'INT',
'unsigned' => true,
],
'replies_count' => [ 'replies_count' => [
'type' => 'INT', 'type' => 'INT',
'unsigned' => true, 'unsigned' => true,
...@@ -75,11 +69,11 @@ class AddComments extends Migration ...@@ -75,11 +69,11 @@ class AddComments extends Migration
$this->forge->addForeignKey('episode_id', 'episodes', 'id', '', 'CASCADE'); $this->forge->addForeignKey('episode_id', 'episodes', 'id', '', 'CASCADE');
$this->forge->addForeignKey('actor_id', 'activitypub_actors', 'id', '', 'CASCADE'); $this->forge->addForeignKey('actor_id', 'activitypub_actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('created_by', 'users', 'id'); $this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->createTable('comments'); $this->forge->createTable('episode_comments');
} }
public function down(): void 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); ...@@ -11,7 +11,7 @@ declare(strict_types=1);
namespace App\Entities; namespace App\Entities;
use App\Libraries\SimpleRSSElement; use App\Libraries\SimpleRSSElement;
use App\Models\CommentModel; use App\Models\EpisodeCommentModel;
use App\Models\PersonModel; use App\Models\PersonModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use App\Models\PostModel; use App\Models\PostModel;
...@@ -122,7 +122,7 @@ class Episode extends Entity ...@@ -122,7 +122,7 @@ class Episode extends Entity
protected ?array $posts = null; protected ?array $posts = null;
/** /**
* @var Comment[]|null * @var EpisodeComment[]|null
*/ */
protected ?array $comments = null; protected ?array $comments = null;
...@@ -402,7 +402,7 @@ class Episode extends Entity ...@@ -402,7 +402,7 @@ class Episode extends Entity
} }
/** /**
* @return Comment[] * @return EpisodeComment[]
*/ */
public function getComments(): array public function getComments(): array
{ {
...@@ -411,7 +411,7 @@ class Episode extends Entity ...@@ -411,7 +411,7 @@ class Episode extends Entity
} }
if ($this->comments === null) { if ($this->comments === null) {
$this->comments = (new CommentModel())->getEpisodeComments($this->id); $this->comments = (new EpisodeCommentModel())->getEpisodeComments($this->id);
} }
return $this->comments; return $this->comments;
......
...@@ -10,6 +10,7 @@ declare(strict_types=1); ...@@ -10,6 +10,7 @@ declare(strict_types=1);
namespace App\Entities; namespace App\Entities;
use App\Models\EpisodeCommentModel;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
use Michalsn\Uuid\UuidEntity; use Michalsn\Uuid\UuidEntity;
...@@ -23,22 +24,28 @@ use RuntimeException; ...@@ -23,22 +24,28 @@ use RuntimeException;
* @property int $actor_id * @property int $actor_id
* @property Actor|null $actor * @property Actor|null $actor
* @property string $in_reply_to_id * @property string $in_reply_to_id
* @property Comment|null $reply_to_comment * @property EpisodeComment|null $reply_to_comment
* @property string $message * @property string $message
* @property string $message_html * @property string $message_html
* @property int $likes_count * @property int $likes_count
* @property int $dislikes_count
* @property int $replies_count * @property int $replies_count
* @property Time $created_at * @property Time $created_at
* @property int $created_by * @property int $created_by
*
* @property EpisodeComment[] $replies
*/ */
class Comment extends UuidEntity class EpisodeComment extends UuidEntity
{ {
protected ?Episode $episode = null; protected ?Episode $episode = null;
protected ?Actor $actor = 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[] * @var string[]
...@@ -57,7 +64,6 @@ class Comment extends UuidEntity ...@@ -57,7 +64,6 @@ class Comment extends UuidEntity
'message' => 'string', 'message' => 'string',
'message_html' => 'string', 'message_html' => 'string',
'likes_count' => 'integer', 'likes_count' => 'integer',
'dislikes_count' => 'integer',
'replies_count' => 'integer', 'replies_count' => 'integer',
'created_by' => 'integer', 'created_by' => 'integer',
'is_from_post' => 'boolean', 'is_from_post' => 'boolean',
...@@ -96,6 +102,22 @@ class Comment extends UuidEntity ...@@ -96,6 +102,22 @@ class Comment extends UuidEntity
return $this->actor; 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 public function setMessage(string $message): static
{ {
helper('activitypub'); 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 [ ...@@ -16,11 +16,8 @@ return [
'submit_reply' => 'Reply', 'submit_reply' => 'Reply',
], ],
'like' => 'Like', 'like' => 'Like',
'dislike' => 'Dislike', 'reply' => 'Reply',
'replies' => '{numberOfReplies, plural, 'view_replies' => 'View replies ({numberOfReplies})',
one {# reply}
other {# replies}
}',
'block_actor' => 'Block user @{actorUsername}', 'block_actor' => 'Block user @{actorUsername}',
'block_domain' => 'Block domain @{actorDomain}', 'block_domain' => 'Block domain @{actorDomain}',
'delete' => 'Delete comment', '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 [ ...@@ -19,7 +19,7 @@ return [
'Écrivez votre message pour l’épisode...', 'Écrivez votre message pour l’épisode...',
'episode_url_placeholder' => 'URL de l’épisode', 'episode_url_placeholder' => 'URL de l’épisode',
'reply_to_placeholder' => 'Répondre à @{actorUsername}', 'reply_to_placeholder' => 'Répondre à @{actorUsername}',
'submit' => 'Envoyer!', 'submit' => 'Envoyer !',
'submit_reply' => 'Répondre', 'submit_reply' => 'Répondre',
], ],
'favourites' => '{numberOfFavourites, plural, 'favourites' => '{numberOfFavourites, plural,
......
...@@ -100,14 +100,12 @@ class FavouriteModel extends UuidModel ...@@ -100,14 +100,12 @@ class FavouriteModel extends UuidModel
->where('id', service('uuid') ->fromString($post->id) ->getBytes()) ->where('id', service('uuid') ->fromString($post->id) ->getBytes())
->decrement('favourites_count'); ->decrement('favourites_count');
$this->db $this->where([
->table('activitypub_favourites') 'actor_id' => $actor->id,
->where([ 'post_id' => service('uuid')
'actor_id' => $actor->id, ->fromString($post->id)
'post_id' => service('uuid') ->getBytes(),
->fromString($post->id) ])
->getBytes(),
])
->delete(); ->delete();
if ($registerActivity) { if ($registerActivity) {
...@@ -161,7 +159,7 @@ class FavouriteModel extends UuidModel ...@@ -161,7 +159,7 @@ class FavouriteModel extends UuidModel
} }
/** /**
* Adds or removes favourite from database and increments count * Adds or removes favourite from database
*/ */
public function toggleFavourite(Actor $actor, Post $post): void public function toggleFavourite(Actor $actor, Post $post): void
{ {
......
...@@ -11,7 +11,7 @@ declare(strict_types=1); ...@@ -11,7 +11,7 @@ declare(strict_types=1);
namespace App\Libraries; namespace App\Libraries;
use ActivityPub\Core\ObjectType; use ActivityPub\Core\ObjectType;
use App\Entities\Comment; use App\Entities\EpisodeComment;
class CommentObject extends ObjectType class CommentObject extends ObjectType
{ {
...@@ -23,7 +23,7 @@ class CommentObject extends ObjectType ...@@ -23,7 +23,7 @@ class CommentObject extends ObjectType
protected string $replies; protected string $replies;
public function __construct(Comment $comment) public function __construct(EpisodeComment $comment)
{ {
$this->id = $comment->uri; $this->id = $comment->uri;
......
...@@ -11,22 +11,22 @@ declare(strict_types=1); ...@@ -11,22 +11,22 @@ declare(strict_types=1);
namespace App\Models; namespace App\Models;
use ActivityPub\Activities\CreateActivity; use ActivityPub\Activities\CreateActivity;
use App\Entities\Comment; use App\Entities\EpisodeComment;
use App\Libraries\CommentObject; use App\Libraries\CommentObject;
use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\BaseBuilder;
use Michalsn\Uuid\UuidModel; use Michalsn\Uuid\UuidModel;
class CommentModel extends UuidModel class EpisodeCommentModel extends UuidModel
{ {
/** /**
* @var string * @var string
*/ */
protected $returnType = Comment::class; protected $returnType = EpisodeComment::class;
/** /**
* @var string * @var string
*/ */
protected $table = 'comments'; protected $table = 'episode_comments';
/** /**
* @var string[] * @var string[]
...@@ -45,7 +45,6 @@ class CommentModel extends UuidModel ...@@ -45,7 +45,6 @@ class CommentModel extends UuidModel
'message', 'message',
'message_html', 'message_html',
'likes_count', 'likes_count',
'dislikes_count',
'replies_count', 'replies_count',
'created_at', 'created_at',
'created_by', 'created_by',
...@@ -56,7 +55,7 @@ class CommentModel extends UuidModel ...@@ -56,7 +55,7 @@ class CommentModel extends UuidModel
*/ */
protected $beforeInsert = ['setCommentId']; protected $beforeInsert = ['setCommentId'];
public function getCommentById(string $commentId): ?Comment public function getCommentById(string $commentId): ?EpisodeComment
{ {
$cacheName = "comment#{$commentId}"; $cacheName = "comment#{$commentId}";
if (! ($found = cache($cacheName))) { if (! ($found = cache($cacheName))) {
...@@ -69,7 +68,7 @@ class CommentModel extends UuidModel ...@@ -69,7 +68,7 @@ class CommentModel extends UuidModel
return $found; return $found;
} }
public function addComment(Comment $comment, bool $registerActivity = false): string | false public function addComment(EpisodeComment $comment, bool $registerActivity = false): string | false
{ {
$this->db->transStart(); $this->db->transStart();
// increment Episode's comments_count // increment Episode's comments_count
...@@ -122,7 +121,9 @@ class CommentModel extends UuidModel ...@@ -122,7 +121,9 @@ class CommentModel extends UuidModel
/** /**
* Retrieves all published posts for a given episode ordered by publication date * Retrieves all published posts for a given episode ordered by publication date
* *
* @return Comment[] * @return EpisodeComment[]
*
* @noRector ReturnTypeDeclarationRector
*/ */
public function getEpisodeComments(int $episodeId): array public function getEpisodeComments(int $episodeId): array
{ {
...@@ -133,7 +134,7 @@ class CommentModel extends UuidModel ...@@ -133,7 +134,7 @@ class CommentModel extends UuidModel
$episodePostsReplies = $this->db->table('activitypub_posts') $episodePostsReplies = $this->db->table('activitypub_posts')
->select( ->select(
'id, uri, episode_id, actor_id, in_reply_to_id, message, message_html, favourites_count as likes_count, 0 as dislikes_count, replies_count, published_at as created_at, created_by, 1 as is_from_post' 'id, uri, episode_id, actor_id, in_reply_to_id, message, message_html, favourites_count as likes_count, replies_count, published_at as created_at, created_by, 1 as is_from_post'
) )
->whereIn('in_reply_to_id', function (BaseBuilder $builder) use (&$episodeId): BaseBuilder { ->whereIn('in_reply_to_id', function (BaseBuilder $builder) use (&$episodeId): BaseBuilder {
return $builder->select('id') return $builder->select('id')
...@@ -147,18 +148,25 @@ class CommentModel extends UuidModel ...@@ -147,18 +148,25 @@ class CommentModel extends UuidModel
$episodeComments . ' UNION ' . $episodePostsReplies . ' ORDER BY created_at ASC' $episodeComments . ' UNION ' . $episodePostsReplies . ' ORDER BY created_at ASC'
); );
return $allEpisodeComments->getCustomResultObject($this->returnType); // FIXME:?
// @phpstan-ignore-next-line
return $this->convertUuidFieldsToStrings(
$allEpisodeComments->getCustomResultObject($this->tempReturnType),
$this->tempReturnType
);
} }
/** /**
* Retrieves all replies for a given comment * Retrieves all replies for a given comment
* *
* @return Comment[] * @return EpisodeComment[]
*/ */
public function getCommentReplies(int $episodeId, string $commentId): array public function getCommentReplies(string $commentId): array
{ {
// TODO: get all replies for a given comment // TODO: get all replies for a given comment
return $this->findAll(); return $this->where('in_reply_to_id', $this->uuid->fromString($commentId)->getBytes())
->orderBy('created_at', 'ASC')
->findAll();
} }
/** /**
......
<?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\Models;
use ActivityPub\Activities\LikeActivity;
use ActivityPub\Activities\UndoActivity;
use ActivityPub\Entities\Actor;
use App\Entities\EpisodeComment;
use App\Entities\Like;
use Michalsn\Uuid\UuidModel;
class LikeModel extends UuidModel
{
/**
* @var string
*/
protected $table = 'likes';
/**
* @var string[]
*/
protected $uuidFields = ['comment_id'];
/**
* @var string[]
*/
protected $allowedFields = ['actor_id', 'comment_id'];
/**
* @var string
*/
protected $returnType = Like::class;
/**
* @var bool
*/
protected $useTimestamps = true;
protected $updatedField;
public function addLike(Actor $actor, EpisodeComment $comment, bool $registerActivity = true): void
{
$this->db->transStart();
$this->insert([
'actor_id' => $actor->id,
'comment_id' => $comment->id,
]);
(new EpisodeCommentModel())
->where('id', service('uuid')->fromString($comment->id)->getBytes())
->increment('likes_count');
if ($registerActivity) {
$likeActivity = new LikeActivity();
$likeActivity->set('actor', $actor->uri)
->set('object', $comment->uri);
$activityId = model('ActivityModel')
->newActivity(
'Like',
$actor->id,
null,
null,
$likeActivity->toJSON(),
$comment->created_at,
'queued',
);
$likeActivity->set('id', url_to('activity', $actor->username, $activityId));
model('ActivityModel')
->update($activityId, [
'payload' => $likeActivity->toJSON(),
]);
}
$this->db->transComplete();
}
public function removeLike(Actor $actor, EpisodeComment $comment, bool $registerActivity = true): void
{
$this->db->transStart();
(new EpisodeCommentModel())
->where('id', service('uuid') ->fromString($comment->id) ->getBytes())
->decrement('likes_count');
$this->where([
'actor_id' => $actor->id,
'comment_id' => service('uuid')
->fromString($comment->id)
->getBytes(),
])
->delete();
if ($registerActivity) {
$undoActivity = new UndoActivity();
// FIXME: get like activity associated with the deleted like
$activity = model('ActivityModel')
->where([
'type' => 'Like',
'actor_id' => $actor->id,
])
->first();
$likeActivity = new LikeActivity();
$likeActivity
->set('id', url_to('activity', $actor->username, $activity->id))
->set('actor', $actor->uri)
->set('object', $comment->uri);
$undoActivity
->set('actor', $actor->uri)
->set('object', $likeActivity);
$activityId = model('ActivityModel')
->newActivity(
'Undo',
$actor->id,
null,
null,
$undoActivity->toJSON(),
$comment->created_at,
'queued',
);
$undoActivity->set('id', url_to('activity', $actor->username, $activityId));
model('ActivityModel')
->update($activityId, [
'payload' => $undoActivity->toJSON(),
]);
}
$this->db->transComplete();
}
/**
* Adds or removes likes from database
*/
public function toggleLike(Actor $actor, EpisodeComment $comment): void
{
if (
$this->where([
'actor_id' => $actor->id,
'comment_id' => service('uuid')
->fromString($comment->id)
->getBytes(),
])->first()
) {
$this->removeLike($actor, $comment);
} else {
$this->addLike($actor, $comment);
}
}
}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M22 15h-3V3h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1zm-5.293 1.293l-6.4 6.4a.5.5 0 0 1-.654.047L8.8 22.1a1.5 1.5 0 0 1-.553-1.57L9.4 16H3a2 2 0 0 1-2-2v-2.104a2 2 0 0 1 .15-.762L4.246 3.62A1 1 0 0 1 5.17 3H16a1 1 0 0 1 1 1v11.586a1 1 0 0 1-.293.707z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M2 9h3v12H2a1 1 0 0 1-1-1V10a1 1 0 0 1 1-1zm5.293-1.293l6.4-6.4a.5.5 0 0 1 .654-.047l.853.64a1.5 1.5 0 0 1 .553 1.57L14.6 8H21a2 2 0 0 1 2 2v2.104a2 2 0 0 1-.15.762l-3.095 7.515a1 1 0 0 1-.925.619H8a1 1 0 0 1-1-1V8.414a1 1 0 0 1 .293-.707z"/>
</g>
</svg>
<article class="relative z-10 flex w-full px-4 py-2 rounded-2xl"> <article class="relative z-10 flex w-full px-4 py-2 rounded-2xl">
<img src="<?= $comment->actor->avatar_image_url ?>" alt="<?= $comment->display_name ?>" class="w-12 h-12 mr-4 rounded-full" /> <img src="<?= $comment->actor->avatar_image_url ?>" alt="<?= $comment->display_name ?>" class="w-12 h-12 mr-4 rounded-full" />
<div class="flex-1"> <div class="flex-1">
<header class="w-full mb-2"> <header class="w-full mb-2 text-sm">
<a href="<?= $comment->actor <a href="<?= $comment->actor
->uri ?>" class="flex items-baseline hover:underline" <?= $comment->actor->is_local ->uri ?>" class="flex items-baseline hover:underline" <?= $comment->actor->is_local
? '' ? ''
...@@ -17,27 +17,10 @@ ...@@ -17,27 +17,10 @@
</a> </a>
</header> </header>
<div class="mb-2 post-content"><?= $comment->message_html ?></div> <div class="mb-2 post-content"><?= $comment->message_html ?></div>
<div class="inline-flex gap-x-4"> <?php if ($comment->is_from_post): ?>
<?= anchor_popup( <?= $this->include('podcast/_partials/comment_actions_from_post') ?>
route_to('comment-remote-action', $podcast->handle, $episode->slug, $comment->id, 'like'), <?php else: ?>
icon('thumb-up', 'text-lg mr-1 text-gray-400 group-hover:text-gray-600') . 0, <?= $this->include('podcast/_partials/comment_actions') ?>
[ <?php endif; ?>
'class' => 'inline-flex items-center hover:underline group',
'width' => 420,
'height' => 620,
'title' => lang('Comment.like'),
],
) ?>
<?= anchor_popup(
route_to('comment-remote-action', $podcast->handle, $episode->slug, $comment->id, 'dislike'),
icon('thumb-down', 'text-lg text-gray-400 group-hover:text-gray-600'),
[
'class' => 'inline-flex items-center hover:underline group',
'width' => 420,
'height' => 620,
'title' => lang('Comment.dislike'),
],
) ?>
</div>
</div> </div>
</article> </article>
<footer>
<form action="<?= route_to('comment-attempt-like', interact_as_actor()->username, $episode->slug, $comment->id) ?>" method="POST" class="flex items-center gap-x-4">
<button type="submit" name="action" value="favourite" class="inline-flex items-center hover:underline group" title="<?= lang(
'Comment.like',
[
'numberOfLikes' => $comment->likes_count,
],
) ?>"><?= icon('heart', 'text-xl mr-1 text-gray-400 group-hover:text-red-600') . $comment->likes_count ?></button>
<?= button(
lang('Comment.reply'),
route_to('comment', $podcast->handle, $episode->slug, $comment->id),
[
'size' => 'small',
],
) ?>
</form>
<?php if($comment->replies_count): ?>
<?= anchor(
route_to('comment', $podcast->handle, $episode->slug, $comment->id),
icon('caret-down', 'text-xl mr-1') . lang('Comment.view_replies', ['numberOfReplies' => $comment->replies_count]),
['class' => 'inline-flex items-center text-xs hover:underline']
) ?>
<?php endif; ?>
</footer>
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment