+
{% block head %}{{ identity.display_name }}{% endblock %}
diff --git a/journal/views/tag.py b/journal/views/tag.py
index 750a223d..575b1fe4 100644
--- a/journal/views/tag.py
+++ b/journal/views/tag.py
@@ -68,10 +68,11 @@ def user_tag_edit(request):
):
msg.error(request.user, _("Duplicated tag."))
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
- tag.title = tag_title
- tag.visibility = int(request.POST.get("visibility", 0))
- tag.visibility = 0 if tag.visibility == 0 else 2
- tag.save()
+ tag.update(
+ tag_title,
+ int(request.POST.get("visibility", 0)),
+ bool(request.POST.get("pinned", 0)),
+ )
msg.info(request.user, _("Tag updated."))
return redirect(
reverse(
diff --git a/locale/zh_Hans/LC_MESSAGES/django.po b/locale/zh_Hans/LC_MESSAGES/django.po
index 4c829c3b..9bd20bf4 100644
--- a/locale/zh_Hans/LC_MESSAGES/django.po
+++ b/locale/zh_Hans/LC_MESSAGES/django.po
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-06-04 10:15-0400\n"
+"POT-Creation-Date: 2024-06-04 16:48-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -667,7 +667,7 @@ msgstr "不再提示"
#: catalog/templates/_item_comments_by_episode.html:78
#: catalog/templates/_item_reviews.html:43
#: catalog/templates/podcast_episode_data.html:41
-#: common/templates/_sidebar.html:203
+#: common/templates/_sidebar.html:204
msgid "show more"
msgstr "显示更多"
@@ -1018,7 +1018,7 @@ msgstr "否"
#: journal/templates/collection.html:132
#: journal/templates/collection_edit.html:9
#: journal/templates/collection_edit.html:25 journal/templates/review.html:80
-#: journal/templates/tag_edit.html:12
+#: journal/templates/tag_edit.html:16
msgid "Edit"
msgstr "编辑"
@@ -1031,7 +1031,7 @@ msgstr "创建"
#: journal/templates/add_to_collection.html:35
#: journal/templates/collection_edit.html:38 journal/templates/comment.html:69
#: journal/templates/mark.html:147 journal/templates/review_edit.html:39
-#: journal/templates/tag_edit.html:60 users/templates/users/account.html:43
+#: journal/templates/tag_edit.html:51 users/templates/users/account.html:43
#: users/templates/users/account.html:104
#: users/templates/users/preferences.html:168
#: users/templates/users/preferences.html:193
@@ -1112,8 +1112,8 @@ msgstr "热门标签"
#: catalog/templates/discover.html:185 catalog/templates/item_base.html:236
#: catalog/templates/item_mark_list.html:54
-#: catalog/templates/item_review_list.html:50 common/templates/_sidebar.html:98
-#: common/templates/_sidebar.html:198
+#: catalog/templates/item_review_list.html:50 common/templates/_sidebar.html:99
+#: common/templates/_sidebar.html:199
#: common/templates/_sidebar_anonymous.html:43
#: common/templates/_sidebar_anonymous.html:58
#: journal/templates/collection_items.html:8 journal/templates/posts.html:45
@@ -1544,31 +1544,31 @@ msgstr "设置用户名"
msgid "approving followers manually"
msgstr "已开启关注审核"
-#: common/templates/_sidebar.html:83
+#: common/templates/_sidebar.html:84
msgid "Current Targets"
msgstr "当前目标"
-#: common/templates/_sidebar.html:96
+#: common/templates/_sidebar.html:97
msgid "Set a collection as target, its progress will show up here."
msgstr "将自己或他人的收藏单设为目标,这里就会显示进度"
-#: common/templates/_sidebar.html:109
+#: common/templates/_sidebar.html:110
msgid "Recent podcast episodes"
msgstr "近期播客节目"
-#: common/templates/_sidebar.html:144
+#: common/templates/_sidebar.html:145
msgid "Currently reading"
msgstr "正在阅读"
-#: common/templates/_sidebar.html:167
+#: common/templates/_sidebar.html:168
msgid "Currently watching"
msgstr "正在追看"
-#: common/templates/_sidebar.html:190
+#: common/templates/_sidebar.html:191
msgid "Common Tags"
msgstr "常用标签"
-#: common/templates/_sidebar.html:214
+#: common/templates/_sidebar.html:215
msgid "Recent Posts"
msgstr "近期帖文"
@@ -1722,8 +1722,8 @@ msgstr "发布到联邦宇宙"
#: journal/forms.py:25 journal/forms.py:45
#: journal/templates/collection_share.html:24
-#: journal/templates/tag_edit.html:30 journal/templates/wrapped_share.html:36
-#: users/templates/users/data.html:38 users/templates/users/data.html:130
+#: journal/templates/wrapped_share.html:36 users/templates/users/data.html:38
+#: users/templates/users/data.html:130
msgid "Visibility"
msgstr "可见性"
@@ -1756,7 +1756,7 @@ msgstr "备注"
#: journal/models/common.py:28 journal/templates/collection_share.html:35
#: journal/templates/comment.html:35 journal/templates/mark.html:93
-#: journal/templates/tag_edit.html:40 journal/templates/wrapped_share.html:43
+#: journal/templates/tag_edit.html:42 journal/templates/wrapped_share.html:43
#: users/templates/users/data.html:47 users/templates/users/data.html:139
#: users/templates/users/preferences.html:54
msgid "Public"
@@ -2493,7 +2493,7 @@ msgstr "日历"
msgid "annual summary"
msgstr "年度小结"
-#: journal/templates/profile.html:131 mastodon/api.py:680
+#: journal/templates/profile.html:131 mastodon/api.py:670
msgid "collection"
msgstr "收藏单"
@@ -2521,19 +2521,19 @@ msgstr "保存时将行首空格替换为全角"
msgid "change review date"
msgstr "指定评论日期"
-#: journal/templates/tag_edit.html:12
-msgid "Tag"
-msgstr "标签"
+#: journal/templates/tag_edit.html:32
+msgid "Pin"
+msgstr "置顶"
-#: journal/templates/tag_edit.html:51
+#: journal/templates/tag_edit.html:49
msgid "Personal"
msgstr "个人"
-#: journal/templates/tag_edit.html:57
-msgid "Personal tags are not shown to others when they view your tag list. However, if you use this tag when marking an item publicly, it might still be visible to others."
-msgstr "个人标签仅限于在个人主页的标签列表里不向他人展示,如果公开标记一个条目时使用这个标签仍会被别人看到。"
+#: journal/templates/tag_edit.html:52
+msgid "Personal tags are not shown to others when they view your tag list, unless you pin them. However, if you use this tag when marking an item publicly, it might still be visible to others."
+msgstr "个人标签不被包括在条目的公共索引中,但如果公开标记一个条目时使用这个标签仍会被别人看到。"
-#: journal/templates/tag_edit.html:67
+#: journal/templates/tag_edit.html:55
msgid "Delete this tag"
msgstr "删除这个标签"
@@ -2685,7 +2685,7 @@ msgstr "标签已删除"
msgid "Duplicated tag."
msgstr "重复标签"
-#: journal/views/tag.py:75
+#: journal/views/tag.py:76
msgid "Tag updated."
msgstr "标签已更新"
@@ -2698,11 +2698,11 @@ msgstr "总结已发布到时间轴"
msgid "regarding {item_title}, may contain spoiler or triggering content"
msgstr "关于 {item_title},可能包含剧透或敏感内容"
-#: mastodon/api.py:685
+#: mastodon/api.py:675
msgid "shared my collection"
msgstr "分享我的收藏单"
-#: mastodon/api.py:688
+#: mastodon/api.py:678
#, python-brace-format
msgid "shared {username}'s collection"
msgstr "分享 {username} 的收藏单"
@@ -2964,31 +2964,31 @@ msgstr "提及"
msgid "follow"
msgstr "关注"
-#: takahe/models.py:427
+#: takahe/models.py:430
msgid "Display Name"
msgstr "昵称"
-#: takahe/models.py:429
+#: takahe/models.py:432
msgid "Bio"
msgstr "简介"
-#: takahe/models.py:431
+#: takahe/models.py:434
msgid "Manually approve new followers"
msgstr "手工审核关注者"
-#: takahe/models.py:435
+#: takahe/models.py:438
msgid "Include profile and posts in discovery"
msgstr "允许个人资料和帖文包含在发现中"
-#: takahe/models.py:438
+#: takahe/models.py:441
msgid "Include posts in search results"
msgstr "允许个人帖文包含在搜索结果中"
-#: takahe/models.py:456
+#: takahe/models.py:459
msgid "Profile picture"
msgstr "头像"
-#: takahe/models.py:463
+#: takahe/models.py:466
msgid "Header picture"
msgstr "背景图片"
diff --git a/locale/zh_Hant/LC_MESSAGES/django.po b/locale/zh_Hant/LC_MESSAGES/django.po
index dd46c426..f7222563 100644
--- a/locale/zh_Hant/LC_MESSAGES/django.po
+++ b/locale/zh_Hant/LC_MESSAGES/django.po
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-06-04 10:15-0400\n"
+"POT-Creation-Date: 2024-06-04 16:48-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -667,7 +667,7 @@ msgstr "不再提示"
#: catalog/templates/_item_comments_by_episode.html:78
#: catalog/templates/_item_reviews.html:43
#: catalog/templates/podcast_episode_data.html:41
-#: common/templates/_sidebar.html:203
+#: common/templates/_sidebar.html:204
msgid "show more"
msgstr "顯示更多"
@@ -1018,7 +1018,7 @@ msgstr "否"
#: journal/templates/collection.html:132
#: journal/templates/collection_edit.html:9
#: journal/templates/collection_edit.html:25 journal/templates/review.html:80
-#: journal/templates/tag_edit.html:12
+#: journal/templates/tag_edit.html:16
msgid "Edit"
msgstr "編輯"
@@ -1031,7 +1031,7 @@ msgstr "創建"
#: journal/templates/add_to_collection.html:35
#: journal/templates/collection_edit.html:38 journal/templates/comment.html:69
#: journal/templates/mark.html:147 journal/templates/review_edit.html:39
-#: journal/templates/tag_edit.html:60 users/templates/users/account.html:43
+#: journal/templates/tag_edit.html:51 users/templates/users/account.html:43
#: users/templates/users/account.html:104
#: users/templates/users/preferences.html:168
#: users/templates/users/preferences.html:193
@@ -1112,8 +1112,8 @@ msgstr "熱門標籤"
#: catalog/templates/discover.html:185 catalog/templates/item_base.html:236
#: catalog/templates/item_mark_list.html:54
-#: catalog/templates/item_review_list.html:50 common/templates/_sidebar.html:98
-#: common/templates/_sidebar.html:198
+#: catalog/templates/item_review_list.html:50 common/templates/_sidebar.html:99
+#: common/templates/_sidebar.html:199
#: common/templates/_sidebar_anonymous.html:43
#: common/templates/_sidebar_anonymous.html:58
#: journal/templates/collection_items.html:8 journal/templates/posts.html:45
@@ -1544,31 +1544,31 @@ msgstr "設置用戶名"
msgid "approving followers manually"
msgstr "已開啓關注審覈"
-#: common/templates/_sidebar.html:83
+#: common/templates/_sidebar.html:84
msgid "Current Targets"
msgstr "當前目標"
-#: common/templates/_sidebar.html:96
+#: common/templates/_sidebar.html:97
msgid "Set a collection as target, its progress will show up here."
msgstr "將自己或他人的收藏單設爲目標,這裏就會顯示進度"
-#: common/templates/_sidebar.html:109
+#: common/templates/_sidebar.html:110
msgid "Recent podcast episodes"
msgstr "近期播客節目"
-#: common/templates/_sidebar.html:144
+#: common/templates/_sidebar.html:145
msgid "Currently reading"
msgstr "正在閱讀"
-#: common/templates/_sidebar.html:167
+#: common/templates/_sidebar.html:168
msgid "Currently watching"
msgstr "正在追看"
-#: common/templates/_sidebar.html:190
+#: common/templates/_sidebar.html:191
msgid "Common Tags"
msgstr "常用標籤"
-#: common/templates/_sidebar.html:214
+#: common/templates/_sidebar.html:215
msgid "Recent Posts"
msgstr "近期帖文"
@@ -1722,8 +1722,8 @@ msgstr "發佈到聯邦宇宙"
#: journal/forms.py:25 journal/forms.py:45
#: journal/templates/collection_share.html:24
-#: journal/templates/tag_edit.html:30 journal/templates/wrapped_share.html:36
-#: users/templates/users/data.html:38 users/templates/users/data.html:130
+#: journal/templates/wrapped_share.html:36 users/templates/users/data.html:38
+#: users/templates/users/data.html:130
msgid "Visibility"
msgstr "可見性"
@@ -1756,7 +1756,7 @@ msgstr "備註"
#: journal/models/common.py:28 journal/templates/collection_share.html:35
#: journal/templates/comment.html:35 journal/templates/mark.html:93
-#: journal/templates/tag_edit.html:40 journal/templates/wrapped_share.html:43
+#: journal/templates/tag_edit.html:42 journal/templates/wrapped_share.html:43
#: users/templates/users/data.html:47 users/templates/users/data.html:139
#: users/templates/users/preferences.html:54
msgid "Public"
@@ -2493,7 +2493,7 @@ msgstr "日曆"
msgid "annual summary"
msgstr "年度小結"
-#: journal/templates/profile.html:131 mastodon/api.py:680
+#: journal/templates/profile.html:131 mastodon/api.py:670
msgid "collection"
msgstr "收藏單"
@@ -2521,19 +2521,19 @@ msgstr "保存時將行首空格替換爲全角"
msgid "change review date"
msgstr "指定評論日期"
-#: journal/templates/tag_edit.html:12
-msgid "Tag"
-msgstr "標籤"
+#: journal/templates/tag_edit.html:32
+msgid "Pin"
+msgstr "置頂"
-#: journal/templates/tag_edit.html:51
+#: journal/templates/tag_edit.html:49
msgid "Personal"
msgstr "個人"
-#: journal/templates/tag_edit.html:57
-msgid "Personal tags are not shown to others when they view your tag list. However, if you use this tag when marking an item publicly, it might still be visible to others."
-msgstr "個人標籤僅限於在個人主頁的標籤列表裏不向他人展示,如果公開標記一個條目時使用這個標籤仍會被別人看到。"
+#: journal/templates/tag_edit.html:52
+msgid "Personal tags are not shown to others when they view your tag list, unless you pin them. However, if you use this tag when marking an item publicly, it might still be visible to others."
+msgstr "個人標籤不被包括在條目的公共索引中,但如果公開標記一個條目時使用這個標籤仍會被別人看到。"
-#: journal/templates/tag_edit.html:67
+#: journal/templates/tag_edit.html:55
msgid "Delete this tag"
msgstr "刪除這個標籤"
@@ -2685,7 +2685,7 @@ msgstr "標籤已刪除"
msgid "Duplicated tag."
msgstr "重複標籤"
-#: journal/views/tag.py:75
+#: journal/views/tag.py:76
msgid "Tag updated."
msgstr "標籤已更新"
@@ -2698,11 +2698,11 @@ msgstr "總結已發佈到時間軸"
msgid "regarding {item_title}, may contain spoiler or triggering content"
msgstr "關於 {item_title},可能包含劇透或敏感內容"
-#: mastodon/api.py:685
+#: mastodon/api.py:675
msgid "shared my collection"
msgstr "分享我的收藏單"
-#: mastodon/api.py:688
+#: mastodon/api.py:678
#, python-brace-format
msgid "shared {username}'s collection"
msgstr "分享 {username} 的收藏單"
@@ -2964,31 +2964,31 @@ msgstr "提及"
msgid "follow"
msgstr "關注"
-#: takahe/models.py:427
+#: takahe/models.py:430
msgid "Display Name"
msgstr "暱稱"
-#: takahe/models.py:429
+#: takahe/models.py:432
msgid "Bio"
msgstr "簡介"
-#: takahe/models.py:431
+#: takahe/models.py:434
msgid "Manually approve new followers"
msgstr "手工審覈關注者"
-#: takahe/models.py:435
+#: takahe/models.py:438
msgid "Include profile and posts in discovery"
msgstr "允許個人資料和帖文包含在發現中"
-#: takahe/models.py:438
+#: takahe/models.py:441
msgid "Include posts in search results"
msgstr "允許個人帖文包含在搜索結果中"
-#: takahe/models.py:456
+#: takahe/models.py:459
msgid "Profile picture"
msgstr "頭像"
-#: takahe/models.py:463
+#: takahe/models.py:466
msgid "Header picture"
msgstr "背景圖片"
diff --git a/takahe/migrations/0001_initial.py b/takahe/migrations/0001_initial.py
index daf13a04..212ba9a8 100644
--- a/takahe/migrations/0001_initial.py
+++ b/takahe/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 4.2.13 on 2024-06-01 05:38
+# Generated by Django 4.2.13 on 2024-06-04 19:33
import functools
@@ -102,6 +102,11 @@ class Migration(migrations.Migration):
("public", models.BooleanField(null=True)),
("state", models.CharField(default="outdated", max_length=100)),
("state_changed", models.DateTimeField(auto_now_add=True)),
+ ("state_next_attempt", models.DateTimeField(blank=True, null=True)),
+ (
+ "state_locked_until",
+ models.DateTimeField(blank=True, db_index=True, null=True),
+ ),
("stats", models.JSONField(blank=True, null=True)),
("stats_updated", models.DateTimeField(blank=True, null=True)),
("aliases", models.JSONField(blank=True, null=True)),
@@ -162,8 +167,7 @@ class Migration(migrations.Migration):
(
"indexable",
models.BooleanField(
- default=True,
- verbose_name="Include posts in search results",
+ default=True, verbose_name="Include posts in search results"
),
),
(
@@ -589,6 +593,40 @@ class Migration(migrations.Migration):
blank=True, related_name="identities", to="takahe.user"
),
),
+ migrations.CreateModel(
+ name="HashtagFeature",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("created", models.DateTimeField(auto_now_add=True)),
+ (
+ "hashtag",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="featurers",
+ to="takahe.hashtag",
+ ),
+ ),
+ (
+ "identity",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="hashtag_features",
+ to="takahe.identity",
+ ),
+ ),
+ ],
+ options={
+ "db_table": "users_hashtagfeature",
+ },
+ ),
migrations.CreateModel(
name="FanOut",
fields=[
diff --git a/takahe/models.py b/takahe/models.py
index 0faa0972..ba6526d5 100644
--- a/takahe/models.py
+++ b/takahe/models.py
@@ -5,7 +5,7 @@ import re
import secrets
import ssl
import time
-from datetime import date
+from datetime import date, timedelta
from functools import cached_property, partial
from typing import TYPE_CHECKING, Literal, Optional
from urllib.parse import urlparse
@@ -385,7 +385,10 @@ class Identity(models.Model):
Represents both local and remote Fediverse identities (actors)
"""
- domain_id: str
+ if TYPE_CHECKING:
+ domain_id: str
+ inbound_follows: "models.QuerySet[Follow]"
+ hashtag_features: "models.QuerySet[HashtagFeature]"
class Restriction(models.IntegerChoices):
none = 0
@@ -729,6 +732,34 @@ class Identity(models.Model):
self.shared_inbox_uri = f"https://{self.domain.uri_domain}/inbox/"
self.save()
+ def get_remote_targets(self):
+ """
+ Returns an iterable with Identities of followers that have unique
+ shared_inbox among each other to be used as target.
+ """
+ if not self.local:
+ return []
+ remote_follower_ids = Follow.objects.filter(
+ target=self,
+ target__local=False,
+ state__in=["unrequested", "pending_approval", "accepting", "accepted"],
+ ).values_list("source", flat=True)
+ deduped_targets = set()
+ shared_inboxes = set()
+ for target in Identity.objects.filter(pk__in=remote_follower_ids):
+ if not target.shared_inbox_uri:
+ deduped_targets.add(target)
+ elif target.shared_inbox_uri not in shared_inboxes:
+ shared_inboxes.add(target.shared_inbox_uri)
+ deduped_targets.add(target)
+ return deduped_targets
+
+ def fanout(self, type: str, **kwargs):
+ for target in self.get_remote_targets():
+ FanOut.objects.create(
+ identity=target, subject_identity=self, type=type, **kwargs
+ )
+
class Follow(models.Model):
"""
@@ -1569,6 +1600,8 @@ class Hashtag(models.Model):
# state = StateField(HashtagStates)
state = models.CharField(max_length=100, default="outdated")
state_changed = models.DateTimeField(auto_now_add=True)
+ state_next_attempt = models.DateTimeField(blank=True, null=True)
+ state_locked_until = models.DateTimeField(null=True, blank=True, db_index=True)
# Metrics for this Hashtag
stats = models.JSONField(null=True, blank=True)
@@ -1639,6 +1672,53 @@ class Hashtag(models.Model):
results[date(year, month, day)] = val
return dict(sorted(results.items(), reverse=True)[:num])
+ @property
+ def needs_update(self):
+ if self.stats_updated is None:
+ return True
+ return timezone.now() - self.stats_updated > timedelta(hours=1)
+
+ @classmethod
+ def ensure_hashtag(cls, name, update=None):
+ """
+ Properly strips/trims/lowercases the hashtag name, and makes sure a Hashtag
+ object exists in the database, and returns it.
+ """
+ name = name.strip().lstrip("#").lower()[: Hashtag.MAXIMUM_LENGTH]
+ hashtag, created = cls.objects.get_or_create(hashtag=name)
+ if created or update or hashtag.needs_update:
+ hashtag.state = "outdated"
+ hashtag.state_changed = timezone.now()
+ hashtag.state_next_attempt = None
+ hashtag.state_locked_until = None
+ hashtag.save(
+ update_fields=[
+ "state",
+ "state_changed",
+ "state_next_attempt",
+ "state_locked_until",
+ ]
+ )
+ return hashtag
+
+
+class HashtagFeature(models.Model):
+ identity = models.ForeignKey(
+ "takahe.Identity",
+ on_delete=models.CASCADE,
+ related_name="hashtag_features",
+ )
+ hashtag = models.ForeignKey(
+ "takahe.Hashtag",
+ on_delete=models.CASCADE,
+ related_name="featurers",
+ )
+
+ created = models.DateTimeField(auto_now_add=True)
+
+ class Meta:
+ db_table = "users_hashtagfeature"
+
class PostInteraction(models.Model):
"""
diff --git a/takahe/utils.py b/takahe/utils.py
index 6ff616f0..bf3d9a49 100644
--- a/takahe/utils.py
+++ b/takahe/utils.py
@@ -990,3 +990,21 @@ class Takahe:
else:
qs = qs.filter(visibility__in=[0, 1, 4])
return qs.prefetch_related("attachments", "author")
+
+ @staticmethod
+ def pin_hashtag_for_user(identity_pk: int, hashtag: str):
+ tag = Hashtag.ensure_hashtag(hashtag)
+ identity = Identity.objects.get(pk=identity_pk)
+ feature, created = identity.hashtag_features.get_or_create(hashtag=tag)
+ if created:
+ identity.fanout("tag_featured", subject_hashtag=tag)
+
+ @staticmethod
+ def unpin_hashtag_for_user(identity_pk: int, hashtag: str):
+ identity = Identity.objects.get(pk=identity_pk)
+ featured = HashtagFeature.objects.filter(
+ identity=identity, hashtag_id=hashtag
+ ).first()
+ if featured:
+ identity.fanout("tag_unfeatured", subject_hashtag_id=hashtag)
+ featured.delete()