diff --git a/CHANGELOG.md b/CHANGELOG.md index 00099b6e36..215154efd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,11 @@ - Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正 ### Client -- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正 -- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968) +- メインタイムラインのタブをカスタマイズできるように ### Server - チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正 -- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949) -- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036) + ## 2024.5.0 @@ -317,6 +315,7 @@ - Enhance: つながりの公開範囲をフォロー/フォロワーで個別に設定可能に #12072 - Enhance: ローカリゼーションの更新 - Enhance: 依存関係の更新 +- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正 ### Client - Feat: 今日誕生日のフォロー中のユーザーを一覧表示できるウィジェットを追加 @@ -534,10 +533,6 @@ ## 2023.10.1 ### General - Enhance: ローカルタイムライン、ソーシャルタイムラインで返信を含むかどうか設定可能に -- Feat: 絵文字申請を追加 - - これによって絵文字リクエストロールが追加されました。 - - カスタム絵文字管理の画面に 申請されている絵文字 タブが追加されました。 - - カスタム絵文字のリクエストボタンが実装されました。 ### Client - Fix: 絵文字ピッカーで横に長いカスタム絵文字が見切れる問題を修正 @@ -581,7 +576,6 @@ - Enhance: 動画再生時のデフォルトボリュームを30%に - Fix: リアクションしたユーザ一覧のUIが稀に左上に残ってしまう不具合を修正 - ### Server - Enhance: drive/files/attached-notes がページネーションに対応しました - Enhance: タイムライン取得時のパフォーマンスを大幅に向上 diff --git a/locales/index.d.ts b/locales/index.d.ts index 90fed35357..a2e16b82a6 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -2260,6 +2260,10 @@ export interface Locale extends ILocale { * アカウント設定 */ "accountSettings": string; + /** + * タイムラインのヘッダー + */ + "timelineHeader": string; /** * プロモーション */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 444c6a34a8..39bbda5f70 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -562,6 +562,7 @@ dayOverDayChanges: "前日比" appearance: "アピアランス" clientSettings: "クライアント設定" accountSettings: "アカウント設定" +timelineHeader: "タイムラインのヘッダー" promotion: "プロモーション" promote: "プロモート" numberOfDays: "日数" diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index 0c5424c4ae..7261343674 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -114,6 +114,11 @@ const menuDef = computed(() => [{ text: i18n.ts.navbar, to: '/settings/navbar', active: currentPage.value?.route.name === 'navbar', + }, { + icon: 'ti ti-layout-navbar', + text: i18n.ts.timelineHeader, + to: '/settings/timelineheader', + active: currentPage.value?.route.name === 'timelineHeader', }, { icon: 'ti ti-equal-double', text: i18n.ts.statusbar, diff --git a/packages/frontend/src/pages/settings/timelineHeader.vue b/packages/frontend/src/pages/settings/timelineHeader.vue new file mode 100644 index 0000000000..16ec626dfe --- /dev/null +++ b/packages/frontend/src/pages/settings/timelineHeader.vue @@ -0,0 +1,153 @@ + + + + + + diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 5e6a9bfe44..84e82bbbe8 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -55,11 +55,11 @@ import { deviceKind } from '@/scripts/device-kind.js'; import { deepMerge } from '@/scripts/merge.js'; import { MenuItem } from '@/types/menu.js'; import { miLocalStorage } from '@/local-storage.js'; +import { timelineHeaderItemDef } from '@/timeline-header.js'; +import { isLocalTimelineAvailable, isGlobalTimelineAvailable } from '@/scripts/get-timeline-available.js'; provide('shouldOmitHeaderTitle', true); -const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable && defaultStore.state.showLocalTimeline) || ($i != null && $i.policies.ltlAvailable && defaultStore.state.showLocalTimeline); -const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable && defaultStore.state.showGlobalTimeline) || ($i != null && $i.policies.gtlAvailable && defaultStore.state.showGlobalTimeline); const keymap = { 't': focus, }; @@ -69,7 +69,7 @@ const rootEl = shallowRef(); const queue = ref(0); const srcWhenNotSignin = ref<'local' | 'global'>(isLocalTimelineAvailable ? 'local' : 'global'); -const src = computed<'home' | 'local' | 'social' | 'global' | `list:${string}`| `channel:${string}`>({ +const src = computed<'home' | 'local' | 'social' | 'global' | `list:${string}`>({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value), set: (x) => saveSrc(x), }); @@ -136,7 +136,6 @@ if (src.value.split(':')[0] === 'channel') { } watch(src, async () => { queue.value = 0; - if (src.value.split(':')[0] === 'channel') { const channelId = src.value.split(':')[1]; channelInfo.value = await misskeyApi('channels/show', { channelId }); @@ -145,6 +144,11 @@ watch(src, async () => { } }); +watch(withSensitive, () => { + // これだけはクライアント側で完結する処理なので手動でリロード + tlComponent.value?.reloadTimeline(); +}); + function queueUpdated(q: number): void { queue.value = q; } @@ -161,7 +165,8 @@ async function chooseList(ev: MouseEvent): Promise { ... lists.map(list => ({ type: 'link' as const, text: list.name, - to: `/timeline/list/${list.id}` })), + to: `/timeline/list/${list.id}`, + })), (lists.length === 0 ? undefined : { type: 'divider' }), { type: 'link' as const, @@ -297,83 +302,36 @@ const headerActions = computed(() => { } return tmp; }); +const headerTabs = computed(() => defaultStore.reactiveState.timelineHeader.value.map(tab => { + if ((tab === 'local' || tab === 'social') && !isLocalTimelineAvailable) { + return {}; + } else if (tab === 'global' && !isGlobalTimelineAvailable) { + return {}; + } -const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({ - key: 'list:' + l.id, - title: l.name, - icon: 'ti ti-star', - iconOnly: defaultStore.state.topBarNameShown ?? false, -}))), ...(defaultStore.reactiveState.pinnedChannels.value.map(l => ({ - key: 'channel:' + l.id, - title: l.name, - icon: 'ti ti-star', - iconOnly: defaultStore.state.topBarNameShown ?? false, -}))), ...(showHomeTimeline.value ? [{ - key: 'home', - title: i18n.ts._timelines.home, - icon: 'ti ti-home', - iconOnly: defaultStore.state.topBarNameShown ?? false, -}] : []), ...(isLocalTimelineAvailable ? [{ - key: 'local', - title: i18n.ts._timelines.local, - icon: 'ti ti-planet', - iconOnly: defaultStore.state.topBarNameShown ?? false, -}] : []), ...(isShowMediaTimeline.value ? [{ - key: 'media', - title: i18n.ts._timelines.media, - icon: 'ti ti-photo', - iconOnly: defaultStore.state.topBarNameShown ?? false, -}] : []), ...(showSocialTimeline.value ? [{ - key: 'social', - title: i18n.ts._timelines.social, - icon: 'ti ti-universe', - iconOnly: defaultStore.state.topBarNameShown ?? false, -}] : []), ...(remoteLocalTimelineEnable1.value ? [{ - key: 'custom-timeline-1', - title: defaultStore.state.remoteLocalTimelineName1, - icon: 'ti ti-plus', - iconOnly: defaultStore.state.topBarNameShown ?? false, -}] : []), ...(remoteLocalTimelineEnable2.value ? [{ - key: 'custom-timeline-2', - title: defaultStore.state.remoteLocalTimelineName2, - icon: 'ti ti-plus', - iconOnly: defaultStore.state.topBarNameShown ?? false, -}] : []), ...(remoteLocalTimelineEnable3.value ? [{ - key: 'custom-timeline-3', - title: defaultStore.state.remoteLocalTimelineName3, - icon: 'ti ti-plus', - iconOnly: defaultStore.state.topBarNameShown ?? false, -}] : []), ...(remoteLocalTimelineEnable4.value ? [{ - key: 'custom-timeline-4', - title: defaultStore.state.remoteLocalTimelineName4, - icon: 'ti ti-plus', - iconOnly: defaultStore.state.topBarNameShown ?? false, -}] : []), ...(remoteLocalTimelineEnable5.value ? [{ - key: 'custom-timeline-5', - title: defaultStore.state.remoteLocalTimelineName5, - icon: 'ti ti-plus', - iconOnly: defaultStore.state.topBarNameShown ?? false, -}] : []), ...(isGlobalTimelineAvailable ? [{ - key: 'global', - title: i18n.ts._timelines.global, - icon: 'ti ti-whirl', - iconOnly: defaultStore.state.topBarNameShown ?? false, -}] : []), { - icon: 'ti ti-list', - title: i18n.ts.lists, - iconOnly: true, - onClick: chooseList, -}, { - icon: 'ti ti-antenna', - title: i18n.ts.antennas, - iconOnly: true, - onClick: chooseAntenna, -}, { - icon: 'ti ti-device-tv', - title: i18n.ts.channel, - iconOnly: true, - onClick: chooseChannel, -}] as Tab[]); + const tabDef = timelineHeaderItemDef[tab]; + if (!tabDef) { + return {}; + } + + return { + ...(!['channels', 'antennas', 'lists'].includes(tab) ? { + key: tab, + } : {}), + title: tabDef.title, + icon: tabDef.icon, + iconOnly: tabDef.iconOnly, + ...(tab === 'lists' ? { + onClick: (ev) => chooseList(ev), + } : {}), + ...(tab === 'antennas' ? { + onClick: (ev) => chooseAntenna(ev), + } : {}), + ...(tab === 'channels' ? { + onClick: (ev) => chooseChannel(ev), + } : {}), + }; +}) as Tab[]); const headerTabsWhenNotLogin = computed(() => [ ...(isLocalTimelineAvailable ? [{ @@ -398,31 +356,31 @@ definePageMetadata(() => ({ diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index 8a443f627b..360930b363 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -112,6 +112,10 @@ const routes: RouteDef[] = [{ path: '/navbar', name: 'navbar', component: page(() => import('@/pages/settings/navbar.vue')), + }, { + path: '/timeline-header', + name: 'timelineHeader', + component: page(() => import('@/pages/settings/timelineHeader.vue')), }, { path: '/statusbar', name: 'statusbar', diff --git a/packages/frontend/src/scripts/get-timeline-available.ts b/packages/frontend/src/scripts/get-timeline-available.ts new file mode 100644 index 0000000000..30197441ea --- /dev/null +++ b/packages/frontend/src/scripts/get-timeline-available.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { $i } from '@/account.js'; +import { instance } from '@/instance.js'; +export const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable); +export const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable); diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 7fb428802c..a4b29de810 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -9,8 +9,7 @@ import { miLocalStorage } from './local-storage.js'; import type { SoundType } from '@/scripts/sound.js'; import { Storage } from '@/pizzax.js'; import { hemisphere } from '@/scripts/intl-const.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; - +import { isLocalTimelineAvailable, isGlobalTimelineAvailable } from '@/scripts/get-timeline-available.js'; interface PostFormAction { title: string, handler: (form: T, update: (key: unknown, value: unknown) => void) => void; @@ -53,7 +52,6 @@ export type SoundStore = { volume: number; } -import { instance } from '@/instance.js'; export const postFormActions: PostFormAction[] = []; export const userActions: UserAction[] = []; export const noteActions: NoteAction[] = []; @@ -238,6 +236,22 @@ export const defaultStore = markRaw(new Storage('base', { 'cacheclear', ], }, + timelineHeader: { + where: 'deviceAccount', + default: [ + 'home', + ...(isLocalTimelineAvailable ? [ + 'local', + 'social', + ] : []), + ...(isGlobalTimelineAvailable ? [ + 'global', + ] : []), + 'lists', + 'antennas', + 'channels', + ] as TimelineHeaderItem[], + }, visibility: { where: 'deviceAccount', default: 'public' as (typeof Misskey.noteVisibilities)[number], @@ -804,6 +818,7 @@ interface Watcher { */ import lightTheme from '@/themes/l-light.json5'; import darkTheme from '@/themes/d-green-lime.json5'; +import { TimelineHeaderItem } from '@/timeline-header.js'; export class ColdDeviceStorage { public static default = { diff --git a/packages/frontend/src/timeline-header.ts b/packages/frontend/src/timeline-header.ts new file mode 100644 index 0000000000..5cf27c0c7b --- /dev/null +++ b/packages/frontend/src/timeline-header.ts @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { reactive } from 'vue'; +import { i18n } from '@/i18n.js'; +import { userListsCache } from '@/cache.js'; +import { isLocalTimelineAvailable, isGlobalTimelineAvailable } from '@/scripts/get-timeline-available.js'; + +export type TimelineHeaderItem = + 'home' | + 'local' | + 'social' | + 'global' | + 'lists' | + 'antennas' | + 'channels' | + `list:${string}` + +type TimelineHeaderItemsDef = { + title: string; + icon: string; + iconOnly?: boolean; // わからん +} + +const lists = await userListsCache.fetch(); +export const timelineHeaderItemDef = reactive>>({ + home: { + title: i18n.ts._timelines.home, + icon: 'ti ti-home', + iconOnly: true, + }, + ...(isLocalTimelineAvailable ? { + local: { + title: i18n.ts._timelines.local, + icon: 'ti ti-planet', + iconOnly: true, + }, + social: { + title: i18n.ts._timelines.social, + icon: 'ti ti-universe', + iconOnly: true, + } } : {}), + ...(isGlobalTimelineAvailable ? { global: { + title: i18n.ts._timelines.global, + icon: 'ti ti-whirl', + iconOnly: true, + } } : {}), + lists: { + icon: 'ti ti-list', + title: i18n.ts.lists, + iconOnly: true, + }, + antennas: { + icon: 'ti ti-antenna', + title: i18n.ts.antennas, + iconOnly: true, + }, + channels: { + icon: 'ti ti-device-tv', + title: i18n.ts.channel, + iconOnly: true, + }, + ...lists.reduce((acc, l) => { + acc['list:' + l.id] = { + title: i18n.ts.lists + ':' + l.name, + icon: 'ti ti-star', + iconOnly: true, + }; + return acc; + }, {}), +});