mirror of
https://github.com/Poniverse/Pony.fm.git
synced 2024-11-25 14:37:59 +01:00
Merge branch 'master' of https://github.com/Poniverse/Pony.fm into feature-playlist-sort
This commit is contained in:
commit
2d458c3d80
48 changed files with 1633 additions and 31 deletions
|
@ -25,6 +25,8 @@ For quick fixes, go ahead and submit a pull request!
|
|||
For larger features, it's best to open an issue before sinking a ton of work
|
||||
into building them, to coordinate with Pony.fm's maintainers.
|
||||
|
||||
Developer documentation is available in the [`documentation` directory](documentation).
|
||||
|
||||
**Protip:** Looking for a place to jump in and start coding? Try a
|
||||
[quickwin issue](https://github.com/Poniverse/Pony.fm/labels/quickwin%21) -
|
||||
these are smaller in scope and easier to tackle if you're unfamiliar with the codebase!
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
namespace Poniverse\Ponyfm\Commands;
|
||||
|
||||
use Notification;
|
||||
use Poniverse\Ponyfm\Models\Album;
|
||||
use Poniverse\Ponyfm\Models\Comment;
|
||||
use Poniverse\Ponyfm\Models\Playlist;
|
||||
|
@ -116,6 +117,8 @@ class CreateCommentCommand extends CommandBase
|
|||
$entity->comment_count = Comment::where($column, $this->_id)->count();
|
||||
$entity->save();
|
||||
|
||||
Notification::newComment($comment);
|
||||
|
||||
return CommandResponse::succeed(Comment::mapPublic($comment));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
namespace Poniverse\Ponyfm\Commands;
|
||||
|
||||
use Notification;
|
||||
use Poniverse\Ponyfm\Models\Playlist;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
@ -69,6 +70,8 @@ class CreatePlaylistCommand extends CommandBase
|
|||
|
||||
$playlist->save();
|
||||
|
||||
Notification::publishedNewPlaylist($playlist);
|
||||
|
||||
if ($this->_input['is_pinned'] == 'true') {
|
||||
$playlist->pin(Auth::user()->id);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
namespace Poniverse\Ponyfm\Commands;
|
||||
|
||||
use Gate;
|
||||
use Notification;
|
||||
use Poniverse\Ponyfm\Models\Album;
|
||||
use Poniverse\Ponyfm\Models\Image;
|
||||
use Poniverse\Ponyfm\Models\Track;
|
||||
|
@ -32,6 +33,10 @@ use DB;
|
|||
class EditTrackCommand extends CommandBase
|
||||
{
|
||||
private $_trackId;
|
||||
|
||||
/**
|
||||
* @var Track
|
||||
*/
|
||||
private $_track;
|
||||
private $_input;
|
||||
|
||||
|
@ -132,6 +137,8 @@ class EditTrackCommand extends CommandBase
|
|||
|
||||
DB::table('tracks')->whereUserId($track->user_id)->update(['is_latest' => false]);
|
||||
$track->is_latest = true;
|
||||
|
||||
Notification::publishedNewTrack($track);
|
||||
}
|
||||
|
||||
if (isset($this->_input['cover_id'])) {
|
||||
|
@ -174,12 +181,13 @@ class EditTrackCommand extends CommandBase
|
|||
return CommandResponse::succeed(['real_cover_url' => $track->getCoverUrl(Image::NORMAL)]);
|
||||
}
|
||||
|
||||
private function removeTrackFromAlbum($track)
|
||||
private function removeTrackFromAlbum(Track $track)
|
||||
{
|
||||
$album = $track->album;
|
||||
$index = 0;
|
||||
|
||||
foreach ($album->tracks as $track) {
|
||||
/** @var $track Track */
|
||||
if ($track->id == $this->_trackId) {
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -20,10 +20,14 @@
|
|||
|
||||
namespace Poniverse\Ponyfm\Commands;
|
||||
|
||||
use Notification;
|
||||
use Poniverse\Ponyfm\Contracts\Favouritable;
|
||||
use Poniverse\Ponyfm\Models\Favourite;
|
||||
use Poniverse\Ponyfm\Models\Playlist;
|
||||
use Poniverse\Ponyfm\Models\ResourceUser;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Poniverse\Ponyfm\Models\Track;
|
||||
|
||||
class ToggleFavouriteCommand extends CommandBase
|
||||
{
|
||||
|
@ -46,6 +50,20 @@ class ToggleFavouriteCommand extends CommandBase
|
|||
return $user != null;
|
||||
}
|
||||
|
||||
private function getEntityBeingFavourited():Favouritable
|
||||
{
|
||||
switch ($this->_resourceType) {
|
||||
case 'track':
|
||||
return Track::find($this->_resourceId);
|
||||
case 'album':
|
||||
return Album::find($this->_resourceId);
|
||||
case 'playlist':
|
||||
return Playlist::find($this->_resourceId);
|
||||
default:
|
||||
throw new \InvalidArgumentException('Unknown resource type given!');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
* @return CommandResponse
|
||||
|
@ -65,6 +83,8 @@ class ToggleFavouriteCommand extends CommandBase
|
|||
$fav->created_at = time();
|
||||
$fav->save();
|
||||
$isFavourited = true;
|
||||
|
||||
Notification::newFavourite($this->getEntityBeingFavourited(), $fav->user);
|
||||
}
|
||||
|
||||
$resourceUser = ResourceUser::get(Auth::user()->id, $this->_resourceType, $this->_resourceId);
|
||||
|
|
|
@ -22,7 +22,8 @@ namespace Poniverse\Ponyfm\Commands;
|
|||
|
||||
use Poniverse\Ponyfm\Models\Follower;
|
||||
use Poniverse\Ponyfm\Models\ResourceUser;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Auth;
|
||||
use Notification;
|
||||
|
||||
class ToggleFollowingCommand extends CommandBase
|
||||
{
|
||||
|
@ -64,6 +65,8 @@ class ToggleFollowingCommand extends CommandBase
|
|||
$follow->created_at = time();
|
||||
$follow->save();
|
||||
$isFollowed = true;
|
||||
|
||||
Notification::newFollower($follow->artist, Auth::user());
|
||||
}
|
||||
|
||||
$resourceUser = ResourceUser::get(Auth::user()->id, $this->_resourceType, $this->_resourceId);
|
||||
|
|
38
app/Contracts/Commentable.php
Normal file
38
app/Contracts/Commentable.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Pony.fm - A community for pony fan music.
|
||||
* Copyright (C) 2016 Peter Deltchev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Poniverse\Ponyfm\Contracts;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* This interface is used for type safety when referring to entities that
|
||||
* are capable of accepting comments.
|
||||
*
|
||||
* @package Poniverse\Ponyfm\Contracts
|
||||
*/
|
||||
interface Commentable extends GeneratesNotifications {
|
||||
/**
|
||||
* This method returns an Eloquent relation to the entity's comments.
|
||||
*
|
||||
* @return HasMany
|
||||
*/
|
||||
public function comments():HasMany;
|
||||
}
|
38
app/Contracts/Favouritable.php
Normal file
38
app/Contracts/Favouritable.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Pony.fm - A community for pony fan music.
|
||||
* Copyright (C) 2016 Peter Deltchev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Poniverse\Ponyfm\Contracts;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* This interface is used for type safety when referring to entities that
|
||||
* are capable of being favourited.
|
||||
*
|
||||
* @package Poniverse\Ponyfm\Contracts
|
||||
*/
|
||||
interface Favouritable extends GeneratesNotifications {
|
||||
/**
|
||||
* This method returns an Eloquent relation to the entity's favourites.
|
||||
*
|
||||
* @return HasMany
|
||||
*/
|
||||
public function favourites():HasMany;
|
||||
}
|
42
app/Contracts/GeneratesNotifications.php
Normal file
42
app/Contracts/GeneratesNotifications.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Pony.fm - A community for pony fan music.
|
||||
* Copyright (C) 2016 Peter Deltchev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Poniverse\Ponyfm\Contracts;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Poniverse\Ponyfm\Models\User;
|
||||
|
||||
/**
|
||||
* This interface is used for type safety when referring to entities that can be
|
||||
* the "target resource" of a notification (ie. what the notification is about).
|
||||
*
|
||||
* @package Poniverse\Ponyfm\Contracts
|
||||
*/
|
||||
interface GeneratesNotifications {
|
||||
/**
|
||||
* Returns a human-friendly string (lowercase & singular) representing this
|
||||
* type of resource.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getResourceType():string;
|
||||
|
||||
public function activities():MorphMany;
|
||||
}
|
68
app/Contracts/NotificationHandler.php
Normal file
68
app/Contracts/NotificationHandler.php
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Pony.fm - A community for pony fan music.
|
||||
* Copyright (C) 2016 Peter Deltchev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Poniverse\Ponyfm\Contracts;
|
||||
|
||||
use Poniverse\Ponyfm\Models\Comment;
|
||||
use Poniverse\Ponyfm\Models\Playlist;
|
||||
use Poniverse\Ponyfm\Models\Track;
|
||||
use Poniverse\Ponyfm\Models\User;
|
||||
|
||||
/**
|
||||
* Interface NotificationHandler
|
||||
* @package Poniverse\Ponyfm\Contracts
|
||||
*
|
||||
* Each method in this interface represents a type of notification. To add a new
|
||||
* type of notification, add a method for it to this interface and every class
|
||||
* that implements it. Your IDE should be able to help with this.
|
||||
*/
|
||||
interface NotificationHandler {
|
||||
/**
|
||||
* @param Track $track
|
||||
* @return void
|
||||
*/
|
||||
public function publishedNewTrack(Track $track);
|
||||
|
||||
/**
|
||||
* @param Playlist $playlist
|
||||
* @return void
|
||||
*/
|
||||
public function publishedNewPlaylist(Playlist $playlist);
|
||||
|
||||
/**
|
||||
* @param User $userBeingFollowed
|
||||
* @param User $follower
|
||||
* @return void
|
||||
*/
|
||||
public function newFollower(User $userBeingFollowed, User $follower);
|
||||
|
||||
/**
|
||||
* @param Comment $comment
|
||||
* @return void
|
||||
*/
|
||||
public function newComment(Comment $comment);
|
||||
|
||||
/**
|
||||
* @param Favouritable $entityBeingFavourited
|
||||
* @param User $favouriter
|
||||
* @return void
|
||||
*/
|
||||
public function newFavourite(Favouritable $entityBeingFavourited, User $favouriter);
|
||||
}
|
28
app/Facades/Notification.php
Normal file
28
app/Facades/Notification.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Pony.fm - A community for pony fan music.
|
||||
* Copyright (C) 2016 Peter Deltchev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Poniverse\Ponyfm\Facades;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class Notification extends Facade {
|
||||
protected static function getFacadeAccessor() {
|
||||
return 'notification';
|
||||
}
|
||||
}
|
|
@ -35,4 +35,9 @@ class AccountController extends Controller
|
|||
{
|
||||
return Redirect::to(Config::get('poniverse.urls')['register']);
|
||||
}
|
||||
|
||||
public function getNotifications()
|
||||
{
|
||||
return View::make('shared.null');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,10 +20,10 @@
|
|||
|
||||
namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
|
||||
use Poniverse\Ponyfm\Commands\SaveAccountSettingsCommand;
|
||||
use Poniverse\Ponyfm\Models\User;
|
||||
use Cover;
|
||||
use Gate;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Input;
|
||||
|
|
|
@ -61,7 +61,10 @@ class AlbumsController extends ApiControllerBase
|
|||
'tracks.cover',
|
||||
'tracks.genre',
|
||||
'tracks.user',
|
||||
'tracks.user.avatar',
|
||||
'tracks.trackFiles',
|
||||
'user',
|
||||
'user.avatar',
|
||||
'comments',
|
||||
'comments.user'
|
||||
])
|
||||
|
@ -146,7 +149,10 @@ class AlbumsController extends ApiControllerBase
|
|||
{
|
||||
$this->authorize('get-albums', $user);
|
||||
|
||||
$query = Album::summary()->where('user_id', $user->id)->orderBy('created_at', 'desc')->get();
|
||||
$query = Album::summary()
|
||||
->with('cover', 'user.avatar')
|
||||
->where('user_id', $user->id)
|
||||
->orderBy('created_at', 'desc')->get();
|
||||
$albums = [];
|
||||
|
||||
foreach ($query as $album) {
|
||||
|
|
|
@ -48,8 +48,13 @@ class ArtistsController extends ApiControllerBase
|
|||
'track.genre',
|
||||
'track.cover',
|
||||
'track.user',
|
||||
'track.user.avatar',
|
||||
'track.album',
|
||||
'track.album.cover',
|
||||
'track.album.user.avatar',
|
||||
'album.cover',
|
||||
'album.user',
|
||||
'album.user.avatar',
|
||||
'track' => function ($query) {
|
||||
$query->userDetails();
|
||||
},
|
||||
|
@ -84,8 +89,14 @@ class ArtistsController extends ApiControllerBase
|
|||
App::abort(404);
|
||||
}
|
||||
|
||||
$query = Track::summary()->published()->listed()->explicitFilter()->with('genre', 'cover',
|
||||
'user')->userDetails()->whereUserId($user->id)->whereNotNull('published_at');
|
||||
$query = Track::summary()
|
||||
->published()
|
||||
->listed()
|
||||
->explicitFilter()
|
||||
->with('genre', 'cover', 'user', 'user.avatar', 'album', 'album.cover')
|
||||
->userDetails()
|
||||
->whereUserId($user->id)
|
||||
->whereNotNull('published_at');
|
||||
$tracks = [];
|
||||
$singles = [];
|
||||
|
||||
|
@ -119,7 +130,7 @@ class ArtistsController extends ApiControllerBase
|
|||
->userDetails()
|
||||
->with([
|
||||
'comments' => function ($query) {
|
||||
$query->with('user');
|
||||
$query->with(['user', 'user.avatar']);
|
||||
}
|
||||
])
|
||||
->first();
|
||||
|
@ -131,7 +142,7 @@ class ArtistsController extends ApiControllerBase
|
|||
->published()
|
||||
->explicitFilter()
|
||||
->listed()
|
||||
->with('genre', 'cover', 'user')
|
||||
->with('genre', 'cover', 'user', 'album', 'album.cover')
|
||||
->userDetails()
|
||||
->whereUserId($user->id)
|
||||
->whereNotNull('published_at')
|
||||
|
@ -184,7 +195,8 @@ class ArtistsController extends ApiControllerBase
|
|||
'user_data' => $userData,
|
||||
'permissions' => [
|
||||
'edit' => Gate::allows('edit', $user)
|
||||
]
|
||||
],
|
||||
'isAdmin' => $user->hasRole('admin')
|
||||
]
|
||||
], 200);
|
||||
}
|
||||
|
|
63
app/Http/Controllers/Api/Web/NotificationsController.php
Normal file
63
app/Http/Controllers/Api/Web/NotificationsController.php
Normal file
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Pony.fm - A community for pony fan music.
|
||||
* Copyright (C) 2016 Peter Deltchev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
|
||||
|
||||
use Auth;
|
||||
use Carbon\Carbon;
|
||||
use Input;
|
||||
use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
|
||||
use Poniverse\Ponyfm\Models\Notification;
|
||||
|
||||
class NotificationsController extends ApiControllerBase
|
||||
{
|
||||
/**
|
||||
* Returns the logged-in user's last 20 notifications.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getNotifications()
|
||||
{
|
||||
$notifications = Notification::forUser(Auth::user())
|
||||
->take(20)
|
||||
->get();
|
||||
|
||||
|
||||
return ['notifications' => $notifications->toArray()];
|
||||
}
|
||||
|
||||
/**
|
||||
* This action returns the number of notifications that were updated.
|
||||
* Any notifications that were specified that don't belong to the logged-in
|
||||
* user are ignored.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function putMarkAsRead()
|
||||
{
|
||||
$notificationIds = Input::get('notification_ids');
|
||||
$numberOfUpdatedRows = Auth::user()
|
||||
->notifications()
|
||||
->whereIn('id', $notificationIds)
|
||||
->update(['is_read' => true]);
|
||||
|
||||
return ['notifications_updated' => $numberOfUpdatedRows];
|
||||
}
|
||||
}
|
|
@ -70,7 +70,14 @@ class PlaylistsController extends ApiControllerBase
|
|||
}
|
||||
|
||||
$query = Playlist::summary()
|
||||
->with('user', 'user.avatar', 'tracks', 'tracks.cover', 'tracks.user', 'tracks.album', 'tracks.album.user')
|
||||
->with('user',
|
||||
'user.avatar',
|
||||
'tracks',
|
||||
'tracks.cover',
|
||||
'tracks.user',
|
||||
'tracks.user.avatar',
|
||||
'tracks.album',
|
||||
'tracks.album.user')
|
||||
->userDetails()
|
||||
->orderBy('favourite_count', 'desc')
|
||||
->where('track_count', '>', 0)
|
||||
|
@ -103,6 +110,7 @@ class PlaylistsController extends ApiControllerBase
|
|||
'tracks' => function ($query) {
|
||||
$query->userDetails();
|
||||
},
|
||||
'tracks.trackFiles',
|
||||
'comments',
|
||||
'comments.user'
|
||||
])->userDetails()->find($id);
|
||||
|
|
|
@ -65,6 +65,8 @@ Route::get('playlist/{id}-{slug}', 'PlaylistsController@getPlaylist');
|
|||
Route::get('p{id}', 'PlaylistsController@getShortlink')->where('id', '\d+');
|
||||
Route::get('p{id}/dl.{extension}', 'PlaylistsController@getDownload' );
|
||||
|
||||
Route::get('notifications', 'AccountController@getNotifications');
|
||||
|
||||
|
||||
|
||||
Route::group(['prefix' => 'api/v1', 'middleware' => 'json-exceptions'], function() {
|
||||
|
@ -134,6 +136,8 @@ Route::group(['prefix' => 'api/web'], function() {
|
|||
|
||||
Route::group(['middleware' => 'auth'], function() {
|
||||
Route::get('/account/settings/{slug}', 'Api\Web\AccountController@getSettings');
|
||||
Route::get('/notifications', 'Api\Web\NotificationsController@getNotifications');
|
||||
Route::put('/notifications/mark-as-read', 'Api\Web\NotificationsController@putMarkAsRead');
|
||||
|
||||
Route::get('/tracks/owned', 'Api\Web\TracksController@getOwned');
|
||||
Route::get('/tracks/edit/{id}', 'Api\Web\TracksController@getEdit');
|
||||
|
|
72
app/Jobs/SendNotifications.php
Normal file
72
app/Jobs/SendNotifications.php
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Pony.fm - A community for pony fan music.
|
||||
* Copyright (C) 2016 Peter Deltchev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Poniverse\Ponyfm\Jobs;
|
||||
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Poniverse\Ponyfm\Jobs\Job;
|
||||
use Illuminate\Contracts\Bus\SelfHandling;
|
||||
use Poniverse\Ponyfm\Library\Notifications\Drivers\AbstractDriver;
|
||||
use Poniverse\Ponyfm\Library\Notifications\Drivers\PonyfmDriver;
|
||||
use Poniverse\Ponyfm\Models\User;
|
||||
use SerializesModels;
|
||||
|
||||
class SendNotifications extends Job implements SelfHandling, ShouldQueue
|
||||
{
|
||||
use InteractsWithQueue, SerializesModels;
|
||||
|
||||
protected $notificationType;
|
||||
protected $notificationData;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
* @param string $notificationType
|
||||
* @param array $notificationData
|
||||
*/
|
||||
public function __construct(string $notificationType, array $notificationData)
|
||||
{
|
||||
$this->notificationType = $notificationType;
|
||||
$this->notificationData = $notificationData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->beforeHandle();
|
||||
|
||||
// This variable is set here instead of as a static class variable
|
||||
// to work around a Laravel bug - namely, the SerializesModels trait
|
||||
// tries (and fails) to serialize static fields.
|
||||
$drivers = [
|
||||
PonyfmDriver::class
|
||||
];
|
||||
|
||||
foreach ($drivers as $driver) {
|
||||
/** @var $driver AbstractDriver */
|
||||
$driver = new $driver;
|
||||
call_user_func_array([$driver, $this->notificationType], $this->notificationData);
|
||||
}
|
||||
}
|
||||
}
|
47
app/Library/Notifications/Drivers/AbstractDriver.php
Normal file
47
app/Library/Notifications/Drivers/AbstractDriver.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Pony.fm - A community for pony fan music.
|
||||
* Copyright (C) 2016 Peter Deltchev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Poniverse\Ponyfm\Library\Notifications\Drivers;
|
||||
|
||||
use ArrayAccess;
|
||||
use Poniverse\Ponyfm\Contracts\NotificationHandler;
|
||||
use Poniverse\Ponyfm\Library\Notifications\RecipientFinder;
|
||||
use Poniverse\Ponyfm\Models\User;
|
||||
|
||||
abstract class AbstractDriver implements NotificationHandler {
|
||||
private $recipientFinder;
|
||||
|
||||
public function __construct() {
|
||||
$this->recipientFinder = new RecipientFinder(get_class($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of users who are to receive the given notification type.
|
||||
* This method is a wrapper around the {@link RecipientFinder} class, which
|
||||
* does the actual processing for all the drivers.
|
||||
*
|
||||
* @param string $notificationType
|
||||
* @param array $notificationData
|
||||
* @return User[] collection of {@link User} objects
|
||||
*/
|
||||
protected function getRecipients(string $notificationType, array $notificationData) {
|
||||
return call_user_func_array([$this->recipientFinder, $notificationType], $notificationData);
|
||||
}
|
||||
}
|
123
app/Library/Notifications/Drivers/PonyfmDriver.php
Normal file
123
app/Library/Notifications/Drivers/PonyfmDriver.php
Normal file
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Pony.fm - A community for pony fan music.
|
||||
* Copyright (C) 2016 Peter Deltchev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Poniverse\Ponyfm\Library\Notifications\Drivers;
|
||||
|
||||
|
||||
use ArrayAccess;
|
||||
use Carbon\Carbon;
|
||||
use Poniverse\Ponyfm\Contracts\Favouritable;
|
||||
use Poniverse\Ponyfm\Models\Activity;
|
||||
use Poniverse\Ponyfm\Models\Comment;
|
||||
use Poniverse\Ponyfm\Models\Notification;
|
||||
use Poniverse\Ponyfm\Models\Playlist;
|
||||
use Poniverse\Ponyfm\Models\Track;
|
||||
use Poniverse\Ponyfm\Models\User;
|
||||
|
||||
class PonyfmDriver extends AbstractDriver {
|
||||
/**
|
||||
* A helper method for bulk insertion of notification records.
|
||||
*
|
||||
* @param int $activityId
|
||||
* @param User[] $recipients collection of {@link User} objects
|
||||
*/
|
||||
private function insertNotifications(int $activityId, $recipients) {
|
||||
$notifications = [];
|
||||
foreach ($recipients as $recipient) {
|
||||
$notifications[] = [
|
||||
'activity_id' => $activityId,
|
||||
'user_id' => $recipient->id
|
||||
];
|
||||
}
|
||||
Notification::insert($notifications);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function publishedNewTrack(Track $track) {
|
||||
$activity = Activity::create([
|
||||
'created_at' => Carbon::now(),
|
||||
'user_id' => $track->user_id,
|
||||
'activity_type' => Activity::TYPE_PUBLISHED_TRACK,
|
||||
'resource_type' => Track::class,
|
||||
'resource_id' => $track->id,
|
||||
]);
|
||||
|
||||
$this->insertNotifications($activity->id, $this->getRecipients(__FUNCTION__, func_get_args()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function publishedNewPlaylist(Playlist $playlist) {
|
||||
$activity = Activity::create([
|
||||
'created_at' => Carbon::now(),
|
||||
'user_id' => $playlist->user_id,
|
||||
'activity_type' => Activity::TYPE_PUBLISHED_PLAYLIST,
|
||||
'resource_type' => Playlist::class,
|
||||
'resource_id' => $playlist->id,
|
||||
]);
|
||||
|
||||
$this->insertNotifications($activity->id, $this->getRecipients(__FUNCTION__, func_get_args()));
|
||||
}
|
||||
|
||||
public function newFollower(User $userBeingFollowed, User $follower) {
|
||||
$activity = Activity::create([
|
||||
'created_at' => Carbon::now(),
|
||||
'user_id' => $follower->id,
|
||||
'activity_type' => Activity::TYPE_NEW_FOLLOWER,
|
||||
'resource_type' => User::class,
|
||||
'resource_id' => $userBeingFollowed->id,
|
||||
]);
|
||||
|
||||
$this->insertNotifications($activity->id, $this->getRecipients(__FUNCTION__, func_get_args()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function newComment(Comment $comment) {
|
||||
$activity = Activity::create([
|
||||
'created_at' => Carbon::now(),
|
||||
'user_id' => $comment->user_id,
|
||||
'activity_type' => Activity::TYPE_NEW_COMMENT,
|
||||
'resource_type' => Comment::class,
|
||||
'resource_id' => $comment->id,
|
||||
]);
|
||||
|
||||
$this->insertNotifications($activity->id, $this->getRecipients(__FUNCTION__, func_get_args()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function newFavourite(Favouritable $entityBeingFavourited, User $favouriter) {
|
||||
$activity = Activity::create([
|
||||
'created_at' => Carbon::now(),
|
||||
'user_id' => $favouriter->id,
|
||||
'activity_type' => Activity::TYPE_CONTENT_FAVOURITED,
|
||||
'resource_type' => get_class($entityBeingFavourited),
|
||||
'resource_id' => $entityBeingFavourited->id,
|
||||
]);
|
||||
|
||||
$this->insertNotifications($activity->id, $this->getRecipients(__FUNCTION__, func_get_args()));
|
||||
}
|
||||
}
|
83
app/Library/Notifications/NotificationManager.php
Normal file
83
app/Library/Notifications/NotificationManager.php
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Pony.fm - A community for pony fan music.
|
||||
* Copyright (C) 2016 Peter Deltchev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Poniverse\Ponyfm\Library\Notifications;
|
||||
|
||||
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Poniverse\Ponyfm\Contracts\Favouritable;
|
||||
use Poniverse\Ponyfm\Contracts\NotificationHandler;
|
||||
use Poniverse\Ponyfm\Jobs\SendNotifications;
|
||||
use Poniverse\Ponyfm\Models\Comment;
|
||||
use Poniverse\Ponyfm\Models\Playlist;
|
||||
use Poniverse\Ponyfm\Models\Track;
|
||||
use Poniverse\Ponyfm\Models\User;
|
||||
|
||||
/**
|
||||
* Class NotificationManager
|
||||
* @package Poniverse\Ponyfm\Library
|
||||
*
|
||||
* This class exists mostly to maintain type safety when sending notifications
|
||||
* from around the Pony.fm codebase. There should be virtually zero logic here.
|
||||
* All the heavy lifting happens asynchronously in the {@link SendNotifications}
|
||||
* job and the notification drivers.
|
||||
*/
|
||||
class NotificationManager implements NotificationHandler {
|
||||
use DispatchesJobs;
|
||||
|
||||
private function dispatchNotification(string $notificationType, array $notificationData) {
|
||||
$this->dispatch( (new SendNotifications($notificationType, $notificationData))->onQueue('notifications') );
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function publishedNewTrack(Track $track) {
|
||||
$this->dispatchNotification(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function publishedNewPlaylist(Playlist $playlist) {
|
||||
$this->dispatchNotification(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function newFollower(User $userBeingFollowed, User $follower) {
|
||||
$this->dispatchNotification(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function newComment(Comment $comment) {
|
||||
$this->dispatchNotification(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function newFavourite(Favouritable $entityBeingFavourited, User $favouriter) {
|
||||
$this->dispatchNotification(__FUNCTION__, func_get_args());
|
||||
}
|
||||
}
|
120
app/Library/Notifications/RecipientFinder.php
Normal file
120
app/Library/Notifications/RecipientFinder.php
Normal file
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Pony.fm - A community for pony fan music.
|
||||
* Copyright (C) 2016 Peter Deltchev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Poniverse\Ponyfm\Library\Notifications;
|
||||
|
||||
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Poniverse\Ponyfm\Contracts\Favouritable;
|
||||
use Poniverse\Ponyfm\Contracts\NotificationHandler;
|
||||
use Poniverse\Ponyfm\Jobs\SendNotifications;
|
||||
use Poniverse\Ponyfm\Library\Notifications\Drivers\PonyfmDriver;
|
||||
use Poniverse\Ponyfm\Models\Comment;
|
||||
use Poniverse\Ponyfm\Models\Playlist;
|
||||
use Poniverse\Ponyfm\Models\Track;
|
||||
use Poniverse\Ponyfm\Models\User;
|
||||
|
||||
/**
|
||||
* Class RecipientFinder
|
||||
* @package Poniverse\Ponyfm\Library\Notifications
|
||||
*
|
||||
* This class returns a list of users who are to receive a particular notification.
|
||||
* It is instantiated on a per-driver basis.
|
||||
*/
|
||||
class RecipientFinder implements NotificationHandler {
|
||||
/**
|
||||
* @var string class name of a notification driver
|
||||
*/
|
||||
private $notificationDriver;
|
||||
|
||||
public function __construct(string $notificationDriver) {
|
||||
$this->notificationDriver = $notificationDriver;
|
||||
}
|
||||
|
||||
private function fail() {
|
||||
throw new \InvalidArgumentException("Unknown notification driver given: {$this->notificationDriver}");
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function publishedNewTrack(Track $track) {
|
||||
switch ($this->notificationDriver) {
|
||||
case PonyfmDriver::class:
|
||||
return $track->user->followers;
|
||||
default:
|
||||
return $this->fail();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function publishedNewPlaylist(Playlist $playlist) {
|
||||
switch ($this->notificationDriver) {
|
||||
case PonyfmDriver::class:
|
||||
return $playlist->user->followers;
|
||||
default:
|
||||
return $this->fail();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function newFollower(User $userBeingFollowed, User $follower) {
|
||||
switch ($this->notificationDriver) {
|
||||
case PonyfmDriver::class:
|
||||
return [$userBeingFollowed];
|
||||
default:
|
||||
return $this->fail();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function newComment(Comment $comment) {
|
||||
switch ($this->notificationDriver) {
|
||||
case PonyfmDriver::class:
|
||||
return
|
||||
$comment->user->id === $comment->resource->user->id
|
||||
? []
|
||||
: [$comment->resource->user];
|
||||
default:
|
||||
return $this->fail();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function newFavourite(Favouritable $entityBeingFavourited, User $favouriter) {
|
||||
switch ($this->notificationDriver) {
|
||||
case PonyfmDriver::class:
|
||||
return
|
||||
$favouriter->id === $entityBeingFavourited->user->id
|
||||
? []
|
||||
: [$entityBeingFavourited->user];
|
||||
default:
|
||||
return $this->fail();
|
||||
}
|
||||
}
|
||||
}
|
205
app/Models/Activity.php
Normal file
205
app/Models/Activity.php
Normal file
|
@ -0,0 +1,205 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Pony.fm - A community for pony fan music.
|
||||
* Copyright (C) 2016 Peter Deltchev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Poniverse\Ponyfm\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Poniverse\Ponyfm\Models\Activity
|
||||
*
|
||||
* @property integer $id
|
||||
* @property \Carbon\Carbon $created_at
|
||||
* @property integer $user_id
|
||||
* @property boolean $activity_type
|
||||
* @property boolean $resource_type
|
||||
* @property integer $resource_id
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Notification[] $notifications
|
||||
* @property-read \Poniverse\Ponyfm\Models\User $initiatingUser
|
||||
* @property-read \Poniverse\Ponyfm\Models\Activity $resource
|
||||
* @property-read mixed $url
|
||||
* @property-read mixed $thumbnail_url
|
||||
* @property-read mixed $text
|
||||
*/
|
||||
class Activity extends Model {
|
||||
public $timestamps = false;
|
||||
protected $dates = ['created_at'];
|
||||
protected $fillable = ['created_at', 'user_id', 'activity_type', 'resource_type', 'resource_id'];
|
||||
protected $appends = ['url', 'thumbnail_url', 'human_friendly_resource_type'];
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
'user_id' => 'integer',
|
||||
'activity_type' => 'integer',
|
||||
// resource_type has its own accessor and mutator
|
||||
'resource_id' => 'integer',
|
||||
];
|
||||
|
||||
const TYPE_NEWS = 1;
|
||||
const TYPE_PUBLISHED_TRACK = 2;
|
||||
const TYPE_PUBLISHED_ALBUM = 3;
|
||||
const TYPE_PUBLISHED_PLAYLIST = 4;
|
||||
const TYPE_NEW_FOLLOWER = 5;
|
||||
const TYPE_NEW_COMMENT = 6;
|
||||
const TYPE_CONTENT_FAVOURITED = 7;
|
||||
|
||||
/**
|
||||
* These "target" constants are an implementation detail of this model and
|
||||
* should not be used directly in other classes. They're used to efficiently
|
||||
* store the type of resource this notification is about in the database.
|
||||
*
|
||||
* The "resource_type" attribute is transformed into a class name at runtime
|
||||
* so that the use of an integer in the database to represent this info
|
||||
* remains an implementation detail of this model. Outside of this class,
|
||||
* the resource_type attribute should be treated as a fully-qualified class
|
||||
* name.
|
||||
*/
|
||||
const TARGET_USER = 1;
|
||||
const TARGET_TRACK = 2;
|
||||
const TARGET_ALBUM = 3;
|
||||
const TARGET_PLAYLIST = 4;
|
||||
const TARGET_COMMENT = 5;
|
||||
|
||||
public function initiatingUser() {
|
||||
return $this->belongsTo(User::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
public function notifications() {
|
||||
return $this->hasMany(Notification::class, 'activity_id', 'id');
|
||||
}
|
||||
|
||||
public function notificationRecipients() {
|
||||
return $this->hasManyThrough(User::class, Notification::class, 'activity_id', 'user_id', 'id');
|
||||
}
|
||||
|
||||
public function resource() {
|
||||
return $this->morphTo('resource', 'resource_type', 'resource_id');
|
||||
}
|
||||
|
||||
public function getUrlAttribute() {
|
||||
return $this->resource->url;
|
||||
}
|
||||
|
||||
public function getResourceTypeAttribute($value) {
|
||||
switch ($value) {
|
||||
case static::TARGET_USER:
|
||||
return User::class;
|
||||
|
||||
case static::TARGET_TRACK:
|
||||
return Track::class;
|
||||
|
||||
case static::TARGET_ALBUM:
|
||||
return Album::class;
|
||||
|
||||
case static::TARGET_PLAYLIST:
|
||||
return Playlist::class;
|
||||
|
||||
case static::TARGET_COMMENT:
|
||||
return Comment::class;
|
||||
|
||||
default:
|
||||
// Null must be returned here for Eloquent's eager-loading
|
||||
// of the polymorphic relation to work.
|
||||
return NULL;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public function setResourceTypeAttribute($value) {
|
||||
switch ($value) {
|
||||
case User::class:
|
||||
$this->attributes['resource_type'] = static::TARGET_USER;
|
||||
break;
|
||||
|
||||
case Track::class:
|
||||
$this->attributes['resource_type'] = static::TARGET_TRACK;
|
||||
break;
|
||||
|
||||
case Album::class:
|
||||
$this->attributes['resource_type'] = static::TARGET_ALBUM;
|
||||
break;
|
||||
|
||||
case Playlist::class:
|
||||
$this->attributes['resource_type'] = static::TARGET_PLAYLIST;
|
||||
break;
|
||||
|
||||
case Comment::class:
|
||||
$this->attributes['resource_type'] = static::TARGET_COMMENT;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function getThumbnailUrlAttribute()
|
||||
{
|
||||
switch ($this->resource_type) {
|
||||
case User::class:
|
||||
return $this->resource->getAvatarUrl(Image::THUMBNAIL);
|
||||
|
||||
case Track::class:
|
||||
case Album::class:
|
||||
case Playlist::class:
|
||||
return $this->resource->getCoverUrl(Image::THUMBNAIL);
|
||||
|
||||
case Comment::class:
|
||||
return $this->resource->user->getAvatarUrl(Image::THUMBNAIL);
|
||||
|
||||
default:
|
||||
throw new \Exception('This activity\'s resource is of an unknown type!');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string human-readable Markdown string describing this notification
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getTextAttribute()
|
||||
{
|
||||
switch ($this->activity_type) {
|
||||
case static::TYPE_NEWS:
|
||||
// not implemented yet
|
||||
throw new \InvalidArgumentException('This type of activity has not been implemented yet!');
|
||||
|
||||
case static::TYPE_PUBLISHED_TRACK:
|
||||
return "{$this->resource->user->display_name} published a new track, __{$this->resource->title}__!";
|
||||
|
||||
case static::TYPE_PUBLISHED_PLAYLIST:
|
||||
return "{$this->resource->user->display_name} published a new playlist, __{$this->resource->title}__!";
|
||||
|
||||
case static::TYPE_NEW_FOLLOWER:
|
||||
return "{$this->initiatingUser->display_name} is now following you!";
|
||||
|
||||
case static::TYPE_NEW_COMMENT:
|
||||
// Is this a profile comment?
|
||||
if ($this->resource_type === User::class) {
|
||||
return "{$this->initiatingUser->display_name} left a comment on your profile!";
|
||||
|
||||
// Must be a content comment.
|
||||
} else {
|
||||
return "{$this->initiatingUser->display_name} left a comment on your {$this->resource->resource->getResourceType()}, __{$this->resource->resource->title}__!";
|
||||
}
|
||||
|
||||
case static::TYPE_CONTENT_FAVOURITED:
|
||||
return "{$this->initiatingUser->display_name} favourited your {$this->resource->type}, __{$this->resource->title}__!";
|
||||
|
||||
default:
|
||||
throw new \Exception('This activity\'s activity type is unknown!');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,10 +23,14 @@ namespace Poniverse\Ponyfm\Models;
|
|||
use Exception;
|
||||
use Helpers;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Auth;
|
||||
use Gate;
|
||||
use Cache;
|
||||
use Poniverse\Ponyfm\Contracts\Commentable;
|
||||
use Poniverse\Ponyfm\Contracts\Favouritable;
|
||||
use Poniverse\Ponyfm\Contracts\Searchable;
|
||||
use Poniverse\Ponyfm\Exceptions\TrackFileNotFoundException;
|
||||
use Poniverse\Ponyfm\Traits\IndexedInElasticsearchTrait;
|
||||
|
@ -60,8 +64,9 @@ use Venturecraft\Revisionable\RevisionableTrait;
|
|||
* @property-read mixed $url
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\Venturecraft\Revisionable\Revision[] $revisionHistory
|
||||
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Album userDetails()
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Activity[] $activities
|
||||
*/
|
||||
class Album extends Model implements Searchable
|
||||
class Album extends Model implements Searchable, Commentable, Favouritable
|
||||
{
|
||||
use SoftDeletes, SlugTrait, TrackCollection, RevisionableTrait, IndexedInElasticsearchTrait;
|
||||
|
||||
|
@ -101,7 +106,7 @@ class Album extends Model implements Searchable
|
|||
return $this->hasMany('Poniverse\Ponyfm\Models\ResourceUser');
|
||||
}
|
||||
|
||||
public function favourites()
|
||||
public function favourites():HasMany
|
||||
{
|
||||
return $this->hasMany('Poniverse\Ponyfm\Models\Favourite');
|
||||
}
|
||||
|
@ -120,11 +125,15 @@ class Album extends Model implements Searchable
|
|||
return $this->hasManyThrough(TrackFile::class, Track::class, 'album_id', 'track_id');
|
||||
}
|
||||
|
||||
public function comments()
|
||||
public function comments():HasMany
|
||||
{
|
||||
return $this->hasMany('Poniverse\Ponyfm\Models\Comment')->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
public function activities():MorphMany {
|
||||
return $this->morphMany(Activity::class, 'resource');
|
||||
}
|
||||
|
||||
public static function mapPublicAlbumShow(Album $album)
|
||||
{
|
||||
$tracks = [];
|
||||
|
@ -429,4 +438,14 @@ class Album extends Model implements Searchable
|
|||
public function shouldBeIndexed():bool {
|
||||
return $this->track_count > 0 && !$this->trashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the corresponding resource type ID from the Activity class for
|
||||
* this resource.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getResourceType():string {
|
||||
return 'album';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,10 @@
|
|||
namespace Poniverse\Ponyfm\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Poniverse\Ponyfm\Contracts\Commentable;
|
||||
use Poniverse\Ponyfm\Contracts\GeneratesNotifications;
|
||||
|
||||
/**
|
||||
* Poniverse\Ponyfm\Models\Comment
|
||||
|
@ -42,7 +45,9 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||
* @property-read \Poniverse\Ponyfm\Models\Album $album
|
||||
* @property-read \Poniverse\Ponyfm\Models\Playlist $playlist
|
||||
* @property-read \Poniverse\Ponyfm\Models\User $profile
|
||||
* @property-read mixed $resource
|
||||
* @property-read Commentable $resource
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Activity[] $activities
|
||||
* @property-read mixed $url
|
||||
*/
|
||||
class Comment extends Model
|
||||
{
|
||||
|
@ -78,6 +83,16 @@ class Comment extends Model
|
|||
return $this->belongsTo('Poniverse\Ponyfm\Models\User', 'profile_id');
|
||||
}
|
||||
|
||||
public function activities():MorphMany
|
||||
{
|
||||
return $this->morphMany(Activity::class, 'resource');
|
||||
}
|
||||
|
||||
public function getUrlAttribute()
|
||||
{
|
||||
return $this->resource->url;
|
||||
}
|
||||
|
||||
public static function mapPublic($comment)
|
||||
{
|
||||
return [
|
||||
|
@ -97,7 +112,7 @@ class Comment extends Model
|
|||
];
|
||||
}
|
||||
|
||||
public function getResourceAttribute()
|
||||
public function getResourceAttribute():Commentable
|
||||
{
|
||||
if ($this->track_id !== null) {
|
||||
return $this->track;
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
namespace Poniverse\Ponyfm\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* Poniverse\Ponyfm\Models\Follower
|
||||
|
@ -30,10 +31,21 @@ use Illuminate\Database\Eloquent\Model;
|
|||
* @property integer $artist_id
|
||||
* @property integer $playlist_id
|
||||
* @property string $created_at
|
||||
* @property-read \Poniverse\Ponyfm\Models\User $follower
|
||||
* @property-read \Poniverse\Ponyfm\Models\User $artist
|
||||
*/
|
||||
class Follower extends Model
|
||||
{
|
||||
protected $table = 'followers';
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
|
||||
public function follower():BelongsTo {
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function artist():BelongsTo {
|
||||
return $this->belongsTo(User::class, 'artist_id');
|
||||
}
|
||||
}
|
||||
|
|
83
app/Models/Notification.php
Normal file
83
app/Models/Notification.php
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Pony.fm - A community for pony fan music.
|
||||
* Copyright (C) 2016 Peter Deltchev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Poniverse\Ponyfm\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Poniverse\Ponyfm\Models\Notification
|
||||
*
|
||||
* @property integer $id
|
||||
* @property integer $activity_id
|
||||
* @property integer $user_id
|
||||
* @property boolean $is_read
|
||||
* @property-read \Poniverse\Ponyfm\Models\Activity $activity
|
||||
* @property-read \Poniverse\Ponyfm\Models\User $recipient
|
||||
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Notification forUser($user)
|
||||
*/
|
||||
class Notification extends Model {
|
||||
public $timestamps = false;
|
||||
protected $fillable = ['activity_id', 'user_id'];
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'activity_id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'is_read' => 'boolean',
|
||||
];
|
||||
|
||||
public function activity() {
|
||||
return $this->belongsTo(Activity::class, 'activity_id', 'id');
|
||||
}
|
||||
|
||||
public function recipient() {
|
||||
return $this->belongsTo(User::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* This scope grabs eager-loaded notifications for the given user.
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param User $user
|
||||
* @return Builder
|
||||
*/
|
||||
public function scopeForUser(Builder $query, User $user) {
|
||||
return $query->with([
|
||||
'activity',
|
||||
'activity.initiatingUser',
|
||||
'activity.resource',
|
||||
'activity.resource.user',
|
||||
])
|
||||
->join('activities', 'notifications.activity_id', '=', 'activities.id')
|
||||
->where('notifications.user_id', $user->id)
|
||||
->orderBy('activities.created_at', 'DESC');
|
||||
}
|
||||
|
||||
public function toArray() {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'date' => $this->activity->created_at->toAtomString(),
|
||||
'thumbnail_url' => $this->activity->thumbnail_url,
|
||||
'text' => $this->activity->text,
|
||||
'url' => $this->activity->url
|
||||
];
|
||||
}
|
||||
}
|
|
@ -22,10 +22,14 @@ namespace Poniverse\Ponyfm\Models;
|
|||
|
||||
use Helpers;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Auth;
|
||||
use Cache;
|
||||
use Poniverse\Ponyfm\Contracts\Commentable;
|
||||
use Poniverse\Ponyfm\Contracts\Favouritable;
|
||||
use Poniverse\Ponyfm\Contracts\Searchable;
|
||||
use Poniverse\Ponyfm\Exceptions\TrackFileNotFoundException;
|
||||
use Poniverse\Ponyfm\Traits\IndexedInElasticsearchTrait;
|
||||
|
@ -59,8 +63,10 @@ use Venturecraft\Revisionable\RevisionableTrait;
|
|||
* @property-read mixed $url
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\Venturecraft\Revisionable\Revision[] $revisionHistory
|
||||
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Playlist userDetails()
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Favourite[] $favourites
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Activity[] $activities
|
||||
*/
|
||||
class Playlist extends Model implements Searchable
|
||||
class Playlist extends Model implements Searchable, Commentable, Favouritable
|
||||
{
|
||||
use SoftDeletes, SlugTrait, TrackCollection, RevisionableTrait, IndexedInElasticsearchTrait;
|
||||
|
||||
|
@ -212,7 +218,7 @@ class Playlist extends Model implements Searchable
|
|||
return $this->hasMany('Poniverse\Ponyfm\Models\ResourceUser');
|
||||
}
|
||||
|
||||
public function comments()
|
||||
public function comments():HasMany
|
||||
{
|
||||
return $this->hasMany('Poniverse\Ponyfm\Models\Comment')->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
@ -222,11 +228,19 @@ class Playlist extends Model implements Searchable
|
|||
return $this->hasMany('Poniverse\Ponyfm\Models\PinnedPlaylist');
|
||||
}
|
||||
|
||||
public function favourites():HasMany {
|
||||
return $this->hasMany(Favourite::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('Poniverse\Ponyfm\Models\User');
|
||||
}
|
||||
|
||||
public function activities():MorphMany {
|
||||
return $this->morphMany(Activity::class, 'resource');
|
||||
}
|
||||
|
||||
public function hasPinFor($userId)
|
||||
{
|
||||
foreach ($this->pins as $pin) {
|
||||
|
@ -324,4 +338,11 @@ class Playlist extends Model implements Searchable
|
|||
$this->track_count > 0 &&
|
||||
!$this->trashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getResourceType():string {
|
||||
return 'playlist';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,10 @@ use Cache;
|
|||
use Config;
|
||||
use DB;
|
||||
use Gate;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Poniverse\Ponyfm\Contracts\Commentable;
|
||||
use Poniverse\Ponyfm\Contracts\Favouritable;
|
||||
use Poniverse\Ponyfm\Contracts\Searchable;
|
||||
use Poniverse\Ponyfm\Exceptions\TrackFileNotFoundException;
|
||||
use Poniverse\Ponyfm\Traits\IndexedInElasticsearchTrait;
|
||||
|
@ -95,8 +99,10 @@ use Venturecraft\Revisionable\RevisionableTrait;
|
|||
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Track explicitFilter()
|
||||
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Track withComments()
|
||||
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Track mlpma()
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Activity[] $notifications
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Activity[] $activities
|
||||
*/
|
||||
class Track extends Model implements Searchable
|
||||
class Track extends Model implements Searchable, Commentable, Favouritable
|
||||
{
|
||||
use SoftDeletes, IndexedInElasticsearchTrait;
|
||||
|
||||
|
@ -204,7 +210,8 @@ class Track extends Model implements Searchable
|
|||
return self::select('tracks.id', 'title', 'user_id', 'slug', 'is_vocal', 'is_explicit', 'created_at',
|
||||
'published_at',
|
||||
'duration', 'is_downloadable', 'genre_id', 'track_type_id', 'cover_id', 'album_id', 'comment_count',
|
||||
'download_count', 'view_count', 'play_count', 'favourite_count');
|
||||
'download_count', 'view_count', 'play_count', 'favourite_count')
|
||||
->with('user', 'cover', 'album');
|
||||
}
|
||||
|
||||
public function scopeUserDetails($query)
|
||||
|
@ -256,6 +263,7 @@ class Track extends Model implements Searchable
|
|||
|
||||
/**
|
||||
* @param integer $count
|
||||
* @return array
|
||||
*/
|
||||
public static function popular($count, $allowExplicit = false)
|
||||
{
|
||||
|
@ -486,12 +494,12 @@ class Track extends Model implements Searchable
|
|||
return $this->belongsTo('Poniverse\Ponyfm\Models\TrackType', 'track_type_id');
|
||||
}
|
||||
|
||||
public function comments()
|
||||
public function comments():HasMany
|
||||
{
|
||||
return $this->hasMany('Poniverse\Ponyfm\Models\Comment')->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
public function favourites()
|
||||
public function favourites():HasMany
|
||||
{
|
||||
return $this->hasMany('Poniverse\Ponyfm\Models\Favourite');
|
||||
}
|
||||
|
@ -526,6 +534,15 @@ class Track extends Model implements Searchable
|
|||
return $this->hasMany('Poniverse\Ponyfm\Models\TrackFile');
|
||||
}
|
||||
|
||||
public function notifications()
|
||||
{
|
||||
return $this->morphMany(Activity::class, 'notification_type');
|
||||
}
|
||||
|
||||
public function activities():MorphMany {
|
||||
return $this->morphMany(Activity::class, 'resource');
|
||||
}
|
||||
|
||||
public function getYearAttribute()
|
||||
{
|
||||
return date('Y', strtotime($this->getReleaseDate()));
|
||||
|
@ -546,7 +563,7 @@ class Track extends Model implements Searchable
|
|||
*/
|
||||
public function getFilesize($formatName)
|
||||
{
|
||||
$trackFile = $this->trackFiles()->where('format', $formatName)->first();
|
||||
$trackFile = $this->trackFiles->where('format', $formatName)->first();
|
||||
|
||||
if ($trackFile) {
|
||||
return (int) $trackFile->filesize;
|
||||
|
@ -664,7 +681,9 @@ class Track extends Model implements Searchable
|
|||
}
|
||||
|
||||
/**
|
||||
* @param $format
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getFileFor($format)
|
||||
{
|
||||
|
@ -863,4 +882,8 @@ class Track extends Model implements Searchable
|
|||
'show_songs' => $this->showSongs->pluck('title')
|
||||
];
|
||||
}
|
||||
|
||||
public function getResourceType():string {
|
||||
return 'track';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,9 +26,12 @@ use Illuminate\Auth\Passwords\CanResetPassword;
|
|||
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Foundation\Auth\Access\Authorizable;
|
||||
use Auth;
|
||||
use Illuminate\Support\Str;
|
||||
use Poniverse\Ponyfm\Contracts\Commentable;
|
||||
use Poniverse\Ponyfm\Contracts\Searchable;
|
||||
use Poniverse\Ponyfm\Traits\IndexedInElasticsearchTrait;
|
||||
use Venturecraft\Revisionable\RevisionableTrait;
|
||||
|
@ -63,8 +66,12 @@ use Venturecraft\Revisionable\RevisionableTrait;
|
|||
* @property-read mixed $message_url
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\Venturecraft\Revisionable\Revision[] $revisionHistory
|
||||
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\User userDetails()
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Notification[] $notifications
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\User[] $followers
|
||||
* @property-read mixed $user
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Activity[] $activities
|
||||
*/
|
||||
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, \Illuminate\Contracts\Auth\Access\Authorizable, Searchable
|
||||
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, \Illuminate\Contracts\Auth\Access\Authorizable, Searchable, Commentable
|
||||
{
|
||||
use Authenticatable, CanResetPassword, Authorizable, RevisionableTrait, IndexedInElasticsearchTrait;
|
||||
|
||||
|
@ -134,12 +141,17 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
|||
return $this->hasMany('Poniverse\Ponyfm\Models\ResourceUser', 'artist_id');
|
||||
}
|
||||
|
||||
public function followers()
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'followers', 'artist_id', 'user_id');
|
||||
}
|
||||
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany(Role::class, 'role_user');
|
||||
}
|
||||
|
||||
public function comments()
|
||||
public function comments():HasMany
|
||||
{
|
||||
return $this->hasMany('Poniverse\Ponyfm\Models\Comment', 'profile_id')->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
@ -149,6 +161,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
|||
return $this->hasMany('Poniverse\Ponyfm\Models\Track', 'user_id');
|
||||
}
|
||||
|
||||
public function notifications()
|
||||
{
|
||||
return $this->hasMany(Notification::class, 'user_id');
|
||||
}
|
||||
|
||||
public function notificationActivities()
|
||||
{
|
||||
return $this->hasManyThrough(Activity::class, Notification::class, 'user_id', 'notification_id', 'id');
|
||||
}
|
||||
|
||||
public function getIsArchivedAttribute()
|
||||
{
|
||||
return (bool) $this->attributes['is_archived'];
|
||||
|
@ -235,6 +257,21 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
|||
return "remember_token";
|
||||
}
|
||||
|
||||
public function getUserAttribute():User {
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getResourceType():string {
|
||||
return 'profile';
|
||||
}
|
||||
|
||||
public function activities():MorphMany {
|
||||
return $this->morphMany(Activity::class, 'resource');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this user has the given role.
|
||||
*
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
namespace Poniverse\Ponyfm\Providers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use PfmValidator;
|
||||
|
@ -58,5 +59,16 @@ class AppServiceProvider extends ServiceProvider
|
|||
$app['config']->get('ponyfm.elasticsearch_index')
|
||||
);
|
||||
});
|
||||
|
||||
// NOTE: Use integer keys exclusively for Pony.fm's morphMap to avoid
|
||||
// any weirdness with merging array indices. $merge = false is
|
||||
// set below so that no morphMap array merging happens!
|
||||
Relation::morphMap([
|
||||
Poniverse\Ponyfm\Models\Activity::TARGET_TRACK => Poniverse\Ponyfm\Models\Track::class,
|
||||
Poniverse\Ponyfm\Models\Activity::TARGET_ALBUM => Poniverse\Ponyfm\Models\Album::class,
|
||||
Poniverse\Ponyfm\Models\Activity::TARGET_PLAYLIST => Poniverse\Ponyfm\Models\Playlist::class,
|
||||
Poniverse\Ponyfm\Models\Activity::TARGET_USER => Poniverse\Ponyfm\Models\User::class,
|
||||
Poniverse\Ponyfm\Models\Activity::TARGET_COMMENT => Poniverse\Ponyfm\Models\Comment::class,
|
||||
], false);
|
||||
}
|
||||
}
|
||||
|
|
50
app/Providers/NotificationServiceProvider.php
Normal file
50
app/Providers/NotificationServiceProvider.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Pony.fm - A community for pony fan music.
|
||||
* Copyright (C) 2016 Peter Deltchev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Poniverse\Ponyfm\Providers;
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Poniverse\Ponyfm\Library\Notifications\NotificationManager;
|
||||
|
||||
class NotificationServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton('notification', function(Application $app){
|
||||
return new NotificationManager();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -145,6 +145,7 @@ return [
|
|||
Poniverse\Ponyfm\Providers\EventServiceProvider::class,
|
||||
Poniverse\Ponyfm\Providers\RouteServiceProvider::class,
|
||||
Poniverse\Ponyfm\Providers\AuthServiceProvider::class,
|
||||
Poniverse\Ponyfm\Providers\NotificationServiceProvider::class,
|
||||
|
||||
Intouch\LaravelNewrelic\NewrelicServiceProvider::class,
|
||||
Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
|
||||
|
@ -201,6 +202,7 @@ return [
|
|||
|
||||
'Elasticsearch' => Cviebrock\LaravelElasticsearch\Facade::class,
|
||||
'Newrelic' => Intouch\LaravelNewrelic\Facades\Newrelic::class,
|
||||
'Notification' => Poniverse\Ponyfm\Facades\Notification::class,
|
||||
|
||||
],
|
||||
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Pony.fm - A community for pony fan music.
|
||||
* Copyright (C) 2016 Peter Deltchev
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateNotificationsTables extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('activities', function(Blueprint $table){
|
||||
$table->bigIncrements('id');
|
||||
$table->dateTime('created_at')->index();
|
||||
$table->unsignedInteger('user_id'); // initiator of the action
|
||||
$table->unsignedTinyInteger('activity_type');
|
||||
$table->unsignedTinyInteger('resource_type');
|
||||
$table->unsignedInteger('resource_id'); // ID of the entity this activity is about
|
||||
});
|
||||
|
||||
Schema::create('notifications', function(Blueprint $table){
|
||||
// Notifications are a pivot table between activities and users.
|
||||
$table->bigIncrements('id');
|
||||
$table->unsignedBigInteger('activity_id')->index();
|
||||
$table->unsignedInteger('user_id')->index(); // recipient of the notification
|
||||
$table->boolean('is_read')->default(false)->index();
|
||||
|
||||
$table->foreign('activity_id')->references('id')->on('activities')->onDelete('cascade');
|
||||
$table->foreign('user_id')->references('id')->on('users');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::drop('notifications');
|
||||
Schema::drop('activities');
|
||||
}
|
||||
}
|
94
documentation/notifications.md
Normal file
94
documentation/notifications.md
Normal file
|
@ -0,0 +1,94 @@
|
|||
Developing notifications for Pony.fm
|
||||
====================================
|
||||
|
||||
Pony.fm's notification system is designed around "drivers" for various
|
||||
notification delivery methods. The types of notification one can receive
|
||||
are defined in the
|
||||
[`NotificationHandler`](app/Contracts/NotificationHandler.php)
|
||||
interface, which is implemented by every class that needs to know about
|
||||
the various notification types.
|
||||
|
||||
|
||||
Sending a notification
|
||||
----------------------
|
||||
|
||||
The `Notification` facade is used to send notifications as follows:
|
||||
|
||||
```php
|
||||
use Notification;
|
||||
|
||||
// Something happens, like a newtrack getting published.
|
||||
$track = new Track();
|
||||
...
|
||||
|
||||
// The "something" is done happening! Time to send a notification.
|
||||
Notification::publishedTrack($track);
|
||||
```
|
||||
|
||||
This facade has a method for every notification type, drawn from the
|
||||
[`NotificationHandler`](../app/Contracts/NotificationHandler.php) interface.
|
||||
Each of these methods accepts the data needed to build a notification
|
||||
message and a list of the notification's recipients.
|
||||
|
||||
|
||||
Adding new notification types
|
||||
-----------------------------
|
||||
|
||||
1. Add a method for the new notification type to the
|
||||
[`NotificationHandler`](../app/Contracts/NotificationHandler.php)
|
||||
interface.
|
||||
|
||||
2. Implement the new methods in every class that implements the
|
||||
interface. Use your IDE to find these. An inexhaustive list:
|
||||
|
||||
- [`NotificationManager`](../app/Library/Notifications/NotificationManager.php)
|
||||
- [`RecipientFinder`](../app/Library/Notifications/RecipientFinder.php)
|
||||
- [`PonyfmDriver`](../app/Library/Notifications/PonyfmDriver.php)
|
||||
|
||||
3. Call the new method on the `Notification` facade from wherever the
|
||||
new notification gets triggered.
|
||||
|
||||
4. Implement any necessary logic for the new notification type in the
|
||||
[`Activity`](../app/Models/Activity.php) model.
|
||||
|
||||
|
||||
Adding new notification drivers
|
||||
-------------------------------
|
||||
|
||||
1. Create a new class for the driver that implements the
|
||||
[`NotificationHandler`](../app/Contracts/NotificationHandler.php)
|
||||
interface.
|
||||
|
||||
2. Make each method from the above interface send the corresponding type
|
||||
of notification to everyone who is to receive it via that driver.
|
||||
Implement UI and API integrations as needed.
|
||||
|
||||
3. Modify the
|
||||
[`RecipientFinder`](../app/Library/Notifications/RecipientFinder.php)
|
||||
class to build recipient lists for the new driver.
|
||||
|
||||
|
||||
Architectural notes
|
||||
-------------------
|
||||
|
||||
The notification system is designed around two ideas: being as type-safe
|
||||
as PHP allows it to be, and doing all the processing and sending of
|
||||
notifications asynchronously.
|
||||
|
||||
To that end, the
|
||||
[`NotificationManager`](../app/Library/Notifications/NotificationManager.php)
|
||||
class is a thin wrapper around the `SendNotifications` job. The job
|
||||
calls the notification drivers asynchronously to actually send the
|
||||
notifications. This job should run on a dedicated queue in production.
|
||||
|
||||
The [`NotificationHandler`](../app/Contracts/NotificationHandler.php)
|
||||
interface is key to maintaining type safety - it ensures that drivers
|
||||
and `NotificationManager` all support every type of notification. All
|
||||
classes that have logic specific to a notification type implement this
|
||||
interface to ensure that all notification types are handled.
|
||||
|
||||
There's one exception to the use of `NotificationHandler` - the
|
||||
[`Activity`](../app/Models/Activity.php) model. The logic for mapping the
|
||||
data we store about an activity in the database to a notification's API
|
||||
representation had to go somewhere, and using the `NotificationHandler`
|
||||
interface here would have made this logic a lot more obtuse.
|
|
@ -3,7 +3,7 @@
|
|||
<header ng-style="{'background-image': 'linear-gradient(135deg, ' + artist.avatar_colors[0] + ' 15%, ' + artist.avatar_colors[1] + ' 100%)'}">
|
||||
<img src="{{::artist.avatars.normal}}">
|
||||
<div class="artist-right">
|
||||
<h1>{{::artist.name}}</h1>
|
||||
<h1>{{::artist.name}}<i class="fa fa-star admin-star" ng-show="::artist.isAdmin" data-title="Admin" bs-tooltip></i></h1>
|
||||
<a href="#" class="btn btn-default" ng-class="{'btn-primary': !artist.user_data.is_following}" ng-show="::auth.isLogged && auth.user.id != artist.id" pfm-eat-click ng-click="toggleFollow()">
|
||||
<span ng-hide="artist.user_data.is_following">Follow</span>
|
||||
<span ng-show="artist.user_data.is_following">Following!</span>
|
||||
|
|
4
public/templates/directives/notification-list.html
Normal file
4
public/templates/directives/notification-list.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
<div ng-repeat="notification in notifications" class="notification">
|
||||
<a href="{{ ::notification.url }}" class="img-link"><img pfm-src-loader="::notification.thumbnail_url" pfm-src-size="thumbnail"></a>
|
||||
<a href="{{ ::notification.url }}" class="message"><p>{{ ::notification.text }}</p></a>
|
||||
</div>
|
|
@ -285,6 +285,11 @@ ponyfm.config [
|
|||
url: '/'
|
||||
templateUrl: '/templates/dashboard/index.html'
|
||||
controller: 'dashboard'
|
||||
|
||||
state.state 'notifications',
|
||||
url: '/notifications'
|
||||
templateUrl: '/templates/notifications/index.html'
|
||||
controller: 'notifications'
|
||||
else
|
||||
state.state 'home',
|
||||
url: '/'
|
||||
|
|
|
@ -22,6 +22,7 @@ module.exports = angular.module('ponyfm').controller "application", [
|
|||
$scope.$stateParams = $stateParams
|
||||
$scope.isPinnedPlaylistSelected = false
|
||||
$scope.menuActive = false
|
||||
$scope.notifActive = false
|
||||
$loadingElement = null
|
||||
loadingStateName = null
|
||||
|
||||
|
@ -34,6 +35,10 @@ module.exports = angular.module('ponyfm').controller "application", [
|
|||
|
||||
$scope.menuToggle = () ->
|
||||
$scope.menuActive = !$scope.menuActive
|
||||
$scope.notifActive = false
|
||||
|
||||
$scope.notifPulloutToggle = () ->
|
||||
$scope.notifActive = !$scope.notifActive
|
||||
|
||||
if window.pfm.error
|
||||
$state.transitionTo 'errors-' + window.pfm.error
|
||||
|
@ -72,6 +77,7 @@ module.exports = angular.module('ponyfm').controller "application", [
|
|||
statesPreloaded = {}
|
||||
$scope.$on '$stateChangeStart', (e, newState, newParams, oldState, oldParams) ->
|
||||
$scope.menuActive = false
|
||||
$scope.notifActive = false
|
||||
$scope.isPinnedPlaylistSelected = false
|
||||
|
||||
if newState.name == 'content.playlist'
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# Pony.fm - A community for pony fan music.
|
||||
# Copyright (C) 2016 Josef Citrine
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
module.exports = angular.module('ponyfm').controller "notifications", [
|
||||
'$scope', 'notifications'
|
||||
($scope, notifications) ->
|
||||
|
||||
notifications.getNotifications().done (result) ->
|
||||
$scope.notifications = result
|
||||
console.log result
|
||||
]
|
|
@ -0,0 +1,29 @@
|
|||
# Pony.fm - A community for pony fan music.
|
||||
# Copyright (C) 2016 Josef Citrine
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
module.exports = angular.module('ponyfm').directive 'pfmNotificationList', () ->
|
||||
restrict: 'E'
|
||||
templateUrl: '/templates/directives/notification-list.html'
|
||||
replace: true
|
||||
scope: {}
|
||||
|
||||
controller: [
|
||||
'$scope', 'notifications'
|
||||
($scope, notifications) ->
|
||||
notifications.getNotifications().done (result) ->
|
||||
$scope.notifications = result
|
||||
console.log result
|
||||
]
|
30
resources/assets/scripts/app/services/notifications.coffee
Normal file
30
resources/assets/scripts/app/services/notifications.coffee
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Pony.fm - A community for pony fan music.
|
||||
# Copyright (C) 2016 Josef Citrine
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
module.exports = angular.module('ponyfm').factory('notifications', [
|
||||
'$rootScope', '$http'
|
||||
($rootScope, $http) ->
|
||||
self =
|
||||
getNotifications: () ->
|
||||
def = new $.Deferred()
|
||||
|
||||
$http.get('/api/web/notifications').success (response) ->
|
||||
def.resolve response.notifications
|
||||
|
||||
def.promise()
|
||||
|
||||
self
|
||||
])
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
body.is-logged {
|
||||
.track-player {
|
||||
margin-right: 75px;
|
||||
margin-right: 130px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
55
resources/assets/styles/content.less
vendored
55
resources/assets/styles/content.less
vendored
|
@ -173,6 +173,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.admin-star {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&.x-archived {
|
||||
background: #eee;
|
||||
}
|
||||
|
@ -535,3 +539,54 @@ html {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notification-menu {
|
||||
float: right;
|
||||
margin-top: 15px;
|
||||
margin-right: 14px;
|
||||
font-size: 20pt;
|
||||
|
||||
a {
|
||||
color: #5A5A5A;
|
||||
|
||||
&:hover {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notification {
|
||||
min-height: 50px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.img-link {
|
||||
float: left;
|
||||
}
|
||||
.message {
|
||||
margin-left: 60px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-pullout {
|
||||
position: absolute;
|
||||
top: 64px;
|
||||
right: -410px;
|
||||
width: 400px;
|
||||
height: ~"calc(100% - 64px)";
|
||||
z-index: 1000;
|
||||
background: #fff;
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
transition: transform 0.5s ease;
|
||||
transform: translateX(0px) translateZ(0px);
|
||||
|
||||
&.active {
|
||||
transform: translateX(-410px) translateZ(0px);
|
||||
}
|
||||
|
||||
.notif-container {
|
||||
padding: 20px;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
1
resources/assets/styles/layout.less
vendored
1
resources/assets/styles/layout.less
vendored
|
@ -28,6 +28,7 @@ html body {
|
|||
background: #444;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
|
||||
padding: 0px !important;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
|
|
4
resources/assets/styles/mobile.less
vendored
4
resources/assets/styles/mobile.less
vendored
|
@ -248,4 +248,8 @@
|
|||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-pullout {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,6 +66,9 @@
|
|||
<li><a href="#" pfm-eat-click ng-click="logout()">Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="notification-menu">
|
||||
<a href="#" ng-click="notifPulloutToggle()"><i class="fa fa-bell fa-fw" aria-hidden="true"></i></a>
|
||||
</div>
|
||||
@endif
|
||||
<pfm-player></pfm-player>
|
||||
</div>
|
||||
|
@ -128,6 +131,14 @@
|
|||
<ui-view class="site-content">
|
||||
@yield('app_content')
|
||||
</ui-view>
|
||||
|
||||
@if (Auth::check())
|
||||
<div class="notification-pullout" ng-class="{'active': notifActive}">
|
||||
<div class="notif-container">
|
||||
<pfm-notification-list></pfm-notification-list>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
|
Loading…
Reference in a new issue