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 @@
- {{#link-to "user" user}}{{user-avatar user}}{{user-name user}}{{/link-to}}
+ {{#if editable}}
+ {{user/avatar-editor user=user class="user-avatar"}}{{user-name user}}
+ {{else}}
+ {{#link-to "user" user}}{{user-avatar user class="user-avatar"}}{{user-name user}}{{/link-to}}
+ {{/if}}
{{ui/item-list items=user.badges class="badges user-badges"}}
diff --git a/migrations/2015_02_24_000000_create_users_table.php b/migrations/2015_02_24_000000_create_users_table.php
index f9ed47aaa..8bc363b52 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_path')->nullable();
$table->dateTime('join_time')->nullable();
$table->dateTime('last_seen_time')->nullable();
$table->dateTime('read_time')->nullable();
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) {
diff --git a/src/Api/Actions/Users/UploadAvatarAction.php b/src/Api/Actions/Users/UploadAvatarAction.php
new file mode 100644
index 000000000..73541abb4
--- /dev/null
+++ b/src/Api/Actions/Users/UploadAvatarAction.php
@@ -0,0 +1,21 @@
+file('avatar');
+
+ $this->dispatch(
+ new UploadAvatarCommand($userId, $file, $this->actor->getUser()),
+ $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
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/CoreServiceProvider.php b/src/Core/CoreServiceProvider.php
index 92279d98e..041d21e47 100644
--- a/src/Core/CoreServiceProvider.php
+++ b/src/Core/CoreServiceProvider.php
@@ -1,6 +1,7 @@
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(function(Container $app) {
+ return $app->make('Illuminate\Contracts\Filesystem\Factory')->disk('avatars')->getDriver();
+ });
}
public function registerGambits()
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/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/Handlers/Commands/UploadAvatarCommandHandler.php b/src/Core/Handlers/Commands/UploadAvatarCommandHandler.php
new file mode 100644
index 000000000..5e36c6da5
--- /dev/null
+++ b/src/Core/Handlers/Commands/UploadAvatarCommandHandler.php
@@ -0,0 +1,60 @@
+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 Filesystem(new Local($command->file->getPath())),
+ 'target' => $this->uploadDir,
+ ]);
+
+ $user->changeAvatarPath($uploadName);
+
+ event(new AvatarWillBeUploaded($user, $command));
+
+ $mount->move("source://$filename", "target://$uploadName");
+ $user->save();
+ $this->dispatchEventsFor($user);
+
+ return $user;
+ }
+}
diff --git a/src/Core/Models/User.php b/src/Core/Models/User.php
index c5e4d91ec..578491041 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,21 @@ class User extends Model
return $this;
}
+ /**
+ * Change the path of the user avatar.
+ *
+ * @param string $path
+ * @return $this
+ */
+ public function changeAvatarPath($path)
+ {
+ $this->avatar_path = $path;
+
+ $this->raise(new UserAvatarWasChanged($this));
+
+ return $this;
+ }
+
/**
* Check if a given password matches the user's password.
*