diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2024-06-01 11:27:03 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-06-01 11:27:03 +0900 |
| commit | fce66b85b603caac79e1bfa87b5f4621b1ba9d4e (patch) | |
| tree | d22952ee3f8e30057977a99a33823f4d52990fbc /packages/frontend/src/ui | |
| parent | Merge pull request #13493 from misskey-dev/develop (diff) | |
| parent | fix(backend): use insertOne insteadof insert/findOneOrFail combination (#13908) (diff) | |
| download | misskey-fce66b85b603caac79e1bfa87b5f4621b1ba9d4e.tar.gz misskey-fce66b85b603caac79e1bfa87b5f4621b1ba9d4e.tar.bz2 misskey-fce66b85b603caac79e1bfa87b5f4621b1ba9d4e.zip | |
Merge pull request #13917 from misskey-dev/develop
Release 2024.5.0 (master)
Diffstat (limited to 'packages/frontend/src/ui')
| -rw-r--r-- | packages/frontend/src/ui/_common_/announcements.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/ui/_common_/common.ts | 11 | ||||
| -rw-r--r-- | packages/frontend/src/ui/_common_/statusbar-rss.vue | 5 | ||||
| -rw-r--r-- | packages/frontend/src/ui/deck/antenna-column.vue | 24 | ||||
| -rw-r--r-- | packages/frontend/src/ui/deck/channel-column.vue | 36 | ||||
| -rw-r--r-- | packages/frontend/src/ui/deck/deck-store.ts | 2 | ||||
| -rw-r--r-- | packages/frontend/src/ui/deck/list-column.vue | 22 | ||||
| -rw-r--r-- | packages/frontend/src/ui/deck/role-timeline-column.vue | 23 | ||||
| -rw-r--r-- | packages/frontend/src/ui/deck/tl-column.vue | 20 | ||||
| -rw-r--r-- | packages/frontend/src/ui/deck/tl-note-notification.ts | 107 | ||||
| -rw-r--r-- | packages/frontend/src/ui/visitor.vue | 7 |
11 files changed, 227 insertions, 32 deletions
diff --git a/packages/frontend/src/ui/_common_/announcements.vue b/packages/frontend/src/ui/_common_/announcements.vue index 362c29e6c2..374bc20b54 100644 --- a/packages/frontend/src/ui/_common_/announcements.vue +++ b/packages/frontend/src/ui/_common_/announcements.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-for="announcement in $i.unreadAnnouncements.filter(x => x.display === 'banner')" :key="announcement.id" :class="$style.item" - to="/announcements" + :to="`/announcements/${announcement.id}`" > <span :class="$style.icon"> <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index 9b510a6292..839fa5faf8 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -79,7 +79,12 @@ export function openInstanceMenu(ev: MouseEvent) { text: i18n.ts.tools, icon: 'ti ti-tool', children: toolsMenuItems(), - }, { type: 'divider' }, (instance.impressumUrl) ? { + }, { type: 'divider' }, { + type: 'link', + text: i18n.ts.inquiry, + icon: 'ti ti-help-circle', + to: '/contact', + }, (instance.impressumUrl) ? { text: i18n.ts.impressum, icon: 'ti ti-file-invoice', action: () => { @@ -98,8 +103,8 @@ export function openInstanceMenu(ev: MouseEvent) { window.open(instance.privacyPolicyUrl, '_blank', 'noopener'); }, } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, { - text: i18n.ts.help, - icon: 'ti ti-help-circle', + text: i18n.ts.document, + icon: 'ti ti-bulb', action: () => { window.open('https://misskey-hub.net/docs/for-users/', '_blank', 'noopener'); }, diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue index b973a4fd6b..6e1d06eec1 100644 --- a/packages/frontend/src/ui/_common_/statusbar-rss.vue +++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue @@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; +import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; import { useInterval } from '@/scripts/use-interval.js'; import { shuffle } from '@/scripts/shuffle.js'; @@ -42,13 +43,13 @@ const props = defineProps<{ refreshIntervalSec?: number; }>(); -const items = ref([]); +const items = ref<Misskey.entities.FetchRssResponse['items']>([]); const fetching = ref(true); const key = ref(0); const tick = () => { window.fetch(`/api/fetch-rss?url=${props.url}`, {}).then(res => { - res.json().then(feed => { + res.json().then((feed: Misskey.entities.FetchRssResponse) => { if (props.shuffle) { shuffle(feed.items); } diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue index b42a21bf6f..c3dc1e4fce 100644 --- a/packages/frontend/src/ui/deck/antenna-column.vue +++ b/packages/frontend/src/ui/deck/antenna-column.vue @@ -9,18 +9,22 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> - <MkTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId"/> + <MkTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @note="onNote"/> </XColumn> </template> <script lang="ts" setup> -import { onMounted, shallowRef } from 'vue'; +import { onMounted, ref, shallowRef, watch } from 'vue'; import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; +import { MenuItem } from '@/types/menu.js'; +import { SoundStore } from '@/store.js'; +import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; +import * as sound from '@/scripts/sound.js'; const props = defineProps<{ column: Column; @@ -28,6 +32,7 @@ const props = defineProps<{ }>(); const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); +const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 }); onMounted(() => { if (props.column.antennaId == null) { @@ -35,6 +40,10 @@ onMounted(() => { } }); +watch(soundSetting, v => { + updateColumn(props.column.id, { soundSetting: v }); +}); + async function setAntenna() { const antennas = await misskeyApi('antennas/list'); const { canceled, result: antenna } = await os.select({ @@ -54,7 +63,11 @@ function editAntenna() { os.pageWindow('my/antennas/' + props.column.antennaId); } -const menu = [ +function onNote() { + sound.playMisskeySfxFile(soundSetting.value); +} + +const menu: MenuItem[] = [ { icon: 'ti ti-pencil', text: i18n.ts.selectAntenna, @@ -65,6 +78,11 @@ const menu = [ text: i18n.ts.editAntenna, action: editAntenna, }, + { + icon: 'ti ti-bell', + text: i18n.ts._deck.newNoteNotificationSettings, + action: () => soundSettingsButton(soundSetting), + }, ]; /* diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue index bd3b059497..7c5b13eaf1 100644 --- a/packages/frontend/src/ui/deck/channel-column.vue +++ b/packages/frontend/src/ui/deck/channel-column.vue @@ -13,21 +13,26 @@ SPDX-License-Identifier: AGPL-3.0-only <div style="padding: 8px; text-align: center;"> <MkButton primary gradate rounded inline small @click="post"><i class="ti ti-pencil"></i></MkButton> </div> - <MkTimeline ref="timeline" src="channel" :channel="column.channelId"/> + <MkTimeline ref="timeline" src="channel" :channel="column.channelId" @note="onNote"/> </template> </XColumn> </template> <script lang="ts" setup> -import { shallowRef } from 'vue'; +import { ref, shallowRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store.js'; import MkTimeline from '@/components/MkTimeline.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; +import { favoritedChannelsCache } from '@/cache.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; +import { MenuItem } from '@/types/menu.js'; +import { SoundStore } from '@/store.js'; +import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; +import * as sound from '@/scripts/sound.js'; const props = defineProps<{ column: Column; @@ -36,26 +41,29 @@ const props = defineProps<{ const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); const channel = shallowRef<Misskey.entities.Channel>(); +const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 }); if (props.column.channelId == null) { setChannel(); } +watch(soundSetting, v => { + updateColumn(props.column.id, { soundSetting: v }); +}); + async function setChannel() { - const channels = await misskeyApi('channels/my-favorites', { - limit: 100, - }); - const { canceled, result: channel } = await os.select({ + const channels = await favoritedChannelsCache.fetch(); + const { canceled, result: chosenChannel } = await os.select({ title: i18n.ts.selectChannel, items: channels.map(x => ({ value: x, text: x.name, })), default: props.column.channelId, }); - if (canceled) return; + if (canceled || chosenChannel == null) return; updateColumn(props.column.id, { - channelId: channel.id, - name: channel.name, + channelId: chosenChannel.id, + name: chosenChannel.name, }); } @@ -71,9 +79,17 @@ async function post() { }); } -const menu = [{ +function onNote() { + sound.playMisskeySfxFile(soundSetting.value); +} + +const menu: MenuItem[] = [{ icon: 'ti ti-pencil', text: i18n.ts.selectChannel, action: setChannel, +}, { + icon: 'ti ti-bell', + text: i18n.ts._deck.newNoteNotificationSettings, + action: () => soundSettingsButton(soundSetting), }]; </script> diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index 70b55e8172..bb3c04cd5c 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -9,6 +9,7 @@ import { notificationTypes } from 'misskey-js'; import { Storage } from '@/pizzax.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { deepClone } from '@/scripts/clone.js'; +import { SoundStore } from '@/store.js'; type ColumnWidget = { name: string; @@ -33,6 +34,7 @@ export type Column = { withRenotes?: boolean; withReplies?: boolean; onlyFiles?: boolean; + soundSetting: SoundStore; }; export const deckStore = markRaw(new Storage('deck', { diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue index 70ea54326f..5369112494 100644 --- a/packages/frontend/src/ui/deck/list-column.vue +++ b/packages/frontend/src/ui/deck/list-column.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> - <MkTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" :withRenotes="withRenotes"/> + <MkTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" :withRenotes="withRenotes" @note="onNote"/> </XColumn> </template> @@ -21,6 +21,10 @@ import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; +import { MenuItem } from '@/types/menu.js'; +import { SoundStore } from '@/store.js'; +import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; +import * as sound from '@/scripts/sound.js'; const props = defineProps<{ column: Column; @@ -29,6 +33,7 @@ const props = defineProps<{ const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); const withRenotes = ref(props.column.withRenotes ?? true); +const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 }); if (props.column.listId == null) { setList(); @@ -40,6 +45,10 @@ watch(withRenotes, v => { }); }); +watch(soundSetting, v => { + updateColumn(props.column.id, { soundSetting: v }); +}); + async function setList() { const lists = await misskeyApi('users/lists/list'); const { canceled, result: list } = await os.select({ @@ -59,7 +68,11 @@ function editList() { os.pageWindow('my/lists/' + props.column.listId); } -const menu = [ +function onNote() { + sound.playMisskeySfxFile(soundSetting.value); +} + +const menu: MenuItem[] = [ { icon: 'ti ti-pencil', text: i18n.ts.selectList, @@ -75,5 +88,10 @@ const menu = [ text: i18n.ts.showRenotes, ref: withRenotes, }, + { + icon: 'ti ti-bell', + text: i18n.ts._deck.newNoteNotificationSettings, + action: () => soundSettingsButton(soundSetting), + }, ]; </script> diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue index eae2ee13f3..32ab7527b4 100644 --- a/packages/frontend/src/ui/deck/role-timeline-column.vue +++ b/packages/frontend/src/ui/deck/role-timeline-column.vue @@ -9,18 +9,22 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> - <MkTimeline v-if="column.roleId" ref="timeline" src="role" :role="column.roleId"/> + <MkTimeline v-if="column.roleId" ref="timeline" src="role" :role="column.roleId" @note="onNote"/> </XColumn> </template> <script lang="ts" setup> -import { onMounted, shallowRef } from 'vue'; +import { onMounted, ref, shallowRef, watch } from 'vue'; import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; +import { MenuItem } from '@/types/menu.js'; +import { SoundStore } from '@/store.js'; +import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; +import * as sound from '@/scripts/sound.js'; const props = defineProps<{ column: Column; @@ -28,6 +32,7 @@ const props = defineProps<{ }>(); const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); +const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 }); onMounted(() => { if (props.column.roleId == null) { @@ -35,6 +40,10 @@ onMounted(() => { } }); +watch(soundSetting, v => { + updateColumn(props.column.id, { soundSetting: v }); +}); + async function setRole() { const roles = (await misskeyApi('roles/list')).filter(x => x.isExplorable); const { canceled, result: role } = await os.select({ @@ -50,10 +59,18 @@ async function setRole() { }); } -const menu = [{ +function onNote() { + sound.playMisskeySfxFile(soundSetting.value); +} + +const menu: MenuItem[] = [{ icon: 'ti ti-pencil', text: i18n.ts.role, action: setRole, +}, { + icon: 'ti ti-bell', + text: i18n.ts._deck.newNoteNotificationSettings, + action: () => soundSettingsButton(soundSetting), }]; /* diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index f9066d9db7..a967335edf 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only :withRenotes="withRenotes" :withReplies="withReplies" :onlyFiles="onlyFiles" + @note="onNote" /> </XColumn> </template> @@ -41,6 +42,10 @@ import * as os from '@/os.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; +import { MenuItem } from '@/types/menu.js'; +import { SoundStore } from '@/store.js'; +import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; +import * as sound from '@/scripts/sound.js'; const props = defineProps<{ column: Column; @@ -52,6 +57,7 @@ const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable)); const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable)); +const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 }); const withRenotes = ref(props.column.withRenotes ?? true); const withReplies = ref(props.column.withReplies ?? false); const onlyFiles = ref(props.column.onlyFiles ?? false); @@ -74,6 +80,10 @@ watch(onlyFiles, v => { }); }); +watch(soundSetting, v => { + updateColumn(props.column.id, { soundSetting: v }); +}); + onMounted(() => { if (props.column.tl == null) { setType(); @@ -108,11 +118,19 @@ async function setType() { }); } -const menu = [{ +function onNote() { + sound.playMisskeySfxFile(soundSetting.value); +} + +const menu: MenuItem[] = [{ icon: 'ti ti-pencil', text: i18n.ts.timeline, action: setType, }, { + icon: 'ti ti-bell', + text: i18n.ts._deck.newNoteNotificationSettings, + action: () => soundSettingsButton(soundSetting), +}, { type: 'switch', text: i18n.ts.showRenotes, ref: withRenotes, diff --git a/packages/frontend/src/ui/deck/tl-note-notification.ts b/packages/frontend/src/ui/deck/tl-note-notification.ts new file mode 100644 index 0000000000..275ea56ba0 --- /dev/null +++ b/packages/frontend/src/ui/deck/tl-note-notification.ts @@ -0,0 +1,107 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; +import { Ref } from 'vue'; +import { SoundStore } from '@/store.js'; +import { getSoundDuration, playMisskeySfxFile, soundsTypes, SoundType } from '@/scripts/sound.js'; +import { i18n } from '@/i18n.js'; +import * as os from '@/os.js'; + +export async function soundSettingsButton(soundSetting: Ref<SoundStore>): Promise<void> { + function getSoundTypeName(f: SoundType): string { + switch (f) { + case null: + return i18n.ts.none; + case '_driveFile_': + return i18n.ts._soundSettings.driveFile; + default: + return f; + } + } + + const { canceled, result } = await os.form(i18n.ts.sound, { + type: { + type: 'enum', + label: i18n.ts.sound, + default: soundSetting.value.type ?? 'none', + enum: soundsTypes.map(f => ({ + value: f ?? 'none', label: getSoundTypeName(f), + })), + }, + soundFile: { + type: 'drive-file', + label: i18n.ts.file, + defaultFileId: soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileId : null, + hidden: v => v.type !== '_driveFile_', + validate: async (file: Misskey.entities.DriveFile) => { + if (!file.type.startsWith('audio')) { + os.alert({ + type: 'warning', + title: i18n.ts._soundSettings.driveFileTypeWarn, + text: i18n.ts._soundSettings.driveFileTypeWarnDescription, + }); + return false; + } + + const duration = await getSoundDuration(file.url); + if (duration >= 2000) { + const { canceled } = await os.confirm({ + type: 'warning', + title: i18n.ts._soundSettings.driveFileDurationWarn, + text: i18n.ts._soundSettings.driveFileDurationWarnDescription, + okText: i18n.ts.continue, + cancelText: i18n.ts.cancel, + }); + if (canceled) return false; + } + + return true; + }, + }, + volume: { + type: 'range', + label: i18n.ts.volume, + default: soundSetting.value.volume ?? 1, + textConverter: (v) => `${Math.floor(v * 100)}%`, + min: 0, + max: 1, + step: 0.05, + }, + listen: { + type: 'button', + content: i18n.ts.listen, + action: (_, v) => { + const sound = buildSoundStore(v); + if (!sound) return; + playMisskeySfxFile(sound); + }, + }, + }); + if (canceled) return; + + const res = buildSoundStore(result); + if (res) soundSetting.value = res; + + function buildSoundStore(result: any): SoundStore | null { + const type = (result.type === 'none' ? null : result.type) as SoundType; + const volume = result.volume as number; + const fileId = result.soundFile?.id ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileId : undefined); + const fileUrl = result.soundFile?.url ?? (soundSetting.value.type === '_driveFile_' ? soundSetting.value.fileUrl : undefined); + + if (type === '_driveFile_') { + if (!fileUrl || !fileId) { + os.alert({ + type: 'warning', + text: i18n.ts._soundSettings.driveFileWarn, + }); + return null; + } + return { type, volume, fileId, fileUrl }; + } else { + return { type, volume }; + } + } +} diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue index 29b305d9bc..80623083cf 100644 --- a/packages/frontend/src/ui/visitor.vue +++ b/packages/frontend/src/ui/visitor.vue @@ -70,11 +70,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, provide, ref, computed } from 'vue'; -import * as Misskey from 'misskey-js'; import XCommon from './_common_/common.vue'; import { instanceName } from '@/config.js'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; import { instance } from '@/instance.js'; import XSigninDialog from '@/components/MkSigninDialog.vue'; import XSignupDialog from '@/components/MkSignupDialog.vue'; @@ -114,7 +112,6 @@ const isTimelineAvailable = ref(instance.policies?.ltlAvailable || instance.poli const showMenu = ref(false); const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD); const narrow = ref(window.innerWidth < 1280); -const meta = ref<Misskey.entities.MetaResponse>(); const keymap = computed(() => { return { @@ -128,10 +125,6 @@ const keymap = computed(() => { }; }); -misskeyApi('meta', { detail: true }).then(res => { - meta.value = res; -}); - function signin() { os.popup(XSigninDialog, { autoSet: true, |