From 5c3d561a674d3859ae41ae1c67791b365b505063 Mon Sep 17 00:00:00 2001 From: Hugo Sales Date: Sun, 28 Nov 2021 13:09:04 +0000 Subject: [PATCH] [COMPONENTS][Tag] Refactor Tag and add self tag stream --- components/Tag/Controller/Tag.php | 39 +++- components/Tag/Tag.php | 18 +- .../Tag/templates/actor_tag_feed.html.twig | 9 + ...ream.html.twig => note_tag_feed.html.twig} | 2 +- src/Core/Cache.php | 2 +- src/Entity/Actor.php | 211 +++++++++--------- src/Entity/ActorCircle.php | 23 +- src/Entity/ActorLanguage.php | 21 +- src/Entity/ActorTag.php | 18 +- templates/cards/profile/view.html.twig | 2 +- 10 files changed, 205 insertions(+), 140 deletions(-) create mode 100644 components/Tag/templates/actor_tag_feed.html.twig rename components/Tag/templates/{tag_stream.html.twig => note_tag_feed.html.twig} (90%) diff --git a/components/Tag/Controller/Tag.php b/components/Tag/Controller/Tag.php index 798adec0d4..09d2694715 100644 --- a/components/Tag/Controller/Tag.php +++ b/components/Tag/Controller/Tag.php @@ -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', ); } } diff --git a/components/Tag/Tag.php b/components/Tag/Tag.php index cc4c38db9e..666fc6c26a 100644 --- a/components/Tag/Tag.php +++ b/components/Tag/Tag.php @@ -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); diff --git a/components/Tag/templates/actor_tag_feed.html.twig b/components/Tag/templates/actor_tag_feed.html.twig new file mode 100644 index 0000000000..292d2125d4 --- /dev/null +++ b/components/Tag/templates/actor_tag_feed.html.twig @@ -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 %} diff --git a/components/Tag/templates/tag_stream.html.twig b/components/Tag/templates/note_tag_feed.html.twig similarity index 90% rename from components/Tag/templates/tag_stream.html.twig rename to components/Tag/templates/note_tag_feed.html.twig index cca76d24e8..c0173a97dc 100644 --- a/components/Tag/templates/tag_stream.html.twig +++ b/components/Tag/templates/note_tag_feed.html.twig @@ -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 %} diff --git a/src/Core/Cache.php b/src/Core/Cache.php index 7c73c4270e..ddb09e4cb2 100644 --- a/src/Core/Cache.php +++ b/src/Core/Cache.php @@ -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]); diff --git a/src/Entity/Actor.php b/src/Entity/Actor.php index 7ae2d5ccb8..1edfe4cebb 100644 --- a/src/Entity/Actor.php +++ b/src/Entity/Actor.php @@ -1,6 +1,6 @@ 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' => [ diff --git a/src/Entity/ActorCircle.php b/src/Entity/ActorCircle.php index f0403d641c..e4718ae0c7 100644 --- a/src/Entity/ActorCircle.php +++ b/src/Entity/ActorCircle.php @@ -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'], diff --git a/src/Entity/ActorLanguage.php b/src/Entity/ActorLanguage.php index 11435cd420..8d043445fe 100644 --- a/src/Entity/ActorLanguage.php +++ b/src/Entity/ActorLanguage.php @@ -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 [ diff --git a/src/Entity/ActorTag.php b/src/Entity/ActorTag.php index 1e467fbe4a..2d8de319f5 100644 --- a/src/Entity/ActorTag.php +++ b/src/Entity/ActorTag.php @@ -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'], ], ]; } diff --git a/templates/cards/profile/view.html.twig b/templates/cards/profile/view.html.twig index 22572e4044..6ede012193 100644 --- a/templates/cards/profile/view.html.twig +++ b/templates/cards/profile/view.html.twig @@ -29,7 +29,7 @@