Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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 @@ -44,7 +44,7 @@ export default class AvatarEditor extends Component {
<Avatar user={user} loading="eager" />
<button
type="button"
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')}
ariaLabel={app.translator.trans('core.forum.user.avatar_upload_tooltip')}
data-toggle="dropdown"
Expand All @@ -57,7 +57,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 @@ -136,7 +136,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 @@ -127,6 +127,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, no avatar will be displayed when a user has not uploaded one.
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
1 change: 1 addition & 0 deletions framework/core/src/Admin/Content/AdminPayload.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public function __invoke(Document $document, Request $request): void
$document->payload['extensions'] = $this->extensions->getExtensions()->toArray();

$document->payload['displayNameDrivers'] = array_keys($this->container->make('flarum.user.display_name.supported_drivers'));
$document->payload['avatarDrivers'] = array_keys($this->container->make('flarum.user.avatar.supported_drivers'));
$document->payload['slugDrivers'] = array_map(array_keys(...), $this->container->make('flarum.http.slugDrivers'));
$document->payload['searchDrivers'] = $this->getSearchDrivers();

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 @@ -33,6 +34,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 @@ -72,6 +87,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 null when no uploaded avatar exists.
*/
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 an avatar driver.
*
* @public
*/
interface DriverInterface
{
/**
* Return an avatar URL for a user.
*/
public function avatarUrl(User $user): ?string;
}
38 changes: 34 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 Down Expand Up @@ -111,7 +112,12 @@ class User extends AbstractModel
/**
* A driver for getting display names.
*/
protected static DriverInterface $displayNameDriver;
protected static DisplayNameDriver $displayNameDriver;

/**
* A driver for getting avatar URLs.
*/
protected static AvatarDriver $avatarDriver;

/**
* The hasher with which to hash passwords.
Expand Down Expand Up @@ -165,11 +171,21 @@ 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;
}

/**
* Set the avatar driver.
*
* @internal
*/
public static function setAvatarDriver(AvatarDriver $driver): void
{
static::$avatarDriver = $driver;
}

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

/**
* Get the raw avatar_url attribute value before any driver processing.
*
* Useful for determining if a user has uploaded a custom avatar.
*/
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;
if ($value) {
return $value;
}

return static::$avatarDriver->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
Loading