diff --git a/framework/core/js/src/admin/AdminApplication.tsx b/framework/core/js/src/admin/AdminApplication.tsx index fd00ac8066..4d5a6290cc 100644 --- a/framework/core/js/src/admin/AdminApplication.tsx +++ b/framework/core/js/src/admin/AdminApplication.tsx @@ -58,6 +58,7 @@ export interface AdminApplicationData extends ApplicationData { settings: Record; modelStatistics: Record; displayNameDrivers: string[]; + avatarDrivers: string[]; slugDrivers: Record; searchDrivers: Record; permissions: Record; diff --git a/framework/core/js/src/admin/components/BasicsPage.tsx b/framework/core/js/src/admin/components/BasicsPage.tsx index 42cd5907b1..29977d7fda 100644 --- a/framework/core/js/src/admin/components/BasicsPage.tsx +++ b/framework/core/js/src/admin/components/BasicsPage.tsx @@ -10,6 +10,7 @@ import extractText from '../../common/utils/extractText'; export type HomePageItem = { path: string; label: Mithril.Children }; export type DriverLocale = { display_name: Record; + avatar: Record; slug: Record>; }; @@ -58,6 +59,9 @@ export default class BasicsPage 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')), @@ -82,6 +86,7 @@ export default class BasicsPage ext const localeOptions: Record = {}; const displayNameOptions: Record = {}; + const avatarDriverOptions: Record = {}; const slugDriverOptions: Record> = {}; const driverLocale = BasicsPage.driverLocale(); @@ -94,6 +99,10 @@ export default class BasicsPage 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] = {}; @@ -169,6 +178,16 @@ export default class BasicsPage 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; diff --git a/framework/core/js/src/common/models/User.tsx b/framework/core/js/src/common/models/User.tsx index d0d03fea68..37bb35324d 100644 --- a/framework/core/js/src/common/models/User.tsx +++ b/framework/core/js/src/common/models/User.tsx @@ -30,6 +30,10 @@ export default class User extends Model { return Model.attribute('password').call(this); } + originalAvatarUrl() { + return Model.attribute('originalAvatarUrl').call(this); + } + avatarUrl() { return Model.attribute('avatarUrl').call(this); } diff --git a/framework/core/js/src/forum/components/AvatarEditor.js b/framework/core/js/src/forum/components/AvatarEditor.js index fd1da14226..38803fc5fc 100644 --- a/framework/core/js/src/forum/components/AvatarEditor.js +++ b/framework/core/js/src/forum/components/AvatarEditor.js @@ -43,7 +43,7 @@ export default class AvatarEditor extends Component {
{this.loading ? ( - ) : user.avatarUrl() ? ( + ) : user.originalAvatarUrl() ? ( ) : ( @@ -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(); diff --git a/framework/core/locale/core.yml b/framework/core/locale/core.yml index a401e9bff9..73254bad92 100644 --- a/framework/core/locale/core.yml +++ b/framework/core/locale/core.yml @@ -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 diff --git a/framework/core/src/Admin/Content/AdminPayload.php b/framework/core/src/Admin/Content/AdminPayload.php index 134340a552..498cb2ca30 100644 --- a/framework/core/src/Admin/Content/AdminPayload.php +++ b/framework/core/src/Admin/Content/AdminPayload.php @@ -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')); diff --git a/framework/core/src/Api/Resource/UserResource.php b/framework/core/src/Api/Resource/UserResource.php index 5160dc3eba..f8eb4a3151 100644 --- a/framework/core/src/Api/Resource/UserResource.php +++ b/framework/core/src/Api/Resource/UserResource.php @@ -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); diff --git a/framework/core/src/Extend/User.php b/framework/core/src/Extend/User.php index 1b76322f59..3d448d4b37 100644 --- a/framework/core/src/Extend/User.php +++ b/framework/core/src/Extend/User.php @@ -17,6 +17,7 @@ class User implements ExtenderInterface { private array $displayNameDrivers = []; + private array $avatarDrivers = []; private array $groupProcessors = []; private array $preferences = []; @@ -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. @@ -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); }); diff --git a/framework/core/src/User/Avatar/DefaultDriver.php b/framework/core/src/User/Avatar/DefaultDriver.php new file mode 100644 index 0000000000..55c649a75a --- /dev/null +++ b/framework/core/src/User/Avatar/DefaultDriver.php @@ -0,0 +1,23 @@ +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 diff --git a/framework/core/src/User/UserServiceProvider.php b/framework/core/src/User/UserServiceProvider.php index 524871feaa..a7c41f1486 100644 --- a/framework/core/src/User/UserServiceProvider.php +++ b/framework/core/src/User/UserServiceProvider.php @@ -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; @@ -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 () { @@ -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 @@ -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);