feat(comments): add comments to episodes + update naming of status to post

- remove confusing counts for episode (total favourites, total reblogs)
- add comments section to
episode page to display episode comments + post replies linked to the episode
This commit is contained in:
Yassine Doghri 2021-08-13 11:07:29 +00:00
parent 3ff1364906
commit bb4752c35e
86 changed files with 1668 additions and 1153 deletions

View File

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Config; namespace Config;
use App\Entities\Actor; use App\Entities\Actor;
use App\Entities\Status; use App\Entities\Post;
use App\Entities\User; use App\Entities\User;
use CodeIgniter\Events\Events; use CodeIgniter\Events\Events;
use CodeIgniter\Exceptions\FrameworkException; use CodeIgniter\Exceptions\FrameworkException;
@ -120,82 +120,72 @@ Events::on('on_undo_follow', function ($actor, $targetActor): void {
}); });
/** /**
* @param Status $status * @param Post $post
*/ */
Events::on('on_status_add', function ($status): void { Events::on('on_post_add', function ($post): void {
if ($status->in_reply_to_id !== null) { $isReply = $post->in_reply_to_id !== null;
$status = $status->reply_to_status;
if ($isReply) {
$post = $post->reply_to_post;
} }
if ($status->episode_id) { if ($post->episode_id !== null) {
model('EpisodeModel') if ($isReply) {
->where('id', $status->episode_id) model('EpisodeModel', false)
->increment('statuses_total'); ->where('id', $post->episode_id)
->increment('comments_count');
} else {
model('EpisodeModel', false)
->where('id', $post->episode_id)
->increment('posts_count');
}
} }
if ($status->actor->is_podcast) { if ($post->actor->is_podcast) {
// Removing all of the podcast pages is a bit overkill, but works to avoid caching bugs // Removing all of the podcast pages is a bit overkill, but works to avoid caching bugs
// same for other events below // same for other events below
cache() cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*"); ->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache() cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*"); ->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
} }
}); });
/** /**
* @param Status $status * @param Post $post
*/ */
Events::on('on_status_remove', function ($status): void { Events::on('on_post_remove', function ($post): void {
if ($status->in_reply_to_id !== null) { if ($post->in_reply_to_id !== null) {
Events::trigger('on_status_remove', $status->reply_to_status); Events::trigger('on_post_remove', $post->reply_to_post);
} }
if ($episodeId = $status->episode_id) { if ($episodeId = $post->episode_id) {
model('EpisodeModel') model('EpisodeModel', false)
->where('id', $episodeId) ->where('id', $episodeId)
->decrement('statuses_total', 1 + $status->reblogs_count); ->decrement('posts_count');
model('EpisodeModel')
->where('id', $episodeId)
->decrement('reblogs_total', $status->reblogs_count);
model('EpisodeModel')
->where('id', $episodeId)
->decrement('favourites_total', $status->favourites_count);
} }
if ($status->actor->is_podcast) { if ($post->actor->is_podcast) {
cache() cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*"); ->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache() cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*"); ->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
} }
cache() cache()
->deleteMatching("page_status#{$status->id}*"); ->deleteMatching("page_post#{$post->id}*");
}); });
/** /**
* @param Actor $actor * @param Actor $actor
* @param Status $status * @param Post $post
*/ */
Events::on('on_status_reblog', function ($actor, $status): void { Events::on('on_post_reblog', function ($actor, $post): void {
if ($episodeId = $status->episode_id) { if ($post->actor->is_podcast) {
model('EpisodeModel')
->where('id', $episodeId)
->increment('reblogs_total');
model('EpisodeModel')
->where('id', $episodeId)
->increment('statuses_total');
}
if ($status->actor->is_podcast) {
cache() cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*"); ->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache() cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*"); ->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
} }
if ($actor->is_podcast) { if ($actor->is_podcast) {
@ -205,111 +195,96 @@ Events::on('on_status_reblog', function ($actor, $status): void {
} }
cache() cache()
->deleteMatching("page_status#{$status->id}*"); ->deleteMatching("page_post#{$post->id}*");
if ($status->in_reply_to_id !== null) { if ($post->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}"); cache()->deleteMatching("page_post#{$post->in_reply_to_id}");
} }
}); });
/** /**
* @param Status $reblogStatus * @param Post $reblogPost
*/ */
Events::on('on_status_undo_reblog', function ($reblogStatus): void { Events::on('on_post_undo_reblog', function ($reblogPost): void {
$status = $reblogStatus->reblog_of_status; $post = $reblogPost->reblog_of_post;
if ($episodeId = $status->episode_id) {
model('EpisodeModel')
->where('id', $episodeId)
->decrement('reblogs_total');
model('EpisodeModel') if ($post->actor->is_podcast) {
->where('id', $episodeId)
->decrement('statuses_total');
}
if ($status->actor->is_podcast) {
cache() cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*"); ->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache() cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*"); ->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
} }
cache() cache()
->deleteMatching("page_status#{$status->id}*"); ->deleteMatching("page_post#{$post->id}*");
cache() cache()
->deleteMatching("page_status#{$reblogStatus->id}*"); ->deleteMatching("page_post#{$reblogPost->id}*");
if ($status->in_reply_to_id !== null) { if ($post->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}"); cache()->deleteMatching("page_post#{$post->in_reply_to_id}");
} }
if ($reblogStatus->actor->is_podcast) { if ($reblogPost->actor->is_podcast) {
cache() cache()
->deleteMatching("podcast#{$reblogStatus->actor->podcast->id}*"); ->deleteMatching("podcast#{$reblogPost->actor->podcast->id}*");
cache() cache()
->deleteMatching("page_podcast#{$reblogStatus->actor->podcast->id}*"); ->deleteMatching("page_podcast#{$reblogPost->actor->podcast->id}*");
} }
}); });
/** /**
* @param Status $reply * @param Post $reply
*/ */
Events::on('on_status_reply', function ($reply): void { Events::on('on_post_reply', function ($reply): void {
$status = $reply->reply_to_status; $post = $reply->reply_to_post;
if ($status->actor->is_podcast) { if ($post->actor->is_podcast) {
cache() cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*"); ->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache() cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*"); ->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
} }
cache() cache()
->deleteMatching("page_status#{$status->id}*"); ->deleteMatching("page_post#{$post->id}*");
}); });
/** /**
* @param Status $reply * @param Post $reply
*/ */
Events::on('on_reply_remove', function ($reply): void { Events::on('on_reply_remove', function ($reply): void {
$status = $reply->reply_to_status; $post = $reply->reply_to_post;
if ($status->actor->is_podcast) { if ($post->actor->is_podcast) {
cache() cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*"); ->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
cache() cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*"); ->deleteMatching("podcast#{$post->actor->podcast->id}*");
} }
cache() cache()
->deleteMatching("page_status#{$status->id}*"); ->deleteMatching("page_post#{$post->id}*");
cache() cache()
->deleteMatching("page_status#{$reply->id}*"); ->deleteMatching("page_post#{$reply->id}*");
}); });
/** /**
* @param Actor $actor * @param Actor $actor
* @param Status $status * @param Post $post
*/ */
Events::on('on_status_favourite', function ($actor, $status): void { Events::on('on_post_favourite', function ($actor, $post): void {
if ($status->episode_id) { if ($post->actor->is_podcast) {
model('EpisodeModel')
->where('id', $status->episode_id)
->increment('favourites_total');
}
if ($status->actor->is_podcast) {
cache() cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*"); ->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache() cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*"); ->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
} }
cache() cache()
->deleteMatching("page_status#{$status->id}*"); ->deleteMatching("page_post#{$post->id}*");
if ($status->in_reply_to_id !== null) { if ($post->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}*"); cache()->deleteMatching("page_post#{$post->in_reply_to_id}*");
} }
if ($actor->is_podcast) { if ($actor->is_podcast) {
@ -321,27 +296,21 @@ Events::on('on_status_favourite', function ($actor, $status): void {
/** /**
* @param Actor $actor * @param Actor $actor
* @param Status $status * @param Post $post
*/ */
Events::on('on_status_undo_favourite', function ($actor, $status): void { Events::on('on_post_undo_favourite', function ($actor, $post): void {
if ($status->episode_id) { if ($post->actor->is_podcast) {
model('EpisodeModel')
->where('id', $status->episode_id)
->decrement('favourites_total');
}
if ($status->actor->is_podcast) {
cache() cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*"); ->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache() cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*"); ->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
} }
cache() cache()
->deleteMatching("page_status#{$status->id}*"); ->deleteMatching("page_post#{$post->id}*");
if ($status->in_reply_to_id !== null) { if ($post->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}*"); cache()->deleteMatching("page_post#{$post->in_reply_to_id}*");
} }
if ($actor->is_podcast) { if ($actor->is_podcast) {
@ -356,7 +325,7 @@ Events::on('on_block_actor', function (int $actorId): void {
cache() cache()
->deleteMatching('podcast*'); ->deleteMatching('podcast*');
cache() cache()
->deleteMatching('page_status*'); ->deleteMatching('page_post*');
}); });
Events::on('on_unblock_actor', function (int $actorId): void { Events::on('on_unblock_actor', function (int $actorId): void {
@ -364,7 +333,7 @@ Events::on('on_unblock_actor', function (int $actorId): void {
cache() cache()
->deleteMatching('podcast*'); ->deleteMatching('podcast*');
cache() cache()
->deleteMatching('page_status*'); ->deleteMatching('page_post*');
}); });
Events::on('on_block_domain', function (string $domainName): void { Events::on('on_block_domain', function (string $domainName): void {
@ -372,7 +341,7 @@ Events::on('on_block_domain', function (string $domainName): void {
cache() cache()
->deleteMatching('podcast*'); ->deleteMatching('podcast*');
cache() cache()
->deleteMatching('page_status*'); ->deleteMatching('page_post*');
}); });
Events::on('on_unblock_domain', function (string $domainName): void { Events::on('on_unblock_domain', function (string $domainName): void {
@ -380,5 +349,5 @@ Events::on('on_unblock_domain', function (string $domainName): void {
cache() cache()
->deleteMatching('podcast*'); ->deleteMatching('podcast*');
cache() cache()
->deleteMatching('page_status*'); ->deleteMatching('page_post*');
}); });

View File

@ -32,10 +32,10 @@ $routes->setAutoRoute(false);
*/ */
$routes->addPlaceholder('podcastHandle', '[a-zA-Z0-9\_]{1,32}'); $routes->addPlaceholder('podcastHandle', '[a-zA-Z0-9\_]{1,32}');
$routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,191}'); $routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,128}');
$routes->addPlaceholder('base64', '[A-Za-z0-9\.\_]+\-{0,2}'); $routes->addPlaceholder('base64', '[A-Za-z0-9\.\_]+\-{0,2}');
$routes->addPlaceholder('platformType', '\bpodcasting|\bsocial|\bfunding'); $routes->addPlaceholder('platformType', '\bpodcasting|\bsocial|\bfunding');
$routes->addPlaceholder('statusAction', '\bfavourite|\breblog|\breply'); $routes->addPlaceholder('postAction', '\bfavourite|\breblog|\breply');
$routes->addPlaceholder('embeddablePlayerTheme', '\blight|\bdark|\blight-transparent|\bdark-transparent'); $routes->addPlaceholder('embeddablePlayerTheme', '\blight|\bdark|\blight-transparent|\bdark-transparent');
$routes->addPlaceholder( $routes->addPlaceholder(
'uuid', 'uuid',
@ -416,6 +416,25 @@ $routes->group(
], ],
); );
}); });
$routes->group('comments', function ($routes): void {
$routes->post(
'new',
'EpisodeController::attemptCommentCreate/$1/$2',
[
'as' => 'comment-attempt-create',
'filter' => 'permission:podcast-manage_publications',
]
);
$routes->post(
'delete',
'EpisodeController::attemptCommentDelete/$1/$2',
[
'as' => 'comment-attempt-delete',
'filter' => 'permission:podcast-manage_publications',
]
);
});
}); });
}); });
@ -752,6 +771,12 @@ $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', [
'as' => 'comment',
]);
$routes->get('comments/(:uuid)/replies', 'EpisodeController::commentReplies/$1/$2/$3', [
'as' => 'comment-replies',
]);
$routes->get('oembed.json', 'EpisodeController::oembedJSON/$1/$2', [ $routes->get('oembed.json', 'EpisodeController::oembedJSON/$1/$2', [
'as' => 'episode-oembed-json', 'as' => 'episode-oembed-json',
]); ]);
@ -803,73 +828,74 @@ $routes->post('interact-as-actor', 'AuthController::attemptInteractAsActor', [
* Overwriting ActivityPub routes file * Overwriting ActivityPub routes file
*/ */
$routes->group('@(:podcastHandle)', function ($routes): void { $routes->group('@(:podcastHandle)', function ($routes): void {
$routes->post('statuses/new', 'StatusController::attemptCreate/$1', [ $routes->post('posts/new', 'PostController::attemptCreate/$1', [
'as' => 'status-attempt-create', 'as' => 'post-attempt-create',
'filter' => 'permission:podcast-manage_publications', 'filter' => 'permission:podcast-manage_publications',
]); ]);
// Status
$routes->group('statuses/(:uuid)', function ($routes): void { // Post
$routes->group('posts/(:uuid)', function ($routes): void {
$routes->options('/', 'ActivityPubController::preflight'); $routes->options('/', 'ActivityPubController::preflight');
$routes->get('/', 'StatusController::view/$1/$2', [ $routes->get('/', 'PostController::view/$1/$2', [
'as' => 'status', 'as' => 'post',
'alternate-content' => [ 'alternate-content' => [
'application/activity+json' => [ 'application/activity+json' => [
'namespace' => 'ActivityPub\Controllers', 'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'StatusController/$2', 'controller-method' => 'PostController/$2',
], ],
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [ 'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'namespace' => 'ActivityPub\Controllers', 'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'StatusController/$2', 'controller-method' => 'PostController/$2',
], ],
], ],
]); ]);
$routes->options('replies', 'ActivityPubController::preflight'); $routes->options('replies', 'ActivityPubController::preflight');
$routes->get('replies', 'StatusController/$1/$2', [ $routes->get('replies', 'PostController/$1/$2', [
'as' => 'status-replies', 'as' => 'post-replies',
'alternate-content' => [ 'alternate-content' => [
'application/activity+json' => [ 'application/activity+json' => [
'namespace' => 'ActivityPub\Controllers', 'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'StatusController::replies/$2', 'controller-method' => 'PostController::replies/$2',
], ],
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [ 'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'namespace' => 'ActivityPub\Controllers', 'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'StatusController::replies/$2', 'controller-method' => 'PostController::replies/$2',
], ],
], ],
]); ]);
// Actions // Actions
$routes->post('action', 'StatusController::attemptAction/$1/$2', [ $routes->post('action', 'PostController::attemptAction/$1/$2', [
'as' => 'status-attempt-action', 'as' => 'post-attempt-action',
'filter' => 'permission:podcast-interact_as', 'filter' => 'permission:podcast-interact_as',
]); ]);
$routes->post( $routes->post(
'block-actor', 'block-actor',
'StatusController::attemptBlockActor/$1/$2', 'PostController::attemptBlockActor/$1/$2',
[ [
'as' => 'status-attempt-block-actor', 'as' => 'post-attempt-block-actor',
'filter' => 'permission:fediverse-block_actors', 'filter' => 'permission:fediverse-block_actors',
], ],
); );
$routes->post( $routes->post(
'block-domain', 'block-domain',
'StatusController::attemptBlockDomain/$1/$2', 'PostController::attemptBlockDomain/$1/$2',
[ [
'as' => 'status-attempt-block-domain', 'as' => 'post-attempt-block-domain',
'filter' => 'permission:fediverse-block_domains', 'filter' => 'permission:fediverse-block_domains',
], ],
); );
$routes->post('delete', 'StatusController::attemptDelete/$1/$2', [ $routes->post('delete', 'PostController::attemptDelete/$1/$2', [
'as' => 'status-attempt-delete', 'as' => 'post-attempt-delete',
'filter' => 'permission:podcast-manage_publications', 'filter' => 'permission:podcast-manage_publications',
]); ]);
$routes->get( $routes->get(
'remote/(:statusAction)', 'remote/(:postAction)',
'StatusController::remoteAction/$1/$2/$3', 'PostController::remoteAction/$1/$2/$3',
[ [
'as' => 'status-remote-action', 'as' => 'post-remote-action',
], ],
); );
}); });

View File

@ -10,15 +10,17 @@ 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\Image; use App\Entities\Image;
use App\Entities\Location; use App\Entities\Location;
use App\Entities\Podcast; use App\Entities\Podcast;
use App\Entities\Status; use App\Entities\Post;
use App\Models\CommentModel;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use App\Models\PostModel;
use App\Models\SoundbiteModel; use App\Models\SoundbiteModel;
use App\Models\StatusModel;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
@ -429,7 +431,7 @@ class EpisodeController extends BaseController
$db = db_connect(); $db = db_connect();
$db->transStart(); $db->transStart();
$newStatus = new Status([ $newPost = new Post([
'actor_id' => $this->podcast->actor_id, 'actor_id' => $this->podcast->actor_id,
'episode_id' => $this->episode->id, 'episode_id' => $this->episode->id,
'message' => $this->request->getPost('message'), 'message' => $this->request->getPost('message'),
@ -456,15 +458,15 @@ class EpisodeController extends BaseController
$this->episode->published_at = Time::now(); $this->episode->published_at = Time::now();
} }
$newStatus->published_at = $this->episode->published_at; $newPost->published_at = $this->episode->published_at;
$statusModel = new StatusModel(); $postModel = new PostModel();
if (! $statusModel->addStatus($newStatus)) { if (! $postModel->addPost($newPost)) {
$db->transRollback(); $db->transRollback();
return redirect() return redirect()
->back() ->back()
->withInput() ->withInput()
->with('errors', $statusModel->errors()); ->with('errors', $postModel->errors());
} }
$episodeModel = new EpisodeModel(); $episodeModel = new EpisodeModel();
@ -489,7 +491,7 @@ class EpisodeController extends BaseController
$data = [ $data = [
'podcast' => $this->podcast, 'podcast' => $this->podcast,
'episode' => $this->episode, 'episode' => $this->episode,
'status' => (new StatusModel()) 'post' => (new PostModel())
->where([ ->where([
'actor_id' => $this->podcast->actor_id, 'actor_id' => $this->podcast->actor_id,
'episode_id' => $this->episode->id, 'episode_id' => $this->episode->id,
@ -513,7 +515,7 @@ class EpisodeController extends BaseController
public function attemptPublishEdit(): RedirectResponse public function attemptPublishEdit(): RedirectResponse
{ {
$rules = [ $rules = [
'status_id' => 'required', 'post_id' => 'required',
'publication_method' => 'required', 'publication_method' => 'required',
'scheduled_publication_date' => 'scheduled_publication_date' =>
'valid_date[Y-m-d H:i]|permit_empty', 'valid_date[Y-m-d H:i]|permit_empty',
@ -549,19 +551,19 @@ class EpisodeController extends BaseController
$this->episode->published_at = Time::now(); $this->episode->published_at = Time::now();
} }
$status = (new StatusModel())->getStatusById($this->request->getPost('status_id')); $post = (new PostModel())->getPostById($this->request->getPost('post_id'));
if ($status !== null) { if ($post !== null) {
$status->message = $this->request->getPost('message'); $post->message = $this->request->getPost('message');
$status->published_at = $this->episode->published_at; $post->published_at = $this->episode->published_at;
$statusModel = new StatusModel(); $postModel = new PostModel();
if (! $statusModel->editStatus($status)) { if (! $postModel->editPost($post)) {
$db->transRollback(); $db->transRollback();
return redirect() return redirect()
->back() ->back()
->withInput() ->withInput()
->with('errors', $statusModel->errors()); ->with('errors', $postModel->errors());
} }
} }
@ -585,14 +587,14 @@ class EpisodeController extends BaseController
$db = db_connect(); $db = db_connect();
$db->transStart(); $db->transStart();
$statusModel = new StatusModel(); $postModel = new PostModel();
$status = $statusModel $post = $postModel
->where([ ->where([
'actor_id' => $this->podcast->actor_id, 'actor_id' => $this->podcast->actor_id,
'episode_id' => $this->episode->id, 'episode_id' => $this->episode->id,
]) ])
->first(); ->first();
$statusModel->removeStatus($status); $postModel->removePost($post);
$this->episode->published_at = null; $this->episode->published_at = null;
@ -656,13 +658,13 @@ class EpisodeController extends BaseController
$db->transStart(); $db->transStart();
$allStatusesLinkedToEpisode = (new StatusModel()) $allPostsLinkedToEpisode = (new PostModel())
->where([ ->where([
'episode_id' => $this->episode->id, 'episode_id' => $this->episode->id,
]) ])
->findAll(); ->findAll();
foreach ($allStatusesLinkedToEpisode as $status) { foreach ($allPostsLinkedToEpisode as $post) {
(new StatusModel())->removeStatus($status); (new PostModel())->removePost($post);
} }
// set episode published_at to null to unpublish // set episode published_at to null to unpublish
@ -782,4 +784,41 @@ class EpisodeController extends BaseController
]); ]);
return view('admin/episode/embeddable_player', $data); return view('admin/episode/embeddable_player', $data);
} }
public function attemptCommentCreate(): RedirectResponse
{
$rules = [
'message' => 'required|max_length[500]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$message = $this->request->getPost('message');
$newComment = new Comment([
'actor_id' => interact_as_actor_id(),
'episode_id' => $this->episode->id,
'message' => $message,
'created_at' => new Time('now'),
'created_by' => user_id(),
]);
$commentModel = new CommentModel();
if (
! $commentModel->addComment($newComment, true)
) {
return redirect()
->back()
->withInput()
->with('errors', $commentModel->errors());
}
// Comment has been successfully created
return redirect()->back();
}
} }

View File

@ -15,8 +15,10 @@ 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;
@ -219,10 +221,10 @@ class EpisodeController extends BaseController
/** /**
* get comments: aggregated replies from posts referring to the episode * get comments: aggregated replies from posts referring to the episode
*/ */
$episodeComments = model('StatusModel') $episodeComments = model('PostModel')
->whereIn('in_reply_to_id', function (BaseBuilder $builder): BaseBuilder { ->whereIn('in_reply_to_id', function (BaseBuilder $builder): BaseBuilder {
return $builder->select('id') return $builder->select('id')
->from('activitypub_statuses') ->from('activitypub_posts')
->where('episode_id', $this->episode->id); ->where('episode_id', $this->episode->id);
}) })
->where('`published_at` <= NOW()', null, false) ->where('`published_at` <= NOW()', null, false)
@ -254,4 +256,57 @@ 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());
}
} }

View File

@ -18,7 +18,7 @@ use App\Libraries\PodcastActor;
use App\Libraries\PodcastEpisode; use App\Libraries\PodcastEpisode;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use App\Models\StatusModel; use App\Models\PostModel;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\Response; use CodeIgniter\HTTP\Response;
@ -81,7 +81,7 @@ class PodcastController extends BaseController
if (! ($cachedView = cache($cacheName))) { if (! ($cachedView = cache($cacheName))) {
$data = [ $data = [
'podcast' => $this->podcast, 'podcast' => $this->podcast,
'statuses' => (new StatusModel())->getActorPublishedStatuses($this->podcast->actor_id), 'posts' => (new PostModel())->getActorPublishedPosts($this->podcast->actor_id),
]; ];
// if user is logged in then send to the authenticated activity view // if user is logged in then send to the authenticated activity view

View File

@ -10,21 +10,21 @@ declare(strict_types=1);
namespace App\Controllers; namespace App\Controllers;
use ActivityPub\Controllers\StatusController as ActivityPubStatusController; use ActivityPub\Controllers\PostController as ActivityPubPostController;
use ActivityPub\Entities\Status as ActivityPubStatus; use ActivityPub\Entities\Post as ActivityPubPost;
use Analytics\AnalyticsTrait; use Analytics\AnalyticsTrait;
use App\Entities\Actor; use App\Entities\Actor;
use App\Entities\Podcast; use App\Entities\Podcast;
use App\Entities\Status as CastopodStatus; use App\Entities\Post as CastopodPost;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use App\Models\StatusModel; use App\Models\PostModel;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\URI; use CodeIgniter\HTTP\URI;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
class StatusController extends ActivityPubStatusController class PostController extends ActivityPubPostController
{ {
use AnalyticsTrait; use AnalyticsTrait;
@ -50,9 +50,9 @@ class StatusController extends ActivityPubStatusController
if ( if (
count($params) > 1 && count($params) > 1 &&
($status = (new StatusModel())->getStatusById($params[1])) !== null ($post = (new PostModel())->getPostById($params[1])) !== null
) { ) {
$this->status = $status; $this->post = $post;
unset($params[0]); unset($params[0]);
unset($params[1]); unset($params[1]);
@ -72,7 +72,7 @@ class StatusController extends ActivityPubStatusController
'_', '_',
array_filter([ array_filter([
'page', 'page',
"status#{$this->status->id}", "post#{$this->post->id}",
service('request') service('request')
->getLocale(), ->getLocale(),
can_user_interact() ? '_authenticated' : null, can_user_interact() ? '_authenticated' : null,
@ -83,15 +83,15 @@ class StatusController extends ActivityPubStatusController
$data = [ $data = [
'podcast' => $this->podcast, 'podcast' => $this->podcast,
'actor' => $this->actor, 'actor' => $this->actor,
'status' => $this->status, 'post' => $this->post,
]; ];
// if user is logged in then send to the authenticated activity view // if user is logged in then send to the authenticated activity view
if (can_user_interact()) { if (can_user_interact()) {
helper('form'); helper('form');
return view('podcast/status_authenticated', $data); return view('podcast/post_authenticated', $data);
} }
return view('podcast/status', $data, [ return view('podcast/post', $data, [
'cache' => DECADE, 'cache' => DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
@ -116,7 +116,7 @@ class StatusController extends ActivityPubStatusController
$message = $this->request->getPost('message'); $message = $this->request->getPost('message');
$newStatus = new CastopodStatus([ $newPost = new CastopodPost([
'actor_id' => interact_as_actor_id(), 'actor_id' => interact_as_actor_id(),
'published_at' => Time::now(), 'published_at' => Time::now(),
'created_by' => user_id(), 'created_by' => user_id(),
@ -129,23 +129,23 @@ class StatusController extends ActivityPubStatusController
($params = extract_params_from_episode_uri(new URI($episodeUri))) && ($params = extract_params_from_episode_uri(new URI($episodeUri))) &&
($episode = (new EpisodeModel())->getEpisodeBySlug($params['podcastHandle'], $params['episodeSlug'])) ($episode = (new EpisodeModel())->getEpisodeBySlug($params['podcastHandle'], $params['episodeSlug']))
) { ) {
$newStatus->episode_id = $episode->id; $newPost->episode_id = $episode->id;
} }
$newStatus->message = $message; $newPost->message = $message;
$statusModel = new StatusModel(); $postModel = new PostModel();
if ( if (
! $statusModel ! $postModel
->addStatus($newStatus, ! (bool) $newStatus->episode_id, true) ->addPost($newPost, ! (bool) $newPost->episode_id, true)
) { ) {
return redirect() return redirect()
->back() ->back()
->withInput() ->withInput()
->with('errors', $statusModel->errors()); ->with('errors', $postModel->errors());
} }
// Status has been successfully created // Post has been successfully created
return redirect()->back(); return redirect()->back();
} }
@ -162,36 +162,36 @@ class StatusController extends ActivityPubStatusController
->with('errors', $this->validator->getErrors()); ->with('errors', $this->validator->getErrors());
} }
$newStatus = new ActivityPubStatus([ $newPost = new ActivityPubPost([
'actor_id' => interact_as_actor_id(), 'actor_id' => interact_as_actor_id(),
'in_reply_to_id' => $this->status->id, 'in_reply_to_id' => $this->post->id,
'message' => $this->request->getPost('message'), 'message' => $this->request->getPost('message'),
'published_at' => Time::now(), 'published_at' => Time::now(),
'created_by' => user_id(), 'created_by' => user_id(),
]); ]);
$statusModel = new StatusModel(); $postModel = new PostModel();
if (! $statusModel->addReply($newStatus)) { if (! $postModel->addReply($newPost)) {
return redirect() return redirect()
->back() ->back()
->withInput() ->withInput()
->with('errors', $statusModel->errors()); ->with('errors', $postModel->errors());
} }
// Reply status without preview card has been successfully created // Reply post without preview card has been successfully created
return redirect()->back(); return redirect()->back();
} }
public function attemptFavourite(): RedirectResponse public function attemptFavourite(): RedirectResponse
{ {
model('FavouriteModel')->toggleFavourite(interact_as_actor(), $this->status); model('FavouriteModel')->toggleFavourite(interact_as_actor(), $this->post);
return redirect()->back(); return redirect()->back();
} }
public function attemptReblog(): RedirectResponse public function attemptReblog(): RedirectResponse
{ {
(new StatusModel())->toggleReblog(interact_as_actor(), $this->status); (new PostModel())->toggleReblog(interact_as_actor(), $this->post);
return redirect()->back(); return redirect()->back();
} }
@ -230,20 +230,20 @@ class StatusController extends ActivityPubStatusController
$cacheName = implode( $cacheName = implode(
'_', '_',
array_filter(['page', "status#{$this->status->id}", "remote_{$action}", service('request') ->getLocale()]), array_filter(['page', "post#{$this->post->id}", "remote_{$action}", service('request') ->getLocale()]),
); );
if (! ($cachedView = cache($cacheName))) { if (! ($cachedView = cache($cacheName))) {
$data = [ $data = [
'podcast' => $this->podcast, 'podcast' => $this->podcast,
'actor' => $this->actor, 'actor' => $this->actor,
'status' => $this->status, 'post' => $this->post,
'action' => $action, 'action' => $action,
]; ];
helper('form'); helper('form');
return view('podcast/status_remote_action', $data, [ return view('podcast/post_remote_action', $data, [
'cache' => DECADE, 'cache' => DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);

View File

@ -38,7 +38,7 @@ class AddEpisodes extends Migration
], ],
'slug' => [ 'slug' => [
'type' => 'VARCHAR', 'type' => 'VARCHAR',
'constraint' => 191, 'constraint' => 128,
], ],
'audio_file_path' => [ 'audio_file_path' => [
'type' => 'VARCHAR', 'type' => 'VARCHAR',
@ -147,17 +147,12 @@ class AddEpisodes extends Migration
'type' => 'JSON', 'type' => 'JSON',
'null' => true, 'null' => true,
], ],
'favourites_total' => [ 'posts_count' => [
'type' => 'INT', 'type' => 'INT',
'unsigned' => true, 'unsigned' => true,
'default' => 0, 'default' => 0,
], ],
'reblogs_total' => [ 'comments_count' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
],
'statuses_total' => [
'type' => 'INT', 'type' => 'INT',
'unsigned' => true, 'unsigned' => true,
'default' => 0, 'default' => 0,

View File

@ -30,7 +30,7 @@ class AddPages extends Migration
], ],
'slug' => [ 'slug' => [
'type' => 'VARCHAR', 'type' => 'VARCHAR',
'constraint' => 191, 'constraint' => 128,
'unique' => true, 'unique' => true,
], ],
'content_markdown' => [ 'content_markdown' => [

View File

@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* Class AddEpisodeIdToStatuses Adds episode_id field to activitypub_statuses table in database * Class AddEpisodeIdToPosts Adds episode_id field to activitypub_posts table in database
* *
* @copyright 2020 Podlibre * @copyright 2020 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
@ -14,23 +14,23 @@ namespace App\Database\Migrations;
use CodeIgniter\Database\Migration; use CodeIgniter\Database\Migration;
class AddEpisodeIdToStatuses extends Migration class AddEpisodeIdToPosts extends Migration
{ {
public function up(): void public function up(): void
{ {
$prefix = $this->db->getPrefix(); $prefix = $this->db->getPrefix();
$createQuery = <<<CODE_SAMPLE $createQuery = <<<CODE_SAMPLE
ALTER TABLE {$prefix}activitypub_statuses ALTER TABLE {$prefix}activitypub_posts
ADD COLUMN `episode_id` INT UNSIGNED NULL AFTER `replies_count`, ADD COLUMN `episode_id` INT UNSIGNED NULL AFTER `replies_count`,
ADD FOREIGN KEY {$prefix}activitypub_statuses_episode_id_foreign(episode_id) REFERENCES {$prefix}episodes(id) ON DELETE CASCADE; ADD FOREIGN KEY {$prefix}activitypub_posts_episode_id_foreign(episode_id) REFERENCES {$prefix}episodes(id) ON DELETE CASCADE;
CODE_SAMPLE; CODE_SAMPLE;
$this->db->query($createQuery); $this->db->query($createQuery);
} }
public function down(): void public function down(): void
{ {
$this->forge->dropForeignKey('activitypub_statuses', 'activitypub_statuses_episode_id_foreign'); $this->forge->dropForeignKey('activitypub_posts', 'activitypub_posts_episode_id_foreign');
$this->forge->dropColumn('activitypub_statuses', 'episode_id'); $this->forge->dropColumn('activitypub_posts', 'episode_id');
} }
} }

View File

@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* Class AddCreatedByToStatuses Adds created_by field to activitypub_statuses table in database * Class AddCreatedByToPosts Adds created_by field to activitypub_posts table in database
* *
* @copyright 2020 Podlibre * @copyright 2020 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
@ -14,23 +14,23 @@ namespace App\Database\Migrations;
use CodeIgniter\Database\Migration; use CodeIgniter\Database\Migration;
class AddCreatedByToStatuses extends Migration class AddCreatedByToPosts extends Migration
{ {
public function up(): void public function up(): void
{ {
$prefix = $this->db->getPrefix(); $prefix = $this->db->getPrefix();
$createQuery = <<<CODE_SAMPLE $createQuery = <<<CODE_SAMPLE
ALTER TABLE {$prefix}activitypub_statuses ALTER TABLE {$prefix}activitypub_posts
ADD COLUMN `created_by` INT UNSIGNED AFTER `episode_id`, ADD COLUMN `created_by` INT UNSIGNED AFTER `episode_id`,
ADD FOREIGN KEY {$prefix}activitypub_statuses_created_by_foreign(created_by) REFERENCES {$prefix}users(id) ON DELETE CASCADE; ADD FOREIGN KEY {$prefix}activitypub_posts_created_by_foreign(created_by) REFERENCES {$prefix}users(id) ON DELETE CASCADE;
CODE_SAMPLE; CODE_SAMPLE;
$this->db->query($createQuery); $this->db->query($createQuery);
} }
public function down(): void public function down(): void
{ {
$this->forge->dropForeignKey('activitypub_statuses', 'activitypub_statuses_created_by_foreign'); $this->forge->dropForeignKey('activitypub_posts', 'activitypub_posts_created_by_foreign');
$this->forge->dropColumn('activitypub_statuses', 'created_by'); $this->forge->dropColumn('activitypub_posts', 'created_by');
} }
} }

View File

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
/**
* Class AddComments creates comments table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddComments extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'BINARY',
'constraint' => 16,
],
'uri' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'episode_id' => [
'type' => 'INT',
'unsigned' => true,
],
'actor_id' => [
'type' => 'INT',
'unsigned' => true,
],
'in_reply_to_id' => [
'type' => 'BINARY',
'constraint' => 16,
'null' => true,
],
'message' => [
'type' => 'VARCHAR',
'constraint' => 500,
'null' => true,
],
'message_html' => [
'type' => 'VARCHAR',
'constraint' => 600,
'null' => true,
],
'likes_count' => [
'type' => 'INT',
'unsigned' => true,
],
'dislikes_count' => [
'type' => 'INT',
'unsigned' => true,
],
'replies_count' => [
'type' => 'INT',
'unsigned' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
$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');
}
public function down(): void
{
$this->forge->dropTable('comments');
}
}

View File

@ -162,13 +162,13 @@ class AuthSeeder extends Seeder
[ [
'name' => 'manage_publications', 'name' => 'manage_publications',
'description' => 'description' =>
'Publish / unpublish episodes & statuses of a podcast', 'Publish / unpublish episodes & posts of a podcast',
'has_permission' => ['podcast_admin'], 'has_permission' => ['podcast_admin'],
], ],
[ [
'name' => 'interact_as', 'name' => 'interact_as',
'description' => 'description' =>
'Interact as the podcast to favourite / share or reply to statuses.', 'Interact as the podcast to favourite / share or reply to posts.',
'has_permission' => ['podcast_admin'], 'has_permission' => ['podcast_admin'],
], ],
], ],

110
app/Entities/Comment.php Normal file
View File

@ -0,0 +1,110 @@
<?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\Entities;
use App\Models\EpisodeModel;
use CodeIgniter\I18n\Time;
use Michalsn\Uuid\UuidEntity;
use RuntimeException;
/**
* @property string $id
* @property string $uri
* @property int $episode_id
* @property Episode|null $episode
* @property int $actor_id
* @property Actor|null $actor
* @property string $in_reply_to_id
* @property Comment|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
*/
class Comment extends UuidEntity
{
protected ?Episode $episode = null;
protected ?Actor $actor = null;
protected ?Comment $reply_to_comment = null;
/**
* @var string[]
*/
protected $dates = ['created_at'];
/**
* @var array<string, string>
*/
protected $casts = [
'id' => 'string',
'uri' => 'string',
'episode_id' => 'integer',
'actor_id' => 'integer',
'in_reply_to_id' => '?string',
'message' => 'string',
'message_html' => 'string',
'likes_count' => 'integer',
'dislikes_count' => 'integer',
'replies_count' => 'integer',
'created_by' => 'integer',
'is_from_post' => 'boolean',
];
/**
* Returns the comment's attached episode
*/
public function getEpisode(): ?Episode
{
if ($this->episode_id === null) {
throw new RuntimeException('Comment must have an episode_id before getting episode.');
}
if (! $this->episode instanceof Episode) {
$this->episode = (new EpisodeModel())->getEpisodeById($this->episode_id);
}
return $this->episode;
}
/**
* Returns the comment's actor
*/
public function getActor(): Actor
{
if ($this->actor_id === null) {
throw new RuntimeException('Comment must have an actor_id before getting actor.');
}
if ($this->actor === null) {
$this->actor = model('ActorModel', false)
->getActorById($this->actor_id);
}
return $this->actor;
}
public function setMessage(string $message): static
{
helper('activitypub');
$messageWithoutTags = strip_tags($message);
$this->attributes['message'] = $messageWithoutTags;
$this->attributes['message_html'] = str_replace("\n", '<br />', linkify($messageWithoutTags));
return $this;
}
}

View File

@ -11,10 +11,11 @@ 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\PersonModel; use App\Models\PersonModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use App\Models\PostModel;
use App\Models\SoundbiteModel; use App\Models\SoundbiteModel;
use App\Models\StatusModel;
use CodeIgniter\Entity\Entity; use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File; use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile; use CodeIgniter\HTTP\Files\UploadedFile;
@ -65,9 +66,8 @@ use RuntimeException;
* @property string|null $location_osm * @property string|null $location_osm
* @property array|null $custom_rss * @property array|null $custom_rss
* @property string $custom_rss_string * @property string $custom_rss_string
* @property int $favourites_total * @property int $posts_count
* @property int $reblogs_total * @property int $comments_count
* @property int $statuses_total
* @property int $created_by * @property int $created_by
* @property int $updated_by * @property int $updated_by
* @property string $publication_status; * @property string $publication_status;
@ -117,12 +117,12 @@ class Episode extends Entity
protected ?array $soundbites = null; protected ?array $soundbites = null;
/** /**
* @var Status[]|null * @var Post[]|null
*/ */
protected ?array $statuses = null; protected ?array $posts = null;
/** /**
* @var Status[]|null * @var Comment[]|null
*/ */
protected ?array $comments = null; protected ?array $comments = null;
@ -168,9 +168,8 @@ class Episode extends Entity
'location_geo' => '?string', 'location_geo' => '?string',
'location_osm' => '?string', 'location_osm' => '?string',
'custom_rss' => '?json-array', 'custom_rss' => '?json-array',
'favourites_total' => 'integer', 'posts_count' => 'integer',
'reblogs_total' => 'integer', 'comments_count' => 'integer',
'statuses_total' => 'integer',
'created_by' => 'integer', 'created_by' => 'integer',
'updated_by' => 'integer', 'updated_by' => 'integer',
]; ];
@ -387,23 +386,23 @@ class Episode extends Entity
} }
/** /**
* @return Status[] * @return Post[]
*/ */
public function getStatuses(): array public function getPosts(): array
{ {
if ($this->id === null) { if ($this->id === null) {
throw new RuntimeException('Episode must be created before getting statuses.'); throw new RuntimeException('Episode must be created before getting posts.');
} }
if ($this->statuses === null) { if ($this->posts === null) {
$this->statuses = (new StatusModel())->getEpisodeStatuses($this->id); $this->posts = (new PostModel())->getEpisodePosts($this->id);
} }
return $this->statuses; return $this->posts;
} }
/** /**
* @return Status[] * @return Comment[]
*/ */
public function getComments(): array public function getComments(): array
{ {
@ -412,7 +411,7 @@ class Episode extends Entity
} }
if ($this->comments === null) { if ($this->comments === null) {
$this->comments = (new StatusModel())->getEpisodeComments($this->id); $this->comments = (new CommentModel())->getEpisodeComments($this->id);
} }
return $this->comments; return $this->comments;
@ -420,7 +419,7 @@ class Episode extends Entity
public function getLink(): string public function getLink(): string
{ {
return url_to('episode', $this->getPodcast()->name, $this->attributes['slug']); return url_to('episode', $this->getPodcast()->handle, $this->attributes['slug']);
} }
public function getEmbeddablePlayerUrl(string $theme = null): string public function getEmbeddablePlayerUrl(string $theme = null): string

View File

@ -10,7 +10,7 @@ declare(strict_types=1);
namespace App\Entities; namespace App\Entities;
use ActivityPub\Entities\Status as ActivityPubStatus; use ActivityPub\Entities\Post as ActivityPubPost;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use RuntimeException; use RuntimeException;
@ -18,7 +18,7 @@ use RuntimeException;
* @property int|null $episode_id * @property int|null $episode_id
* @property Episode|null $episode * @property Episode|null $episode
*/ */
class Status extends ActivityPubStatus class Post extends ActivityPubPost
{ {
protected ?Episode $episode = null; protected ?Episode $episode = null;
@ -41,12 +41,12 @@ class Status extends ActivityPubStatus
]; ];
/** /**
* Returns the status' attached episode * Returns the post's attached episode
*/ */
public function getEpisode(): ?Episode public function getEpisode(): ?Episode
{ {
if ($this->episode_id === null) { if ($this->episode_id === null) {
throw new RuntimeException('Status must have an episode_id before getting episode.'); throw new RuntimeException('Post must have an episode_id before getting episode.');
} }
if (! $this->episode instanceof Episode) { if (! $this->episode instanceof Episode) {

View File

@ -255,7 +255,7 @@ if (! function_exists('publication_button')) {
/** /**
* Publication button component * Publication button component
* *
* Displays the appropriate publication button depending on the publication status. * Displays the appropriate publication button depending on the publication post.
*/ */
function publication_button(int $podcastId, int $episodeId, string $publicationStatus): string function publication_button(int $podcastId, int $episodeId, string $publicationStatus): string
{ {

View File

@ -40,7 +40,7 @@ if (! function_exists('extract_params_from_episode_uri')) {
function extract_params_from_episode_uri(URI $episodeUri): ?array function extract_params_from_episode_uri(URI $episodeUri): ?array
{ {
preg_match( preg_match(
'~@(?P<podcastHandle>[a-zA-Z0-9\_]{1,32})\/episodes\/(?P<episodeSlug>[a-zA-Z0-9\-]{1,191})~', '~@(?P<podcastHandle>[a-zA-Z0-9\_]{1,32})\/episodes\/(?P<episodeSlug>[a-zA-Z0-9\-]{1,128})~',
$episodeUri->getPath(), $episodeUri->getPath(),
$matches, $matches,
); );

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' => 'Write a comment...',
'reply_to_placeholder' => 'Reply to @{actorUsername}',
'submit' => 'Send!',
'submit_reply' => 'Reply',
],
'like' => 'Like',
'dislike' => 'Dislike',
'replies' => '{numberOfReplies, plural,
one {# reply}
other {# replies}
}',
'block_actor' => 'Block user @{actorUsername}',
'block_domain' => 'Block domain @{actorDomain}',
'delete' => 'Delete comment',
];

View File

@ -16,19 +16,12 @@ return [
'season_episode' => 'Season {seasonNumber} episode {episodeNumber}', 'season_episode' => 'Season {seasonNumber} episode {episodeNumber}',
'season_episode_abbr' => 'S{seasonNumber}E{episodeNumber}', 'season_episode_abbr' => 'S{seasonNumber}E{episodeNumber}',
'back_to_episodes' => 'Back to episodes of {podcast}', 'back_to_episodes' => 'Back to episodes of {podcast}',
'comments' => 'Comments',
'activity' => 'Activity', 'activity' => 'Activity',
'description' => 'Description', 'description' => 'Description',
'total_favourites' => '{numberOfTotalFavourites, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# total favourite} one {# comment}
other {# total favourites} other {# comments}
}',
'total_reblogs' => '{numberOfTotalReblogs, plural,
one {# total share}
other {# total shares}
}',
'total_statuses' => '{numberOfTotalStatuses, plural,
one {# total post}
other {# total posts}
}', }',
'all_podcast_episodes' => 'All podcast episodes', 'all_podcast_episodes' => 'All podcast episodes',
'back_to_podcast' => 'Go back to podcast', 'back_to_podcast' => 'Go back to podcast',
@ -116,14 +109,14 @@ return [
'custom_rss_hint' => 'This will be injected within the ❬item❭ tag.', 'custom_rss_hint' => 'This will be injected within the ❬item❭ tag.',
'block' => 'Episode should be hidden from all platforms', 'block' => 'Episode should be hidden from all platforms',
'block_hint' => 'block_hint' =>
'The episode show or hide status. If you want this episode removed from the Apple directory, toggle this on.', 'The episode show or hide post. If you want this episode removed from the Apple directory, toggle this on.',
'submit_create' => 'Create episode', 'submit_create' => 'Create episode',
'submit_edit' => 'Save episode', 'submit_edit' => 'Save episode',
], ],
'publish_form' => [ 'publish_form' => [
'back_to_episode_dashboard' => 'Back to episode dashboard', 'back_to_episode_dashboard' => 'Back to episode dashboard',
'status' => 'Your announcement post', 'post' => 'Your announcement post',
'status_hint' => 'post_hint' =>
"Write a message to announce the publication of your episode. The message will be broadcasted to all your followers in the fediverse and be featured in your podcast's homepage.", "Write a message to announce the publication of your episode. The message will be broadcasted to all your followers in the fediverse and be featured in your podcast's homepage.",
'publication_date' => 'Publication date', 'publication_date' => 'Publication date',
'publication_method' => [ 'publication_method' => [

View File

@ -223,7 +223,7 @@ return [
one {<span class="font-semibold">#</span> follower} one {<span class="font-semibold">#</span> follower}
other {<span class="font-semibold">#</span> followers} other {<span class="font-semibold">#</span> followers}
}', }',
'statuses' => '{numberOfStatuses, plural, 'posts' => '{numberOfPosts, plural,
one {<span class="font-semibold">#</span> post} one {<span class="font-semibold">#</span> post}
other {<span class="font-semibold">#</span> posts} other {<span class="font-semibold">#</span> posts}
}', }',

View File

@ -10,7 +10,7 @@ declare(strict_types=1);
return [ return [
'title' => "{actorDisplayName}'s post", 'title' => "{actorDisplayName}'s post",
'back_to_actor_statuses' => 'Back to {actor} posts', 'back_to_actor_posts' => 'Back to {actor} posts',
'actor_shared' => '{actor} shared', 'actor_shared' => '{actor} shared',
'reply_to' => 'Reply to @{actorUsername}', 'reply_to' => 'Reply to @{actorUsername}',
'form' => [ 'form' => [

View File

@ -16,19 +16,12 @@ return [
'season_episode' => 'Saison {seasonNumber} épisode {episodeNumber}', 'season_episode' => 'Saison {seasonNumber} épisode {episodeNumber}',
'season_episode_abbr' => 'S{seasonNumber}E{episodeNumber}', 'season_episode_abbr' => 'S{seasonNumber}E{episodeNumber}',
'back_to_episodes' => 'Retour aux épisodes de {podcast}', 'back_to_episodes' => 'Retour aux épisodes de {podcast}',
'comments' => 'Commentaires',
'activity' => 'Activité', 'activity' => 'Activité',
'description' => 'Description', 'description' => 'Description',
'total_favourites' => '{numberOfTotalFavourites, plural, 'number_of_comments' => '{numberOfComments, plural,
one {# favori en tout} one {# commentaire}
other {# favoris en tout} other {# commentaires}
}',
'total_reblogs' => '{numberOfTotalReblogs, plural,
one {# partage en tout}
other {# partages en tout}
}',
'total_statuses' => '{numberOfTotalStatuses, plural,
one {# message}
other {# messages}
}', }',
'all_podcast_episodes' => 'Tous les épisodes du podcast', 'all_podcast_episodes' => 'Tous les épisodes du podcast',
'back_to_podcast' => 'Revenir au podcast', 'back_to_podcast' => 'Revenir au podcast',
@ -125,8 +118,8 @@ return [
], ],
'publish_form' => [ 'publish_form' => [
'back_to_episode_dashboard' => 'Retour au tableau de bord de lépisode', 'back_to_episode_dashboard' => 'Retour au tableau de bord de lépisode',
'status' => 'Votre message de publication', 'post' => 'Votre message de publication',
'status_hint' => 'post_hint' =>
'Écrivez un message pour annoncer la publication de votre épisode. Le message sera diffusé à toutes les personnes qui vous suivent dans le fédiverse et mis en évidence sur la page daccueil de votre podcast.', 'Écrivez un message pour annoncer la publication de votre épisode. Le message sera diffusé à toutes les personnes qui vous suivent dans le fédiverse et mis en évidence sur la page daccueil de votre podcast.',
'publication_date' => 'Date de publication', 'publication_date' => 'Date de publication',
'publication_date_clear' => 'Effacer la date de publication', 'publication_date_clear' => 'Effacer la date de publication',

View File

@ -225,7 +225,7 @@ return [
one {<span class="font-semibold">#</span> abonné·e} one {<span class="font-semibold">#</span> abonné·e}
other {<span class="font-semibold">#</span> abonné·e·s} other {<span class="font-semibold">#</span> abonné·e·s}
}', }',
'statuses' => '{numberOfStatuses, plural, 'posts' => '{numberOfPosts, plural,
one {<span class="font-semibold">#</span> publication} one {<span class="font-semibold">#</span> publication}
other {<span class="font-semibold">#</span> publications} other {<span class="font-semibold">#</span> publications}
}', }',

View File

@ -10,7 +10,7 @@ declare(strict_types=1);
return [ return [
'title' => 'Publication de {actorDisplayName}', 'title' => 'Publication de {actorDisplayName}',
'back_to_actor_statuses' => 'Retour aux publications de {actor}', 'back_to_actor_posts' => 'Retour aux publications de {actor}',
'actor_shared' => '{actor} a partagé', 'actor_shared' => '{actor} a partagé',
'reply_to' => 'Répondre à @{actorUsername}', 'reply_to' => 'Répondre à @{actorUsername}',
'form' => [ 'form' => [

View File

@ -14,19 +14,19 @@ declare(strict_types=1);
namespace ActivityPub\Activities; namespace ActivityPub\Activities;
use ActivityPub\Core\Activity; use ActivityPub\Core\Activity;
use ActivityPub\Entities\Status; use ActivityPub\Entities\Post;
class AnnounceActivity extends Activity class AnnounceActivity extends Activity
{ {
protected string $type = 'Announce'; protected string $type = 'Announce';
public function __construct(Status $reblogStatus) public function __construct(Post $reblogPost)
{ {
$this->actor = $reblogStatus->actor->uri; $this->actor = $reblogPost->actor->uri;
$this->object = $reblogStatus->reblog_of_status->uri; $this->object = $reblogPost->reblog_of_post->uri;
$this->published = $reblogStatus->published_at->format(DATE_W3C); $this->published = $reblogPost->published_at->format(DATE_W3C);
$this->cc = [$reblogStatus->actor->uri, $reblogStatus->actor->followers_url]; $this->cc = [$reblogPost->actor->uri, $reblogPost->actor->followers_url];
} }
} }

View File

@ -13,7 +13,7 @@ $routes->addPlaceholder(
'uuid', 'uuid',
'[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}', '[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}',
); );
$routes->addPlaceholder('statusAction', '\bfavourite|\breblog|\breply'); $routes->addPlaceholder('postAction', '\bfavourite|\breblog|\breply');
/** /**
* ActivityPub routes file * ActivityPub routes file
@ -54,24 +54,24 @@ $routes->group('', [
]); ]);
}); });
// Status // Post
$routes->post('statuses/new', 'StatusController::attemptCreate/$1', [ $routes->post('posts/new', 'PostController::attemptCreate/$1', [
'as' => 'status-attempt-create', 'as' => 'post-attempt-create',
]); ]);
$routes->get('statuses/(:uuid)', 'StatusController/$1', [ $routes->get('posts/(:uuid)', 'PostController/$1', [
'as' => 'status', 'as' => 'post',
]); ]);
$routes->get('statuses/(:uuid)/replies', 'StatusController/$1', [ $routes->get('posts/(:uuid)/replies', 'PostController/$1', [
'as' => 'status-replies', 'as' => 'post-replies',
]); ]);
$routes->post( $routes->post(
'statuses/(:uuid)/remote/(:statusAction)', 'posts/(:uuid)/remote/(:postAction)',
'StatusController::attemptRemoteAction/$1/$2/$3', 'PostController::attemptRemoteAction/$1/$2/$3',
[ [
'as' => 'status-attempt-remote-action', 'as' => 'post-attempt-remote-action',
], ],
); );

View File

@ -12,7 +12,7 @@ namespace ActivityPub\Controllers;
use ActivityPub\Config\ActivityPub; use ActivityPub\Config\ActivityPub;
use ActivityPub\Entities\Actor; use ActivityPub\Entities\Actor;
use ActivityPub\Entities\Status; use ActivityPub\Entities\Post;
use ActivityPub\Objects\OrderedCollectionObject; use ActivityPub\Objects\OrderedCollectionObject;
use ActivityPub\Objects\OrderedCollectionPage; use ActivityPub\Objects\OrderedCollectionPage;
use CodeIgniter\Controller; use CodeIgniter\Controller;
@ -101,30 +101,30 @@ class ActorController extends Controller
->setJSON([]); ->setJSON([]);
} }
$replyToStatus = model('StatusModel') $replyToPost = model('PostModel')
->getStatusByUri($payload->object->inReplyTo); ->getPostByUri($payload->object->inReplyTo);
$reply = null; $reply = null;
if ($replyToStatus !== null) { if ($replyToPost !== null) {
// TODO: strip content from html to retrieve message // TODO: strip content from html to retrieve message
// remove all html tags and reconstruct message with mentions? // remove all html tags and reconstruct message with mentions?
extract_text_from_html($payload->object->content); extract_text_from_html($payload->object->content);
$reply = new Status([ $reply = new Post([
'uri' => $payload->object->id, 'uri' => $payload->object->id,
'actor_id' => $payloadActor->id, 'actor_id' => $payloadActor->id,
'in_reply_to_id' => $replyToStatus->id, 'in_reply_to_id' => $replyToPost->id,
'message' => $payload->object->content, 'message' => $payload->object->content,
'published_at' => Time::parse($payload->object->published), 'published_at' => Time::parse($payload->object->published),
]); ]);
} }
if ($reply !== null) { if ($reply !== null) {
$statusId = model('StatusModel') $postId = model('PostModel')
->addReply($reply, true, false); ->addReply($reply, true, false);
model('ActivityModel') model('ActivityModel')
->update($activityId, [ ->update($activityId, [
'status_id' => $statusId, 'post_id' => $postId,
]); ]);
} }
@ -135,12 +135,12 @@ class ActorController extends Controller
return $this->response->setStatusCode(501) return $this->response->setStatusCode(501)
->setJSON([]); ->setJSON([]);
case 'Delete': case 'Delete':
$statusToDelete = model('StatusModel') $postToDelete = model('PostModel')
->getStatusByUri($payload->object->id); ->getPostByUri($payload->object->id);
if ($statusToDelete !== null) { if ($postToDelete !== null) {
model('StatusModel') model('PostModel')
->removeStatus($statusToDelete, false); ->removePost($postToDelete, false);
} }
return $this->response->setStatusCode(200) return $this->response->setStatusCode(200)
@ -158,35 +158,35 @@ class ActorController extends Controller
->setJSON([]); ->setJSON([]);
case 'Like': case 'Like':
// get favourited status // get favourited post
$status = model('StatusModel') $post = model('PostModel')
->getStatusByUri($payload->object); ->getPostByUri($payload->object);
if ($status !== null) { if ($post !== null) {
// Like side-effect // Like side-effect
model('FavouriteModel') model('FavouriteModel')
->addFavourite($payloadActor, $status, false); ->addFavourite($payloadActor, $post, false);
model('ActivityModel') model('ActivityModel')
->update($activityId, [ ->update($activityId, [
'status_id' => $status->id, 'post_id' => $post->id,
]); ]);
} }
return $this->response->setStatusCode(200) return $this->response->setStatusCode(200)
->setJSON([]); ->setJSON([]);
case 'Announce': case 'Announce':
$status = model('StatusModel') $post = model('PostModel')
->getStatusByUri($payload->object); ->getPostByUri($payload->object);
if ($status !== null) { if ($post !== null) {
model('ActivityModel') model('ActivityModel')
->update($activityId, [ ->update($activityId, [
'status_id' => $status->id, 'post_id' => $post->id,
]); ]);
model('StatusModel') model('PostModel')
->reblog($payloadActor, $status, false); ->reblog($payloadActor, $post, false);
} }
return $this->response->setStatusCode(200) return $this->response->setStatusCode(200)
@ -204,45 +204,45 @@ class ActorController extends Controller
return $this->response->setStatusCode(202) return $this->response->setStatusCode(202)
->setJSON([]); ->setJSON([]);
case 'Like': case 'Like':
$status = model('StatusModel') $post = model('PostModel')
->getStatusByUri($payload->object->object); ->getPostByUri($payload->object->object);
if ($status !== null) { if ($post !== null) {
// revert side-effect by removing favourite from database // revert side-effect by removing favourite from database
model('FavouriteModel') model('FavouriteModel')
->removeFavourite($payloadActor, $status, false); ->removeFavourite($payloadActor, $post, false);
model('ActivityModel') model('ActivityModel')
->update($activityId, [ ->update($activityId, [
'status_id' => $status->id, 'post_id' => $post->id,
]); ]);
} }
return $this->response->setStatusCode(200) return $this->response->setStatusCode(200)
->setJSON([]); ->setJSON([]);
case 'Announce': case 'Announce':
$status = model('StatusModel') $post = model('PostModel')
->getStatusByUri($payload->object->object); ->getPostByUri($payload->object->object);
$reblogStatus = null; $reblogPost = null;
if ($status !== null) { if ($post !== null) {
$reblogStatus = model('StatusModel') $reblogPost = model('PostModel')
->where([ ->where([
'actor_id' => $payloadActor->id, 'actor_id' => $payloadActor->id,
'reblog_of_id' => service('uuid') 'reblog_of_id' => service('uuid')
->fromString($status->id) ->fromString($post->id)
->getBytes(), ->getBytes(),
]) ])
->first(); ->first();
} }
if ($reblogStatus !== null) { if ($reblogPost !== null) {
model('StatusModel') model('PostModel')
->undoReblog($reblogStatus, false); ->undoReblog($reblogPost, false);
model('ActivityModel') model('ActivityModel')
->update($activityId, [ ->update($activityId, [
'status_id' => $status->id, 'post_id' => $post->id,
]); ]);
} }

View File

@ -11,7 +11,7 @@ declare(strict_types=1);
namespace ActivityPub\Controllers; namespace ActivityPub\Controllers;
use ActivityPub\Config\ActivityPub; use ActivityPub\Config\ActivityPub;
use ActivityPub\Entities\Status; use ActivityPub\Entities\Post;
use ActivityPub\Objects\OrderedCollectionObject; use ActivityPub\Objects\OrderedCollectionObject;
use ActivityPub\Objects\OrderedCollectionPage; use ActivityPub\Objects\OrderedCollectionPage;
use CodeIgniter\Controller; use CodeIgniter\Controller;
@ -21,14 +21,14 @@ use CodeIgniter\HTTP\Response;
use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
class StatusController extends Controller class PostController extends Controller
{ {
/** /**
* @var string[] * @var string[]
*/ */
protected $helpers = ['activitypub']; protected $helpers = ['activitypub'];
protected Status $status; protected Post $post;
protected ActivityPub $config; protected ActivityPub $config;
@ -39,11 +39,11 @@ class StatusController extends Controller
public function _remap(string $method, string ...$params): mixed public function _remap(string $method, string ...$params): mixed
{ {
if (($status = model('StatusModel')->getStatusById($params[0])) === null) { if (($post = model('PostModel')->getPostById($params[0])) === null) {
throw PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
$this->status = $status; $this->post = $post;
unset($params[0]); unset($params[0]);
@ -56,7 +56,7 @@ class StatusController extends Controller
public function index(): Response public function index(): Response
{ {
$noteObjectClass = $this->config->noteObject; $noteObjectClass = $this->config->noteObject;
$noteObject = new $noteObjectClass($this->status); $noteObject = new $noteObjectClass($this->post);
return $this->response return $this->response
->setContentType('application/activity+json') ->setContentType('application/activity+json')
@ -69,22 +69,22 @@ class StatusController extends Controller
public function replies(): Response public function replies(): Response
{ {
/** /**
* get status replies * get post replies
*/ */
$statusReplies = model('StatusModel') $postReplies = model('PostModel')
->where('in_reply_to_id', service('uuid') ->fromString($this->status->id) ->getBytes()) ->where('in_reply_to_id', service('uuid') ->fromString($this->post->id) ->getBytes())
->where('`published_at` <= NOW()', null, false) ->where('`published_at` <= NOW()', null, false)
->orderBy('published_at', 'ASC'); ->orderBy('published_at', 'ASC');
$pageNumber = (int) $this->request->getGet('page'); $pageNumber = (int) $this->request->getGet('page');
if ($pageNumber < 1) { if ($pageNumber < 1) {
$statusReplies->paginate(12); $postReplies->paginate(12);
$pager = $statusReplies->pager; $pager = $postReplies->pager;
$collection = new OrderedCollectionObject(null, $pager); $collection = new OrderedCollectionObject(null, $pager);
} else { } else {
$paginatedReplies = $statusReplies->paginate(12, 'default', $pageNumber); $paginatedReplies = $postReplies->paginate(12, 'default', $pageNumber);
$pager = $statusReplies->pager; $pager = $postReplies->pager;
$orderedItems = []; $orderedItems = [];
$noteObjectClass = $this->config->noteObject; $noteObjectClass = $this->config->noteObject;
@ -118,21 +118,21 @@ class StatusController extends Controller
->with('errors', $this->validator->getErrors()); ->with('errors', $this->validator->getErrors());
} }
$newStatus = new Status([ $newPost = new Post([
'actor_id' => $this->request->getPost('actor_id'), 'actor_id' => $this->request->getPost('actor_id'),
'message' => $this->request->getPost('message'), 'message' => $this->request->getPost('message'),
'published_at' => Time::now(), 'published_at' => Time::now(),
]); ]);
if (! model('StatusModel')->addStatus($newStatus)) { if (! model('PostModel')->addPost($newPost)) {
return redirect() return redirect()
->back() ->back()
->withInput() ->withInput()
// TODO: translate // TODO: translate
->with('error', "Couldn't create Status"); ->with('error', "Couldn't create Post");
} }
// Status without preview card has been successfully created // Post without preview card has been successfully created
return redirect()->back(); return redirect()->back();
} }
@ -153,7 +153,7 @@ class StatusController extends Controller
->getActorById($this->request->getPost('actor_id')); ->getActorById($this->request->getPost('actor_id'));
model('FavouriteModel') model('FavouriteModel')
->toggleFavourite($actor, $this->status->id); ->toggleFavourite($actor, $this->post->id);
return redirect()->back(); return redirect()->back();
} }
@ -174,8 +174,8 @@ class StatusController extends Controller
$actor = model('ActorModel') $actor = model('ActorModel')
->getActorById($this->request->getPost('actor_id')); ->getActorById($this->request->getPost('actor_id'));
model('StatusModel') model('PostModel')
->toggleReblog($actor, $this->status); ->toggleReblog($actor, $this->post);
return redirect()->back(); return redirect()->back();
} }
@ -194,14 +194,14 @@ class StatusController extends Controller
->with('errors', $this->validator->getErrors()); ->with('errors', $this->validator->getErrors());
} }
$newReplyStatus = new Status([ $newReplyPost = new Post([
'actor_id' => $this->request->getPost('actor_id'), 'actor_id' => $this->request->getPost('actor_id'),
'in_reply_to_id' => $this->status->id, 'in_reply_to_id' => $this->post->id,
'message' => $this->request->getPost('message'), 'message' => $this->request->getPost('message'),
'published_at' => Time::now(), 'published_at' => Time::now(),
]); ]);
if (! model('StatusModel')->addReply($newReplyStatus)) { if (! model('PostModel')->addReply($newReplyPost)) {
return redirect() return redirect()
->back() ->back()
->withInput() ->withInput()
@ -209,7 +209,7 @@ class StatusController extends Controller
->with('error', "Couldn't create Reply"); ->with('error', "Couldn't create Reply");
} }
// Reply status without preview card has been successfully created // Reply post without preview card has been successfully created
return redirect()->back(); return redirect()->back();
} }
@ -249,33 +249,33 @@ class StatusController extends Controller
); );
if (! $ostatusKey) { if (! $ostatusKey) {
// TODO: error, couldn't remote favourite/share/reply to status // TODO: error, couldn't remote favourite/share/reply to post
// The instance doesn't allow its users remote actions on statuses // The instance doesn't allow its users remote actions on posts
return $this->response->setJSON([]); return $this->response->setJSON([]);
} }
return redirect()->to( return redirect()->to(
str_replace('{uri}', urlencode($this->status->uri), $data->links[$ostatusKey]->template), str_replace('{uri}', urlencode($this->post->uri), $data->links[$ostatusKey]->template),
); );
} }
public function attemptBlockActor(): RedirectResponse public function attemptBlockActor(): RedirectResponse
{ {
model('ActorModel')->blockActor($this->status->actor->id); model('ActorModel')->blockActor($this->post->actor->id);
return redirect()->back(); return redirect()->back();
} }
public function attemptBlockDomain(): RedirectResponse public function attemptBlockDomain(): RedirectResponse
{ {
model('BlockedDomainModel')->blockDomain($this->status->actor->domain); model('BlockedDomainModel')->blockDomain($this->post->actor->domain);
return redirect()->back(); return redirect()->back();
} }
public function attemptDelete(): RedirectResponse public function attemptDelete(): RedirectResponse
{ {
model('StatusModel', false)->removeStatus($this->status); model('PostModel', false)->removePost($this->post);
return redirect()->back(); return redirect()->back();
} }

View File

@ -33,7 +33,7 @@ class SchedulerController extends Controller
json_encode($scheduledActivity->payload, JSON_THROW_ON_ERROR), json_encode($scheduledActivity->payload, JSON_THROW_ON_ERROR),
); );
// set activity status to delivered // set activity post to delivered
model('ActivityModel') model('ActivityModel')
->update($scheduledActivity->id, [ ->update($scheduledActivity->id, [
'task_status' => 'delivered', 'task_status' => 'delivered',

View File

@ -34,7 +34,7 @@ class AddActors extends Migration
], ],
'domain' => [ 'domain' => [
'type' => 'VARCHAR', 'type' => 'VARCHAR',
'constraint' => 191, 'constraint' => 255,
], ],
'private_key' => [ 'private_key' => [
'type' => 'TEXT', 'type' => 'TEXT',
@ -93,7 +93,7 @@ class AddActors extends Migration
'unsigned' => true, 'unsigned' => true,
'default' => 0, 'default' => 0,
], ],
'statuses_count' => [ 'posts_count' => [
'type' => 'INT', 'type' => 'INT',
'unsigned' => true, 'unsigned' => true,
'default' => 0, 'default' => 0,

View File

@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* Class AddStatuses Creates activitypub_statuses table in database * Class AddPosts Creates activitypub_posts table in database
* *
* @copyright 2021 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
@ -14,7 +14,7 @@ namespace ActivityPub\Database\Migrations;
use CodeIgniter\Database\Migration; use CodeIgniter\Database\Migration;
class AddStatuses extends Migration class AddPosts extends Migration
{ {
public function up(): void public function up(): void
{ {
@ -25,7 +25,7 @@ class AddStatuses extends Migration
], ],
'uri' => [ 'uri' => [
'type' => 'VARCHAR', 'type' => 'VARCHAR',
'constraint' => 191, 'constraint' => 255,
], ],
'actor_id' => [ 'actor_id' => [
'type' => 'INT', 'type' => 'INT',
@ -76,16 +76,16 @@ class AddStatuses extends Migration
]); ]);
$this->forge->addPrimaryKey('id'); $this->forge->addPrimaryKey('id');
$this->forge->addUniqueKey('uri'); $this->forge->addUniqueKey('uri');
// FIXME: an actor must reblog a status only once // FIXME: an actor must reblog a post only once
// $this->forge->addUniqueKey(['actor_id', 'reblog_of_id']); // $this->forge->addUniqueKey(['actor_id', 'reblog_of_id']);
$this->forge->addForeignKey('actor_id', 'activitypub_actors', 'id', '', 'CASCADE'); $this->forge->addForeignKey('actor_id', 'activitypub_actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('in_reply_to_id', 'activitypub_statuses', 'id', '', 'CASCADE'); $this->forge->addForeignKey('in_reply_to_id', 'activitypub_posts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('reblog_of_id', 'activitypub_statuses', 'id', '', 'CASCADE'); $this->forge->addForeignKey('reblog_of_id', 'activitypub_posts', 'id', '', 'CASCADE');
$this->forge->createTable('activitypub_statuses'); $this->forge->createTable('activitypub_posts');
} }
public function down(): void public function down(): void
{ {
$this->forge->dropTable('activitypub_statuses'); $this->forge->dropTable('activitypub_posts');
} }
} }

View File

@ -32,7 +32,7 @@ class AddActivities extends Migration
'unsigned' => true, 'unsigned' => true,
'null' => true, 'null' => true,
], ],
'status_id' => [ 'post_id' => [
'type' => 'BINARY', 'type' => 'BINARY',
'constraint' => 16, 'constraint' => 16,
'null' => true, 'null' => true,
@ -62,7 +62,7 @@ class AddActivities extends Migration
$this->forge->addPrimaryKey('id'); $this->forge->addPrimaryKey('id');
$this->forge->addForeignKey('actor_id', 'activitypub_actors', 'id', '', 'CASCADE'); $this->forge->addForeignKey('actor_id', 'activitypub_actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('target_actor_id', 'activitypub_actors', 'id', '', 'CASCADE'); $this->forge->addForeignKey('target_actor_id', 'activitypub_actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('status_id', 'activitypub_statuses', 'id', '', 'CASCADE'); $this->forge->addForeignKey('post_id', 'activitypub_posts', 'id', '', 'CASCADE');
$this->forge->createTable('activitypub_activities'); $this->forge->createTable('activitypub_activities');
} }

View File

@ -23,15 +23,15 @@ class AddFavourites extends Migration
'type' => 'INT', 'type' => 'INT',
'unsigned' => true, 'unsigned' => true,
], ],
'status_id' => [ 'post_id' => [
'type' => 'BINARY', 'type' => 'BINARY',
'constraint' => 16, 'constraint' => 16,
], ],
]); ]);
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT current_timestamp()'); $this->forge->addField('`created_at` timestamp NOT NULL DEFAULT current_timestamp()');
$this->forge->addPrimaryKey(['actor_id', 'status_id']); $this->forge->addPrimaryKey(['actor_id', 'post_id']);
$this->forge->addForeignKey('actor_id', 'activitypub_actors', 'id', '', 'CASCADE'); $this->forge->addForeignKey('actor_id', 'activitypub_actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('status_id', 'activitypub_statuses', 'id', '', 'CASCADE'); $this->forge->addForeignKey('post_id', 'activitypub_posts', 'id', '', 'CASCADE');
$this->forge->createTable('activitypub_favourites'); $this->forge->createTable('activitypub_favourites');
} }

View File

@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* Class AddStatusesPreviewCards Creates activitypub_statuses_preview_cards table in database * Class AddPostsPreviewCards Creates activitypub_posts_preview_cards table in database
* *
* @copyright 2021 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
@ -14,12 +14,12 @@ namespace ActivityPub\Database\Migrations;
use CodeIgniter\Database\Migration; use CodeIgniter\Database\Migration;
class AddStatusesPreviewCards extends Migration class AddPostsPreviewCards extends Migration
{ {
public function up(): void public function up(): void
{ {
$this->forge->addField([ $this->forge->addField([
'status_id' => [ 'post_id' => [
'type' => 'BINARY', 'type' => 'BINARY',
'constraint' => 16, 'constraint' => 16,
], ],
@ -29,14 +29,14 @@ class AddStatusesPreviewCards extends Migration
], ],
]); ]);
$this->forge->addPrimaryKey(['status_id', 'preview_card_id']); $this->forge->addPrimaryKey(['post_id', 'preview_card_id']);
$this->forge->addForeignKey('status_id', 'activitypub_statuses', 'id', '', 'CASCADE'); $this->forge->addForeignKey('post_id', 'activitypub_posts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('preview_card_id', 'activitypub_preview_cards', 'id', '', 'CASCADE'); $this->forge->addForeignKey('preview_card_id', 'activitypub_preview_cards', 'id', '', 'CASCADE');
$this->forge->createTable('activitypub_statuses_preview_cards'); $this->forge->createTable('activitypub_posts_preview_cards');
} }
public function down(): void public function down(): void
{ {
$this->forge->dropTable('activitypub_statuses_preview_cards'); $this->forge->dropTable('activitypub_posts_preview_cards');
} }
} }

View File

@ -21,7 +21,7 @@ class AddBlockedDomains extends Migration
$this->forge->addField([ $this->forge->addField([
'name' => [ 'name' => [
'type' => 'VARCHAR', 'type' => 'VARCHAR',
'constraint' => 191, 'constraint' => 255,
], ],
'created_at' => [ 'created_at' => [
'type' => 'DATETIME', 'type' => 'DATETIME',

View File

@ -19,8 +19,8 @@ use RuntimeException;
* @property Actor $actor * @property Actor $actor
* @property int|null $target_actor_id * @property int|null $target_actor_id
* @property Actor $target_actor * @property Actor $target_actor
* @property string|null $status_id * @property string|null $post_id
* @property Status $status * @property Post $post
* @property string $type * @property string $type
* @property object $payload * @property object $payload
* @property string|null $task_status * @property string|null $task_status
@ -33,12 +33,12 @@ class Activity extends UuidEntity
protected ?Actor $target_actor = null; protected ?Actor $target_actor = null;
protected ?Status $status = null; protected ?Post $post = null;
/** /**
* @var string[] * @var string[]
*/ */
protected $uuids = ['id', 'status_id']; protected $uuids = ['id', 'post_id'];
/** /**
* @var string[] * @var string[]
@ -52,7 +52,7 @@ class Activity extends UuidEntity
'id' => 'string', 'id' => 'string',
'actor_id' => 'integer', 'actor_id' => 'integer',
'target_actor_id' => '?integer', 'target_actor_id' => '?integer',
'status_id' => '?string', 'post_id' => '?string',
'type' => 'string', 'type' => 'string',
'payload' => 'json', 'payload' => 'json',
'task_status' => '?string', 'task_status' => '?string',
@ -86,17 +86,17 @@ class Activity extends UuidEntity
return $this->target_actor; return $this->target_actor;
} }
public function getStatus(): Status public function getPost(): Post
{ {
if ($this->status_id === null) { if ($this->post_id === null) {
throw new RuntimeException('Activity must have a status_id before getting status.'); throw new RuntimeException('Activity must have a post_id before getting post.');
} }
if ($this->status === null) { if ($this->post === null) {
$this->status = model('StatusModel', false) $this->post = model('PostModel', false)
->getStatusById($this->status_id); ->getPostById($this->post_id);
} }
return $this->status; return $this->post;
} }
} }

View File

@ -31,7 +31,7 @@ use RuntimeException;
* @property string|null $outbox_url * @property string|null $outbox_url
* @property string|null $followers_url * @property string|null $followers_url
* @property int $followers_count * @property int $followers_count
* @property int $statuses_count * @property int $posts_count
* @property bool $is_blocked * @property bool $is_blocked
* *
* @property Actor[] $followers * @property Actor[] $followers
@ -68,7 +68,7 @@ class Actor extends Entity
'outbox_url' => '?string', 'outbox_url' => '?string',
'followers_url' => '?string', 'followers_url' => '?string',
'followers_count' => 'integer', 'followers_count' => 'integer',
'statuses_count' => 'integer', 'posts_count' => 'integer',
'is_blocked' => 'boolean', 'is_blocked' => 'boolean',
]; ];

View File

@ -14,20 +14,20 @@ use Michalsn\Uuid\UuidEntity;
/** /**
* @property int $actor_id * @property int $actor_id
* @property string $status_id * @property string $post_id
*/ */
class Favourite extends UuidEntity class Favourite extends UuidEntity
{ {
/** /**
* @var string[] * @var string[]
*/ */
protected $uuids = ['status_id']; protected $uuids = ['post_id'];
/** /**
* @var array<string, string> * @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'actor_id' => 'integer', 'actor_id' => 'integer',
'status_id' => 'string', 'post_id' => 'string',
]; ];
} }

View File

@ -20,9 +20,9 @@ use RuntimeException;
* @property int $actor_id * @property int $actor_id
* @property Actor $actor * @property Actor $actor
* @property string|null $in_reply_to_id * @property string|null $in_reply_to_id
* @property Status|null $reply_to_status * @property Post|null $reply_to_post
* @property string|null $reblog_of_id * @property string|null $reblog_of_id
* @property Status|null $reblog_of_status * @property Post|null $reblog_of_post
* @property string $message * @property string $message
* @property string $message_html * @property string $message_html
* @property int $favourites_count * @property int $favourites_count
@ -35,30 +35,30 @@ use RuntimeException;
* @property PreviewCard|null $preview_card * @property PreviewCard|null $preview_card
* *
* @property bool $has_replies * @property bool $has_replies
* @property Status[] $replies * @property Post[] $replies
* @property Status[] $reblogs * @property Post[] $reblogs
*/ */
class Status extends UuidEntity class Post extends UuidEntity
{ {
protected ?Actor $actor = null; protected ?Actor $actor = null;
protected ?Status $reply_to_status = null; protected ?Post $reply_to_post = null;
protected ?Status $reblog_of_status = null; protected ?Post $reblog_of_post = null;
protected ?PreviewCard $preview_card = null; protected ?PreviewCard $preview_card = null;
protected bool $has_preview_card = false; protected bool $has_preview_card = false;
/** /**
* @var Status[]|null * @var Post[]|null
*/ */
protected ?array $replies = null; protected ?array $replies = null;
protected bool $has_replies = false; protected bool $has_replies = false;
/** /**
* @var Status[]|null * @var Post[]|null
*/ */
protected ?array $reblogs = null; protected ?array $reblogs = null;
@ -89,12 +89,12 @@ class Status extends UuidEntity
]; ];
/** /**
* Returns the status's actor * Returns the post's actor
*/ */
public function getActor(): Actor public function getActor(): Actor
{ {
if ($this->actor_id === null) { if ($this->actor_id === null) {
throw new RuntimeException('Status must have an actor_id before getting actor.'); throw new RuntimeException('Post must have an actor_id before getting actor.');
} }
if ($this->actor === null) { if ($this->actor === null) {
@ -108,12 +108,12 @@ class Status extends UuidEntity
public function getPreviewCard(): ?PreviewCard public function getPreviewCard(): ?PreviewCard
{ {
if ($this->id === null) { if ($this->id === null) {
throw new RuntimeException('Status must be created before getting preview_card.'); throw new RuntimeException('Post must be created before getting preview_card.');
} }
if ($this->preview_card === null) { if ($this->preview_card === null) {
$this->preview_card = model('PreviewCardModel', false) $this->preview_card = model('PreviewCardModel', false)
->getStatusPreviewCard($this->id); ->getPostPreviewCard($this->id);
} }
return $this->preview_card; return $this->preview_card;
@ -125,17 +125,17 @@ class Status extends UuidEntity
} }
/** /**
* @return Status[] * @return Post[]
*/ */
public function getReplies(): array public function getReplies(): array
{ {
if ($this->id === null) { if ($this->id === null) {
throw new RuntimeException('Status must be created before getting replies.'); throw new RuntimeException('Post must be created before getting replies.');
} }
if ($this->replies === null) { if ($this->replies === null) {
$this->replies = (array) model('StatusModel', false) $this->replies = (array) model('PostModel', false)
->getStatusReplies($this->id); ->getPostReplies($this->id);
} }
return $this->replies; return $this->replies;
@ -146,49 +146,49 @@ class Status extends UuidEntity
return $this->getReplies() !== null; return $this->getReplies() !== null;
} }
public function getReplyToStatus(): ?self public function getReplyToPost(): ?self
{ {
if ($this->in_reply_to_id === null) { if ($this->in_reply_to_id === null) {
throw new RuntimeException('Status is not a reply.'); throw new RuntimeException('Post is not a reply.');
} }
if ($this->reply_to_status === null) { if ($this->reply_to_post === null) {
$this->reply_to_status = model('StatusModel', false) $this->reply_to_post = model('PostModel', false)
->getStatusById($this->in_reply_to_id); ->getPostById($this->in_reply_to_id);
} }
return $this->reply_to_status; return $this->reply_to_post;
} }
/** /**
* @return Status[] * @return Post[]
*/ */
public function getReblogs(): array public function getReblogs(): array
{ {
if ($this->id === null) { if ($this->id === null) {
throw new RuntimeException('Status must be created before getting reblogs.'); throw new RuntimeException('Post must be created before getting reblogs.');
} }
if ($this->reblogs === null) { if ($this->reblogs === null) {
$this->reblogs = (array) model('StatusModel', false) $this->reblogs = (array) model('PostModel', false)
->getStatusReblogs($this->id); ->getPostReblogs($this->id);
} }
return $this->reblogs; return $this->reblogs;
} }
public function getReblogOfStatus(): ?self public function getReblogOfPost(): ?self
{ {
if ($this->reblog_of_id === null) { if ($this->reblog_of_id === null) {
throw new RuntimeException('Status is not a reblog.'); throw new RuntimeException('Post is not a reblog.');
} }
if ($this->reblog_of_status === null) { if ($this->reblog_of_post === null) {
$this->reblog_of_status = model('StatusModel', false) $this->reblog_of_post = model('PostModel', false)
->getStatusById($this->reblog_of_id); ->getPostById($this->reblog_of_id);
} }
return $this->reblog_of_status; return $this->reblog_of_post;
} }
public function setMessage(string $message): static public function setMessage(string $message): static

View File

@ -14,7 +14,7 @@ use CodeIgniter\Entity\Entity;
/** /**
* @property int $id * @property int $id
* @property string $status_id * @property string $post_id
* @property string $url * @property string $url
* @property string $title * @property string $title
* @property string $description * @property string $description
@ -33,7 +33,7 @@ class PreviewCard extends Entity
*/ */
protected $casts = [ protected $casts = [
'id' => 'integer', 'id' => 'integer',
'status_id' => 'string', 'post_id' => 'string',
'url' => 'string', 'url' => 'string',
'title' => 'string', 'title' => 'string',
'description' => 'string', 'description' => 'string',

View File

@ -31,7 +31,7 @@ class ActivityModel extends UuidModel
/** /**
* @var string[] * @var string[]
*/ */
protected $uuidFields = ['id', 'status_id']; protected $uuidFields = ['id', 'post_id'];
/** /**
* @var string[] * @var string[]
@ -40,7 +40,7 @@ class ActivityModel extends UuidModel
'id', 'id',
'actor_id', 'actor_id',
'target_actor_id', 'target_actor_id',
'status_id', 'post_id',
'type', 'type',
'payload', 'payload',
'task_status', 'task_status',
@ -88,7 +88,7 @@ class ActivityModel extends UuidModel
string $type, string $type,
int $actorId, int $actorId,
?int $targetActorId, ?int $targetActorId,
?string $statusId, ?string $postId,
string $payload, string $payload,
DateTimeInterface $scheduledAt = null, DateTimeInterface $scheduledAt = null,
?string $taskStatus = null ?string $taskStatus = null
@ -97,7 +97,7 @@ class ActivityModel extends UuidModel
[ [
'actor_id' => $actorId, 'actor_id' => $actorId,
'target_actor_id' => $targetActorId, 'target_actor_id' => $targetActorId,
'status_id' => $statusId, 'post_id' => $postId,
'type' => $type, 'type' => $type,
'payload' => $payload, 'payload' => $payload,
'scheduled_at' => $scheduledAt, 'scheduled_at' => $scheduledAt,

View File

@ -41,7 +41,7 @@ class ActorModel extends Model
'outbox_url', 'outbox_url',
'followers_url', 'followers_url',
'followers_count', 'followers_count',
'statuses_count', 'posts_count',
'is_blocked', 'is_blocked',
]; ];

View File

@ -14,7 +14,7 @@ use ActivityPub\Activities\LikeActivity;
use ActivityPub\Activities\UndoActivity; use ActivityPub\Activities\UndoActivity;
use ActivityPub\Entities\Actor; use ActivityPub\Entities\Actor;
use ActivityPub\Entities\Favourite; use ActivityPub\Entities\Favourite;
use ActivityPub\Entities\Status; use ActivityPub\Entities\Post;
use CodeIgniter\Events\Events; use CodeIgniter\Events\Events;
use Michalsn\Uuid\UuidModel; use Michalsn\Uuid\UuidModel;
@ -28,12 +28,12 @@ class FavouriteModel extends UuidModel
/** /**
* @var string[] * @var string[]
*/ */
protected $uuidFields = ['status_id']; protected $uuidFields = ['post_id'];
/** /**
* @var string[] * @var string[]
*/ */
protected $allowedFields = ['actor_id', 'status_id']; protected $allowedFields = ['actor_id', 'post_id'];
/** /**
* @var string * @var string
@ -47,32 +47,32 @@ class FavouriteModel extends UuidModel
protected $updatedField; protected $updatedField;
public function addFavourite(Actor $actor, Status $status, bool $registerActivity = true): void public function addFavourite(Actor $actor, Post $post, bool $registerActivity = true): void
{ {
$this->db->transStart(); $this->db->transStart();
$this->insert([ $this->insert([
'actor_id' => $actor->id, 'actor_id' => $actor->id,
'status_id' => $status->id, 'post_id' => $post->id,
]); ]);
model('StatusModel') model('PostModel')
->where('id', service('uuid') ->fromString($status->id) ->getBytes()) ->where('id', service('uuid') ->fromString($post->id) ->getBytes())
->increment('favourites_count'); ->increment('favourites_count');
if ($registerActivity) { if ($registerActivity) {
$likeActivity = new LikeActivity(); $likeActivity = new LikeActivity();
$likeActivity->set('actor', $actor->uri) $likeActivity->set('actor', $actor->uri)
->set('object', $status->uri); ->set('object', $post->uri);
$activityId = model('ActivityModel') $activityId = model('ActivityModel')
->newActivity( ->newActivity(
'Like', 'Like',
$actor->id, $actor->id,
null, null,
$status->id, $post->id,
$likeActivity->toJSON(), $likeActivity->toJSON(),
$status->published_at, $post->published_at,
'queued', 'queued',
); );
@ -84,28 +84,28 @@ class FavouriteModel extends UuidModel
]); ]);
} }
Events::trigger('on_status_favourite', $actor, $status); Events::trigger('on_post_favourite', $actor, $post);
model('StatusModel') model('PostModel')
->clearCache($status); ->clearCache($post);
$this->db->transComplete(); $this->db->transComplete();
} }
public function removeFavourite(Actor $actor, Status $status, bool $registerActivity = true): void public function removeFavourite(Actor $actor, Post $post, bool $registerActivity = true): void
{ {
$this->db->transStart(); $this->db->transStart();
model('StatusModel') model('PostModel')
->where('id', service('uuid') ->fromString($status->id) ->getBytes()) ->where('id', service('uuid') ->fromString($post->id) ->getBytes())
->decrement('favourites_count'); ->decrement('favourites_count');
$this->db $this->db
->table('activitypub_favourites') ->table('activitypub_favourites')
->where([ ->where([
'actor_id' => $actor->id, 'actor_id' => $actor->id,
'status_id' => service('uuid') 'post_id' => service('uuid')
->fromString($status->id) ->fromString($post->id)
->getBytes(), ->getBytes(),
]) ])
->delete(); ->delete();
@ -117,8 +117,8 @@ class FavouriteModel extends UuidModel
->where([ ->where([
'type' => 'Like', 'type' => 'Like',
'actor_id' => $actor->id, 'actor_id' => $actor->id,
'status_id' => service('uuid') 'post_id' => service('uuid')
->fromString($status->id) ->fromString($post->id)
->getBytes(), ->getBytes(),
]) ])
->first(); ->first();
@ -127,7 +127,7 @@ class FavouriteModel extends UuidModel
$likeActivity $likeActivity
->set('id', url_to('activity', $actor->username, $activity->id)) ->set('id', url_to('activity', $actor->username, $activity->id))
->set('actor', $actor->uri) ->set('actor', $actor->uri)
->set('object', $status->uri); ->set('object', $post->uri);
$undoActivity $undoActivity
->set('actor', $actor->uri) ->set('actor', $actor->uri)
@ -138,9 +138,9 @@ class FavouriteModel extends UuidModel
'Undo', 'Undo',
$actor->id, $actor->id,
null, null,
$status->id, $post->id,
$undoActivity->toJSON(), $undoActivity->toJSON(),
$status->published_at, $post->published_at,
'queued', 'queued',
); );
@ -152,10 +152,10 @@ class FavouriteModel extends UuidModel
]); ]);
} }
Events::trigger('on_status_undo_favourite', $actor, $status); Events::trigger('on_post_undo_favourite', $actor, $post);
model('StatusModel') model('PostModel')
->clearCache($status); ->clearCache($post);
$this->db->transComplete(); $this->db->transComplete();
} }
@ -163,19 +163,19 @@ class FavouriteModel extends UuidModel
/** /**
* Adds or removes favourite from database and increments count * Adds or removes favourite from database and increments count
*/ */
public function toggleFavourite(Actor $actor, Status $status): void public function toggleFavourite(Actor $actor, Post $post): void
{ {
if ( if (
$this->where([ $this->where([
'actor_id' => $actor->id, 'actor_id' => $actor->id,
'status_id' => service('uuid') 'post_id' => service('uuid')
->fromString($status->id) ->fromString($post->id)
->getBytes(), ->getBytes(),
])->first() ])->first()
) { ) {
$this->removeFavourite($actor, $status); $this->removeFavourite($actor, $post);
} else { } else {
$this->addFavourite($actor, $status); $this->addFavourite($actor, $post);
} }
} }
} }

View File

@ -15,7 +15,7 @@ use ActivityPub\Activities\CreateActivity;
use ActivityPub\Activities\DeleteActivity; use ActivityPub\Activities\DeleteActivity;
use ActivityPub\Activities\UndoActivity; use ActivityPub\Activities\UndoActivity;
use ActivityPub\Entities\Actor; use ActivityPub\Entities\Actor;
use ActivityPub\Entities\Status; use ActivityPub\Entities\Post;
use ActivityPub\Objects\TombstoneObject; use ActivityPub\Objects\TombstoneObject;
use CodeIgniter\Database\BaseResult; use CodeIgniter\Database\BaseResult;
use CodeIgniter\Database\Query; use CodeIgniter\Database\Query;
@ -25,12 +25,12 @@ use CodeIgniter\I18n\Time;
use Exception; use Exception;
use Michalsn\Uuid\UuidModel; use Michalsn\Uuid\UuidModel;
class StatusModel extends UuidModel class PostModel extends UuidModel
{ {
/** /**
* @var string * @var string
*/ */
protected $table = 'activitypub_statuses'; protected $table = 'activitypub_posts';
/** /**
* @var string * @var string
@ -62,7 +62,7 @@ class StatusModel extends UuidModel
/** /**
* @var string * @var string
*/ */
protected $returnType = Status::class; protected $returnType = Post::class;
/** /**
* @var bool * @var bool
@ -87,14 +87,14 @@ class StatusModel extends UuidModel
/** /**
* @var string[] * @var string[]
*/ */
protected $beforeInsert = ['setStatusId']; protected $beforeInsert = ['setPostId'];
public function getStatusById(string $statusId): ?Status public function getPostById(string $postId): ?Post
{ {
$cacheName = config('ActivityPub') $cacheName = config('ActivityPub')
->cachePrefix . "status#{$statusId}"; ->cachePrefix . "post#{$postId}";
if (! ($found = cache($cacheName))) { if (! ($found = cache($cacheName))) {
$found = $this->find($statusId); $found = $this->find($postId);
cache() cache()
->save($cacheName, $found, DECADE); ->save($cacheName, $found, DECADE);
@ -103,14 +103,14 @@ class StatusModel extends UuidModel
return $found; return $found;
} }
public function getStatusByUri(string $statusUri): ?Status public function getPostByUri(string $postUri): ?Post
{ {
$hashedStatusUri = md5($statusUri); $hashedPostUri = md5($postUri);
$cacheName = $cacheName =
config('ActivityPub') config('ActivityPub')
->cachePrefix . "status-{$hashedStatusUri}"; ->cachePrefix . "post-{$hashedPostUri}";
if (! ($found = cache($cacheName))) { if (! ($found = cache($cacheName))) {
$found = $this->where('uri', $statusUri) $found = $this->where('uri', $postUri)
->first(); ->first();
cache() cache()
@ -121,16 +121,16 @@ class StatusModel extends UuidModel
} }
/** /**
* Retrieves all published statuses for a given actor ordered by publication date * Retrieves all published posts for a given actor ordered by publication date
* *
* @return Status[] * @return Post[]
*/ */
public function getActorPublishedStatuses(int $actorId): array public function getActorPublishedPosts(int $actorId): array
{ {
$cacheName = $cacheName =
config('ActivityPub') config('ActivityPub')
->cachePrefix . ->cachePrefix .
"actor#{$actorId}_published_statuses"; "actor#{$actorId}_published_posts";
if (! ($found = cache($cacheName))) { if (! ($found = cache($cacheName))) {
$found = $this->where([ $found = $this->where([
'actor_id' => $actorId, 'actor_id' => $actorId,
@ -140,20 +140,20 @@ class StatusModel extends UuidModel
->orderBy('published_at', 'DESC') ->orderBy('published_at', 'DESC')
->findAll(); ->findAll();
$secondsToNextUnpublishedStatus = $this->getSecondsToNextUnpublishedStatuses($actorId); $secondsToNextUnpublishedPost = $this->getSecondsToNextUnpublishedPosts($actorId);
cache() cache()
->save($cacheName, $found, $secondsToNextUnpublishedStatus ? $secondsToNextUnpublishedStatus : DECADE); ->save($cacheName, $found, $secondsToNextUnpublishedPost ? $secondsToNextUnpublishedPost : DECADE);
} }
return $found; return $found;
} }
/** /**
* Returns the timestamp difference in seconds between the next status to publish and the current timestamp. Returns * Returns the timestamp difference in seconds between the next post to publish and the current timestamp. Returns
* false if there's no status to publish * false if there's no post to publish
*/ */
public function getSecondsToNextUnpublishedStatuses(int $actorId): int | false public function getSecondsToNextUnpublishedPosts(int $actorId): int | false
{ {
$result = $this->select('TIMESTAMPDIFF(SECOND, NOW(), `published_at`) as timestamp_diff') $result = $this->select('TIMESTAMPDIFF(SECOND, NOW(), `published_at`) as timestamp_diff')
->where([ ->where([
@ -170,26 +170,26 @@ class StatusModel extends UuidModel
} }
/** /**
* Retrieves all published replies for a given status. By default, it does not get replies from blocked actors. * Retrieves all published replies for a given post. By default, it does not get replies from blocked actors.
* *
* @return Status[] * @return Post[]
*/ */
public function getStatusReplies(string $statusId, bool $withBlocked = false): array public function getPostReplies(string $postId, bool $withBlocked = false): array
{ {
$cacheName = $cacheName =
config('ActivityPub') config('ActivityPub')
->cachePrefix . ->cachePrefix .
"status#{$statusId}_replies" . "post#{$postId}_replies" .
($withBlocked ? '_withBlocked' : ''); ($withBlocked ? '_withBlocked' : '');
if (! ($found = cache($cacheName))) { if (! ($found = cache($cacheName))) {
if (! $withBlocked) { if (! $withBlocked) {
$this->select('activitypub_statuses.*') $this->select('activitypub_posts.*')
->join('activitypub_actors', 'activitypub_actors.id = activitypub_statuses.actor_id', 'inner') ->join('activitypub_actors', 'activitypub_actors.id = activitypub_posts.actor_id', 'inner')
->where('activitypub_actors.is_blocked', 0); ->where('activitypub_actors.is_blocked', 0);
} }
$this->where('in_reply_to_id', $this->uuid->fromString($statusId) ->getBytes()) $this->where('in_reply_to_id', $this->uuid->fromString($postId) ->getBytes())
->where('`published_at` <= NOW()', null, false) ->where('`published_at` <= NOW()', null, false)
->orderBy('published_at', 'ASC'); ->orderBy('published_at', 'ASC');
$found = $this->findAll(); $found = $this->findAll();
@ -202,18 +202,18 @@ class StatusModel extends UuidModel
} }
/** /**
* Retrieves all published reblogs for a given status * Retrieves all published reblogs for a given post
* *
* @return Status[] * @return Post[]
*/ */
public function getStatusReblogs(string $statusId): array public function getPostReblogs(string $postId): array
{ {
$cacheName = $cacheName =
config('ActivityPub') config('ActivityPub')
->cachePrefix . "status#{$statusId}_reblogs"; ->cachePrefix . "post#{$postId}_reblogs";
if (! ($found = cache($cacheName))) { if (! ($found = cache($cacheName))) {
$found = $this->where('reblog_of_id', $this->uuid->fromString($statusId) ->getBytes()) $found = $this->where('reblog_of_id', $this->uuid->fromString($postId) ->getBytes())
->where('`published_at` <= NOW()', null, false) ->where('`published_at` <= NOW()', null, false)
->orderBy('published_at', 'ASC') ->orderBy('published_at', 'ASC')
->findAll(); ->findAll();
@ -225,23 +225,23 @@ class StatusModel extends UuidModel
return $found; return $found;
} }
public function addPreviewCard(string $statusId, int $previewCardId): Query | bool public function addPreviewCard(string $postId, int $previewCardId): Query | bool
{ {
return $this->db->table('activitypub_statuses_preview_cards') return $this->db->table('activitypub_posts_preview_cards')
->insert([ ->insert([
'status_id' => $this->uuid->fromString($statusId) 'post_id' => $this->uuid->fromString($postId)
->getBytes(), ->getBytes(),
'preview_card_id' => $previewCardId, 'preview_card_id' => $previewCardId,
]); ]);
} }
/** /**
* Adds status in database along preview card if relevant * Adds post in database along preview card if relevant
* *
* @return string|false returns the new status id if success or false otherwise * @return string|false returns the new post id if success or false otherwise
*/ */
public function addStatus( public function addPost(
Status $status, Post $post,
bool $createPreviewCard = true, bool $createPreviewCard = true,
bool $registerActivity = true bool $registerActivity = true
): string | false { ): string | false {
@ -249,101 +249,101 @@ class StatusModel extends UuidModel
$this->db->transStart(); $this->db->transStart();
if (! ($newStatusId = $this->insert($status, true))) { if (! ($newPostId = $this->insert($post, true))) {
$this->db->transRollback(); $this->db->transRollback();
// Couldn't insert status // Couldn't insert post
return false; return false;
} }
if ($createPreviewCard) { if ($createPreviewCard) {
// parse message // parse message
$messageUrls = extract_urls_from_message($status->message); $messageUrls = extract_urls_from_message($post->message);
if ( if (
$messageUrls !== [] && $messageUrls !== [] &&
($previewCard = get_or_create_preview_card_from_url(new URI($messageUrls[0]))) && ($previewCard = get_or_create_preview_card_from_url(new URI($messageUrls[0]))) &&
! $this->addPreviewCard($newStatusId, $previewCard->id) ! $this->addPreviewCard($newPostId, $previewCard->id)
) { ) {
$this->db->transRollback(); $this->db->transRollback();
// problem when linking status to preview card // problem when linking post to preview card
return false; return false;
} }
} }
model('ActorModel') model('ActorModel', false)
->where('id', $status->actor_id) ->where('id', $post->actor_id)
->increment('statuses_count'); ->increment('posts_count');
if ($registerActivity) { if ($registerActivity) {
// set status id and uri to construct NoteObject // set post id and uri to construct NoteObject
$status->id = $newStatusId; $post->id = $newPostId;
$status->uri = url_to('status', $status->actor->username, $newStatusId); $post->uri = url_to('post', $post->actor->username, $newPostId);
$createActivity = new CreateActivity(); $createActivity = new CreateActivity();
$noteObjectClass = config('ActivityPub') $noteObjectClass = config('ActivityPub')
->noteObject; ->noteObject;
$createActivity $createActivity
->set('actor', $status->actor->uri) ->set('actor', $post->actor->uri)
->set('object', new $noteObjectClass($status)); ->set('object', new $noteObjectClass($post));
$activityId = model('ActivityModel') $activityId = model('ActivityModel', false)
->newActivity( ->newActivity(
'Create', 'Create',
$status->actor_id, $post->actor_id,
null, null,
$newStatusId, $newPostId,
$createActivity->toJSON(), $createActivity->toJSON(),
$status->published_at, $post->published_at,
'queued', 'queued',
); );
$createActivity->set('id', url_to('activity', $status->actor->username, $activityId)); $createActivity->set('id', url_to('activity', $post->actor->username, $activityId));
model('ActivityModel') model('ActivityModel', false)
->update($activityId, [ ->update($activityId, [
'payload' => $createActivity->toJSON(), 'payload' => $createActivity->toJSON(),
]); ]);
} }
Events::trigger('on_status_add', $status); Events::trigger('on_post_add', $post);
$this->clearCache($status); $this->clearCache($post);
$this->db->transComplete(); $this->db->transComplete();
return $newStatusId; return $newPostId;
} }
public function editStatus(Status $updatedStatus): bool public function editPost(Post $updatedPost): bool
{ {
$this->db->transStart(); $this->db->transStart();
// update status create activity schedule in database // update post create activity schedule in database
$scheduledActivity = model('ActivityModel') $scheduledActivity = model('ActivityModel', false)
->where([ ->where([
'type' => 'Create', 'type' => 'Create',
'status_id' => $this->uuid 'post_id' => $this->uuid
->fromString($updatedStatus->id) ->fromString($updatedPost->id)
->getBytes(), ->getBytes(),
]) ])
->first(); ->first();
// update published date in payload // update published date in payload
$newPayload = $scheduledActivity->payload; $newPayload = $scheduledActivity->payload;
$newPayload->object->published = $updatedStatus->published_at->format(DATE_W3C); $newPayload->object->published = $updatedPost->published_at->format(DATE_W3C);
model('ActivityModel') model('ActivityModel', false)
->update($scheduledActivity->id, [ ->update($scheduledActivity->id, [
'payload' => json_encode($newPayload, JSON_THROW_ON_ERROR), 'payload' => json_encode($newPayload, JSON_THROW_ON_ERROR),
'scheduled_at' => $updatedStatus->published_at, 'scheduled_at' => $updatedPost->published_at,
]); ]);
// update status // update post
$updateResult = $this->update($updatedStatus->id, $updatedStatus); $updateResult = $this->update($updatedPost->id, $updatedPost);
Events::trigger('on_status_edit', $updatedStatus); Events::trigger('on_post_edit', $updatedPost);
$this->clearCache($updatedStatus); $this->clearCache($updatedPost);
$this->db->transComplete(); $this->db->transComplete();
@ -351,59 +351,59 @@ class StatusModel extends UuidModel
} }
/** /**
* Removes a status from the database and decrements meta data * Removes a post from the database and decrements meta data
*/ */
public function removeStatus(Status $status, bool $registerActivity = true): BaseResult | bool public function removePost(Post $post, bool $registerActivity = true): BaseResult | bool
{ {
$this->db->transStart(); $this->db->transStart();
model('ActorModel') model('ActorModel', false)
->where('id', $status->actor_id) ->where('id', $post->actor_id)
->decrement('statuses_count'); ->decrement('posts_count');
if ($status->in_reply_to_id !== null) { if ($post->in_reply_to_id !== null) {
// Status to remove is a reply // Post to remove is a reply
model('StatusModel') model('PostModel', false)
->where('id', $this->uuid->fromString($status->in_reply_to_id) ->getBytes()) ->where('id', $this->uuid->fromString($post->in_reply_to_id) ->getBytes())
->decrement('replies_count'); ->decrement('replies_count');
Events::trigger('on_reply_remove', $status); Events::trigger('on_reply_remove', $post);
} }
// remove all status reblogs // remove all post reblogs
foreach ($status->reblogs as $reblog) { foreach ($post->reblogs as $reblog) {
// FIXME: issue when actor is not local, can't get actor information // FIXME: issue when actor is not local, can't get actor information
$this->removeStatus($reblog); $this->removePost($reblog);
} }
// remove all status replies // remove all post replies
foreach ($status->replies as $reply) { foreach ($post->replies as $reply) {
$this->removeStatus($reply); $this->removePost($reply);
} }
// check that preview card is no longer used elsewhere before deleting it // check that preview card is no longer used elsewhere before deleting it
if ( if (
$status->preview_card && $post->preview_card &&
$this->db $this->db
->table('activitypub_statuses_preview_cards') ->table('activitypub_posts_preview_cards')
->where('preview_card_id', $status->preview_card->id) ->where('preview_card_id', $post->preview_card->id)
->countAll() <= 1 ->countAll() <= 1
) { ) {
model('PreviewCardModel')->deletePreviewCard($status->preview_card->id, $status->preview_card->url); model('PreviewCardModel', false)->deletePreviewCard($post->preview_card->id, $post->preview_card->url);
} }
if ($registerActivity) { if ($registerActivity) {
$deleteActivity = new DeleteActivity(); $deleteActivity = new DeleteActivity();
$tombstoneObject = new TombstoneObject(); $tombstoneObject = new TombstoneObject();
$tombstoneObject->set('id', $status->uri); $tombstoneObject->set('id', $post->uri);
$deleteActivity $deleteActivity
->set('actor', $status->actor->uri) ->set('actor', $post->actor->uri)
->set('object', $tombstoneObject); ->set('object', $tombstoneObject);
$activityId = model('ActivityModel') $activityId = model('ActivityModel', false)
->newActivity( ->newActivity(
'Delete', 'Delete',
$status->actor_id, $post->actor_id,
null, null,
null, null,
$deleteActivity->toJSON(), $deleteActivity->toJSON(),
@ -411,20 +411,20 @@ class StatusModel extends UuidModel
'queued', 'queued',
); );
$deleteActivity->set('id', url_to('activity', $status->actor->username, $activityId)); $deleteActivity->set('id', url_to('activity', $post->actor->username, $activityId));
model('ActivityModel') model('ActivityModel', false)
->update($activityId, [ ->update($activityId, [
'payload' => $deleteActivity->toJSON(), 'payload' => $deleteActivity->toJSON(),
]); ]);
} }
$result = model('StatusModel', false) $result = model('PostModel', false)
->delete($status->id); ->delete($post->id);
Events::trigger('on_status_remove', $status); Events::trigger('on_post_remove', $post);
$this->clearCache($status); $this->clearCache($post);
$this->db->transComplete(); $this->db->transComplete();
@ -432,182 +432,182 @@ class StatusModel extends UuidModel
} }
public function addReply( public function addReply(
Status $reply, Post $reply,
bool $createPreviewCard = true, bool $createPreviewCard = true,
bool $registerActivity = true bool $registerActivity = true
): string | false { ): string | false {
if (! $reply->in_reply_to_id) { if (! $reply->in_reply_to_id) {
throw new Exception('Passed status is not a reply!'); throw new Exception('Passed post is not a reply!');
} }
$this->db->transStart(); $this->db->transStart();
$statusId = $this->addStatus($reply, $createPreviewCard, $registerActivity); $postId = $this->addPost($reply, $createPreviewCard, $registerActivity);
model('StatusModel') model('PostModel', false)
->where('id', $this->uuid->fromString($reply->in_reply_to_id) ->getBytes()) ->where('id', $this->uuid->fromString($reply->in_reply_to_id) ->getBytes())
->increment('replies_count'); ->increment('replies_count');
Events::trigger('on_status_reply', $reply); Events::trigger('on_post_reply', $reply);
$this->clearCache($reply); $this->clearCache($reply);
$this->db->transComplete(); $this->db->transComplete();
return $statusId; return $postId;
} }
public function reblog(Actor $actor, Status $status, bool $registerActivity = true): string | false public function reblog(Actor $actor, Post $post, bool $registerActivity = true): string | false
{ {
$this->db->transStart(); $this->db->transStart();
$reblog = new Status([ $reblog = new Post([
'actor_id' => $actor->id, 'actor_id' => $actor->id,
'reblog_of_id' => $status->id, 'reblog_of_id' => $post->id,
'published_at' => Time::now(), 'published_at' => Time::now(),
]); ]);
// add reblog // add reblog
$reblogId = $this->insert($reblog); $reblogId = $this->insert($reblog);
model('ActorModel') model('ActorModel', false)
->where('id', $actor->id) ->where('id', $actor->id)
->increment('statuses_count'); ->increment('posts_count');
model('StatusModel') model('PostModel', false)
->where('id', $this->uuid->fromString($status->id)->getBytes()) ->where('id', $this->uuid->fromString($post->id)->getBytes())
->increment('reblogs_count'); ->increment('reblogs_count');
if ($registerActivity) { if ($registerActivity) {
$announceActivity = new AnnounceActivity($reblog); $announceActivity = new AnnounceActivity($reblog);
$activityId = model('ActivityModel') $activityId = model('ActivityModel', false)
->newActivity( ->newActivity(
'Announce', 'Announce',
$actor->id, $actor->id,
null, null,
$status->id, $post->id,
$announceActivity->toJSON(), $announceActivity->toJSON(),
$reblog->published_at, $reblog->published_at,
'queued', 'queued',
); );
$announceActivity->set('id', url_to('activity', $status->actor->username, $activityId)); $announceActivity->set('id', url_to('activity', $post->actor->username, $activityId));
model('ActivityModel') model('ActivityModel', false)
->update($activityId, [ ->update($activityId, [
'payload' => $announceActivity->toJSON(), 'payload' => $announceActivity->toJSON(),
]); ]);
} }
Events::trigger('on_status_reblog', $actor, $status); Events::trigger('on_post_reblog', $actor, $post);
$this->clearCache($status); $this->clearCache($post);
$this->db->transComplete(); $this->db->transComplete();
return $reblogId; return $reblogId;
} }
public function undoReblog(Status $reblogStatus, bool $registerActivity = true): BaseResult | bool public function undoReblog(Post $reblogPost, bool $registerActivity = true): BaseResult | bool
{ {
$this->db->transStart(); $this->db->transStart();
model('ActorModel') model('ActorModel', false)
->where('id', $reblogStatus->actor_id) ->where('id', $reblogPost->actor_id)
->decrement('statuses_count'); ->decrement('posts_count');
model('StatusModel') model('PostModel', false)
->where('id', $this->uuid->fromString($reblogStatus->reblog_of_id) ->getBytes()) ->where('id', $this->uuid->fromString($reblogPost->reblog_of_id) ->getBytes())
->decrement('reblogs_count'); ->decrement('reblogs_count');
if ($registerActivity) { if ($registerActivity) {
$undoActivity = new UndoActivity(); $undoActivity = new UndoActivity();
// get like activity // get like activity
$activity = model('ActivityModel') $activity = model('ActivityModel', false)
->where([ ->where([
'type' => 'Announce', 'type' => 'Announce',
'actor_id' => $reblogStatus->actor_id, 'actor_id' => $reblogPost->actor_id,
'status_id' => $this->uuid 'post_id' => $this->uuid
->fromString($reblogStatus->reblog_of_id) ->fromString($reblogPost->reblog_of_id)
->getBytes(), ->getBytes(),
]) ])
->first(); ->first();
$announceActivity = new AnnounceActivity($reblogStatus); $announceActivity = new AnnounceActivity($reblogPost);
$announceActivity->set('id', url_to('activity', $reblogStatus->actor->username, $activity->id),); $announceActivity->set('id', url_to('activity', $reblogPost->actor->username, $activity->id),);
$undoActivity $undoActivity
->set('actor', $reblogStatus->actor->uri) ->set('actor', $reblogPost->actor->uri)
->set('object', $announceActivity); ->set('object', $announceActivity);
$activityId = model('ActivityModel') $activityId = model('ActivityModel', false)
->newActivity( ->newActivity(
'Undo', 'Undo',
$reblogStatus->actor_id, $reblogPost->actor_id,
null, null,
$reblogStatus->reblog_of_id, $reblogPost->reblog_of_id,
$undoActivity->toJSON(), $undoActivity->toJSON(),
Time::now(), Time::now(),
'queued', 'queued',
); );
$undoActivity->set('id', url_to('activity', $reblogStatus->actor->username, $activityId)); $undoActivity->set('id', url_to('activity', $reblogPost->actor->username, $activityId));
model('ActivityModel') model('ActivityModel', false)
->update($activityId, [ ->update($activityId, [
'payload' => $undoActivity->toJSON(), 'payload' => $undoActivity->toJSON(),
]); ]);
} }
$result = model('StatusModel', false) $result = model('PostModel', false)
->delete($reblogStatus->id); ->delete($reblogPost->id);
Events::trigger('on_status_undo_reblog', $reblogStatus); Events::trigger('on_post_undo_reblog', $reblogPost);
$this->clearCache($reblogStatus); $this->clearCache($reblogPost);
$this->db->transComplete(); $this->db->transComplete();
return $result; return $result;
} }
public function toggleReblog(Actor $actor, Status $status): void public function toggleReblog(Actor $actor, Post $post): void
{ {
if ( if (
! ($reblogStatus = $this->where([ ! ($reblogPost = $this->where([
'actor_id' => $actor->id, 'actor_id' => $actor->id,
'reblog_of_id' => $this->uuid 'reblog_of_id' => $this->uuid
->fromString($status->id) ->fromString($post->id)
->getBytes(), ->getBytes(),
])->first()) ])->first())
) { ) {
$this->reblog($actor, $status); $this->reblog($actor, $post);
} else { } else {
$this->undoReblog($reblogStatus); $this->undoReblog($reblogPost);
} }
} }
public function clearCache(Status $status): void public function clearCache(Post $post): void
{ {
$cachePrefix = config('ActivityPub') $cachePrefix = config('ActivityPub')
->cachePrefix; ->cachePrefix;
$hashedStatusUri = md5($status->uri); $hashedPostUri = md5($post->uri);
model('ActorModel') model('ActorModel', false)
->clearCache($status->actor); ->clearCache($post->actor);
cache() cache()
->deleteMatching($cachePrefix . "status#{$status->id}*"); ->deleteMatching($cachePrefix . "post#{$post->id}*");
cache() cache()
->deleteMatching($cachePrefix . "status-{$hashedStatusUri}*"); ->deleteMatching($cachePrefix . "post-{$hashedPostUri}*");
if ($status->in_reply_to_id !== null) { if ($post->in_reply_to_id !== null) {
$this->clearCache($status->reply_to_status); $this->clearCache($post->reply_to_post);
} }
if ($status->reblog_of_id !== null) { if ($post->reblog_of_id !== null) {
$this->clearCache($status->reblog_of_status); $this->clearCache($post->reblog_of_post);
} }
} }
@ -615,16 +615,16 @@ class StatusModel extends UuidModel
* @param array<string, array<string|int, mixed>> $data * @param array<string, array<string|int, mixed>> $data
* @return array<string, array<string|int, mixed>> * @return array<string, array<string|int, mixed>>
*/ */
protected function setStatusId(array $data): array protected function setPostId(array $data): array
{ {
$uuid4 = $this->uuid->{$this->uuidVersion}(); $uuid4 = $this->uuid->{$this->uuidVersion}();
$data['data']['id'] = $uuid4->toString(); $data['data']['id'] = $uuid4->toString();
if (! isset($data['data']['uri'])) { if (! isset($data['data']['uri'])) {
$actor = model('ActorModel') $actor = model('ActorModel', false)
->getActorById((int) $data['data']['actor_id']); ->getActorById((int) $data['data']['actor_id']);
$data['data']['uri'] = url_to('status', $actor->username, $uuid4->toString()); $data['data']['uri'] = url_to('post', $actor->username, $uuid4->toString());
} }
return $data; return $data;

View File

@ -70,18 +70,18 @@ class PreviewCardModel extends Model
return $found; return $found;
} }
public function getStatusPreviewCard(string $statusId): ?PreviewCard public function getPostPreviewCard(string $postId): ?PreviewCard
{ {
$cacheName = $cacheName =
config('ActivityPub') config('ActivityPub')
->cachePrefix . "status#{$statusId}_preview_card"; ->cachePrefix . "post#{$postId}_preview_card";
if (! ($found = cache($cacheName))) { if (! ($found = cache($cacheName))) {
$found = $this->join( $found = $this->join(
'activitypub_statuses_preview_cards', 'activitypub_posts_preview_cards',
'activitypub_statuses_preview_cards.preview_card_id = id', 'activitypub_posts_preview_cards.preview_card_id = id',
'inner', 'inner',
) )
->where('status_id', service('uuid') ->fromString($statusId) ->getBytes()) ->where('post_id', service('uuid') ->fromString($postId) ->getBytes())
->first(); ->first();
cache() cache()

View File

@ -15,7 +15,7 @@ declare(strict_types=1);
namespace ActivityPub\Objects; namespace ActivityPub\Objects;
use ActivityPub\Core\ObjectType; use ActivityPub\Core\ObjectType;
use ActivityPub\Entities\Status; use ActivityPub\Entities\Post;
class NoteObject extends ObjectType class NoteObject extends ObjectType
{ {
@ -27,20 +27,20 @@ class NoteObject extends ObjectType
protected string $replies; protected string $replies;
public function __construct(Status $status) public function __construct(Post $post)
{ {
$this->id = $status->uri; $this->id = $post->uri;
$this->content = $status->message_html; $this->content = $post->message_html;
$this->published = $status->published_at->format(DATE_W3C); $this->published = $post->published_at->format(DATE_W3C);
$this->attributedTo = $status->actor->uri; $this->attributedTo = $post->actor->uri;
if ($status->in_reply_to_id !== null) { if ($post->in_reply_to_id !== null) {
$this->inReplyTo = $status->reply_to_status->uri; $this->inReplyTo = $post->reply_to_post->uri;
} }
$this->replies = url_to('status-replies', $status->actor->username, $status->id); $this->replies = url_to('post-replies', $post->actor->username, $post->id);
$this->cc = [$status->actor->followers_url]; $this->cc = [$post->actor->followers_url];
} }
} }

View File

@ -0,0 +1,42 @@
<?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\Libraries;
use ActivityPub\Core\ObjectType;
use App\Entities\Comment;
class CommentObject extends ObjectType
{
protected string $type = 'Note';
protected string $attributedTo;
protected string $inReplyTo;
protected string $replies;
public function __construct(Comment $comment)
{
$this->id = $comment->uri;
$this->content = $comment->message_html;
$this->published = $comment->created_at->format(DATE_W3C);
$this->attributedTo = $comment->actor->uri;
if ($comment->in_reply_to_id !== null) {
$this->inReplyTo = $comment->reply_to_comment->uri;
}
$this->replies = url_to('comment-replies', $comment->actor->username, $comment->episode->slug, $comment->id);
$this->cc = [$comment->actor->followers_url];
}
}

View File

@ -11,25 +11,25 @@ declare(strict_types=1);
namespace App\Libraries; namespace App\Libraries;
use ActivityPub\Objects\NoteObject as ActivityPubNoteObject; use ActivityPub\Objects\NoteObject as ActivityPubNoteObject;
use App\Entities\Status; use App\Entities\Post;
class NoteObject extends ActivityPubNoteObject class NoteObject extends ActivityPubNoteObject
{ {
/** /**
* @param Status $status * @param Post $post
*/ */
public function __construct(\ActivityPub\Entities\Status $status) public function __construct(\ActivityPub\Entities\Post $post)
{ {
parent::__construct($status); parent::__construct($post);
if ($status->episode_id) { if ($post->episode_id) {
$this->content = $this->content =
'<a href="' . '<a href="' .
$status->episode->link . $post->episode->link .
'" target="_blank" rel="noopener noreferrer">' . '" target="_blank" rel="noopener noreferrer">' .
$status->episode->title . $post->episode->title .
'</a><br/>' . '</a><br/>' .
$status->message_html; $post->message_html;
} }
} }
} }

184
app/Models/CommentModel.php Normal file
View File

@ -0,0 +1,184 @@
<?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\CreateActivity;
use App\Entities\Comment;
use App\Libraries\CommentObject;
use CodeIgniter\Database\BaseBuilder;
use Michalsn\Uuid\UuidModel;
class CommentModel extends UuidModel
{
/**
* @var string
*/
protected $returnType = Comment::class;
/**
* @var string
*/
protected $table = 'comments';
/**
* @var string[]
*/
protected $uuidFields = ['id', 'in_reply_to_id'];
/**
* @var string[]
*/
protected $allowedFields = [
'id',
'uri',
'episode_id',
'actor_id',
'in_reply_to_id',
'message',
'message_html',
'likes_count',
'dislikes_count',
'replies_count',
'created_at',
'created_by',
];
/**
* @var string[]
*/
protected $beforeInsert = ['setCommentId'];
public function getCommentById(string $commentId): ?Comment
{
$cacheName = "comment#{$commentId}";
if (! ($found = cache($cacheName))) {
$found = $this->find($commentId);
cache()
->save($cacheName, $found, DECADE);
}
return $found;
}
public function addComment(Comment $comment, bool $registerActivity = false): string | false
{
$this->db->transStart();
// increment Episode's comments_count
if (! ($newCommentId = $this->insert($comment, true))) {
$this->db->transRollback();
// Couldn't insert comment
return false;
}
(new EpisodeModel())
->where('id', $comment->episode_id)
->increment('comments_count');
if ($registerActivity) {
// set post id and uri to construct NoteObject
$comment->id = $newCommentId;
$comment->uri = url_to('comment', $comment->actor->username, $comment->episode->slug, $comment->id);
$createActivity = new CreateActivity();
$createActivity
->set('actor', $comment->actor->uri)
->set('object', new CommentObject($comment));
$activityId = model('ActivityModel', false)
->newActivity(
'Create',
$comment->actor_id,
null,
null,
$createActivity->toJSON(),
$comment->created_at,
'queued',
);
$createActivity->set('id', url_to('activity', $comment->actor->username, $activityId));
model('ActivityModel', false)
->update($activityId, [
'payload' => $createActivity->toJSON(),
]);
}
$this->db->transComplete();
return $newCommentId;
}
/**
* Retrieves all published posts for a given episode ordered by publication date
*
* @return Comment[]
*/
public function getEpisodeComments(int $episodeId): array
{
// TODO: merge with replies from posts linked to episode linked
$episodeComments = $this->select('*, 0 as is_from_post')
->where('episode_id', $episodeId)
->getCompiledSelect();
$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'
)
->whereIn('in_reply_to_id', function (BaseBuilder $builder) use (&$episodeId): BaseBuilder {
return $builder->select('id')
->from('activitypub_posts')
->where('episode_id', $episodeId);
})
->where('`created_at` <= NOW()', null, false)
->getCompiledSelect();
$allEpisodeComments = $this->db->query(
$episodeComments . ' UNION ' . $episodePostsReplies . ' ORDER BY created_at ASC'
);
return $allEpisodeComments->getCustomResultObject($this->returnType);
}
/**
* Retrieves all replies for a given comment
*
* @return Comment[]
*/
public function getCommentReplies(int $episodeId, string $commentId): array
{
// TODO: get all replies for a given comment
return $this->findAll();
}
/**
* @param array<string, array<string|int, mixed>> $data
* @return array<string, array<string|int, mixed>>
*/
protected function setCommentId(array $data): array
{
$uuid4 = $this->uuid->{$this->uuidVersion}();
$data['data']['id'] = $uuid4->toString();
if (! isset($data['data']['uri'])) {
$actor = model('ActorModel', false)
->getActorById((int) $data['data']['actor_id']);
$episode = model('EpisodeModel', false)
->find((int) $data['data']['episode_id']);
$data['data']['uri'] = url_to('comment', $actor->username, $episode->slug, $uuid4->toString());
}
return $data;
}
}

View File

@ -90,9 +90,8 @@ class EpisodeModel extends Model
'location_geo', 'location_geo',
'location_osm', 'location_osm',
'custom_rss', 'custom_rss',
'favourites_total', 'posts_count',
'reblogs_total', 'comments_count',
'statuses_total',
'published_at', 'published_at',
'created_by', 'created_by',
'updated_by', 'updated_by',

56
app/Models/PostModel.php Normal file
View File

@ -0,0 +1,56 @@
<?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\Models\PostModel as ActivityPubPostModel;
use App\Entities\Post;
class PostModel extends ActivityPubPostModel
{
/**
* @var string
*/
protected $returnType = Post::class;
/**
* @var string[]
*/
protected $allowedFields = [
'id',
'uri',
'actor_id',
'in_reply_to_id',
'reblog_of_id',
'episode_id',
'message',
'message_html',
'favourites_count',
'reblogs_count',
'replies_count',
'created_by',
'published_at',
];
/**
* Retrieves all published posts for a given episode ordered by publication date
*
* @return Post[]
*/
public function getEpisodePosts(int $episodeId): array
{
return $this->where([
'episode_id' => $episodeId,
])
->where('`published_at` <= NOW()', null, false)
->orderBy('published_at', 'DESC')
->findAll();
}
}

View File

@ -1,74 +0,0 @@
<?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\Models\StatusModel as ActivityPubStatusModel;
use App\Entities\Status;
use CodeIgniter\Database\BaseBuilder;
class StatusModel extends ActivityPubStatusModel
{
/**
* @var string
*/
protected $returnType = Status::class;
/**
* @var string[]
*/
protected $allowedFields = [
'id',
'uri',
'actor_id',
'in_reply_to_id',
'reblog_of_id',
'episode_id',
'message',
'message_html',
'favourites_count',
'reblogs_count',
'replies_count',
'created_by',
'published_at',
];
/**
* Retrieves all published statuses for a given episode ordered by publication date
*
* @return Status[]
*/
public function getEpisodeStatuses(int $episodeId): array
{
return $this->where([
'episode_id' => $episodeId,
])
->where('`published_at` <= NOW()', null, false)
->orderBy('published_at', 'DESC')
->findAll();
}
/**
* Retrieves all published statuses for a given episode ordered by publication date
*
* @return Status[]
*/
public function getEpisodeComments(int $episodeId): array
{
return $this->whereIn('in_reply_to_id', function (BaseBuilder $builder) use (&$episodeId): BaseBuilder {
return $builder->select('id')
->from('activitypub_statuses')
->where('episode_id', $episodeId);
})
->where('`published_at` <= NOW()', null, false)
->orderBy('published_at', 'ASC')
->findAll();
}
}

View File

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

After

Width:  |  Height:  |  Size: 393 B

View File

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

After

Width:  |  Height:  |  Size: 391 B

View File

@ -7,7 +7,7 @@
@import "./radioBtn.css"; @import "./radioBtn.css";
@import "./switch.css"; @import "./switch.css";
@import "./charts.css"; @import "./charts.css";
@import "./status.css"; @import "./post.css";
@import "./tabs.css"; @import "./tabs.css";
@import "./radioToggler.css"; @import "./radioToggler.css";
@import "./formInputTabs.css"; @import "./formInputTabs.css";

View File

@ -1,11 +1,11 @@
@layer components { @layer components {
.status-content { .post-content {
& a { & a {
@apply text-sm font-semibold text-pine-600 hover:underline; @apply text-sm font-semibold text-pine-600 hover:underline;
} }
} }
.status-replies > * { .post-replies > * {
@apply relative; @apply relative;
& img { & img {

View File

@ -1,6 +1,6 @@
@layer components { @layer components {
.tabset { .tabset {
@apply grid grid-cols-2; @apply grid grid-cols-3;
} }
.tabset > input[type="radio"] { .tabset > input[type="radio"] {
@ -11,9 +11,10 @@
@apply hidden; @apply hidden;
} }
/* Logic for 2 tabs at most */ /* Logic for 3 tabs at most */
.tabset > input:first-child:checked ~ .tab-panels > .tab-panel:first-child, .tabset > input:first-child:checked ~ .tab-panels > .tab-panel:first-child,
.tabset > input:nth-child(3):checked ~ .tab-panels > .tab-panel:nth-child(2) { .tabset > input:nth-child(3):checked ~ .tab-panels > .tab-panel:nth-child(2),
.tabset > input:nth-child(5):checked ~ .tab-panels > .tab-panel:nth-child(3) {
@apply block; @apply block;
} }
@ -23,7 +24,7 @@
} }
.tabset > input:checked + label::after { .tabset > input:checked + label::after {
@apply absolute inset-x-0 bottom-0 w-1/2 h-1 mx-auto bg-pine-700; @apply absolute inset-x-0 bottom-0 w-1/3 h-1 mx-auto bg-pine-700;
content: ""; content: "";
} }
@ -32,6 +33,6 @@
} }
.tabset .tab-panels { .tabset .tab-panels {
@apply col-span-2 p-6; @apply col-span-3 p-6;
} }
} }

View File

@ -71,7 +71,7 @@
[ [
'header' => lang('Episode.list.comments'), 'header' => lang('Episode.list.comments'),
'cell' => function ($episode): int { 'cell' => function ($episode): int {
return count($episode->comments); return $episode->comments_count;
}, },
], ],
[ [

View File

@ -27,9 +27,9 @@
<label for="message" class="text-lg font-semibold"><?= lang( <label for="message" class="text-lg font-semibold"><?= lang(
'Episode.publish_form.status', 'Episode.publish_form.post',
) ?></label> ) ?></label>
<small class="max-w-md mb-2 text-gray-600"><?= lang('Episode.publish_form.status_hint') ?></small> <small class="max-w-md mb-2 text-gray-600"><?= lang('Episode.publish_form.post_hint') ?></small>
<div class="mb-8 overflow-hidden bg-white shadow-md rounded-xl"> <div class="mb-8 overflow-hidden bg-white shadow-md rounded-xl">
<div class="flex px-4 py-3"> <div class="flex px-4 py-3">
<img src="<?= $podcast->actor->avatar_image_url ?>" alt="<?= $podcast <img src="<?= $podcast->actor->avatar_image_url ?>" alt="<?= $podcast

View File

@ -24,13 +24,13 @@
]) ?> ]) ?>
<?= csrf_field() ?> <?= csrf_field() ?>
<?= form_hidden('client_timezone', 'UTC') ?> <?= form_hidden('client_timezone', 'UTC') ?>
<?= form_hidden('status_id', $status->id) ?> <?= form_hidden('post_id', $post->id) ?>
<label for="message" class="text-lg font-semibold"><?= lang( <label for="message" class="text-lg font-semibold"><?= lang(
'Episode.publish_form.status', 'Episode.publish_form.post',
) ?></label> ) ?></label>
<small class="max-w-md mb-2 text-gray-600"><?= lang('Episode.publish_form.status_hint') ?></small> <small class="max-w-md mb-2 text-gray-600"><?= lang('Episode.publish_form.post_hint') ?></small>
<div class="mb-8 overflow-hidden bg-white shadow-md rounded-xl"> <div class="mb-8 overflow-hidden bg-white shadow-md rounded-xl">
<div class="flex px-4 py-3"> <div class="flex px-4 py-3">
<img src="<?= $podcast->actor->avatar_image_url ?>" alt="<?= $podcast->actor <img src="<?= $podcast->actor->avatar_image_url ?>" alt="<?= $podcast->actor
@ -42,7 +42,7 @@
<span class="text-sm text-gray-500 truncate">@<?= $podcast <span class="text-sm text-gray-500 truncate">@<?= $podcast
->actor->username ?></span> ->actor->username ?></span>
</p> </p>
<?= relative_time($status->published_at, 'text-xs text-gray-500') ?> <?= relative_time($post->published_at, 'text-xs text-gray-500') ?>
</div> </div>
</div> </div>
<div class="px-4 mb-2"> <div class="px-4 mb-2">
@ -54,7 +54,7 @@
'placeholder' => 'Write your message...', 'placeholder' => 'Write your message...',
'autofocus' => '' 'autofocus' => ''
], ],
old('message', $status->message, false), old('message', $post->message, false),
['rows' => 2], ['rows' => 2],
) ?> ) ?>
</div> </div>

View File

@ -0,0 +1,43 @@
<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">
<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>
<?= relative_time($comment->created_at, 'text-xs text-gray-500 ml-auto') ?>
</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>
</div>
</article>

View File

@ -28,12 +28,12 @@
<?= anchor( <?= anchor(
route_to('episode', $podcast->handle, $episode->slug), route_to('episode', $podcast->handle, $episode->slug),
icon('chat', 'text-xl mr-1 text-gray-400') . icon('chat', 'text-xl mr-1 text-gray-400') .
$episode->statuses_total, $episode->comments_count,
[ [
'class' => 'class' =>
'inline-flex items-center hover:underline', 'inline-flex items-center hover:underline',
'title' => lang('Episode.total_statuses', [ 'title' => lang('Episode.number_of_comments', [
'numberOfTotalStatuses' => $episode->statuses_total, 'numberOfComments' => $episode->comments_count,
]), ]),
], ],
) ?> ) ?>

View File

@ -50,8 +50,8 @@
<a href="<?= route_to( <a href="<?= route_to(
'podcast-activity', 'podcast-activity',
$podcast->handle, $podcast->handle,
) ?>" class="hover:underline"><?= lang('Podcast.statuses', [ ) ?>" class="hover:underline"><?= lang('Podcast.posts', [
'numberOfStatuses' => $podcast->actor->statuses_count, 'numberOfPosts' => $podcast->actor->posts_count,
]) ?></a> ]) ?></a>
</div> </div>
</div> </div>

View File

@ -0,0 +1,36 @@
<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" />
<div class="flex flex-col min-w-0">
<a href="<?= $post->actor
->uri ?>" class="flex items-baseline hover:underline" <?= $post
->actor->is_local
? ''
: 'target="_blank" rel="noopener noreferrer"' ?>>
<span class="mr-2 font-semibold truncate"><?= $post->actor
->display_name ?></span>
<span class="text-sm text-gray-500 truncate">@<?= $post->actor
->username .
($post->actor->is_local
? ''
: '@' . $post->actor->domain) ?></span>
</a>
<a href="<?= route_to('post', $podcast->handle, $post->id) ?>"
class="text-xs text-gray-500">
<?= relative_time($post->published_at) ?>
</a>
</div>
</header>
<div class="px-6 mb-4 post-content"><?= $post->message_html ?></div>
<?php if ($post->episode_id): ?>
<?= view('podcast/_partials/episode_preview_card', [
'episode' => $post->episode,
]) ?>
<?php elseif ($post->has_preview_card): ?>
<?= view('podcast/_partials/preview_card', [
'preview_card' => $post->preview_card,
]) ?>
<?php endif; ?>
<?= $this->include('podcast/_partials/post_actions') ?>
</article>

View File

@ -0,0 +1,36 @@
<footer class="flex justify-around px-6 py-3">
<?= anchor(
route_to('post', $podcast->handle, $post->id),
icon('chat', 'text-2xl mr-1 text-gray-400') . $post->replies_count,
[
'class' => 'inline-flex items-center hover:underline',
'title' => lang('Post.replies', [
'numberOfReplies' => $post->replies_count,
]),
],
) ?>
<?= anchor_popup(
route_to('post-remote-action', $podcast->handle, $post->id, 'reblog'),
icon('repeat', 'text-2xl mr-1 text-gray-400') . $post->reblogs_count,
[
'class' => 'inline-flex items-center hover:underline',
'width' => 420,
'height' => 620,
'title' => lang('Post.reblogs', [
'numberOfReblogs' => $post->reblogs_count,
]),
],
) ?>
<?= anchor_popup(
route_to('post-remote-action', $podcast->handle, $post->id, 'favourite'),
icon('heart', 'text-2xl mr-1 text-gray-400') . $post->favourites_count,
[
'class' => 'inline-flex items-center hover:underline',
'width' => 420,
'height' => 620,
'title' => lang('Post.favourites', [
'numberOfFavourites' => $post->favourites_count,
]),
],
) ?>
</footer>

View File

@ -1,87 +1,87 @@
<footer class="px-6 py-3"> <footer class="px-6 py-3">
<form action="<?= route_to( <form action="<?= route_to(
'status-attempt-action', 'post-attempt-action',
interact_as_actor()->username, interact_as_actor()->username,
$status->id, $post->id,
) ?>" method="POST" class="flex justify-around"> ) ?>" method="POST" class="flex justify-around">
<?= csrf_field() ?> <?= csrf_field() ?>
<?= anchor( <?= anchor(
route_to('status', $podcast->handle, $status->id), route_to('post', $podcast->handle, $post->id),
icon('chat', 'text-2xl mr-1 text-gray-400') . $status->replies_count, icon('chat', 'text-2xl mr-1 text-gray-400') . $post->replies_count,
[ [
'class' => 'inline-flex items-center hover:underline', 'class' => 'inline-flex items-center hover:underline',
'title' => lang('Status.replies', [ 'title' => lang('Post.replies', [
'numberOfReplies' => $status->replies_count, 'numberOfReplies' => $post->replies_count,
]), ]),
], ],
) ?> ) ?>
<button type="submit" name="action" value="reblog" class="inline-flex items-center hover:underline" title="<?= lang( <button type="submit" name="action" value="reblog" class="inline-flex items-center hover:underline" title="<?= lang(
'Status.reblogs', 'Post.reblogs',
[ [
'numberOfReblogs' => $status->reblogs_count, 'numberOfReblogs' => $post->reblogs_count,
], ],
) ?>"><?= icon('repeat', 'text-2xl mr-1 text-gray-400') . ) ?>"><?= icon('repeat', 'text-2xl mr-1 text-gray-400') .
$status->reblogs_count ?></button> $post->reblogs_count ?></button>
<button type="submit" name="action" value="favourite" class="inline-flex items-center hover:underline" title="<?= lang( <button type="submit" name="action" value="favourite" class="inline-flex items-center hover:underline" title="<?= lang(
'Status.favourites', 'Post.favourites',
[ [
'numberOfFavourites' => $status->favourites_count, 'numberOfFavourites' => $post->favourites_count,
], ],
) ?>"><?= icon('heart', 'text-2xl mr-1 text-gray-400') . ) ?>"><?= icon('heart', 'text-2xl mr-1 text-gray-400') .
$status->favourites_count ?></button> $post->favourites_count ?></button>
<button id="<?= $status->id . <button id="<?= $post->id .
'-more-dropdown' ?>" type="button" class="px-2 py-1 text-2xl text-gray-500 outline-none focus:ring" data-dropdown="button" data-dropdown-target="<?= $status->id . '-more-dropdown' ?>" type="button" class="px-2 py-1 text-2xl text-gray-500 outline-none focus:ring" data-dropdown="button" data-dropdown-target="<?= $post->id .
'-more-dropdown-menu' ?>" aria-label="<?= lang( '-more-dropdown-menu' ?>" aria-label="<?= lang(
'Common.more', 'Common.more',
) ?>" aria-haspopup="true" aria-expanded="false"><?= icon('more') ?> ) ?>" aria-haspopup="true" aria-expanded="false"><?= icon('more') ?>
</button> </button>
</form> </form>
<nav id="<?= $status->id . <nav id="<?= $post->id .
'-more-dropdown-menu' ?>" class="flex flex-col py-2 text-sm bg-white border rounded-lg shadow" aria-labelledby="<?= $status->id . '-more-dropdown-menu' ?>" class="flex flex-col py-2 text-sm bg-white border rounded-lg shadow" aria-labelledby="<?= $post->id .
'-more-dropdown' ?>" data-dropdown="menu" data-dropdown-placement="bottom"> '-more-dropdown' ?>" data-dropdown="menu" data-dropdown-placement="bottom">
<?= anchor( <?= anchor(
route_to('status', $podcast->handle, $status->id), route_to('post', $podcast->handle, $post->id),
lang('Status.expand'), lang('Post.expand'),
[ [
'class' => 'px-4 py-1 hover:bg-gray-100', 'class' => 'px-4 py-1 hover:bg-gray-100',
], ],
) ?> ) ?>
<form action="<?= route_to( <form action="<?= route_to(
'status-attempt-block-actor', 'post-attempt-block-actor',
interact_as_actor()->username, interact_as_actor()->username,
$status->id, $post->id,
) ?>" method="POST"> ) ?>" method="POST">
<?= csrf_field() ?> <?= csrf_field() ?>
<button class="w-full px-4 py-1 text-left hover:bg-gray-100"><?= lang( <button class="w-full px-4 py-1 text-left hover:bg-gray-100"><?= lang(
'Status.block_actor', 'Post.block_actor',
[ [
'actorUsername' => $status->actor->username, 'actorUsername' => $post->actor->username,
], ],
) ?></button> ) ?></button>
</form> </form>
<form action="<?= route_to( <form action="<?= route_to(
'status-attempt-block-domain', 'post-attempt-block-domain',
interact_as_actor()->username, interact_as_actor()->username,
$status->id, $post->id,
) ?>" method="POST"> ) ?>" method="POST">
<?= csrf_field() ?> <?= csrf_field() ?>
<button class="w-full px-4 py-1 text-left hover:bg-gray-100"><?= lang( <button class="w-full px-4 py-1 text-left hover:bg-gray-100"><?= lang(
'Status.block_domain', 'Post.block_domain',
[ [
'actorDomain' => $status->actor->domain, 'actorDomain' => $post->actor->domain,
], ],
) ?></button> ) ?></button>
</form> </form>
<?php if ($status->actor->is_local): ?> <?php if ($post->actor->is_local): ?>
<hr class="my-2" /> <hr class="my-2" />
<form action="<?= route_to( <form action="<?= route_to(
'status-attempt-delete', 'post-attempt-delete',
$status->actor->username, $post->actor->username,
$status->id, $post->id,
) ?>" method="POST"> ) ?>" method="POST">
<?= csrf_field() ?> <?= csrf_field() ?>
<button class="w-full px-4 py-1 font-semibold text-left text-red-600 hover:bg-gray-100"><?= lang( <button class="w-full px-4 py-1 font-semibold text-left text-red-600 hover:bg-gray-100"><?= lang(
'Status.delete', 'Post.delete',
) ?></button> ) ?></button>
</form> </form>
<?php endif; ?> <?php endif; ?>

View File

@ -0,0 +1,36 @@
<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" />
<div class="flex flex-col min-w-0">
<a href="<?= $post->actor
->uri ?>" class="flex items-baseline hover:underline" <?= $post
->actor->is_local
? ''
: 'target="_blank" rel="noopener noreferrer"' ?>>
<span class="mr-2 font-semibold truncate"><?= $post->actor
->display_name ?></span>
<span class="text-sm text-gray-500 truncate">@<?= $post->actor
->username .
($post->actor->is_local
? ''
: '@' . $post->actor->domain) ?></span>
</a>
<a href="<?= route_to('post', $podcast->handle, $post->id) ?>"
class="text-xs text-gray-500">
<?= relative_time($post->published_at) ?>
</a>
</div>
</header>
<div class="px-6 mb-4 post-content"><?= $post->message_html ?></div>
<?php if ($post->episode_id): ?>
<?= view('podcast/_partials/episode_preview_card', [
'episode' => $post->episode,
]) ?>
<?php elseif ($post->has_preview_card): ?>
<?= view('podcast/_partials/preview_card', [
'preview_card' => $post->preview_card,
]) ?>
<?php endif; ?>
<?= $this->include('podcast/_partials/post_actions_authenticated') ?>
</article>

View File

@ -1,10 +1,10 @@
<?= $this->include('podcast/_partials/status') ?> <?= $this->include('podcast/_partials/post') ?>
<div class="-mt-2 overflow-hidden border-b border-l border-r status-replies rounded-b-xl"> <div class="-mt-2 overflow-hidden border-b border-l border-r post-replies rounded-b-xl">
<div class="px-6 pt-8 pb-4 bg-gray-50"> <div class="px-6 pt-8 pb-4 bg-gray-50">
<?= anchor_popup( <?= anchor_popup(
route_to('status-remote-action', $podcast->handle, $status->id, 'reply'), route_to('post-remote-action', $podcast->handle, $post->id, 'reply'),
lang('Status.reply_to', ['actorUsername' => $status->actor->username]), lang('Post.reply_to', ['actorUsername' => $post->actor->username]),
[ [
'class' => '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', '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',
@ -15,8 +15,8 @@
</div> </div>
<?php if ($status->has_replies): ?> <?php if ($post->has_replies): ?>
<?php foreach ($status->replies as $reply): ?> <?php foreach ($post->replies as $reply): ?>
<?= view('podcast/_partials/reply', ['reply' => $reply]) ?> <?= view('podcast/_partials/reply', ['reply' => $reply]) ?>
<?php endforeach; ?> <?php endforeach; ?>
<?php endif; ?> <?php endif; ?>

View File

@ -1,7 +1,7 @@
<?= $this->include('podcast/_partials/status_authenticated') ?> <?= $this->include('podcast/_partials/post_authenticated') ?>
<div class="-mt-2 overflow-hidden border-b border-l border-r status-replies rounded-b-xl"> <div class="-mt-2 overflow-hidden border-b border-l border-r post-replies rounded-b-xl">
<?= form_open( <?= form_open(
route_to('status-attempt-action', interact_as_actor()->username, $status->id), route_to('post-attempt-action', interact_as_actor()->username, $post->id),
[ [
'class' => 'bg-gray-50 flex px-6 pt-8 pb-4', 'class' => 'bg-gray-50 flex px-6 pt-8 pb-4',
], ],
@ -16,8 +16,8 @@
'name' => 'message', 'name' => 'message',
'class' => 'form-textarea mb-4 w-full', 'class' => 'form-textarea mb-4 w-full',
'required' => 'required', 'required' => 'required',
'placeholder' => lang('Status.form.reply_to_placeholder', [ 'placeholder' => lang('Post.form.reply_to_placeholder', [
'actorUsername' => $status->actor->username, 'actorUsername' => $post->actor->username,
]), ]),
], ],
old('message', '', false), old('message', '', false),
@ -26,7 +26,7 @@
], ],
) ?> ) ?>
<?= button( <?= button(
lang('Status.form.submit_reply'), lang('Post.form.submit_reply'),
'', '',
['variant' => 'primary', 'size' => 'small'], ['variant' => 'primary', 'size' => 'small'],
[ [
@ -39,8 +39,8 @@
</div> </div>
<?= form_close() ?> <?= form_close() ?>
<?php if ($status->has_replies): ?> <?php if ($post->has_replies): ?>
<?php foreach ($status->replies as $reply): ?> <?php foreach ($post->replies as $reply): ?>
<?= view('podcast/_partials/reply_authenticated', [ <?= view('podcast/_partials/reply_authenticated', [
'reply' => $reply, 'reply' => $reply,
]) ?> ]) ?>

View File

@ -1,43 +1,43 @@
<article class="relative z-10 w-full bg-white shadow-md rounded-2xl"> <article class="relative z-10 w-full bg-white shadow rounded-2xl">
<p class="inline-flex px-6 pt-4 text-xs text-gray-700"><?= icon( <p class="inline-flex px-6 pt-4 text-xs text-gray-700"><?= icon(
'repeat', 'repeat',
'text-lg mr-2 text-gray-400', 'text-lg mr-2 text-gray-400',
) . ) .
lang('Status.actor_shared', [ lang('Post.actor_shared', [
'actor' => $status->actor->display_name, 'actor' => $post->actor->display_name,
]) ?></p> ]) ?></p>
<header class="flex px-6 py-4"> <header class="flex px-6 py-4">
<img src="<?= $status->actor <img src="<?= $post->actor
->avatar_image_url ?>" alt="<?= $status->display_name ?>" class="w-12 h-12 mr-4 rounded-full" /> ->avatar_image_url ?>" alt="<?= $post->display_name ?>" class="w-12 h-12 mr-4 rounded-full" />
<div class="flex flex-col min-w-0"> <div class="flex flex-col min-w-0">
<a href="<?= $status->actor <a href="<?= $post->actor
->uri ?>" class="flex items-baseline hover:underline" <?= $status ->uri ?>" class="flex items-baseline hover:underline" <?= $post
->actor->is_local ->actor->is_local
? '' ? ''
: 'target="_blank" rel="noopener noreferrer"' ?>> : 'target="_blank" rel="noopener noreferrer"' ?>>
<span class="mr-2 font-semibold truncate"><?= $status->actor <span class="mr-2 font-semibold truncate"><?= $post->actor
->display_name ?></span> ->display_name ?></span>
<span class="text-sm text-gray-500 truncate">@<?= $status->actor <span class="text-sm text-gray-500 truncate">@<?= $post->actor
->username . ->username .
($status->actor->is_local ($post->actor->is_local
? '' ? ''
: '@' . $status->actor->domain) ?></span> : '@' . $post->actor->domain) ?></span>
</a> </a>
<a href="<?= route_to('status', $podcast->handle, $status->id) ?>" <a href="<?= route_to('post', $podcast->handle, $post->id) ?>"
class="text-xs text-gray-500"> class="text-xs text-gray-500">
<?= relative_time($status->published_at) ?> <?= relative_time($post->published_at) ?>
</a> </a>
</div> </div>
</header> </header>
<div class="px-6 mb-4 status-content"><?= $status->message_html ?></div> <div class="px-6 mb-4 post-content"><?= $post->message_html ?></div>
<?php if ($status->episode_id): ?> <?php if ($post->episode_id): ?>
<?= view('podcast/_partials/episode_preview_card', [ <?= view('podcast/_partials/episode_preview_card', [
'episode' => $status->episode, 'episode' => $post->episode,
]) ?> ]) ?>
<?php elseif ($status->has_preview_card): ?> <?php elseif ($post->has_preview_card): ?>
<?= view('podcast/_partials/preview_card', [ <?= view('podcast/_partials/preview_card', [
'preview_card' => $status->preview_card, 'preview_card' => $post->preview_card,
]) ?> ]) ?>
<?php endif; ?> <?php endif; ?>
<?= $this->include('podcast/_partials/status_actions') ?> <?= $this->include('podcast/_partials/post_actions') ?>
</article> </article>

View File

@ -1,43 +1,43 @@
<article class="relative z-10 w-full bg-white shadow-md rounded-2xl"> <article class="relative z-10 w-full bg-white shadow rounded-2xl">
<p class="inline-flex px-6 pt-4 text-xs text-gray-700"><?= icon( <p class="inline-flex px-6 pt-4 text-xs text-gray-700"><?= icon(
'repeat', 'repeat',
'text-lg mr-2 text-gray-400', 'text-lg mr-2 text-gray-400',
) . ) .
lang('Status.actor_shared', [ lang('Post.actor_shared', [
'actor' => $status->actor->display_name, 'actor' => $post->actor->display_name,
]) ?></p> ]) ?></p>
<header class="flex px-6 py-4"> <header class="flex px-6 py-4">
<img src="<?= $status->actor <img src="<?= $post->actor
->avatar_image_url ?>" alt="<?= $status->display_name ?>" class="w-12 h-12 mr-4 rounded-full" /> ->avatar_image_url ?>" alt="<?= $post->display_name ?>" class="w-12 h-12 mr-4 rounded-full" />
<div class="flex flex-col min-w-0"> <div class="flex flex-col min-w-0">
<a href="<?= $status->actor <a href="<?= $post->actor
->uri ?>" class="flex items-baseline hover:underline" <?= $status ->uri ?>" class="flex items-baseline hover:underline" <?= $post
->actor->is_local ->actor->is_local
? '' ? ''
: 'target="_blank" rel="noopener noreferrer"' ?>> : 'target="_blank" rel="noopener noreferrer"' ?>>
<span class="mr-2 font-semibold truncate"><?= $status->actor <span class="mr-2 font-semibold truncate"><?= $post->actor
->display_name ?></span> ->display_name ?></span>
<span class="text-sm text-gray-500 truncate">@<?= $status->actor <span class="text-sm text-gray-500 truncate">@<?= $post->actor
->username . ->username .
($status->actor->is_local ($post->actor->is_local
? '' ? ''
: '@' . $status->actor->domain) ?></span> : '@' . $post->actor->domain) ?></span>
</a> </a>
<a href="<?= route_to('status', $podcast->handle, $status->id) ?>" <a href="<?= route_to('post', $podcast->handle, $post->id) ?>"
class="text-xs text-gray-500"> class="text-xs text-gray-500">
<?= relative_time($status->published_at) ?> <?= relative_time($post->published_at) ?>
</a> </a>
</div> </div>
</header> </header>
<div class="px-6 mb-4 status-content"><?= $status->message_html ?></div> <div class="px-6 mb-4 post-content"><?= $post->message_html ?></div>
<?php if ($status->episode_id): ?> <?php if ($post->episode_id): ?>
<?= view('podcast/_partials/episode_preview_card', [ <?= view('podcast/_partials/episode_preview_card', [
'episode' => $status->episode, 'episode' => $post->episode,
]) ?> ]) ?>
<?php elseif ($status->has_preview_card): ?> <?php elseif ($post->has_preview_card): ?>
<?= view('podcast/_partials/preview_card', [ <?= view('podcast/_partials/preview_card', [
'preview_card' => $status->preview_card, 'preview_card' => $post->preview_card,
]) ?> ]) ?>
<?php endif; ?> <?php endif; ?>
<?= $this->include('podcast/_partials/status_actions_authenticated') ?> <?= $this->include('podcast/_partials/post_actions_authenticated') ?>
</article> </article>

View File

@ -11,9 +11,9 @@
->display_name ?><span class="ml-1 text-sm font-normal text-gray-600">@<?= $reply ->display_name ?><span class="ml-1 text-sm font-normal text-gray-600">@<?= $reply
->actor->username . ->actor->username .
($reply->actor->is_local ? '' : '@' . $reply->actor->domain) ?></span></a> ($reply->actor->is_local ? '' : '@' . $reply->actor->domain) ?></span></a>
<?= relative_time($status->published_at, 'flex-shrink-0 ml-auto text-xs text-gray-600') ?> <?= relative_time($post->published_at, 'flex-shrink-0 ml-auto text-xs text-gray-600') ?>
</header> </header>
<p class="mb-2 status-content"><?= $reply->message_html ?></p> <p class="mb-2 post-content"><?= $reply->message_html ?></p>
<?php if ($reply->has_preview_card): ?> <?php if ($reply->has_preview_card): ?>
<?= view('podcast/_partials/preview_card', [ <?= view('podcast/_partials/preview_card', [
'preview_card' => $reply->preview_card, 'preview_card' => $reply->preview_card,

View File

@ -1,34 +1,34 @@
<footer class="mt-2 space-x-6 text-sm"> <footer class="mt-2 space-x-6 text-sm">
<?= anchor( <?= anchor(
route_to('status', $podcast->handle, $reply->id), route_to('post', $podcast->handle, $reply->id),
icon('chat', 'text-xl mr-1 text-gray-400') . $reply->replies_count, icon('chat', 'text-xl mr-1 text-gray-400') . $reply->replies_count,
[ [
'class' => 'inline-flex items-center hover:underline', 'class' => 'inline-flex items-center hover:underline',
'title' => lang('Status.replies', [ 'title' => lang('Post.replies', [
'numberOfReplies' => $reply->replies_count, 'numberOfReplies' => $reply->replies_count,
]), ]),
], ],
) ?> ) ?>
<?= anchor_popup( <?= anchor_popup(
route_to('status-remote-action', $podcast->handle, $reply->id, 'reblog'), route_to('post-remote-action', $podcast->handle, $reply->id, 'reblog'),
icon('repeat', 'text-xl mr-1 text-gray-400') . $reply->reblogs_count, icon('repeat', 'text-xl mr-1 text-gray-400') . $reply->reblogs_count,
[ [
'class' => 'inline-flex items-center hover:underline', 'class' => 'inline-flex items-center hover:underline',
'width' => 420, 'width' => 420,
'height' => 620, 'height' => 620,
'title' => lang('Status.reblogs', [ 'title' => lang('Post.reblogs', [
'numberOfReblogs' => $reply->reblogs_count, 'numberOfReblogs' => $reply->reblogs_count,
]), ]),
], ],
) ?> ) ?>
<?= anchor_popup( <?= anchor_popup(
route_to('status-remote-action', $podcast->handle, $reply->id, 'favourite'), route_to('post-remote-action', $podcast->handle, $reply->id, 'favourite'),
icon('heart', 'text-xl mr-1 text-gray-400') . $reply->favourites_count, icon('heart', 'text-xl mr-1 text-gray-400') . $reply->favourites_count,
[ [
'class' => 'inline-flex items-center hover:underline', 'class' => 'inline-flex items-center hover:underline',
'width' => 420, 'width' => 420,
'height' => 620, 'height' => 620,
'title' => lang('Status.favourites', [ 'title' => lang('Post.favourites', [
'numberOfFavourites' => $reply->favourites_count, 'numberOfFavourites' => $reply->favourites_count,
]), ]),
], ],

View File

@ -1,29 +1,29 @@
<footer class="mt-2 text-sm"> <footer class="mt-2 text-sm">
<form action="<?= route_to( <form action="<?= route_to(
'status-attempt-action', 'post-attempt-action',
interact_as_actor()->username, interact_as_actor()->username,
$reply->id, $reply->id,
) ?>" method="POST" class="flex items-start"> ) ?>" method="POST" class="flex items-start">
<?= csrf_field() ?> <?= csrf_field() ?>
<?= anchor( <?= anchor(
route_to('status', $podcast->handle, $reply->id), route_to('post', $podcast->handle, $reply->id),
icon('chat', 'text-xl mr-1 text-gray-400') . $reply->replies_count, icon('chat', 'text-xl mr-1 text-gray-400') . $reply->replies_count,
[ [
'class' => 'inline-flex items-center mr-6 hover:underline', 'class' => 'inline-flex items-center mr-6 hover:underline',
'title' => lang('Status.replies', [ 'title' => lang('Post.replies', [
'numberOfReplies' => $reply->replies_count, 'numberOfReplies' => $reply->replies_count,
]), ]),
], ],
) ?> ) ?>
<button type="submit" name="action" value="reblog" class="inline-flex items-center mr-6 hover:underline" title="<?= lang( <button type="submit" name="action" value="reblog" class="inline-flex items-center mr-6 hover:underline" title="<?= lang(
'Status.reblogs', 'Post.reblogs',
[ [
'numberOfReblogs' => $reply->reblogs_count, 'numberOfReblogs' => $reply->reblogs_count,
], ],
) ?>"><?= icon('repeat', 'text-xl mr-1 text-gray-400') . ) ?>"><?= icon('repeat', 'text-xl mr-1 text-gray-400') .
$reply->reblogs_count ?></button> $reply->reblogs_count ?></button>
<button type="submit" name="action" value="favourite" class="inline-flex items-center mr-6 hover:underline" title="<?= lang( <button type="submit" name="action" value="favourite" class="inline-flex items-center mr-6 hover:underline" title="<?= lang(
'Status.favourites', 'Post.favourites',
[ [
'numberOfFavourites' => $reply->favourites_count, 'numberOfFavourites' => $reply->favourites_count,
], ],
@ -40,33 +40,33 @@
'-more-dropdown-menu' ?>" class="flex flex-col py-2 text-sm bg-white border rounded-lg shadow" aria-labelledby="<?= $reply->id . '-more-dropdown-menu' ?>" class="flex flex-col py-2 text-sm bg-white border rounded-lg shadow" aria-labelledby="<?= $reply->id .
'-more-dropdown' ?>" data-dropdown="menu" data-dropdown-placement="bottom"> '-more-dropdown' ?>" data-dropdown="menu" data-dropdown-placement="bottom">
<?= anchor( <?= anchor(
route_to('status', $podcast->handle, $reply->id), route_to('post', $podcast->handle, $reply->id),
lang('Status.expand'), lang('Post.expand'),
[ [
'class' => 'px-4 py-1 hover:bg-gray-100', 'class' => 'px-4 py-1 hover:bg-gray-100',
], ],
) ?> ) ?>
<form action="<?= route_to( <form action="<?= route_to(
'status-attempt-block-actor', 'post-attempt-block-actor',
interact_as_actor()->username, interact_as_actor()->username,
$reply->id, $reply->id,
) ?>" method="POST"> ) ?>" method="POST">
<?= csrf_field() ?> <?= csrf_field() ?>
<button class="w-full px-4 py-1 text-left hover:bg-gray-100"><?= lang( <button class="w-full px-4 py-1 text-left hover:bg-gray-100"><?= lang(
'Status.block_actor', 'Post.block_actor',
[ [
'actorUsername' => $reply->actor->username, 'actorUsername' => $reply->actor->username,
], ],
) ?></button> ) ?></button>
</form> </form>
<form action="<?= route_to( <form action="<?= route_to(
'status-attempt-block-domain', 'post-attempt-block-domain',
interact_as_actor()->username, interact_as_actor()->username,
$reply->id, $reply->id,
) ?>" method="POST"> ) ?>" method="POST">
<?= csrf_field() ?> <?= csrf_field() ?>
<button class="w-full px-4 py-1 text-left hover:bg-gray-100"><?= lang( <button class="w-full px-4 py-1 text-left hover:bg-gray-100"><?= lang(
'Status.block_domain', 'Post.block_domain',
[ [
'actorDomain' => $reply->actor->domain, 'actorDomain' => $reply->actor->domain,
], ],
@ -75,13 +75,13 @@
<?php if ($reply->actor->is_local): ?> <?php if ($reply->actor->is_local): ?>
<hr class="my-2" /> <hr class="my-2" />
<form action="<?= route_to( <form action="<?= route_to(
'status-attempt-delete', 'post-attempt-delete',
$reply->actor->username, $reply->actor->username,
$reply->id, $reply->id,
) ?>" method="POST"> ) ?>" method="POST">
<?= csrf_field() ?> <?= csrf_field() ?>
<button class="w-full px-4 py-1 font-semibold text-left text-red-600 hover:bg-gray-100"><?= lang( <button class="w-full px-4 py-1 font-semibold text-left text-red-600 hover:bg-gray-100"><?= lang(
'Status.delete', 'Post.delete',
) ?></button> ) ?></button>
</form> </form>
<?php endif; ?> <?php endif; ?>

View File

@ -11,9 +11,9 @@
->display_name ?><span class="ml-1 text-sm font-normal text-gray-600">@<?= $reply ->display_name ?><span class="ml-1 text-sm font-normal text-gray-600">@<?= $reply
->actor->username . ->actor->username .
($reply->actor->is_local ? '' : '@' . $reply->actor->domain) ?></span></a> ($reply->actor->is_local ? '' : '@' . $reply->actor->domain) ?></span></a>
<?= relative_time($status->published_at, 'flex-shrink-0 ml-auto text-xs text-gray-600') ?> <?= relative_time($post->published_at, 'flex-shrink-0 ml-auto text-xs text-gray-600') ?>
</header> </header>
<p class="mb-2 status-content"><?= $reply->message_html ?></p> <p class="mb-2 post-content"><?= $reply->message_html ?></p>
<?php if ($reply->has_preview_card): ?> <?php if ($reply->has_preview_card): ?>
<?= view('podcast/_partials/preview_card', [ <?= view('podcast/_partials/preview_card', [
'preview_card' => $reply->preview_card, 'preview_card' => $reply->preview_card,

View File

@ -1,36 +0,0 @@
<article class="relative z-10 w-full bg-white shadow rounded-2xl">
<header class="flex px-6 py-4">
<img src="<?= $status->actor
->avatar_image_url ?>" alt="<?= $status->display_name ?>" class="w-12 h-12 mr-4 rounded-full" />
<div class="flex flex-col min-w-0">
<a href="<?= $status->actor
->uri ?>" class="flex items-baseline hover:underline" <?= $status
->actor->is_local
? ''
: 'target="_blank" rel="noopener noreferrer"' ?>>
<span class="mr-2 font-semibold truncate"><?= $status->actor
->display_name ?></span>
<span class="text-sm text-gray-500 truncate">@<?= $status->actor
->username .
($status->actor->is_local
? ''
: '@' . $status->actor->domain) ?></span>
</a>
<a href="<?= route_to('status', $podcast->handle, $status->id) ?>"
class="text-xs text-gray-500">
<?= relative_time($status->published_at) ?>
</a>
</div>
</header>
<div class="px-6 mb-4 status-content"><?= $status->message_html ?></div>
<?php if ($status->episode_id): ?>
<?= view('podcast/_partials/episode_preview_card', [
'episode' => $status->episode,
]) ?>
<?php elseif ($status->has_preview_card): ?>
<?= view('podcast/_partials/preview_card', [
'preview_card' => $status->preview_card,
]) ?>
<?php endif; ?>
<?= $this->include('podcast/_partials/status_actions') ?>
</article>

View File

@ -1,36 +0,0 @@
<footer class="flex justify-around px-6 py-3">
<?= anchor(
route_to('status', $podcast->handle, $status->id),
icon('chat', 'text-2xl mr-1 text-gray-400') . $status->replies_count,
[
'class' => 'inline-flex items-center hover:underline',
'title' => lang('Status.replies', [
'numberOfReplies' => $status->replies_count,
]),
],
) ?>
<?= anchor_popup(
route_to('status-remote-action', $podcast->handle, $status->id, 'reblog'),
icon('repeat', 'text-2xl mr-1 text-gray-400') . $status->reblogs_count,
[
'class' => 'inline-flex items-center hover:underline',
'width' => 420,
'height' => 620,
'title' => lang('Status.reblogs', [
'numberOfReblogs' => $status->reblogs_count,
]),
],
) ?>
<?= anchor_popup(
route_to('status-remote-action', $podcast->handle, $status->id, 'favourite'),
icon('heart', 'text-2xl mr-1 text-gray-400') . $status->favourites_count,
[
'class' => 'inline-flex items-center hover:underline',
'width' => 420,
'height' => 620,
'title' => lang('Status.favourites', [
'numberOfFavourites' => $status->favourites_count,
]),
],
) ?>
</footer>

View File

@ -1,36 +0,0 @@
<article class="relative z-10 w-full bg-white shadow-md rounded-2xl">
<header class="flex px-6 py-4">
<img src="<?= $status->actor
->avatar_image_url ?>" alt="<?= $status->display_name ?>" class="w-12 h-12 mr-4 rounded-full" />
<div class="flex flex-col min-w-0">
<a href="<?= $status->actor
->uri ?>" class="flex items-baseline hover:underline" <?= $status
->actor->is_local
? ''
: 'target="_blank" rel="noopener noreferrer"' ?>>
<span class="mr-2 font-semibold truncate"><?= $status->actor
->display_name ?></span>
<span class="text-sm text-gray-500 truncate">@<?= $status->actor
->username .
($status->actor->is_local
? ''
: '@' . $status->actor->domain) ?></span>
</a>
<a href="<?= route_to('status', $podcast->handle, $status->id) ?>"
class="text-xs text-gray-500">
<?= relative_time($status->published_at) ?>
</a>
</div>
</header>
<div class="px-6 mb-4 status-content"><?= $status->message_html ?></div>
<?php if ($status->episode_id): ?>
<?= view('podcast/_partials/episode_preview_card', [
'episode' => $status->episode,
]) ?>
<?php elseif ($status->has_preview_card): ?>
<?= view('podcast/_partials/preview_card', [
'preview_card' => $status->preview_card,
]) ?>
<?php endif; ?>
<?= $this->include('podcast/_partials/status_actions_authenticated') ?>
</article>

View File

@ -40,13 +40,13 @@
</nav> </nav>
<section class="max-w-2xl px-6 py-8 mx-auto space-y-8"> <section class="max-w-2xl px-6 py-8 mx-auto space-y-8">
<?php foreach ($statuses as $status): ?> <?php foreach ($posts as $post): ?>
<?php if ($status->reblog_of_id !== null): ?> <?php if ($post->reblog_of_id !== null): ?>
<?= view('podcast/_partials/reblog', [ <?= view('podcast/_partials/reblog', [
'status' => $status->reblog_of_status, 'post' => $post->reblog_of_post,
]) ?> ]) ?>
<?php else: ?> <?php else: ?>
<?= view('podcast/_partials/status', ['status' => $status]) ?> <?= view('podcast/_partials/post', ['post' => $post]) ?>
<?php endif; ?> <?php endif; ?>
<?php endforeach; ?> <?php endforeach; ?>
</section> </section>

View File

@ -40,7 +40,7 @@
</nav> </nav>
<section class="max-w-2xl px-6 py-8 mx-auto"> <section class="max-w-2xl px-6 py-8 mx-auto">
<?= form_open(route_to('status-attempt-create', interact_as_actor()->username), [ <?= form_open(route_to('post-attempt-create', interact_as_actor()->username), [
'class' => 'flex p-4 bg-white shadow rounded-xl', 'class' => 'flex p-4 bg-white shadow rounded-xl',
]) ?> ]) ?>
<?= csrf_field() ?> <?= csrf_field() ?>
@ -57,7 +57,7 @@
'name' => 'message', 'name' => 'message',
'class' => 'form-textarea', 'class' => 'form-textarea',
'required' => 'required', 'required' => 'required',
'placeholder' => lang('Status.form.message_placeholder'), 'placeholder' => lang('Post.form.message_placeholder'),
], ],
old('message', '', false), old('message', '', false),
['rows' => 2], ['rows' => 2],
@ -67,7 +67,7 @@
'name' => 'episode_url', 'name' => 'episode_url',
'class' => 'form-input mb-2', 'class' => 'form-input mb-2',
'placeholder' => 'placeholder' =>
lang('Status.form.episode_url_placeholder') . lang('Post.form.episode_url_placeholder') .
' (' . ' (' .
lang('Common.optional') . lang('Common.optional') .
')', ')',
@ -75,7 +75,7 @@
]) ?> ]) ?>
<?= button( <?= button(
lang('Status.form.submit'), lang('Post.form.submit'),
'', '',
['variant' => 'primary', 'size' => 'small'], ['variant' => 'primary', 'size' => 'small'],
['type' => 'submit', 'class' => 'self-end'], ['type' => 'submit', 'class' => 'self-end'],
@ -85,13 +85,13 @@
<hr class="my-4 border-2 border-pine-100"> <hr class="my-4 border-2 border-pine-100">
<div class="space-y-8"> <div class="space-y-8">
<?php foreach ($statuses as $status): ?> <?php foreach ($posts as $post): ?>
<?php if ($status->reblog_of_id !== null): ?> <?php if ($post->reblog_of_id !== null): ?>
<?= view('podcast/_partials/reblog_authenticated', [ <?= view('podcast/_partials/reblog_authenticated', [
'status' => $status->reblog_of_status, 'post' => $post->reblog_of_post,
]) ?> ]) ?>
<?php else: ?> <?php else: ?>
<?= view('podcast/_partials/status_authenticated', ['status' => $status]) ?> <?= view('podcast/_partials/post_authenticated', ['post' => $post]) ?>
<?php endif; ?> <?php endif; ?>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>

View File

@ -65,46 +65,6 @@
<?= format_duration($episode->audio_file_duration) ?> <?= format_duration($episode->audio_file_duration) ?>
</time> </time>
</div> </div>
<div class="mb-2 space-x-4 text-sm">
<?= anchor(
route_to('episode', $podcast->handle, $episode->slug),
icon('chat', 'text-xl mr-1 text-gray-400') .
$episode->statuses_total,
[
'class' =>
'inline-flex items-center hover:underline',
'title' => lang('Episode.total_statuses', [
'numberOfTotalStatuses' => $episode->statuses_total,
]),
],
) ?>
<?= anchor(
route_to('episode', $podcast->handle, $episode->slug),
icon('repeat', 'text-xl mr-1 text-gray-400') .
$episode->reblogs_total,
[
'class' =>
'inline-flex items-center hover:underline',
'title' => lang('Episode.total_reblogs', [
'numberOfTotalReblogs' =>
$episode->reblogs_total,
]),
],
) ?>
<?= anchor(
route_to('episode', $podcast->handle, $episode->slug),
icon('heart', 'text-xl mr-1 text-gray-400') .
$episode->favourites_total,
[
'class' =>
'inline-flex items-center hover:underline',
'title' => lang('Episode.total_favourites', [
'numberOfTotalFavourites' =>
$episode->favourites_total,
]),
],
) ?>
</div>
<?= location_link($episode->location, 'text-sm mb-4') ?> <?= location_link($episode->location, 'text-sm mb-4') ?>
<?= person_list($episode->persons) ?> <?= person_list($episode->persons) ?>
<?= play_episode_button($episode->id, $episode->image->thumbnail_url, $episode->title, $podcast->title, $episode->audio_file_web_url, $episode->audio_file_mimetype) ?> <?= play_episode_button($episode->id, $episode->image->thumbnail_url, $episode->title, $podcast->title, $episode->audio_file_web_url, $episode->audio_file_mimetype) ?>
@ -113,27 +73,26 @@
</header> </header>
<div class="tabset"> <div class="tabset">
<?php if ($episode->statuses): ?> <input type="radio" name="tabset" id="comments" aria-controls="comments" checked="checked" />
<label for="comments"><?= lang('Episode.comments') . '(' . $episode->comments_count . ')' ?></label>
<input type="radio" name="tabset" id="activity" aria-controls="activity" />
<label for="activity"><?= lang('Episode.activity') . '(' . $episode->posts_count . ')' ?></label>
<input type="radio" name="tabset" id="activity" aria-controls="activity" checked="checked" /> <input type="radio" name="tabset" id="description" aria-controls="description" />
<label for="activity"><?= lang('Episode.activity') ?></label> <label for="description"><?= lang('Episode.description') ?></label>
<?php endif; ?>
<input type="radio" name="tabset" id="description" aria-controls="description" <?= $episode->statuses
? ''
: 'checked="checked"' ?> />
<label for="description" class="<?= $episode->statuses
? ''
: 'col-span-2' ?>"><?= lang('Episode.description') ?></label>
<div class="tab-panels"> <div class="tab-panels">
<?php if ($episode->statuses): ?> <section id="comments" class="space-y-6 tab-panel">
<section id="activity" class="space-y-8 tab-panel"> <?php foreach ($episode->comments as $comment): ?>
<?php foreach ($episode->statuses as $status): ?> <?= view('podcast/_partials/comment', ['comment' => $comment]) ?>
<?= view('podcast/_partials/status', ['status' => $status]) ?> <?php endforeach; ?>
<?php endforeach; ?> </section>
</section> <section id="activity" class="space-y-8 tab-panel">
<?php endif; ?> <?php foreach ($episode->posts as $post): ?>
<?= view('podcast/_partials/post', ['post' => $post]) ?>
<?php endforeach; ?>
</section>
<section id="description" class="prose tab-panel"> <section id="description" class="prose tab-panel">
<?= $episode->getDescriptionHtml('-+Website+-') ?> <?= $episode->getDescriptionHtml('-+Website+-') ?>
</section> </section>

View File

@ -65,46 +65,6 @@
<?= format_duration($episode->audio_file_duration) ?> <?= format_duration($episode->audio_file_duration) ?>
</time> </time>
</div> </div>
<div class="mb-2 space-x-4 text-sm">
<?= anchor(
route_to('episode', $podcast->handle, $episode->slug),
icon('chat', 'text-xl mr-1 text-gray-400') .
$episode->statuses_total,
[
'class' =>
'inline-flex items-center hover:underline',
'title' => lang('Episode.total_statuses', [
'numberOfTotalStatuses' => $episode->statuses_total,
]),
],
) ?>
<?= anchor(
route_to('episode', $podcast->handle, $episode->slug),
icon('repeat', 'text-xl mr-1 text-gray-400') .
$episode->reblogs_total,
[
'class' =>
'inline-flex items-center hover:underline',
'title' => lang('Episode.total_reblogs', [
'numberOfTotalReblogs' =>
$episode->reblogs_total,
]),
],
) ?>
<?= anchor(
route_to('episode', $podcast->handle, $episode->slug),
icon('heart', 'text-xl mr-1 text-gray-400') .
$episode->favourites_total,
[
'class' =>
'inline-flex items-center hover:underline',
'title' => lang('Episode.total_favourites', [
'numberOfTotalFavourites' =>
$episode->favourites_total,
]),
],
) ?>
</div>
<?= location_link($episode->location, 'text-sm mb-4') ?> <?= location_link($episode->location, 'text-sm mb-4') ?>
<?= person_list($episode->persons) ?> <?= person_list($episode->persons) ?>
<?= play_episode_button($episode->id, $episode->image->thumbnail_url, $episode->title, $podcast->title, $episode->audio_file_web_url, $episode->audio_file_mimetype) ?> <?= play_episode_button($episode->id, $episode->image->thumbnail_url, $episode->title, $podcast->title, $episode->audio_file_web_url, $episode->audio_file_mimetype) ?>
@ -113,15 +73,18 @@
</header> </header>
<div class="tabset"> <div class="tabset">
<input type="radio" name="tabset" id="activity" aria-controls="activity" checked="checked" /> <input type="radio" name="tabset" id="comments" aria-controls="comments" checked="checked" />
<label for="activity"><?= lang('Episode.activity') ?></label> <label for="comments"><?= lang('Episode.comments') . '(' . $episode->comments_count . ')' ?></label>
<input type="radio" name="tabset" id="activity" aria-controls="activity" />
<label for="activity"><?= lang('Episode.activity') . '('. $episode->posts_count .')' ?></label>
<input type="radio" name="tabset" id="description" aria-controls="description" /> <input type="radio" name="tabset" id="description" aria-controls="description" />
<label for="description"><?= lang('Episode.description') ?></label> <label for="description"><?= lang('Episode.description') ?></label>
<div class="tab-panels"> <div class="tab-panels">
<section id="activity" class="space-y-8 tab-panel"> <section id="comments" class="space-y-6 tab-panel">
<?= form_open(route_to('status-attempt-create', $podcast->handle), [ <?= form_open(route_to('comment-attempt-create', $podcast->id, $episode->id), [
'class' => 'flex p-4 bg-white shadow rounded-xl', 'class' => 'flex p-4 bg-white shadow rounded-xl',
]) ?> ]) ?>
<?= csrf_field() ?> <?= csrf_field() ?>
@ -129,8 +92,7 @@
<?= view('_message_block') ?> <?= view('_message_block') ?>
<img src="<?= interact_as_actor() <img src="<?= interact_as_actor()
->avatar_image_url ?>" alt="<?= interact_as_actor() ->avatar_image_url ?>" alt="<?= interact_as_actor()->display_name ?>" class="w-12 h-12 mr-4 rounded-full" />
->display_name ?>" class="w-12 h-12 mr-4 rounded-full" />
<div class="flex flex-col flex-1 min-w-0"> <div class="flex flex-col flex-1 min-w-0">
<?= form_textarea( <?= form_textarea(
[ [
@ -139,7 +101,47 @@
'class' => 'form-textarea mb-2', 'class' => 'form-textarea mb-2',
'required' => 'required', 'required' => 'required',
'placeholder' => lang( 'placeholder' => lang(
'Status.form.episode_message_placeholder', 'Comment.form.episode_message_placeholder',
),
],
old('message', '', false),
[
'rows' => 2,
],
) ?>
<?= button(
lang('Comment.form.submit'),
'',
['variant' => 'primary', 'size' => 'small'],
['type' => 'submit', 'class' => 'self-end'],
) ?>
</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; ?>
</section>
<section id="activity" class="space-y-8 tab-panel">
<?= form_open(route_to('post-attempt-create', $podcast->handle), [
'class' => 'flex p-4 bg-white shadow rounded-xl',
]) ?>
<?= csrf_field() ?>
<?= view('_message_block') ?>
<img src="<?= interact_as_actor()
->avatar_image_url ?>" alt="<?= interact_as_actor()->display_name ?>" class="w-12 h-12 mr-4 rounded-full" />
<div class="flex flex-col flex-1 min-w-0">
<?= form_textarea(
[
'id' => 'message',
'name' => 'message',
'class' => 'form-textarea mb-2',
'required' => 'required',
'placeholder' => lang(
'Post.form.episode_message_placeholder',
), ),
], ],
old('message', '', false), old('message', '', false),
@ -154,7 +156,7 @@
'type' => 'hidden', 'type' => 'hidden',
]) ?> ]) ?>
<?= button( <?= button(
lang('Status.form.submit'), lang('Post.form.submit'),
'', '',
['variant' => 'primary', 'size' => 'small'], ['variant' => 'primary', 'size' => 'small'],
['type' => 'submit', 'class' => 'self-end'], ['type' => 'submit', 'class' => 'self-end'],
@ -162,9 +164,9 @@
</div> </div>
<?= form_close() ?> <?= form_close() ?>
<hr class="my-4 border border-pine-100"> <hr class="my-4 border border-pine-100">
<?php foreach ($episode->statuses as $status): ?> <?php foreach ($episode->posts as $post): ?>
<?= view('podcast/_partials/status_authenticated', [ <?= view('podcast/_partials/post_authenticated', [
'status' => $status, 'post' => $post,
]) ?> ]) ?>
<?php endforeach; ?> <?php endforeach; ?>
</section> </section>

View File

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

View File

@ -1,20 +1,20 @@
<?= $this->extend('podcast/_layout_authenticated') ?> <?= $this->extend('podcast/_layout_authenticated') ?>
<?= $this->section('meta-tags') ?> <?= $this->section('meta-tags') ?>
<title><?= lang('Status.title', [ <title><?= lang('Post.title', [
'actorDisplayName' => $status->actor->display_name, 'actorDisplayName' => $post->actor->display_name,
]) ?></title> ]) ?></title>
<meta name="description" content="<?= $status->message ?>"/> <meta name="description" content="<?= $post->message ?>"/>
<meta property="og:title" content="<?= lang('Status.title', [ <meta property="og:title" content="<?= lang('Post.title', [
'actorDisplayName' => $status->actor->display_name, 'actorDisplayName' => $post->actor->display_name,
]) ?>"/> ]) ?>"/>
<meta property="og:locale" content="<?= service( <meta property="og:locale" content="<?= service(
'request', 'request',
)->getLocale() ?>" /> )->getLocale() ?>" />
<meta property="og:site_name" content="<?= $status->actor->display_name ?>" /> <meta property="og:site_name" content="<?= $post->actor->display_name ?>" />
<meta property="og:url" content="<?= current_url() ?>" /> <meta property="og:url" content="<?= current_url() ?>" />
<meta property="og:image" content="<?= $status->actor->avatar_image_url ?>" /> <meta property="og:image" content="<?= $post->actor->avatar_image_url ?>" />
<meta property="og:description" content="<?= $status->message ?>" /> <meta property="og:description" content="<?= $post->message ?>" />
<?= $this->endSection() ?> <?= $this->endSection() ?>
<?= $this->section('content') ?> <?= $this->section('content') ?>
@ -25,13 +25,13 @@
'arrow-left', 'arrow-left',
'mr-2 text-lg', 'mr-2 text-lg',
) . ) .
lang('Status.back_to_actor_statuses', [ lang('Post.back_to_actor_posts', [
'actor' => $status->actor->display_name, 'actor' => $post->actor->display_name,
]) ?></a> ]) ?></a>
</nav> </nav>
<div class="pb-12"> <div class="pb-12">
<?= $this->include( <?= $this->include(
'podcast/_partials/status_with_replies_authenticated', 'podcast/_partials/post_with_replies_authenticated',
) ?> ) ?>
</div> </div>
</div> </div>

View File

@ -7,22 +7,22 @@
<link rel="shortcut icon" type="image/png" href="/favicon.ico" /> <link rel="shortcut icon" type="image/png" href="/favicon.ico" />
<title><?= lang('ActivityPub.' . $action . '.title', [ <title><?= lang('ActivityPub.' . $action . '.title', [
'actorDisplayName' => $status->actor->display_name, 'actorDisplayName' => $post->actor->display_name,
]) ?></title> ]) ?></title>
<meta name="description" content="<?= $status->message ?>"/> <meta name="description" content="<?= $post->message ?>"/>
<meta property="og:title" content="<?= lang( <meta property="og:title" content="<?= lang(
'ActivityPub.' . $action . '.title', 'ActivityPub.' . $action . '.title',
[ [
'actorDisplayName' => $status->actor->display_name, 'actorDisplayName' => $post->actor->display_name,
], ],
) ?>"/> ) ?>"/>
<meta property="og:locale" content="<?= service( <meta property="og:locale" content="<?= service(
'request', 'request',
)->getLocale() ?>" /> )->getLocale() ?>" />
<meta property="og:site_name" content="<?= $status->actor->display_name ?>" /> <meta property="og:site_name" content="<?= $post->actor->display_name ?>" />
<meta property="og:url" content="<?= current_url() ?>" /> <meta property="og:url" content="<?= current_url() ?>" />
<meta property="og:image" content="<?= $status->actor->avatar_image_url ?>" /> <meta property="og:image" content="<?= $post->actor->avatar_image_url ?>" />
<meta property="og:description" content="<?= $status->message ?>" /> <meta property="og:description" content="<?= $post->message ?>" />
<?= service('vite')->asset('styles/index.css', 'css') ?> <?= service('vite')->asset('styles/index.css', 'css') ?>
<?= service('vite')->asset('js/podcast.ts', 'js') ?> <?= service('vite')->asset('js/podcast.ts', 'js') ?>
@ -35,10 +35,10 @@
) ?></h1> ) ?></h1>
</header> </header>
<main class="flex-1 max-w-xl px-4 pb-8 mx-auto -mt-24"> <main class="flex-1 max-w-xl px-4 pb-8 mx-auto -mt-24">
<?= $this->include('podcast/_partials/status') ?> <?= $this->include('podcast/_partials/post') ?>
<?= form_open( <?= form_open(
route_to('status-attempt-remote-action', $status->id, $action), route_to('post-attempt-remote-action', $post->id, $action),
['method' => 'post', 'class' => 'flex flex-col mt-8'], ['method' => 'post', 'class' => 'flex flex-col mt-8'],
) ?> ) ?>
<?= csrf_field() ?> <?= csrf_field() ?>

View File

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