From e98976a6cfc2636f50df460f35a05ff863b8ea76 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 24 Nov 2024 16:39:42 +0700 Subject: [PATCH 01/12] feat: add support for customizable user avatar drivers --- extensions/nicknames/extend.php | 1 + .../core/js/src/admin/AdminApplication.tsx | 1 + .../js/src/admin/components/BasicsPage.tsx | 19 ++++++++++++ framework/core/js/src/common/models/User.tsx | 4 +++ .../js/src/forum/components/AvatarEditor.js | 6 ++-- framework/core/locale/core.yml | 4 +++ .../core/src/Admin/Content/AdminPayload.php | 3 +- .../core/src/Api/Resource/UserResource.php | 1 + framework/core/src/Extend/User.php | 19 ++++++++++++ .../core/src/User/Avatar/DefaultDriver.php | 23 ++++++++++++++ .../core/src/User/Avatar/DriverInterface.php | 25 +++++++++++++++ framework/core/src/User/User.php | 22 ++++++++++--- .../core/src/User/UserServiceProvider.php | 31 +++++++++++++++++-- 13 files changed, 149 insertions(+), 10 deletions(-) create mode 100644 framework/core/src/User/Avatar/DefaultDriver.php create mode 100644 framework/core/src/User/Avatar/DriverInterface.php diff --git a/extensions/nicknames/extend.php b/extensions/nicknames/extend.php index c38f586c3b..bb94627d04 100644 --- a/extensions/nicknames/extend.php +++ b/extensions/nicknames/extend.php @@ -14,6 +14,7 @@ use Flarum\Nicknames\Access\UserPolicy; use Flarum\Nicknames\Api\UserResourceFields; use Flarum\Search\Database\DatabaseSearchDriver; +use Flarum\User\Avatar\GravatarDriver; use Flarum\User\Search\UserSearcher; use Flarum\User\User; 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..8eaed0ca9a 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::setAvatarUrlDriver($container->make('flarum.user.avatar.driver')); $events->listen(Saving::class, SelfDemotionGuard::class); $events->listen(Registered::class, AccountActivationMailer::class); From 6db0e31a7d6aaa5fffb2d951a2844c82810ca6d1 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 24 Nov 2024 16:41:09 +0700 Subject: [PATCH 02/12] fix: remove unused GravatarDriver import from nicknames extension --- extensions/nicknames/extend.php | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/nicknames/extend.php b/extensions/nicknames/extend.php index bb94627d04..c38f586c3b 100644 --- a/extensions/nicknames/extend.php +++ b/extensions/nicknames/extend.php @@ -14,7 +14,6 @@ use Flarum\Nicknames\Access\UserPolicy; use Flarum\Nicknames\Api\UserResourceFields; use Flarum\Search\Database\DatabaseSearchDriver; -use Flarum\User\Avatar\GravatarDriver; use Flarum\User\Search\UserSearcher; use Flarum\User\User; From a54d1049faec6b24761594ed05b67f4d84dc719c Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 24 Nov 2024 16:43:37 +0700 Subject: [PATCH 03/12] fix: rename setAvatarUrlDriver to setAvatarDriver for consistency --- framework/core/src/User/User.php | 2 +- framework/core/src/User/UserServiceProvider.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/core/src/User/User.php b/framework/core/src/User/User.php index 863485e177..8592b70dd4 100644 --- a/framework/core/src/User/User.php +++ b/framework/core/src/User/User.php @@ -174,7 +174,7 @@ public static function setDisplayNameDriver(DisplayNameDriver $driver): void static::$displayNameDriver = $driver; } - public static function setAvatarUrlDriver(AvatarDriver $driver): void + public static function setAvatarDriver(AvatarDriver $driver): void { static::$avatarUrlDriver = $driver; } diff --git a/framework/core/src/User/UserServiceProvider.php b/framework/core/src/User/UserServiceProvider.php index 8eaed0ca9a..a7c41f1486 100644 --- a/framework/core/src/User/UserServiceProvider.php +++ b/framework/core/src/User/UserServiceProvider.php @@ -139,7 +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::setAvatarUrlDriver($container->make('flarum.user.avatar.driver')); + User::setAvatarDriver($container->make('flarum.user.avatar.driver')); $events->listen(Saving::class, SelfDemotionGuard::class); $events->listen(Registered::class, AccountActivationMailer::class); From b4a3e50f3f22e0b7df73ff64f8b24798ee17bd2a Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Fri, 26 Dec 2025 10:36:31 +0700 Subject: [PATCH 04/12] wip --- framework/core/src/Admin/Content/AdminPayload.php | 2 +- framework/core/src/User/Avatar/DefaultDriver.php | 2 +- framework/core/src/User/Avatar/DriverInterface.php | 4 ++-- framework/core/src/User/User.php | 4 ++++ 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/framework/core/src/Admin/Content/AdminPayload.php b/framework/core/src/Admin/Content/AdminPayload.php index c2a2fbb6a0..d66929b734 100644 --- a/framework/core/src/Admin/Content/AdminPayload.php +++ b/framework/core/src/Admin/Content/AdminPayload.php @@ -55,7 +55,7 @@ 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(abstract: 'flarum.user.display_name.supported_drivers')); + $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(); diff --git a/framework/core/src/User/Avatar/DefaultDriver.php b/framework/core/src/User/Avatar/DefaultDriver.php index 55c649a75a..efa596c4f7 100644 --- a/framework/core/src/User/Avatar/DefaultDriver.php +++ b/framework/core/src/User/Avatar/DefaultDriver.php @@ -12,7 +12,7 @@ use Flarum\User\User; /** - * The default driver, which returns the user's avatar URL. + * The default driver, which returns null when no uploaded avatar exists. */ class DefaultDriver implements DriverInterface { diff --git a/framework/core/src/User/Avatar/DriverInterface.php b/framework/core/src/User/Avatar/DriverInterface.php index 163a9a033b..dda1010f02 100644 --- a/framework/core/src/User/Avatar/DriverInterface.php +++ b/framework/core/src/User/Avatar/DriverInterface.php @@ -12,14 +12,14 @@ use Flarum\User\User; /** - * An interface for a avatar driver. + * An interface for an avatar driver. * * @public */ interface DriverInterface { /** - * Return a avatar for a user. + * Return an avatar URL for a user. */ public function avatarUrl(User $user): ?string; } diff --git a/framework/core/src/User/User.php b/framework/core/src/User/User.php index d755afb9ce..396ac94bd7 100644 --- a/framework/core/src/User/User.php +++ b/framework/core/src/User/User.php @@ -273,6 +273,10 @@ public function getAvatarUrlAttribute(?string $value = null): ?string return resolve(Factory::class)->disk('flarum-avatars')->url($value); } + if ($value) { + return $value; + } + return static::$avatarUrlDriver->avatarUrl($this); } From 086c53e5546977066a8222492ef5eaa9aa77bf9e Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Fri, 26 Dec 2025 11:05:14 +0700 Subject: [PATCH 05/12] wip --- framework/core/locale/core.yml | 2 +- framework/core/src/User/User.php | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/framework/core/locale/core.yml b/framework/core/locale/core.yml index a0f4c6a51d..61d9cab539 100644 --- a/framework/core/locale/core.yml +++ b/framework/core/locale/core.yml @@ -130,7 +130,7 @@ core: 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. + 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 diff --git a/framework/core/src/User/User.php b/framework/core/src/User/User.php index 396ac94bd7..b21e86db35 100644 --- a/framework/core/src/User/User.php +++ b/framework/core/src/User/User.php @@ -115,7 +115,10 @@ class User extends AbstractModel */ protected static DisplayNameDriver $displayNameDriver; - protected static AvatarDriver $avatarUrlDriver; + /** + * A driver for getting avatar URLs. + */ + protected static AvatarDriver $avatarDriver; /** * The hasher with which to hash passwords. @@ -174,9 +177,14 @@ public static function setDisplayNameDriver(DisplayNameDriver $driver): void static::$displayNameDriver = $driver; } + /** + * Set the avatar driver. + * + * @internal + */ public static function setAvatarDriver(AvatarDriver $driver): void { - static::$avatarUrlDriver = $driver; + static::$avatarDriver = $driver; } public static function setPasswordCheckers(array $checkers): void @@ -277,7 +285,7 @@ public function getAvatarUrlAttribute(?string $value = null): ?string return $value; } - return static::$avatarUrlDriver->avatarUrl($this); + return static::$avatarDriver->avatarUrl($this); } public function getDisplayNameAttribute(): string From 88ad73861c10cb105af07186a61440a4e345a8f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Fri, 26 Dec 2025 11:12:21 +0700 Subject: [PATCH 06/12] Update framework/core/src/User/User.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- framework/core/src/User/User.php | 1 - 1 file changed, 1 deletion(-) diff --git a/framework/core/src/User/User.php b/framework/core/src/User/User.php index b21e86db35..f4e72265bb 100644 --- a/framework/core/src/User/User.php +++ b/framework/core/src/User/User.php @@ -35,7 +35,6 @@ 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; From c53256cc257db6ad1202959395ed71d3185822c3 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Fri, 26 Dec 2025 11:16:17 +0700 Subject: [PATCH 07/12] wip --- framework/core/src/User/User.php | 5 ++ .../tests/integration/extenders/UserTest.php | 59 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/framework/core/src/User/User.php b/framework/core/src/User/User.php index f4e72265bb..535c8c6191 100644 --- a/framework/core/src/User/User.php +++ b/framework/core/src/User/User.php @@ -269,6 +269,11 @@ 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']; diff --git a/framework/core/tests/integration/extenders/UserTest.php b/framework/core/tests/integration/extenders/UserTest.php index 954bd3b62f..24fb4c8f21 100644 --- a/framework/core/tests/integration/extenders/UserTest.php +++ b/framework/core/tests/integration/extenders/UserTest.php @@ -12,6 +12,7 @@ use Flarum\Extend; use Flarum\Testing\integration\RetrievesAuthorizedUsers; use Flarum\Testing\integration\TestCase; +use Flarum\User\Avatar\DriverInterface as AvatarDriverInterface; use Flarum\User\DisplayName\DriverInterface; use Flarum\User\User; use Illuminate\Support\Arr; @@ -70,6 +71,56 @@ public function can_use_custom_display_name_driver() $this->assertEquals('admin@machine.local$$$suffix', $user->displayName); } + #[Test] + public function default_avatar_driver_returns_null_when_no_avatar_uploaded() + { + $this->app(); + + $user = User::find(1); + + $this->assertNull($user->avatar_url); + } + + #[Test] + public function can_use_custom_avatar_driver() + { + $this->setting('avatar_driver', 'custom'); + + $this->extend( + (new Extend\User) + ->avatarDriver('custom', CustomAvatarDriver::class) + ); + + $this->app(); + + $user = User::find(1); + + $this->assertEquals('https://example.com/avatar/1', $user->avatar_url); + } + + #[Test] + public function custom_avatar_driver_not_used_when_user_has_uploaded_avatar() + { + $this->setting('avatar_driver', 'custom'); + + $this->extend( + (new Extend\User) + ->avatarDriver('custom', CustomAvatarDriver::class) + ); + + $this->prepareDatabase([ + User::class => [ + ['id' => 1, 'username' => 'admin', 'email' => 'admin@machine.local', 'is_email_confirmed' => 1, 'avatar_url' => 'https://uploaded.example.com/my-avatar.png'], + ] + ]); + + $this->app(); + + $user = User::find(1); + + $this->assertEquals('https://uploaded.example.com/my-avatar.png', $user->avatar_url); + } + #[Test] public function user_has_permissions_for_expected_groups_if_no_processors_added() { @@ -168,3 +219,11 @@ public function __invoke(User $user, array $groupIds) }); } } + +class CustomAvatarDriver implements AvatarDriverInterface +{ + public function avatarUrl(User $user): ?string + { + return 'https://example.com/avatar/'.$user->id; + } +} From f24230c06b6c574ed6eb91ec24b0ee685d118a99 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Fri, 26 Dec 2025 11:31:41 +0700 Subject: [PATCH 08/12] wip --- js-packages/jest-config/src/boostrap/admin.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js-packages/jest-config/src/boostrap/admin.js b/js-packages/jest-config/src/boostrap/admin.js index 2b4be87910..8a6be9676a 100644 --- a/js-packages/jest-config/src/boostrap/admin.js +++ b/js-packages/jest-config/src/boostrap/admin.js @@ -8,6 +8,7 @@ export default function bootstrapAdmin(payload = {}) { settings: {}, permissions: {}, displayNameDrivers: [], + avatarDrivers: [], slugDrivers: {}, searchDrivers: {}, modelStatistics: { From 43063b5203e7c6031b35c6706f040ff1c0c481b1 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Fri, 26 Dec 2025 11:32:58 +0700 Subject: [PATCH 09/12] wip --- framework/core/src/User/User.php | 2 +- framework/core/src/User/UserServiceProvider.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/core/src/User/User.php b/framework/core/src/User/User.php index 535c8c6191..5b60eb2623 100644 --- a/framework/core/src/User/User.php +++ b/framework/core/src/User/User.php @@ -20,8 +20,8 @@ use Flarum\Http\AccessToken; use Flarum\Notification\Notification; use Flarum\Post\Post; -use Flarum\User\DisplayName\DriverInterface as DisplayNameDriver; use Flarum\User\Avatar\DriverInterface as AvatarDriver; +use Flarum\User\DisplayName\DriverInterface as DisplayNameDriver; use Flarum\User\Event\Activated; use Flarum\User\Event\AvatarChanged; use Flarum\User\Event\Deleted; diff --git a/framework/core/src/User/UserServiceProvider.php b/framework/core/src/User/UserServiceProvider.php index 689f204e17..182670c92c 100644 --- a/framework/core/src/User/UserServiceProvider.php +++ b/framework/core/src/User/UserServiceProvider.php @@ -22,8 +22,8 @@ use Flarum\Settings\SettingsRepositoryInterface; use Flarum\User\Access\ScopeUserVisibility; 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\DriverInterface as DisplayNameDriverInterface; use Flarum\User\DisplayName\UsernameDriver; use Flarum\User\Event\EmailChangeRequested; use Flarum\User\Event\Registered; From 6621bc2dea2ecda9f1a4788130bc0125dff926e4 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 22 Feb 2026 00:29:51 +0700 Subject: [PATCH 10/12] Refactor avatar upload check to boolean attribute Replaces the `originalAvatarUrl` attribute with `hasUploadedAvatar` for a more semantic check of whether a user has uploaded a custom avatar. This change improves clarity and maintainability of avatar-related logic. --- framework/core/js/src/common/models/User.tsx | 4 ++-- .../core/js/src/forum/components/AvatarEditor.js | 6 +++--- framework/core/src/Api/Resource/UserResource.php | 2 +- framework/core/src/User/User.php | 14 ++++---------- framework/core/src/User/UserServiceProvider.php | 8 +++----- .../core/tests/integration/extenders/UserTest.php | 2 ++ 6 files changed, 15 insertions(+), 21 deletions(-) diff --git a/framework/core/js/src/common/models/User.tsx b/framework/core/js/src/common/models/User.tsx index 37bb35324d..eee5f59bab 100644 --- a/framework/core/js/src/common/models/User.tsx +++ b/framework/core/js/src/common/models/User.tsx @@ -30,8 +30,8 @@ export default class User extends Model { return Model.attribute('password').call(this); } - originalAvatarUrl() { - return Model.attribute('originalAvatarUrl').call(this); + hasUploadedAvatar() { + return Model.attribute('hasUploadedAvatar').call(this); } avatarUrl() { diff --git a/framework/core/js/src/forum/components/AvatarEditor.js b/framework/core/js/src/forum/components/AvatarEditor.js index 01ee7364a3..790bce4b1e 100644 --- a/framework/core/js/src/forum/components/AvatarEditor.js +++ b/framework/core/js/src/forum/components/AvatarEditor.js @@ -44,7 +44,7 @@ export default class AvatarEditor extends Component {