[COMPONENTS][Subscription] Subscribe Actor action implemented
[TWIG] AddProfileAction event added [CARDS][Profile] Refactor and restyling to accomodate Actor actions
This commit is contained in:
parent
0d1ab2c9cf
commit
0c245fcb6e
@ -23,9 +23,19 @@ declare(strict_types = 1);
|
|||||||
|
|
||||||
namespace Component\Subscription\Controller;
|
namespace Component\Subscription\Controller;
|
||||||
|
|
||||||
|
use App\Core\DB\DB;
|
||||||
|
use App\Core\Form;
|
||||||
use function App\Core\I18n\_m;
|
use function App\Core\I18n\_m;
|
||||||
|
use App\Core\Log;
|
||||||
|
use App\Core\Router\Router;
|
||||||
|
use App\Entity\Actor;
|
||||||
|
use App\Util\Common;
|
||||||
|
use App\Util\Exception\ClientException;
|
||||||
|
use App\Util\Exception\RedirectException;
|
||||||
use Component\Collection\Util\ActorControllerTrait;
|
use Component\Collection\Util\ActorControllerTrait;
|
||||||
use Component\Collection\Util\Controller\CircleController;
|
use Component\Collection\Util\Controller\CircleController;
|
||||||
|
use Component\Subscription\Subscription as SubscriptionComponent;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -58,4 +68,100 @@ class Subscribers extends CircleController
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \App\Util\Exception\DuplicateFoundException
|
||||||
|
* @throws \App\Util\Exception\NoLoggedInUser
|
||||||
|
* @throws \App\Util\Exception\NotFoundException
|
||||||
|
* @throws \App\Util\Exception\ServerException
|
||||||
|
* @throws ClientException
|
||||||
|
* @throws RedirectException
|
||||||
|
*/
|
||||||
|
public function subscribersAdd(Request $request, int $object_id): array
|
||||||
|
{
|
||||||
|
$subject = Common::ensureLoggedIn();
|
||||||
|
$object = Actor::getById($object_id);
|
||||||
|
$form = Form::create(
|
||||||
|
[
|
||||||
|
['subscriber_add', SubmitType::class, ['label' => _m('Subscribe!')]],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
if (!\is_null(SubscriptionComponent::subscribe($subject, $object))) {
|
||||||
|
DB::flush();
|
||||||
|
SubscriptionComponent::refreshSubscriptionCount($subject, $object);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect user to where they came from
|
||||||
|
// Prevent open redirect
|
||||||
|
if (!\is_null($from = $this->string('from'))) {
|
||||||
|
if (Router::isAbsolute($from)) {
|
||||||
|
Log::warning("Actor {$object_id} attempted to reply to a note and then get redirected to another host, or the URL was invalid ({$from})");
|
||||||
|
throw new ClientException(_m('Can not redirect to outside the website from here'), 400); // 400 Bad request (deceptive)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO anchor on element id
|
||||||
|
throw new RedirectException(url: $from);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have a URL to return to, go to the instance root
|
||||||
|
throw new RedirectException('root');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'_template' => 'subscription/add_subscriber.html.twig',
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'object' => $object,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \App\Util\Exception\DuplicateFoundException
|
||||||
|
* @throws \App\Util\Exception\NoLoggedInUser
|
||||||
|
* @throws \App\Util\Exception\NotFoundException
|
||||||
|
* @throws \App\Util\Exception\ServerException
|
||||||
|
* @throws ClientException
|
||||||
|
* @throws RedirectException
|
||||||
|
*/
|
||||||
|
public function subscribersRemove(Request $request, int $object_id): array
|
||||||
|
{
|
||||||
|
$subject = Common::ensureLoggedIn();
|
||||||
|
$object = Actor::getById($object_id);
|
||||||
|
$form = Form::create(
|
||||||
|
[
|
||||||
|
['subscriber_remove', SubmitType::class, ['label' => _m('Unsubscribe')]],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
if (!\is_null(SubscriptionComponent::unsubscribe($subject, $object))) {
|
||||||
|
DB::flush();
|
||||||
|
SubscriptionComponent::refreshSubscriptionCount($subject, $object);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect user to where they came from
|
||||||
|
// Prevent open redirect
|
||||||
|
if (!\is_null($from = $this->string('from'))) {
|
||||||
|
if (Router::isAbsolute($from)) {
|
||||||
|
Log::warning("Actor {$object_id} attempted to subscribe an actor and then get redirected to another host, or the URL was invalid ({$from})");
|
||||||
|
throw new ClientException(_m('Can not redirect to outside the website from here'), 400); // 400 Bad request (deceptive)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO anchor on element id
|
||||||
|
throw new RedirectException(url: $from);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have a URL to return to, go to the instance root
|
||||||
|
throw new RedirectException('root');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'_template' => 'subscription/remove_subscriber.html.twig',
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'object' => $object,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,49 +23,84 @@ declare(strict_types = 1);
|
|||||||
|
|
||||||
namespace Component\Subscription;
|
namespace Component\Subscription;
|
||||||
|
|
||||||
|
use App\Core\Cache;
|
||||||
use App\Core\DB\DB;
|
use App\Core\DB\DB;
|
||||||
use App\Core\Event;
|
use App\Core\Event;
|
||||||
use function App\Core\I18n\_m;
|
use function App\Core\I18n\_m;
|
||||||
use App\Core\Modules\Component;
|
use App\Core\Modules\Component;
|
||||||
use App\Core\Router\RouteLoader;
|
use App\Core\Router\RouteLoader;
|
||||||
|
use App\Core\Router\Router;
|
||||||
use App\Entity\Activity;
|
use App\Entity\Activity;
|
||||||
use App\Entity\Actor;
|
use App\Entity\Actor;
|
||||||
use App\Entity\LocalUser;
|
use App\Entity\LocalUser;
|
||||||
|
use App\Util\Common;
|
||||||
|
use App\Util\Exception\DuplicateFoundException;
|
||||||
|
use App\Util\Exception\NotFoundException;
|
||||||
use App\Util\Exception\ServerException;
|
use App\Util\Exception\ServerException;
|
||||||
use App\Util\Nickname;
|
use App\Util\Nickname;
|
||||||
use Component\Subscription\Controller\Subscribers;
|
use Component\Subscription\Controller\Subscribers as SubscribersController;
|
||||||
use Component\Subscription\Controller\Subscriptions;
|
use Component\Subscription\Controller\Subscriptions as SubscriptionsController;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
class Subscription extends Component
|
class Subscription extends Component
|
||||||
{
|
{
|
||||||
public function onAddRoute(RouteLoader $r): bool
|
public function onAddRoute(RouteLoader $r): bool
|
||||||
{
|
{
|
||||||
$r->connect(id: 'actor_subscriptions_id', uri_path: '/actor/{id<\d+>}/subscriptions', target: [Subscriptions::class, 'subscriptionsByActorId']);
|
$r->connect(id: 'actor_subscribe_add', uri_path: '/actor/subscribe/{object_id<\d+>}', target: [SubscribersController::class, 'subscribersAdd']);
|
||||||
$r->connect(id: 'actor_subscriptions_nickname', uri_path: '/@{nickname<' . Nickname::DISPLAY_FMT . '>}/subscriptions', target: [Subscriptions::class, 'subscriptionsByActorNickname']);
|
$r->connect(id: 'actor_subscribe_remove', uri_path: '/actor/unsubscribe/{object_id<\d+>}', target: [SubscribersController::class, 'subscribersRemove']);
|
||||||
$r->connect(id: 'actor_subscribers_id', uri_path: '/actor/{id<\d+>}/subscribers', target: [Subscribers::class, 'subscribersByActorId']);
|
$r->connect(id: 'actor_subscriptions_id', uri_path: '/actor/{id<\d+>}/subscriptions', target: [SubscriptionsController::class, 'subscriptionsByActorId']);
|
||||||
$r->connect(id: 'actor_subscribers_nickname', uri_path: '/@{nickname<' . Nickname::DISPLAY_FMT . '>}/subscribers', target: [Subscribers::class, 'subscribersByActorNickname']);
|
$r->connect(id: 'actor_subscriptions_nickname', uri_path: '/@{nickname<' . Nickname::DISPLAY_FMT . '>}/subscriptions', target: [SubscriptionsController::class, 'subscriptionsByActorNickname']);
|
||||||
|
$r->connect(id: 'actor_subscribers_id', uri_path: '/actor/{id<\d+>}/subscribers', target: [SubscribersController::class, 'subscribersByActorId']);
|
||||||
|
$r->connect(id: 'actor_subscribers_nickname', uri_path: '/@{nickname<' . Nickname::DISPLAY_FMT . '>}/subscribers', target: [SubscribersController::class, 'subscribersByActorNickname']);
|
||||||
return Event::next;
|
return Event::next;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persists a new Subscription Entity from Subscriber to Subject (Actor being subscribed) and Activity
|
* To use after Subscribe/Unsubscribe and DB::flush()
|
||||||
|
*
|
||||||
|
* @param Actor|int|LocalUser $subject The Actor who subscribed or unsubscribed
|
||||||
|
* @param Actor|int|LocalUser $object The Actor who was subscribed or unsubscribed from
|
||||||
|
*/
|
||||||
|
public static function refreshSubscriptionCount(int|Actor|LocalUser $subject, int|Actor|LocalUser $object): array
|
||||||
|
{
|
||||||
|
$subscriber_id = \is_int($subject) ? $subject : $subject->getId();
|
||||||
|
$subscribed_id = \is_int($object) ? $object : $object->getId();
|
||||||
|
|
||||||
|
$cache_subscriber = Cache::delete(Actor::cacheKeys($subscriber_id)['subscribed']);
|
||||||
|
$cache_subscribed = Cache::delete(Actor::cacheKeys($subscribed_id)['subscribers']);
|
||||||
|
|
||||||
|
return [$cache_subscriber,$cache_subscribed];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists a new Subscription Entity from Subject to Object (Actor being subscribed) and Activity
|
||||||
*
|
*
|
||||||
* A new notification is then handled, informing all interested Actors of this action
|
* A new notification is then handled, informing all interested Actors of this action
|
||||||
*
|
*
|
||||||
|
* @param Actor|int|LocalUser $subject The actor performing the subscription
|
||||||
|
* @param Actor|int|LocalUser $object The target of the subscription
|
||||||
|
*
|
||||||
|
* @throws DuplicateFoundException
|
||||||
|
* @throws NotFoundException
|
||||||
* @throws ServerException
|
* @throws ServerException
|
||||||
|
*
|
||||||
|
* @return null|Activity a new Activity if changes were made
|
||||||
|
*
|
||||||
|
* @see self::refreshSubscriptionCount() to delete cache after this action
|
||||||
*/
|
*/
|
||||||
public static function subscribe(int|Actor|LocalUser $subscriber, int|Actor|LocalUser $subject, string $source = 'web'): ?Activity
|
public static function subscribe(int|Actor|LocalUser $subject, int|Actor|LocalUser $object, string $source = 'web'): ?Activity
|
||||||
{
|
{
|
||||||
$subscriber_id = \is_int($subscriber) ? $subscriber : $subscriber->getId();
|
$subscriber_id = \is_int($subject) ? $subject : $subject->getId();
|
||||||
$subscribed_id = \is_int($subject) ? $subject : $subject->getId();
|
$subscribed_id = \is_int($object) ? $object : $object->getId();
|
||||||
$opts = [
|
$opts = [
|
||||||
'subscriber_id' => $subscriber_id,
|
'subscriber_id' => $subscriber_id,
|
||||||
'subscribed_id' => $subscribed_id,
|
'subscribed_id' => $subscribed_id,
|
||||||
];
|
];
|
||||||
$subscription = DB::findOneBy(table: \Component\Subscription\Entity\Subscription::class, criteria: $opts, return_null: true);
|
$subscription = DB::findOneBy(table: Entity\Subscription::class, criteria: $opts, return_null: true);
|
||||||
$activity = null;
|
$activity = null;
|
||||||
if (\is_null($subscription)) {
|
if (\is_null($subscription)) {
|
||||||
DB::persist(\Component\Subscription\Entity\Subscription::create($opts));
|
DB::persist(Entity\Subscription::create($opts));
|
||||||
$activity = Activity::create([
|
$activity = Activity::create([
|
||||||
'actor_id' => $subscriber_id,
|
'actor_id' => $subscriber_id,
|
||||||
'verb' => 'subscribe',
|
'verb' => 'subscribe',
|
||||||
@ -76,31 +111,40 @@ class Subscription extends Component
|
|||||||
DB::persist($activity);
|
DB::persist($activity);
|
||||||
|
|
||||||
Event::handle('NewNotification', [
|
Event::handle('NewNotification', [
|
||||||
$actor = ($subscriber instanceof Actor ? $subscriber : Actor::getById($subscribed_id)),
|
\is_int($subject) ? $subject : Actor::getById($subscriber_id),
|
||||||
$activity,
|
$activity,
|
||||||
['object' => [$subscribed_id]],
|
['object' => [$activity->getObjectId()]],
|
||||||
_m('{nickname} subscribed to {subject}.', ['{actor}' => $actor->getId(), '{subject}' => $activity->getObjectId()]),
|
_m('{subject} subscribed to {object}.', ['{subject}' => $activity->getActorId(), '{object}' => $activity->getObjectId()]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
return $activity;
|
return $activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the Subscription Entity created beforehand, by the same Actor, and on the same subject
|
* Removes the Subscription Entity created beforehand, by the same Actor, and on the same object
|
||||||
*
|
*
|
||||||
* Informs all interested Actors of this action, handling out the NewNotification event
|
* Informs all interested Actors of this action, handling out the NewNotification event
|
||||||
*
|
*
|
||||||
|
* @param Actor|int|LocalUser $subject The actor undoing the subscription
|
||||||
|
* @param Actor|int|LocalUser $object The target of the subscription
|
||||||
|
*
|
||||||
|
* @throws DuplicateFoundException
|
||||||
|
* @throws NotFoundException
|
||||||
* @throws ServerException
|
* @throws ServerException
|
||||||
|
*
|
||||||
|
* @return null|Activity a new Activity if changes were made
|
||||||
|
*
|
||||||
|
* @see self::refreshSubscriptionCount() to delete cache after this action
|
||||||
*/
|
*/
|
||||||
public static function unsubscribe(int|Actor|LocalUser $subscriber, int|Actor|LocalUser $subject, string $source = 'web'): ?Activity
|
public static function unsubscribe(int|Actor|LocalUser $subject, int|Actor|LocalUser $object, string $source = 'web'): ?Activity
|
||||||
{
|
{
|
||||||
$subscriber_id = \is_int($subscriber) ? $subscriber : $subscriber->getId();
|
$subscriber_id = \is_int($subject) ? $subject : $subject->getId();
|
||||||
$subscribed_id = \is_int($subject) ? $subject : $subject->getId();
|
$subscribed_id = \is_int($object) ? $object : $object->getId();
|
||||||
$opts = [
|
$opts = [
|
||||||
'subscriber_id' => $subscriber_id,
|
'subscriber_id' => $subscriber_id,
|
||||||
'subscribed_id' => $subscribed_id,
|
'subscribed_id' => $subscribed_id,
|
||||||
];
|
];
|
||||||
$subscription = DB::findOneBy(table: \Component\Subscription\Entity\Subscription::class, criteria: $opts, return_null: true);
|
$subscription = DB::findOneBy(table: Entity\Subscription::class, criteria: $opts, return_null: true);
|
||||||
$activity = null;
|
$activity = null;
|
||||||
if (!\is_null($subscription)) {
|
if (!\is_null($subscription)) {
|
||||||
// Remove Subscription
|
// Remove Subscription
|
||||||
@ -115,7 +159,88 @@ class Subscription extends Component
|
|||||||
'source' => $source,
|
'source' => $source,
|
||||||
]);
|
]);
|
||||||
DB::persist($activity);
|
DB::persist($activity);
|
||||||
|
|
||||||
|
Event::handle('NewNotification', [
|
||||||
|
\is_int($subject) ? $subject : Actor::getById($subscriber_id),
|
||||||
|
$activity,
|
||||||
|
['object' => [$previous_follow_activity->getObjectId()]],
|
||||||
|
_m('{subject} unsubscribed from {object}.', ['{subject}' => $activity->getActorId(), '{object}' => $previous_follow_activity->getObjectId()]),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
return $activity;
|
return $activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides ``\App\templates\cards\profile\view.html.twig`` an **additional action** to be performed **on the given
|
||||||
|
* Actor** (which the profile card of is currently being rendered).
|
||||||
|
*
|
||||||
|
* In the case of ``\App\Component\Subscription``, the action added allows a **LocalUser** to **subscribe** or
|
||||||
|
* **unsubscribe** a given **Actor**.
|
||||||
|
*
|
||||||
|
* @param Actor $object The Actor on which the action is to be performed
|
||||||
|
* @param array $actions An array containing all actions added to the
|
||||||
|
* current profile, this event adds an action to it
|
||||||
|
*
|
||||||
|
* @throws DuplicateFoundException
|
||||||
|
* @throws NotFoundException
|
||||||
|
* @throws ServerException
|
||||||
|
*
|
||||||
|
* @return bool EventHook
|
||||||
|
*/
|
||||||
|
public function onAddProfileActions(Request $request, Actor $object, array &$actions): bool
|
||||||
|
{
|
||||||
|
// Action requires a user to be logged in
|
||||||
|
// We know it's a LocalUser, which has the same id as Actor
|
||||||
|
// We don't want the Actor to unfollow itself
|
||||||
|
if ((\is_null($subject = Common::user())) || ($subject->getId() === $object->getId())) {
|
||||||
|
return Event::next;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's retrieve from here this subject came from to redirect it to previous location
|
||||||
|
$from = $request->query->has('from')
|
||||||
|
? $request->query->get('from')
|
||||||
|
: $request->getPathInfo();
|
||||||
|
|
||||||
|
// Who is the subject attempting to subscribe to?
|
||||||
|
$object_id = $object->getId();
|
||||||
|
|
||||||
|
// The id of both the subject and object
|
||||||
|
$opts = [
|
||||||
|
'subscriber_id' => $subject->getId(),
|
||||||
|
'subscribed_id' => $object_id,
|
||||||
|
];
|
||||||
|
|
||||||
|
// If subject is not subbed to object already, then route it to add subscription
|
||||||
|
// Else, route to remove subscription
|
||||||
|
$subscribe_action_url = ($not_subscribed_already = \is_null(DB::findOneBy(table: Entity\Subscription::class, criteria: $opts, return_null: true))) ? Router::url(
|
||||||
|
'actor_subscribe_add',
|
||||||
|
[
|
||||||
|
'object_id' => $object_id,
|
||||||
|
'from' => $from . '#profile-' . $object_id,
|
||||||
|
],
|
||||||
|
Router::ABSOLUTE_PATH,
|
||||||
|
) : Router::url(
|
||||||
|
'actor_subscribe_remove',
|
||||||
|
[
|
||||||
|
'object_id' => $object_id,
|
||||||
|
'from' => $from . '#profile-' . $object_id,
|
||||||
|
],
|
||||||
|
Router::ABSOLUTE_PATH,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Finally, create an array with proper keys set accordingly
|
||||||
|
// to provide Profile Card template, the info it needs in order to render it properly
|
||||||
|
$action_extra_class = $not_subscribed_already ? 'add-actor-button-container' : 'remove-actor-button-container';
|
||||||
|
$title = $not_subscribed_already ? 'Subscribe ' . $object->getNickname() : 'Unsubscribe ' . $object->getNickname();
|
||||||
|
$subscribe_action = [
|
||||||
|
'url' => $subscribe_action_url,
|
||||||
|
'title' => _m($title),
|
||||||
|
'classes' => 'button-container note-actions-unset ' . $action_extra_class,
|
||||||
|
'id' => 'add-actor-button-container-' . $object_id,
|
||||||
|
];
|
||||||
|
|
||||||
|
$actions[] = $subscribe_action;
|
||||||
|
|
||||||
|
return Event::next;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{% block profile_view %}
|
||||||
|
{% include 'cards/profile/view.html.twig' with { actor: object } %}
|
||||||
|
{% endblock profile_view %}
|
||||||
|
{{ form(form) }}
|
||||||
|
{% endblock body %}
|
@ -0,0 +1,8 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
{% block profile_view %}
|
||||||
|
{% include 'cards/profile/view.html.twig' with { actor: object } %}
|
||||||
|
{% endblock profile_view %}
|
||||||
|
{{ form(form) }}
|
||||||
|
{% endblock body %}
|
@ -160,6 +160,9 @@ embed header {
|
|||||||
display: flex;
|
display: flex;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
.note-actions ul > li {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
.note-actions-extra-details {
|
.note-actions-extra-details {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -201,12 +204,14 @@ embed header {
|
|||||||
mask-repeat: no-repeat !important;
|
mask-repeat: no-repeat !important;
|
||||||
mask-size: cover !important;
|
mask-size: cover !important;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: var(--s);
|
|
||||||
width: var(--unit);
|
width: var(--unit);
|
||||||
height: var(--unit);
|
height: var(--unit);
|
||||||
background-color: var(--foreground);
|
background-color: var(--foreground);
|
||||||
opacity: 0.33;
|
opacity: 0.33;
|
||||||
}
|
}
|
||||||
|
.button-container:not(:first-of-type) {
|
||||||
|
margin-left: var(--s);
|
||||||
|
}
|
||||||
.button-container:focus,
|
.button-container:focus,
|
||||||
.button-container:hover {
|
.button-container:hover {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
|
@ -1,38 +1,89 @@
|
|||||||
.profile {
|
.profile {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
font-family: 'Open Sans',sans-serif;
|
font-family: 'Open Sans',sans-serif;
|
||||||
margin-bottom: var(--s);
|
margin-bottom: var(--s);
|
||||||
|
border: 2px solid var(--border);
|
||||||
border-radius: var(--s);
|
border-radius: var(--s);
|
||||||
padding: var(--s);
|
padding: var(--unit);
|
||||||
background: var(--gradient) !important;
|
background: var(--gradient) !important;
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
}
|
||||||
.profile *[class*="profile-info-"] {
|
.profile header {
|
||||||
flex: 1;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.profile-info {
|
.profile-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
.profile-info-nickname {
|
.profile-info-url {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.profile-info-url-nickname {
|
||||||
font-size: var(--m);
|
font-size: var(--m);
|
||||||
}
|
}
|
||||||
.profile-info-tags {
|
.profile-info-url-remote {
|
||||||
margin: unset;
|
opacity: 0.66;
|
||||||
}
|
}
|
||||||
.profile-info-stats strong {
|
.profile-info-url > * {
|
||||||
margin-right: 5px;
|
display: block;
|
||||||
}
|
}
|
||||||
.profile-info-stats {
|
.profile-stats {
|
||||||
margin-top: var(--s);
|
align-self: center;
|
||||||
|
margin-left: auto;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
.profile-info-bio,
|
.profile-stats-subscriptions,
|
||||||
.profile-info-nickname {
|
.profile-stats-subscribers {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.profile-stats-subscriptions strong,
|
||||||
|
.profile-stats-subscribers strong {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.profile-info-url,
|
||||||
|
.profile-bio {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
.profile-bio {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.profile-tags {
|
||||||
|
margin: unset;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.button-container {
|
||||||
|
border: none !important;
|
||||||
|
mask-repeat: no-repeat !important;
|
||||||
|
mask-size: cover !important;
|
||||||
|
display: inline-block;
|
||||||
|
width: var(--unit);
|
||||||
|
height: var(--unit);
|
||||||
|
background-color: var(--foreground);
|
||||||
|
opacity: 0.33;
|
||||||
|
}
|
||||||
|
.button-container:not(:first-of-type) {
|
||||||
|
margin-left: var(--s);
|
||||||
|
}
|
||||||
|
.button-container:focus,
|
||||||
|
.button-container:hover {
|
||||||
|
border: none !important;
|
||||||
|
mask-repeat: no-repeat !important;
|
||||||
|
mask-size: cover !important;
|
||||||
|
opacity: 1;
|
||||||
|
background-color: var(--accent);
|
||||||
|
}
|
||||||
|
.add-actor-button-container {
|
||||||
|
-o-mask-image: url("../../icons/add-actor.svg");
|
||||||
|
-moz-mask-image: url("../../icons/add-actor.svg");
|
||||||
|
-webkit-mask-image: url("../../icons/add-actor.svg");
|
||||||
|
mask-image: url("../../icons/add-actor.svg");
|
||||||
|
}
|
||||||
|
.remove-actor-button-container {
|
||||||
|
-o-mask-image: url("../../icons/remove-actor.svg");
|
||||||
|
-moz-mask-image: url("../../icons/remove-actor.svg");
|
||||||
|
-webkit-mask-image: url("../../icons/remove-actor.svg");
|
||||||
|
mask-image: url("../../icons/remove-actor.svg");
|
||||||
|
}
|
||||||
.avatar {
|
.avatar {
|
||||||
max-width: 4rem;
|
max-width: 4rem;
|
||||||
max-height: 4rem;
|
max-height: 4rem;
|
||||||
|
5
public/assets/default_theme/icons/add-actor.svg
Normal file
5
public/assets/default_theme/icons/add-actor.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<!-- https://github.com/primer/octicons -->
|
||||||
|
<!-- MIT License -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<path fill-rule="evenodd" d="M19.25 1a.75.75 0 01.75.75V4h2.25a.75.75 0 010 1.5H20v2.25a.75.75 0 01-1.5 0V5.5h-2.25a.75.75 0 010-1.5h2.25V1.75a.75.75 0 01.75-.75zM9 6a3.5 3.5 0 100 7 3.5 3.5 0 000-7zM4 9.5a5 5 0 117.916 4.062 7.973 7.973 0 015.018 7.166.75.75 0 11-1.499.044 6.469 6.469 0 00-12.932 0 .75.75 0 01-1.499-.044 7.973 7.973 0 015.059-7.181A4.993 4.993 0 014 9.5z"></path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 543 B |
14
public/assets/default_theme/icons/remove-actor.svg
Normal file
14
public/assets/default_theme/icons/remove-actor.svg
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!-- https://github.com/primer/octicons -->
|
||||||
|
<!-- MIT License -->
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="m 22.25,4 c 1,0 1,1.5 0,1.5 -3.829871,-0.01723 0.571664,0 -6,0 -1,0 -1,-1.5 0,-1.5 3.712329,4.776e-4 -0.745902,0 6,0 z M 9,6 c -4.6666646,0 -4.6666646,7 0,7 4.666665,0 4.666665,-7 0,-7 z M 4,9.5 C 4.000267,5.7603244 7.9555419,3.344606 11.282679,5.0518864 14.609816,6.7591667 14.953845,11.381021 11.916,13.562 c 2.950793,1.175822 4.922134,3.991013 5.018,7.166 0.05218,1.020644 -1.491226,1.065947 -1.499,0.044 -0.106459,-3.49441 -2.969971,-6.272008 -6.466,-6.272008 -3.4960292,0 -6.3595411,2.777598 -6.466,6.272008 -0.049962,0.977219 -1.50644332,0.934467 -1.499,-0.044 0.096826,-3.19055 2.0872357,-6.015839 5.059,-7.181 C 4.7659785,12.607017 3.9986894,11.101821 4,9.5 Z"
|
||||||
|
id="path924" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 958 B |
@ -259,7 +259,7 @@ class Actor extends Entity
|
|||||||
'fullname' => "actor-fullname-id-{$actor_id}",
|
'fullname' => "actor-fullname-id-{$actor_id}",
|
||||||
'self-tags' => "actor-self-tags-{$actor_id}",
|
'self-tags' => "actor-self-tags-{$actor_id}",
|
||||||
'circles' => "actor-circles-{$actor_id}",
|
'circles' => "actor-circles-{$actor_id}",
|
||||||
'subscriber' => "subscriber-{$actor_id}",
|
'subscribers' => "subscribers-{$actor_id}",
|
||||||
'subscribed' => "subscribed-{$actor_id}",
|
'subscribed' => "subscribed-{$actor_id}",
|
||||||
'relative-nickname' => "actor-{$actor_id}-relative-nickname-{$other}", // $other is $nickname
|
'relative-nickname' => "actor-{$actor_id}-relative-nickname-{$other}", // $other is $nickname
|
||||||
'can-admin' => "actor-{$actor_id}-can-admin-{$other}", // $other is an actor id
|
'can-admin' => "actor-{$actor_id}-can-admin-{$other}", // $other is an actor id
|
||||||
@ -340,7 +340,7 @@ class Actor extends Entity
|
|||||||
|
|
||||||
public function getSubscribersCount(): int
|
public function getSubscribersCount(): int
|
||||||
{
|
{
|
||||||
return $this->getSubCount(which: 'subscriber', column: 'subscribed_id');
|
return $this->getSubCount(which: 'subscribers', column: 'subscribed_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSubscribedCount(): int
|
public function getSubscribedCount(): int
|
||||||
|
@ -65,6 +65,7 @@ class Extension extends AbstractExtension
|
|||||||
new TwigFunction('config', [Runtime::class, 'getConfig']),
|
new TwigFunction('config', [Runtime::class, 'getConfig']),
|
||||||
new TwigFunction('dd', 'dd'),
|
new TwigFunction('dd', 'dd'),
|
||||||
new TwigFunction('die', 'die'),
|
new TwigFunction('die', 'die'),
|
||||||
|
new TwigFunction('get_profile_actions', [Runtime::class, 'getProfileActions']),
|
||||||
new TwigFunction('get_extra_note_actions', [Runtime::class, 'getExtraNoteActions']),
|
new TwigFunction('get_extra_note_actions', [Runtime::class, 'getExtraNoteActions']),
|
||||||
new TwigFunction('get_feeds', [Runtime::class, 'getFeeds']),
|
new TwigFunction('get_feeds', [Runtime::class, 'getFeeds']),
|
||||||
new TwigFunction('get_note_actions', [Runtime::class, 'getNoteActions']),
|
new TwigFunction('get_note_actions', [Runtime::class, 'getNoteActions']),
|
||||||
|
@ -67,6 +67,13 @@ class Runtime implements RuntimeExtensionInterface, EventSubscriberInterface
|
|||||||
return F\some($routes, F\partial_left([Formatting::class, 'startsWith'], $current_route)) ? $class : '';
|
return F\some($routes, F\partial_left([Formatting::class, 'startsWith'], $current_route)) ? $class : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getProfileActions(Actor $actor)
|
||||||
|
{
|
||||||
|
$actions = [];
|
||||||
|
Event::handle('AddProfileActions', [$this->request, $actor, &$actions]);
|
||||||
|
return $actions;
|
||||||
|
}
|
||||||
|
|
||||||
public function getNoteActions(Note $note)
|
public function getNoteActions(Note $note)
|
||||||
{
|
{
|
||||||
$actions = [];
|
$actions = [];
|
||||||
|
@ -10,34 +10,43 @@
|
|||||||
|
|
||||||
{% block profile_view %}
|
{% block profile_view %}
|
||||||
<section id='profile-{{ actor.id }}' class='profile' title="{{ actor_nickname }}'s {{ 'profile information.' | trans }}">
|
<section id='profile-{{ actor.id }}' class='profile' title="{{ actor_nickname }}'s {{ 'profile information.' | trans }}">
|
||||||
<a href="{{ actor_uri }}">
|
<header>
|
||||||
<div class="profile-info">
|
<div class="profile-info">
|
||||||
<img src="{{ actor_avatar }}" class="avatar" alt="{% trans %} %actor_nickname%'s avatar. {% endtrans %}" width="{{actor_avatar_dimensions['width']}}" height="{{actor_avatar_dimensions['height']}}">
|
<img src="{{ actor_avatar }}" class="profile-avatar avatar" alt="{% trans %} %actor_nickname%'s avatar. {% endtrans %}" width="{{actor_avatar_dimensions['width']}}" height="{{actor_avatar_dimensions['height']}}">
|
||||||
<strong class="profile-info-nickname" title="{{ actor_nickname }}{{ '\'s nickname.' | trans }}">{{ actor_nickname }}</strong>
|
<div>
|
||||||
|
<a class="profile-info-url" href="{{ actor_uri }}">
|
||||||
|
<strong class="profile-info-url-nickname" title="{{ actor_nickname }}{{ '\'s nickname.' | trans }}">{{ actor_nickname }}</strong>
|
||||||
|
{% if not actor_is_local %}
|
||||||
|
<span class="profile-info-url-remote">{{ mention }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
<ul class="profile-info-actions">
|
||||||
|
{% for current_action in get_profile_actions(actor) %}
|
||||||
|
<li>
|
||||||
|
<a title="{{ current_action["title"] | trans }}"
|
||||||
|
class="{{ current_action["classes"] }}"
|
||||||
|
href="{{ current_action["url"] }}"></a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
<div class="profile-stats">
|
||||||
|
<span class="profile-stats-subscriptions"><strong><a href="{{ actor.getSubscriptionsUrl() }}">{{ 'Subscribed' | trans }}</a></strong>{{ actor.getSubscribedCount() }}</span>
|
||||||
{% if not actor_is_local %}
|
<span class="profile-stats-subscribers"><strong><a href="{{ actor.getSubscribersUrl() }}">{{ 'Subscribers' | trans }}</a></strong>{{ actor.getSubscribersCount() }}</span>
|
||||||
<section class="profile-info-remote">
|
</div>
|
||||||
<span>{{ mention }}</span>
|
</header>
|
||||||
|
<div>
|
||||||
|
<section class="profile-bio">
|
||||||
|
<span>{{ actor.getBio() }}</span>
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<section class="profile-info-bio">
|
|
||||||
<span>{{ actor.getBio() }}</span>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="profile-info-stats">
|
|
||||||
<div><strong><a href="{{ actor.getSubscriptionsUrl() }}">{{ 'Subscribed' | trans }}</a></strong>{{ actor.getSubscribedCount() }}</div>
|
|
||||||
<div><strong><a href="{{ actor.getSubscribersUrl() }}">{{ 'Subscribers' | trans }}</a></strong>{{ actor.getSubscribersCount() }}</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<nav class="profile-info-tags">
|
|
||||||
{% for tag in actor_tags %}
|
|
||||||
{% include 'cards/tag/actor_tag.html.twig' with { 'tag': tag, 'actor': actor } %}
|
|
||||||
{% endfor %}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
|
<nav class="profile-tags">
|
||||||
|
{% for tag in actor_tags %}
|
||||||
|
{% include 'cards/tag/actor_tag.html.twig' with { 'tag': tag, 'actor': actor } %}
|
||||||
|
{% endfor %}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
{% for block in handle_event('AppendCardProfile', { 'actor': actor }) %}
|
{% for block in handle_event('AppendCardProfile', { 'actor': actor }) %}
|
||||||
{{ block | raw }}
|
{{ block | raw }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
Loading…
Reference in New Issue
Block a user