diff --git a/app/Helpers/ConfigHelper.php b/app/Helpers/ConfigHelper.php index ac964b2..05424d1 100644 --- a/app/Helpers/ConfigHelper.php +++ b/app/Helpers/ConfigHelper.php @@ -133,6 +133,7 @@ class ConfigHelper 'social_facebook_login' => false, 'social_google_login' => false, 'social_twitter_login' => false, + 'social_user_feeds' => false, 'social_user_profiles' => false, 'theme' => 'default', 'twitter_app_id' => '', @@ -187,6 +188,7 @@ class ConfigHelper public function isSocialMediaLoginEnabled() { return $this->get('social_facebook_login') || - $this->get('social_twitter_login'); + $this->get('social_twitter_login') || + $this->get('social_google_login'); } } \ No newline at end of file diff --git a/app/Http/Controllers/Admin/DefaultController.php b/app/Http/Controllers/Admin/DefaultController.php index 60bf4b6..8e97e92 100644 --- a/app/Http/Controllers/Admin/DefaultController.php +++ b/app/Http/Controllers/Admin/DefaultController.php @@ -249,6 +249,7 @@ class DefaultController extends Controller 'social_facebook_login', 'social_google_login', 'social_twitter_login', + 'social_user_feeds', 'social_user_profiles' ]; $updateKeys = [ diff --git a/app/Http/Controllers/Admin/PhotoController.php b/app/Http/Controllers/Admin/PhotoController.php index 6846115..273916f 100644 --- a/app/Http/Controllers/Admin/PhotoController.php +++ b/app/Http/Controllers/Admin/PhotoController.php @@ -15,6 +15,8 @@ use App\Photo; use App\Services\PhotoService; use App\Upload; use App\UploadPhoto; +use App\User; +use App\UserActivity; use Illuminate\Http\Request; use App\Http\Controllers\Controller; use Illuminate\Http\UploadedFile; @@ -52,6 +54,15 @@ class PhotoController extends Controller $photoService = new PhotoService($photo); $photoService->analyse($queue_token); + // Log an activity record for the user's feed (remove an existing one as the date may have changed) + $this->removeExistingActivityRecords($photo, 'photo.taken'); + + if (!is_null($photo->taken_at)) + { + // Log an activity record for the user's feed + $this->createActivityRecord($photo, 'photo.taken', $photo->taken_at); + } + $result['is_successful'] = true; } catch (\Exception $ex) @@ -115,6 +126,9 @@ class PhotoController extends Controller $photoService = new PhotoService($photo); $photoService->flip($horizontal, $vertical); + + // Log an activity record for the user's feed + $this->createActivityRecord($photo, 'photo.edited'); } public function move(Request $request, $photoId) @@ -158,6 +172,15 @@ class PhotoController extends Controller $photoService->downloadOriginalToFolder(FileHelper::getQueuePath($queue_token)); $photoService->analyse($queue_token); + // Log an activity record for the user's feed (remove an existing one as the date may have changed) + $this->removeExistingActivityRecords($photo, 'photo.taken'); + + if (!is_null($photo->taken_at)) + { + // Log an activity record for the user's feed + $this->createActivityRecord($photo, 'photo.taken', $photo->taken_at); + } + $result['is_successful'] = true; } catch (\Exception $ex) @@ -209,6 +232,9 @@ class PhotoController extends Controller $photoService = new PhotoService($photo); $photoService->rotate($angle); + + // Log an activity record for the user's feed + $this->createActivityRecord($photo, 'photo.edited'); } /** @@ -253,6 +279,9 @@ class PhotoController extends Controller /** @var File $savedFile */ $savedFile = FileHelper::saveUploadedFile($photoFile, $queueFolder, $photo->storage_file_name); + + $this->removeExistingActivityRecords($photo, 'photo.uploaded'); + $this->removeExistingActivityRecords($photo, 'photo.taken'); } else { @@ -272,6 +301,9 @@ class PhotoController extends Controller $photo->is_analysed = false; $photo->save(); + // Log an activity record for the user's feed + $this->createActivityRecord($photo, 'photo.uploaded'); + $isSuccessful = true; } } @@ -381,6 +413,10 @@ class PhotoController extends Controller $photo->file_size = $savedFile->getSize(); $photo->is_analysed = false; $photo->save(); + + // Log an activity record for the user's feed + // Log an activity record for the user's feed + $this->createActivityRecord($photo, 'photo.uploaded'); } return redirect(route('albums.analyse', [ @@ -566,6 +602,12 @@ class PhotoController extends Controller $photo->save(); } + if (!in_array(strtolower($action), ['delete', 'refresh_thumbnails', 'change_album'])) + { + // Log an activity record for the user's feed + $this->createActivityRecord($photo, 'photo.edited'); + } + if ($changed) { $numberChanged++; @@ -575,6 +617,21 @@ class PhotoController extends Controller return $numberChanged; } + private function createActivityRecord(Photo $photo, $type, $activityDateTime = null) + { + if (is_null($activityDateTime)) + { + $activityDateTime = new \DateTime(); + } + + $userActivity = new UserActivity(); + $userActivity->user_id = $this->getUser()->id; + $userActivity->activity_at = $activityDateTime; + $userActivity->type = $type; + $userActivity->photo_id = $photo->id; + $userActivity->save(); + } + /** * @param $id * @return Album @@ -615,6 +672,20 @@ class PhotoController extends Controller return $photo; } + private function removeExistingActivityRecords(Photo $photo, $type) + { + $existingFeedRecords = UserActivity::where([ + 'user_id' => $this->getUser()->id, + 'photo_id' => $photo->id, + 'type' => $type + ])->get(); + + foreach ($existingFeedRecords as $existingFeedRecord) + { + $existingFeedRecord->delete(); + } + } + private function updatePhotoDetails(Request $request, Album $album) { $numberChanged = 0; diff --git a/app/Http/Controllers/Gallery/PhotoCommentController.php b/app/Http/Controllers/Gallery/PhotoCommentController.php index 3ae5db5..ac165ed 100644 --- a/app/Http/Controllers/Gallery/PhotoCommentController.php +++ b/app/Http/Controllers/Gallery/PhotoCommentController.php @@ -12,10 +12,12 @@ use App\Http\Requests\StorePhotoCommentRequest; use App\Mail\ModeratePhotoComment; use App\Mail\PhotoCommentApproved; use App\Mail\PhotoCommentApprovedUser; +use App\Mail\PhotoCommentRepliedTo; use App\Permission; use App\Photo; use App\PhotoComment; use App\User; +use App\UserActivity; use Illuminate\Http\Request; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Auth; @@ -54,6 +56,7 @@ class PhotoCommentController extends Controller $comment->approved_user_id = $this->getUser()->id; $comment->save(); + $this->createUserActivityRecord($comment); $this->notifyAlbumOwnerAndPoster($album, $photo, $comment); $request->getSession()->flash('success', trans('gallery.photo_comment_approved_successfully')); } @@ -191,6 +194,8 @@ class PhotoCommentController extends Controller } else { + // Log an activity record for the user's feed + $this->createUserActivityRecord($comment); $this->notifyAlbumOwnerAndPoster($album, $photo, $comment); $request->getSession()->flash('success', trans('gallery.photo_comment_posted_successfully')); } @@ -223,6 +228,29 @@ class PhotoCommentController extends Controller } } + private function createUserActivityRecord(PhotoComment $comment) + { + if (!is_null($comment->created_user_id)) + { + $userActivity = new UserActivity(); + $userActivity->user_id = $comment->created_user_id; + $userActivity->activity_at = $comment->created_at; + + if (is_null($comment->parent_comment_id)) + { + $userActivity->type = 'photo.commented'; + } + else + { + $userActivity->type = 'photo.comment_replied'; + } + + $userActivity->photo_id = $comment->photo_id; + $userActivity->photo_comment_id = $comment->id; + $userActivity->save(); + } + } + private function loadAlbumPhotoComment($albumUrlAlias, $photoFilename, $commentID, &$album, &$photo, &$comment) { $album = DbHelper::getAlbumByPath($albumUrlAlias); @@ -256,6 +284,22 @@ class PhotoCommentController extends Controller return true; } + /** + * Loads a given comment by its ID. + * @param $id + * @return PhotoComment + */ + private function loadCommentByID($id) + { + $comment = PhotoComment::where('id', intval($id))->first(); + if (is_null($comment)) + { + App::abort(404); + } + + return $comment; + } + /** * Sends an e-mail notification to an album's moderators that a comment is available to moderate. * @param Album $album diff --git a/app/Http/Controllers/Gallery/UserController.php b/app/Http/Controllers/Gallery/UserController.php index 12e4714..bcddb9e 100644 --- a/app/Http/Controllers/Gallery/UserController.php +++ b/app/Http/Controllers/Gallery/UserController.php @@ -10,6 +10,7 @@ use App\Http\Controllers\Controller; use App\Http\Requests\SaveUserSettingsRequest; use App\Mail\UserChangeEmailRequired; use App\User; +use App\UserActivity; use Illuminate\Support\Collection; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\DB; @@ -118,16 +119,7 @@ class UserController extends Controller public function show(Request $request, $idOrAlias) { - // If a user has a profile alias set, their profile page cannot be accessed by the ID - $user = User::where(DB::raw('COALESCE(profile_alias, id)'), strtolower($idOrAlias))->first(); - - if (is_null($user)) - { - App::abort(404); - return null; - } - - $this->authorizeForUser($this->getUser(), 'view', $user); + $user = $this->loadUserProfilePage($idOrAlias); $albums = $this->getAlbumsForUser($user); $albumIDs = $this->getAlbumIDsForUser($user); @@ -137,6 +129,7 @@ class UserController extends Controller $daysInMonth = $this->getDaysInMonths(); return Theme::render('gallery.user_profile', [ + 'active_tab' => $request->get('tab'), 'activity_taken' => $this->constructActivityGrid($activity['taken']), 'activity_uploaded' => $this->constructActivityGrid($activity['uploaded']), 'albums' => $albums, @@ -146,6 +139,46 @@ class UserController extends Controller ]); } + public function showFeedJson(Request $request, $idOrAlias) + { + $user = $this->loadUserProfilePage($idOrAlias); + + $result = []; + $activities = UserActivity::with('photo') + ->with('photoComment') + ->where([ + 'user_id' => $user->id + ]) + ->orderBy('activity_at', 'desc') + ->limit(100) // TODO: make this configurable + ->get(); + + $userName = $user->name; + $userProfileUrl = $user->profileUrl(); + $userAvatar = Theme::gravatarUrl($user->email, 32); + + /** @var UserActivity $activity */ + foreach ($activities as $activity) + { + $newItem = [ + 'activity_at' => date(UserConfig::get('date_format'), strtotime($activity->activity_at)), + 'avatar' => $userAvatar, + 'description' => trans(sprintf('gallery.user_feed_type.%s', $activity->type)) + ]; + + $params = []; + $params['user_name'] = $userName; + $params['user_url'] = $userProfileUrl; + $params['photo_name'] = $activity->photo->name; + $params['photo_url'] = $activity->photo->url(); + $newItem['params'] = $params; + + $result[] = $newItem; + } + + return response()->json($result); + } + private function constructActivityGrid(Collection $collection) { $results = []; @@ -300,6 +333,26 @@ class UserController extends Controller return $results; } + /** + * @param $idOrAlias + * @return User + */ + private function loadUserProfilePage($idOrAlias) + { + // If a user has a profile alias set, their profile page cannot be accessed by the ID + $user = User::where(DB::raw('COALESCE(profile_alias, id)'), strtolower($idOrAlias))->first(); + + if (is_null($user)) + { + App::abort(404); + return null; + } + + $this->authorizeForUser($this->getUser(), 'view', $user); + + return $user; + } + private function sendEmailChangeConfirmationEmail(User $user, $newEmailAddress) { $oldEmailAddress = $user->email; diff --git a/app/User.php b/app/User.php index 0e7f501..95ce522 100644 --- a/app/User.php +++ b/app/User.php @@ -55,6 +55,13 @@ class User extends Authenticatable return $this->hasMany(Album::class); } + public function feedJsonUrl() + { + return route('viewUserFeedJson', [ + 'idOrAlias' => (!empty($this->profile_alias) ? trim(strtolower($this->profile_alias)) : $this->id) + ]); + } + public function groups() { return $this->belongsToMany(Group::class, 'user_groups'); diff --git a/app/UserActivity.php b/app/UserActivity.php new file mode 100644 index 0000000..c504d98 --- /dev/null +++ b/app/UserActivity.php @@ -0,0 +1,20 @@ +belongsTo(Photo::class); + } + + public function photoComment() + { + return $this->belongsTo(PhotoComment::class); + } +} diff --git a/database/migrations/2018_10_06_143935_create_user_activities_table.php b/database/migrations/2018_10_06_143935_create_user_activities_table.php new file mode 100644 index 0000000..9ec8634 --- /dev/null +++ b/database/migrations/2018_10_06_143935_create_user_activities_table.php @@ -0,0 +1,46 @@ +bigIncrements('id'); + $table->unsignedInteger('user_id'); + $table->dateTime('activity_at'); + $table->string('type', 100); + $table->unsignedBigInteger('photo_id')->nullable(true); + $table->unsignedBigInteger('photo_comment_id')->nullable(true); + $table->timestamps(); + + $table->foreign('user_id') + ->references('id')->on('users') + ->onDelete('cascade'); + $table->foreign('photo_id') + ->references('id')->on('photos') + ->onDelete('cascade'); + $table->foreign('photo_comment_id') + ->references('id')->on('photo_comments') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('user_activity'); + } +} diff --git a/resources/assets/js/gallery.js b/resources/assets/js/gallery.js index bcd9bf9..15873dd 100644 --- a/resources/assets/js/gallery.js +++ b/resources/assets/js/gallery.js @@ -69,6 +69,80 @@ function PhotoViewModel(urls) { this.is_reply_form_loading = true; $('#comment-reply-modal').modal('show'); + e.preventDefault(); + return false; + } + }; +} + +/** + * This model is used by gallery/user_profile.blade.php, to handle a user's profile. + * @constructor + */ +function UserViewModel(urls) +{ + this.el = '#user-app'; + + this.data = { + feed_items: [], + is_loading: true, + selected_view: 'profile' + }; + + this.computed = { + isFeed: function() { + return this.selected_view === 'feed'; + }, + isProfile: function() { + return this.selected_view === 'profile'; + } + }; + + this.methods = { + loadFeedItems: function(e) + { + var self = this; + + $.get(urls.feed_url, function (data) + { + for (var i = 0; i < data.length; i++) + { + // User name + if (data[i].params.user_name && data[i].params.user_url) + { + data[i].description = data[i].description + .replace( + ':user_name', + '' + data[i].params.user_name + '' + ); + } + + // Photo name + if (data[i].params.photo_name && data[i].params.photo_url) + { + data[i].description = data[i].description + .replace( + ':photo_name', + '' + data[i].params.photo_name + '' + ); + } + } + + self.feed_items = data; + self.is_loading = false; + }); + }, + switchToFeed: function(e) { + this.selected_view = 'feed'; + history.pushState('data to be passed', 'Title of the page', urls.current_url + '?tab=feed'); + + e.preventDefault(); + return false; + }, + switchToProfile: function(e) { + this.selected_view = 'profile'; + history.pushState('data to be passed', 'Title of the page', urls.current_url + '?tab=profile'); + e.preventDefault(); return false; } diff --git a/resources/lang/en/forms.php b/resources/lang/en/forms.php index 429aa08..5f64662 100644 --- a/resources/lang/en/forms.php +++ b/resources/lang/en/forms.php @@ -89,6 +89,8 @@ return [ 'settings_social_twitter_app_secret' => 'Twitter App Secret:', 'settings_social_twitter_login' => 'Allow login/registration with a Twitter account', 'settings_social_twitter_login_help' => 'With this option enabled, users can register (if enabled) and login with their Twitter account.', + 'settings_social_user_feeds' => 'Enable user feeds and following', + 'settings_social_user_feeds_help' => 'Show activity feeds for users and allow users to follow others.', 'settings_social_user_profiles' => 'Enable public user profiles', 'settings_social_user_profiles_help' => 'Display public pages for users showing their albums, cameras used and activity.', 'storage_access_key_label' => 'Access key:', diff --git a/resources/lang/en/gallery.php b/resources/lang/en/gallery.php index ce11487..e900990 100644 --- a/resources/lang/en/gallery.php +++ b/resources/lang/en/gallery.php @@ -77,6 +77,15 @@ return [ 'title' => 'Statistics', 'uploaded_12_months' => 'Photos uploaded in the last 12 months', ], + 'user_feed_type' => [ + 'photo' => [ + 'comment_replied' => ':user_name replied to a comment on the :photo_name photo.', + 'commented' => ':user_name commented on the :photo_name photo.', + 'edited' => ':user_name edited the :photo_name photo.', + 'taken' => ':user_name took the :photo_name photo.', + 'uploaded' => ':user_name uploaded the :photo_name photo.' + ] + ], 'user_profile' => [ 'activity' => 'Activity', 'activity_summary' => ':count photo on :date|:count photos on :date', @@ -86,8 +95,10 @@ return [ 'activity_uploaded_tab' => 'Uploaded', 'albums' => 'Albums by :user_name', 'cameras' => 'Cameras', + 'feed_tab' => 'Activity', 'no_albums_p1' => 'No Photo Albums', - 'no_albums_p2' => ':user_name has not created any albums yet.' + 'no_albums_p2' => ':user_name has not created any albums yet.', + 'profile_tab' => 'Profile' ], 'user_settings' => [ 'cancel_email_change' => 'Don\'t change e-mail address', diff --git a/resources/views/themes/base/admin/settings.blade.php b/resources/views/themes/base/admin/settings.blade.php index 869241a..dae1233 100644 --- a/resources/views/themes/base/admin/settings.blade.php +++ b/resources/views/themes/base/admin/settings.blade.php @@ -346,6 +346,14 @@ +
+ + +
+
{{-- Facebook --}} @@ -440,6 +448,8 @@ +
+ {{-- Google+ --}}
diff --git a/resources/views/themes/base/gallery/user_profile.blade.php b/resources/views/themes/base/gallery/user_profile.blade.php index 86133e1..27cddb9 100644 --- a/resources/views/themes/base/gallery/user_profile.blade.php +++ b/resources/views/themes/base/gallery/user_profile.blade.php @@ -7,7 +7,7 @@ @endsection @section('content') -
+
@@ -21,7 +21,20 @@
-
+ + +
@if (count($albums) == 0)

@lang('gallery.user_profile.no_albums_p1')

@@ -87,5 +100,49 @@ @endif
+ +
+
+
+

+ @lang('global.please_wait') +

+

+ @lang('global.please_wait') +

+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
-@endsection \ No newline at end of file +@endsection + +@push('scripts') + +@endpush \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index abb18de..bf32593 100644 --- a/routes/web.php +++ b/routes/web.php @@ -140,6 +140,9 @@ Route::get('i/{albumUrlAlias}/{photoFilename}', 'Gallery\PhotoController@downloa Route::get('label/{labelAlias}', 'Gallery\LabelController@show') ->name('viewLabel') ->where('labelAlias', '.*'); +Route::get('u/{idOrAlias}/feed.json', 'Gallery\UserController@showFeedJson') + ->name('viewUserFeedJson') + ->where('idOrAlias', '.*'); Route::get('u/{idOrAlias}', 'Gallery\UserController@show') ->name('viewUser') ->where('idOrAlias', '.*');