Merge remote-tracking branch 'refs/remotes/misskey-original/develop' into develop
# Conflicts: # packages/frontend/src/cache.ts # packages/frontend/src/pages/admin/index.vue # packages/frontend/src/pages/settings/general.vue # packages/frontend/src/pages/timeline.vue
This commit is contained in:
commit
07b4338eff
100 changed files with 1929 additions and 328 deletions
|
|
@ -11,6 +11,7 @@ export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, ()
|
|||
export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list'));
|
||||
export const userListsCache = new Cache<Misskey.entities.UserList[]>(1000 * 60 * 30, () => misskeyApi('users/lists/list'));
|
||||
export const antennasCache = new Cache<Misskey.entities.Antenna[]>(1000 * 60 * 30, () => misskeyApi('antennas/list'));
|
||||
export const favoritedChannelsCache = new Cache<Misskey.entities.Channel[]>(1000 * 60 * 30, () => misskeyApi('channels/my-favorites', { limit: 100 }));
|
||||
export const userFavoriteListsCache = new Cache(1000 * 60 * 30, () => misskeyApi('users/lists/list-favorite'));
|
||||
export const userChannelsCache = new Cache<Misskey.entities.UserChannel[]>(1000 * 60 * 30, () => misskeyApi('channels/owned'));
|
||||
export const userChannelFollowingsCache = new Cache<Misskey.entities.UserChannelFollowing[]>(1000 * 60 * 30, () => misskeyApi('channels/followed'));
|
||||
|
|
|
|||
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">
|
||||
<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))">
|
||||
<MkInput v-if="v.type === 'number'" v-model="values[k]" type="number" :step="v.step || 1">
|
||||
<template v-for="(v, k) in Object.fromEntries(Object.entries(form))">
|
||||
<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 v-if="v.description" #caption>{{ v.description }}</template>
|
||||
</MkInput>
|
||||
|
|
@ -53,6 +54,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton v-else-if="v.type === 'button'" @click="v.action($event, values)">
|
||||
<span v-text="v.content || k"></span>
|
||||
</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>
|
||||
</div>
|
||||
<div v-else class="_fullinfo">
|
||||
|
|
@ -72,6 +79,7 @@ import MkSelect from './MkSelect.vue';
|
|||
import MkRange from './MkRange.vue';
|
||||
import MkButton from './MkButton.vue';
|
||||
import MkRadios from './MkRadios.vue';
|
||||
import XFile from './MkFormDialog.file.vue';
|
||||
import type { Form } from '@/scripts/form.js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
|
|||
|
|
@ -536,7 +536,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 => {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form: f }, {
|
||||
done: result => {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import MkInput from '@/components/MkInput.vue';
|
|||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { lookupFile } from '@/scripts/admin-lookup.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
|
|
@ -73,33 +73,10 @@ function clear() {
|
|||
});
|
||||
}
|
||||
|
||||
function show(file) {
|
||||
os.pageWindow(`/admin/file/${file.id}`);
|
||||
}
|
||||
|
||||
async function find() {
|
||||
const { canceled, result: q } = await os.inputText({
|
||||
title: i18n.ts.fileIdOrUrl,
|
||||
minLength: 1,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
misskeyApi('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => {
|
||||
show(file);
|
||||
}).catch(err => {
|
||||
if (err.code === 'NO_SUCH_FILE') {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.notFound,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
text: i18n.ts.lookup,
|
||||
icon: 'ti ti-search',
|
||||
handler: find,
|
||||
handler: lookupFile,
|
||||
}, {
|
||||
text: i18n.ts.clearCachedFiles,
|
||||
icon: 'ti ti-trash',
|
||||
|
|
|
|||
|
|
@ -33,9 +33,10 @@ import { i18n } from '@/i18n.js';
|
|||
import MkSuperMenu from '@/components/MkSuperMenu.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { instance } from '@/instance.js';
|
||||
import { lookup } from '@/scripts/lookup.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { lookupUser, lookupUserByEmail } from '@/scripts/lookup-user.js';
|
||||
import { lookupUser, lookupUserByEmail, lookupFile } from '@/scripts/admin-lookup.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { PageMetadata, definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
|
||||
import { bannerDark, bannerLight, defaultStore, iconDark, iconLight } from '@/store.js';
|
||||
|
|
@ -96,7 +97,7 @@ const menuDef = computed(() => [{
|
|||
type: 'button',
|
||||
icon: 'ti ti-search',
|
||||
text: i18n.ts.lookup,
|
||||
action: lookup,
|
||||
action: adminLookup,
|
||||
}, ...(instance.disableRegistration ? [{
|
||||
type: 'button',
|
||||
icon: 'ti ti-user-plus',
|
||||
|
|
@ -296,7 +297,7 @@ function invite() {
|
|||
});
|
||||
}
|
||||
|
||||
function lookup(ev: MouseEvent) {
|
||||
function adminLookup(ev: MouseEvent) {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.user,
|
||||
icon: 'ti ti-user',
|
||||
|
|
@ -309,23 +310,17 @@ function lookup(ev: MouseEvent) {
|
|||
action: () => {
|
||||
lookupUserByEmail();
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.note,
|
||||
icon: 'ti ti-pencil',
|
||||
action: () => {
|
||||
alert('TODO');
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.file,
|
||||
icon: 'ti ti-cloud',
|
||||
action: () => {
|
||||
alert('TODO');
|
||||
lookupFile();
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.instance,
|
||||
icon: 'ti ti-planet',
|
||||
text: i18n.ts.lookup,
|
||||
icon: 'ti ti-world-search',
|
||||
action: () => {
|
||||
alert('TODO');
|
||||
lookup();
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ import MkInput from '@/components/MkInput.vue';
|
|||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { lookupUser } from '@/scripts/lookup-user.js';
|
||||
import { lookupUser } from '@/scripts/admin-lookup.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
|
|
|
|||
142
packages/frontend/src/pages/announcement.vue
Normal file
142
packages/frontend/src/pages/announcement.vue
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<Transition
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.fadeEnterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.fadeLeaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.fadeEnterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.fadeLeaveTo : ''"
|
||||
mode="out-in"
|
||||
>
|
||||
<div v-if="announcement" :key="announcement.id" class="_panel" :class="$style.announcement">
|
||||
<div v-if="announcement.forYou" :class="$style.forYou"><i class="ti ti-pin"></i> {{ i18n.ts.forYou }}</div>
|
||||
<div :class="$style.header">
|
||||
<span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span>
|
||||
<span style="margin-right: 0.5em;">
|
||||
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
|
||||
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
|
||||
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
|
||||
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
|
||||
</span>
|
||||
<Mfm :text="announcement.title"/>
|
||||
</div>
|
||||
<div :class="$style.content">
|
||||
<Mfm :text="announcement.text"/>
|
||||
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
|
||||
<div style="margin-top: 8px; opacity: 0.7; font-size: 85%;">
|
||||
{{ i18n.ts.createdAt }}: <MkTime :time="announcement.createdAt" mode="detail"/>
|
||||
</div>
|
||||
<div v-if="announcement.updatedAt" style="opacity: 0.7; font-size: 85%;">
|
||||
{{ i18n.ts.updatedAt }}: <MkTime :time="announcement.updatedAt" mode="detail"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$i && !announcement.silence && !announcement.isRead" :class="$style.footer">
|
||||
<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<MkError v-else-if="error" @retry="fetch()"/>
|
||||
<MkLoading v-else/>
|
||||
</Transition>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { $i, updateAccount } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const props = defineProps<{
|
||||
announcementId: string;
|
||||
}>();
|
||||
|
||||
const announcement = ref<Misskey.entities.Announcement | null>(null);
|
||||
const error = ref<any>(null);
|
||||
const path = computed(() => props.announcementId);
|
||||
|
||||
function fetch() {
|
||||
announcement.value = null;
|
||||
misskeyApi('announcements/show', {
|
||||
announcementId: props.announcementId,
|
||||
}).then(async _announcement => {
|
||||
announcement.value = _announcement;
|
||||
}).catch(err => {
|
||||
error.value = err;
|
||||
});
|
||||
}
|
||||
|
||||
async function read(target: Misskey.entities.Announcement): Promise<void> {
|
||||
if (target.needConfirmationToRead) {
|
||||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts._announcement.readConfirmTitle,
|
||||
text: i18n.tsx._announcement.readConfirmText({ title: target.title }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
}
|
||||
|
||||
target.isRead = true;
|
||||
await misskeyApi('i/read-announcement', { announcementId: target.id });
|
||||
if ($i) {
|
||||
updateAccount({
|
||||
unreadAnnouncements: $i.unreadAnnouncements.filter((a: { id: string; }) => a.id !== target.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => path.value, fetch, { immediate: true });
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
title: announcement.value ? `${i18n.ts.announcements}: ${announcement.value.title}` : i18n.ts.announcements,
|
||||
icon: 'ti ti-speakerphone',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.announcement {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.forYou {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 24px;
|
||||
font-size: 90%;
|
||||
white-space: pre;
|
||||
color: #d28a3f;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 16px;
|
||||
font-weight: bold;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
.content {
|
||||
> img {
|
||||
display: block;
|
||||
max-height: 300px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -21,14 +21,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
|
||||
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
|
||||
</span>
|
||||
<span>{{ announcement.title }}</span>
|
||||
<MkA :to="`/announcements/${announcement.id}`"><span>{{ announcement.title }}</span></MkA>
|
||||
</div>
|
||||
<div :class="$style.content">
|
||||
<Mfm :text="announcement.text"/>
|
||||
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
|
||||
<div style="opacity: 0.7; font-size: 85%;">
|
||||
<MkTime :time="announcement.updatedAt ?? announcement.createdAt" mode="detail"/>
|
||||
</div>
|
||||
<MkA :to="`/announcements/${announcement.id}`">
|
||||
<div style="margin-top: 8px; opacity: 0.7; font-size: 85%;">
|
||||
{{ i18n.ts.createdAt }}: <MkTime :time="announcement.createdAt" mode="detail"/>
|
||||
</div>
|
||||
<div v-if="announcement.updatedAt" style="opacity: 0.7; font-size: 85%;">
|
||||
{{ i18n.ts.updatedAt }}: <MkTime :time="announcement.updatedAt" mode="detail"/>
|
||||
</div>
|
||||
</MkA>
|
||||
</div>
|
||||
<div v-if="tab !== 'past' && $i && !announcement.silence && !announcement.isRead" :class="$style.footer">
|
||||
<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
|
||||
|
|
@ -73,24 +78,24 @@ const paginationEl = ref<InstanceType<typeof MkPagination>>();
|
|||
|
||||
const tab = ref('current');
|
||||
|
||||
async function read(announcement) {
|
||||
if (announcement.needConfirmationToRead) {
|
||||
async function read(target) {
|
||||
if (target.needConfirmationToRead) {
|
||||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts._announcement.readConfirmTitle,
|
||||
text: i18n.tsx._announcement.readConfirmText({ title: announcement.title }),
|
||||
text: i18n.tsx._announcement.readConfirmText({ title: target.title }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
}
|
||||
|
||||
if (!paginationEl.value) return;
|
||||
paginationEl.value.updateItem(announcement.id, a => {
|
||||
paginationEl.value.updateItem(target.id, a => {
|
||||
a.isRead = true;
|
||||
return a;
|
||||
});
|
||||
misskeyApi('i/read-announcement', { announcementId: announcement.id });
|
||||
misskeyApi('i/read-announcement', { announcementId: target.id });
|
||||
updateAccount({
|
||||
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== announcement.id),
|
||||
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import { url } from '@/config.js';
|
||||
import { favoritedChannelsCache } from '@/cache.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
|
@ -152,6 +153,7 @@ function favorite() {
|
|||
channelId: channel.value.id,
|
||||
}).then(() => {
|
||||
favorited.value = true;
|
||||
favoritedChannelsCache.delete();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -167,6 +169,7 @@ async function unfavorite() {
|
|||
channelId: channel.value.id,
|
||||
}).then(() => {
|
||||
favorited.value = false;
|
||||
favoritedChannelsCache.delete();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
|
||||
<MkSwitch v-model="caseSensitive">{{ i18n.ts.caseSensitive }}</MkSwitch>
|
||||
<MkSwitch v-model="withFile">{{ i18n.ts.withFileAntenna }}</MkSwitch>
|
||||
<MkSwitch v-model="notify">{{ i18n.ts.notifyAntenna }}</MkSwitch>
|
||||
</div>
|
||||
<div :class="$style.actions">
|
||||
<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 withReplies = ref<boolean>(props.antenna.withReplies);
|
||||
const withFile = ref<boolean>(props.antenna.withFile);
|
||||
const notify = ref<boolean>(props.antenna.notify);
|
||||
const userLists = ref<Misskey.entities.UserList[] | null>(null);
|
||||
|
||||
watch(() => src.value, async () => {
|
||||
|
|
@ -99,7 +97,6 @@ async function saveAntenna() {
|
|||
excludeBots: excludeBots.value,
|
||||
withReplies: withReplies.value,
|
||||
withFile: withFile.value,
|
||||
notify: notify.value,
|
||||
caseSensitive: caseSensitive.value,
|
||||
localOnly: localOnly.value,
|
||||
users: users.value.trim().split('\n').map(x => x.trim()),
|
||||
|
|
|
|||
|
|
@ -94,6 +94,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<div class="_gaps_m">
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="collapseRenotes">
|
||||
<template #label>{{ i18n.ts.collapseRenotes }}</template>
|
||||
<template #caption>{{ i18n.ts.collapseRenotesDescription }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="showNoteActionsOnlyHover">{{ i18n.ts.showNoteActionsOnlyHover }}</MkSwitch>
|
||||
<MkSwitch v-model="showClipButtonInNoteFooter">{{ i18n.ts.showClipButtonInNoteFooter }}</MkSwitch>
|
||||
<MkSwitch v-model="collapseRenotes">{{ i18n.ts.collapseRenotes }}</MkSwitch>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { instance } from '@/instance.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { antennasCache, userFavoriteListsCache, userListsCache } from '@/cache.js';
|
||||
import { antennasCache, userFavoriteListsCache, userListsCache, favoritedChannelsCache } from '@/cache.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { deepMerge } from '@/scripts/merge.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
|
|
@ -193,9 +193,7 @@ async function chooseAntenna(ev: MouseEvent): Promise<void> {
|
|||
}
|
||||
|
||||
async function chooseChannel(ev: MouseEvent): Promise<void> {
|
||||
const channels = await misskeyApi('channels/my-favorites', {
|
||||
limit: 100,
|
||||
});
|
||||
const channels = await favoritedChannelsCache.fetch();
|
||||
const items: MenuItem[] = [
|
||||
...channels.map(channel => {
|
||||
const lastReadedAt = miLocalStorage.getItemAsJson(`channelLastReadedAt:${channel.id}`) ?? null;
|
||||
|
|
|
|||
|
|
@ -193,6 +193,9 @@ const routes: RouteDef[] = [{
|
|||
}, {
|
||||
path: '/announcements',
|
||||
component: page(() => import('@/pages/announcements.vue')),
|
||||
}, {
|
||||
path: '/announcements/:announcementId',
|
||||
component: page(() => import('@/pages/announcement.vue')),
|
||||
}, {
|
||||
path: '/about',
|
||||
component: page(() => import('@/pages/about.vue')),
|
||||
|
|
|
|||
|
|
@ -63,3 +63,26 @@ export async function lookupUserByEmail() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function lookupFile() {
|
||||
const { canceled, result: q } = await os.inputText({
|
||||
title: i18n.ts.fileIdOrUrl,
|
||||
minLength: 1,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
const show = (file) => {
|
||||
os.pageWindow(`/admin/file/${file.id}`);
|
||||
};
|
||||
|
||||
misskeyApi('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => {
|
||||
show(file);
|
||||
}).catch(err => {
|
||||
if (err.code === 'NO_SUCH_FILE') {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.notFound,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -3,18 +3,22 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
type EnumItem = string | {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type Hidden = boolean | ((v: any) => boolean);
|
||||
|
||||
export type FormItem = {
|
||||
label?: string;
|
||||
type: 'string';
|
||||
default: string | null;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
hidden?: boolean;
|
||||
hidden?: Hidden;
|
||||
multiline?: boolean;
|
||||
treatAsMfm?: boolean;
|
||||
} | {
|
||||
|
|
@ -23,27 +27,27 @@ export type FormItem = {
|
|||
default: number | null;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
hidden?: boolean;
|
||||
hidden?: Hidden;
|
||||
step?: number;
|
||||
} | {
|
||||
label?: string;
|
||||
type: 'boolean';
|
||||
default: boolean | null;
|
||||
description?: string;
|
||||
hidden?: boolean;
|
||||
hidden?: Hidden;
|
||||
} | {
|
||||
label?: string;
|
||||
type: 'enum';
|
||||
default: string | null;
|
||||
required?: boolean;
|
||||
hidden?: boolean;
|
||||
hidden?: Hidden;
|
||||
enum: EnumItem[];
|
||||
} | {
|
||||
label?: string;
|
||||
type: 'radio';
|
||||
default: unknown | null;
|
||||
required?: boolean;
|
||||
hidden?: boolean;
|
||||
hidden?: Hidden;
|
||||
options: {
|
||||
label: string;
|
||||
value: unknown;
|
||||
|
|
@ -58,20 +62,27 @@ export type FormItem = {
|
|||
min: number;
|
||||
max: number;
|
||||
textConverter?: (value: number) => string;
|
||||
hidden?: Hidden;
|
||||
} | {
|
||||
label?: string;
|
||||
type: 'object';
|
||||
default: Record<string, unknown> | null;
|
||||
hidden: boolean;
|
||||
hidden: Hidden;
|
||||
} | {
|
||||
label?: string;
|
||||
type: 'array';
|
||||
default: unknown[] | null;
|
||||
hidden: boolean;
|
||||
hidden: Hidden;
|
||||
} | {
|
||||
type: 'button';
|
||||
content?: string;
|
||||
hidden?: Hidden;
|
||||
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>;
|
||||
|
|
@ -84,8 +95,9 @@ type GetItemType<Item extends FormItem> =
|
|||
Item['type'] extends 'range' ? number :
|
||||
Item['type'] extends 'enum' ? string :
|
||||
Item['type'] extends 'array' ? unknown[] :
|
||||
Item['type'] extends 'object' ? Record<string, unknown>
|
||||
: never;
|
||||
Item['type'] extends 'object' ? Record<string, unknown> :
|
||||
Item['type'] extends 'drive-file' ? Misskey.entities.DriveFile | undefined :
|
||||
never;
|
||||
|
||||
export type GetFormResultType<F extends Form> = {
|
||||
[P in keyof F]: GetItemType<F[P]>;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { url } from '@/config.js';
|
|||
import { defaultStore, noteActions } from '@/store.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { getUserMenu } from '@/scripts/get-user-menu.js';
|
||||
import { clipsCache } from '@/cache.js';
|
||||
import { clipsCache, favoritedChannelsCache } from '@/cache.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
|
|
@ -609,9 +609,7 @@ export function getRenoteMenu(props: {
|
|||
icon: 'ti ti-repeat',
|
||||
text: appearNote.channel ? i18n.ts.renoteToOtherChannel : i18n.ts.renoteToChannel,
|
||||
children: async () => {
|
||||
const channels = await misskeyApi('channels/my-favorites', {
|
||||
limit: 30,
|
||||
});
|
||||
const channels = await favoritedChannelsCache.fetch();
|
||||
return channels.filter((channel) => {
|
||||
if (!appearNote.channelId) return true;
|
||||
return channel.id !== appearNote.channelId;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
];
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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', {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}];
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:withRenotes="withRenotes"
|
||||
:withReplies="withReplies"
|
||||
:onlyFiles="onlyFiles"
|
||||
@note="onNote"
|
||||
/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
|
@ -42,6 +43,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;
|
||||
|
|
@ -53,6 +58,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);
|
||||
|
|
@ -75,6 +81,10 @@ watch(onlyFiles, v => {
|
|||
});
|
||||
});
|
||||
|
||||
watch(soundSetting, v => {
|
||||
updateColumn(props.column.id, { soundSetting: v });
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (props.column.tl == null) {
|
||||
setType();
|
||||
|
|
@ -111,10 +121,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,
|
||||
|
|
|
|||
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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue