Merge branch 'master' of https://github.com/Poniverse/Pony.fm into feature-playlist-sort

This commit is contained in:
Maximilian Walter 2016-05-31 15:03:26 +02:00
commit 2d458c3d80
48 changed files with 1633 additions and 31 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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;
}

View 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;
}

View 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;
}

View 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);
}

View 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';
}
}

View file

@ -35,4 +35,9 @@ class AccountController extends Controller
{
return Redirect::to(Config::get('poniverse.urls')['register']);
}
public function getNotifications()
{
return View::make('shared.null');
}
}

View file

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

View file

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

View file

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

View 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];
}
}

View file

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

View file

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

View 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);
}
}
}

View 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);
}
}

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

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

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

View file

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

View file

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

View file

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

View 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
];
}
}

View file

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

View file

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

View file

@ -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.
*

View file

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

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

View file

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

View file

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

View 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.

View file

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

View 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>

View file

@ -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: '/'

View file

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

View file

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

View file

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

View 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
])

View file

@ -21,7 +21,7 @@
body.is-logged {
.track-player {
margin-right: 75px;
margin-right: 130px;
}
}

View file

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

View file

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

View file

@ -248,4 +248,8 @@
margin: 0;
}
}
.notification-pullout {
display: none;
}
}

View file

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