Commits (6)
......@@ -11,7 +11,7 @@
[
"@semantic-release/exec",
{
"prepareCmd": "./scripts/bundle.sh ${nextRelease.version} && ./scripts/package.sh && npx prettier --write CHANGELOG.md"
"prepareCmd": "./scripts/bundle.sh ${nextRelease.version} && ./scripts/package.sh ${nextRelease.version} && npx prettier --write CHANGELOG.md"
}
],
"@semantic-release/npm",
......
# [1.0.0-alpha.61](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.60...v1.0.0-alpha.61) (2021-06-23)
### Bug Fixes
- **release:** add missing version number to castopod-host package
([8f3e9d9](https://code.podlibre.org/podlibre/castopod-host/commit/8f3e9d90c14545d3f84d4469b26a53db4554b4dc))
- **ux:** allow for empty message upon episode publication and warn user on
submit
([33d01b8](https://code.podlibre.org/podlibre/castopod-host/commit/33d01b8d4fd6ebf24e9f011aa705c456c846956c)),
closes [#129](https://code.podlibre.org/podlibre/castopod-host/issues/129)
# [1.0.0-alpha.60](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.59...v1.0.0-alpha.60) (2021-06-21)
### Features
......
......@@ -11,7 +11,7 @@ declare(strict_types=1);
|
| NOTE: this constant is updated upon release with Continuous Integration.
*/
defined('CP_VERSION') || define('CP_VERSION', '1.0.0-alpha.60');
defined('CP_VERSION') || define('CP_VERSION', '1.0.0-alpha.61');
/*
| --------------------------------------------------------------------
......
......@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Config;
use App\Entities\Actor;
use App\Entities\Note;
use App\Entities\Status;
use App\Entities\User;
use CodeIgniter\Events\Events;
use CodeIgniter\Exceptions\FrameworkException;
......@@ -120,82 +120,82 @@ Events::on('on_undo_follow', function ($actor, $targetActor): void {
});
/**
* @param Note $note
* @param Status $status
*/
Events::on('on_note_add', function ($note): void {
if ($note->in_reply_to_id !== null) {
$note = $note->reply_to_note;
Events::on('on_status_add', function ($status): void {
if ($status->in_reply_to_id !== null) {
$status = $status->reply_to_status;
}
if ($note->episode_id) {
if ($status->episode_id) {
model('EpisodeModel')
->where('id', $note->episode_id)
->increment('notes_total');
->where('id', $status->episode_id)
->increment('statuses_total');
}
if ($note->actor->is_podcast) {
if ($status->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#{$note->actor->podcast->id}*");
->deleteMatching("podcast#{$status->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
}
});
/**
* @param Note $note
* @param Status $status
*/
Events::on('on_note_remove', function ($note): void {
if ($note->in_reply_to_id !== null) {
Events::trigger('on_note_remove', $note->reply_to_note);
Events::on('on_status_remove', function ($status): void {
if ($status->in_reply_to_id !== null) {
Events::trigger('on_status_remove', $status->reply_to_status);
}
if ($episodeId = $note->episode_id) {
if ($episodeId = $status->episode_id) {
model('EpisodeModel')
->where('id', $episodeId)
->decrement('notes_total', 1 + $note->reblogs_count);
->decrement('statuses_total', 1 + $status->reblogs_count);
model('EpisodeModel')
->where('id', $episodeId)
->decrement('reblogs_total', $note->reblogs_count);
->decrement('reblogs_total', $status->reblogs_count);
model('EpisodeModel')
->where('id', $episodeId)
->decrement('favourites_total', $note->favourites_count);
->decrement('favourites_total', $status->favourites_count);
}
if ($note->actor->is_podcast) {
if ($status->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$note->actor->podcast->id}*");
->deleteMatching("podcast#{$status->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
}
cache()
->deleteMatching("page_note#{$note->id}*");
->deleteMatching("page_status#{$status->id}*");
});
/**
* @param Actor $actor
* @param Note $note
* @param Status $status
*/
Events::on('on_note_reblog', function ($actor, $note): void {
if ($episodeId = $note->episode_id) {
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('notes_total');
->increment('statuses_total');
}
if ($note->actor->is_podcast) {
if ($status->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$note->actor->podcast->id}*");
->deleteMatching("podcast#{$status->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
}
if ($actor->is_podcast) {
......@@ -205,111 +205,111 @@ Events::on('on_note_reblog', function ($actor, $note): void {
}
cache()
->deleteMatching("page_note#{$note->id}*");
->deleteMatching("page_status#{$status->id}*");
if ($note->in_reply_to_id !== null) {
cache()->deleteMatching("page_note#{$note->in_reply_to_id}");
if ($status->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}");
}
});
/**
* @param Note $reblogNote
* @param Status $reblogStatus
*/
Events::on('on_note_undo_reblog', function ($reblogNote): void {
$note = $reblogNote->reblog_of_note;
if ($episodeId = $note->episode_id) {
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');
model('EpisodeModel')
->where('id', $episodeId)
->decrement('notes_total');
->decrement('statuses_total');
}
if ($note->actor->is_podcast) {
if ($status->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$note->actor->podcast->id}*");
->deleteMatching("podcast#{$status->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
}
cache()
->deleteMatching("page_note#{$note->id}*");
->deleteMatching("page_status#{$status->id}*");
cache()
->deleteMatching("page_note#{$reblogNote->id}*");
->deleteMatching("page_status#{$reblogStatus->id}*");
if ($note->in_reply_to_id !== null) {
cache()->deleteMatching("page_note#{$note->in_reply_to_id}");
if ($status->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}");
}
if ($reblogNote->actor->is_podcast) {
if ($reblogStatus->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$reblogNote->actor->podcast->id}*");
->deleteMatching("podcast#{$reblogStatus->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$reblogNote->actor->podcast->id}*");
->deleteMatching("page_podcast#{$reblogStatus->actor->podcast->id}*");
}
});
/**
* @param Note $reply
* @param Status $reply
*/
Events::on('on_note_reply', function ($reply): void {
$note = $reply->reply_to_note;
Events::on('on_status_reply', function ($reply): void {
$status = $reply->reply_to_status;
if ($note->actor->is_podcast) {
if ($status->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$note->actor->podcast->id}*");
->deleteMatching("podcast#{$status->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
}
cache()
->deleteMatching("page_note#{$note->id}*");
->deleteMatching("page_status#{$status->id}*");
});
/**
* @param Note $reply
* @param Status $reply
*/
Events::on('on_reply_remove', function ($reply): void {
$note = $reply->reply_to_note;
$status = $reply->reply_to_status;
if ($note->actor->is_podcast) {
if ($status->actor->is_podcast) {
cache()
->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
cache()
->deleteMatching("podcast#{$note->actor->podcast->id}*");
->deleteMatching("podcast#{$status->actor->podcast->id}*");
}
cache()
->deleteMatching("page_note#{$note->id}*");
->deleteMatching("page_status#{$status->id}*");
cache()
->deleteMatching("page_note#{$reply->id}*");
->deleteMatching("page_status#{$reply->id}*");
});
/**
* @param Actor $actor
* @param Note $note
* @param Status $status
*/
Events::on('on_note_favourite', function ($actor, $note): void {
if ($note->episode_id) {
Events::on('on_status_favourite', function ($actor, $status): void {
if ($status->episode_id) {
model('EpisodeModel')
->where('id', $note->episode_id)
->where('id', $status->episode_id)
->increment('favourites_total');
}
if ($note->actor->is_podcast) {
if ($status->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$note->actor->podcast->id}*");
->deleteMatching("podcast#{$status->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
}
cache()
->deleteMatching("page_note#{$note->id}*");
->deleteMatching("page_status#{$status->id}*");
if ($note->in_reply_to_id !== null) {
cache()->deleteMatching("page_note#{$note->in_reply_to_id}*");
if ($status->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}*");
}
if ($actor->is_podcast) {
......@@ -321,27 +321,27 @@ Events::on('on_note_favourite', function ($actor, $note): void {
/**
* @param Actor $actor
* @param Note $note
* @param Status $status
*/
Events::on('on_note_undo_favourite', function ($actor, $note): void {
if ($note->episode_id) {
Events::on('on_status_undo_favourite', function ($actor, $status): void {
if ($status->episode_id) {
model('EpisodeModel')
->where('id', $note->episode_id)
->where('id', $status->episode_id)
->decrement('favourites_total');
}
if ($note->actor->is_podcast) {
if ($status->actor->is_podcast) {
cache()
->deleteMatching("podcast#{$note->actor->podcast->id}*");
->deleteMatching("podcast#{$status->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
}
cache()
->deleteMatching("page_note#{$note->id}*");
->deleteMatching("page_status#{$status->id}*");
if ($note->in_reply_to_id !== null) {
cache()->deleteMatching("page_note#{$note->in_reply_to_id}*");
if ($status->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}*");
}
if ($actor->is_podcast) {
......@@ -356,7 +356,7 @@ Events::on('on_block_actor', function (int $actorId): void {
cache()
->deleteMatching('podcast*');
cache()
->deleteMatching('page_note*');
->deleteMatching('page_status*');
});
Events::on('on_unblock_actor', function (int $actorId): void {
......@@ -364,7 +364,7 @@ Events::on('on_unblock_actor', function (int $actorId): void {
cache()
->deleteMatching('podcast*');
cache()
->deleteMatching('page_note*');
->deleteMatching('page_status*');
});
Events::on('on_block_domain', function (string $domainName): void {
......@@ -372,7 +372,7 @@ Events::on('on_block_domain', function (string $domainName): void {
cache()
->deleteMatching('podcast*');
cache()
->deleteMatching('page_note*');
->deleteMatching('page_status*');
});
Events::on('on_unblock_domain', function (string $domainName): void {
......@@ -380,5 +380,5 @@ Events::on('on_unblock_domain', function (string $domainName): void {
cache()
->deleteMatching('podcast*');
cache()
->deleteMatching('page_note*');
->deleteMatching('page_status*');
});
......@@ -35,7 +35,7 @@ $routes->addPlaceholder('podcastName', '[a-zA-Z0-9\_]{1,32}');
$routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,191}');
$routes->addPlaceholder('base64', '[A-Za-z0-9\.\_]+\-{0,2}');
$routes->addPlaceholder('platformType', '\bpodcasting|\bsocial|\bfunding');
$routes->addPlaceholder('noteAction', '\bfavourite|\breblog|\breply');
$routes->addPlaceholder('statusAction', '\bfavourite|\breblog|\breply');
$routes->addPlaceholder('embeddablePlayerTheme', '\blight|\bdark|\blight-transparent|\bdark-transparent');
$routes->addPlaceholder(
'uuid',
......@@ -310,6 +310,15 @@ $routes->group(
'permission:podcast-manage_publications',
],
);
$routes->get(
'publish-cancel',
'EpisodeController::publishCancel/$1/$2',
[
'as' => 'episode-publish-cancel',
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->get(
'unpublish',
'EpisodeController::unpublish/$1/$2',
......@@ -746,71 +755,71 @@ $routes->post('interact-as-actor', 'AuthController::attemptInteractAsActor', [
* Overwriting ActivityPub routes file
*/
$routes->group('@(:podcastName)', function ($routes): void {
$routes->post('notes/new', 'NoteController::attemptCreate/$1', [
'as' => 'note-attempt-create',
$routes->post('statuses/new', 'StatusController::attemptCreate/$1', [
'as' => 'status-attempt-create',
'filter' => 'permission:podcast-manage_publications',
]);
// Note
$routes->group('notes/(:uuid)', function ($routes): void {
$routes->get('/', 'NoteController::view/$1/$2', [
'as' => 'note',
// Status
$routes->group('statuses/(:uuid)', function ($routes): void {
$routes->get('/', 'StatusController::view/$1/$2', [
'as' => 'status',
'alternate-content' => [
'application/activity+json' => [
'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'NoteController/$2',
'controller-method' => 'StatusController/$2',
],
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'NoteController/$2',
'controller-method' => 'StatusController/$2',
],
],
]);
$routes->get('replies', 'NoteController/$1/$2', [
'as' => 'note-replies',
$routes->get('replies', 'StatusController/$1/$2', [
'as' => 'status-replies',
'alternate-content' => [
'application/activity+json' => [
'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'NoteController::replies/$2',
'controller-method' => 'StatusController::replies/$2',
],
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'NoteController::replies/$2',
'controller-method' => 'StatusController::replies/$2',
],
],
]);
// Actions
$routes->post('action', 'NoteController::attemptAction/$1/$2', [
'as' => 'note-attempt-action',
$routes->post('action', 'StatusController::attemptAction/$1/$2', [
'as' => 'status-attempt-action',
'filter' => 'permission:podcast-interact_as',
]);
$routes->post(
'block-actor',
'NoteController::attemptBlockActor/$1/$2',
'StatusController::attemptBlockActor/$1/$2',
[
'as' => 'note-attempt-block-actor',
'as' => 'status-attempt-block-actor',
'filter' => 'permission:fediverse-block_actors',
],
);
$routes->post(
'block-domain',
'NoteController::attemptBlockDomain/$1/$2',
'StatusController::attemptBlockDomain/$1/$2',
[
'as' => 'note-attempt-block-domain',
'as' => 'status-attempt-block-domain',
'filter' => 'permission:fediverse-block_domains',
],
);
$routes->post('delete', 'NoteController::attemptDelete/$1/$2', [
'as' => 'note-attempt-delete',
$routes->post('delete', 'StatusController::attemptDelete/$1/$2', [
'as' => 'status-attempt-delete',
'filter' => 'permission:podcast-manage_publications',
]);
$routes->get(
'remote/(:noteAction)',
'NoteController::remoteAction/$1/$2/$3',
'remote/(:statusAction)',
'StatusController::remoteAction/$1/$2/$3',
[
'as' => 'note-remote-action',
'as' => 'status-remote-action',
],
);
});
......
......@@ -13,12 +13,12 @@ namespace App\Controllers\Admin;
use App\Entities\Episode;
use App\Entities\Image;
use App\Entities\Location;
use App\Entities\Note;
use App\Entities\Podcast;
use App\Entities\Status;
use App\Models\EpisodeModel;
use App\Models\NoteModel;
use App\Models\PodcastModel;
use App\Models\SoundbiteModel;
use App\Models\StatusModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\I18n\Time;
......@@ -388,7 +388,7 @@ class EpisodeController extends BaseController
return redirect()->back();
}
public function publish(): string
public function publish(): string | RedirectResponse
{
if ($this->episode->publication_status === 'not_published') {
helper(['form']);
......@@ -405,7 +405,10 @@ class EpisodeController extends BaseController
return view('admin/episode/publish', $data);
}
throw PageNotFoundException::forPageNotFound();
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with(
'error',
lang('Episode.publish_error')
);
}
public function attemptPublish(): RedirectResponse
......@@ -426,7 +429,7 @@ class EpisodeController extends BaseController
$db = db_connect();
$db->transStart();
$newNote = new Note([
$newStatus = new Status([
'actor_id' => $this->podcast->actor_id,
'episode_id' => $this->episode->id,
'message' => $this->request->getPost('message'),
......@@ -453,15 +456,15 @@ class EpisodeController extends BaseController
$this->episode->published_at = Time::now();
}
$newNote->published_at = $this->episode->published_at;
$newStatus->published_at = $this->episode->published_at;
$noteModel = new NoteModel();
if (! $noteModel->addNote($newNote)) {
$statusModel = new StatusModel();
if (! $statusModel->addStatus($newStatus)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $noteModel->errors());
->with('errors', $statusModel->errors());
}
$episodeModel = new EpisodeModel();
......@@ -478,7 +481,7 @@ class EpisodeController extends BaseController
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]);
}
public function publishEdit(): string
public function publishEdit(): string | RedirectResponse
{
if ($this->episode->publication_status === 'scheduled') {
helper(['form']);
......@@ -486,7 +489,7 @@ class EpisodeController extends BaseController
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
'note' => (new NoteModel())
'status' => (new StatusModel())
->where([
'actor_id' => $this->podcast->actor_id,
'episode_id' => $this->episode->id,
......@@ -500,13 +503,17 @@ class EpisodeController extends BaseController
]);
return view('admin/episode/publish_edit', $data);
}
throw PageNotFoundException::forPageNotFound();
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with(
'error',
lang('Episode.publish_edit_error')
);
}
public function attemptPublishEdit(): RedirectResponse
{
$rules = [
'note_id' => 'required',
'status_id' => 'required',
'publication_method' => 'required',
'scheduled_publication_date' =>
'valid_date[Y-m-d H:i]|permit_empty',
......@@ -542,19 +549,19 @@ class EpisodeController extends BaseController
$this->episode->published_at = Time::now();
}
$note = (new NoteModel())->getNoteById($this->request->getPost('note_id'));
$status = (new StatusModel())->getStatusById($this->request->getPost('status_id'));
if ($note !== null) {
$note->message = $this->request->getPost('message');
$note->published_at = $this->episode->published_at;
if ($status !== null) {
$status->message = $this->request->getPost('message');
$status->published_at = $this->episode->published_at;
$noteModel = new NoteModel();
if (! $noteModel->editNote($note)) {
$statusModel = new StatusModel();
if (! $statusModel->editStatus($status)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $noteModel->errors());
->with('errors', $statusModel->errors());
}
}
......@@ -572,7 +579,44 @@ class EpisodeController extends BaseController
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]);
}
public function unpublish(): string
public function publishCancel(): RedirectResponse
{
if ($this->episode->publication_status === 'scheduled') {
$db = db_connect();
$db->transStart();
$statusModel = new StatusModel();
$status = $statusModel
->where([
'actor_id' => $this->podcast->actor_id,
'episode_id' => $this->episode->id,
])
->first();
$statusModel->removeStatus($status);
$this->episode->published_at = null;
$episodeModel = new EpisodeModel();
if (! $episodeModel->update($this->episode->id, $this->episode)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
$db->transComplete();
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]);
}
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with(
'error',
lang('Episode.publish_cancel_error')
);
}
public function unpublish(): string | RedirectResponse
{
if ($this->episode->publication_status === 'published') {
helper(['form']);
......@@ -589,7 +633,10 @@ class EpisodeController extends BaseController
return view('admin/episode/unpublish', $data);
}
throw PageNotFoundException::forPageNotFound();
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with(
'error',
lang('Episode.unpublish_error')
);
}
public function attemptUnpublish(): RedirectResponse
......@@ -609,13 +656,13 @@ class EpisodeController extends BaseController
$db->transStart();
$allNotesLinkedToEpisode = (new NoteModel())
$allStatusesLinkedToEpisode = (new StatusModel())
->where([
'episode_id' => $this->episode->id,
])
->findAll();
foreach ($allNotesLinkedToEpisode as $note) {
(new NoteModel())->removeNote($note);
foreach ($allStatusesLinkedToEpisode as $status) {
(new StatusModel())->removeStatus($status);
}
// set episode published_at to null to unpublish
......
......@@ -13,8 +13,8 @@ namespace App\Controllers;
use Analytics\AnalyticsTrait;
use App\Entities\Podcast;
use App\Models\EpisodeModel;
use App\Models\NoteModel;
use App\Models\PodcastModel;
use App\Models\StatusModel;
use CodeIgniter\Exceptions\PageNotFoundException;
class PodcastController extends BaseController
......@@ -64,7 +64,7 @@ class PodcastController extends BaseController
if (! ($cachedView = cache($cacheName))) {
$data = [
'podcast' => $this->podcast,
'notes' => (new NoteModel())->getActorPublishedNotes($this->podcast->actor_id),
'statuses' => (new StatusModel())->getActorPublishedStatuses($this->podcast->actor_id),
];
// if user is logged in then send to the authenticated activity view
......
......@@ -10,21 +10,21 @@ declare(strict_types=1);
namespace App\Controllers;
use ActivityPub\Controllers\NoteController as ActivityPubNoteController;
use ActivityPub\Entities\Note as ActivityPubNote;
use ActivityPub\Controllers\StatusController as ActivityPubStatusController;
use ActivityPub\Entities\Status as ActivityPubStatus;
use Analytics\AnalyticsTrait;
use App\Entities\Actor;
use App\Entities\Note as CastopodNote;
use App\Entities\Podcast;
use App\Entities\Status as CastopodStatus;
use App\Models\EpisodeModel;
use App\Models\NoteModel;
use App\Models\PodcastModel;
use App\Models\StatusModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\URI;
use CodeIgniter\I18n\Time;
class NoteController extends ActivityPubNoteController
class StatusController extends ActivityPubStatusController
{
use AnalyticsTrait;
......@@ -50,9 +50,9 @@ class NoteController extends ActivityPubNoteController
if (
count($params) > 1 &&
($note = (new NoteModel())->getNoteById($params[1])) !== null
($status = (new StatusModel())->getStatusById($params[1])) !== null
) {
$this->note = $note;
$this->status = $status;
unset($params[0]);
unset($params[1]);
......@@ -72,7 +72,7 @@ class NoteController extends ActivityPubNoteController
'_',
array_filter([
'page',
"note#{$this->note->id}",
"status#{$this->status->id}",
service('request')
->getLocale(),
can_user_interact() ? '_authenticated' : null,
......@@ -83,15 +83,15 @@ class NoteController extends ActivityPubNoteController
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'note' => $this->note,
'status' => $this->status,
];
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
helper('form');
return view('podcast/note_authenticated', $data);
return view('podcast/status_authenticated', $data);
}
return view('podcast/note', $data, [
return view('podcast/status', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
......@@ -116,7 +116,7 @@ class NoteController extends ActivityPubNoteController
$message = $this->request->getPost('message');
$newNote = new CastopodNote([
$newStatus = new CastopodStatus([
'actor_id' => interact_as_actor_id(),
'published_at' => Time::now(),
'created_by' => user_id(),
......@@ -129,23 +129,23 @@ class NoteController extends ActivityPubNoteController
($params = extract_params_from_episode_uri(new URI($episodeUri))) &&
($episode = (new EpisodeModel())->getEpisodeBySlug($params['podcastName'], $params['episodeSlug']))
) {
$newNote->episode_id = $episode->id;
$newStatus->episode_id = $episode->id;
}
$newNote->message = $message;
$newStatus->message = $message;
$noteModel = new NoteModel();
$statusModel = new StatusModel();
if (
! $noteModel
->addNote($newNote, ! (bool) $newNote->episode_id, true)
! $statusModel
->addStatus($newStatus, ! (bool) $newStatus->episode_id, true)
) {
return redirect()
->back()
->withInput()
->with('errors', $noteModel->errors());
->with('errors', $statusModel->errors());
}
// Note has been successfully created
// Status has been successfully created
return redirect()->back();
}
......@@ -162,36 +162,36 @@ class NoteController extends ActivityPubNoteController
->with('errors', $this->validator->getErrors());
}
$newNote = new ActivityPubNote([
$newStatus = new ActivityPubStatus([
'actor_id' => interact_as_actor_id(),
'in_reply_to_id' => $this->note->id,
'in_reply_to_id' => $this->status->id,
'message' => $this->request->getPost('message'),
'published_at' => Time::now(),
'created_by' => user_id(),
]);
$noteModel = new NoteModel();
if (! $noteModel->addReply($newNote)) {
$statusModel = new StatusModel();
if (! $statusModel->addReply($newStatus)) {
return redirect()
->back()
->withInput()
->with('errors', $noteModel->errors());
->with('errors', $statusModel->errors());
}
// Reply note without preview card has been successfully created
// Reply status without preview card has been successfully created
return redirect()->back();
}
public function attemptFavourite(): RedirectResponse
{
model('FavouriteModel')->toggleFavourite(interact_as_actor(), $this->note);
model('FavouriteModel')->toggleFavourite(interact_as_actor(), $this->status);
return redirect()->back();
}
public function attemptReblog(): RedirectResponse
{
(new NoteModel())->toggleReblog(interact_as_actor(), $this->note);
(new StatusModel())->toggleReblog(interact_as_actor(), $this->status);
return redirect()->back();
}
......@@ -230,20 +230,20 @@ class NoteController extends ActivityPubNoteController
$cacheName = implode(
'_',
array_filter(['page', "note#{$this->note->id}", "remote_{$action}", service('request') ->getLocale()]),
array_filter(['page', "status#{$this->status->id}", "remote_{$action}", service('request') ->getLocale()]),
);
if (! ($cachedView = cache($cacheName))) {
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'note' => $this->note,
'status' => $this->status,
'action' => $action,
];
helper('form');
return view('podcast/note_remote_action', $data, [
return view('podcast/status_remote_action', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
......
......@@ -157,7 +157,7 @@ class AddEpisodes extends Migration
'unsigned' => true,
'default' => 0,
],
'notes_total' => [
'statuses_total' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
......
......@@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Class AddEpisodeIdToNotes Adds episode_id field to activitypub_notes table in database
* Class AddEpisodeIdToStatuses Adds episode_id field to activitypub_statuses 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 AddEpisodeIdToNotes extends Migration
class AddEpisodeIdToStatuses extends Migration
{
public function up(): void
{
$prefix = $this->db->getPrefix();
$createQuery = <<<CODE_SAMPLE
ALTER TABLE {$prefix}activitypub_notes
ALTER TABLE {$prefix}activitypub_statuses
ADD COLUMN `episode_id` INT UNSIGNED NULL AFTER `replies_count`,
ADD FOREIGN KEY {$prefix}activitypub_notes_episode_id_foreign(episode_id) REFERENCES {$prefix}episodes(id) ON DELETE CASCADE;
ADD FOREIGN KEY {$prefix}activitypub_statuses_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_notes', 'activitypub_notes_episode_id_foreign');
$this->forge->dropColumn('activitypub_notes', 'episode_id');
$this->forge->dropForeignKey('activitypub_statuses', 'activitypub_statuses_episode_id_foreign');
$this->forge->dropColumn('activitypub_statuses', 'episode_id');
}
}
......@@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Class AddCreatedByToNotes Adds created_by field to activitypub_notes table in database
* Class AddCreatedByToStatuses Adds created_by field to activitypub_statuses 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 AddCreatedByToNotes extends Migration
class AddCreatedByToStatuses extends Migration
{
public function up(): void
{
$prefix = $this->db->getPrefix();
$createQuery = <<<CODE_SAMPLE
ALTER TABLE {$prefix}activitypub_notes
ALTER TABLE {$prefix}activitypub_statuses
ADD COLUMN `created_by` INT UNSIGNED AFTER `episode_id`,
ADD FOREIGN KEY {$prefix}activitypub_notes_created_by_foreign(created_by) REFERENCES {$prefix}users(id) ON DELETE CASCADE;
ADD FOREIGN KEY {$prefix}activitypub_statuses_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_notes', 'activitypub_notes_created_by_foreign');
$this->forge->dropColumn('activitypub_notes', 'created_by');
$this->forge->dropForeignKey('activitypub_statuses', 'activitypub_statuses_created_by_foreign');
$this->forge->dropColumn('activitypub_statuses', 'created_by');
}
}
......@@ -162,13 +162,13 @@ class AuthSeeder extends Seeder
[
'name' => 'manage_publications',
'description' =>
'Publish / unpublish episodes & notes of a podcast',
'Publish / unpublish episodes & statuses of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'interact_as',
'description' =>
'Interact as the podcast to favourite / share or reply to notes.',
'Interact as the podcast to favourite / share or reply to statuses.',
'has_permission' => ['podcast_admin'],
],
],
......
......@@ -11,10 +11,10 @@ declare(strict_types=1);
namespace App\Entities;
use App\Libraries\SimpleRSSElement;
use App\Models\NoteModel;
use App\Models\PersonModel;
use App\Models\PodcastModel;
use App\Models\SoundbiteModel;
use App\Models\StatusModel;
use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile;
......@@ -67,7 +67,7 @@ use RuntimeException;
* @property string $custom_rss_string
* @property int $favourites_total
* @property int $reblogs_total
* @property int $notes_total
* @property int $statuses_total
* @property int $created_by
* @property int $updated_by
* @property string $publication_status;
......@@ -117,9 +117,9 @@ class Episode extends Entity
protected ?array $soundbites = null;
/**
* @var Note[]|null
* @var Status[]|null
*/
protected ?array $notes = null;
protected ?array $statuses = null;
protected ?Location $location = null;
......@@ -165,7 +165,7 @@ class Episode extends Entity
'custom_rss' => '?json-array',
'favourites_total' => 'integer',
'reblogs_total' => 'integer',
'notes_total' => 'integer',
'statuses_total' => 'integer',
'created_by' => 'integer',
'updated_by' => 'integer',
];
......@@ -382,19 +382,19 @@ class Episode extends Entity
}
/**
* @return Note[]
* @return Status[]
*/
public function getNotes(): array
public function getStatuses(): array
{
if ($this->id === null) {
throw new RuntimeException('Episode must be created before getting soundbites.');
}
if ($this->notes === null) {
$this->notes = (new NoteModel())->getEpisodeNotes($this->id);
if ($this->statuses === null) {
$this->statuses = (new StatusModel())->getEpisodeStatuses($this->id);
}
return $this->notes;
return $this->statuses;
}
public function getLink(): string
......
......@@ -10,7 +10,7 @@ declare(strict_types=1);
namespace App\Entities;
use ActivityPub\Entities\Note as ActivityPubNote;
use ActivityPub\Entities\Status as ActivityPubStatus;
use App\Models\EpisodeModel;
use RuntimeException;
......@@ -18,7 +18,7 @@ use RuntimeException;
* @property int|null $episode_id
* @property Episode|null $episode
*/
class Note extends ActivityPubNote
class Status extends ActivityPubStatus
{
protected ?Episode $episode = null;
......@@ -41,12 +41,12 @@ class Note extends ActivityPubNote
];
/**
* Returns the note's attached episode
* Returns the status' attached episode
*/
public function getEpisode(): ?Episode
{
if ($this->episode_id === null) {
throw new RuntimeException('Note must have an episode_id before getting episode.');
throw new RuntimeException('Status must have an episode_id before getting episode.');
}
if ($this->episode === null) {
......
......@@ -86,11 +86,11 @@ if (! function_exists('button')) {
}
if ($options['iconLeft']) {
$label = icon($options['iconLeft'], 'mr-2') . $label;
$label = icon((string) $options['iconLeft'], 'mr-2') . $label;
}
if ($options['iconRight']) {
$label .= icon($options['iconRight'], 'ml-2');
$label .= icon((string) $options['iconRight'], 'ml-2');
}
if ($uri !== '') {
......
......@@ -26,9 +26,9 @@ return [
one {# total share}
other {# total shares}
}',
'total_notes' => '{numberOfTotalNotes, plural,
one {# note}
other {# total notes}
'total_statuses' => '{numberOfTotalStatuses, plural,
one {# total post}
other {# total posts}
}',
'all_podcast_episodes' => 'All podcast episodes',
'back_to_podcast' => 'Go back to podcast',
......@@ -36,6 +36,10 @@ return [
'publish' => 'Publish',
'publish_edit' => 'Edit publication',
'unpublish' => 'Unpublish',
'publish_error' => 'Episode is already published.',
'publish_edit_error' => 'Episode is already published.',
'publish_cancel_error' => 'Episode is already published.',
'unpublish_error' => 'Episode is not published.',
'delete' => 'Delete',
'go_to_page' => 'Go to page',
'create' => 'Add an episode',
......@@ -112,9 +116,10 @@ return [
'submit_edit' => 'Save episode',
],
'publish_form' => [
'note' => 'Your note',
'note_hint' =>
'The message you write will be broadcasted to all your followers in the fediverse.',
'back_to_episode_dashboard' => 'Back to episode dashboard',
'status' => 'Your announcement post',
'status_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' => [
'now' => 'Now',
......@@ -126,6 +131,10 @@ return [
'You can schedule the episode release by setting a future publication date. This field must be formatted as YYYY-MM-DD HH:mm',
'submit' => 'Publish',
'submit_edit' => 'Edit publication',
'cancel_publication' => 'Cancel publication',
'message_warning' => 'You did not write a message for your announcement post!',
'message_warning_hint' => 'Having a message increases social engagement, resulting in a better visibility for your episode.',
'message_warning_submit' => 'Publish anyways',
],
'unpublish_form' => [
'disclaimer' =>
......
......@@ -223,9 +223,9 @@ return [
one {<span class="font-semibold">#</span> follower}
other {<span class="font-semibold">#</span> followers}
}',
'notes' => '{numberOfNotes, plural,
one {<span class="font-semibold">#</span> note}
other {<span class="font-semibold">#</span> notes}
'statuses' => '{numberOfStatuses, plural,
one {<span class="font-semibold">#</span> post}
other {<span class="font-semibold">#</span> posts}
}',
'activity' => 'Activity',
'episodes' => 'Episodes',
......
......@@ -10,7 +10,7 @@ declare(strict_types=1);
return [
'title' => "{actorDisplayName}'s Note",
'back_to_actor_notes' => 'Back to {actor} notes',
'back_to_actor_statuses' => 'Back to {actor} notes',
'actor_shared' => '{actor} shared',
'reply_to' => 'Reply to @{actorUsername}',
'form' => [
......
......@@ -26,9 +26,9 @@ return [
one {# partage en tout}
other {# partages en tout}
}',
'total_notes' => '{numberOfTotalNotes, plural,
one {# note}
other {# notes}
'total_statuses' => '{numberOfTotalStatuses, plural,
one {# message}
other {# messages}
}',
'all_podcast_episodes' => 'Tous les épisodes du podcast',
'back_to_podcast' => 'Revenir au podcast',
......@@ -36,6 +36,10 @@ return [
'publish' => 'Publier',
'publish_edit' => 'Modifier la publication',
'unpublish' => 'Dépublier',
'publish_error' => 'L’épisode est déjà publié.',
'publish_edit_error' => 'L’épisode est déjà publié.',
'publish_cancel_error' => 'L’épisode est déjà publié.',
'unpublish_error' => 'L’épisode n’est pas publié.',
'delete' => 'Supprimer',
'go_to_page' => 'Voir',
'create' => 'Ajouter un épisode',
......@@ -115,9 +119,10 @@ return [
'submit_edit' => 'Enregistrer l’épisode',
],
'publish_form' => [
'note' => 'Votre note',
'note_hint' =>
'Le message que vous écrirez sera diffusé à toutes les personnes qui vous suivent dans le fédiverse.',
'back_to_episode_dashboard' => 'Retour au tableau de bord de l’épisode',
'status' => 'Votre message de publication',
'status_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 d’accueil de votre podcast.',
'publication_date' => 'Date de publication',
'publication_date_clear' => 'Effacer la date de publication',
'publication_date_hint' =>
......@@ -132,6 +137,10 @@ return [
'Vous pouvez planifier la sortie de l’épisode en saisissant une date de publication future. Ce champ doit être au format YYYY-MM-DD HH:mm',
'submit' => 'Publier',
'submit_edit' => 'Modifier la publication',
'cancel_publication' => 'Annuler la publication',
'message_warning' => 'Vous n’avez pas saisi de message pour l’annonce de votre épisode !',
'message_warning_hint' => 'Ajouter un message augmente l’engagement social, menant à une meilleure visibilité pour votre épisode.',
'message_warning_submit' => 'Publish quand même',
],
'soundbites' => 'Extraits sonores',
'soundbites_form' => [
......
......@@ -225,9 +225,9 @@ return [
one {<span class="font-semibold">#</span> abonné·e}
other {<span class="font-semibold">#</span> abonné·e·s}
}',
'notes' => '{numberOfNotes, plural,
one {<span class="font-semibold">#</span> note}
other {<span class="font-semibold">#</span> notes}
'notes' => '{numberOfStatuses, plural,
one {<span class="font-semibold">#</span> message}
other {<span class="font-semibold">#</span> messages}
}',
'activity' => 'Activité',
'episodes' => 'Épisodes',
......