From 901b6b0839efb7794a64794e524fa7dcf64c107e Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Wed, 25 Mar 2015 13:00:23 +0100 Subject: [PATCH 01/10] Provide empty run() method. This allows me to override the handle() method in subclasses (where I need access to the request object) without having to overwrite run(), too. The class is still abstract. --- src/Api/Actions/BaseAction.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Api/Actions/BaseAction.php b/src/Api/Actions/BaseAction.php index d9aa5077c..764acdf9d 100644 --- a/src/Api/Actions/BaseAction.php +++ b/src/Api/Actions/BaseAction.php @@ -13,8 +13,6 @@ use Response; abstract class BaseAction extends Action { - abstract protected function run(ApiParams $params); - public function __construct(Actor $actor, Dispatcher $bus) { $this->actor = $actor; @@ -40,6 +38,15 @@ abstract class BaseAction extends Action return $this->run($params); } + /** + * @param ApiParams $params + * @return mixed + */ + protected function run(ApiParams $params) + { + // Should be implemented by subclasses + } + public function hydrate($object, $params) { foreach ($params as $k => $v) { From 7f66a77edefeb714a9ba2a336d82a770ba79e57f Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Wed, 25 Mar 2015 14:21:50 +0100 Subject: [PATCH 02/10] Add avatar handling to user model. --- migrations/2015_02_24_000000_create_users_table.php | 1 + src/Core/Events/UserAvatarWasChanged.php | 13 +++++++++++++ src/Core/Models/User.php | 10 ++++++++++ 3 files changed, 24 insertions(+) create mode 100644 src/Core/Events/UserAvatarWasChanged.php diff --git a/migrations/2015_02_24_000000_create_users_table.php b/migrations/2015_02_24_000000_create_users_table.php index f9ed47aaa..751bee3dc 100644 --- a/migrations/2015_02_24_000000_create_users_table.php +++ b/migrations/2015_02_24_000000_create_users_table.php @@ -23,6 +23,7 @@ class CreateUsersTable extends Migration { $table->string('password'); $table->text('bio')->nullable(); $table->text('bio_html')->nullable(); + $table->string('avatar')->nullable(); $table->dateTime('join_time')->nullable(); $table->dateTime('last_seen_time')->nullable(); $table->dateTime('read_time')->nullable(); diff --git a/src/Core/Events/UserAvatarWasChanged.php b/src/Core/Events/UserAvatarWasChanged.php new file mode 100644 index 000000000..b83c9471d --- /dev/null +++ b/src/Core/Events/UserAvatarWasChanged.php @@ -0,0 +1,13 @@ +user = $user; + } +} diff --git a/src/Core/Models/User.php b/src/Core/Models/User.php index c5e4d91ec..39867a983 100755 --- a/src/Core/Models/User.php +++ b/src/Core/Models/User.php @@ -10,6 +10,7 @@ use Flarum\Core\Events\UserWasRenamed; use Flarum\Core\Events\UserEmailWasChanged; use Flarum\Core\Events\UserPasswordWasChanged; use Flarum\Core\Events\UserBioWasChanged; +use Flarum\Core\Events\UserAvatarWasChanged; use Flarum\Core\Events\UserWasActivated; use Flarum\Core\Events\UserEmailWasConfirmed; @@ -210,6 +211,15 @@ class User extends Model return $this; } + public function changeAvatarUrl($url) + { + $this->avatar = $url; + + $this->raise(new UserAvatarWasChanged($this)); + + return $this; + } + /** * Check if a given password matches the user's password. * From a1f723671daa83414d869090f086fe4e283f637a Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Wed, 25 Mar 2015 14:23:31 +0100 Subject: [PATCH 03/10] Add simple implementation (command handler) for avatar upload. --- src/Core/Commands/UploadAvatarCommand.php | 30 ++++++++++ src/Core/Events/AvatarWillBeUploaded.php | 16 +++++ .../Commands/UploadAvatarCommandHandler.php | 59 +++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 src/Core/Commands/UploadAvatarCommand.php create mode 100644 src/Core/Events/AvatarWillBeUploaded.php create mode 100644 src/Core/Handlers/Commands/UploadAvatarCommandHandler.php diff --git a/src/Core/Commands/UploadAvatarCommand.php b/src/Core/Commands/UploadAvatarCommand.php new file mode 100644 index 000000000..acbd8aaef --- /dev/null +++ b/src/Core/Commands/UploadAvatarCommand.php @@ -0,0 +1,30 @@ +userId = $userId; + $this->file = $file; + $this->actor = $actor; + } +} diff --git a/src/Core/Events/AvatarWillBeUploaded.php b/src/Core/Events/AvatarWillBeUploaded.php new file mode 100644 index 000000000..efb93c2aa --- /dev/null +++ b/src/Core/Events/AvatarWillBeUploaded.php @@ -0,0 +1,16 @@ +user = $user; + $this->command = $command; + } +} diff --git a/src/Core/Handlers/Commands/UploadAvatarCommandHandler.php b/src/Core/Handlers/Commands/UploadAvatarCommandHandler.php new file mode 100644 index 000000000..5914a768b --- /dev/null +++ b/src/Core/Handlers/Commands/UploadAvatarCommandHandler.php @@ -0,0 +1,59 @@ +users = $users; + $this->uploadDir = $uploadDir; + } + + public function handle(UploadAvatarCommand $command) + { + $user = $this->users->findOrFail($command->userId); + + // Make sure the current user is allowed to edit the user profile. + // This will let admins and the user themselves pass through, and + // throw an exception otherwise. + $user->assertCan($command->actor, 'edit'); + + $filename = $command->file->getFilename(); + $uploadName = Str::lower(Str::quickRandom()) . '.jpg'; + + $mount = new MountManager([ + 'source' => new Local($command->file->getPath()), + 'target' => $this->uploadDir, + ]); + + $user->changeAvatarUrl($uploadName); + + event(new AvatarWillBeUploaded($user, $command)); + + $mount->move("source://$filename", "target://$uploadName"); + $user->save(); + $this->dispatchEventsFor($user); + + return $user; + } +} From 100a5038bf5b922b626fb4a3734f869212a7da06 Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Wed, 25 Mar 2015 14:26:17 +0100 Subject: [PATCH 04/10] Add route and action for uploading user avatars. --- src/Api/Actions/Users/UploadAvatarAction.php | 21 ++++++++++++++++++++ src/Api/routes.php | 5 +++++ 2 files changed, 26 insertions(+) create mode 100644 src/Api/Actions/Users/UploadAvatarAction.php diff --git a/src/Api/Actions/Users/UploadAvatarAction.php b/src/Api/Actions/Users/UploadAvatarAction.php new file mode 100644 index 000000000..e98ec9ab9 --- /dev/null +++ b/src/Api/Actions/Users/UploadAvatarAction.php @@ -0,0 +1,21 @@ +file('avatar'); + + $this->dispatch( + new UploadAvatarCommand($userId, $this->actor->getUser(), $file), + $routeParams + ); + + return $this->respondWithoutContent(201); + } +} diff --git a/src/Api/routes.php b/src/Api/routes.php index 8c413b27b..c1fb00eec 100644 --- a/src/Api/routes.php +++ b/src/Api/routes.php @@ -59,6 +59,11 @@ Route::group(['prefix' => 'api', 'middleware' => 'Flarum\Api\Middleware\LoginWit 'uses' => $action('Flarum\Api\Actions\Users\DeleteAction') ]); + Route::post('users/{id}/avatar', [ + 'as' => 'flarum.api.users.avatar.upload', + 'uses' => $action('Flarum\Api\Actions\Users\UploadAvatarAction') + ]); + /* |-------------------------------------------------------------------------- | Activity From e4ed05755744c7c4b8173ec332d1bb14905b8dab Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Wed, 25 Mar 2015 14:26:38 +0100 Subject: [PATCH 05/10] Wire up instantiation of Flysystem adapter for avatar storage. --- src/Core/CoreServiceProvider.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Core/CoreServiceProvider.php b/src/Core/CoreServiceProvider.php index 92279d98e..baa8f8921 100644 --- a/src/Core/CoreServiceProvider.php +++ b/src/Core/CoreServiceProvider.php @@ -12,6 +12,7 @@ use Flarum\Core\Models\User; use Flarum\Core\Models\Discussion; use Flarum\Core\Models\Notification; use Flarum\Core\Search\GambitManager; +use League\Flysystem\Adapter\Local; class CoreServiceProvider extends ServiceProvider { @@ -83,6 +84,14 @@ class CoreServiceProvider extends ServiceProvider 'Flarum\Core\Repositories\NotificationRepositoryInterface', 'Flarum\Core\Repositories\EloquentNotificationRepository' ); + + $this->app->singleton('flarum.avatars.storage', function () { + return new Local(__DIR__.'/../../ember/public/avatars'); + }); + + $this->app->when('Flarum\Core\Handlers\Commands\UploadAvatarCommandHandler') + ->needs('League\Flysystem\FilesystemInterface') + ->give('flarum.avatars.storage'); } public function registerGambits() From ab9cf922db3d8f60ed00d6fc88514c190d5c1cf9 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Thu, 26 Mar 2015 10:19:47 +1030 Subject: [PATCH 06/10] Implement rough UI for uploading avatars --- ember/app/components/user/avatar-editor.js | 45 +++++++++++++++++++ ember/app/styles/flarum/user.less | 38 +++++++++++++++- .../components/user/avatar-editor.hbs | 12 +++++ .../templates/components/user/user-card.hbs | 6 ++- 4 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 ember/app/components/user/avatar-editor.js create mode 100644 ember/app/templates/components/user/avatar-editor.hbs diff --git a/ember/app/components/user/avatar-editor.js b/ember/app/components/user/avatar-editor.js new file mode 100644 index 000000000..1c742b7ff --- /dev/null +++ b/ember/app/components/user/avatar-editor.js @@ -0,0 +1,45 @@ +import Ember from 'ember'; + +import config from 'flarum/config/environment'; + +var $ = Ember.$; + +export default Ember.Component.extend({ + layoutName: 'components/user/avatar-editor', + classNames: ['avatar-editor', 'dropdown'], + classNameBindings: ['loading'], + + click: function(e) { + if (! this.get('user.avatarUrl')) { + e.preventDefault(); + e.stopPropagation(); + this.send('upload'); + } + }, + + actions: { + upload: function() { + if (this.get('loading')) { return; } + + var $input = $(''); + var userId = this.get('user.id'); + var component = this; + $input.appendTo('body').hide().click().on('change', function() { + var formData = new FormData(); + formData.append('avatar', $(this)[0].files[0]); + component.set('loading', true); + $.ajax({ + type: 'POST', + url: config.apiURL+'/users/'+userId+'/avatar', + data: formData, + cache: false, + contentType: false, + processData: false, + complete: function() { + component.set('loading', false); + } + }); + }); + } + } +}); diff --git a/ember/app/styles/flarum/user.less b/ember/app/styles/flarum/user.less index 0f13aa769..3c068c098 100644 --- a/ember/app/styles/flarum/user.less +++ b/ember/app/styles/flarum/user.less @@ -47,10 +47,17 @@ display: inline; vertical-align: middle; } - & .avatar { - .avatar-size(96px); + & .user-avatar { float: left; margin-left: -130px; + } + & .avatar-editor .dropdown-toggle { + margin: 4px; + line-height: 96px; + font-size: 26px; + } + & .avatar { + .avatar-size(96px); border: 4px solid #fff; .box-shadow(0 2px 6px @fl-shadow-color); } @@ -170,3 +177,30 @@ } } } + +.avatar-editor { + position: relative; + + & .dropdown-toggle { + opacity: 0; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + border-radius: 100%; + background: rgba(0, 0, 0, 0.4); + text-align: center; + text-decoration: none; + } + &:hover .dropdown-toggle, &.open .dropdown-toggle, &.loading .dropdown-toggle { + opacity: 1; + } + & .loading-indicator { + color: #fff; + } + & .dropdown-menu { + left: 35%; + top: 65%; + } +} diff --git a/ember/app/templates/components/user/avatar-editor.hbs b/ember/app/templates/components/user/avatar-editor.hbs new file mode 100644 index 000000000..7f204a6cd --- /dev/null +++ b/ember/app/templates/components/user/avatar-editor.hbs @@ -0,0 +1,12 @@ +{{user-avatar user}} + + {{#if loading}} + {{ui/loading-indicator}} + {{else}} + {{fa-icon "pencil"}} + {{/if}} + + diff --git a/ember/app/templates/components/user/user-card.hbs b/ember/app/templates/components/user/user-card.hbs index 17946bebb..0a8e80dcf 100644 --- a/ember/app/templates/components/user/user-card.hbs +++ b/ember/app/templates/components/user/user-card.hbs @@ -6,7 +6,11 @@