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;
use App\Entities\Actor;
use App\Entities\Status;
use App\Entities\Post;
use App\Entities\User;
use CodeIgniter\Events\Events;
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 {
if ($status->in_reply_to_id !== null) {
$status = $status->reply_to_status;
Events::on('on_post_add', function ($post): void {
$isReply = $post->in_reply_to_id !== null;
if ($isReply) {
$post = $post->reply_to_post;
}
if ($status->episode_id) {
model('EpisodeModel')
->where('id', $status->episode_id)
->increment('statuses_total');
if ($post->episode_id !== null) {
if ($isReply) {
model('EpisodeModel', false)
->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
// same for other events below
cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*");
->deleteMatching("podcast#{$post->actor->podcast->id}*");
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 {
if ($status->in_reply_to_id !== null) {
Events::trigger('on_status_remove', $status->reply_to_status);
Events::on('on_post_remove', function ($post): void {
if ($post->in_reply_to_id !== null) {
Events::trigger('on_post_remove', $post->reply_to_post);
}
if ($episodeId = $status->episode_id) {
model('EpisodeModel')
if ($episodeId = $post->episode_id) {
model('EpisodeModel', false)
->where('id', $episodeId)
->decrement('statuses_total', 1 + $status->reblogs_count);
model('EpisodeModel')
->where('id', $episodeId)
->decrement('reblogs_total', $status->reblogs_count);
model('EpisodeModel')
->where('id', $episodeId)
->decrement('favourites_total', $status->favourites_count);
->decrement('posts_count');
}
if ($status->actor->is_podcast) {
if ($post->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*");
->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
}
cache()
->deleteMatching("page_status#{$status->id}*");
->deleteMatching("page_post#{$post->id}*");
});
/**
* @param Actor $actor
* @param Status $status
* @param Post $post
*/
Events::on('on_status_reblog', function ($actor, $status): void {
if ($episodeId = $status->episode_id) {
model('EpisodeModel')
->where('id', $episodeId)
->increment('reblogs_total');
model('EpisodeModel')
->where('id', $episodeId)
->increment('statuses_total');
}
if ($status->actor->is_podcast) {
Events::on('on_post_reblog', function ($actor, $post): void {
if ($post->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*");
->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
}
if ($actor->is_podcast) {
@ -205,111 +195,96 @@ Events::on('on_status_reblog', function ($actor, $status): void {
}
cache()
->deleteMatching("page_status#{$status->id}*");
->deleteMatching("page_post#{$post->id}*");
if ($status->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}");
if ($post->in_reply_to_id !== null) {
cache()->deleteMatching("page_post#{$post->in_reply_to_id}");
}
});
/**
* @param Status $reblogStatus
* @param Post $reblogPost
*/
Events::on('on_status_undo_reblog', function ($reblogStatus): void {
$status = $reblogStatus->reblog_of_status;
if ($episodeId = $status->episode_id) {
model('EpisodeModel')
->where('id', $episodeId)
->decrement('reblogs_total');
Events::on('on_post_undo_reblog', function ($reblogPost): void {
$post = $reblogPost->reblog_of_post;
model('EpisodeModel')
->where('id', $episodeId)
->decrement('statuses_total');
}
if ($status->actor->is_podcast) {
if ($post->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*");
->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
}
cache()
->deleteMatching("page_status#{$status->id}*");
->deleteMatching("page_post#{$post->id}*");
cache()
->deleteMatching("page_status#{$reblogStatus->id}*");
->deleteMatching("page_post#{$reblogPost->id}*");
if ($status->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}");
if ($post->in_reply_to_id !== null) {
cache()->deleteMatching("page_post#{$post->in_reply_to_id}");
}
if ($reblogStatus->actor->is_podcast) {
if ($reblogPost->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$reblogStatus->actor->podcast->id}*");
->deleteMatching("podcast#{$reblogPost->actor->podcast->id}*");
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 {
$status = $reply->reply_to_status;
Events::on('on_post_reply', function ($reply): void {
$post = $reply->reply_to_post;
if ($status->actor->is_podcast) {
if ($post->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*");
->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
}
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 {
$status = $reply->reply_to_status;
$post = $reply->reply_to_post;
if ($status->actor->is_podcast) {
if ($post->actor->is_podcast) {
cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*");
->deleteMatching("podcast#{$post->actor->podcast->id}*");
}
cache()
->deleteMatching("page_status#{$status->id}*");
->deleteMatching("page_post#{$post->id}*");
cache()
->deleteMatching("page_status#{$reply->id}*");
->deleteMatching("page_post#{$reply->id}*");
});
/**
* @param Actor $actor
* @param Status $status
* @param Post $post
*/
Events::on('on_status_favourite', function ($actor, $status): void {
if ($status->episode_id) {
model('EpisodeModel')
->where('id', $status->episode_id)
->increment('favourites_total');
}
if ($status->actor->is_podcast) {
Events::on('on_post_favourite', function ($actor, $post): void {
if ($post->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*");
->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
}
cache()
->deleteMatching("page_status#{$status->id}*");
->deleteMatching("page_post#{$post->id}*");
if ($status->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}*");
if ($post->in_reply_to_id !== null) {
cache()->deleteMatching("page_post#{$post->in_reply_to_id}*");
}
if ($actor->is_podcast) {
@ -321,27 +296,21 @@ Events::on('on_status_favourite', function ($actor, $status): void {
/**
* @param Actor $actor
* @param Status $status
* @param Post $post
*/
Events::on('on_status_undo_favourite', function ($actor, $status): void {
if ($status->episode_id) {
model('EpisodeModel')
->where('id', $status->episode_id)
->decrement('favourites_total');
}
if ($status->actor->is_podcast) {
Events::on('on_post_undo_favourite', function ($actor, $post): void {
if ($post->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$status->actor->podcast->id}*");
->deleteMatching("podcast#{$post->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
}
cache()
->deleteMatching("page_status#{$status->id}*");
->deleteMatching("page_post#{$post->id}*");
if ($status->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}*");
if ($post->in_reply_to_id !== null) {
cache()->deleteMatching("page_post#{$post->in_reply_to_id}*");
}
if ($actor->is_podcast) {
@ -356,7 +325,7 @@ Events::on('on_block_actor', function (int $actorId): void {
cache()
->deleteMatching('podcast*');
cache()
->deleteMatching('page_status*');
->deleteMatching('page_post*');
});
Events::on('on_unblock_actor', function (int $actorId): void {
@ -364,7 +333,7 @@ Events::on('on_unblock_actor', function (int $actorId): void {
cache()
->deleteMatching('podcast*');
cache()
->deleteMatching('page_status*');
->deleteMatching('page_post*');
});
Events::on('on_block_domain', function (string $domainName): void {
@ -372,7 +341,7 @@ Events::on('on_block_domain', function (string $domainName): void {
cache()
->deleteMatching('podcast*');
cache()
->deleteMatching('page_status*');
->deleteMatching('page_post*');
});
Events::on('on_unblock_domain', function (string $domainName): void {
@ -380,5 +349,5 @@ Events::on('on_unblock_domain', function (string $domainName): void {
cache()
->deleteMatching('podcast*');
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('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('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(
'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',
],
]);
$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', [
'as' => 'episode-oembed-json',
]);
@ -803,73 +828,74 @@ $routes->post('interact-as-actor', 'AuthController::attemptInteractAsActor', [
* Overwriting ActivityPub routes file
*/
$routes->group('@(:podcastHandle)', function ($routes): void {
$routes->post('statuses/new', 'StatusController::attemptCreate/$1', [
'as' => 'status-attempt-create',
$routes->post('posts/new', 'PostController::attemptCreate/$1', [
'as' => 'post-attempt-create',
'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->get('/', 'StatusController::view/$1/$2', [
'as' => 'status',
$routes->get('/', 'PostController::view/$1/$2', [
'as' => 'post',
'alternate-content' => [
'application/activity+json' => [
'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'StatusController/$2',
'controller-method' => 'PostController/$2',
],
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'StatusController/$2',
'controller-method' => 'PostController/$2',
],
],
]);
$routes->options('replies', 'ActivityPubController::preflight');
$routes->get('replies', 'StatusController/$1/$2', [
'as' => 'status-replies',
$routes->get('replies', 'PostController/$1/$2', [
'as' => 'post-replies',
'alternate-content' => [
'application/activity+json' => [
'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'StatusController::replies/$2',
'controller-method' => 'PostController::replies/$2',
],
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'StatusController::replies/$2',
'controller-method' => 'PostController::replies/$2',
],
],
]);
// Actions
$routes->post('action', 'StatusController::attemptAction/$1/$2', [
'as' => 'status-attempt-action',
$routes->post('action', 'PostController::attemptAction/$1/$2', [
'as' => 'post-attempt-action',
'filter' => 'permission:podcast-interact_as',
]);
$routes->post(
'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',
],
);
$routes->post(
'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',
],
);
$routes->post('delete', 'StatusController::attemptDelete/$1/$2', [
'as' => 'status-attempt-delete',
$routes->post('delete', 'PostController::attemptDelete/$1/$2', [
'as' => 'post-attempt-delete',
'filter' => 'permission:podcast-manage_publications',
]);
$routes->get(
'remote/(:statusAction)',
'StatusController::remoteAction/$1/$2/$3',
'remote/(:postAction)',
'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;
use App\Entities\Comment;
use App\Entities\Episode;
use App\Entities\Image;
use App\Entities\Location;
use App\Entities\Podcast;
use App\Entities\Status;
use App\Entities\Post;
use App\Models\CommentModel;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use App\Models\PostModel;
use App\Models\SoundbiteModel;
use App\Models\StatusModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\I18n\Time;
@ -429,7 +431,7 @@ class EpisodeController extends BaseController
$db = db_connect();
$db->transStart();
$newStatus = new Status([
$newPost = new Post([
'actor_id' => $this->podcast->actor_id,
'episode_id' => $this->episode->id,
'message' => $this->request->getPost('message'),
@ -456,15 +458,15 @@ class EpisodeController extends BaseController
$this->episode->published_at = Time::now();
}
$newStatus->published_at = $this->episode->published_at;
$newPost->published_at = $this->episode->published_at;
$statusModel = new StatusModel();
if (! $statusModel->addStatus($newStatus)) {
$postModel = new PostModel();
if (! $postModel->addPost($newPost)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $statusModel->errors());
->with('errors', $postModel->errors());
}
$episodeModel = new EpisodeModel();
@ -489,7 +491,7 @@ class EpisodeController extends BaseController
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
'status' => (new StatusModel())
'post' => (new PostModel())
->where([
'actor_id' => $this->podcast->actor_id,
'episode_id' => $this->episode->id,
@ -513,7 +515,7 @@ class EpisodeController extends BaseController
public function attemptPublishEdit(): RedirectResponse
{
$rules = [
'status_id' => 'required',
'post_id' => 'required',
'publication_method' => 'required',
'scheduled_publication_date' =>
'valid_date[Y-m-d H:i]|permit_empty',
@ -549,19 +551,19 @@ class EpisodeController extends BaseController
$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) {
$status->message = $this->request->getPost('message');
$status->published_at = $this->episode->published_at;
if ($post !== null) {
$post->message = $this->request->getPost('message');
$post->published_at = $this->episode->published_at;
$statusModel = new StatusModel();
if (! $statusModel->editStatus($status)) {
$postModel = new PostModel();
if (! $postModel->editPost($post)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $statusModel->errors());
->with('errors', $postModel->errors());
}
}
@ -585,14 +587,14 @@ class EpisodeController extends BaseController
$db = db_connect();
$db->transStart();
$statusModel = new StatusModel();
$status = $statusModel
$postModel = new PostModel();
$post = $postModel
->where([
'actor_id' => $this->podcast->actor_id,
'episode_id' => $this->episode->id,
])
->first();
$statusModel->removeStatus($status);
$postModel->removePost($post);
$this->episode->published_at = null;
@ -656,13 +658,13 @@ class EpisodeController extends BaseController
$db->transStart();
$allStatusesLinkedToEpisode = (new StatusModel())
$allPostsLinkedToEpisode = (new PostModel())
->where([
'episode_id' => $this->episode->id,
])
->findAll();
foreach ($allStatusesLinkedToEpisode as $status) {
(new StatusModel())->removeStatus($status);
foreach ($allPostsLinkedToEpisode as $post) {
(new PostModel())->removePost($post);
}
// set episode published_at to null to unpublish
@ -782,4 +784,41 @@ class EpisodeController extends BaseController
]);
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 App\Entities\Episode;
use App\Entities\Podcast;
use App\Libraries\CommentObject;
use App\Libraries\NoteObject;
use App\Libraries\PodcastEpisode;
use App\Models\CommentModel;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Database\BaseBuilder;
@ -219,10 +221,10 @@ class EpisodeController extends BaseController
/**
* 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 {
return $builder->select('id')
->from('activitypub_statuses')
->from('activitypub_posts')
->where('episode_id', $this->episode->id);
})
->where('`published_at` <= NOW()', null, false)
@ -254,4 +256,57 @@ class EpisodeController extends BaseController
->setHeader('Access-Control-Allow-Origin', '*')
->setBody($collection->toJSON());
}
/**
* @noRector ReturnTypeDeclarationRector
*/
public function comment(string $commentId): Response
{
if (
($comment = (new CommentModel())->getCommentById($commentId)) === null
) {
throw PageNotFoundException::forPageNotFound();
}
$commentObject = new CommentObject($comment);
return $this->response
->setContentType('application/json')
->setBody($commentObject->toJSON());
}
public function commentReplies(string $commentId): Response
{
/**
* get comment replies
*/
$commentReplies = model('CommentModel', false)
->where('in_reply_to_id', service('uuid')->fromString($commentId)->getBytes())
->orderBy('created_at', 'ASC');
$pageNumber = (int) $this->request->getGet('page');
if ($pageNumber < 1) {
$commentReplies->paginate(12);
$pager = $commentReplies->pager;
$collection = new OrderedCollectionObject(null, $pager);
} else {
$paginatedReplies = $commentReplies->paginate(12, 'default', $pageNumber);
$pager = $commentReplies->pager;
$orderedItems = [];
if ($paginatedReplies !== null) {
foreach ($paginatedReplies as $reply) {
$replyObject = new CommentObject($reply);
$orderedItems[] = $replyObject;
}
}
$collection = new OrderedCollectionPage($pager, $orderedItems);
}
return $this->response
->setContentType('application/activity+json')
->setBody($collection->toJSON());
}
}

View File

@ -18,7 +18,7 @@ use App\Libraries\PodcastActor;
use App\Libraries\PodcastEpisode;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use App\Models\StatusModel;
use App\Models\PostModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\Response;
@ -81,7 +81,7 @@ class PodcastController extends BaseController
if (! ($cachedView = cache($cacheName))) {
$data = [
'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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
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
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
@ -14,23 +14,23 @@ namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddEpisodeIdToStatuses extends Migration
class AddEpisodeIdToPosts extends Migration
{
public function up(): void
{
$prefix = $this->db->getPrefix();
$createQuery = <<<CODE_SAMPLE
ALTER TABLE {$prefix}activitypub_statuses
ALTER TABLE {$prefix}activitypub_posts
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;
$this->db->query($createQuery);
}
public function down(): void
{
$this->forge->dropForeignKey('activitypub_statuses', 'activitypub_statuses_episode_id_foreign');
$this->forge->dropColumn('activitypub_statuses', 'episode_id');
$this->forge->dropForeignKey('activitypub_posts', 'activitypub_posts_episode_id_foreign');
$this->forge->dropColumn('activitypub_posts', 'episode_id');
}
}

View File

@ -3,7 +3,7 @@
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
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
@ -14,23 +14,23 @@ namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddCreatedByToStatuses extends Migration
class AddCreatedByToPosts extends Migration
{
public function up(): void
{
$prefix = $this->db->getPrefix();
$createQuery = <<<CODE_SAMPLE
ALTER TABLE {$prefix}activitypub_statuses
ALTER TABLE {$prefix}activitypub_posts
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;
$this->db->query($createQuery);
}
public function down(): void
{
$this->forge->dropForeignKey('activitypub_statuses', 'activitypub_statuses_created_by_foreign');
$this->forge->dropColumn('activitypub_statuses', 'created_by');
$this->forge->dropForeignKey('activitypub_posts', 'activitypub_posts_created_by_foreign');
$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',
'description' =>
'Publish / unpublish episodes & statuses of a podcast',
'Publish / unpublish episodes & posts of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'interact_as',
'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'],
],
],

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

View File

@ -10,7 +10,7 @@ declare(strict_types=1);
namespace App\Entities;
use ActivityPub\Entities\Status as ActivityPubStatus;
use ActivityPub\Entities\Post as ActivityPubPost;
use App\Models\EpisodeModel;
use RuntimeException;
@ -18,7 +18,7 @@ use RuntimeException;
* @property int|null $episode_id
* @property Episode|null $episode
*/
class Status extends ActivityPubStatus
class Post extends ActivityPubPost
{
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
{
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) {

View File

@ -255,7 +255,7 @@ if (! function_exists('publication_button')) {
/**
* 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
{

View File

@ -40,7 +40,7 @@ if (! function_exists('extract_params_from_episode_uri')) {
function extract_params_from_episode_uri(URI $episodeUri): ?array
{
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(),
$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_abbr' => 'S{seasonNumber}E{episodeNumber}',
'back_to_episodes' => 'Back to episodes of {podcast}',
'comments' => 'Comments',
'activity' => 'Activity',
'description' => 'Description',
'total_favourites' => '{numberOfTotalFavourites, plural,
one {# total favourite}
other {# total favourites}
}',
'total_reblogs' => '{numberOfTotalReblogs, plural,
one {# total share}
other {# total shares}
}',
'total_statuses' => '{numberOfTotalStatuses, plural,
one {# total post}
other {# total posts}
'number_of_comments' => '{numberOfComments, plural,
one {# comment}
other {# comments}
}',
'all_podcast_episodes' => 'All podcast episodes',
'back_to_podcast' => 'Go back to podcast',
@ -116,14 +109,14 @@ return [
'custom_rss_hint' => 'This will be injected within the ❬item❭ tag.',
'block' => 'Episode should be hidden from all platforms',
'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_edit' => 'Save episode',
],
'publish_form' => [
'back_to_episode_dashboard' => 'Back to episode dashboard',
'status' => 'Your announcement post',
'status_hint' =>
'post' => 'Your announcement post',
'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.",
'publication_date' => 'Publication date',
'publication_method' => [

View File

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

View File

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

View File

@ -16,19 +16,12 @@ return [
'season_episode' => 'Saison {seasonNumber} épisode {episodeNumber}',
'season_episode_abbr' => 'S{seasonNumber}E{episodeNumber}',
'back_to_episodes' => 'Retour aux épisodes de {podcast}',
'comments' => 'Commentaires',
'activity' => 'Activité',
'description' => 'Description',
'total_favourites' => '{numberOfTotalFavourites, plural,
one {# favori en tout}
other {# favoris en tout}
}',
'total_reblogs' => '{numberOfTotalReblogs, plural,
one {# partage en tout}
other {# partages en tout}
}',
'total_statuses' => '{numberOfTotalStatuses, plural,
one {# message}
other {# messages}
'number_of_comments' => '{numberOfComments, plural,
one {# commentaire}
other {# commentaires}
}',
'all_podcast_episodes' => 'Tous les épisodes du podcast',
'back_to_podcast' => 'Revenir au podcast',
@ -125,8 +118,8 @@ return [
],
'publish_form' => [
'back_to_episode_dashboard' => 'Retour au tableau de bord de lépisode',
'status' => 'Votre message de publication',
'status_hint' =>
'post' => 'Votre message de publication',
'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.',
'publication_date' => '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}
other {<span class="font-semibold">#</span> abonné·e·s}
}',
'statuses' => '{numberOfStatuses, plural,
'posts' => '{numberOfPosts, plural,
one {<span class="font-semibold">#</span> publication}
other {<span class="font-semibold">#</span> publications}
}',

View File

@ -10,7 +10,7 @@ declare(strict_types=1);
return [
'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é',
'reply_to' => 'Répondre à @{actorUsername}',
'form' => [

View File

@ -14,19 +14,19 @@ declare(strict_types=1);
namespace ActivityPub\Activities;
use ActivityPub\Core\Activity;
use ActivityPub\Entities\Status;
use ActivityPub\Entities\Post;
class AnnounceActivity extends Activity
{
protected string $type = 'Announce';
public function __construct(Status $reblogStatus)
public function __construct(Post $reblogPost)
{
$this->actor = $reblogStatus->actor->uri;
$this->object = $reblogStatus->reblog_of_status->uri;
$this->actor = $reblogPost->actor->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',
'[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
@ -54,24 +54,24 @@ $routes->group('', [
]);
});
// Status
$routes->post('statuses/new', 'StatusController::attemptCreate/$1', [
'as' => 'status-attempt-create',
// Post
$routes->post('posts/new', 'PostController::attemptCreate/$1', [
'as' => 'post-attempt-create',
]);
$routes->get('statuses/(:uuid)', 'StatusController/$1', [
'as' => 'status',
$routes->get('posts/(:uuid)', 'PostController/$1', [
'as' => 'post',
]);
$routes->get('statuses/(:uuid)/replies', 'StatusController/$1', [
'as' => 'status-replies',
$routes->get('posts/(:uuid)/replies', 'PostController/$1', [
'as' => 'post-replies',
]);
$routes->post(
'statuses/(:uuid)/remote/(:statusAction)',
'StatusController::attemptRemoteAction/$1/$2/$3',
'posts/(:uuid)/remote/(:postAction)',
'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\Entities\Actor;
use ActivityPub\Entities\Status;
use ActivityPub\Entities\Post;
use ActivityPub\Objects\OrderedCollectionObject;
use ActivityPub\Objects\OrderedCollectionPage;
use CodeIgniter\Controller;
@ -101,30 +101,30 @@ class ActorController extends Controller
->setJSON([]);
}
$replyToStatus = model('StatusModel')
->getStatusByUri($payload->object->inReplyTo);
$replyToPost = model('PostModel')
->getPostByUri($payload->object->inReplyTo);
$reply = null;
if ($replyToStatus !== null) {
if ($replyToPost !== null) {
// TODO: strip content from html to retrieve message
// remove all html tags and reconstruct message with mentions?
extract_text_from_html($payload->object->content);
$reply = new Status([
$reply = new Post([
'uri' => $payload->object->id,
'actor_id' => $payloadActor->id,
'in_reply_to_id' => $replyToStatus->id,
'in_reply_to_id' => $replyToPost->id,
'message' => $payload->object->content,
'published_at' => Time::parse($payload->object->published),
]);
}
if ($reply !== null) {
$statusId = model('StatusModel')
$postId = model('PostModel')
->addReply($reply, true, false);
model('ActivityModel')
->update($activityId, [
'status_id' => $statusId,
'post_id' => $postId,
]);
}
@ -135,12 +135,12 @@ class ActorController extends Controller
return $this->response->setStatusCode(501)
->setJSON([]);
case 'Delete':
$statusToDelete = model('StatusModel')
->getStatusByUri($payload->object->id);
$postToDelete = model('PostModel')
->getPostByUri($payload->object->id);
if ($statusToDelete !== null) {
model('StatusModel')
->removeStatus($statusToDelete, false);
if ($postToDelete !== null) {
model('PostModel')
->removePost($postToDelete, false);
}
return $this->response->setStatusCode(200)
@ -158,35 +158,35 @@ class ActorController extends Controller
->setJSON([]);
case 'Like':
// get favourited status
$status = model('StatusModel')
->getStatusByUri($payload->object);
// get favourited post
$post = model('PostModel')
->getPostByUri($payload->object);
if ($status !== null) {
if ($post !== null) {
// Like side-effect
model('FavouriteModel')
->addFavourite($payloadActor, $status, false);
->addFavourite($payloadActor, $post, false);
model('ActivityModel')
->update($activityId, [
'status_id' => $status->id,
'post_id' => $post->id,
]);
}
return $this->response->setStatusCode(200)
->setJSON([]);
case 'Announce':
$status = model('StatusModel')
->getStatusByUri($payload->object);
$post = model('PostModel')
->getPostByUri($payload->object);
if ($status !== null) {
if ($post !== null) {
model('ActivityModel')
->update($activityId, [
'status_id' => $status->id,
'post_id' => $post->id,
]);
model('StatusModel')
->reblog($payloadActor, $status, false);
model('PostModel')
->reblog($payloadActor, $post, false);
}
return $this->response->setStatusCode(200)
@ -204,45 +204,45 @@ class ActorController extends Controller
return $this->response->setStatusCode(202)
->setJSON([]);
case 'Like':
$status = model('StatusModel')
->getStatusByUri($payload->object->object);
$post = model('PostModel')
->getPostByUri($payload->object->object);
if ($status !== null) {
if ($post !== null) {
// revert side-effect by removing favourite from database
model('FavouriteModel')
->removeFavourite($payloadActor, $status, false);
->removeFavourite($payloadActor, $post, false);
model('ActivityModel')
->update($activityId, [
'status_id' => $status->id,
'post_id' => $post->id,
]);
}
return $this->response->setStatusCode(200)
->setJSON([]);
case 'Announce':
$status = model('StatusModel')
->getStatusByUri($payload->object->object);
$post = model('PostModel')
->getPostByUri($payload->object->object);
$reblogStatus = null;
if ($status !== null) {
$reblogStatus = model('StatusModel')
$reblogPost = null;
if ($post !== null) {
$reblogPost = model('PostModel')
->where([
'actor_id' => $payloadActor->id,
'reblog_of_id' => service('uuid')
->fromString($status->id)
->fromString($post->id)
->getBytes(),
])
->first();
}
if ($reblogStatus !== null) {
model('StatusModel')
->undoReblog($reblogStatus, false);
if ($reblogPost !== null) {
model('PostModel')
->undoReblog($reblogPost, false);
model('ActivityModel')
->update($activityId, [
'status_id' => $status->id,
'post_id' => $post->id,
]);
}

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Class AddStatuses Creates activitypub_statuses table in database
* Class AddPosts Creates activitypub_posts table in database
*
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
@ -14,7 +14,7 @@ namespace ActivityPub\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddStatuses extends Migration
class AddPosts extends Migration
{
public function up(): void
{
@ -25,7 +25,7 @@ class AddStatuses extends Migration
],
'uri' => [
'type' => 'VARCHAR',
'constraint' => 191,
'constraint' => 255,
],
'actor_id' => [
'type' => 'INT',
@ -76,16 +76,16 @@ class AddStatuses extends Migration
]);
$this->forge->addPrimaryKey('id');
$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->addForeignKey('actor_id', 'activitypub_actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('in_reply_to_id', 'activitypub_statuses', 'id', '', 'CASCADE');
$this->forge->addForeignKey('reblog_of_id', 'activitypub_statuses', 'id', '', 'CASCADE');
$this->forge->createTable('activitypub_statuses');
$this->forge->addForeignKey('in_reply_to_id', 'activitypub_posts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('reblog_of_id', 'activitypub_posts', 'id', '', 'CASCADE');
$this->forge->createTable('activitypub_posts');
}
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,
'null' => true,
],
'status_id' => [
'post_id' => [
'type' => 'BINARY',
'constraint' => 16,
'null' => true,
@ -62,7 +62,7 @@ class AddActivities extends Migration
$this->forge->addPrimaryKey('id');
$this->forge->addForeignKey('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');
}

View File

@ -23,15 +23,15 @@ class AddFavourites extends Migration
'type' => 'INT',
'unsigned' => true,
],
'status_id' => [
'post_id' => [
'type' => 'BINARY',
'constraint' => 16,
],
]);
$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('status_id', 'activitypub_statuses', 'id', '', 'CASCADE');
$this->forge->addForeignKey('post_id', 'activitypub_posts', 'id', '', 'CASCADE');
$this->forge->createTable('activitypub_favourites');
}

View File

@ -3,7 +3,7 @@
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
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
@ -14,12 +14,12 @@ namespace ActivityPub\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddStatusesPreviewCards extends Migration
class AddPostsPreviewCards extends Migration
{
public function up(): void
{
$this->forge->addField([
'status_id' => [
'post_id' => [
'type' => 'BINARY',
'constraint' => 16,
],
@ -29,14 +29,14 @@ class AddStatusesPreviewCards extends Migration
],
]);
$this->forge->addPrimaryKey(['status_id', 'preview_card_id']);
$this->forge->addForeignKey('status_id', 'activitypub_statuses', 'id', '', 'CASCADE');
$this->forge->addPrimaryKey(['post_id', 'preview_card_id']);
$this->forge->addForeignKey('post_id', 'activitypub_posts', '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
{
$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([
'name' => [
'type' => 'VARCHAR',
'constraint' => 191,
'constraint' => 255,
],
'created_at' => [
'type' => 'DATETIME',

View File

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

View File

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

View File

@ -20,9 +20,9 @@ use RuntimeException;
* @property int $actor_id
* @property Actor $actor
* @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 Status|null $reblog_of_status
* @property Post|null $reblog_of_post
* @property string $message
* @property string $message_html
* @property int $favourites_count
@ -35,30 +35,30 @@ use RuntimeException;
* @property PreviewCard|null $preview_card
*
* @property bool $has_replies
* @property Status[] $replies
* @property Status[] $reblogs
* @property Post[] $replies
* @property Post[] $reblogs
*/
class Status extends UuidEntity
class Post extends UuidEntity
{
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 bool $has_preview_card = false;
/**
* @var Status[]|null
* @var Post[]|null
*/
protected ?array $replies = null;
protected bool $has_replies = false;
/**
* @var Status[]|null
* @var Post[]|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
{
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) {
@ -108,12 +108,12 @@ class Status extends UuidEntity
public function getPreviewCard(): ?PreviewCard
{
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) {
$this->preview_card = model('PreviewCardModel', false)
->getStatusPreviewCard($this->id);
->getPostPreviewCard($this->id);
}
return $this->preview_card;
@ -125,17 +125,17 @@ class Status extends UuidEntity
}
/**
* @return Status[]
* @return Post[]
*/
public function getReplies(): array
{
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) {
$this->replies = (array) model('StatusModel', false)
->getStatusReplies($this->id);
$this->replies = (array) model('PostModel', false)
->getPostReplies($this->id);
}
return $this->replies;
@ -146,49 +146,49 @@ class Status extends UuidEntity
return $this->getReplies() !== null;
}
public function getReplyToStatus(): ?self
public function getReplyToPost(): ?self
{
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) {
$this->reply_to_status = model('StatusModel', false)
->getStatusById($this->in_reply_to_id);
if ($this->reply_to_post === null) {
$this->reply_to_post = model('PostModel', false)
->getPostById($this->in_reply_to_id);
}
return $this->reply_to_status;
return $this->reply_to_post;
}
/**
* @return Status[]
* @return Post[]
*/
public function getReblogs(): array
{
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) {
$this->reblogs = (array) model('StatusModel', false)
->getStatusReblogs($this->id);
$this->reblogs = (array) model('PostModel', false)
->getPostReblogs($this->id);
}
return $this->reblogs;
}
public function getReblogOfStatus(): ?self
public function getReblogOfPost(): ?self
{
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) {
$this->reblog_of_status = model('StatusModel', false)
->getStatusById($this->reblog_of_id);
if ($this->reblog_of_post === null) {
$this->reblog_of_post = model('PostModel', false)
->getPostById($this->reblog_of_id);
}
return $this->reblog_of_status;
return $this->reblog_of_post;
}
public function setMessage(string $message): static

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ use ActivityPub\Activities\LikeActivity;
use ActivityPub\Activities\UndoActivity;
use ActivityPub\Entities\Actor;
use ActivityPub\Entities\Favourite;
use ActivityPub\Entities\Status;
use ActivityPub\Entities\Post;
use CodeIgniter\Events\Events;
use Michalsn\Uuid\UuidModel;
@ -28,12 +28,12 @@ class FavouriteModel extends UuidModel
/**
* @var string[]
*/
protected $uuidFields = ['status_id'];
protected $uuidFields = ['post_id'];
/**
* @var string[]
*/
protected $allowedFields = ['actor_id', 'status_id'];
protected $allowedFields = ['actor_id', 'post_id'];
/**
* @var string
@ -47,32 +47,32 @@ class FavouriteModel extends UuidModel
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->insert([
'actor_id' => $actor->id,
'status_id' => $status->id,
'post_id' => $post->id,
]);
model('StatusModel')
->where('id', service('uuid') ->fromString($status->id) ->getBytes())
model('PostModel')
->where('id', service('uuid') ->fromString($post->id) ->getBytes())
->increment('favourites_count');
if ($registerActivity) {
$likeActivity = new LikeActivity();
$likeActivity->set('actor', $actor->uri)
->set('object', $status->uri);
->set('object', $post->uri);
$activityId = model('ActivityModel')
->newActivity(
'Like',
$actor->id,
null,
$status->id,
$post->id,
$likeActivity->toJSON(),
$status->published_at,
$post->published_at,
'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')
->clearCache($status);
model('PostModel')
->clearCache($post);
$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();
model('StatusModel')
->where('id', service('uuid') ->fromString($status->id) ->getBytes())
model('PostModel')
->where('id', service('uuid') ->fromString($post->id) ->getBytes())
->decrement('favourites_count');
$this->db
->table('activitypub_favourites')
->where([
'actor_id' => $actor->id,
'status_id' => service('uuid')
->fromString($status->id)
'post_id' => service('uuid')
->fromString($post->id)
->getBytes(),
])
->delete();
@ -117,8 +117,8 @@ class FavouriteModel extends UuidModel
->where([
'type' => 'Like',
'actor_id' => $actor->id,
'status_id' => service('uuid')
->fromString($status->id)
'post_id' => service('uuid')
->fromString($post->id)
->getBytes(),
])
->first();
@ -127,7 +127,7 @@ class FavouriteModel extends UuidModel
$likeActivity
->set('id', url_to('activity', $actor->username, $activity->id))
->set('actor', $actor->uri)
->set('object', $status->uri);
->set('object', $post->uri);
$undoActivity
->set('actor', $actor->uri)
@ -138,9 +138,9 @@ class FavouriteModel extends UuidModel
'Undo',
$actor->id,
null,
$status->id,
$post->id,
$undoActivity->toJSON(),
$status->published_at,
$post->published_at,
'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')
->clearCache($status);
model('PostModel')
->clearCache($post);
$this->db->transComplete();
}
@ -163,19 +163,19 @@ class FavouriteModel extends UuidModel
/**
* 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 (
$this->where([
'actor_id' => $actor->id,
'status_id' => service('uuid')
->fromString($status->id)
'post_id' => service('uuid')
->fromString($post->id)
->getBytes(),
])->first()
) {
$this->removeFavourite($actor, $status);
$this->removeFavourite($actor, $post);
} 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\UndoActivity;
use ActivityPub\Entities\Actor;
use ActivityPub\Entities\Status;
use ActivityPub\Entities\Post;
use ActivityPub\Objects\TombstoneObject;
use CodeIgniter\Database\BaseResult;
use CodeIgniter\Database\Query;
@ -25,12 +25,12 @@ use CodeIgniter\I18n\Time;
use Exception;
use Michalsn\Uuid\UuidModel;
class StatusModel extends UuidModel
class PostModel extends UuidModel
{
/**
* @var string
*/
protected $table = 'activitypub_statuses';
protected $table = 'activitypub_posts';
/**
* @var string
@ -62,7 +62,7 @@ class StatusModel extends UuidModel
/**
* @var string
*/
protected $returnType = Status::class;
protected $returnType = Post::class;
/**
* @var bool
@ -87,14 +87,14 @@ class StatusModel extends UuidModel
/**
* @var string[]
*/
protected $beforeInsert = ['setStatusId'];
protected $beforeInsert = ['setPostId'];
public function getStatusById(string $statusId): ?Status
public function getPostById(string $postId): ?Post
{
$cacheName = config('ActivityPub')
->cachePrefix . "status#{$statusId}";
->cachePrefix . "post#{$postId}";
if (! ($found = cache($cacheName))) {
$found = $this->find($statusId);
$found = $this->find($postId);
cache()
->save($cacheName, $found, DECADE);
@ -103,14 +103,14 @@ class StatusModel extends UuidModel
return $found;
}
public function getStatusByUri(string $statusUri): ?Status
public function getPostByUri(string $postUri): ?Post
{
$hashedStatusUri = md5($statusUri);
$hashedPostUri = md5($postUri);
$cacheName =
config('ActivityPub')
->cachePrefix . "status-{$hashedStatusUri}";
->cachePrefix . "post-{$hashedPostUri}";
if (! ($found = cache($cacheName))) {
$found = $this->where('uri', $statusUri)
$found = $this->where('uri', $postUri)
->first();
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 =
config('ActivityPub')
->cachePrefix .
"actor#{$actorId}_published_statuses";
"actor#{$actorId}_published_posts";
if (! ($found = cache($cacheName))) {
$found = $this->where([
'actor_id' => $actorId,
@ -140,20 +140,20 @@ class StatusModel extends UuidModel
->orderBy('published_at', 'DESC')
->findAll();
$secondsToNextUnpublishedStatus = $this->getSecondsToNextUnpublishedStatuses($actorId);
$secondsToNextUnpublishedPost = $this->getSecondsToNextUnpublishedPosts($actorId);
cache()
->save($cacheName, $found, $secondsToNextUnpublishedStatus ? $secondsToNextUnpublishedStatus : DECADE);
->save($cacheName, $found, $secondsToNextUnpublishedPost ? $secondsToNextUnpublishedPost : DECADE);
}
return $found;
}
/**
* Returns the timestamp difference in seconds between the next status to publish and the current timestamp. Returns
* false if there's no status to publish
* Returns the timestamp difference in seconds between the next post to publish and the current timestamp. Returns
* 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')
->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 =
config('ActivityPub')
->cachePrefix .
"status#{$statusId}_replies" .
"post#{$postId}_replies" .
($withBlocked ? '_withBlocked' : '');
if (! ($found = cache($cacheName))) {
if (! $withBlocked) {
$this->select('activitypub_statuses.*')
->join('activitypub_actors', 'activitypub_actors.id = activitypub_statuses.actor_id', 'inner')
$this->select('activitypub_posts.*')
->join('activitypub_actors', 'activitypub_actors.id = activitypub_posts.actor_id', 'inner')
->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)
->orderBy('published_at', 'ASC');
$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 =
config('ActivityPub')
->cachePrefix . "status#{$statusId}_reblogs";
->cachePrefix . "post#{$postId}_reblogs";
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)
->orderBy('published_at', 'ASC')
->findAll();
@ -225,23 +225,23 @@ class StatusModel extends UuidModel
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([
'status_id' => $this->uuid->fromString($statusId)
'post_id' => $this->uuid->fromString($postId)
->getBytes(),
'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(
Status $status,
public function addPost(
Post $post,
bool $createPreviewCard = true,
bool $registerActivity = true
): string | false {
@ -249,101 +249,101 @@ class StatusModel extends UuidModel
$this->db->transStart();
if (! ($newStatusId = $this->insert($status, true))) {
if (! ($newPostId = $this->insert($post, true))) {
$this->db->transRollback();
// Couldn't insert status
// Couldn't insert post
return false;
}
if ($createPreviewCard) {
// parse message
$messageUrls = extract_urls_from_message($status->message);
$messageUrls = extract_urls_from_message($post->message);
if (
$messageUrls !== [] &&
($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();
// problem when linking status to preview card
// problem when linking post to preview card
return false;
}
}
model('ActorModel')
->where('id', $status->actor_id)
->increment('statuses_count');
model('ActorModel', false)
->where('id', $post->actor_id)
->increment('posts_count');
if ($registerActivity) {
// set status id and uri to construct NoteObject
$status->id = $newStatusId;
$status->uri = url_to('status', $status->actor->username, $newStatusId);
// set post id and uri to construct NoteObject
$post->id = $newPostId;
$post->uri = url_to('post', $post->actor->username, $newPostId);
$createActivity = new CreateActivity();
$noteObjectClass = config('ActivityPub')
->noteObject;
$createActivity
->set('actor', $status->actor->uri)
->set('object', new $noteObjectClass($status));
->set('actor', $post->actor->uri)
->set('object', new $noteObjectClass($post));
$activityId = model('ActivityModel')
$activityId = model('ActivityModel', false)
->newActivity(
'Create',
$status->actor_id,
$post->actor_id,
null,
$newStatusId,
$newPostId,
$createActivity->toJSON(),
$status->published_at,
$post->published_at,
'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, [
'payload' => $createActivity->toJSON(),
]);
}
Events::trigger('on_status_add', $status);
Events::trigger('on_post_add', $post);
$this->clearCache($status);
$this->clearCache($post);
$this->db->transComplete();
return $newStatusId;
return $newPostId;
}
public function editStatus(Status $updatedStatus): bool
public function editPost(Post $updatedPost): bool
{
$this->db->transStart();
// update status create activity schedule in database
$scheduledActivity = model('ActivityModel')
// update post create activity schedule in database
$scheduledActivity = model('ActivityModel', false)
->where([
'type' => 'Create',
'status_id' => $this->uuid
->fromString($updatedStatus->id)
'post_id' => $this->uuid
->fromString($updatedPost->id)
->getBytes(),
])
->first();
// update published date in payload
$newPayload = $scheduledActivity->payload;
$newPayload->object->published = $updatedStatus->published_at->format(DATE_W3C);
model('ActivityModel')
$newPayload->object->published = $updatedPost->published_at->format(DATE_W3C);
model('ActivityModel', false)
->update($scheduledActivity->id, [
'payload' => json_encode($newPayload, JSON_THROW_ON_ERROR),
'scheduled_at' => $updatedStatus->published_at,
'scheduled_at' => $updatedPost->published_at,
]);
// update status
$updateResult = $this->update($updatedStatus->id, $updatedStatus);
// update post
$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();
@ -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();
model('ActorModel')
->where('id', $status->actor_id)
->decrement('statuses_count');
model('ActorModel', false)
->where('id', $post->actor_id)
->decrement('posts_count');
if ($status->in_reply_to_id !== null) {
// Status to remove is a reply
model('StatusModel')
->where('id', $this->uuid->fromString($status->in_reply_to_id) ->getBytes())
if ($post->in_reply_to_id !== null) {
// Post to remove is a reply
model('PostModel', false)
->where('id', $this->uuid->fromString($post->in_reply_to_id) ->getBytes())
->decrement('replies_count');
Events::trigger('on_reply_remove', $status);
Events::trigger('on_reply_remove', $post);
}
// remove all status reblogs
foreach ($status->reblogs as $reblog) {
// remove all post reblogs
foreach ($post->reblogs as $reblog) {
// FIXME: issue when actor is not local, can't get actor information
$this->removeStatus($reblog);
$this->removePost($reblog);
}
// remove all status replies
foreach ($status->replies as $reply) {
$this->removeStatus($reply);
// remove all post replies
foreach ($post->replies as $reply) {
$this->removePost($reply);
}
// check that preview card is no longer used elsewhere before deleting it
if (
$status->preview_card &&
$post->preview_card &&
$this->db
->table('activitypub_statuses_preview_cards')
->where('preview_card_id', $status->preview_card->id)
->table('activitypub_posts_preview_cards')
->where('preview_card_id', $post->preview_card->id)
->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) {
$deleteActivity = new DeleteActivity();
$tombstoneObject = new TombstoneObject();
$tombstoneObject->set('id', $status->uri);
$tombstoneObject->set('id', $post->uri);
$deleteActivity
->set('actor', $status->actor->uri)
->set('actor', $post->actor->uri)
->set('object', $tombstoneObject);
$activityId = model('ActivityModel')
$activityId = model('ActivityModel', false)
->newActivity(
'Delete',
$status->actor_id,
$post->actor_id,
null,
null,
$deleteActivity->toJSON(),
@ -411,20 +411,20 @@ class StatusModel extends UuidModel
'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, [
'payload' => $deleteActivity->toJSON(),
]);
}
$result = model('StatusModel', false)
->delete($status->id);
$result = model('PostModel', false)
->delete($post->id);
Events::trigger('on_status_remove', $status);
Events::trigger('on_post_remove', $post);
$this->clearCache($status);
$this->clearCache($post);
$this->db->transComplete();
@ -432,182 +432,182 @@ class StatusModel extends UuidModel
}
public function addReply(
Status $reply,
Post $reply,
bool $createPreviewCard = true,
bool $registerActivity = true
): string | false {
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();
$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())
->increment('replies_count');
Events::trigger('on_status_reply', $reply);
Events::trigger('on_post_reply', $reply);
$this->clearCache($reply);
$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();
$reblog = new Status([
$reblog = new Post([
'actor_id' => $actor->id,
'reblog_of_id' => $status->id,
'reblog_of_id' => $post->id,
'published_at' => Time::now(),
]);
// add reblog
$reblogId = $this->insert($reblog);
model('ActorModel')
model('ActorModel', false)
->where('id', $actor->id)
->increment('statuses_count');
->increment('posts_count');
model('StatusModel')
->where('id', $this->uuid->fromString($status->id)->getBytes())
model('PostModel', false)
->where('id', $this->uuid->fromString($post->id)->getBytes())
->increment('reblogs_count');
if ($registerActivity) {
$announceActivity = new AnnounceActivity($reblog);
$activityId = model('ActivityModel')
$activityId = model('ActivityModel', false)
->newActivity(
'Announce',
$actor->id,
null,
$status->id,
$post->id,
$announceActivity->toJSON(),
$reblog->published_at,
'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, [
'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();
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();
model('ActorModel')
->where('id', $reblogStatus->actor_id)
->decrement('statuses_count');
model('ActorModel', false)
->where('id', $reblogPost->actor_id)
->decrement('posts_count');
model('StatusModel')
->where('id', $this->uuid->fromString($reblogStatus->reblog_of_id) ->getBytes())
model('PostModel', false)
->where('id', $this->uuid->fromString($reblogPost->reblog_of_id) ->getBytes())
->decrement('reblogs_count');
if ($registerActivity) {
$undoActivity = new UndoActivity();
// get like activity
$activity = model('ActivityModel')
$activity = model('ActivityModel', false)
->where([
'type' => 'Announce',
'actor_id' => $reblogStatus->actor_id,
'status_id' => $this->uuid
->fromString($reblogStatus->reblog_of_id)
'actor_id' => $reblogPost->actor_id,
'post_id' => $this->uuid
->fromString($reblogPost->reblog_of_id)
->getBytes(),
])
->first();
$announceActivity = new AnnounceActivity($reblogStatus);
$announceActivity->set('id', url_to('activity', $reblogStatus->actor->username, $activity->id),);
$announceActivity = new AnnounceActivity($reblogPost);
$announceActivity->set('id', url_to('activity', $reblogPost->actor->username, $activity->id),);
$undoActivity
->set('actor', $reblogStatus->actor->uri)
->set('actor', $reblogPost->actor->uri)
->set('object', $announceActivity);
$activityId = model('ActivityModel')
$activityId = model('ActivityModel', false)
->newActivity(
'Undo',
$reblogStatus->actor_id,
$reblogPost->actor_id,
null,
$reblogStatus->reblog_of_id,
$reblogPost->reblog_of_id,
$undoActivity->toJSON(),
Time::now(),
'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, [
'payload' => $undoActivity->toJSON(),
]);
}
$result = model('StatusModel', false)
->delete($reblogStatus->id);
$result = model('PostModel', false)
->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();
return $result;
}
public function toggleReblog(Actor $actor, Status $status): void
public function toggleReblog(Actor $actor, Post $post): void
{
if (
! ($reblogStatus = $this->where([
! ($reblogPost = $this->where([
'actor_id' => $actor->id,
'reblog_of_id' => $this->uuid
->fromString($status->id)
->fromString($post->id)
->getBytes(),
])->first())
) {
$this->reblog($actor, $status);
$this->reblog($actor, $post);
} else {
$this->undoReblog($reblogStatus);
$this->undoReblog($reblogPost);
}
}
public function clearCache(Status $status): void
public function clearCache(Post $post): void
{
$cachePrefix = config('ActivityPub')
->cachePrefix;
$hashedStatusUri = md5($status->uri);
$hashedPostUri = md5($post->uri);
model('ActorModel')
->clearCache($status->actor);
model('ActorModel', false)
->clearCache($post->actor);
cache()
->deleteMatching($cachePrefix . "status#{$status->id}*");
->deleteMatching($cachePrefix . "post#{$post->id}*");
cache()
->deleteMatching($cachePrefix . "status-{$hashedStatusUri}*");
->deleteMatching($cachePrefix . "post-{$hashedPostUri}*");
if ($status->in_reply_to_id !== null) {
$this->clearCache($status->reply_to_status);
if ($post->in_reply_to_id !== null) {
$this->clearCache($post->reply_to_post);
}
if ($status->reblog_of_id !== null) {
$this->clearCache($status->reblog_of_status);
if ($post->reblog_of_id !== null) {
$this->clearCache($post->reblog_of_post);
}
}
@ -615,16 +615,16 @@ class StatusModel extends UuidModel
* @param array<string, array<string|int, mixed>> $data
* @return array<string, array<string|int, mixed>>
*/
protected function setStatusId(array $data): array
protected function setPostId(array $data): array
{
$uuid4 = $this->uuid->{$this->uuidVersion}();
$data['data']['id'] = $uuid4->toString();
if (! isset($data['data']['uri'])) {
$actor = model('ActorModel')
$actor = model('ActorModel', false)
->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;

View File

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

View File

@ -15,7 +15,7 @@ declare(strict_types=1);
namespace ActivityPub\Objects;
use ActivityPub\Core\ObjectType;
use ActivityPub\Entities\Status;
use ActivityPub\Entities\Post;
class NoteObject extends ObjectType
{
@ -27,20 +27,20 @@ class NoteObject extends ObjectType
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->published = $status->published_at->format(DATE_W3C);
$this->attributedTo = $status->actor->uri;
$this->content = $post->message_html;
$this->published = $post->published_at->format(DATE_W3C);
$this->attributedTo = $post->actor->uri;
if ($status->in_reply_to_id !== null) {
$this->inReplyTo = $status->reply_to_status->uri;
if ($post->in_reply_to_id !== null) {
$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;
use ActivityPub\Objects\NoteObject as ActivityPubNoteObject;
use App\Entities\Status;
use App\Entities\Post;
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 =
'<a href="' .
$status->episode->link .
$post->episode->link .
'" target="_blank" rel="noopener noreferrer">' .
$status->episode->title .
$post->episode->title .
'</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_osm',
'custom_rss',
'favourites_total',
'reblogs_total',
'statuses_total',
'posts_count',
'comments_count',
'published_at',
'created_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 "./switch.css";
@import "./charts.css";
@import "./status.css";
@import "./post.css";
@import "./tabs.css";
@import "./radioToggler.css";
@import "./formInputTabs.css";

View File

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

View File

@ -1,6 +1,6 @@
@layer components {
.tabset {
@apply grid grid-cols-2;
@apply grid grid-cols-3;
}
.tabset > input[type="radio"] {
@ -11,9 +11,10 @@
@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: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;
}
@ -23,7 +24,7 @@
}
.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: "";
}
@ -32,6 +33,6 @@
}
.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'),
'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(
'Episode.publish_form.status',
'Episode.publish_form.post',
) ?></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="flex px-4 py-3">
<img src="<?= $podcast->actor->avatar_image_url ?>" alt="<?= $podcast

View File

@ -24,13 +24,13 @@
]) ?>
<?= csrf_field() ?>
<?= 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(
'Episode.publish_form.status',
'Episode.publish_form.post',
) ?></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="flex px-4 py-3">
<img src="<?= $podcast->actor->avatar_image_url ?>" alt="<?= $podcast->actor
@ -42,7 +42,7 @@
<span class="text-sm text-gray-500 truncate">@<?= $podcast
->actor->username ?></span>
</p>
<?= relative_time($status->published_at, 'text-xs text-gray-500') ?>
<?= relative_time($post->published_at, 'text-xs text-gray-500') ?>
</div>
</div>
<div class="px-4 mb-2">
@ -54,7 +54,7 @@
'placeholder' => 'Write your message...',
'autofocus' => ''
],
old('message', $status->message, false),
old('message', $post->message, false),
['rows' => 2],
) ?>
</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(
route_to('episode', $podcast->handle, $episode->slug),
icon('chat', 'text-xl mr-1 text-gray-400') .
$episode->statuses_total,
$episode->comments_count,
[
'class' =>
'inline-flex items-center hover:underline',
'title' => lang('Episode.total_statuses', [
'numberOfTotalStatuses' => $episode->statuses_total,
'inline-flex items-center hover:underline',
'title' => lang('Episode.number_of_comments', [
'numberOfComments' => $episode->comments_count,
]),
],
) ?>

View File

@ -50,8 +50,8 @@
<a href="<?= route_to(
'podcast-activity',
$podcast->handle,
) ?>" class="hover:underline"><?= lang('Podcast.statuses', [
'numberOfStatuses' => $podcast->actor->statuses_count,
) ?>" class="hover:underline"><?= lang('Podcast.posts', [
'numberOfPosts' => $podcast->actor->posts_count,
]) ?></a>
</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">
<form action="<?= route_to(
'status-attempt-action',
'post-attempt-action',
interact_as_actor()->username,
$status->id,
$post->id,
) ?>" method="POST" class="flex justify-around">
<?= csrf_field() ?>
<?= anchor(
route_to('status', $podcast->handle, $status->id),
icon('chat', 'text-2xl mr-1 text-gray-400') . $status->replies_count,
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('Status.replies', [
'numberOfReplies' => $status->replies_count,
'title' => lang('Post.replies', [
'numberOfReplies' => $post->replies_count,
]),
],
) ?>
<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') .
$status->reblogs_count ?></button>
$post->reblogs_count ?></button>
<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') .
$status->favourites_count ?></button>
<button id="<?= $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="<?= $status->id .
$post->favourites_count ?></button>
<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="<?= $post->id .
'-more-dropdown-menu' ?>" aria-label="<?= lang(
'Common.more',
) ?>" aria-haspopup="true" aria-expanded="false"><?= icon('more') ?>
</button>
</form>
<nav id="<?= $status->id .
'-more-dropdown-menu' ?>" class="flex flex-col py-2 text-sm bg-white border rounded-lg shadow" aria-labelledby="<?= $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="<?= $post->id .
'-more-dropdown' ?>" data-dropdown="menu" data-dropdown-placement="bottom">
<?= anchor(
route_to('status', $podcast->handle, $status->id),
lang('Status.expand'),
route_to('post', $podcast->handle, $post->id),
lang('Post.expand'),
[
'class' => 'px-4 py-1 hover:bg-gray-100',
],
) ?>
<form action="<?= route_to(
'status-attempt-block-actor',
'post-attempt-block-actor',
interact_as_actor()->username,
$status->id,
$post->id,
) ?>" method="POST">
<?= csrf_field() ?>
<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>
</form>
<form action="<?= route_to(
'status-attempt-block-domain',
'post-attempt-block-domain',
interact_as_actor()->username,
$status->id,
$post->id,
) ?>" method="POST">
<?= csrf_field() ?>
<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>
</form>
<?php if ($status->actor->is_local): ?>
<?php if ($post->actor->is_local): ?>
<hr class="my-2" />
<form action="<?= route_to(
'status-attempt-delete',
$status->actor->username,
$status->id,
'post-attempt-delete',
$post->actor->username,
$post->id,
) ?>" method="POST">
<?= csrf_field() ?>
<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>
</form>
<?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') ?>
<div class="-mt-2 overflow-hidden border-b border-l border-r status-replies rounded-b-xl">
<?= $this->include('podcast/_partials/post') ?>
<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">
<?= anchor_popup(
route_to('status-remote-action', $podcast->handle, $status->id, 'reply'),
lang('Status.reply_to', ['actorUsername' => $status->actor->username]),
route_to('post-remote-action', $podcast->handle, $post->id, 'reply'),
lang('Post.reply_to', ['actorUsername' => $post->actor->username]),
[
'class' =>
'text-center justify-center font-semibold rounded-full shadow relative z-10 px-4 py-2 w-full bg-rose-600 text-white inline-flex items-center hover:bg-rose-700',
@ -15,8 +15,8 @@
</div>
<?php if ($status->has_replies): ?>
<?php foreach ($status->replies as $reply): ?>
<?php if ($post->has_replies): ?>
<?php foreach ($post->replies as $reply): ?>
<?= view('podcast/_partials/reply', ['reply' => $reply]) ?>
<?php endforeach; ?>
<?php endif; ?>

View File

@ -1,7 +1,7 @@
<?= $this->include('podcast/_partials/status_authenticated') ?>
<div class="-mt-2 overflow-hidden border-b border-l border-r status-replies rounded-b-xl">
<?= $this->include('podcast/_partials/post_authenticated') ?>
<div class="-mt-2 overflow-hidden border-b border-l border-r post-replies rounded-b-xl">
<?= 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',
],
@ -16,8 +16,8 @@
'name' => 'message',
'class' => 'form-textarea mb-4 w-full',
'required' => 'required',
'placeholder' => lang('Status.form.reply_to_placeholder', [
'actorUsername' => $status->actor->username,
'placeholder' => lang('Post.form.reply_to_placeholder', [
'actorUsername' => $post->actor->username,
]),
],
old('message', '', false),
@ -26,7 +26,7 @@
],
) ?>
<?= button(
lang('Status.form.submit_reply'),
lang('Post.form.submit_reply'),
'',
['variant' => 'primary', 'size' => 'small'],
[
@ -39,8 +39,8 @@
</div>
<?= form_close() ?>
<?php if ($status->has_replies): ?>
<?php foreach ($status->replies as $reply): ?>
<?php if ($post->has_replies): ?>
<?php foreach ($post->replies as $reply): ?>
<?= view('podcast/_partials/reply_authenticated', [
'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(
'repeat',
'text-lg mr-2 text-gray-400',
) .
lang('Status.actor_shared', [
'actor' => $status->actor->display_name,
lang('Post.actor_shared', [
'actor' => $post->actor->display_name,
]) ?></p>
<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" />
<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="<?= $status->actor
->uri ?>" class="flex items-baseline hover:underline" <?= $status
<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"><?= $status->actor
<span class="mr-2 font-semibold truncate"><?= $post->actor
->display_name ?></span>
<span class="text-sm text-gray-500 truncate">@<?= $status->actor
<span class="text-sm text-gray-500 truncate">@<?= $post->actor
->username .
($status->actor->is_local
($post->actor->is_local
? ''
: '@' . $status->actor->domain) ?></span>
: '@' . $post->actor->domain) ?></span>
</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">
<?= relative_time($status->published_at) ?>
<?= relative_time($post->published_at) ?>
</a>
</div>
</header>
<div class="px-6 mb-4 status-content"><?= $status->message_html ?></div>
<?php if ($status->episode_id): ?>
<div class="px-6 mb-4 post-content"><?= $post->message_html ?></div>
<?php if ($post->episode_id): ?>
<?= 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', [
'preview_card' => $status->preview_card,
'preview_card' => $post->preview_card,
]) ?>
<?php endif; ?>
<?= $this->include('podcast/_partials/status_actions') ?>
<?= $this->include('podcast/_partials/post_actions') ?>
</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(
'repeat',
'text-lg mr-2 text-gray-400',
) .
lang('Status.actor_shared', [
'actor' => $status->actor->display_name,
lang('Post.actor_shared', [
'actor' => $post->actor->display_name,
]) ?></p>
<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" />
<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="<?= $status->actor
->uri ?>" class="flex items-baseline hover:underline" <?= $status
<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"><?= $status->actor
<span class="mr-2 font-semibold truncate"><?= $post->actor
->display_name ?></span>
<span class="text-sm text-gray-500 truncate">@<?= $status->actor
<span class="text-sm text-gray-500 truncate">@<?= $post->actor
->username .
($status->actor->is_local
($post->actor->is_local
? ''
: '@' . $status->actor->domain) ?></span>
: '@' . $post->actor->domain) ?></span>
</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">
<?= relative_time($status->published_at) ?>
<?= relative_time($post->published_at) ?>
</a>
</div>
</header>
<div class="px-6 mb-4 status-content"><?= $status->message_html ?></div>
<?php if ($status->episode_id): ?>
<div class="px-6 mb-4 post-content"><?= $post->message_html ?></div>
<?php if ($post->episode_id): ?>
<?= 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', [
'preview_card' => $status->preview_card,
'preview_card' => $post->preview_card,
]) ?>
<?php endif; ?>
<?= $this->include('podcast/_partials/status_actions_authenticated') ?>
<?= $this->include('podcast/_partials/post_actions_authenticated') ?>
</article>

View File

@ -11,9 +11,9 @@
->display_name ?><span class="ml-1 text-sm font-normal text-gray-600">@<?= $reply
->actor->username .
($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>
<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): ?>
<?= view('podcast/_partials/preview_card', [
'preview_card' => $reply->preview_card,

View File

@ -1,34 +1,34 @@
<footer class="mt-2 space-x-6 text-sm">
<?= 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,
[
'class' => 'inline-flex items-center hover:underline',
'title' => lang('Status.replies', [
'title' => lang('Post.replies', [
'numberOfReplies' => $reply->replies_count,
]),
],
) ?>
<?= 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,
[
'class' => 'inline-flex items-center hover:underline',
'width' => 420,
'height' => 620,
'title' => lang('Status.reblogs', [
'title' => lang('Post.reblogs', [
'numberOfReblogs' => $reply->reblogs_count,
]),
],
) ?>
<?= 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,
[
'class' => 'inline-flex items-center hover:underline',
'width' => 420,
'height' => 620,
'title' => lang('Status.favourites', [
'title' => lang('Post.favourites', [
'numberOfFavourites' => $reply->favourites_count,
]),
],

View File

@ -1,29 +1,29 @@
<footer class="mt-2 text-sm">
<form action="<?= route_to(
'status-attempt-action',
'post-attempt-action',
interact_as_actor()->username,
$reply->id,
) ?>" method="POST" class="flex items-start">
<?= csrf_field() ?>
<?= 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,
[
'class' => 'inline-flex items-center mr-6 hover:underline',
'title' => lang('Status.replies', [
'title' => lang('Post.replies', [
'numberOfReplies' => $reply->replies_count,
]),
],
) ?>
<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,
],
) ?>"><?= icon('repeat', 'text-xl mr-1 text-gray-400') .
$reply->reblogs_count ?></button>
<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,
],
@ -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' ?>" data-dropdown="menu" data-dropdown-placement="bottom">
<?= anchor(
route_to('status', $podcast->handle, $reply->id),
lang('Status.expand'),
route_to('post', $podcast->handle, $reply->id),
lang('Post.expand'),
[
'class' => 'px-4 py-1 hover:bg-gray-100',
],
) ?>
<form action="<?= route_to(
'status-attempt-block-actor',
'post-attempt-block-actor',
interact_as_actor()->username,
$reply->id,
) ?>" method="POST">
<?= csrf_field() ?>
<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,
],
) ?></button>
</form>
<form action="<?= route_to(
'status-attempt-block-domain',
'post-attempt-block-domain',
interact_as_actor()->username,
$reply->id,
) ?>" method="POST">
<?= csrf_field() ?>
<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,
],
@ -75,13 +75,13 @@
<?php if ($reply->actor->is_local): ?>
<hr class="my-2" />
<form action="<?= route_to(
'status-attempt-delete',
'post-attempt-delete',
$reply->actor->username,
$reply->id,
) ?>" method="POST">
<?= csrf_field() ?>
<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>
</form>
<?php endif; ?>

View File

@ -11,9 +11,9 @@
->display_name ?><span class="ml-1 text-sm font-normal text-gray-600">@<?= $reply
->actor->username .
($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>
<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): ?>
<?= view('podcast/_partials/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>
<section class="max-w-2xl px-6 py-8 mx-auto space-y-8">
<?php foreach ($statuses as $status): ?>
<?php if ($status->reblog_of_id !== null): ?>
<?php foreach ($posts as $post): ?>
<?php if ($post->reblog_of_id !== null): ?>
<?= view('podcast/_partials/reblog', [
'status' => $status->reblog_of_status,
'post' => $post->reblog_of_post,
]) ?>
<?php else: ?>
<?= view('podcast/_partials/status', ['status' => $status]) ?>
<?= view('podcast/_partials/post', ['post' => $post]) ?>
<?php endif; ?>
<?php endforeach; ?>
</section>

View File

@ -40,7 +40,7 @@
</nav>
<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',
]) ?>
<?= csrf_field() ?>
@ -57,7 +57,7 @@
'name' => 'message',
'class' => 'form-textarea',
'required' => 'required',
'placeholder' => lang('Status.form.message_placeholder'),
'placeholder' => lang('Post.form.message_placeholder'),
],
old('message', '', false),
['rows' => 2],
@ -67,7 +67,7 @@
'name' => 'episode_url',
'class' => 'form-input mb-2',
'placeholder' =>
lang('Status.form.episode_url_placeholder') .
lang('Post.form.episode_url_placeholder') .
' (' .
lang('Common.optional') .
')',
@ -75,7 +75,7 @@
]) ?>
<?= button(
lang('Status.form.submit'),
lang('Post.form.submit'),
'',
['variant' => 'primary', 'size' => 'small'],
['type' => 'submit', 'class' => 'self-end'],
@ -85,13 +85,13 @@
<hr class="my-4 border-2 border-pine-100">
<div class="space-y-8">
<?php foreach ($statuses as $status): ?>
<?php if ($status->reblog_of_id !== null): ?>
<?php foreach ($posts as $post): ?>
<?php if ($post->reblog_of_id !== null): ?>
<?= view('podcast/_partials/reblog_authenticated', [
'status' => $status->reblog_of_status,
'post' => $post->reblog_of_post,
]) ?>
<?php else: ?>
<?= view('podcast/_partials/status_authenticated', ['status' => $status]) ?>
<?= view('podcast/_partials/post_authenticated', ['post' => $post]) ?>
<?php endif; ?>
<?php endforeach; ?>
</div>

View File

@ -65,46 +65,6 @@
<?= format_duration($episode->audio_file_duration) ?>
</time>
</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') ?>
<?= 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) ?>
@ -113,27 +73,26 @@
</header>
<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" />
<label for="activity"><?= lang('Episode.activity') ?></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>
<input type="radio" name="tabset" id="description" aria-controls="description" />
<label for="description"><?= lang('Episode.description') ?></label>
<div class="tab-panels">
<?php if ($episode->statuses): ?>
<section id="activity" class="space-y-8 tab-panel">
<?php foreach ($episode->statuses as $status): ?>
<?= view('podcast/_partials/status', ['status' => $status]) ?>
<?php endforeach; ?>
</section>
<?php endif; ?>
<section id="comments" class="space-y-6 tab-panel">
<?php foreach ($episode->comments as $comment): ?>
<?= view('podcast/_partials/comment', ['comment' => $comment]) ?>
<?php endforeach; ?>
</section>
<section id="activity" class="space-y-8 tab-panel">
<?php foreach ($episode->posts as $post): ?>
<?= view('podcast/_partials/post', ['post' => $post]) ?>
<?php endforeach; ?>
</section>
<section id="description" class="prose tab-panel">
<?= $episode->getDescriptionHtml('-+Website+-') ?>
</section>

View File

@ -65,46 +65,6 @@
<?= format_duration($episode->audio_file_duration) ?>
</time>
</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') ?>
<?= 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) ?>
@ -113,15 +73,18 @@
</header>
<div class="tabset">
<input type="radio" name="tabset" id="activity" aria-controls="activity" checked="checked" />
<label for="activity"><?= lang('Episode.activity') ?></label>
<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="description" aria-controls="description" />
<label for="description"><?= lang('Episode.description') ?></label>
<div class="tab-panels">
<section id="activity" class="space-y-8 tab-panel">
<?= form_open(route_to('status-attempt-create', $podcast->handle), [
<section id="comments" class="space-y-6 tab-panel">
<?= form_open(route_to('comment-attempt-create', $podcast->id, $episode->id), [
'class' => 'flex p-4 bg-white shadow rounded-xl',
]) ?>
<?= csrf_field() ?>
@ -129,8 +92,7 @@
<?= 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" />
->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(
[
@ -139,7 +101,47 @@
'class' => 'form-textarea mb-2',
'required' => 'required',
'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),
@ -154,7 +156,7 @@
'type' => 'hidden',
]) ?>
<?= button(
lang('Status.form.submit'),
lang('Post.form.submit'),
'',
['variant' => 'primary', 'size' => 'small'],
['type' => 'submit', 'class' => 'self-end'],
@ -162,9 +164,9 @@
</div>
<?= form_close() ?>
<hr class="my-4 border border-pine-100">
<?php foreach ($episode->statuses as $status): ?>
<?= view('podcast/_partials/status_authenticated', [
'status' => $status,
<?php foreach ($episode->posts as $post): ?>
<?= view('podcast/_partials/post_authenticated', [
'post' => $post,
]) ?>
<?php endforeach; ?>
</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->section('meta-tags') ?>
<title><?= lang('Status.title', [
'actorDisplayName' => $status->actor->display_name,
<title><?= lang('Post.title', [
'actorDisplayName' => $post->actor->display_name,
]) ?></title>
<meta name="description" content="<?= $status->message ?>"/>
<meta property="og:title" content="<?= lang('Status.title', [
'actorDisplayName' => $status->actor->display_name,
<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="<?= $status->actor->display_name ?>" />
<meta property="og:site_name" content="<?= $post->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 ?>" />
<meta property="og:image" content="<?= $post->actor->avatar_image_url ?>" />
<meta property="og:description" content="<?= $post->message ?>" />
<?= $this->endSection() ?>
<?= $this->section('content') ?>
@ -25,13 +25,13 @@
'arrow-left',
'mr-2 text-lg',
) .
lang('Status.back_to_actor_statuses', [
'actor' => $status->actor->display_name,
lang('Post.back_to_actor_posts', [
'actor' => $post->actor->display_name,
]) ?></a>
</nav>
<div class="pb-12">
<?= $this->include(
'podcast/_partials/status_with_replies_authenticated',
'podcast/_partials/post_with_replies_authenticated',
) ?>
</div>
</div>

View File

@ -7,22 +7,22 @@
<link rel="shortcut icon" type="image/png" href="/favicon.ico" />
<title><?= lang('ActivityPub.' . $action . '.title', [
'actorDisplayName' => $status->actor->display_name,
'actorDisplayName' => $post->actor->display_name,
]) ?></title>
<meta name="description" content="<?= $status->message ?>"/>
<meta name="description" content="<?= $post->message ?>"/>
<meta property="og:title" content="<?= lang(
'ActivityPub.' . $action . '.title',
[
'actorDisplayName' => $status->actor->display_name,
'actorDisplayName' => $post->actor->display_name,
],
) ?>"/>
<meta property="og:locale" content="<?= service(
'request',
)->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:image" content="<?= $status->actor->avatar_image_url ?>" />
<meta property="og:description" content="<?= $status->message ?>" />
<meta property="og:image" content="<?= $post->actor->avatar_image_url ?>" />
<meta property="og:description" content="<?= $post->message ?>" />
<?= service('vite')->asset('styles/index.css', 'css') ?>
<?= service('vite')->asset('js/podcast.ts', 'js') ?>
@ -35,10 +35,10 @@
) ?></h1>
</header>
<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(
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'],
) ?>
<?= 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()
?>