[COMPONENTS][Tag] Refactor Tag and add self tag stream

This commit is contained in:
Hugo Sales 2021-11-28 13:09:04 +00:00 committed by Diogo Peralta Cordeiro
parent 6680772e47
commit 5c3d561a67
Signed by: diogo
GPG Key ID: 18D2D35001FBFAB0
10 changed files with 205 additions and 140 deletions

View File

@ -12,7 +12,7 @@ use Functional as F;
class Tag extends Controller
{
private function process(string|array $tag_or_tags, callable $key, string $query)
private function process(string|array $tag_or_tags, callable $key, string $query, string $template)
{
$actor = Common::actor();
$page = $this->int('page') ?: 1;
@ -26,7 +26,7 @@ class Tag extends Controller
} else {
$canonical = F\map($tag_or_tags, fn ($t) => CompTag::canonicalTag($t, $lang));
}
$notes = Cache::pagedStream(
$results = Cache::pagedStream(
key: $key($canonical),
query: $query,
query_args: ['canon' => $canonical],
@ -35,28 +35,51 @@ class Tag extends Controller
);
return [
'_template' => 'tag_stream.html.twig',
'notes' => $notes,
'_template' => $template,
'results' => $results,
'page' => $page,
];
}
public function single_tag(string $tag)
public function single_note_tag(string $tag)
{
return $this->process(
tag_or_tags: $tag,
key: fn ($canonical) => "tag-{$canonical}",
key: fn ($canonical) => "note-tag-feed-{$canonical}",
query: 'select n from note n join note_tag nt with n.id = nt.note_id where nt.canonical = :canon order by nt.created DESC, nt.note_id DESC',
template: 'note_tag_feed.html.twig',
);
}
public function multi_tags(string $tags)
public function multi_note_tags(string $tags)
{
$tags = explode(',', $tags);
return $this->process(
tag_or_tags: $tags,
key: fn ($canonical) => 'tags-' . implode('-', $canonical),
key: fn ($canonical) => 'note-tags-feed-' . implode('-', $canonical),
query: 'select n from note n join note_tag nt with n.id = nt.note_id where nt.canonical in (:canon) order by nt.created DESC, nt.note_id DESC',
template: 'note_tag_feed.html.twig',
);
}
public function single_actor_tag(string $tag)
{
return $this->process(
tag_or_tags: $tag,
key: fn ($canonical) => "actor-tag-feed-{$canonical}",
query: 'select a from actor a join actor_tag at with a.id = at.tagged where at.canonical = :canon order by at.modified DESC',
template: 'actor_tag_feed.html.twig',
);
}
public function multi_actor_tag(string $tag)
{
$tags = explode(',', $tags);
return $this->process(
tag_or_tags: $tag,
key: fn ($canonical) => 'actor-tags-feed-' . implode('-', $canonical),
query: 'select a from actor a join actor_tag at with a.id = at.tagged where at.canonical = :canon order by at.modified DESC',
template: 'actor_tag_feed.html.twig',
);
}
}

View File

@ -52,8 +52,10 @@ class Tag extends Component
public function onAddRoute($r): bool
{
$r->connect('single_tag', '/tag/{tag<' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'single_tag']);
$r->connect('multiple_tags', '/tags/{tags<(' . self::TAG_SLUG_REGEX . ',)+' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'multi_tags']);
$r->connect('single_note_tag', '/note-tag/{tag<' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'single_note_tag']);
$r->connect('multiple_note_tags', '/note-tags/{tags<(' . self::TAG_SLUG_REGEX . ',)+' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'multi_note_tags']);
$r->connect('single_actor_tag', '/actor-tag/{tag<' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'single_actor_tag']);
$r->connect('multiple_actor_tags', '/actor-tags/{tags<(' . self::TAG_SLUG_REGEX . ',)+' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'multi_actor_tags']);
return Event::next;
}
@ -80,17 +82,15 @@ class Tag extends Component
public function onRenderPlainTextNoteContent(string &$text, ?string $language = null): bool
{
if (!is_null($language)) {
$text = preg_replace_callback(self::TAG_REGEX, fn ($m) => $m[1] . self::tagLink($m[2], $language), $text);
}
$text = preg_replace_callback(self::TAG_REGEX, fn ($m) => $m[1] . self::tagLink($m[2], $language), $text);
return Event::next;
}
private static function tagLink(string $tag, string $language): string
private static function tagLink(string $tag, ?string $language): string
{
$tag = self::ensureLength($tag);
$canonical = self::canonicalTag($tag, $language);
$url = Router::url('tag', ['tag' => $canonical, 'lang' => $language]);
$url = Router::url('single_note_tag', !\is_null($language) ? ['tag' => $canonical, 'lang' => $language] : ['tag' => $canonical]);
return HTML::html(['a' => ['attrs' => ['href' => $url, 'title' => $tag, 'rel' => 'tag'], $tag]], options: ['indent' => false]);
}
@ -99,12 +99,12 @@ class Tag extends Component
return mb_substr($tag, 0, self::MAX_TAG_LENGTH);
}
public static function canonicalTag(string $tag, string $language): string
public static function canonicalTag(string $tag, ?string $language): string
{
$result = '';
foreach (Formatting::splitWords(str_replace('#', '', $tag)) as $word) {
$temp_res = null;
if (Event::handle('StemWord', [$language, $word, &$temp_res]) !== Event::stop) {
if (\is_null($language) || Event::handle('StemWord', [$language, $word, &$temp_res]) !== Event::stop) {
$temp_res = $word;
}
$result .= Formatting::slugify($temp_res);

View File

@ -0,0 +1,9 @@
{% extends 'base.html.twig' %}
{% block body %}
{% for actor in results %}
{% block profile_view %}{% include 'cards/profile/view.html.twig' %}{% endblock profile_view %}
{% endfor %}
{{ "Page: " ~ page }}
{% endblock %}

View File

@ -2,7 +2,7 @@
{% import '/cards/note/view.html.twig' as noteView %}
{% block body %}
{% for note in notes %}
{% for note in results %}
{% block current_note %}
{{ noteView.macro_note(note) }}
{% endblock current_note %}

View File

@ -372,7 +372,7 @@ abstract class Cache
$per_page = Common::config('streams', 'notes_per_page');
}
$filter_scope = fn (Note $n) => $n->isVisibleTo($actor);
$filter_scope = fn (Note|Actor $o) => $o->isVisibleTo($actor);
$getter = fn (int $offset, int $length) => DB::dql($query, $query_args, options: ['offset' => $offset, 'limit' => $length]);

View File

@ -1,6 +1,6 @@
<?php
declare(strict_types=1);
declare(strict_types = 1);
// {{{ License
@ -29,15 +29,13 @@ use App\Core\Entity;
use App\Core\Event;
use App\Core\Router\Router;
use App\Core\UserRoles;
use App\Util\Common;
use App\Util\Exception\NicknameException;
use App\Util\Exception\NotFoundException;
use App\Util\Nickname;
use Component\Avatar\Avatar;
use Component\Tag\Tag as TagComponent;
use DateTimeInterface;
use Functional as F;
use function in_array;
use function is_null;
/**
* Entity for actors
@ -60,7 +58,7 @@ class Actor extends Entity
private int $id;
private string $nickname;
private ?string $fullname = null;
private int $roles = 4;
private int $roles = 4;
private ?string $homepage;
private ?string $bio;
private ?string $location;
@ -102,7 +100,7 @@ class Actor extends Entity
public function getFullname(): ?string
{
if (is_null($this->fullname)) {
if (\is_null($this->fullname)) {
return null;
}
return $this->fullname;
@ -254,17 +252,17 @@ class Actor extends Entity
public static function getById(int $id): ?self
{
return Cache::get('actor-id-' . $id, fn() => DB::find('actor', ['id' => $id]));
return Cache::get('actor-id-' . $id, fn () => DB::find('actor', ['id' => $id]));
}
public static function getNicknameById(int $id): string
{
return Cache::get('actor-nickname-id-' . $id, fn() => self::getById($id)->getNickname());
return Cache::get('actor-nickname-id-' . $id, fn () => self::getById($id)->getNickname());
}
public static function getFullnameById(int $id): ?string
{
return Cache::get('actor-fullname-id-' . $id, fn() => self::getById($id)->getFullname());
return Cache::get('actor-fullname-id-' . $id, fn () => self::getById($id)->getFullname());
}
/**
@ -280,91 +278,92 @@ class Actor extends Entity
/**
* Get tags that other people put on this actor, in reverse-chron order
*
* @param Actor|int|null $scoped Actor we are requesting as:
* @param null|Actor|int $scoped Actor we are requesting as:
* - If null = All tags attributed to self by other actors (excludes self tags)
* - If self = Same as getSelfTags
* - otherwise = Tags that $scoped attributed to $this
* @param int|null $offset Offset from latest
* @param int|null $limit Max number to get
* @param bool $_test_force_recompute
* @param null|int $offset Offset from latest
* @param null|int $limit Max number to get
*
* @return [ActorCircle] resulting lists
*/
public function getOtherTags(Actor|int|null $scoped = null, ?int $offset = null, ?int $limit = null, bool $_test_force_recompute = false): array
public function getOtherTags(self|int|null $scoped = null, ?int $offset = null, ?int $limit = null, bool $_test_force_recompute = false): array
{
if (is_null($scoped)) {
if (\is_null($scoped)) {
return Cache::get(
"othertags-{$this->getId()}",
fn() => DB::dql(
<<< EOQ
SELECT circle
FROM App\Entity\ActorTag tag
JOIN App\Entity\ActorCircle circle
WITH
tag.tagger = circle.tagger
AND tag.tag = circle.tag
WHERE tag.tagged = :id
ORDER BY tag.modified DESC, tag.tagged DESC
EOQ,
fn () => DB::dql(
<<< 'EOQ'
SELECT circle
FROM App\Entity\ActorTag tag
JOIN App\Entity\ActorCircle circle
WITH
tag.tagger = circle.tagger
AND tag.tag = circle.tag
WHERE tag.tagged = :id
ORDER BY tag.modified DESC, tag.tagged DESC
EOQ,
['id' => $this->getId()],
['offset' => $offset,
'limit' => $limit])
['offset' => $offset,
'limit' => $limit, ],
),
);
} else {
$scoped_id = is_int($scoped) ? $scoped : $scoped->getId();
$scoped_id = \is_int($scoped) ? $scoped : $scoped->getId();
return Cache::get(
"othertags-{$this->getId()}-by-{$scoped_id}",
fn() => DB::dql(
<<< EOQ
SELECT circle
FROM App\Entity\ActorTag tag
JOIN App\Entity\ActorCircle circle
WITH
tag.tagger = circle.tagger
AND tag.tag = circle.tag
WHERE
tag.tagged = :id
AND ( circle.private != true
OR ( circle.tagger = :scoped
AND circle.private = true
)
)
ORDER BY tag.modified DESC, tag.tagged DESC
EOQ,
['id' => $this->getId(),
'scoped' => $scoped_id],
['offset' => $offset,
'limit' => $limit]
)
fn () => DB::dql(
<<< 'EOQ'
SELECT circle
FROM App\Entity\ActorTag tag
JOIN App\Entity\ActorCircle circle
WITH
tag.tagger = circle.tagger
AND tag.tag = circle.tag
WHERE
tag.tagged = :id
AND ( circle.private != true
OR ( circle.tagger = :scoped
AND circle.private = true
)
)
ORDER BY tag.modified DESC, tag.tagged DESC
EOQ,
['id' => $this->getId(),
'scoped' => $scoped_id, ],
['offset' => $offset,
'limit' => $limit, ],
),
);
}
}
/**
* @param array $tags array of strings to become self tags
* @param array|null $existing array of existing self tags (actor_circle[])
* @return $this
* @throws NotFoundException
* @param array $tags array of strings to become self tags
* @param null|array $existing array of existing self tags (actor_circle[])
*
* @throws \App\Util\Exception\DuplicateFoundException
* @throws NotFoundException
*
* @return $this
*/
public function setSelfTags(array $tags, ?array $existing = null): self
{
if (is_null($existing)) {
if (\is_null($existing)) {
$existing = $this->getSelfTags();
}
$existing_actor_circles = F\map($existing, fn($actor_circle) => $actor_circle->getTag());
$tags_to_add = array_diff($tags, $existing_actor_circles);
$tags_to_remove = array_diff($existing_actor_circles, $tags);
$actor_circles_to_remove = F\filter($existing, fn($actor_circle) => in_array($actor_circle->getTag(), $tags_to_remove));
$existing_actor_circles = F\map($existing, fn ($actor_circle) => $actor_circle->getTag());
$tags_to_add = array_diff($tags, $existing_actor_circles);
$tags_to_remove = array_diff($existing_actor_circles, $tags);
$actor_circles_to_remove = F\filter($existing, fn ($actor_circle) => \in_array($actor_circle->getTag(), $tags_to_remove));
foreach ($tags_to_add as $tag) {
$actor_circle = ActorCircle::create(['tagger' => $this->getId(), 'tag' => $tag, 'private' => false]);
$actor_tag = ActorTag::create(['tagger' => $this->id, 'tagged' => $this->id, 'tag' => $tag]);
DB::persist($actor_circle);
DB::persist($actor_tag);
$canonical_tag = TagComponent::canonicalTag($tag, $this->getTopLanguage()->getLocale());
DB::persist(ActorCircle::create(['tagger' => $this->getId(), 'tag' => $canonical_tag, 'private' => false]));
DB::persist(ActorTag::create(['tagger' => $this->id, 'tagged' => $this->id, 'tag' => $tag, 'canonical' => $canonical_tag]));
}
foreach ($actor_circles_to_remove as $actor_circle) {
$actor_tag = DB::findOneBy('actor_tag', ['tagger' => $this->getId(), 'tagged' => $this->getId(), 'tag' => $actor_circle->getTag()]);
DB::persist($actor_tag);
DB::remove($actor_tag);
$canonical_tag = TagComponent::canonicalTag($actor_circle->getTag(), $this->getTopLanguage()->getLocale());
DB::removeBy('actor_tag', ['tagger' => $this->getId(), 'tagged' => $this->getId(), 'canonical' => $canonical_tag]);
DB::removeBy('actor_circle', ['id' => $actor_circle->getId()]);
}
Cache::delete("selftags-{$this->getId()}");
@ -378,9 +377,9 @@ class Actor extends Entity
'subscribers-' . $this->id,
function () {
return DB::dql(
'select count(f) from App\Entity\Subscription f where f.subscribed = :subscribed',
['subscribed' => $this->id],
)[0][1] - 1; // Remove self subscription
'select count(f) from App\Entity\Subscription f where f.subscribed = :subscribed',
['subscribed' => $this->id],
)[0][1] - 1; // Remove self subscription
},
);
}
@ -391,9 +390,9 @@ class Actor extends Entity
'subscribed-' . $this->id,
function () {
return DB::dql(
'select count(f) from App\Entity\Subscription f where f.subscriber = :subscriber',
['subscriber' => $this->id],
)[0][1] - 1; // Remove self subscription
'select count(f) from App\Entity\Subscription f where f.subscriber = :subscriber',
['subscriber' => $this->id],
)[0][1] - 1; // Remove self subscription
},
);
}
@ -419,16 +418,16 @@ class Actor extends Entity
$nickname = Nickname::normalize($nickname, check_already_used: false);
return Cache::get(
'relative-nickname-' . $nickname . '-' . $this->getId(),
fn() => DB::dql(
<<<'EOF'
fn () => DB::dql(
<<<'EOF'
select a from actor a where
a.id in (select fa.subscribed from subscription fa join actor aa with fa.subscribed = aa.id where fa.subscriber = :actor_id and aa.nickname = :nickname) or
a.id in (select fb.subscriber from subscription fb join actor ab with fb.subscriber = ab.id where fb.subscribed = :actor_id and ab.nickname = :nickname) or
a.nickname = :nickname
EOF,
['nickname' => $nickname, 'actor_id' => $this->getId()],
['limit' => 1],
)[0] ?? null,
['nickname' => $nickname, 'actor_id' => $this->getId()],
['limit' => 1],
)[0] ?? null,
);
}
@ -471,6 +470,11 @@ class Actor extends Entity
return $aliases;
}
public function getTopLanguage(): Language
{
return ActorLanguage::getActorLanguages($this, context: null)[0];
}
/**
* Get the most appropriate language for $this to use when
* referring to $context (a reply or a group, for instance)
@ -479,43 +483,38 @@ class Actor extends Entity
*/
public function getPreferredLanguageChoices(?self $context = null): array
{
$id = $context?->getId() ?? $this->getId();
$key = ActorLanguage::collectionCacheKey($this, $context);
$langs = Cache::getList(
$key,
fn() => DB::dql(
'select l from actor_language al join language l with al.language_id = l.id where al.actor_id = :id order by al.ordering ASC',
['id' => $id],
),
) ?: [
Language::getFromLocale(Common::config('site', 'language')),
];
return array_merge(...F\map($langs, fn($l) => $l->toChoiceFormat()));
$langs = ActorLanguage::getActorLanguages($this, context: $context);
return array_merge(...F\map($langs, fn ($l) => $l->toChoiceFormat()));
}
public function isVisibleTo(self $other): bool
{
return true; // TODO
}
public static function schemaDef(): array
{
return [
'name' => 'actor',
'name' => 'actor',
'description' => 'local and remote users, groups and bots are actors, for instance',
'fields' => [
'id' => ['type' => 'serial', 'not null' => true, 'description' => 'unique identifier'],
'nickname' => ['type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'nickname or username'],
'fullname' => ['type' => 'text', 'description' => 'display name'],
'roles' => ['type' => 'int', 'not null' => true, 'default' => UserRoles::USER, 'description' => 'Bitmap of permissions this actor has'],
'homepage' => ['type' => 'text', 'description' => 'identifying URL'],
'bio' => ['type' => 'text', 'description' => 'descriptive biography'],
'location' => ['type' => 'text', 'description' => 'physical location'],
'lat' => ['type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'latitude'],
'lon' => ['type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'longitude'],
'location_id' => ['type' => 'int', 'description' => 'location id if possible'],
'fields' => [
'id' => ['type' => 'serial', 'not null' => true, 'description' => 'unique identifier'],
'nickname' => ['type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'nickname or username'],
'fullname' => ['type' => 'text', 'description' => 'display name'],
'roles' => ['type' => 'int', 'not null' => true, 'default' => UserRoles::USER, 'description' => 'Bitmap of permissions this actor has'],
'homepage' => ['type' => 'text', 'description' => 'identifying URL'],
'bio' => ['type' => 'text', 'description' => 'descriptive biography'],
'location' => ['type' => 'text', 'description' => 'physical location'],
'lat' => ['type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'latitude'],
'lon' => ['type' => 'numeric', 'precision' => 10, 'scale' => 7, 'description' => 'longitude'],
'location_id' => ['type' => 'int', 'description' => 'location id if possible'],
'location_service' => ['type' => 'int', 'description' => 'service used to obtain location id'],
'is_local' => ['type' => 'bool', 'not null' => true, 'description' => 'Does this actor have a LocalUser associated'],
'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'],
'is_local' => ['type' => 'bool', 'not null' => true, 'description' => 'Does this actor have a LocalUser associated'],
'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'],
'indexes' => [
'indexes' => [
'actor_nickname_idx' => ['nickname'],
],
'fulltext indexes' => [

View File

@ -53,7 +53,7 @@ class ActorCircle extends Entity
private DateTimeInterface $created;
private DateTimeInterface $modified;
public function setId(int $id): ActorCircle
public function setId(int $id): self
{
$this->id = $id;
return $this;
@ -133,22 +133,29 @@ class ActorCircle extends Entity
// @codeCoverageIgnoreEnd
// }}} Autocode
public function getActorTag()
{
return Cache::get(
"actor-tag-{$this->getTag()}",
fn () => DB::findBy('actor_tag', ['tagger' => $this->getTagger(), 'canonical' => $this->getTag()], limit: 1)[0], // TODO jank
);
}
public function getSubscribedActors(?int $offset = null, ?int $limit = null): array
{
return Cache::get(
"circle-{$this->getId()}",
fn() => DB::dql(
<<< EOQ
fn () => DB::dql(
<<< 'EOQ'
SELECT a
FROM App\Entity\Actor a
JOIN App\Entity\ActorCircleSubscription 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, ],
),
);
}
@ -160,7 +167,7 @@ class ActorCircle extends Entity
'fields' => [
'id' => ['type' => 'serial', 'not null' => true, 'description' => 'unique identifier'],
'tagger' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'many to one', 'name' => 'actor_list_tagger_fkey', 'not null' => true, 'description' => 'user making the 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
'tag' => ['type' => 'varchar', 'length' => 64, 'foreign key' => true, 'target' => 'ActorTag.canonical', '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
'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'],

View File

@ -23,8 +23,10 @@ declare(strict_types = 1);
namespace App\Entity;
use App\Core\Cache;
use App\Core\DB\DB;
use App\Core\Entity;
use App\Util\Common;
/**
* Entity for actor languages
@ -79,9 +81,9 @@ class ActorLanguage extends Entity
// @codeCoverageIgnoreEnd
// }}} Autocode
public static function collectionCacheKey(LocalUser|Actor $actor, ?Actor $content = null)
public static function collectionCacheKey(LocalUser|Actor $actor, ?Actor $context = null)
{
return 'actor-' . $actor->getId() . '-langs' . (!\is_null($content) ? '-cxt-' . $content->getId() : '');
return 'actor-' . $actor->getId() . '-langs' . (!\is_null($context) ? '-cxt-' . $context->getId() : '');
}
public static function normalizeOrdering(LocalUser|Actor $actor)
@ -94,6 +96,21 @@ class ActorLanguage extends Entity
}
}
/**
* @return self[]
*/
public static function getActorLanguages(LocalUser|Actor $actor, ?Actor $context): array
{
$id = $context?->getId() ?? $actor->getId();
return Cache::getList(
self::collectionCacheKey($actor, context: $context),
fn () => DB::dql(
'select l from actor_language al join language l with al.language_id = l.id where al.actor_id = :id order by al.ordering ASC',
['id' => $id],
),
) ?: [Language::getFromLocale(Common::config('site', 'language'))];
}
public static function schemaDef(): array
{
return [

View File

@ -23,6 +23,7 @@ namespace App\Entity;
use App\Core\DB\DB;
use App\Core\Entity;
use App\Core\Router\Router;
use Component\Tag\Tag;
use DateTimeInterface;
@ -108,6 +109,15 @@ class ActorTag extends Entity
// @codeCoverageIgnoreEnd
// }}} Autocode
public function getUrl(?Actor $actor = null): string
{
$params = ['tag' => $this->getCanonical()];
if (!\is_null($actor)) {
$params['lang'] = $actor->getTopLanguage()->getLocale();
}
return Router::url('single_actor_tag', $params);
}
public static function schemaDef(): array
{
return [
@ -119,11 +129,11 @@ class ActorTag extends Entity
'canonical' => ['type' => 'varchar', 'length' => Tag::MAX_TAG_LENGTH, 'not null' => true, 'description' => 'ascii slug of tag'],
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
],
'primary key' => ['tagger', 'tagged', 'tag'],
'primary key' => ['tagger', 'tagged', 'canonical'],
'indexes' => [
'actor_tag_modified_idx' => ['modified'],
'actor_tag_tagger_tag_idx' => ['tagger', 'tag'], // For Circles
'actor_tag_tagged_idx' => ['tagged'],
'actor_tag_modified_idx' => ['modified'],
'actor_tag_tagger_canonical_idx' => ['tagger', 'canonical'], // For Circles
'actor_tag_tagged_idx' => ['tagged'],
],
];
}

View File

@ -29,7 +29,7 @@
<nav class="profile-info-tags">
{% if actor_tags %}
{% for tag in actor_tags %}
<a href='#'><em>#{{ tag.getTag() }}</em></a>
<a href="{{ tag.getActorTag().getUrl(actor) }}"><em>#{{ tag.getTag() }}</em></a>
{% endfor %}
{% else %}
{{ '(No tags)' | trans }}