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

This commit is contained in:
Yassine Doghri 2021-08-13 16:07:45 +00:00
parent bb4752c35e
commit 0c187ef7a9
29 changed files with 760 additions and 152 deletions

View File

@ -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',
]);

View File

@ -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)
) {

View File

@ -0,0 +1,173 @@
<?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();
}
}

View File

@ -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());
}
}

View File

@ -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');
}
}

View File

@ -0,0 +1,42 @@
<?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');
}
}

View File

@ -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;

View File

@ -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');

33
app/Entities/Like.php Normal file
View File

@ -0,0 +1,33 @@
<?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',
];
}

View File

@ -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',

View File

@ -0,0 +1,27 @@
<?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' => 'Jaime',
'reply' => 'Répondre',
'replies' => '{numberOfReplies, plural,
one {# réponse}
other {# réponses}
}',
'block_actor' => 'Bloquer lutilisateur @{actorUsername}',
'block_domain' => 'Bloquer le domaine @{actorDomain}',
'delete' => 'Supprimer le commentaire',
];

View File

@ -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,

View File

@ -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(),
])
$this->where([
'actor_id' => $actor->id,
'post_id' => service('uuid')
->fromString($post->id)
->getBytes(),
])
->delete();
if ($registerActivity) {
@ -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
{

View File

@ -11,7 +11,7 @@ declare(strict_types=1);
namespace App\Libraries;
use ActivityPub\Core\ObjectType;
use App\Entities\Comment;
use App\Entities\EpisodeComment;
class CommentObject extends ObjectType
{
@ -23,7 +23,7 @@ class CommentObject extends ObjectType
protected string $replies;
public function __construct(Comment $comment)
public function __construct(EpisodeComment $comment)
{
$this->id = $comment->uri;

View File

@ -11,22 +11,22 @@ declare(strict_types=1);
namespace App\Models;
use ActivityPub\Activities\CreateActivity;
use App\Entities\Comment;
use App\Entities\EpisodeComment;
use App\Libraries\CommentObject;
use CodeIgniter\Database\BaseBuilder;
use Michalsn\Uuid\UuidModel;
class CommentModel extends UuidModel
class EpisodeCommentModel extends UuidModel
{
/**
* @var string
*/
protected $returnType = Comment::class;
protected $returnType = EpisodeComment::class;
/**
* @var string
*/
protected $table = 'comments';
protected $table = 'episode_comments';
/**
* @var string[]
@ -45,7 +45,6 @@ class CommentModel extends UuidModel
'message',
'message_html',
'likes_count',
'dislikes_count',
'replies_count',
'created_at',
'created_by',
@ -56,7 +55,7 @@ class CommentModel extends UuidModel
*/
protected $beforeInsert = ['setCommentId'];
public function getCommentById(string $commentId): ?Comment
public function getCommentById(string $commentId): ?EpisodeComment
{
$cacheName = "comment#{$commentId}";
if (! ($found = cache($cacheName))) {
@ -69,7 +68,7 @@ class CommentModel extends UuidModel
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();
// increment Episode's comments_count
@ -122,7 +121,9 @@ class CommentModel extends UuidModel
/**
* Retrieves all published posts for a given episode ordered by publication date
*
* @return Comment[]
* @return EpisodeComment[]
*
* @noRector ReturnTypeDeclarationRector
*/
public function getEpisodeComments(int $episodeId): array
{
@ -133,7 +134,7 @@ class CommentModel extends UuidModel
$episodePostsReplies = $this->db->table('activitypub_posts')
->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 {
return $builder->select('id')
@ -147,18 +148,25 @@ class CommentModel extends UuidModel
$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
*
* @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
return $this->findAll();
return $this->where('in_reply_to_id', $this->uuid->fromString($commentId)->getBytes())
->orderBy('created_at', 'ASC')
->findAll();
}
/**

165
app/Models/LikeModel.php Normal file
View File

@ -0,0 +1,165 @@
<?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);
}
}
}

View File

@ -1,6 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 393 B

View File

@ -1,6 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 391 B

View File

@ -1,7 +1,7 @@
<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" />
<div class="flex-1">
<header class="w-full mb-2">
<header class="w-full mb-2 text-sm">
<a href="<?= $comment->actor
->uri ?>" class="flex items-baseline hover:underline" <?= $comment->actor->is_local
? ''
@ -17,27 +17,10 @@
</a>
</header>
<div class="mb-2 post-content"><?= $comment->message_html ?></div>
<div class="inline-flex gap-x-4">
<?= anchor_popup(
route_to('comment-remote-action', $podcast->handle, $episode->slug, $comment->id, 'like'),
icon('thumb-up', 'text-lg mr-1 text-gray-400 group-hover:text-gray-600') . 0,
[
'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>
<?php if ($comment->is_from_post): ?>
<?= $this->include('podcast/_partials/comment_actions_from_post') ?>
<?php else: ?>
<?= $this->include('podcast/_partials/comment_actions') ?>
<?php endif; ?>
</div>
</article>

View File

@ -0,0 +1,24 @@
<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>

View File

@ -0,0 +1,24 @@
<footer>
<form action="<?= route_to('post-attempt-action', interact_as_actor()->username, $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('post', $podcast->handle, $comment->id),
[
'size' => 'small',
],
) ?>
</form>
<?php if($comment->replies_count): ?>
<?= anchor(
route_to('post', $podcast->handle, $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>

View File

@ -0,0 +1,27 @@
<article class="relative z-10 w-full bg-white shadow-md rounded-2xl">
<header class="flex px-6 py-4">
<img src="<?= $comment->actor
->avatar_image_url ?>" alt="<?= $comment->actor->display_name ?>" class="w-12 h-12 mr-4 rounded-full" />
<div class="flex flex-col min-w-0">
<a href="<?= $comment->actor
->uri ?>" class="flex items-baseline hover:underline" <?= $comment
->actor->is_local
? ''
: 'target="_blank" rel="noopener noreferrer"' ?>>
<span class="mr-2 font-semibold truncate"><?= $comment->actor
->display_name ?></span>
<span class="text-sm text-gray-500 truncate">@<?= $comment->actor
->username .
($comment->actor->is_local
? ''
: '@' . $comment->actor->domain) ?></span>
</a>
<a href="<?= route_to('comment', $podcast->handle, $episode->slug, $comment->id) ?>"
class="text-xs text-gray-500">
<?= relative_time($comment->created_at) ?>
</a>
</div>
</header>
<div class="px-6 mb-4 post-content"><?= $comment->message_html ?></div>
<?= $this->include('podcast/_partials/comment_actions') ?>
</article>

View File

@ -0,0 +1,22 @@
<?= $this->include('podcast/_partials/comment') ?>
<div class="-mt-2 overflow-hidden border-b border-l border-r comment-replies rounded-b-xl">
<div class="px-6 pt-8 pb-4 bg-gray-50">
<?= anchor_popup(
route_to('comment-remote-action', $podcast->handle, $comment->id, 'reply'),
lang('comment.reply_to', ['actorUsername' => $comment->actor->username]),
[
'class' =>
'text-center justify-center font-semibold rounded-full shadow relative z-10 px-4 py-2 w-full bg-rose-600 text-white inline-flex items-center hover:bg-rose-700',
'width' => 420,
'height' => 620,
],
) ?>
</div>
<?php foreach ($comment->replies as $reply): ?>
<?= view('podcast/_partials/comment', ['comment' => $reply]) ?>
<?php endforeach; ?>
</div>

View File

@ -0,0 +1,47 @@
<?= $this->include('podcast/_partials/comment_authenticated') ?>
<div class="-mt-2 overflow-hidden border-b border-l border-r post-replies rounded-b-xl">
<?= form_open(
route_to('comment-attempt-action', interact_as_actor()->username, $episode->slug, $comment->id),
[
'class' => 'bg-gray-50 flex px-6 pt-8 pb-4',
],
) ?>
<img src="<?= interact_as_actor()
->avatar_image_url ?>" alt="<?= interact_as_actor()
->display_name ?>" class="w-12 h-12 mr-4 rounded-full ring-gray-50 ring-2" />
<div class="flex flex-col flex-1">
<?= form_textarea(
[
'id' => 'message',
'name' => 'message',
'class' => 'form-textarea mb-4 w-full',
'required' => 'required',
'placeholder' => lang('Comment.form.reply_to_placeholder', [
'actorUsername' => $comment->actor->username,
]),
],
old('message', '', false),
[
'rows' => 1,
],
) ?>
<?= button(
lang('Comment.form.submit_reply'),
'',
['variant' => 'primary', 'size' => 'small'],
[
'type' => 'submit',
'class' => 'self-end',
'name' => 'action',
'value' => 'reply',
],
) ?>
</div>
<?= form_close() ?>
<?php foreach ($comment->replies as $reply): ?>
<?= view('podcast/_partials/comment_authenticated', [
'comment' => $reply,
]) ?>
<?php endforeach; ?>
</div>

View File

@ -1,7 +1,7 @@
<article class="relative z-10 w-full bg-white shadow rounded-2xl">
<header class="flex px-6 py-4">
<img src="<?= $post->actor
->avatar_image_url ?>" alt="<?= $post->display_name ?>" class="w-12 h-12 mr-4 rounded-full" />
->avatar_image_url ?>" alt="<?= $post->actor->display_name ?>" class="w-12 h-12 mr-4 rounded-full" />
<div class="flex flex-col min-w-0">
<a href="<?= $post->actor
->uri ?>" class="flex items-baseline hover:underline" <?= $post

View File

@ -1,7 +1,7 @@
<article class="relative z-10 w-full bg-white shadow-md rounded-2xl">
<header class="flex px-6 py-4">
<img src="<?= $post->actor
->avatar_image_url ?>" alt="<?= $post->display_name ?>" class="w-12 h-12 mr-4 rounded-full" />
->avatar_image_url ?>" alt="<?= $post->actor->display_name ?>" class="w-12 h-12 mr-4 rounded-full" />
<div class="flex flex-col min-w-0">
<a href="<?= $post->actor
->uri ?>" class="flex items-baseline hover:underline" <?= $post

View File

@ -0,0 +1,38 @@
<?= $this->extend('podcast/_layout') ?>
<?= $this->section('meta-tags') ?>
<title><?= lang('Comment.title', [
'actorDisplayName' => $comment->actor->display_name,
]) ?></title>
<meta name="description" content="<?= $comment->message ?>"/>
<meta property="og:title" content="<?= lang('Comment.title', [
'actorDisplayName' => $comment->actor->display_name,
]) ?>"/>
<meta property="og:locale" content="<?= service(
'request',
)->getLocale() ?>" />
<meta property="og:site_name" content="<?= $comment->actor->display_name ?>" />
<meta property="og:url" content="<?= current_url() ?>" />
<meta property="og:image" content="<?= $comment->actor->avatar_image_url ?>" />
<meta property="og:description" content="<?= $comment->message ?>" />
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="max-w-2xl px-6 mx-auto">
<nav class="py-3">
<a href="<?= route_to('episode', $podcast->handle, $episode->slug) ?>"
class="inline-flex items-center px-4 py-2 text-sm"><?= icon(
'arrow-left',
'mr-2 text-lg',
) .
lang('Comment.back_to_episode', [
'actor' => $comment->actor->display_name,
]) ?></a>
</nav>
<div class="pb-12">
<?= $this->include('podcast/_partials/comment_with_replies') ?>
</div>
</div>
<?= $this->endSection()
?>

View File

@ -0,0 +1,40 @@
<?= $this->extend('podcast/_layout_authenticated') ?>
<?= $this->section('meta-tags') ?>
<title><?= lang('Comment.title', [
'actorDisplayName' => $comment->actor->display_name,
]) ?></title>
<meta name="description" content="<?= $comment->message ?>"/>
<meta property="og:title" content="<?= lang('Comment.title', [
'actorDisplayName' => $comment->actor->display_name,
]) ?>"/>
<meta property="og:locale" content="<?= service(
'request',
)->getLocale() ?>" />
<meta property="og:site_name" content="<?= $comment->actor->display_name ?>" />
<meta property="og:url" content="<?= current_url() ?>" />
<meta property="og:image" content="<?= $comment->actor->avatar_image_url ?>" />
<meta property="og:description" content="<?= $comment->message ?>" />
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="max-w-2xl px-6 mx-auto">
<nav class="py-3">
<a href="<?= route_to('episode', $podcast->handle, $episode->slug) ?>"
class="inline-flex items-center px-4 py-2 text-sm"><?= icon(
'arrow-left',
'mr-2 text-lg',
) .
lang('Comment.back_to_episode', [
'actor' => $comment->actor->display_name,
]) ?></a>
</nav>
<div class="pb-12">
<?= $this->include(
'podcast/_partials/comment_with_replies_authenticated',
) ?>
</div>
</div>
<?= $this->endSection()
?>

View File

@ -85,7 +85,7 @@
<div class="tab-panels">
<section id="comments" class="space-y-6 tab-panel">
<?= form_open(route_to('comment-attempt-create', $podcast->id, $episode->id), [
'class' => 'flex p-4 bg-white shadow rounded-xl',
'class' => 'flex p-4',
]) ?>
<?= csrf_field() ?>
@ -118,7 +118,6 @@
) ?>
</div>
<?= form_close() ?>
<hr class="my-4 border border-pine-100">
<?php foreach ($episode->comments as $comment): ?>
<?= view('podcast/_partials/comment', ['comment' => $comment]) ?>
<?php endforeach; ?>