diff --git a/CHANGELOG_YOJO.md b/CHANGELOG_YOJO.md
index f47e502fcd..0e52f14901 100644
--- a/CHANGELOG_YOJO.md
+++ b/CHANGELOG_YOJO.md
@@ -33,6 +33,7 @@
- リモートユーザー高度な検索画面で照会しますか?のダイアログが出ない問題
- ユーザー検索画面で照会しますか?のダイアログが2つ出る問題
- Fix: 更新情報を確認のCherryPickの項目へのリンクを修正
+- Feat: お気に入りのタグリストを作成できるように
### Server
- Fix: ユーザーnull(System)の場合forceがfalseでも新規追加されるのを修正
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 65808d30ba..ecef123d81 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5575,6 +5575,14 @@ export interface Locale extends ILocale {
* プロフィールを翻訳する
*/
"translateProfile": string;
+ /**
+ * タグ名を入力
+ */
+ "enterTagName": string;
+ /**
+ * タグに使用できない文字が含まれています
+ */
+ "invalidTagName": string;
"_official_tag": {
/**
* 公式タグ
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 2d5350f859..0fc3f38434 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1388,6 +1388,8 @@ additionalPermissionsForFlash: "Playへの追加許可"
thisFlashRequiresTheFollowingPermissions: "このPlayは以下の権限を要求しています"
doYouWantToAllowThisPlayToAccessYourAccount: "このPlayによるアカウントへのアクセスを許可しますか?"
translateProfile: "プロフィールを翻訳する"
+enterTagName: "タグ名を入力"
+invalidTagName: "タグに使用できない文字が含まれています"
_official_tag:
title: "公式タグ"
diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts
index 46a01d47f8..9ed393da56 100644
--- a/packages/frontend/src/navbar.ts
+++ b/packages/frontend/src/navbar.ts
@@ -93,6 +93,12 @@ export const navbarItemDef = reactive({
show: computed(() => $i != null),
to: '/my/antennas',
},
+ tags: {
+ title: i18n.ts.tags,
+ icon: 'ti ti-hash',
+ show: computed(() => $i != null),
+ to: '/my/tags',
+ },
favorites: {
title: i18n.ts.favorites,
icon: 'ti ti-star',
diff --git a/packages/frontend/src/pages/my-tags/index.vue b/packages/frontend/src/pages/my-tags/index.vue
new file mode 100644
index 0000000000..107826e9f6
--- /dev/null
+++ b/packages/frontend/src/pages/my-tags/index.vue
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue
index 27a35d35cb..f5fa4400ce 100644
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ b/packages/frontend/src/pages/settings/preferences-backups.vue
@@ -127,6 +127,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'enableListTimeline',
'enableAntennaTimeline',
'enableMediaTimeline',
+ 'enableTagTimeline',
'useEnterToSend',
'postFormVisibilityHotkey',
'showRenoteConfirmPopup',
diff --git a/packages/frontend/src/pages/settings/timeline.vue b/packages/frontend/src/pages/settings/timeline.vue
index 7e26b680b7..8cee762924 100644
--- a/packages/frontend/src/pages/settings/timeline.vue
+++ b/packages/frontend/src/pages/settings/timeline.vue
@@ -20,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.lists }}
{{ i18n.ts.antennas }}
+ {{ i18n.ts.tags }}
@@ -59,6 +60,7 @@ const enableGlobalTimeline = computed(defaultStore.makeGetterSetter('enableGloba
const enableListTimeline = computed(defaultStore.makeGetterSetter('enableListTimeline'));
const enableAntennaTimeline = computed(defaultStore.makeGetterSetter('enableAntennaTimeline'));
const enableMediaTimeline = computed(defaultStore.makeGetterSetter('enableMediaTimeline'));
+const enableTagTimeline = computed(defaultStore.makeGetterSetter('enableTagTimeline'));
const headerActions = computed(() => []);
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 3336c49f10..880911df71 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -230,6 +230,47 @@ async function chooseAntenna(ev: MouseEvent): Promise {
os.popupMenu(items, ev.currentTarget ?? ev.target);
}
+async function chooseHashTag(ev: MouseEvent): Promise {
+ let tags: string[];
+ try {
+ tags = await misskeyApi('i/registry/get', {
+ scope: ['client', 'base'],
+ key: 'hashTag',
+ });
+ } catch (err) {
+ if (err.code === 'NO_SUCH_KEY') {
+ tags = [];
+ await misskeyApi('i/registry/set', {
+ scope: ['client', 'base'],
+ key: 'hashTag',
+ value: [],
+ });
+ tags = await misskeyApi('i/registry/get', {
+ scope: ['client', 'base'],
+ key: 'hashTag',
+ });
+ } else {
+ throw err;
+ }
+ }
+
+ const items: MenuItem[] = [
+ ...tags.map(tag => ({
+ type: 'link' as const,
+ text: tag,
+ to: `/tags/${encodeURIComponent(tag)}`,
+ })),
+ (tags.length === 0 ? undefined : { type: 'divider' }),
+ {
+ type: 'link' as const,
+ icon: 'ti ti-plus',
+ text: i18n.ts.createNew,
+ to: '/my/tags',
+ },
+ ];
+ os.popupMenu(items, ev.currentTarget ?? ev.target);
+}
+
async function chooseChannel(ev: MouseEvent): Promise {
const channels = await favoritedChannelsCache.fetch();
const items: MenuItem[] = [
@@ -380,6 +421,11 @@ const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserList
title: i18n.ts.antennas,
iconOnly: true,
onClick: chooseAntenna,
+}] : []), ...(defaultStore.state.enableTagTimeline ? [{
+ icon: 'ti ti-hash',
+ title: i18n.ts.tags,
+ iconOnly: true,
+ onClick: chooseHashTag,
}] : [])] as Tab[]);
const headerTabsWhenNotLogin = computed(() => [...availableBasicTimelines().map(tl => ({
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index a207099c92..17e639627c 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -252,7 +252,7 @@ const routes: RouteDef[] = [{
origin: 'origin',
},
}, {
- // Legacy Compatibility
+ // Legacy Compatibility
path: '/authorize-follow',
redirect: '/lookup',
loginRequired: true,
@@ -552,6 +552,10 @@ const routes: RouteDef[] = [{
path: '/my/clips',
component: page(() => import('@/pages/my-clips/index.vue')),
loginRequired: true,
+}, {
+ path: '/my/tags',
+ component: page(() => import('@/pages/my-tags/index.vue')),
+ loginRequired: true,
}, {
path: '/my/antennas/create',
component: page(() => import('@/pages/my-antennas/create.vue')),
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index bd018ef8f8..87af7c6e64 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -632,6 +632,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: true,
},
+ enableTagTimeline: {
+ where: 'device',
+ default: true,
+ },
// - Settings/Sounds & Vibrations
vibrate: {