新着ノートをサウンドで通知する機能を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:
parent
d7982e471c
commit
4579be0f54
26 changed files with 341 additions and 53 deletions
|
@ -49,6 +49,7 @@
|
||||||
- Enhance: AiScriptを0.18.0にバージョンアップ
|
- Enhance: AiScriptを0.18.0にバージョンアップ
|
||||||
- Enhance: 通常のノートでも、お気に入りに登録したチャンネルにリノートできるように
|
- Enhance: 通常のノートでも、お気に入りに登録したチャンネルにリノートできるように
|
||||||
- Enhance: 長いテキストをペーストした際にテキストファイルとして添付するかどうかを選択できるように
|
- Enhance: 長いテキストをペーストした際にテキストファイルとして添付するかどうかを選択できるように
|
||||||
|
- Enhance: 新着ノートをサウンドで通知する機能をdeck UIに追加しました
|
||||||
- Enhance: コントロールパネルのクイックアクションからファイルを照会できるように
|
- Enhance: コントロールパネルのクイックアクションからファイルを照会できるように
|
||||||
- Enhance: コントロールパネルのクイックアクションから通常の照会を行えるように
|
- Enhance: コントロールパネルのクイックアクションから通常の照会を行えるように
|
||||||
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
|
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
|
||||||
|
|
8
locales/index.d.ts
vendored
8
locales/index.d.ts
vendored
|
@ -1280,6 +1280,10 @@ export interface Locale extends ILocale {
|
||||||
* フォルダーを選択
|
* フォルダーを選択
|
||||||
*/
|
*/
|
||||||
"selectFolders": string;
|
"selectFolders": string;
|
||||||
|
/**
|
||||||
|
* ファイルが選択されていません
|
||||||
|
*/
|
||||||
|
"fileNotSelected": string;
|
||||||
/**
|
/**
|
||||||
* ファイル名を変更
|
* ファイル名を変更
|
||||||
*/
|
*/
|
||||||
|
@ -9143,6 +9147,10 @@ export interface Locale extends ILocale {
|
||||||
* カラムを追加
|
* カラムを追加
|
||||||
*/
|
*/
|
||||||
"addColumn": string;
|
"addColumn": string;
|
||||||
|
/**
|
||||||
|
* 新着ノート通知の設定
|
||||||
|
*/
|
||||||
|
"newNoteNotificationSettings": string;
|
||||||
/**
|
/**
|
||||||
* カラムの設定
|
* カラムの設定
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -316,6 +316,7 @@ selectFile: "ファイルを選択"
|
||||||
selectFiles: "ファイルを選択"
|
selectFiles: "ファイルを選択"
|
||||||
selectFolder: "フォルダーを選択"
|
selectFolder: "フォルダーを選択"
|
||||||
selectFolders: "フォルダーを選択"
|
selectFolders: "フォルダーを選択"
|
||||||
|
fileNotSelected: "ファイルが選択されていません"
|
||||||
renameFile: "ファイル名を変更"
|
renameFile: "ファイル名を変更"
|
||||||
folderName: "フォルダー名"
|
folderName: "フォルダー名"
|
||||||
createFolder: "フォルダーを作成"
|
createFolder: "フォルダーを作成"
|
||||||
|
@ -2420,6 +2421,7 @@ _deck:
|
||||||
alwaysShowMainColumn: "常にメインカラムを表示"
|
alwaysShowMainColumn: "常にメインカラムを表示"
|
||||||
columnAlign: "カラムの寄せ"
|
columnAlign: "カラムの寄せ"
|
||||||
addColumn: "カラムを追加"
|
addColumn: "カラムを追加"
|
||||||
|
newNoteNotificationSettings: "新着ノート通知の設定"
|
||||||
configureColumn: "カラムの設定"
|
configureColumn: "カラムの設定"
|
||||||
swapLeft: "左に移動"
|
swapLeft: "左に移動"
|
||||||
swapRight: "右に移動"
|
swapRight: "右に移動"
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class RemoveAntennaNotify1716450883149 {
|
||||||
|
name = 'RemoveAntennaNotify1716450883149'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "notify"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "antenna" ADD "notify" boolean NOT NULL`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,7 +38,6 @@ export class AntennaEntityService {
|
||||||
users: antenna.users,
|
users: antenna.users,
|
||||||
caseSensitive: antenna.caseSensitive,
|
caseSensitive: antenna.caseSensitive,
|
||||||
localOnly: antenna.localOnly,
|
localOnly: antenna.localOnly,
|
||||||
notify: antenna.notify,
|
|
||||||
excludeBots: antenna.excludeBots,
|
excludeBots: antenna.excludeBots,
|
||||||
withReplies: antenna.withReplies,
|
withReplies: antenna.withReplies,
|
||||||
withFile: antenna.withFile,
|
withFile: antenna.withFile,
|
||||||
|
|
|
@ -90,9 +90,6 @@ export class MiAntenna {
|
||||||
})
|
})
|
||||||
public expression: string | null;
|
public expression: string | null;
|
||||||
|
|
||||||
@Column('boolean')
|
|
||||||
public notify: boolean;
|
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: true,
|
default: true,
|
||||||
|
|
|
@ -72,10 +72,6 @@ export const packedAntennaSchema = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
notify: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
excludeBots: {
|
excludeBots: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
|
|
@ -84,7 +84,6 @@ export class ExportAntennasProcessorService {
|
||||||
excludeBots: antenna.excludeBots,
|
excludeBots: antenna.excludeBots,
|
||||||
withReplies: antenna.withReplies,
|
withReplies: antenna.withReplies,
|
||||||
withFile: antenna.withFile,
|
withFile: antenna.withFile,
|
||||||
notify: antenna.notify,
|
|
||||||
}));
|
}));
|
||||||
if (antennas.length - 1 !== index) {
|
if (antennas.length - 1 !== index) {
|
||||||
write(', ');
|
write(', ');
|
||||||
|
|
|
@ -47,9 +47,8 @@ const validate = new Ajv().compile({
|
||||||
excludeBots: { type: 'boolean' },
|
excludeBots: { type: 'boolean' },
|
||||||
withReplies: { type: 'boolean' },
|
withReplies: { type: 'boolean' },
|
||||||
withFile: { type: 'boolean' },
|
withFile: { type: 'boolean' },
|
||||||
notify: { type: 'boolean' },
|
|
||||||
},
|
},
|
||||||
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'],
|
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'],
|
||||||
});
|
});
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -92,7 +91,6 @@ export class ImportAntennasProcessorService {
|
||||||
excludeBots: antenna.excludeBots,
|
excludeBots: antenna.excludeBots,
|
||||||
withReplies: antenna.withReplies,
|
withReplies: antenna.withReplies,
|
||||||
withFile: antenna.withFile,
|
withFile: antenna.withFile,
|
||||||
notify: antenna.notify,
|
|
||||||
}).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
this.logger.succ('Antenna created: ' + result.id);
|
this.logger.succ('Antenna created: ' + result.id);
|
||||||
this.globalEventService.publishInternalEvent('antennaCreated', result);
|
this.globalEventService.publishInternalEvent('antennaCreated', result);
|
||||||
|
|
|
@ -67,9 +67,8 @@ export const paramDef = {
|
||||||
excludeBots: { type: 'boolean' },
|
excludeBots: { type: 'boolean' },
|
||||||
withReplies: { type: 'boolean' },
|
withReplies: { type: 'boolean' },
|
||||||
withFile: { type: 'boolean' },
|
withFile: { type: 'boolean' },
|
||||||
notify: { type: 'boolean' },
|
|
||||||
},
|
},
|
||||||
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'],
|
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -128,7 +127,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
excludeBots: ps.excludeBots,
|
excludeBots: ps.excludeBots,
|
||||||
withReplies: ps.withReplies,
|
withReplies: ps.withReplies,
|
||||||
withFile: ps.withFile,
|
withFile: ps.withFile,
|
||||||
notify: ps.notify,
|
|
||||||
}).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('antennaCreated', antenna);
|
this.globalEventService.publishInternalEvent('antennaCreated', antenna);
|
||||||
|
|
|
@ -66,7 +66,6 @@ export const paramDef = {
|
||||||
excludeBots: { type: 'boolean' },
|
excludeBots: { type: 'boolean' },
|
||||||
withReplies: { type: 'boolean' },
|
withReplies: { type: 'boolean' },
|
||||||
withFile: { type: 'boolean' },
|
withFile: { type: 'boolean' },
|
||||||
notify: { type: 'boolean' },
|
|
||||||
},
|
},
|
||||||
required: ['antennaId'],
|
required: ['antennaId'],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -124,7 +123,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
excludeBots: ps.excludeBots,
|
excludeBots: ps.excludeBots,
|
||||||
withReplies: ps.withReplies,
|
withReplies: ps.withReplies,
|
||||||
withFile: ps.withFile,
|
withFile: ps.withFile,
|
||||||
notify: ps.notify,
|
|
||||||
isActive: true,
|
isActive: true,
|
||||||
lastUsedAt: new Date(),
|
lastUsedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
|
@ -38,7 +38,6 @@ describe('アンテナ', () => {
|
||||||
excludeKeywords: [['']],
|
excludeKeywords: [['']],
|
||||||
keywords: [['keyword']],
|
keywords: [['keyword']],
|
||||||
name: 'test',
|
name: 'test',
|
||||||
notify: false,
|
|
||||||
src: 'all' as const,
|
src: 'all' as const,
|
||||||
userListId: null,
|
userListId: null,
|
||||||
users: [''],
|
users: [''],
|
||||||
|
@ -151,7 +150,6 @@ describe('アンテナ', () => {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
keywords: [['keyword']],
|
keywords: [['keyword']],
|
||||||
name: 'test',
|
name: 'test',
|
||||||
notify: false,
|
|
||||||
src: 'all',
|
src: 'all',
|
||||||
userListId: null,
|
userListId: null,
|
||||||
users: [''],
|
users: [''],
|
||||||
|
@ -219,8 +217,6 @@ describe('アンテナ', () => {
|
||||||
{ parameters: () => ({ withReplies: true }) },
|
{ parameters: () => ({ withReplies: true }) },
|
||||||
{ parameters: () => ({ withFile: false }) },
|
{ parameters: () => ({ withFile: false }) },
|
||||||
{ parameters: () => ({ withFile: true }) },
|
{ parameters: () => ({ withFile: true }) },
|
||||||
{ parameters: () => ({ notify: false }) },
|
|
||||||
{ parameters: () => ({ notify: true }) },
|
|
||||||
];
|
];
|
||||||
test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => {
|
test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => {
|
||||||
const response = await successfulApiCall({
|
const response = await successfulApiCall({
|
||||||
|
|
|
@ -191,7 +191,6 @@ describe('Account Move', () => {
|
||||||
localOnly: false,
|
localOnly: false,
|
||||||
withReplies: false,
|
withReplies: false,
|
||||||
withFile: false,
|
withFile: false,
|
||||||
notify: false,
|
|
||||||
}, alice);
|
}, alice);
|
||||||
antennaId = antenna.body.id;
|
antennaId = antenna.body.id;
|
||||||
|
|
||||||
|
@ -435,7 +434,6 @@ describe('Account Move', () => {
|
||||||
localOnly: false,
|
localOnly: false,
|
||||||
withReplies: false,
|
withReplies: false,
|
||||||
withFile: false,
|
withFile: false,
|
||||||
notify: false,
|
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(res.status, 403);
|
assert.strictEqual(res.status, 403);
|
||||||
|
|
71
packages/frontend/src/components/MkFormDialog.file.vue
Normal file
71
packages/frontend/src/components/MkFormDialog.file.vue
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<MkButton inline rounded primary @click="selectButton($event)">{{ i18n.ts.selectFile }}</MkButton>
|
||||||
|
<div :class="['_nowrap', !fileName && $style.fileNotSelected]">{{ friendlyFileName }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import { selectFile } from '@/scripts/select-file.js';
|
||||||
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fileId?: string | null;
|
||||||
|
validate?: (file: Misskey.entities.DriveFile) => Promise<boolean>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'update', result: Misskey.entities.DriveFile): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const fileUrl = ref('');
|
||||||
|
const fileName = ref<string>('');
|
||||||
|
|
||||||
|
const friendlyFileName = computed<string>(() => {
|
||||||
|
if (fileName.value) {
|
||||||
|
return fileName.value;
|
||||||
|
}
|
||||||
|
if (fileUrl.value) {
|
||||||
|
return fileUrl.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return i18n.ts.fileNotSelected;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (props.fileId) {
|
||||||
|
misskeyApi('drive/files/show', {
|
||||||
|
fileId: props.fileId,
|
||||||
|
}).then((apiRes) => {
|
||||||
|
fileName.value = apiRes.name;
|
||||||
|
fileUrl.value = apiRes.url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectButton(ev: MouseEvent) {
|
||||||
|
selectFile(ev.currentTarget ?? ev.target).then(async (file) => {
|
||||||
|
if (!file) return;
|
||||||
|
if (props.validate && !await props.validate(file)) return;
|
||||||
|
|
||||||
|
emit('update', file);
|
||||||
|
fileName.value = file.name;
|
||||||
|
fileUrl.value = file.url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
.fileNotSelected {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--infoWarnFg);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -21,8 +21,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<MkSpacer :marginMin="20" :marginMax="32">
|
<MkSpacer :marginMin="20" :marginMax="32">
|
||||||
<div v-if="Object.keys(form).filter(item => !form[item].hidden).length > 0" class="_gaps_m">
|
<div v-if="Object.keys(form).filter(item => !form[item].hidden).length > 0" class="_gaps_m">
|
||||||
<template v-for="(v, k) in Object.fromEntries(Object.entries(form).filter(([_, v]) => !('hidden' in v) || 'hidden' in v && !v.hidden))">
|
<template v-for="(v, k) in Object.fromEntries(Object.entries(form))">
|
||||||
<MkInput v-if="v.type === 'number'" v-model="values[k]" type="number" :step="v.step || 1">
|
<template v-if="typeof v.hidden == 'function' ? v.hidden(values) : v.hidden"></template>
|
||||||
|
<MkInput v-else-if="v.type === 'number'" v-model="values[k]" type="number" :step="v.step || 1">
|
||||||
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||||
<template v-if="v.description" #caption>{{ v.description }}</template>
|
<template v-if="v.description" #caption>{{ v.description }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
@ -53,6 +54,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkButton v-else-if="v.type === 'button'" @click="v.action($event, values)">
|
<MkButton v-else-if="v.type === 'button'" @click="v.action($event, values)">
|
||||||
<span v-text="v.content || k"></span>
|
<span v-text="v.content || k"></span>
|
||||||
</MkButton>
|
</MkButton>
|
||||||
|
<XFile
|
||||||
|
v-else-if="v.type === 'drive-file'"
|
||||||
|
:fileId="v.defaultFileId"
|
||||||
|
:validate="async f => !v.validate || await v.validate(f)"
|
||||||
|
@update="f => values[k] = f"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="_fullinfo">
|
<div v-else class="_fullinfo">
|
||||||
|
@ -72,6 +79,7 @@ import MkSelect from './MkSelect.vue';
|
||||||
import MkRange from './MkRange.vue';
|
import MkRange from './MkRange.vue';
|
||||||
import MkButton from './MkButton.vue';
|
import MkButton from './MkButton.vue';
|
||||||
import MkRadios from './MkRadios.vue';
|
import MkRadios from './MkRadios.vue';
|
||||||
|
import XFile from './MkFormDialog.file.vue';
|
||||||
import type { Form } from '@/scripts/form.js';
|
import type { Form } from '@/scripts/form.js';
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
|
@ -518,7 +518,7 @@ export function waiting(): Promise<void> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function form<F extends Form>(title: string, f: F): Promise<{ canceled: true } | { result: GetFormResultType<F> }> {
|
export function form<F extends Form>(title: string, f: F): Promise<{ canceled: true, result?: undefined } | { canceled?: false, result: GetFormResultType<F> }> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form: f }, {
|
popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form: f }, {
|
||||||
done: result => {
|
done: result => {
|
||||||
|
|
|
@ -39,7 +39,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
|
<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
|
||||||
<MkSwitch v-model="caseSensitive">{{ i18n.ts.caseSensitive }}</MkSwitch>
|
<MkSwitch v-model="caseSensitive">{{ i18n.ts.caseSensitive }}</MkSwitch>
|
||||||
<MkSwitch v-model="withFile">{{ i18n.ts.withFileAntenna }}</MkSwitch>
|
<MkSwitch v-model="withFile">{{ i18n.ts.withFileAntenna }}</MkSwitch>
|
||||||
<MkSwitch v-model="notify">{{ i18n.ts.notifyAntenna }}</MkSwitch>
|
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.actions">
|
<div :class="$style.actions">
|
||||||
<MkButton inline primary @click="saveAntenna()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
<MkButton inline primary @click="saveAntenna()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||||
|
@ -82,7 +81,6 @@ const localOnly = ref<boolean>(props.antenna.localOnly);
|
||||||
const excludeBots = ref<boolean>(props.antenna.excludeBots);
|
const excludeBots = ref<boolean>(props.antenna.excludeBots);
|
||||||
const withReplies = ref<boolean>(props.antenna.withReplies);
|
const withReplies = ref<boolean>(props.antenna.withReplies);
|
||||||
const withFile = ref<boolean>(props.antenna.withFile);
|
const withFile = ref<boolean>(props.antenna.withFile);
|
||||||
const notify = ref<boolean>(props.antenna.notify);
|
|
||||||
const userLists = ref<Misskey.entities.UserList[] | null>(null);
|
const userLists = ref<Misskey.entities.UserList[] | null>(null);
|
||||||
|
|
||||||
watch(() => src.value, async () => {
|
watch(() => src.value, async () => {
|
||||||
|
@ -99,7 +97,6 @@ async function saveAntenna() {
|
||||||
excludeBots: excludeBots.value,
|
excludeBots: excludeBots.value,
|
||||||
withReplies: withReplies.value,
|
withReplies: withReplies.value,
|
||||||
withFile: withFile.value,
|
withFile: withFile.value,
|
||||||
notify: notify.value,
|
|
||||||
caseSensitive: caseSensitive.value,
|
caseSensitive: caseSensitive.value,
|
||||||
localOnly: localOnly.value,
|
localOnly: localOnly.value,
|
||||||
users: users.value.trim().split('\n').map(x => x.trim()),
|
users: users.value.trim().split('\n').map(x => x.trim()),
|
||||||
|
|
|
@ -3,18 +3,22 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
type EnumItem = string | {
|
type EnumItem = string | {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Hidden = boolean | ((v: any) => boolean);
|
||||||
|
|
||||||
export type FormItem = {
|
export type FormItem = {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'string';
|
type: 'string';
|
||||||
default: string | null;
|
default: string | null;
|
||||||
description?: string;
|
description?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: Hidden;
|
||||||
multiline?: boolean;
|
multiline?: boolean;
|
||||||
treatAsMfm?: boolean;
|
treatAsMfm?: boolean;
|
||||||
} | {
|
} | {
|
||||||
|
@ -23,27 +27,27 @@ export type FormItem = {
|
||||||
default: number | null;
|
default: number | null;
|
||||||
description?: string;
|
description?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: Hidden;
|
||||||
step?: number;
|
step?: number;
|
||||||
} | {
|
} | {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'boolean';
|
type: 'boolean';
|
||||||
default: boolean | null;
|
default: boolean | null;
|
||||||
description?: string;
|
description?: string;
|
||||||
hidden?: boolean;
|
hidden?: Hidden;
|
||||||
} | {
|
} | {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'enum';
|
type: 'enum';
|
||||||
default: string | null;
|
default: string | null;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: Hidden;
|
||||||
enum: EnumItem[];
|
enum: EnumItem[];
|
||||||
} | {
|
} | {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'radio';
|
type: 'radio';
|
||||||
default: unknown | null;
|
default: unknown | null;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: Hidden;
|
||||||
options: {
|
options: {
|
||||||
label: string;
|
label: string;
|
||||||
value: unknown;
|
value: unknown;
|
||||||
|
@ -58,20 +62,27 @@ export type FormItem = {
|
||||||
min: number;
|
min: number;
|
||||||
max: number;
|
max: number;
|
||||||
textConverter?: (value: number) => string;
|
textConverter?: (value: number) => string;
|
||||||
|
hidden?: Hidden;
|
||||||
} | {
|
} | {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'object';
|
type: 'object';
|
||||||
default: Record<string, unknown> | null;
|
default: Record<string, unknown> | null;
|
||||||
hidden: boolean;
|
hidden: Hidden;
|
||||||
} | {
|
} | {
|
||||||
label?: string;
|
label?: string;
|
||||||
type: 'array';
|
type: 'array';
|
||||||
default: unknown[] | null;
|
default: unknown[] | null;
|
||||||
hidden: boolean;
|
hidden: Hidden;
|
||||||
} | {
|
} | {
|
||||||
type: 'button';
|
type: 'button';
|
||||||
content?: string;
|
content?: string;
|
||||||
|
hidden?: Hidden;
|
||||||
action: (ev: MouseEvent, v: any) => void;
|
action: (ev: MouseEvent, v: any) => void;
|
||||||
|
} | {
|
||||||
|
type: 'drive-file';
|
||||||
|
defaultFileId?: string | null;
|
||||||
|
hidden?: Hidden;
|
||||||
|
validate?: (v: Misskey.entities.DriveFile) => Promise<boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Form = Record<string, FormItem>;
|
export type Form = Record<string, FormItem>;
|
||||||
|
@ -84,8 +95,9 @@ type GetItemType<Item extends FormItem> =
|
||||||
Item['type'] extends 'range' ? number :
|
Item['type'] extends 'range' ? number :
|
||||||
Item['type'] extends 'enum' ? string :
|
Item['type'] extends 'enum' ? string :
|
||||||
Item['type'] extends 'array' ? unknown[] :
|
Item['type'] extends 'array' ? unknown[] :
|
||||||
Item['type'] extends 'object' ? Record<string, unknown>
|
Item['type'] extends 'object' ? Record<string, unknown> :
|
||||||
: never;
|
Item['type'] extends 'drive-file' ? Misskey.entities.DriveFile | undefined :
|
||||||
|
never;
|
||||||
|
|
||||||
export type GetFormResultType<F extends Form> = {
|
export type GetFormResultType<F extends Form> = {
|
||||||
[P in keyof F]: GetItemType<F[P]>;
|
[P in keyof F]: GetItemType<F[P]>;
|
||||||
|
|
|
@ -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>
|
<i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||||
</template>
|
</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>
|
</XColumn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, shallowRef } from 'vue';
|
import { onMounted, ref, shallowRef, watch } from 'vue';
|
||||||
import XColumn from './column.vue';
|
import XColumn from './column.vue';
|
||||||
import { updateColumn, Column } from './deck-store.js';
|
import { updateColumn, Column } from './deck-store.js';
|
||||||
import MkTimeline from '@/components/MkTimeline.vue';
|
import MkTimeline from '@/components/MkTimeline.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.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<{
|
const props = defineProps<{
|
||||||
column: Column;
|
column: Column;
|
||||||
|
@ -28,6 +32,7 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
|
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
|
||||||
|
const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.column.antennaId == null) {
|
if (props.column.antennaId == null) {
|
||||||
|
@ -35,6 +40,10 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(soundSetting, v => {
|
||||||
|
updateColumn(props.column.id, { soundSetting: v });
|
||||||
|
});
|
||||||
|
|
||||||
async function setAntenna() {
|
async function setAntenna() {
|
||||||
const antennas = await misskeyApi('antennas/list');
|
const antennas = await misskeyApi('antennas/list');
|
||||||
const { canceled, result: antenna } = await os.select({
|
const { canceled, result: antenna } = await os.select({
|
||||||
|
@ -54,7 +63,11 @@ function editAntenna() {
|
||||||
os.pageWindow('my/antennas/' + props.column.antennaId);
|
os.pageWindow('my/antennas/' + props.column.antennaId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const menu = [
|
function onNote() {
|
||||||
|
sound.playMisskeySfxFile(soundSetting.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu: MenuItem[] = [
|
||||||
{
|
{
|
||||||
icon: 'ti ti-pencil',
|
icon: 'ti ti-pencil',
|
||||||
text: i18n.ts.selectAntenna,
|
text: i18n.ts.selectAntenna,
|
||||||
|
@ -65,6 +78,11 @@ const menu = [
|
||||||
text: i18n.ts.editAntenna,
|
text: i18n.ts.editAntenna,
|
||||||
action: editAntenna,
|
action: editAntenna,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: 'ti ti-bell',
|
||||||
|
text: i18n.ts._deck.newNoteNotificationSettings,
|
||||||
|
action: () => soundSettingsButton(soundSetting),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -13,13 +13,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div style="padding: 8px; text-align: center;">
|
<div style="padding: 8px; text-align: center;">
|
||||||
<MkButton primary gradate rounded inline small @click="post"><i class="ti ti-pencil"></i></MkButton>
|
<MkButton primary gradate rounded inline small @click="post"><i class="ti ti-pencil"></i></MkButton>
|
||||||
</div>
|
</div>
|
||||||
<MkTimeline ref="timeline" src="channel" :channel="column.channelId"/>
|
<MkTimeline ref="timeline" src="channel" :channel="column.channelId" @note="onNote"/>
|
||||||
</template>
|
</template>
|
||||||
</XColumn>
|
</XColumn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { shallowRef } from 'vue';
|
import { ref, shallowRef, watch } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import XColumn from './column.vue';
|
import XColumn from './column.vue';
|
||||||
import { updateColumn, Column } from './deck-store.js';
|
import { updateColumn, Column } from './deck-store.js';
|
||||||
|
@ -29,6 +29,10 @@ import * as os from '@/os.js';
|
||||||
import { favoritedChannelsCache } from '@/cache.js';
|
import { favoritedChannelsCache } from '@/cache.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.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<{
|
const props = defineProps<{
|
||||||
column: Column;
|
column: Column;
|
||||||
|
@ -37,11 +41,16 @@ const props = defineProps<{
|
||||||
|
|
||||||
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
|
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
|
||||||
const channel = shallowRef<Misskey.entities.Channel>();
|
const channel = shallowRef<Misskey.entities.Channel>();
|
||||||
|
const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
|
||||||
|
|
||||||
if (props.column.channelId == null) {
|
if (props.column.channelId == null) {
|
||||||
setChannel();
|
setChannel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(soundSetting, v => {
|
||||||
|
updateColumn(props.column.id, { soundSetting: v });
|
||||||
|
});
|
||||||
|
|
||||||
async function setChannel() {
|
async function setChannel() {
|
||||||
const channels = await favoritedChannelsCache.fetch();
|
const channels = await favoritedChannelsCache.fetch();
|
||||||
const { canceled, result: chosenChannel } = await os.select({
|
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',
|
icon: 'ti ti-pencil',
|
||||||
text: i18n.ts.selectChannel,
|
text: i18n.ts.selectChannel,
|
||||||
action: setChannel,
|
action: setChannel,
|
||||||
|
}, {
|
||||||
|
icon: 'ti ti-bell',
|
||||||
|
text: i18n.ts._deck.newNoteNotificationSettings,
|
||||||
|
action: () => soundSettingsButton(soundSetting),
|
||||||
}];
|
}];
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { notificationTypes } from 'misskey-js';
|
||||||
import { Storage } from '@/pizzax.js';
|
import { Storage } from '@/pizzax.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { deepClone } from '@/scripts/clone.js';
|
import { deepClone } from '@/scripts/clone.js';
|
||||||
|
import { SoundStore } from '@/store.js';
|
||||||
|
|
||||||
type ColumnWidget = {
|
type ColumnWidget = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -33,6 +34,7 @@ export type Column = {
|
||||||
withRenotes?: boolean;
|
withRenotes?: boolean;
|
||||||
withReplies?: boolean;
|
withReplies?: boolean;
|
||||||
onlyFiles?: boolean;
|
onlyFiles?: boolean;
|
||||||
|
soundSetting: SoundStore;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deckStore = markRaw(new Storage('deck', {
|
export const deckStore = markRaw(new Storage('deck', {
|
||||||
|
|
|
@ -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>
|
<i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||||
</template>
|
</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>
|
</XColumn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -21,6 +21,10 @@ import MkTimeline from '@/components/MkTimeline.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.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<{
|
const props = defineProps<{
|
||||||
column: Column;
|
column: Column;
|
||||||
|
@ -29,6 +33,7 @@ const props = defineProps<{
|
||||||
|
|
||||||
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
|
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
|
||||||
const withRenotes = ref(props.column.withRenotes ?? true);
|
const withRenotes = ref(props.column.withRenotes ?? true);
|
||||||
|
const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
|
||||||
|
|
||||||
if (props.column.listId == null) {
|
if (props.column.listId == null) {
|
||||||
setList();
|
setList();
|
||||||
|
@ -40,6 +45,10 @@ watch(withRenotes, v => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(soundSetting, v => {
|
||||||
|
updateColumn(props.column.id, { soundSetting: v });
|
||||||
|
});
|
||||||
|
|
||||||
async function setList() {
|
async function setList() {
|
||||||
const lists = await misskeyApi('users/lists/list');
|
const lists = await misskeyApi('users/lists/list');
|
||||||
const { canceled, result: list } = await os.select({
|
const { canceled, result: list } = await os.select({
|
||||||
|
@ -59,7 +68,11 @@ function editList() {
|
||||||
os.pageWindow('my/lists/' + props.column.listId);
|
os.pageWindow('my/lists/' + props.column.listId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const menu = [
|
function onNote() {
|
||||||
|
sound.playMisskeySfxFile(soundSetting.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu: MenuItem[] = [
|
||||||
{
|
{
|
||||||
icon: 'ti ti-pencil',
|
icon: 'ti ti-pencil',
|
||||||
text: i18n.ts.selectList,
|
text: i18n.ts.selectList,
|
||||||
|
@ -75,5 +88,10 @@ const menu = [
|
||||||
text: i18n.ts.showRenotes,
|
text: i18n.ts.showRenotes,
|
||||||
ref: withRenotes,
|
ref: withRenotes,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: 'ti ti-bell',
|
||||||
|
text: i18n.ts._deck.newNoteNotificationSettings,
|
||||||
|
action: () => soundSettingsButton(soundSetting),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -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>
|
<i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||||
</template>
|
</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>
|
</XColumn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, shallowRef } from 'vue';
|
import { onMounted, ref, shallowRef, watch } from 'vue';
|
||||||
import XColumn from './column.vue';
|
import XColumn from './column.vue';
|
||||||
import { updateColumn, Column } from './deck-store.js';
|
import { updateColumn, Column } from './deck-store.js';
|
||||||
import MkTimeline from '@/components/MkTimeline.vue';
|
import MkTimeline from '@/components/MkTimeline.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.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<{
|
const props = defineProps<{
|
||||||
column: Column;
|
column: Column;
|
||||||
|
@ -28,6 +32,7 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
|
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
|
||||||
|
const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.column.roleId == null) {
|
if (props.column.roleId == null) {
|
||||||
|
@ -35,6 +40,10 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(soundSetting, v => {
|
||||||
|
updateColumn(props.column.id, { soundSetting: v });
|
||||||
|
});
|
||||||
|
|
||||||
async function setRole() {
|
async function setRole() {
|
||||||
const roles = (await misskeyApi('roles/list')).filter(x => x.isExplorable);
|
const roles = (await misskeyApi('roles/list')).filter(x => x.isExplorable);
|
||||||
const { canceled, result: role } = await os.select({
|
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',
|
icon: 'ti ti-pencil',
|
||||||
text: i18n.ts.role,
|
text: i18n.ts.role,
|
||||||
action: setRole,
|
action: setRole,
|
||||||
|
}, {
|
||||||
|
icon: 'ti ti-bell',
|
||||||
|
text: i18n.ts._deck.newNoteNotificationSettings,
|
||||||
|
action: () => soundSettingsButton(soundSetting),
|
||||||
}];
|
}];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:withRenotes="withRenotes"
|
:withRenotes="withRenotes"
|
||||||
:withReplies="withReplies"
|
:withReplies="withReplies"
|
||||||
:onlyFiles="onlyFiles"
|
:onlyFiles="onlyFiles"
|
||||||
|
@note="onNote"
|
||||||
/>
|
/>
|
||||||
</XColumn>
|
</XColumn>
|
||||||
</template>
|
</template>
|
||||||
|
@ -41,6 +42,10 @@ import * as os from '@/os.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { instance } from '@/instance.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<{
|
const props = defineProps<{
|
||||||
column: Column;
|
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 isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable));
|
||||||
const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable));
|
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 withRenotes = ref(props.column.withRenotes ?? true);
|
||||||
const withReplies = ref(props.column.withReplies ?? false);
|
const withReplies = ref(props.column.withReplies ?? false);
|
||||||
const onlyFiles = ref(props.column.onlyFiles ?? 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(() => {
|
onMounted(() => {
|
||||||
if (props.column.tl == null) {
|
if (props.column.tl == null) {
|
||||||
setType();
|
setType();
|
||||||
|
@ -108,10 +118,18 @@ async function setType() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const menu = [{
|
function onNote() {
|
||||||
|
sound.playMisskeySfxFile(soundSetting.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu: MenuItem[] = [{
|
||||||
icon: 'ti ti-pencil',
|
icon: 'ti ti-pencil',
|
||||||
text: i18n.ts.timeline,
|
text: i18n.ts.timeline,
|
||||||
action: setType,
|
action: setType,
|
||||||
|
}, {
|
||||||
|
icon: 'ti ti-bell',
|
||||||
|
text: i18n.ts._deck.newNoteNotificationSettings,
|
||||||
|
action: () => soundSettingsButton(soundSetting),
|
||||||
}, {
|
}, {
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
text: i18n.ts.showRenotes,
|
text: i18n.ts.showRenotes,
|
||||||
|
|
107
packages/frontend/src/ui/deck/tl-note-notification.ts
Normal file
107
packages/frontend/src/ui/deck/tl-note-notification.ts
Normal 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4441,7 +4441,6 @@ export type components = {
|
||||||
caseSensitive: boolean;
|
caseSensitive: boolean;
|
||||||
/** @default false */
|
/** @default false */
|
||||||
localOnly: boolean;
|
localOnly: boolean;
|
||||||
notify: boolean;
|
|
||||||
/** @default false */
|
/** @default false */
|
||||||
excludeBots: boolean;
|
excludeBots: boolean;
|
||||||
/** @default false */
|
/** @default false */
|
||||||
|
@ -9748,7 +9747,6 @@ export type operations = {
|
||||||
excludeBots?: boolean;
|
excludeBots?: boolean;
|
||||||
withReplies: boolean;
|
withReplies: boolean;
|
||||||
withFile: boolean;
|
withFile: boolean;
|
||||||
notify: boolean;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -10030,7 +10028,6 @@ export type operations = {
|
||||||
excludeBots?: boolean;
|
excludeBots?: boolean;
|
||||||
withReplies?: boolean;
|
withReplies?: boolean;
|
||||||
withFile?: boolean;
|
withFile?: boolean;
|
||||||
notify?: boolean;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue