新着ノートをサウンドで通知する機能をdeck UIに追加 (#13867)

* feat(deck-ui): implement note notification

* chore: remove notify in antenna

* docs(changelog): 新着ノートをサウンドで通知する機能をdeck UIに追加

* fix: type error in test

* lint: key order

* fix: remove notify column

* test: remove test for notify

* chore: make sound selectable

* fix: add license header

* fix: add license header again

* Unnecessary await

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>

* ファイルを選択してください -> ファイルが選択されていません

* fix: i18n忘れ

* fix: i18n忘れ

* pleaseSelectFile > fileNotSelected

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
This commit is contained in:
anatawa12
2024-05-27 20:54:53 +09:00
committed by GitHub
parent d7982e471c
commit 4579be0f54
26 changed files with 341 additions and 53 deletions

View File

@@ -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),
},
];
/*

View File

@@ -13,13 +13,13 @@ 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';
@@ -29,6 +29,10 @@ 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;
@@ -37,11 +41,16 @@ 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 favoritedChannelsCache.fetch();
const { canceled, result: chosenChannel } = await os.select({
@@ -70,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>

View File

@@ -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', {

View File

@@ -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>

View File

@@ -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),
}];
/*

View File

@@ -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,10 +118,18 @@ 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,

View File

@@ -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 };
}
}
}