[COMPONENT][Circle] Move circles to a component, various bug fixes
Mention links are now correct
This commit is contained in:
parent
627d92b290
commit
cd6ce3542e
223
components/Circle/Circle.php
Normal file
223
components/Circle/Circle.php
Normal file
|
@ -0,0 +1,223 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
// {{{ License
|
||||
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// }}}
|
||||
|
||||
namespace Component\Circle;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\Event;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router\Router;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\Feed;
|
||||
use App\Entity\LocalUser;
|
||||
use App\Util\Common;
|
||||
use App\Util\Nickname;
|
||||
use Component\Circle\Controller as CircleController;
|
||||
use Component\Circle\Entity\ActorCircle;
|
||||
use Component\Circle\Entity\ActorCircleSubscription;
|
||||
use Component\Circle\Entity\ActorTag;
|
||||
use Component\Collection\Util\MetaCollectionTrait;
|
||||
use Component\Tag\Tag;
|
||||
use Functional as F;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Component responsible for handling and representing ActorCircles and ActorTags
|
||||
*
|
||||
* @author Hugo Sales <hugo@hsal.es>
|
||||
* @author Phablulo <phablulo@gmail.com>
|
||||
* @author Diogo Peralta Cordeiro <@diogo.site>
|
||||
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class Circle extends Component
|
||||
{
|
||||
use MetaCollectionTrait;
|
||||
public const TAG_CIRCLE_REGEX = '/' . Nickname::BEFORE_MENTIONS . '@#([\pL\pN_\-\.]{1,64})/';
|
||||
protected string $slug = 'circle';
|
||||
protected string $plural_slug = 'circles';
|
||||
|
||||
public function onAddRoute(RouteLoader $r): bool
|
||||
{
|
||||
$r->connect('actor_circle_view_by_circle_id', '/circle/{circle_id<\d+>}', [CircleController\Circle::class, 'circleById']);
|
||||
// View circle members by (tagger id or nickname) and tag
|
||||
$r->connect('actor_circle_view_by_circle_tagger_tag', '/circle/actor/{tagger_id<\d+>/{tag<' . Tag::TAG_SLUG_REGEX . '>}}', [CircleController\Circle::class, 'circleByTaggerIdAndTag']);
|
||||
$r->connect('actor_circle_view_by_circle_tagger_tag', '/circle/@{nickname<' . Nickname::DISPLAY_FMT . '>}/{tag<' . Tag::TAG_SLUG_REGEX . '>}', [CircleController\Circle::class, 'circleByTaggerNicknameAndTag']);
|
||||
|
||||
// View all circles by actor id or nickname
|
||||
$r->connect(
|
||||
id: 'actor_circles_view_by_actor_id',
|
||||
uri_path: '/actor/{tag<' . Tag::TAG_SLUG_REGEX . '>}/circles',
|
||||
target: [CircleController\Circles::class, 'collectionsViewByActorId'],
|
||||
);
|
||||
$r->connect(
|
||||
id: 'actor_circles_view_by_nickname',
|
||||
uri_path: '/@{nickname<' . Nickname::DISPLAY_FMT . '>}/circles',
|
||||
target: [CircleController\Circles::class, 'collectionsViewByActorNickname'],
|
||||
);
|
||||
|
||||
$r->connect('actor_circle_view_feed_by_circle_id', '/circle/{circle_id<\d+>}/feed', [CircleController\Circles::class, 'feedByCircleId']);
|
||||
// View circle feed by (tagger id or nickname) and tag
|
||||
$r->connect('actor_circle_view_feed_by_circle_tagger_tag', '/circle/actor/{tagger_id<\d+>/{tag<' . Tag::TAG_SLUG_REGEX . '>}}/feed', [CircleController\Circles::class, 'feedByTaggerIdAndTag']);
|
||||
$r->connect('actor_circle_view_feed_by_circle_tagger_tag', '/circle/@{nickname<' . Nickname::DISPLAY_FMT . '>}/{tag<' . Tag::TAG_SLUG_REGEX . '>}/feed', [CircleController\Circles::class, 'feedByTaggerNicknameAndTag']);
|
||||
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public static function cacheKeys(string $tag_single_or_multi): array
|
||||
{
|
||||
return [
|
||||
'actor_single' => "actor-tag-feed-{$tag_single_or_multi}",
|
||||
'actor_multi' => "actor-tags-feed-{$tag_single_or_multi}",
|
||||
];
|
||||
}
|
||||
|
||||
public function onPopulateSettingsTabs(Request $request, string $section, array &$tabs): bool
|
||||
{
|
||||
if ($section === 'profile' && $request->get('_route') === 'settings') {
|
||||
$tabs[] = [
|
||||
'title' => 'Self tags',
|
||||
'desc' => 'Add or remove tags on yourself',
|
||||
'id' => 'settings-self-tags',
|
||||
'controller' => CircleController\SelfTagsSettings::settingsSelfTags($request, Common::actor(), 'settings-self-tags-details'),
|
||||
];
|
||||
}
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onPostingFillTargetChoices(Request $request, Actor $actor, array &$targets): bool
|
||||
{
|
||||
$circles = $actor->getCircles();
|
||||
foreach ($circles as $circle) {
|
||||
$tag = $circle->getTag();
|
||||
$targets["#{$tag}"] = $tag;
|
||||
}
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
// Meta Collection -------------------------------------------------------------------
|
||||
|
||||
private function getActorIdFromVars(array $vars): int
|
||||
{
|
||||
$id = $vars['request']->get('id', null);
|
||||
if ($id) {
|
||||
return (int) $id;
|
||||
}
|
||||
$nick = $vars['request']->get('nickname');
|
||||
$user = LocalUser::getByNickname($nick);
|
||||
return $user->getId();
|
||||
}
|
||||
|
||||
public static function createCircle(Actor|int $tagger_id, string $tag): int
|
||||
{
|
||||
$tagger_id = \is_int($tagger_id) ? $tagger_id : $tagger_id->getId();
|
||||
$circle = ActorCircle::create([
|
||||
'tagger' => $tagger_id,
|
||||
'tag' => $tag,
|
||||
'description' => null, // TODO
|
||||
'private' => false, // TODO
|
||||
]);
|
||||
DB::persist($circle);
|
||||
|
||||
Cache::delete(Actor::cacheKeys($tagger_id)['circles']);
|
||||
|
||||
return $circle->getId();
|
||||
}
|
||||
|
||||
protected function createCollection(Actor $owner, array $vars, string $name)
|
||||
{
|
||||
$this->createCircle($owner, $name);
|
||||
DB::persist(ActorTag::create([
|
||||
'tagger' => $owner->getId(),
|
||||
'tagged' => self::getActorIdFromVars($vars),
|
||||
'tag' => $name,
|
||||
]));
|
||||
}
|
||||
|
||||
protected function removeItems(Actor $owner, array $vars, $items, array $collections)
|
||||
{
|
||||
$tagger_id = $owner->getId();
|
||||
$tagged_id = $this->getActorIdFromVars($vars);
|
||||
$circles_to_remove_tagged_from = DB::findBy(ActorCircle::class, ['id' => $items]);
|
||||
foreach ($circles_to_remove_tagged_from as $circle) {
|
||||
DB::removeBy(ActorCircleSubscription::class, ['actor_id' => $tagged_id, 'circle_id' => $circle->getId()]);
|
||||
}
|
||||
$tags = F\map($circles_to_remove_tagged_from, fn ($x) => $x->getTag());
|
||||
foreach ($tags as $tag) {
|
||||
DB::removeBy(ActorTag::class, ['tagger' => $tagger_id, 'tagged' => $tagged_id, 'tag' => $tag]);
|
||||
}
|
||||
Cache::delete(Actor::cacheKeys($tagger_id)['circles']);
|
||||
}
|
||||
|
||||
protected function addItems(Actor $owner, array $vars, $items, array $collections)
|
||||
{
|
||||
$tagger_id = $owner->getId();
|
||||
$tagged_id = $this->getActorIdFromVars($vars);
|
||||
$circles_to_add_tagged_to = DB::findBy(ActorCircle::class, ['id' => $items]);
|
||||
foreach ($circles_to_add_tagged_to as $circle) {
|
||||
DB::persist(ActorCircleSubscription::create(['actor_id' => $tagged_id, 'circle_id' => $circle->getId()]));
|
||||
}
|
||||
$tags = F\map($circles_to_add_tagged_to, fn ($x) => $x->getTag());
|
||||
foreach ($tags as $tag) {
|
||||
DB::persist(ActorTag::create(['tagger' => $tagger_id, 'tagged' => $tagged_id, 'tag' => $tag]));
|
||||
}
|
||||
Cache::delete(Actor::cacheKeys($tagger_id)['circles']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see MetaCollectionPlugin->shouldAddToRightPanel
|
||||
*/
|
||||
protected function shouldAddToRightPanel(Actor $user, $vars, Request $request): bool
|
||||
{
|
||||
return \in_array($vars['path'], ['actor_view_nickname', 'actor_view_id', 'group_actor_view_nickname', 'group_actor_view_id']);
|
||||
}
|
||||
|
||||
protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array
|
||||
{
|
||||
$tagged_id = !\is_null($vars) ? $this->getActorIdFromVars($vars) : null;
|
||||
$circles = \is_null($tagged_id) ? $owner->getCircles() : F\select($owner->getCircles(), function ($x) use ($tagged_id) {
|
||||
foreach ($x->getActorTags() as $at) {
|
||||
if ($at->getTagged() === $tagged_id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return $ids_only ? array_map(fn ($x) => $x->getId(), $circles) : $circles;
|
||||
}
|
||||
|
||||
public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering)
|
||||
{
|
||||
DB::persist(Feed::create([
|
||||
'actor_id' => $actor_id,
|
||||
'url' => Router::url($route = 'actor_circles_view_by_nickname', ['nickname' => $user->getNickname()]),
|
||||
'route' => $route,
|
||||
'title' => _m('Circles'),
|
||||
'ordering' => $ordering++,
|
||||
]));
|
||||
return Event::next;
|
||||
}
|
||||
}
|
61
components/Circle/Controller/Circle.php
Normal file
61
components/Circle/Controller/Circle.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
// {{{ License
|
||||
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// }}}
|
||||
|
||||
namespace Component\Circle\Controller;
|
||||
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Entity\LocalUser;
|
||||
use App\Util\Exception\ClientException;
|
||||
use Component\Circle\Entity\ActorCircle;
|
||||
use Component\Collection\Util\Controller\CircleController;
|
||||
|
||||
class Circle extends CircleController
|
||||
{
|
||||
public function circleById(int|ActorCircle $circle_id): array
|
||||
{
|
||||
$circle = \is_int($circle_id) ? ActorCircle::getByPK(['id' => $circle_id]) : $circle_id;
|
||||
unset($circle_id);
|
||||
if (\is_null($circle)) {
|
||||
throw new ClientException(_m('No such circle.'), 404);
|
||||
} else {
|
||||
return [
|
||||
'_template' => 'collection/actors.html.twig',
|
||||
'title' => _m('Circle'),
|
||||
'empty_message' => _m('No members.'),
|
||||
'sort_form_fields' => [],
|
||||
'page' => $this->int('page') ?? 1,
|
||||
'actors' => $circle->getTaggedActors(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function circleByTaggerIdAndTag(int $tagger_id, string $tag): array
|
||||
{
|
||||
return $this->circleById(ActorCircle::getByPK(['tagger' => $tagger_id, 'tag' => $tag]));
|
||||
}
|
||||
|
||||
public function circleByTaggerNicknameAndTag(string $tagger_nickname, string $tag): array
|
||||
{
|
||||
return $this->circleById(ActorCircle::getByPK(['tagger' => LocalUser::getByNickname($tagger_nickname)->getId(), 'tag' => $tag]));
|
||||
}
|
||||
}
|
107
components/Circle/Controller/Circles.php
Normal file
107
components/Circle/Controller/Circles.php
Normal file
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
// {{{ License
|
||||
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// }}}
|
||||
|
||||
namespace Component\Circle\Controller;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\Router\Router;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\LocalUser;
|
||||
use Component\Circle\Entity\ActorCircle;
|
||||
use Component\Collection\Util\Controller\MetaCollectionController;
|
||||
|
||||
class Circles extends MetaCollectionController
|
||||
{
|
||||
protected string $slug = 'circle';
|
||||
protected string $plural_slug = 'circles';
|
||||
protected string $page_title = 'Actor circles';
|
||||
|
||||
public function createCollection(int $owner_id, string $name)
|
||||
{
|
||||
return \Component\Circle\Circle::createCircle($owner_id, $name);
|
||||
}
|
||||
public function getCollectionUrl(int $owner_id, ?string $owner_nickname, int $collection_id): string
|
||||
{
|
||||
return Router::url(
|
||||
'actor_circle_view_by_circle_id',
|
||||
['circle_id' => $collection_id],
|
||||
);
|
||||
}
|
||||
|
||||
public function getCollectionItems(int $owner_id, $collection_id): array
|
||||
{
|
||||
$notes = []; // TODO: Use Feed::query
|
||||
return [
|
||||
'_template' => 'collection/notes.html.twig',
|
||||
'notes' => $notes,
|
||||
];
|
||||
}
|
||||
|
||||
public function feedByCircleId(int $circle_id)
|
||||
{
|
||||
// Owner id isn't used
|
||||
return $this->getCollectionItems(0, $circle_id);
|
||||
}
|
||||
|
||||
public function feedByTaggerIdAndTag(int $tagger_id, string $tag)
|
||||
{
|
||||
// Owner id isn't used
|
||||
$circle_id = ActorCircle::getByPK(['tagger' => $tagger_id, 'tag' => $tag])->getId();
|
||||
return $this->getCollectionItems($tagger_id, $circle_id);
|
||||
}
|
||||
|
||||
public function feedByTaggerNicknameAndTag(string $tagger_nickname, string $tag)
|
||||
{
|
||||
$tagger_id = LocalUser::getByNickname($tagger_nickname)->getId();
|
||||
$circle_id = ActorCircle::getByPK(['tagger' => $tagger_id, 'tag' => $tag])->getId();
|
||||
return $this->getCollectionItems($tagger_id, $circle_id);
|
||||
}
|
||||
|
||||
public function getCollectionsByActorId(int $owner_id): array
|
||||
{
|
||||
return DB::findBy(ActorCircle::class, ['tagger' => $owner_id], order_by: ['id' => 'desc']);
|
||||
}
|
||||
public function getCollectionBy(int $owner_id, int $collection_id): ActorCircle
|
||||
{
|
||||
return DB::findOneBy(ActorCircle::class, ['id' => $collection_id, 'actor_id' => $owner_id]);
|
||||
}
|
||||
|
||||
public function setCollectionName(int $actor_id, string $actor_nickname, ActorCircle $collection, string $name)
|
||||
{
|
||||
foreach ($collection->getActorTags(db_reference: true) as $at) {
|
||||
$at->setTag($name);
|
||||
}
|
||||
$collection->setTag($name);
|
||||
Cache::delete(Actor::cacheKeys($actor_id)['circles']);
|
||||
}
|
||||
|
||||
public function removeCollection(int $actor_id, string $actor_nickname, ActorCircle $collection)
|
||||
{
|
||||
foreach ($collection->getActorTags(db_reference: true) as $at) {
|
||||
DB::remove($at);
|
||||
}
|
||||
DB::remove($collection);
|
||||
Cache::delete(Actor::cacheKeys($actor_id)['circles']);
|
||||
}
|
||||
}
|
117
components/Circle/Controller/SelfTagsSettings.php
Normal file
117
components/Circle/Controller/SelfTagsSettings.php
Normal file
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Component\Circle\Controller;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\Controller;
|
||||
use App\Core\DB\DB;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Entity as E;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
use App\Util\Exception\RedirectException;
|
||||
use Component\Circle\Entity\ActorCircle;
|
||||
use Component\Circle\Entity\ActorTag;
|
||||
use Component\Circle\Form\SelfTagsForm;
|
||||
use Component\Tag\Tag as CompTag;
|
||||
use Symfony\Component\Form\SubmitButton;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class SelfTagsSettings extends Controller
|
||||
{
|
||||
/**
|
||||
* Generic settings page for an Actor's self tags
|
||||
*/
|
||||
public static function settingsSelfTags(Request $request, E\Actor $target, string $details_id)
|
||||
{
|
||||
$actor = Common::actor();
|
||||
if (!$actor->canAdmin($target)) {
|
||||
throw new ClientException(_m('You don\'t have enough permissions to edit {nickname}\'s settings', ['{nickname}' => $target->getNickname()]));
|
||||
}
|
||||
|
||||
$actor_self_tags = $target->getSelfTags();
|
||||
[$add_form, $existing_form] = SelfTagsForm::handleTags(
|
||||
$request,
|
||||
$actor_self_tags,
|
||||
handle_new: /**
|
||||
* Handle adding tags
|
||||
*/
|
||||
function ($form) use ($request, $target, $details_id) {
|
||||
$data = $form->getData();
|
||||
$tags = $data['new-tags'];
|
||||
foreach ($tags as $tag) {
|
||||
$tag = CompTag::sanitize($tag);
|
||||
|
||||
[$actor_tag, $actor_tag_existed] = ActorTag::createOrUpdate([
|
||||
'tagger' => $target->getId(), // self tag means tagger = tagger in ActorTag
|
||||
'tagged' => $target->getId(),
|
||||
'tag' => $tag,
|
||||
]);
|
||||
if (!$actor_tag_existed) {
|
||||
DB::persist($actor_tag);
|
||||
// Try to find the self-tag circle
|
||||
$actor_circle = DB::findOneBy(
|
||||
ActorCircle::class,
|
||||
[
|
||||
'tagger' => null, // Self-tag circle
|
||||
'tag' => $tag,
|
||||
],
|
||||
return_null: true,
|
||||
);
|
||||
// It is the first time someone uses this self-tag!
|
||||
if (\is_null($actor_circle)) {
|
||||
DB::persist(ActorCircle::create([
|
||||
'tagger' => null, // Self-tag circle
|
||||
'tag' => $tag,
|
||||
'private' => false, // by definition
|
||||
'description' => null, // The controller can show this in every language as appropriate
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
DB::flush();
|
||||
Cache::delete(E\Actor::cacheKeys($target->getId())['self-tags']);
|
||||
throw new RedirectException($request->get('_route'), ['nickname' => $target->getNickname(), 'open' => $details_id, '_fragment' => $details_id]);
|
||||
},
|
||||
handle_existing: /**
|
||||
* Handle changes to the existing tags
|
||||
*/
|
||||
function ($form, array $form_definition) use ($request, $target, $details_id) {
|
||||
$changed = false;
|
||||
foreach (array_chunk($form_definition, 2) as $entry) {
|
||||
$tag = CompTag::sanitize($entry[0][2]['data']);
|
||||
|
||||
/** @var SubmitButton $remove */
|
||||
$remove = $form->get($entry[1][0]);
|
||||
if ($remove->isClicked()) {
|
||||
$changed = true;
|
||||
DB::removeBy(
|
||||
'actor_tag',
|
||||
[
|
||||
'tagger' => $target->getId(),
|
||||
'tagged' => $target->getId(),
|
||||
'tag' => $tag,
|
||||
],
|
||||
);
|
||||
// We intentionally leave the self-tag actor circle, even if it is now empty
|
||||
}
|
||||
}
|
||||
if ($changed) {
|
||||
DB::flush();
|
||||
Cache::delete(E\Actor::cacheKeys($target->getId())['self-tags']);
|
||||
throw new RedirectException($request->get('_route'), ['nickname' => $target->getNickname(), 'open' => $details_id, '_fragment' => $details_id]);
|
||||
}
|
||||
},
|
||||
remove_label: _m('Remove self tag'),
|
||||
add_label: _m('Add self tag'),
|
||||
);
|
||||
|
||||
return [
|
||||
'_template' => 'self_tags_settings.fragment.html.twig',
|
||||
'add_self_tags_form' => $add_form->createView(),
|
||||
'existing_self_tags_form' => $existing_form?->createView(),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ declare(strict_types = 1);
|
|||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
// }}}
|
||||
|
||||
namespace App\Entity;
|
||||
namespace Component\Circle\Entity;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
|
@ -49,14 +49,12 @@ class ActorCircle extends Entity
|
|||
// {{{ Autocode
|
||||
// @codeCoverageIgnoreStart
|
||||
private int $id;
|
||||
private ?int $tagger = null;
|
||||
private int $tagged;
|
||||
private ?int $tagger = null; // If null, is the special global self-tag circle
|
||||
private string $tag;
|
||||
private bool $use_canonical;
|
||||
private ?string $description = null;
|
||||
private ?bool $private = false;
|
||||
private \DateTimeInterface $created;
|
||||
private \DateTimeInterface $modified;
|
||||
private ?bool $private = false;
|
||||
private DateTimeInterface $created;
|
||||
private DateTimeInterface $modified;
|
||||
|
||||
public function setId(int $id): self
|
||||
{
|
||||
|
@ -80,20 +78,9 @@ class ActorCircle extends Entity
|
|||
return $this->tagger;
|
||||
}
|
||||
|
||||
public function setTagged(int $tagged): self
|
||||
{
|
||||
$this->tagged = $tagged;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTagged(): int
|
||||
{
|
||||
return $this->tagged;
|
||||
}
|
||||
|
||||
public function setTag(string $tag): self
|
||||
{
|
||||
$this->tag = \mb_substr($tag, 0, 64);
|
||||
$this->tag = mb_substr($tag, 0, 64);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -102,17 +89,6 @@ class ActorCircle extends Entity
|
|||
return $this->tag;
|
||||
}
|
||||
|
||||
public function setUseCanonical(bool $use_canonical): self
|
||||
{
|
||||
$this->use_canonical = $use_canonical;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUseCanonical(): bool
|
||||
{
|
||||
return $this->use_canonical;
|
||||
}
|
||||
|
||||
public function setDescription(?string $description): self
|
||||
{
|
||||
$this->description = $description;
|
||||
|
@ -135,24 +111,24 @@ class ActorCircle extends Entity
|
|||
return $this->private;
|
||||
}
|
||||
|
||||
public function setCreated(\DateTimeInterface $created): self
|
||||
public function setCreated(DateTimeInterface $created): self
|
||||
{
|
||||
$this->created = $created;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreated(): \DateTimeInterface
|
||||
public function getCreated(): DateTimeInterface
|
||||
{
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
public function setModified(\DateTimeInterface $modified): self
|
||||
public function setModified(DateTimeInterface $modified): self
|
||||
{
|
||||
$this->modified = $modified;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getModified(): \DateTimeInterface
|
||||
public function getModified(): DateTimeInterface
|
||||
{
|
||||
return $this->modified;
|
||||
}
|
||||
|
@ -160,64 +136,86 @@ class ActorCircle extends Entity
|
|||
// @codeCoverageIgnoreEnd
|
||||
// }}} Autocode
|
||||
|
||||
public function getActorTag()
|
||||
/**
|
||||
* For use with MetaCollection trait only
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->tag;
|
||||
}
|
||||
|
||||
public function getActorTags(bool $db_reference = false): array
|
||||
{
|
||||
$handle = fn () => DB::findBy('actor_tag', ['tagger' => $this->getTagger(), 'tag' => $this->getTag()]);
|
||||
if ($db_reference) {
|
||||
return $handle();
|
||||
}
|
||||
return Cache::get(
|
||||
"circle-{$this->getId()}-tagged",
|
||||
$handle,
|
||||
);
|
||||
}
|
||||
|
||||
public function getTaggedActors()
|
||||
{
|
||||
return Cache::get(
|
||||
"actor-tag-{$this->getTag()}",
|
||||
fn () => DB::findBy('actor_tag', ['tagger' => $this->getTagger(), 'canonical' => $this->getTag()], limit: 1)[0], // TODO jank
|
||||
"circle-{$this->getId()}-tagged-actors",
|
||||
function () {
|
||||
if ($this->getTagger()) {
|
||||
return DB::dql('SELECT a FROM actor AS a JOIN actor_tag AS at WITH at.tagged = a.id WHERE at.tag = :tag AND at.tagger = :tagger', ['tag' => $this->getTag(), 'tagger' => $this->getTagger()]);
|
||||
} else { // Self-tag
|
||||
return DB::dql('SELECT a FROM actor AS a JOIN actor_tag AS at WITH at.tagged = a.id WHERE at.tag = :tag AND at.tagger = at.tagged', ['tag' => $this->getTag()]);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public function getSubscribedActors(?int $offset = null, ?int $limit = null): array
|
||||
{
|
||||
return Cache::get(
|
||||
"circle-{$this->getId()}",
|
||||
"circle-{$this->getId()}-subscribers",
|
||||
fn () => DB::dql(
|
||||
<<< 'EOQ'
|
||||
SELECT a
|
||||
FROM App\Entity\Actor a
|
||||
JOIN App\Entity\ActorCircleSubscription s
|
||||
FROM actor a
|
||||
JOIN actor_circle_subscription s
|
||||
WITH a.id = s.actor_id
|
||||
ORDER BY s.created DESC, a.id DESC
|
||||
EOQ,
|
||||
options: ['offset' => $offset,
|
||||
'limit' => $limit, ],
|
||||
options: [
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function getUrl(int $type = Router::ABSOLUTE_PATH): string {
|
||||
return Router::url('actor_circle', ['actor_id' => $this->getTagger(), 'tag' => $this->getTag()]);
|
||||
public function getUrl(int $type = Router::ABSOLUTE_PATH): string
|
||||
{
|
||||
return Router::url('actor_circle_view_by_circle_id', ['circle_id' => $this->getId()], type: $type);
|
||||
}
|
||||
|
||||
public static function schemaDef(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'actor_circle',
|
||||
'description' => 'a actor can have lists of actors, to separate their feed',
|
||||
'description' => 'An actor can have lists of actors, to separate their feed or quickly mention his friend',
|
||||
'fields' => [
|
||||
'id' => ['type' => 'serial', 'not null' => true, 'description' => 'unique identifier'], // An actor can be tagged by many actors
|
||||
'tagger' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'many to one', 'name' => 'actor_list_tagger_fkey', 'description' => 'user making the tag'],
|
||||
'tagged' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'actor_tag_tagged_fkey', 'not null' => true, 'description' => 'actor tagged'],
|
||||
'tag' => ['type' => 'varchar', 'length' => 64, 'foreign key' => true, 'target' => 'ActorTag.tag', 'multiplicity' => 'many to one', 'not null' => true, 'description' => 'actor tag'], // Join with ActorTag // // so, Doctrine doesn't like that the target is not unique, even though the pair is // Many Actor Circles can reference (and probably will) an Actor Tag
|
||||
'use_canonical' => ['type' => 'bool', 'not null' => true, 'description' => 'whether the user wanted to block canonical tags'],
|
||||
'description' => ['type' => 'text', 'description' => 'description of the people tag'],
|
||||
'private' => ['type' => 'bool', 'default' => false, 'description' => 'is this tag private'],
|
||||
'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'],
|
||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
||||
'id' => ['type' => 'serial', 'not null' => true, 'description' => 'unique identifier'], // An actor can be tagged by many actors
|
||||
'tagger' => ['type' => 'int', 'default' => null, 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'many to one', 'name' => 'actor_list_tagger_fkey', 'description' => 'user making the tag, null if self-tag'],
|
||||
'tag' => ['type' => 'varchar', 'length' => 64, 'foreign key' => true, 'target' => 'ActorTag.tag', 'multiplicity' => 'many to one', 'not null' => true, 'description' => 'actor tag'], // Join with ActorTag // // so, Doctrine doesn't like that the target is not unique, even though the pair is // Many Actor Circles can reference (and probably will) an Actor Tag
|
||||
'description' => ['type' => 'text', 'description' => 'description of the people tag'],
|
||||
'private' => ['type' => 'bool', 'default' => false, 'description' => 'is this tag private'],
|
||||
'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'],
|
||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
||||
],
|
||||
'primary key' => ['id'],
|
||||
'primary key' => ['id'], // But we will mostly refer to them with `tagger` and `tag`
|
||||
'indexes' => [
|
||||
'actor_list_modified_idx' => ['modified'],
|
||||
'actor_list_tagger_tag_idx' => ['tagger', 'tag'], // The actual identifier we will use the most
|
||||
'actor_list_tag_idx' => ['tag'],
|
||||
'actor_list_tagger_tag_idx' => ['tagger', 'tag'],
|
||||
'actor_list_tagger_idx' => ['tagger'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return $this->getTag();
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
// {{{ License
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
|
@ -17,7 +19,7 @@
|
|||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
// }}}
|
||||
|
||||
namespace App\Entity;
|
||||
namespace Component\Circle\Entity;
|
||||
|
||||
use App\Core\Entity;
|
||||
use DateTimeInterface;
|
||||
|
@ -45,8 +47,8 @@ class ActorCircleSubscription extends Entity
|
|||
// @codeCoverageIgnoreStart
|
||||
private int $actor_id;
|
||||
private int $circle_id;
|
||||
private \DateTimeInterface $created;
|
||||
private \DateTimeInterface $modified;
|
||||
private DateTimeInterface $created;
|
||||
private DateTimeInterface $modified;
|
||||
|
||||
public function setActorId(int $actor_id): self
|
||||
{
|
||||
|
@ -70,24 +72,24 @@ class ActorCircleSubscription extends Entity
|
|||
return $this->circle_id;
|
||||
}
|
||||
|
||||
public function setCreated(\DateTimeInterface $created): self
|
||||
public function setCreated(DateTimeInterface $created): self
|
||||
{
|
||||
$this->created = $created;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreated(): \DateTimeInterface
|
||||
public function getCreated(): DateTimeInterface
|
||||
{
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
public function setModified(\DateTimeInterface $modified): self
|
||||
public function setModified(DateTimeInterface $modified): self
|
||||
{
|
||||
$this->modified = $modified;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getModified(): \DateTimeInterface
|
||||
public function getModified(): DateTimeInterface
|
||||
{
|
||||
return $this->modified;
|
||||
}
|
||||
|
@ -98,18 +100,18 @@ class ActorCircleSubscription extends Entity
|
|||
public static function schemaDef(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'actor_circle_subscription',
|
||||
'name' => 'actor_circle_subscription',
|
||||
'fields' => [
|
||||
'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'actor_circle_subscription_actor_id_fkey', 'not null' => true, 'description' => 'foreign key to actor table'],
|
||||
'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'actor_circle_subscription_actor_id_fkey', 'not null' => true, 'description' => 'foreign key to actor table'],
|
||||
// An actor subscribes many circles; A Circle is subscribed by many actors.
|
||||
'circle_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'ActorCircle.id', 'multiplicity' => 'one to many', 'name' => 'actor_circle_subscription_actor_circle_fkey', 'not null' => true, 'description' => 'foreign key to actor_circle'],
|
||||
'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'],
|
||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
||||
],
|
||||
'primary key' => ['circle_id', 'actor_id'],
|
||||
'indexes' => [
|
||||
'indexes' => [
|
||||
'actor_circle_subscription_actor_id_idx' => ['actor_id'],
|
||||
'actor_circle_subscription_created_idx' => ['created'],
|
||||
'actor_circle_subscription_created_idx' => ['created'],
|
||||
],
|
||||
];
|
||||
}
|
|
@ -19,12 +19,12 @@ declare(strict_types = 1);
|
|||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
// }}}
|
||||
|
||||
namespace App\Entity;
|
||||
namespace Component\Circle\Entity;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Core\Router\Router;
|
||||
use App\Entity\Actor;
|
||||
use Component\Tag\Tag;
|
||||
use DateTimeInterface;
|
||||
|
||||
|
@ -54,9 +54,7 @@ class ActorTag extends Entity
|
|||
private int $tagger;
|
||||
private int $tagged;
|
||||
private string $tag;
|
||||
private string $canonical;
|
||||
private bool $use_canonical;
|
||||
private \DateTimeInterface $modified;
|
||||
private DateTimeInterface $modified;
|
||||
|
||||
public function setTagger(int $tagger): self
|
||||
{
|
||||
|
@ -82,7 +80,7 @@ class ActorTag extends Entity
|
|||
|
||||
public function setTag(string $tag): self
|
||||
{
|
||||
$this->tag = \mb_substr($tag, 0, 64);
|
||||
$this->tag = mb_substr($tag, 0, 64);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -91,35 +89,13 @@ class ActorTag extends Entity
|
|||
return $this->tag;
|
||||
}
|
||||
|
||||
public function setCanonical(string $canonical): self
|
||||
{
|
||||
$this->canonical = \mb_substr($canonical, 0, 64);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCanonical(): string
|
||||
{
|
||||
return $this->canonical;
|
||||
}
|
||||
|
||||
public function setUseCanonical(bool $use_canonical): self
|
||||
{
|
||||
$this->use_canonical = $use_canonical;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUseCanonical(): bool
|
||||
{
|
||||
return $this->use_canonical;
|
||||
}
|
||||
|
||||
public function setModified(\DateTimeInterface $modified): self
|
||||
public function setModified(DateTimeInterface $modified): self
|
||||
{
|
||||
$this->modified = $modified;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getModified(): \DateTimeInterface
|
||||
public function getModified(): DateTimeInterface
|
||||
{
|
||||
return $this->modified;
|
||||
}
|
||||
|
@ -127,18 +103,22 @@ class ActorTag extends Entity
|
|||
// @codeCoverageIgnoreEnd
|
||||
// }}} Autocode
|
||||
|
||||
public static function getByActorId(int $actor_id): array
|
||||
public function getUrl(?Actor $actor = null, int $type = Router::ABSOLUTE_PATH): string
|
||||
{
|
||||
return Cache::getList(Actor::cacheKeys($actor_id)['tags'], fn () => DB::dql('select at from actor_tag at join actor a with a.id = at.tagger where a.id = :id', ['id' => $actor_id]));
|
||||
$params = ['tag' => $this->getTag()];
|
||||
if (!\is_null($actor)) {
|
||||
$params['locale'] = $actor->getTopLanguage()->getLocale();
|
||||
}
|
||||
return Router::url('single_actor_tag', $params, type: $type);
|
||||
}
|
||||
|
||||
public function getUrl(?Actor $actor = null): string
|
||||
public function getCircle(): ActorCircle
|
||||
{
|
||||
$params = ['canon' => $this->getCanonical(), 'tag' => $this->getTag()];
|
||||
if (!\is_null($actor)) {
|
||||
$params['lang'] = $actor->getTopLanguage()->getLocale();
|
||||
if ($this->getTagger() === $this->getTagged()) { // Self-tag
|
||||
return DB::findOneBy(ActorCircle::class, ['tagger' => null, 'tag' => $this->getTag()]);
|
||||
} else {
|
||||
return DB::findOneBy(ActorCircle::class, ['tagger' => $this->getTagger(), 'tag' => $this->getTag()]);
|
||||
}
|
||||
return Router::url('single_actor_tag', $params);
|
||||
}
|
||||
|
||||
public static function schemaDef(): array
|
||||
|
@ -146,24 +126,16 @@ class ActorTag extends Entity
|
|||
return [
|
||||
'name' => 'actor_tag',
|
||||
'fields' => [
|
||||
'tagger' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'actor_tag_tagger_fkey', 'not null' => true, 'description' => 'actor making the tag'],
|
||||
'tagged' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'actor_tag_tagged_fkey', 'not null' => true, 'description' => 'actor tagged'],
|
||||
'tag' => ['type' => 'varchar', 'length' => Tag::MAX_TAG_LENGTH, 'not null' => true, 'description' => 'hash tag associated with this actor'],
|
||||
'canonical' => ['type' => 'varchar', 'length' => Tag::MAX_TAG_LENGTH, 'not null' => true, 'description' => 'ascii slug of tag'],
|
||||
'use_canonical' => ['type' => 'bool', 'not null' => true, 'description' => 'whether the user wanted to block canonical tags'],
|
||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
||||
],
|
||||
'primary key' => ['tagger', 'tagged', 'tag', 'use_canonical'],
|
||||
'tagger' => ['type' => 'int', 'not null' => true, 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'actor_tag_tagger_fkey', 'description' => 'actor making the tag'],
|
||||
'tagged' => ['type' => 'int', 'not null' => true, 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'actor_tag_tagged_fkey', 'description' => 'actor tagged'],
|
||||
'tag' => ['type' => 'varchar', 'length' => Tag::MAX_TAG_LENGTH, 'not null' => true, 'description' => 'hashtag associated with this note'],
|
||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
||||
], // We will always assume the tagger's preferred language for tags and circles
|
||||
'primary key' => ['tagger', 'tagged', 'tag'],
|
||||
'indexes' => [
|
||||
'actor_tag_modified_idx' => ['modified'],
|
||||
'actor_tag_tagger_tag_idx' => ['tagger', 'tag'], // For Circles
|
||||
'actor_tag_tagged_idx' => ['tagged'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getTag();
|
||||
}
|
||||
}
|
57
components/Circle/Form/SelfTagsForm.php
Normal file
57
components/Circle/Form/SelfTagsForm.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Component\Circle\Form;
|
||||
|
||||
use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Util\Form\ArrayTransformer;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
abstract class SelfTagsForm
|
||||
{
|
||||
/**
|
||||
* @return array [Form (add), ?Form (existing)]
|
||||
*/
|
||||
public static function handleTags(
|
||||
Request $request,
|
||||
array $actor_self_tags,
|
||||
callable $handle_new,
|
||||
callable $handle_existing,
|
||||
string $remove_label,
|
||||
string $add_label,
|
||||
): array {
|
||||
$form_definition = [];
|
||||
foreach ($actor_self_tags as $tag) {
|
||||
$tag = $tag->getTag();
|
||||
$form_definition[] = ["{$tag}:old-tag", TextType::class, ['data' => $tag, 'label' => ' ', 'disabled' => true]];
|
||||
$form_definition[] = [$existing_form_name = "{$tag}:remove", SubmitType::class, ['label' => $remove_label]];
|
||||
}
|
||||
|
||||
$existing_form = !empty($form_definition) ? Form::create($form_definition) : null;
|
||||
|
||||
$add_form = Form::create([
|
||||
['new-tags', TextType::class, ['label' => ' ', 'data' => [], 'required' => false, 'help' => _m('Tags for yourself (letters, numbers, -, ., and _), comma- or space-separated.'), 'transformer' => ArrayTransformer::class]],
|
||||
[$add_form_name = 'new-tags-add', SubmitType::class, ['label' => $add_label]],
|
||||
]);
|
||||
|
||||
if ($request->getMethod() === 'POST' && $request->request->has($add_form_name)) {
|
||||
$add_form->handleRequest($request);
|
||||
if ($add_form->isSubmitted() && $add_form->isValid()) {
|
||||
$handle_new($add_form);
|
||||
}
|
||||
}
|
||||
|
||||
if (!\is_null($existing_form) && $request->getMethod() === 'POST' && $request->request->has($existing_form_name ?? '')) {
|
||||
$existing_form->handleRequest($request);
|
||||
if ($existing_form->isSubmitted() && $existing_form->isValid()) {
|
||||
$handle_existing($existing_form, $form_definition);
|
||||
}
|
||||
}
|
||||
|
||||
return [$add_form, $existing_form];
|
||||
}
|
||||
}
|
|
@ -30,9 +30,9 @@ use App\Entity\Actor;
|
|||
use App\Util\Common;
|
||||
use App\Util\HTML;
|
||||
use App\Util\Nickname;
|
||||
use Component\Circle\Controller\SelfTagsSettings;
|
||||
use Component\Group\Controller as C;
|
||||
use Component\Group\Entity\LocalGroup;
|
||||
use Component\Tag\Controller\Tag as TagController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class Group extends Component
|
||||
|
@ -68,7 +68,7 @@ class Group extends Component
|
|||
'title' => 'Self tags',
|
||||
'desc' => 'Add or remove tags on this group',
|
||||
'id' => 'settings-self-tags',
|
||||
'controller' => TagController::settingsSelfTags($request, $group, 'settings-self-tags-details'),
|
||||
'controller' => SelfTagsSettings::settingsSelfTags($request, $group, 'settings-self-tags-details'),
|
||||
];
|
||||
}
|
||||
return Event::next;
|
||||
|
|
|
@ -1,181 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
// {{{ License
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
// }}}
|
||||
/**
|
||||
* Actor Circles for GNU social
|
||||
*
|
||||
* @package GNUsocial
|
||||
* @category Plugin
|
||||
*
|
||||
* @author Phablulo <phablulo@gmail.com>
|
||||
* @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
|
||||
namespace Plugin\ActorCircles;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\Event;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router\Router;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\Feed;
|
||||
use App\Entity\LocalUser;
|
||||
use App\Util\Nickname;
|
||||
use Component\Collection\Util\MetaCollectionPlugin;
|
||||
use Plugin\ActorCircles\Controller as C;
|
||||
use Plugin\ActorCircles\Entity as E;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class ActorCircles extends MetaCollectionPlugin
|
||||
{
|
||||
protected string $slug = 'circle';
|
||||
protected string $plural_slug = 'circles';
|
||||
|
||||
private function getActorIdFromVars(array $vars): int
|
||||
{
|
||||
$id = $vars['request']->get('id', null);
|
||||
if ($id) {
|
||||
return (int) $id;
|
||||
}
|
||||
$nick = $vars['request']->get('nickname');
|
||||
$actor = DB::findOneBy(Actor::class, ['nickname' => $nick]);
|
||||
return $actor->getId();
|
||||
}
|
||||
|
||||
protected function createCollection(Actor $owner, array $vars, string $name)
|
||||
{
|
||||
$actor_id = $this->getActorIdFromVars($vars);
|
||||
$col = E\ActorCircles::create([
|
||||
'name' => $name,
|
||||
'actor_id' => $owner->getId(),
|
||||
]);
|
||||
DB::persist($col);
|
||||
DB::persist(E\ActorCirclesEntry::create([
|
||||
'actor_id' => $actor_id,
|
||||
'circle_id' => $col->getId(),
|
||||
]));
|
||||
}
|
||||
|
||||
protected function removeItems(Actor $owner, array $vars, $items, array $collections)
|
||||
{
|
||||
$actor_id = $this->getActorIdFromVars($vars);
|
||||
// can only delete what you own
|
||||
$items = array_filter($items, fn ($x) => \in_array($x, $collections));
|
||||
DB::dql(<<<'EOF'
|
||||
DELETE FROM \Plugin\ActorCircles\Entity\ActorCirclesEntry AS entry
|
||||
WHERE entry.actor_id = :actor_id AND entry.circle_id IN (:ids)
|
||||
EOF, [
|
||||
'actor_id' => $actor_id,
|
||||
'ids' => $items,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addItems(Actor $owner, array $vars, $items, array $collections)
|
||||
{
|
||||
$actor_id = $this->getActorIdFromVars($vars);
|
||||
foreach ($items as $id) {
|
||||
// prevent user from putting something in a collection (s)he doesn't own:
|
||||
if (\in_array($id, $collections)) {
|
||||
DB::persist(E\ActorCirclesEntry::create([
|
||||
'actor_id' => $actor_id,
|
||||
'circle_id' => $id,
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see MetaCollectionPlugin->shouldAddToRightPanel
|
||||
*/
|
||||
protected function shouldAddToRightPanel(Actor $user, $vars, Request $request): bool
|
||||
{
|
||||
return
|
||||
$vars['path'] === 'actor_view_nickname'
|
||||
|| $vars['path'] === 'actor_view_id'
|
||||
|| $vars['path'] === 'group_actor_view_nickname'
|
||||
|| $vars['path'] === 'group_actor_view_id';
|
||||
}
|
||||
|
||||
protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array
|
||||
{
|
||||
if (\is_null($vars)) {
|
||||
$res = DB::findBy(E\ActorCircles::class, ['actor_id' => $owner->getId()]);
|
||||
} else {
|
||||
$actor_id = $this->getActorIdFromVars($vars);
|
||||
$res = DB::dql(
|
||||
<<<'EOF'
|
||||
SELECT entry.circle_id FROM \Plugin\ActorCircles\Entity\ActorCirclesEntry AS entry
|
||||
INNER JOIN \Plugin\ActorCircles\Entity\ActorCircles AS circle
|
||||
WITH circle.id = entry.circle_id
|
||||
WHERE circle.actor_id = :owner_id AND entry.actor_id = :actor_id
|
||||
EOF,
|
||||
[
|
||||
'owner_id' => $owner->getId(),
|
||||
'actor_id' => $actor_id,
|
||||
],
|
||||
);
|
||||
}
|
||||
if (!$ids_only) {
|
||||
return $res;
|
||||
}
|
||||
return array_map(fn ($x) => $x['circle_id'], $res);
|
||||
}
|
||||
|
||||
public function onAddRoute(RouteLoader $r): bool
|
||||
{
|
||||
// View all circles by actor id and nickname
|
||||
$r->connect(
|
||||
id: 'actor_circles_view_by_actor_id',
|
||||
uri_path: '/actor/{id<\d+>}/circles',
|
||||
target: [C\Circles::class, 'collectionsViewByActorId'],
|
||||
);
|
||||
$r->connect(
|
||||
id: 'actor_circles_view_by_nickname',
|
||||
uri_path: '/@{nickname<' . Nickname::DISPLAY_FMT . '>}/circles',
|
||||
target: [C\Circles::class, 'collectionsViewByActorNickname'],
|
||||
);
|
||||
// View notes from a circle by actor id and nickname
|
||||
$r->connect(
|
||||
id: 'actor_circles_notes_view_by_actor_id',
|
||||
uri_path: '/actor/{id<\d+>}/circles/{cid<\d+>}',
|
||||
target: [C\Circles::class, 'collectionsEntryViewNotesByActorId'],
|
||||
);
|
||||
$r->connect(
|
||||
id: 'actor_circles_notes_view_by_nickname',
|
||||
uri_path: '/@{nickname<' . Nickname::DISPLAY_FMT . '>}/circles/{cid<\d+>}',
|
||||
target: [C\Circles::class, 'collectionsEntryViewNotesByNickname'],
|
||||
);
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering)
|
||||
{
|
||||
DB::persist(Feed::create([
|
||||
'actor_id' => $actor_id,
|
||||
'url' => Router::url($route = 'actor_circles_view_by_nickname', ['nickname' => $user->getNickname()]),
|
||||
'route' => $route,
|
||||
'title' => _m('Circles'),
|
||||
'ordering' => $ordering++,
|
||||
]));
|
||||
return Event::next;
|
||||
}
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
// {{{ License
|
||||
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// }}}
|
||||
|
||||
namespace Plugin\ActorCircles\Controller;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\Router\Router;
|
||||
use Component\Collection\Util\Controller\MetaCollectionController;
|
||||
use Plugin\ActorCircles\Entity\ActorCircles;
|
||||
|
||||
class Circles extends MetaCollectionController
|
||||
{
|
||||
protected string $slug = 'circle';
|
||||
protected string $plural_slug = 'circles';
|
||||
protected string $page_title = 'Actor circles';
|
||||
|
||||
public function createCollection(int $owner_id, string $name)
|
||||
{
|
||||
DB::persist(ActorCircles::create([
|
||||
'name' => $name,
|
||||
'actor_id' => $owner_id,
|
||||
]));
|
||||
}
|
||||
public function getCollectionUrl(int $owner_id, ?string $owner_nickname, int $collection_id): string
|
||||
{
|
||||
if (\is_null($owner_nickname)) {
|
||||
return Router::url(
|
||||
'actor_circles_notes_view_by_actor_id',
|
||||
['id' => $owner_id, 'cid' => $collection_id],
|
||||
);
|
||||
}
|
||||
return Router::url(
|
||||
'actor_circles_notes_view_by_nickname',
|
||||
['nickname' => $owner_nickname, 'cid' => $collection_id],
|
||||
);
|
||||
}
|
||||
public function getCollectionItems(int $owner_id, $collection_id): array
|
||||
{
|
||||
$notes = DB::dql(
|
||||
<<<'EOF'
|
||||
SELECT n FROM \App\Entity\Note as n WHERE n.actor_id in (
|
||||
SELECT entry.actor_id FROM \Plugin\ActorCircles\Entity\ActorCirclesEntry as entry
|
||||
inner join \Plugin\ActorCircles\Entity\ActorCircles as ac
|
||||
with ac.id = entry.circle_id
|
||||
WHERE ac.id = :circle_id
|
||||
)
|
||||
ORDER BY n.created DESC, n.id DESC
|
||||
EOF,
|
||||
['circle_id' => $collection_id],
|
||||
);
|
||||
return [
|
||||
'_template' => 'collection/notes.html.twig',
|
||||
'notes' => array_values($notes),
|
||||
];
|
||||
}
|
||||
public function getCollectionsByActorId(int $owner_id): array
|
||||
{
|
||||
return DB::findBy(ActorCircles::class, ['actor_id' => $owner_id], order_by: ['id' => 'desc']);
|
||||
}
|
||||
public function getCollectionBy(int $owner_id, int $collection_id): ActorCircles
|
||||
{
|
||||
return DB::findOneBy(ActorCircles::class, ['id' => $collection_id, 'actor_id' => $owner_id]);
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Plugin\ActorCircles\Entity;
|
||||
|
||||
use App\Core\Entity;
|
||||
|
||||
class ActorCircles extends Entity
|
||||
{
|
||||
// These tags are meant to be literally included and will be populated with the appropriate fields, setters and getters by `bin/generate_entity_fields`
|
||||
// {{{ Autocode
|
||||
// @codeCoverageIgnoreStart
|
||||
private int $id;
|
||||
private string $name;
|
||||
private int $actor_id;
|
||||
|
||||
public function setId(int $id): self
|
||||
{
|
||||
$this->id = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setName(string $name): self
|
||||
{
|
||||
$this->name = mb_substr($name, 0, 255);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setActorId(int $actor_id): self
|
||||
{
|
||||
$this->actor_id = $actor_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getActorId(): int
|
||||
{
|
||||
return $this->actor_id;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
// }}} Autocode
|
||||
public static function schemaDef()
|
||||
{
|
||||
return [
|
||||
'name' => 'actor_circles_a',
|
||||
'fields' => [
|
||||
'id' => ['type' => 'serial', 'not null' => true, 'description' => 'unique identifier'],
|
||||
'name' => ['type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'collection\'s name'],
|
||||
'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to many', 'not null' => true, 'description' => 'foreign key to actor table'],
|
||||
],
|
||||
'primary key' => ['id'],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Plugin\ActorCircles\Entity;
|
||||
|
||||
use App\Core\Entity;
|
||||
|
||||
class ActorCirclesEntry extends Entity
|
||||
{
|
||||
// These tags are meant to be literally included and will be populated with the appropriate fields, setters and getters by `bin/generate_entity_fields`
|
||||
// {{{ Autocode
|
||||
// @codeCoverageIgnoreStart
|
||||
private int $id;
|
||||
private int $actor_id;
|
||||
private int $circle_id;
|
||||
|
||||
public function setId(int $id): self
|
||||
{
|
||||
$this->id = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setActorId(int $actor_id): self
|
||||
{
|
||||
$this->actor_id = $actor_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getActorId(): int
|
||||
{
|
||||
return $this->actor_id;
|
||||
}
|
||||
|
||||
public function setCircleId(int $circle_id): self
|
||||
{
|
||||
$this->circle_id = $circle_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCircleId(): int
|
||||
{
|
||||
return $this->circle_id;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
// }}} Autocode
|
||||
|
||||
public static function schemaDef()
|
||||
{
|
||||
return [
|
||||
'name' => 'actor_circles_entry',
|
||||
'fields' => [
|
||||
'id' => ['type' => 'serial', 'not null' => true, 'description' => 'unique identifier'],
|
||||
'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'foreign key to attachment table'],
|
||||
'circle_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'actor_circles_a.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'foreign key to collection table'],
|
||||
],
|
||||
'primary key' => ['id'],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
// {{{ License
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
// }}}
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\Entity;
|
||||
use Component\Tag\Tag;
|
||||
use DateTimeInterface;
|
||||
use Functional as F;
|
||||
|
||||
/**
|
||||
* Entity for User's Note Tag block
|
||||
*
|
||||
* @category DB
|
||||
* @package GNUsocial
|
||||
*
|
||||
* @author Hugo Sales <hugo@hsal.es>
|
||||
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class ActorTagBlock extends Entity
|
||||
{
|
||||
// {{{ Autocode
|
||||
// @codeCoverageIgnoreStart
|
||||
private int $blocker;
|
||||
private string $tag;
|
||||
private string $canonical;
|
||||
private bool $use_canonical;
|
||||
private \DateTimeInterface $modified;
|
||||
|
||||
public function setBlocker(int $blocker): self
|
||||
{
|
||||
$this->blocker = $blocker;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBlocker(): int
|
||||
{
|
||||
return $this->blocker;
|
||||
}
|
||||
|
||||
public function setTag(string $tag): self
|
||||
{
|
||||
$this->tag = \mb_substr($tag, 0, 64);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTag(): string
|
||||
{
|
||||
return $this->tag;
|
||||
}
|
||||
|
||||
public function setCanonical(string $canonical): self
|
||||
{
|
||||
$this->canonical = \mb_substr($canonical, 0, 64);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCanonical(): string
|
||||
{
|
||||
return $this->canonical;
|
||||
}
|
||||
|
||||
public function setUseCanonical(bool $use_canonical): self
|
||||
{
|
||||
$this->use_canonical = $use_canonical;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUseCanonical(): bool
|
||||
{
|
||||
return $this->use_canonical;
|
||||
}
|
||||
|
||||
public function setModified(\DateTimeInterface $modified): self
|
||||
{
|
||||
$this->modified = $modified;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getModified(): \DateTimeInterface
|
||||
{
|
||||
return $this->modified;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
// }}} Autocode
|
||||
|
||||
public static function cacheKey(int $actor_id)
|
||||
{
|
||||
return "actor-tag-blocks-{$actor_id}";
|
||||
}
|
||||
|
||||
public static function getByActorId(int $actor_id)
|
||||
{
|
||||
return Cache::getList(self::cacheKey($actor_id), fn () => DB::findBy('actor_tag_block', ['blocker' => $actor_id]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether $actor_tag is considered blocked by one of
|
||||
* $actor_tag_blocks
|
||||
*/
|
||||
public static function checkBlocksActorTag(ActorTag $actor_tag, array $actor_tag_blocks): bool
|
||||
{
|
||||
return F\some($actor_tag_blocks, fn ($ntb) => ($ntb->getUseCanonical() && $actor_tag->getCanonical() === $ntb->getCanonical()) || $actor_tag->getTag() === $ntb->getTag());
|
||||
}
|
||||
|
||||
public static function schemaDef(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'actor_tag_block',
|
||||
'fields' => [
|
||||
'blocker' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'many to one', 'name' => 'actor_block_blocker_fkey', 'not null' => true, 'description' => 'user making the block'],
|
||||
'tag' => ['type' => 'varchar', 'length' => Tag::MAX_TAG_LENGTH, 'not null' => true, 'description' => 'hash tag this is blocking'],
|
||||
'canonical' => ['type' => 'varchar', 'length' => Tag::MAX_TAG_LENGTH, 'foreign key' => true, 'target' => 'NoteTag.canonical', 'multiplicity' => 'many to one', 'not null' => true, 'description' => 'ascii slug of tag'],
|
||||
'use_canonical' => ['type' => 'bool', 'not null' => true, 'description' => 'whether the user wanted to block canonical tags'],
|
||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
||||
],
|
||||
'primary key' => ['blocker', 'canonical'],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -32,19 +32,20 @@ declare(strict_types = 1);
|
|||
|
||||
namespace App\Util;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\Event;
|
||||
use App\Core\Log;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\Note;
|
||||
use App\Util\Exception\NicknameException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use Component\Circle\Circle;
|
||||
use Component\Circle\Entity\ActorCircle;
|
||||
use Component\Group\Entity\LocalGroup;
|
||||
use Component\Tag\Tag;
|
||||
use Exception;
|
||||
use Functional as F;
|
||||
use InvalidArgumentException;
|
||||
use App\Core\DB\DB;
|
||||
use App\Entity\ActorCircle;
|
||||
|
||||
abstract class Formatting
|
||||
{
|
||||
|
@ -298,7 +299,7 @@ abstract class Formatting
|
|||
$text = str_replace('<span>', '', $text);
|
||||
if (Event::handle('StartFindMentions', [$actor, $text, &$mentions])) {
|
||||
|
||||
// Person mentions
|
||||
// @person mentions
|
||||
$person_matches = self::findMentionsRaw($text, '@');
|
||||
foreach ($person_matches as $match) {
|
||||
try {
|
||||
|
@ -330,45 +331,39 @@ abstract class Formatting
|
|||
}
|
||||
}
|
||||
|
||||
@#/tag
|
||||
// TODO Tag subscriptions
|
||||
// @#tag => mention of all subscriptions tagged 'tag'
|
||||
// @#circle/self-tag => mention of all subscribed circles tagged 'tag'
|
||||
$tag_matches = [];
|
||||
preg_match_all(
|
||||
Tag::TAG_CIRCLE_REGEX,
|
||||
Circle::TAG_CIRCLE_REGEX,
|
||||
$text,
|
||||
$tag_matches,
|
||||
PREG_OFFSET_CAPTURE
|
||||
\PREG_OFFSET_CAPTURE,
|
||||
);
|
||||
foreach ($tag_matches[1] as $tag_match) {
|
||||
$tag = Tag::ensureValid($tag_match[0]);
|
||||
$ac = DB::findOneBy(ActorCircle::class, [
|
||||
'or' => [
|
||||
'tagger' => $actor->getID(),
|
||||
'and' => [
|
||||
'tagger' => null,
|
||||
'tagged' => $actor->getID(),
|
||||
]
|
||||
],
|
||||
'tag' => $tag,
|
||||
$tag = Tag::extract($tag_match[0]);
|
||||
if (!Tag::validate($tag)) {
|
||||
continue; // Ignore invalid tags
|
||||
}
|
||||
$ac = DB::findOneBy(ActorCircle::class, [
|
||||
'tag' => $tag, // Notify circle of name tag WHERE
|
||||
'tagger' => $actor->getID(), // Circle was created by Actor
|
||||
], return_null: true);
|
||||
|
||||
if (\is_null($ac) || $ac->getPrivate()) {
|
||||
continue;
|
||||
}
|
||||
$tagged = $ac->getSubscribedActors();
|
||||
$url = $ac->getUrl();
|
||||
|
||||
$mentions[] = [
|
||||
'mentioned' => $tagged,
|
||||
'mentioned' => $ac->getSubscribedActors(),
|
||||
'type' => 'list',
|
||||
'text' => $tag_match[0],
|
||||
'position' => $tag_match[1],
|
||||
'length' => mb_strlen($tag_match[0]),
|
||||
'url' => $url,
|
||||
'url' => $ac->getUrl(),
|
||||
];
|
||||
}
|
||||
|
||||
// Group mentions
|
||||
// !group/!org mentions
|
||||
$group_matches = self::findMentionsRaw($text, '!');
|
||||
foreach ($group_matches as $match) {
|
||||
try {
|
||||
|
@ -436,7 +431,7 @@ abstract class Formatting
|
|||
*
|
||||
* @return array [partially-rendered HTML, array of mentions]
|
||||
*/
|
||||
public static function linkifyMentions(string $text, Actor $author, string $language): array
|
||||
public static function linkifyMentions(string $text, Actor $author, string $locale): array
|
||||
{
|
||||
$mentions = self::findMentions($text, $author);
|
||||
|
||||
|
@ -455,7 +450,7 @@ abstract class Formatting
|
|||
foreach ($points as $position => $mention) {
|
||||
$linkText = self::linkifyMentionArray($mention);
|
||||
|
||||
$text = substr_replace($text, $linkText, $position, $mention['length']);
|
||||
$text = substr_replace($text, $linkText, $position-1, $mention['length']+1);
|
||||
}
|
||||
|
||||
return [$text, $mentions];
|
||||
|
@ -465,17 +460,19 @@ abstract class Formatting
|
|||
{
|
||||
$output = null;
|
||||
|
||||
if (Event::handle('StartLinkifyMention', [$mention, &$output])) {
|
||||
if (Event::handle('StartLinkifyMention', [$mention, &$output]) === Event::next) {
|
||||
$attrs = [
|
||||
'href' => $mention['url'],
|
||||
'class' => 'h-card u-url p-nickname ' . $mention['type'],
|
||||
'class' => 'h-card u-url p-nickname ' . $mention['type'], // https://microformats.org/wiki/h-card
|
||||
];
|
||||
|
||||
if (!empty($mention['title'])) {
|
||||
$attrs['title'] = $mention['title'];
|
||||
}
|
||||
|
||||
$output = HTML::html(['a' => ['attrs' => $attrs, $mention['text']]]);
|
||||
$output = HTML::html(['span' => ['attrs' => ['class' => 'h-card'],
|
||||
'@' . HTML::html(['a' => ['attrs' => $attrs, $mention['title']]], options: ['indent' => false]),
|
||||
]], options: ['indent' => false, 'raw' => true]);
|
||||
|
||||
Event::handle('EndLinkifyMention', [$mention, &$output]);
|
||||
}
|
||||
|
|
|
@ -28,8 +28,8 @@
|
|||
</section>
|
||||
|
||||
<section class="profile-info-stats">
|
||||
<div><strong>{{ 'Subscribed' | trans }}</strong>{{ actor.getSubscribedCount() }}</div>
|
||||
<div><strong>{{ 'Subscribers' | trans }}</strong>{{ actor.getSubscribersCount() }}</div>
|
||||
<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">
|
||||
|
|
|
@ -1 +1 @@
|
|||
<a href="{{ tag.getUrl(actor) }}" title="{{ tag.getTag() }}" rel="tag"><em>#{{ tag.getTag() }}</em></a>
|
||||
<span class="tag">#<a href="{{ tag.getCircle().getUrl() }}" title="{{ tag.getTag() }}" rel="tag"><em>{{ tag.getTag() }}</em></a></span>
|
||||
|
|
|
@ -1 +1 @@
|
|||
<a href="{{ tag.getUrl() }}" title="{{ tag.getTag() }}" rel="tag">#{{ tag.getTag() }}</a>
|
||||
<span class="tag">#<a href="{{ tag.getUrl() }}" title="{{ tag.getTag() }}" rel="tag">{{ tag.getTag() }}</a></span>
|
|
@ -22,8 +22,8 @@ declare(strict_types = 1);
|
|||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Entity\ActorTag;
|
||||
use App\Util\GNUsocialTestCase;
|
||||
use Component\Circle\Entity\ActorTag;
|
||||
use Functional as F;
|
||||
use Jchook\AssertThrows\AssertThrows;
|
||||
|
||||
|
@ -43,7 +43,7 @@ class ActorTest extends GNUsocialTestCase
|
|||
$tags = $actor->getSelfTags();
|
||||
$actor->setSelfTags(['foo'], $tags);
|
||||
DB::flush();
|
||||
$get_tags = fn ($tags) => F\map($tags, fn (ActorTag $t) => (string) $t);
|
||||
$get_tags = fn ($tags) => F\map($tags, fn (ActorTag $t) => $t->getTag());
|
||||
static::assertSame(['foo'], $get_tags($tags = $actor->getSelfTags()));
|
||||
$actor->setSelfTags(['bar'], $tags);
|
||||
DB::flush();
|
||||
|
|
Loading…
Reference in New Issue
Block a user