diff --git a/src/Controller/Api/User/UserDeleteImagesApi.php b/src/Controller/Api/User/UserDeleteImagesApi.php index d1bf49e60..b7d06a4b8 100644 --- a/src/Controller/Api/User/UserDeleteImagesApi.php +++ b/src/Controller/Api/User/UserDeleteImagesApi.php @@ -5,6 +5,7 @@ namespace App\Controller\Api\User; use App\DTO\UserResponseDto; +use App\Event\User\UserEditedEvent; use App\Factory\UserFactory; use App\Service\UserManager; use Nelmio\ApiDocBundle\Attribute\Model; @@ -51,7 +52,12 @@ public function avatar( ): JsonResponse { $headers = $this->rateLimit($apiImageLimiter); - $manager->detachAvatar($this->getUserOrThrow()); + $user = $this->getUserOrThrow(); + $manager->detachAvatar($user); + /* + * Call edit so the @see UserEditedEvent is triggered and the changes are federated + */ + $manager->edit($user, $manager->createDto($user)); return new JsonResponse( $this->serializeUser($factory->createDto($this->getUserOrThrow())), @@ -94,7 +100,12 @@ public function cover( ): JsonResponse { $headers = $this->rateLimit($apiImageLimiter); - $manager->detachCover($this->getUserOrThrow()); + $user = $this->getUserOrThrow(); + $manager->detachCover($user); + /* + * Call edit so the @see UserEditedEvent is triggered and the changes are federated + */ + $manager->edit($user, $manager->createDto($user)); return new JsonResponse( $this->serializeUser($factory->createDto($this->getUserOrThrow())), diff --git a/src/Controller/User/Profile/UserEditController.php b/src/Controller/User/Profile/UserEditController.php index 38ad7bc37..b0f78c202 100644 --- a/src/Controller/User/Profile/UserEditController.php +++ b/src/Controller/User/Profile/UserEditController.php @@ -6,9 +6,11 @@ use App\Controller\AbstractController; use App\DTO\UserDto; +use App\Exception\ImageDownloadTooLargeException; use App\Form\UserBasicType; use App\Form\UserEmailType; use App\Form\UserPasswordType; +use App\Service\SettingsManager; use App\Service\UserManager; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Form\FormError; @@ -27,6 +29,7 @@ public function __construct( private readonly UserPasswordHasherInterface $userPasswordHasher, private readonly TranslatorInterface $translator, private readonly Security $security, + private readonly SettingsManager $settingsManager, ) { } @@ -176,6 +179,10 @@ private function handleForm( } return $form; + } catch (ImageDownloadTooLargeException $e) { + $this->addFlash('error', $this->translator->trans('flash_image_download_too_large_error', ['%bytes%' => $this->settingsManager->getMaxImageByteString()])); + + return null; } catch (\Exception $e) { return null; } diff --git a/src/Controller/User/UserAvatarDeleteController.php b/src/Controller/User/UserAvatarDeleteController.php index b3508ab44..a8131d081 100644 --- a/src/Controller/User/UserAvatarDeleteController.php +++ b/src/Controller/User/UserAvatarDeleteController.php @@ -22,7 +22,12 @@ public function __invoke(Request $request): Response { $this->denyAccessUnlessGranted('edit_profile', $this->getUserOrThrow()); - $this->userManager->detachAvatar($this->getUserOrThrow()); + $user = $this->getUserOrThrow(); + $this->userManager->detachAvatar($user); + /* + * Call edit so the @see UserEditedEvent is triggered and the changes are federated + */ + $this->userManager->edit($user, $this->userManager->createDto($user)); if ($request->isXmlHttpRequest()) { return new JsonResponse( diff --git a/src/Controller/User/UserCoverDeleteController.php b/src/Controller/User/UserCoverDeleteController.php index 95e5cabac..8a1386875 100644 --- a/src/Controller/User/UserCoverDeleteController.php +++ b/src/Controller/User/UserCoverDeleteController.php @@ -22,7 +22,12 @@ public function __invoke(Request $request): Response { $this->denyAccessUnlessGranted('edit_profile', $this->getUserOrThrow()); - $this->userManager->detachCover($this->getUserOrThrow()); + $user = $this->getUserOrThrow(); + $this->userManager->detachCover($user); + /* + * Call edit so the @see UserEditedEvent is triggered and the changes are federated + */ + $this->userManager->edit($user, $this->userManager->createDto($user)); if ($request->isXmlHttpRequest()) { return new JsonResponse( diff --git a/src/MessageHandler/ActivityPub/Outbox/UpdateHandler.php b/src/MessageHandler/ActivityPub/Outbox/UpdateHandler.php index 441c84914..1b2a65e24 100644 --- a/src/MessageHandler/ActivityPub/Outbox/UpdateHandler.php +++ b/src/MessageHandler/ActivityPub/Outbox/UpdateHandler.php @@ -23,6 +23,7 @@ use App\Service\DeliverManager; use App\Service\SettingsManager; use Doctrine\ORM\EntityManagerInterface; +use Psr\Log\LoggerInterface; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; @@ -38,6 +39,7 @@ public function __construct( private readonly DeliverManager $deliverManager, private readonly UpdateWrapper $updateWrapper, private readonly KernelInterface $kernel, + private readonly LoggerInterface $logger, ) { parent::__construct($this->entityManager, $this->kernel); } @@ -88,6 +90,7 @@ public function doWork(MessageInterface $message): void $activity = $this->updateWrapper->buildForActor($entity, $editedByUser); if ($entity instanceof User) { $inboxes = $this->userRepository->findAudience($entity); + $this->logger->debug('[UpdateHandler::doWork] sending update user activity for user {u} to {i}', ['u' => $entity->username, 'i' => join(', ', $inboxes)]); } elseif ($entity instanceof Magazine) { if ('random' === $entity->name) { // do not federate the random magazine diff --git a/src/Service/ActivityPubManager.php b/src/Service/ActivityPubManager.php index 75d10a915..e42c6b4ea 100644 --- a/src/Service/ActivityPubManager.php +++ b/src/Service/ActivityPubManager.php @@ -409,6 +409,9 @@ private function updateUser(string $actorUrl): ?User $this->bus->dispatch(new DeleteImageMessage($user->avatar->getId())); } $user->avatar = $newImage; + } elseif (null !== $user->avatar) { + $this->bus->dispatch(new DeleteImageMessage($user->avatar->getId())); + $user->avatar = null; } // Only update cover if image is set @@ -421,6 +424,9 @@ private function updateUser(string $actorUrl): ?User $this->bus->dispatch(new DeleteImageMessage($user->cover->getId())); } $user->cover = $newImage; + } elseif (null !== $user->cover) { + $this->bus->dispatch(new DeleteImageMessage($user->cover->getId())); + $user->cover = null; } if (null !== $user->apFollowersUrl) { @@ -565,6 +571,9 @@ private function updateMagazine(string $actorUrl): ?Magazine $this->bus->dispatch(new DeleteImageMessage($magazine->icon->getId())); } $magazine->icon = $newImage; + } elseif (null !== $magazine->icon) { + $this->bus->dispatch(new DeleteImageMessage($magazine->icon->getId())); + $magazine->icon = null; } if ($actor['name']) { diff --git a/templates/user/settings/profile.html.twig b/templates/user/settings/profile.html.twig index b3f1f4114..706405f7c 100644 --- a/templates/user/settings/profile.html.twig +++ b/templates/user/settings/profile.html.twig @@ -29,20 +29,54 @@ {{ form_start(form) }} {{ component('editor_toolbar', {id: 'user_basic_about'}) }} {{ form_row(form.about, {label: false, attr: { - placeholder: 'about', - 'data-controller': 'input-length rich-textarea autogrow', + placeholder: 'about', + 'data-controller': 'input-length rich-textarea autogrow', 'data-entry-link-create-target': 'user_about', 'data-action' : 'input-length#updateDisplay', 'data-input-length-max-value' : constant('App\\DTO\\UserDto::MAX_ABOUT_LENGTH') }}) }} {{ form_row(form.username, {label: 'username', attr: { - 'data-controller': 'input-length autogrow', + 'data-controller': 'input-length autogrow', 'data-entry-link-create-target': 'user_about', 'data-action' : 'input-length#updateDisplay', 'data-input-length-max-value' : constant('App\\DTO\\UserDto::MAX_USERNAME_LENGTH') }}) }} {{ form_row(form.avatar, {label: 'avatar'}) }} + {% if app.user.avatar is not same as null %} +