Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for customizable user avatar drivers #4130

Open
wants to merge 3 commits into
base: 2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions framework/core/js/src/admin/AdminApplication.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface AdminApplicationData extends ApplicationData {
settings: Record<string, string>;
modelStatistics: Record<string, { total: number }>;
displayNameDrivers: string[];
avatarDrivers: string[];
slugDrivers: Record<string, string[]>;
searchDrivers: Record<string, string[]>;
permissions: Record<string, string[]>;
Expand Down
19 changes: 19 additions & 0 deletions framework/core/js/src/admin/components/BasicsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import extractText from '../../common/utils/extractText';
export type HomePageItem = { path: string; label: Mithril.Children };
export type DriverLocale = {
display_name: Record<string, string>;
avatar: Record<string, string>;
slug: Record<string, Record<string, string>>;
};

Expand Down Expand Up @@ -58,6 +59,9 @@ export default class BasicsPage<CustomAttrs extends IPageAttrs = IPageAttrs> ext
display_name: {
username: extractText(app.translator.trans('core.admin.basics.display_name_driver_options.username')),
},
avatar: {
default: extractText(app.translator.trans('core.admin.basics.avatar_driver_options.default')),
},
slug: {
'Flarum\\Discussion\\Discussion': {
default: extractText(app.translator.trans('core.admin.basics.slug_driver_options.discussions.default')),
Expand All @@ -82,6 +86,7 @@ export default class BasicsPage<CustomAttrs extends IPageAttrs = IPageAttrs> ext

const localeOptions: Record<string, string> = {};
const displayNameOptions: Record<string, string> = {};
const avatarDriverOptions: Record<string, string> = {};
const slugDriverOptions: Record<string, Record<string, string>> = {};

const driverLocale = BasicsPage.driverLocale();
Expand All @@ -94,6 +99,10 @@ export default class BasicsPage<CustomAttrs extends IPageAttrs = IPageAttrs> ext
displayNameOptions[identifier] = driverLocale.display_name[identifier] || identifier;
});

app.data.avatarDrivers.forEach((identifier) => {
avatarDriverOptions[identifier] = driverLocale.avatar[identifier] || identifier;
});

Object.keys(app.data.slugDrivers).forEach((model) => {
slugDriverOptions[model] = {};

Expand Down Expand Up @@ -169,6 +178,16 @@ export default class BasicsPage<CustomAttrs extends IPageAttrs = IPageAttrs> ext
});
}

if (Object.keys(avatarDriverOptions).length > 1) {
app.registry.registerSetting({
type: 'select',
setting: 'avatar_driver',
options: avatarDriverOptions,
label: app.translator.trans('core.admin.basics.avatar_driver_heading'),
help: app.translator.trans('core.admin.basics.avatar_driver_text'),
});
}

Object.keys(slugDriverOptions).forEach((model) => {
const options = slugDriverOptions[model];
const modelLocale = AdminPage.modelLocale()[model] || model;
Expand Down
4 changes: 4 additions & 0 deletions framework/core/js/src/common/models/User.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export default class User extends Model {
return Model.attribute<string | undefined>('password').call(this);
}

originalAvatarUrl() {
return Model.attribute<string | null>('originalAvatarUrl').call(this);
}

avatarUrl() {
return Model.attribute<string | null>('avatarUrl').call(this);
}
Expand Down
6 changes: 3 additions & 3 deletions framework/core/js/src/forum/components/AvatarEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default class AvatarEditor extends Component {
<div className={classList(['AvatarEditor', 'Dropdown', this.attrs.className, this.loading && 'loading', this.isDraggedOver && 'dragover'])}>
<Avatar user={user} loading="eager" />
<a
className={user.avatarUrl() ? 'Dropdown-toggle' : 'Dropdown-toggle AvatarEditor--noAvatar'}
className={user.originalAvatarUrl() ? 'Dropdown-toggle' : 'Dropdown-toggle AvatarEditor--noAvatar'}
title={app.translator.trans('core.forum.user.avatar_upload_tooltip')}
data-toggle="dropdown"
onclick={this.quickUpload.bind(this)}
Expand All @@ -55,7 +55,7 @@ export default class AvatarEditor extends Component {
>
{this.loading ? (
<LoadingIndicator display="unset" size="large" />
) : user.avatarUrl() ? (
) : user.originalAvatarUrl() ? (
<Icon name={'fas fa-pencil-alt'} />
) : (
<Icon name={'fas fa-plus-circle'} />
Expand Down Expand Up @@ -134,7 +134,7 @@ export default class AvatarEditor extends Component {
* @param {MouseEvent} e
*/
quickUpload(e) {
if (!this.attrs.user.avatarUrl()) {
if (!this.attrs.user.originalAvatarUrl()) {
e.preventDefault();
e.stopPropagation();
this.openPicker();
Expand Down
4 changes: 4 additions & 0 deletions framework/core/locale/core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ core:
username: Username
display_name_heading: User Display Name
display_name_text: Select the driver that should be used for users' display names. By default, the username is shown.
avatar_driver_options:
default: Default
avatar_driver_heading: User Avatar
avatar_driver_text: Select a driver that should be used for users' avatars when no user-uploaded avatar is available. By default, the default avatar is shown.
forum_description_heading: Forum Description
forum_description_text: Enter a short sentence or two that describes your community. This will appear in the meta tag and show up in search engines.
forum_title_heading: Forum Title
Expand Down
3 changes: 2 additions & 1 deletion framework/core/src/Admin/Content/AdminPayload.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ public function __invoke(Document $document, Request $request): void
$document->payload['permissions'] = Permission::map();
$document->payload['extensions'] = $this->extensions->getExtensions()->toArray();

$document->payload['displayNameDrivers'] = array_keys($this->container->make('flarum.user.display_name.supported_drivers'));
$document->payload['displayNameDrivers'] = array_keys($this->container->make(abstract: 'flarum.user.display_name.supported_drivers'));
$document->payload['avatarDrivers'] = array_keys($this->container->make('flarum.user.avatar.supported_drivers'));
$document->payload['slugDrivers'] = array_map(function ($resourceDrivers) {
return array_keys($resourceDrivers);
}, $this->container->make('flarum.http.slugDrivers'));
Expand Down
1 change: 1 addition & 0 deletions framework/core/src/Api/Resource/UserResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ public function fields(): array
->save(fn () => null),
Schema\Str::make('displayName'),
Schema\Str::make('avatarUrl'),
Schema\Str::make('originalAvatarUrl'),
Schema\Str::make('slug')
->get(function (User $user) {
return $this->slugManager->forResource(User::class)->toSlug($user);
Expand Down
19 changes: 19 additions & 0 deletions framework/core/src/Extend/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
class User implements ExtenderInterface
{
private array $displayNameDrivers = [];
private array $avatarDrivers = [];
private array $groupProcessors = [];
private array $preferences = [];

Expand All @@ -34,6 +35,20 @@ public function displayNameDriver(string $identifier, string $driver): self
return $this;
}

/**
* Add an avatar driver.
*
* @param string $identifier: Identifier for avatar driver. E.g. 'gravatar' for GravatarDriver
* @param class-string<\Flarum\User\Avatar\DriverInterface> $driver: ::class attribute of driver class, which must implement Flarum\User\Avatar\DriverInterface
* @return self
*/
public function avatarDriver(string $identifier, string $driver): self
{
$this->avatarDrivers[$identifier] = $driver;

return $this;
}

/**
* Dynamically process a user's list of groups when calculating permissions.
* This can be used to give a user permissions for groups they aren't actually in, based on context.
Expand Down Expand Up @@ -78,6 +93,10 @@ public function extend(Container $container, ?Extension $extension = null): void
return array_merge($existingDrivers, $this->displayNameDrivers);
});

$container->extend('flarum.user.avatar.supported_drivers', function ($existingDrivers) {
return array_merge($existingDrivers, $this->avatarDrivers);
});

$container->extend('flarum.user.group_processors', function ($existingRelations) {
return array_merge($existingRelations, $this->groupProcessors);
});
Expand Down
23 changes: 23 additions & 0 deletions framework/core/src/User/Avatar/DefaultDriver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/

namespace Flarum\User\Avatar;

use Flarum\User\User;

/**
* The default driver, which returns the user's avatar URL.
*/
class DefaultDriver implements DriverInterface
{
public function avatarUrl(User $user): ?string
{
return null;
}
}
25 changes: 25 additions & 0 deletions framework/core/src/User/Avatar/DriverInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/

namespace Flarum\User\Avatar;

use Flarum\User\User;

/**
* An interface for a avatar driver.
*
* @public
*/
interface DriverInterface
{
/**
* Return a avatar for a user.
*/
public function avatarUrl(User $user): ?string;
}
22 changes: 18 additions & 4 deletions framework/core/src/User/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
use Flarum\Http\AccessToken;
use Flarum\Notification\Notification;
use Flarum\Post\Post;
use Flarum\User\DisplayName\DriverInterface;
use Flarum\User\DisplayName\DriverInterface as DisplayNameDriver;
use Flarum\User\Avatar\DriverInterface as AvatarDriver;
use Flarum\User\Event\Activated;
use Flarum\User\Event\AvatarChanged;
use Flarum\User\Event\Deleted;
Expand All @@ -34,6 +35,7 @@
use Illuminate\Contracts\Filesystem\Factory;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
Expand Down Expand Up @@ -111,7 +113,9 @@ class User extends AbstractModel
/**
* A driver for getting display names.
*/
protected static DriverInterface $displayNameDriver;
protected static DisplayNameDriver $displayNameDriver;

protected static AvatarDriver $avatarUrlDriver;

/**
* The hasher with which to hash passwords.
Expand Down Expand Up @@ -165,11 +169,16 @@ public static function setGate(Access\Gate $gate): void
static::$gate = $gate;
}

public static function setDisplayNameDriver(DriverInterface $driver): void
public static function setDisplayNameDriver(DisplayNameDriver $driver): void
{
static::$displayNameDriver = $driver;
}

public static function setAvatarDriver(AvatarDriver $driver): void
{
static::$avatarUrlDriver = $driver;
}

public static function setPasswordCheckers(array $checkers): void
{
static::$passwordCheckers = $checkers;
Expand Down Expand Up @@ -253,13 +262,18 @@ public function changeAvatarPath(?string $path): static
return $this;
}

public function getOriginalAvatarUrlAttribute(): ?string
{
return $this->attributes['avatar_url'];
}

public function getAvatarUrlAttribute(?string $value = null): ?string
{
if ($value && ! str_contains($value, '://')) {
return resolve(Factory::class)->disk('flarum-avatars')->url($value);
}

return $value;
return static::$avatarUrlDriver->avatarUrl($this);
}

public function getDisplayNameAttribute(): string
Expand Down
31 changes: 29 additions & 2 deletions framework/core/src/User/UserServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
use Flarum\Post\Post;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\Access\ScopeUserVisibility;
use Flarum\User\DisplayName\DriverInterface;
use Flarum\User\Avatar\DefaultDriver as AvatarDefaultDriver;
use Flarum\User\DisplayName\DriverInterface as DisplayNameDriverInterface;
use Flarum\User\Avatar\DriverInterface as AvatarDriverInterface;
use Flarum\User\DisplayName\UsernameDriver;
use Flarum\User\Event\EmailChangeRequested;
use Flarum\User\Event\Registered;
Expand All @@ -38,6 +40,7 @@ class UserServiceProvider extends AbstractServiceProvider
public function register(): void
{
$this->registerDisplayNameDrivers();
$this->registerAvatarDrivers();
$this->registerPasswordCheckers();

$this->container->singleton('flarum.user.group_processors', function () {
Expand Down Expand Up @@ -84,7 +87,30 @@ protected function registerDisplayNameDrivers(): void
: $container->make(UsernameDriver::class);
});

$this->container->alias('flarum.user.display_name.driver', DriverInterface::class);
$this->container->alias('flarum.user.display_name.driver', DisplayNameDriverInterface::class);
}

protected function registerAvatarDrivers(): void
{
$this->container->singleton('flarum.user.avatar.supported_drivers', function () {
return [
'default' => AvatarDefaultDriver::class,
];
});

$this->container->singleton('flarum.user.avatar.driver', function (Container $container) {
$drivers = $container->make('flarum.user.avatar.supported_drivers');
$settings = $container->make(SettingsRepositoryInterface::class);
$driverName = $settings->get('avatar_driver', '');

$driverClass = Arr::get($drivers, $driverName);

return $driverClass
? $container->make($driverClass)
: $container->make(AvatarDefaultDriver::class);
});

$this->container->alias('flarum.user.avatar.driver', AvatarDriverInterface::class);
}

protected function registerPasswordCheckers(): void
Expand Down Expand Up @@ -113,6 +139,7 @@ public function boot(Container $container, Dispatcher $events): void
User::setPasswordCheckers($container->make('flarum.user.password_checkers'));
User::setGate($container->makeWith(Access\Gate::class, ['policyClasses' => $container->make('flarum.policies')]));
User::setDisplayNameDriver($container->make('flarum.user.display_name.driver'));
User::setAvatarDriver($container->make('flarum.user.avatar.driver'));

$events->listen(Saving::class, SelfDemotionGuard::class);
$events->listen(Registered::class, AccountActivationMailer::class);
Expand Down
Loading