merge upstream
This commit is contained in:
commit
d8908ef2d8
1065 changed files with 32953 additions and 20092 deletions
|
|
@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<MkLoading v-if="!loaded"/>
|
||||
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear>
|
||||
<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" appear>
|
||||
<div v-show="loaded" :class="$style.root">
|
||||
<img :src="serverErrorImageUrl" class="_ghost" :class="$style.img"/>
|
||||
<img :src="serverErrorImageUrl" draggable="false" :class="$style.img"/>
|
||||
<div class="_gaps">
|
||||
<div><b><i class="ti ti-alert-triangle"></i> {{ i18n.ts.pageLoadError }}</b></div>
|
||||
<div v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</div>
|
||||
|
|
@ -27,15 +27,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { version } from '@@/js/config.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
import { version } from '@@/js/config.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { unisonReload } from '@/utility/unison-reload.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { serverErrorImageUrl } from '@/instance.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
|
@ -67,7 +67,7 @@ const headerActions = computed(() => []);
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.error,
|
||||
icon: 'ti ti-alert-triangle',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ import MkInput from '@/components/MkInput.vue';
|
|||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import { customEmojis, customEmojiCategories, getCustomEmojiTags } from '@/custom-emojis.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
|
||||
const customEmojiTags = getCustomEmojiTags();
|
||||
const q = ref('');
|
||||
|
|
|
|||
|
|
@ -56,7 +56,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { computed, ref } from 'vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ import { host, version } from '@@/js/config.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import number from '@/filters/number.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20">
|
||||
<XOverview/>
|
||||
|
|
@ -20,15 +19,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkInstanceStats/>
|
||||
</MkSpacer>
|
||||
</MkHorizontalSwipe>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||
import { instance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { claimAchievement } from '@/utility/achievements.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
|
||||
const XOverview = defineAsyncComponent(() => import('@/pages/about.overview.vue'));
|
||||
|
|
@ -81,7 +80,7 @@ const headerTabs = computed(() => {
|
|||
return items;
|
||||
});
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.instanceInfo,
|
||||
icon: 'ti ti-info-circle',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -4,21 +4,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<PageWithHeader>
|
||||
<MkSpacer :contentMax="1200">
|
||||
<MkAchievements :user="$i"/>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue';
|
||||
import MkAchievements from '@/components/MkAchievements.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { claimAchievement } from '@/utility/achievements.js';
|
||||
|
||||
let timer: number | null;
|
||||
|
||||
|
|
@ -48,7 +47,7 @@ onDeactivated(() => {
|
|||
}
|
||||
});
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.achievements,
|
||||
icon: 'ti ti-medal',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
|
||||
<MkSpacer v-if="file" :contentMax="600" :marginMin="16" :marginMax="32">
|
||||
<div v-if="tab === 'overview'" class="cxqhhsmd _gaps_m">
|
||||
<a class="thumbnail" :href="file.url" target="_blank">
|
||||
|
|
@ -36,8 +35,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkA v-if="file.user" class="user" :to="`/admin/user/${file.user.id}`">
|
||||
<MkUserCardMini :user="file.user"/>
|
||||
</MkA>
|
||||
|
||||
<div>
|
||||
<MkSwitch v-model="isSensitive" @update:modelValue="toggleIsSensitive">{{ i18n.ts.sensitive }}</MkSwitch>
|
||||
<MkSwitch :modelValue="isSensitive" @update:modelValue="toggleSensitive">{{ i18n.ts.sensitive }}</MkSwitch>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
@ -66,7 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkObjectView>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -82,10 +82,10 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
|||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import bytes from '@/filters/bytes.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { iAmAdmin, iAmModerator } from '@/account.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { iAmAdmin, iAmModerator } from '@/i.js';
|
||||
|
||||
const tab = ref('overview');
|
||||
const file = ref<Misskey.entities.DriveFile | null>(null);
|
||||
|
|
@ -117,9 +117,21 @@ async function del() {
|
|||
});
|
||||
}
|
||||
|
||||
async function toggleIsSensitive(v) {
|
||||
await misskeyApi('drive/files/update', { fileId: props.fileId, isSensitive: v });
|
||||
isSensitive.value = v;
|
||||
async function toggleSensitive() {
|
||||
if (!file.value) return;
|
||||
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: isSensitive.value ? i18n.ts.unmarkAsSensitiveConfirm : i18n.ts.markAsSensitiveConfirm,
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
isSensitive.value = !isSensitive.value;
|
||||
|
||||
os.apiWithDialog('drive/files/update', {
|
||||
fileId: file.value.id,
|
||||
isSensitive: !file.value.isSensitive,
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
|
|
@ -148,7 +160,7 @@ const headerTabs = computed(() => [{
|
|||
icon: 'ti ti-code',
|
||||
}]);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: file.value ? `${i18n.ts.file}: ${file.value.name}` : i18n.ts.file,
|
||||
icon: 'ti ti-file',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
|
||||
<MkSpacer :contentMax="600" :marginMin="16" :marginMax="32">
|
||||
<FormSuspense :p="init">
|
||||
<div v-if="tab === 'overview'" class="_gaps_m">
|
||||
|
|
@ -39,18 +38,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #value><span class="_monospace">{{ ips[0].ip }}</span></template>
|
||||
</MkKeyValue>
|
||||
-->
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.createdAt }}</template>
|
||||
<template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="info" oneline>
|
||||
<template #key>{{ i18n.ts.lastActiveDate }}</template>
|
||||
<template #value><span class="_monospace"><MkTime :time="info.lastActiveDate" :mode="'detail'"/></span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="info" oneline>
|
||||
<template #key>{{ i18n.ts.email }}</template>
|
||||
<template #value><span class="_monospace">{{ info.email }}</span></template>
|
||||
</MkKeyValue>
|
||||
<template v-if="!isSystem">
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.createdAt }}</template>
|
||||
<template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="info" oneline>
|
||||
<template #key>{{ i18n.ts.lastActiveDate }}</template>
|
||||
<template #value><span class="_monospace"><MkTime :time="info.lastActiveDate" :mode="'detail'"/></span></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="info" oneline>
|
||||
<template #key>{{ i18n.ts.email }}</template>
|
||||
<template #value><span class="_monospace">{{ info.email }}</span></template>
|
||||
</MkKeyValue>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<MkTextarea v-model="moderationNote" manualSave @update:modelValue="onModerationNoteChanged">
|
||||
|
|
@ -77,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<FormSection v-if="!isSystem">
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="silenced" @update:modelValue="toggleSilence">{{ i18n.ts.silence }}</MkSwitch>
|
||||
<MkSwitch v-if="!isSystem" v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
|
||||
|
|
@ -200,7 +201,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -221,11 +222,11 @@ import FormSuspense from '@/components/form/suspense.vue';
|
|||
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { acct } from '@/filters/user.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { iAmAdmin, $i, iAmModerator } from '@/account.js';
|
||||
import { iAmAdmin, $i, iAmModerator } from '@/i.js';
|
||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
|
|
@ -469,7 +470,7 @@ async function deleteAccount() {
|
|||
}
|
||||
|
||||
async function assignRole() {
|
||||
const roles = await misskeyApi('admin/roles/list');
|
||||
const roles = await misskeyApi('admin/roles/list').then(it => it.filter(r => r.target === 'manual'));
|
||||
|
||||
const { canceled, result: roleId } = await os.select({
|
||||
title: i18n.ts._role.chooseRoleToAssign,
|
||||
|
|
@ -558,7 +559,15 @@ watch(user, () => {
|
|||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => [{
|
||||
const headerTabs = computed(() => isSystem.value ? [{
|
||||
key: 'overview',
|
||||
title: i18n.ts.overview,
|
||||
icon: 'ti ti-info-circle',
|
||||
}, {
|
||||
key: 'raw',
|
||||
title: 'Raw',
|
||||
icon: 'ti ti-code',
|
||||
}] : [{
|
||||
key: 'overview',
|
||||
title: i18n.ts.overview,
|
||||
icon: 'ti ti-info-circle',
|
||||
|
|
@ -584,7 +593,7 @@ const headerTabs = computed(() => [{
|
|||
icon: 'ti ti-code',
|
||||
}]);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: user.value ? acct(user.value) : i18n.ts.userInfo,
|
||||
icon: 'ti ti-user-exclamation',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ import MkInput from '@/components/MkInput.vue';
|
|||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import { rolesCache } from '@/cache.js';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
|
|
|||
|
|
@ -33,13 +33,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref, shallowRef, watch, nextTick } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref, useTemplateRef, watch, nextTick, inject } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { popupMenu } from '@/os.js';
|
||||
import { scrollToTop } from '@@/js/scroll.js';
|
||||
import { popupMenu } from '@/os.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { injectReactiveMetadata } from '@/scripts/page-metadata.js';
|
||||
import { DI } from '@/di.js';
|
||||
|
||||
type Tab = {
|
||||
key?: string | null;
|
||||
|
|
@ -66,11 +66,11 @@ const emit = defineEmits<{
|
|||
(ev: 'update:tab', key: string);
|
||||
}>();
|
||||
|
||||
const pageMetadata = injectReactiveMetadata();
|
||||
const pageMetadata = inject(DI.pageMetadata);
|
||||
|
||||
const el = shallowRef<HTMLElement>(null);
|
||||
const el = useTemplateRef('el');
|
||||
const tabHighlightEl = useTemplateRef('tabHighlightEl');
|
||||
const tabRefs = {};
|
||||
const tabHighlightEl = shallowRef<HTMLElement | null>(null);
|
||||
const bg = ref<string | null>(null);
|
||||
const height = ref(0);
|
||||
const hasTabs = computed(() => {
|
||||
|
|
@ -119,15 +119,15 @@ function onTabClick(tab: Tab, ev: MouseEvent): void {
|
|||
}
|
||||
|
||||
const calcBg = () => {
|
||||
const rawBg = pageMetadata.value?.bg ?? 'var(--MI_THEME-bg)';
|
||||
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||
const rawBg = pageMetadata.value.bg ?? 'var(--MI_THEME-bg)';
|
||||
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(window.document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||
tinyBg.setAlpha(0.85);
|
||||
bg.value = tinyBg.toRgbString();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
calcBg();
|
||||
globalEvents.on('themeChanged', calcBg);
|
||||
globalEvents.on('themeChanging', calcBg);
|
||||
|
||||
watch(() => [props.tab, props.tabs], () => {
|
||||
nextTick(() => {
|
||||
|
|
@ -147,7 +147,7 @@ onMounted(() => {
|
|||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
globalEvents.off('themeChanged', calcBg);
|
||||
globalEvents.off('themeChanging', calcBg);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -71,15 +71,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
|
||||
import { computed, onMounted, ref, useTemplateRef, toRefs } from 'vue';
|
||||
import { entities } from 'misskey-js';
|
||||
import type { MkSystemWebhookResult } from '@/components/MkSystemWebhookEditor.impl.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import { MkSystemWebhookResult, showSystemWebhookEditorDialog } from '@/components/MkSystemWebhookEditor.impl.js';
|
||||
import { showSystemWebhookEditorDialog } from '@/components/MkSystemWebhookEditor.impl.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkDivider from '@/components/MkDivider.vue';
|
||||
import * as os from '@/os.js';
|
||||
|
|
@ -99,7 +100,7 @@ const props = defineProps<{
|
|||
|
||||
const { mode, id } = toRefs(props);
|
||||
|
||||
const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
const dialogEl = useTemplateRef('dialogEl');
|
||||
|
||||
const loading = ref<number>(0);
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ import { entities } from 'misskey-js';
|
|||
import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
|
||||
import XRecipient from './notification-recipient.item.vue';
|
||||
import XHeader from '@/pages/admin/_header_.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ i18n.ts.notificationSetting }}</MkButton>
|
||||
</div>
|
||||
|
||||
<MkInfo v-if="!defaultStore.reactiveState.abusesTutorial.value" closable @close="closeTutorial()">
|
||||
<MkInfo v-if="!store.r.abusesTutorial.value" closable @close="closeTutorial()">
|
||||
{{ i18n.ts._abuseUserReport.resolveTutorial }}
|
||||
</MkInfo>
|
||||
|
||||
|
|
@ -59,18 +59,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, shallowRef, ref } from 'vue';
|
||||
import { computed, useTemplateRef, ref } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import XAbuseReport from '@/components/MkAbuseReport.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { store } from '@/store.js';
|
||||
|
||||
const reports = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
const reports = useTemplateRef('reports');
|
||||
|
||||
const state = ref('unresolved');
|
||||
const reporterOrigin = ref('combined');
|
||||
|
|
@ -93,14 +93,14 @@ function resolved(reportId) {
|
|||
}
|
||||
|
||||
function closeTutorial() {
|
||||
defaultStore.set('abusesTutorial', false);
|
||||
store.set('abusesTutorial', false);
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.abuseReports,
|
||||
icon: 'ti ti-exclamation-circle',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -96,9 +96,9 @@ import MkFolder from '@/components/MkFolder.vue';
|
|||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
|
||||
const ads = ref<Misskey.entities.Ad[]>([]);
|
||||
|
||||
|
|
@ -255,7 +255,7 @@ const headerActions = computed(() => [{
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.ads,
|
||||
icon: 'ti ti-ad',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -94,9 +94,9 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
|||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
|
||||
|
|
@ -199,7 +199,7 @@ const headerActions = computed(() => [{
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.announcements,
|
||||
icon: 'ti ti-speakerphone',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -196,14 +196,14 @@ import MkRadios from '@/components/MkRadios.vue';
|
|||
import MkInput from '@/components/MkInput.vue';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { fetchInstance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useForm } from '@/scripts/use-form.js';
|
||||
import { useForm } from '@/use/use-form.js';
|
||||
import MkFormFooter from '@/components/MkFormFooter.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { ApiWithDialogCustomErrors } from '@/os.js';
|
||||
import type { ApiWithDialogCustomErrors } from '@/os.js';
|
||||
|
||||
const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue'));
|
||||
|
||||
|
|
|
|||
|
|
@ -128,10 +128,10 @@ import MkTextarea from '@/components/MkTextarea.vue';
|
|||
import FromSlot from '@/components/form/slot.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { instance, fetchInstance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkColorInput from '@/components/MkColorInput.vue';
|
||||
import { host } from '@@/js/config.js';
|
||||
|
|
@ -210,7 +210,7 @@ function chooseNewLike(ev: MouseEvent) {
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.branding,
|
||||
icon: 'ti ti-paint',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -71,25 +71,25 @@ export type EmojiSearchQuery = {
|
|||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent, onMounted, ref, nextTick, useCssModule } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js';
|
||||
import type { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
|
||||
import type { GridSetting } from '@/components/grid/grid.js';
|
||||
import * as os from '@/os.js';
|
||||
import {
|
||||
emptyStrToEmptyArray,
|
||||
emptyStrToNull,
|
||||
emptyStrToUndefined,
|
||||
RequestLogItem,
|
||||
roleIdsParser,
|
||||
} from '@/pages/admin/custom-emojis-manager.impl.js';
|
||||
import MkGrid from '@/components/grid/MkGrid.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { validators } from '@/components/grid/cell-validators.js';
|
||||
import { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import MkPagingButtons from '@/components/MkPagingButtons.vue';
|
||||
import { GridSetting } from '@/components/grid/grid.js';
|
||||
import { selectFile } from '@/scripts/select-file.js';
|
||||
import { selectFile } from '@/utility/select-file.js';
|
||||
import { copyGridDataToClipboard, removeDataFromGrid } from '@/components/grid/grid-utils.js';
|
||||
import { useLoading } from "@/components/hook/useLoading.js";
|
||||
import { useLoading } from '@/components/hook/useLoading.js';
|
||||
|
||||
type GridItem = {
|
||||
checked: boolean;
|
||||
|
|
@ -108,7 +108,7 @@ type GridItem = {
|
|||
publicUrl?: string | null;
|
||||
originalUrl?: string | null;
|
||||
type: string | null;
|
||||
}
|
||||
};
|
||||
|
||||
function setupGrid(): GridSetting {
|
||||
const $style = useCssModule();
|
||||
|
|
@ -464,8 +464,8 @@ async function refreshCustomEmojis() {
|
|||
aliases: emptyStrToUndefined(searchQuery.value.aliases),
|
||||
category: emptyStrToUndefined(searchQuery.value.category),
|
||||
license: emptyStrToUndefined(searchQuery.value.license),
|
||||
isSensitive: searchQuery.value.sensitive ? Boolean(searchQuery.value.sensitive).valueOf() : undefined,
|
||||
localOnly: searchQuery.value.localOnly ? Boolean(searchQuery.value.localOnly).valueOf() : undefined,
|
||||
isSensitive: searchQuery.value.sensitive != null ? Boolean(searchQuery.value.sensitive).valueOf() : undefined,
|
||||
localOnly: searchQuery.value.localOnly != null ? Boolean(searchQuery.value.localOnly).valueOf() : undefined,
|
||||
updatedAtFrom: emptyStrToUndefined(searchQuery.value.updatedAtFrom),
|
||||
updatedAtTo: emptyStrToUndefined(searchQuery.value.updatedAtTo),
|
||||
roleIds: searchQuery.value.roles.map(it => it.id),
|
||||
|
|
@ -592,7 +592,7 @@ const headerActions = computed(() => [{
|
|||
dispose();
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
}]);
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -78,30 +78,32 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { onMounted, ref, useCssModule } from 'vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js';
|
||||
import type { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
|
||||
import type { DroppedFile } from '@/utility/file-drop.js';
|
||||
import type { GridSetting } from '@/components/grid/grid.js';
|
||||
import type { GridRow } from '@/components/grid/row.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import {
|
||||
emptyStrToEmptyArray,
|
||||
emptyStrToNull,
|
||||
RequestLogItem,
|
||||
roleIdsParser,
|
||||
} from '@/pages/admin/custom-emojis-manager.impl.js';
|
||||
import MkGrid from '@/components/grid/MkGrid.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { validators } from '@/components/grid/cell-validators.js';
|
||||
import { chooseFileFromDrive, chooseFileFromPc } from '@/scripts/select-file.js';
|
||||
import { uploadFile } from '@/scripts/upload.js';
|
||||
import { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
|
||||
import { DroppedFile, extractDroppedItems, flattenDroppedFiles } from '@/scripts/file-drop.js';
|
||||
import { chooseFileFromDrive, chooseFileFromPc } from '@/utility/select-file.js';
|
||||
import { uploadFile } from '@/utility/upload.js';
|
||||
import { extractDroppedItems, flattenDroppedFiles } from '@/utility/file-drop.js';
|
||||
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
|
||||
import { GridSetting } from '@/components/grid/grid.js';
|
||||
import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';
|
||||
import { GridRow } from '@/components/grid/row.js';
|
||||
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const MAXIMUM_EMOJI_REGISTER_COUNT = 100;
|
||||
|
||||
|
|
@ -122,7 +124,7 @@ type GridItem = {
|
|||
localOnly: boolean;
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: { id: string, name: string }[];
|
||||
type: string | null;
|
||||
}
|
||||
};
|
||||
|
||||
function setupGrid(): GridSetting {
|
||||
const $style = useCssModule();
|
||||
|
|
@ -242,8 +244,8 @@ function setupGrid(): GridSetting {
|
|||
|
||||
const uploadFolders = ref<FolderItem[]>([]);
|
||||
const gridItems = ref<GridItem[]>([]);
|
||||
const selectedFolderId = ref(defaultStore.state.uploadFolder);
|
||||
const keepOriginalUploading = ref(defaultStore.state.keepOriginalUploading);
|
||||
const selectedFolderId = ref(prefer.s.uploadFolder);
|
||||
const keepOriginalUploading = ref(prefer.s.keepOriginalUploading);
|
||||
const directoryToCategory = ref<boolean>(false);
|
||||
const registerButtonDisabled = ref<boolean>(false);
|
||||
const requestLogs = ref<RequestLogItem[]>([]);
|
||||
|
|
|
|||
|
|
@ -143,35 +143,32 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { computed, onMounted, ref, useCssModule } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkRemoteEmojiEditDialog from '@/components/MkRemoteEmojiEditDialog.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkGrid from '@/components/grid/MkGrid.vue';
|
||||
import {
|
||||
emptyStrToUndefined,
|
||||
GridSortOrderKey,
|
||||
gridSortOrderKeys,
|
||||
RequestLogItem,
|
||||
} from '@/pages/admin/custom-emojis-manager.impl.js';
|
||||
import { GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
|
||||
import { emptyStrToUndefined, gridSortOrderKeys } from '@/pages/admin/custom-emojis-manager.impl.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { GridSetting } from '@/components/grid/grid.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { deviceKind } from '@/utility/device-kind.js';
|
||||
import MkPagingButtons from '@/components/MkPagingButtons.vue';
|
||||
import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue';
|
||||
import { SortOrder } from '@/components/MkSortOrderEditor.define.js';
|
||||
import { useLoading } from '@/components/hook/useLoading.js';
|
||||
|
||||
import type { GridSortOrderKey, RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js';
|
||||
import type { GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
|
||||
import type { GridSetting } from '@/components/grid/grid.js';
|
||||
import type { SortOrder } from '@/components/MkSortOrderEditor.define.js';
|
||||
|
||||
type GridItem = {
|
||||
checked: boolean;
|
||||
id: string;
|
||||
url: string;
|
||||
name: string;
|
||||
host: string;
|
||||
}
|
||||
};
|
||||
|
||||
function setupGrid(): GridSetting {
|
||||
const $style = useCssModule();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { delay, http, HttpResponse } from 'msw';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { entities } from 'misskey-js';
|
||||
import { commonHandlers } from '../../../.storybook/mocks.js';
|
||||
import { emoji } from '../../../.storybook/fakes.js';
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import XGridLocalComponent from '@/pages/admin/custom-emojis-manager.local.vue';
|
||||
import XGridRemoteComponent from '@/pages/admin/custom-emojis-manager.remote.vue';
|
||||
import MkPageHeader from '@/components/global/MkPageHeader.vue';
|
||||
|
|
@ -36,7 +36,7 @@ const headerTabs = computed(() => [{
|
|||
title: i18n.ts.remote,
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
definePage(computed(() => ({
|
||||
title: i18n.ts.customEmojis,
|
||||
icon: 'ti ti-icons',
|
||||
needWideArea: true,
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
||||
<MkSpacer :contentMax="800" :marginMin="16" :marginMax="32">
|
||||
<FormSuspense v-slot="{ result: database }" :p="databasePromiseFactory">
|
||||
<MkKeyValue v-for="table in database" :key="table[0]" oneline style="margin: 1em 0;">
|
||||
|
|
@ -14,18 +13,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkKeyValue>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import bytes from '@/filters/bytes.js';
|
||||
import number from '@/filters/number.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
|
||||
const databasePromiseFactory = () => misskeyApi('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size));
|
||||
|
||||
|
|
@ -33,7 +32,7 @@ const headerActions = computed(() => []);
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.database,
|
||||
icon: 'ti ti-database',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -73,10 +73,10 @@ import FormSuspense from '@/components/form/suspense.vue';
|
|||
import FormSplit from '@/components/form/split.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { fetchInstance, instance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
const enableEmail = ref<boolean>(false);
|
||||
|
|
@ -130,7 +130,7 @@ function save() {
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.emailServer,
|
||||
icon: 'ti ti-mail',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -9,6 +9,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_gaps_m">
|
||||
<MkFolder>
|
||||
<template #label>Google Analytics<span class="_beta">{{ i18n.ts.beta }}</span></template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="googleAnalyticsMeasurementId">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>Measurement ID</template>
|
||||
</MkInput>
|
||||
<MkButton primary @click="save_googleAnalytics">Save</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #label>DeepL Translation</template>
|
||||
|
||||
|
|
@ -65,10 +77,10 @@ import MkButton from '@/components/MkButton.vue';
|
|||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { fetchInstance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
||||
const deeplAuthKey = ref<string | null>('');
|
||||
|
|
@ -78,14 +90,17 @@ const deeplFreeInstance = ref<string | null>('');
|
|||
const libreTranslateURL = ref<string | null>('');
|
||||
const libreTranslateKey = ref<string | null>('');
|
||||
|
||||
const googleAnalyticsMeasurementId = ref<string>('');
|
||||
|
||||
async function init() {
|
||||
const meta = await misskeyApi('admin/meta');
|
||||
deeplAuthKey.value = meta.deeplAuthKey;
|
||||
deeplAuthKey.value = meta.deeplAuthKey ?? '';
|
||||
deeplIsPro.value = meta.deeplIsPro;
|
||||
deeplFreeMode.value = meta.deeplFreeMode;
|
||||
deeplFreeInstance.value = meta.deeplFreeInstance;
|
||||
libreTranslateURL.value = meta.libreTranslateURL;
|
||||
libreTranslateKey.value = meta.libreTranslateKey;
|
||||
googleAnalyticsMeasurementId.value = meta.googleAnalyticsMeasurementId ?? '';
|
||||
}
|
||||
|
||||
function save_deepl() {
|
||||
|
|
@ -108,11 +123,19 @@ function save_libre() {
|
|||
});
|
||||
}
|
||||
|
||||
function save_googleAnalytics() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
googleAnalyticsMeasurementId: googleAnalyticsMeasurementId.value,
|
||||
}).then(() => {
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.externalServices,
|
||||
icon: 'ph-arrow-square-out ph-bold ph-lg',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ import MkPagination from '@/components/MkPagination.vue';
|
|||
import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
|
||||
const host = ref('');
|
||||
const state = ref('federating');
|
||||
|
|
@ -116,7 +116,7 @@ const headerActions = computed(() => []);
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.federation,
|
||||
icon: 'ti ti-whirl',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -42,9 +42,9 @@ 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 { lookupFile } from '@/scripts/admin-lookup.js';
|
||||
import { lookupFile } from '@/utility/admin-lookup.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
|
||||
const origin = ref('local');
|
||||
const type = ref<string | null>(null);
|
||||
|
|
@ -85,7 +85,7 @@ const headerActions = computed(() => [{
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.files,
|
||||
icon: 'ti ti-cloud',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -27,24 +27,25 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkSpacer>
|
||||
</div>
|
||||
<div v-if="!(narrow && currentPage?.route.name == null)" class="main">
|
||||
<RouterView nested/>
|
||||
<NestedRouterView/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onActivated, onMounted, onUnmounted, provide, watch, ref, computed } from 'vue';
|
||||
import type { SuperMenuDef } from '@/components/MkSuperMenu.vue';
|
||||
import type { PageMetadata } from '@/page.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkSuperMenu from '@/components/MkSuperMenu.vue';
|
||||
import type { SuperMenuDef } from '@/components/MkSuperMenu.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { instance } from '@/instance.js';
|
||||
import { lookup } from '@/scripts/lookup.js';
|
||||
import { lookup } from '@/utility/lookup.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { lookupUser, lookupUserByEmail, lookupFile } from '@/scripts/admin-lookup.js';
|
||||
import { PageMetadata, definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { lookupUser, lookupUserByEmail, lookupFile } from '@/utility/admin-lookup.js';
|
||||
import { definePage, provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
||||
const isEmpty = (x: string | null) => x == null || x === '';
|
||||
|
||||
|
|
@ -339,7 +340,7 @@ const headerActions = computed(() => []);
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => INFO.value);
|
||||
definePage(() => INFO.value);
|
||||
|
||||
defineExpose({
|
||||
header: {
|
||||
|
|
|
|||
|
|
@ -55,21 +55,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, shallowRef } from 'vue';
|
||||
import { computed, ref, useTemplateRef } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkInviteCode from '@/components/MkInviteCode.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
|
||||
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
const pagingComponent = useTemplateRef('pagingComponent');
|
||||
|
||||
const type = ref('all');
|
||||
const sort = ref('+createdAt');
|
||||
|
|
@ -113,7 +114,7 @@ function deleted(id: string) {
|
|||
const headerActions = computed(() => []);
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.invite,
|
||||
icon: 'ti ti-user-plus',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -162,10 +162,10 @@ import MkInput from '@/components/MkInput.vue';
|
|||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { fetchInstance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
|
@ -319,7 +319,7 @@ function save_mediaSilencedHosts() {
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.moderation,
|
||||
icon: 'ti ti-shield',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -274,6 +274,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template v-else-if="log.type === 'removeRelay'">
|
||||
<div>{{ i18n.ts.inboxUrl }}: {{ log.info.inbox }}</div>
|
||||
</template>
|
||||
<template v-else-if="log.type === 'updateProxyAccountDescription'">
|
||||
<div :class="$style.diff">
|
||||
<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<details>
|
||||
<summary>raw</summary>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, shallowRef, ref } from 'vue';
|
||||
import { computed, useTemplateRef, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XHeader from './_header_.vue';
|
||||
import XModLog from './modlog.ModLog.vue';
|
||||
|
|
@ -38,10 +38,10 @@ import MkSelect from '@/components/MkSelect.vue';
|
|||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||
|
||||
const logs = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
const logs = useTemplateRef('logs');
|
||||
|
||||
const type = ref<string | null>(null);
|
||||
const moderatorId = ref('');
|
||||
|
|
@ -59,7 +59,7 @@ const headerActions = computed(() => []);
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.moderationLogs,
|
||||
icon: 'ti ti-list-search',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -90,10 +90,10 @@ import MkInput from '@/components/MkInput.vue';
|
|||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { fetchInstance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
const useObjectStorage = ref<boolean>(false);
|
||||
|
|
@ -149,7 +149,7 @@ function save() {
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.objectStorage,
|
||||
icon: 'ti ti-cloud',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -13,18 +13,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, shallowRef, ref } from 'vue';
|
||||
import { onMounted, useTemplateRef, ref } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import gradient from 'chartjs-plugin-gradient';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
|
||||
import { chartVLine } from '@/scripts/chart-vline.js';
|
||||
import { initChart } from '@/scripts/init-chart.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { store } from '@/store.js';
|
||||
import { useChartTooltip } from '@/use/use-chart-tooltip.js';
|
||||
import { chartVLine } from '@/utility/chart-vline.js';
|
||||
import { initChart } from '@/utility/init-chart.js';
|
||||
|
||||
initChart();
|
||||
|
||||
const chartEl = shallowRef<HTMLCanvasElement>(null);
|
||||
const chartEl = useTemplateRef('chartEl');
|
||||
const now = new Date();
|
||||
let chartInstance: Chart = null;
|
||||
const chartLimit = 7;
|
||||
|
|
@ -54,7 +54,7 @@ async function renderChart() {
|
|||
|
||||
const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' });
|
||||
|
||||
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
|
||||
const colorRead = '#3498db';
|
||||
const colorWrite = '#2ecc71';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { commonHandlers } from '../../../.storybook/mocks.js';
|
||||
|
|
|
|||
|
|
@ -20,22 +20,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, shallowRef, ref } from 'vue';
|
||||
import { onMounted, useTemplateRef, ref } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import gradient from 'chartjs-plugin-gradient';
|
||||
import isChromatic from 'chromatic';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
|
||||
import { chartVLine } from '@/scripts/chart-vline.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { alpha } from '@/scripts/color.js';
|
||||
import { initChart } from '@/scripts/init-chart.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { useChartTooltip } from '@/use/use-chart-tooltip.js';
|
||||
import { chartVLine } from '@/utility/chart-vline.js';
|
||||
import { store } from '@/store.js';
|
||||
import { alpha } from '@/utility/color.js';
|
||||
import { initChart } from '@/utility/init-chart.js';
|
||||
|
||||
initChart();
|
||||
|
||||
const chartLimit = 50;
|
||||
const chartEl = shallowRef<HTMLCanvasElement>();
|
||||
const chartEl2 = shallowRef<HTMLCanvasElement>();
|
||||
const chartEl = useTemplateRef('chartEl');
|
||||
const chartEl2 = useTemplateRef('chartEl2');
|
||||
const fetching = ref(true);
|
||||
|
||||
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||
|
|
@ -68,7 +68,7 @@ onMounted(async () => {
|
|||
|
||||
const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' });
|
||||
|
||||
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
const succColor = '#87e000';
|
||||
const failColor = '#ff4400';
|
||||
|
||||
|
|
|
|||
|
|
@ -47,13 +47,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import XPie, { type InstanceForPie } from './overview.pie.vue';
|
||||
import XPie from './overview.pie.vue';
|
||||
import type { InstanceForPie } from './overview.pie.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApiGet } from '@/utility/misskey-api.js';
|
||||
import number from '@/filters/number.js';
|
||||
import MkNumberDiff from '@/components/MkNumberDiff.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
|
||||
import { useChartTooltip } from '@/use/use-chart-tooltip.js';
|
||||
|
||||
const topSubInstancesForPie = ref<InstanceForPie[] | null>(null);
|
||||
const topPubInstancesForPie = ref<InstanceForPie[] | null>(null);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
|
||||
<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in">
|
||||
<MkLoading v-if="fetching"/>
|
||||
<div v-else :class="$style.instances">
|
||||
<MkA v-for="(instance, i) in instances" :key="instance.id" v-tooltip.mfm.noDelay="`${instance.name}\n${instance.host}\n${instance.softwareName} ${instance.softwareVersion}`" :to="`/instance-info/${instance.host}`" :class="$style.instance">
|
||||
|
|
@ -18,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const instances = ref<Misskey.entities.FederationInstance[]>([]);
|
||||
const fetching = ref(true);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
|
||||
<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in">
|
||||
<MkLoading v-if="fetching"/>
|
||||
<div v-else :class="$style.root" class="_panel">
|
||||
<MkA v-for="user in moderators" :key="user.id" class="user" :to="`/admin/user/${user.id}`">
|
||||
|
|
@ -18,9 +18,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const moderators = ref<Misskey.entities.UserDetailed[] | null>(null);
|
||||
const fetching = ref(true);
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, shallowRef } from 'vue';
|
||||
import { onMounted, useTemplateRef } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
|
||||
import { initChart } from '@/scripts/init-chart.js';
|
||||
import { useChartTooltip } from '@/use/use-chart-tooltip.js';
|
||||
import { initChart } from '@/utility/init-chart.js';
|
||||
|
||||
export type InstanceForPie = {
|
||||
name: string,
|
||||
|
|
@ -26,7 +26,7 @@ const props = defineProps<{
|
|||
data: InstanceForPie[];
|
||||
}>();
|
||||
|
||||
const chartEl = shallowRef<HTMLCanvasElement>(null);
|
||||
const chartEl = useTemplateRef('chartEl');
|
||||
|
||||
const { handler: externalTooltipHandler } = useChartTooltip({
|
||||
position: 'middle',
|
||||
|
|
@ -41,7 +41,7 @@ onMounted(() => {
|
|||
labels: props.data.map(x => x.name),
|
||||
datasets: [{
|
||||
backgroundColor: props.data.map(x => x.color),
|
||||
borderColor: getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'),
|
||||
borderColor: getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel'),
|
||||
borderWidth: 2,
|
||||
hoverOffset: 0,
|
||||
data: props.data.map(x => x.value),
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, shallowRef } from 'vue';
|
||||
import { onMounted, useTemplateRef } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
|
||||
import { chartVLine } from '@/scripts/chart-vline.js';
|
||||
import { alpha } from '@/scripts/color.js';
|
||||
import { initChart } from '@/scripts/init-chart.js';
|
||||
import { store } from '@/store.js';
|
||||
import { useChartTooltip } from '@/use/use-chart-tooltip.js';
|
||||
import { chartVLine } from '@/utility/chart-vline.js';
|
||||
import { alpha } from '@/utility/color.js';
|
||||
import { initChart } from '@/utility/init-chart.js';
|
||||
|
||||
initChart();
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ const props = defineProps<{
|
|||
type: string;
|
||||
}>();
|
||||
|
||||
const chartEl = shallowRef<HTMLCanvasElement>(null);
|
||||
const chartEl = useTemplateRef('chartEl');
|
||||
|
||||
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ const color =
|
|||
'?' as never;
|
||||
|
||||
onMounted(() => {
|
||||
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
|
||||
chartInstance = new Chart(chartEl.value, {
|
||||
type: 'line',
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue';
|
||||
import { markRaw, onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XChart from './overview.queue.chart.vue';
|
||||
import type { ApQueueDomain } from '@/pages/admin/queue.vue';
|
||||
|
|
@ -48,10 +48,10 @@ const activeSincePrevTick = ref(0);
|
|||
const active = ref(0);
|
||||
const delayed = ref(0);
|
||||
const waiting = ref(0);
|
||||
const chartProcess = shallowRef<InstanceType<typeof XChart>>();
|
||||
const chartActive = shallowRef<InstanceType<typeof XChart>>();
|
||||
const chartDelayed = shallowRef<InstanceType<typeof XChart>>();
|
||||
const chartWaiting = shallowRef<InstanceType<typeof XChart>>();
|
||||
const chartProcess = useTemplateRef('chartProcess');
|
||||
const chartActive = useTemplateRef('chartActive');
|
||||
const chartDelayed = useTemplateRef('chartDelayed');
|
||||
const chartWaiting = useTemplateRef('chartWaiting');
|
||||
|
||||
const props = defineProps<{
|
||||
domain: ApQueueDomain;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
|
||||
<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in">
|
||||
<MkLoading v-if="fetching"/>
|
||||
<div v-else :class="$style.root">
|
||||
<div class="item _panel users">
|
||||
|
|
@ -63,12 +63,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
|
||||
import MkNumberDiff from '@/components/MkNumberDiff.vue';
|
||||
import MkNumber from '@/components/MkNumber.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const stats = ref<Misskey.entities.StatsResponse | null>(null);
|
||||
const usersComparedToThePrevDay = ref<number>();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
|
||||
<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in">
|
||||
<MkLoading v-if="fetching"/>
|
||||
<div v-else class="users">
|
||||
<MkA v-for="(user, i) in newUsers" :key="user.id" :to="`/admin/user/${user.id}`" class="user">
|
||||
|
|
@ -18,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const newUsers = ref<Misskey.entities.UserDetailed[] | null>(null);
|
||||
const fetching = ref(true);
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { markRaw, onMounted, onBeforeUnmount, nextTick, shallowRef, ref, computed } from 'vue';
|
||||
import { markRaw, onMounted, onBeforeUnmount, nextTick, shallowRef, ref, computed, useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XFederation from './overview.federation.vue';
|
||||
import XInstances from './overview.instances.vue';
|
||||
|
|
@ -79,13 +79,13 @@ import XModerators from './overview.moderators.vue';
|
|||
import XHeatmap from './overview.heatmap.vue';
|
||||
import type { InstanceForPie } from './overview.pie.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
|
||||
const rootEl = shallowRef<HTMLElement>();
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
const serverInfo = ref<Misskey.entities.ServerInfoResponse | null>(null);
|
||||
const topSubInstancesForPie = ref<InstanceForPie[] | null>(null);
|
||||
const topPubInstancesForPie = ref<InstanceForPie[] | null>(null);
|
||||
|
|
@ -184,7 +184,7 @@ const headerActions = computed(() => []);
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.dashboard,
|
||||
icon: 'ti ti-dashboard',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -111,15 +111,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { ref, computed } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { fetchInstance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
import { useForm } from '@/scripts/use-form.js';
|
||||
import { useForm } from '@/use/use-form.js';
|
||||
import MkFormFooter from '@/components/MkFormFooter.vue';
|
||||
|
||||
const meta = await misskeyApi('admin/meta');
|
||||
|
|
@ -202,7 +202,7 @@ const headerActions = computed(() => []);
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.other,
|
||||
icon: 'ti ti-adjustments',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, shallowRef } from 'vue';
|
||||
import { onMounted, useTemplateRef } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
|
||||
import { chartVLine } from '@/scripts/chart-vline.js';
|
||||
import { alpha } from '@/scripts/color.js';
|
||||
import { initChart } from '@/scripts/init-chart.js';
|
||||
import { store } from '@/store.js';
|
||||
import { useChartTooltip } from '@/use/use-chart-tooltip.js';
|
||||
import { chartVLine } from '@/utility/chart-vline.js';
|
||||
import { alpha } from '@/utility/color.js';
|
||||
import { initChart } from '@/utility/init-chart.js';
|
||||
|
||||
initChart();
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ const props = defineProps<{
|
|||
type: string;
|
||||
}>();
|
||||
|
||||
const chartEl = shallowRef<HTMLCanvasElement>(null);
|
||||
const chartEl = useTemplateRef('chartEl');
|
||||
|
||||
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ const color =
|
|||
'?' as never;
|
||||
|
||||
onMounted(() => {
|
||||
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
|
||||
chartInstance = new Chart(chartEl.value, {
|
||||
type: 'line',
|
||||
|
|
|
|||
|
|
@ -48,12 +48,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue';
|
||||
import { markRaw, onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XChart from './queue.chart.chart.vue';
|
||||
import type { ApQueueDomain } from '@/pages/admin/queue.vue';
|
||||
import number from '@/filters/number.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
|
@ -65,10 +65,10 @@ const active = ref(0);
|
|||
const delayed = ref(0);
|
||||
const waiting = ref(0);
|
||||
const jobs = ref<Misskey.Endpoints[`admin/queue/${ApQueueDomain}-delayed`]['res']>([]);
|
||||
const chartProcess = shallowRef<InstanceType<typeof XChart>>();
|
||||
const chartActive = shallowRef<InstanceType<typeof XChart>>();
|
||||
const chartDelayed = shallowRef<InstanceType<typeof XChart>>();
|
||||
const chartWaiting = shallowRef<InstanceType<typeof XChart>>();
|
||||
const chartProcess = useTemplateRef('chartProcess');
|
||||
const chartActive = useTemplateRef('chartActive');
|
||||
const chartDelayed = useTemplateRef('chartDelayed');
|
||||
const chartWaiting = useTemplateRef('chartWaiting');
|
||||
|
||||
const props = defineProps<{
|
||||
domain: ApQueueDomain;
|
||||
|
|
|
|||
|
|
@ -16,13 +16,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, type Ref } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import * as config from '@@/js/config.js';
|
||||
import XQueue from './queue.chart.vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import type { Ref } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import * as config from '@@/js/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
export type ApQueueDomain = 'deliver' | 'inbox';
|
||||
|
|
@ -53,14 +54,7 @@ function promoteAllQueues() {
|
|||
});
|
||||
}
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'ti ti-external-link',
|
||||
text: i18n.ts.dashboard,
|
||||
handler: () => {
|
||||
window.open(config.url + '/queue', '_blank', 'noopener');
|
||||
},
|
||||
}]);
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => [{
|
||||
key: 'deliver',
|
||||
|
|
@ -70,7 +64,7 @@ const headerTabs = computed(() => [{
|
|||
title: 'Inbox',
|
||||
}]);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.jobQueue,
|
||||
icon: 'ti ti-clock-play',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -29,9 +29,9 @@ import * as Misskey from 'misskey-js';
|
|||
import XHeader from './_header_.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
|
||||
const relays = ref<Misskey.entities.AdminRelaysListResponse>([]);
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ const headerActions = computed(() => [{
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.relays,
|
||||
icon: 'ti ti-planet',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -28,12 +28,12 @@ import { v4 as uuid } from 'uuid';
|
|||
import XHeader from './_header_.vue';
|
||||
import XEditor from './roles.editor.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { rolesCache } from '@/cache.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
@ -87,7 +87,7 @@ async function save() {
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: role.value ? `${i18n.ts._role.edit}: ${role.value.name}` : i18n.ts._role.new,
|
||||
icon: 'ti ti-badge',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -219,6 +219,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canChat, 'canChat'])">
|
||||
<template #label>{{ i18n.ts._role._options.canChat }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.canChat.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.canChat.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canChat)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.canChat.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="role.policies.canChat.value" :disabled="role.policies.canChat.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange v-model="role.policies.canChat.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
|
||||
<template #suffix>
|
||||
|
|
@ -769,7 +789,7 @@ import MkRange from '@/components/MkRange.vue';
|
|||
import FormSlot from '@/components/form/slot.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', v: any): void;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkPagination :pagination="usersPagination" :displayLimit="50">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.noUsers }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -67,15 +67,15 @@ import XHeader from './_header_.vue';
|
|||
import XEditor from './roles.editor.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
@ -170,7 +170,7 @@ const headerActions = computed(() => []);
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: `${i18n.ts.role}: ${role.name}`,
|
||||
icon: 'ti ti-badge',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -77,6 +77,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canChat, 'canChat'])">
|
||||
<template #label>{{ i18n.ts._role._options.canChat }}</template>
|
||||
<template #suffix>{{ policies.canChat ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canChat">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
|
||||
<template #suffix>{{ policies.mentionLimit }}</template>
|
||||
|
|
@ -317,12 +325,12 @@ import MkRange from '@/components/MkRange.vue';
|
|||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { instance, fetchInstance } from '@/instance.js';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
||||
const router = useRouter();
|
||||
const baseRoleQ = ref('');
|
||||
|
|
@ -365,7 +373,7 @@ const headerActions = computed(() => []);
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.roles,
|
||||
icon: 'ti ti-badges',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -103,11 +103,11 @@ import MkRange from '@/components/MkRange.vue';
|
|||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { fetchInstance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { useForm } from '@/scripts/use-form.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { useForm } from '@/use/use-form.js';
|
||||
import MkFormFooter from '@/components/MkFormFooter.vue';
|
||||
|
||||
const meta = await misskeyApi('admin/meta');
|
||||
|
|
@ -162,7 +162,7 @@ const headerActions = computed(() => []);
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.security,
|
||||
icon: 'ti ti-lock',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ import XHeader from './_header_.vue';
|
|||
import * as os from '@/os.js';
|
||||
import { fetchInstance, instance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ const remove = (index: number): void => {
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.serverRules,
|
||||
icon: 'ti ti-checkbox',
|
||||
}));
|
||||
|
|
@ -122,7 +122,7 @@ definePageMetadata(() => ({
|
|||
border-radius: var(--MI-radius-sm);
|
||||
|
||||
&:hover {
|
||||
background: var(--MI_THEME-X5);
|
||||
background: light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -270,15 +270,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkFolder>
|
||||
<template #icon><i class="ti ti-ghost"></i></template>
|
||||
<template #label>{{ i18n.ts.proxyAccount }}</template>
|
||||
<template v-if="proxyAccountForm.modified.value" #footer>
|
||||
<MkFormFooter :form="proxyAccountForm"/>
|
||||
</template>
|
||||
|
||||
<div class="_gaps">
|
||||
<MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.proxyAccount }}</template>
|
||||
<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template>
|
||||
</MkKeyValue>
|
||||
|
||||
<MkButton primary @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</MkButton>
|
||||
<MkTextarea v-model="proxyAccountForm.state.description" :max="500" tall mfmAutocomplete :mfmPreview="true">
|
||||
<template #label>{{ i18n.ts._profile.description }}</template>
|
||||
<template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
|
||||
</MkTextarea>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
|
|
@ -288,7 +290,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, reactive } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
|
|
@ -296,20 +298,20 @@ import MkTextarea from '@/components/MkTextarea.vue';
|
|||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { fetchInstance, instance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import { useForm } from '@/scripts/use-form.js';
|
||||
import { useForm } from '@/use/use-form.js';
|
||||
import MkFormFooter from '@/components/MkFormFooter.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
|
||||
const meta = await misskeyApi('admin/meta');
|
||||
|
||||
const proxyAccount = ref(meta.proxyAccountId ? await misskeyApi('users/show', { userId: meta.proxyAccountId }) : null);
|
||||
const proxyAccount = await misskeyApi('users/show', { userId: meta.proxyAccountId });
|
||||
|
||||
const infoForm = useForm({
|
||||
name: meta.name ?? '',
|
||||
|
|
@ -425,16 +427,14 @@ const federationForm = useForm({
|
|||
fetchInstance(true);
|
||||
});
|
||||
|
||||
function chooseProxyAccount() {
|
||||
os.selectUser({ localOnly: true }).then(user => {
|
||||
proxyAccount.value = user;
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
proxyAccountId: user.id,
|
||||
}).then(() => {
|
||||
fetchInstance(true);
|
||||
});
|
||||
const proxyAccountForm = useForm({
|
||||
description: proxyAccount.description,
|
||||
}, async (state) => {
|
||||
await os.apiWithDialog('admin/update-proxy-account', {
|
||||
description: state.description,
|
||||
});
|
||||
}
|
||||
fetchInstance(true);
|
||||
});
|
||||
|
||||
async function genKeys() {
|
||||
if (serviceWorkerForm.savedState.swPrivateKey) {
|
||||
|
|
@ -450,7 +450,7 @@ async function genKeys() {
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.general,
|
||||
icon: 'ti ti-settings',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -30,11 +30,11 @@ import { computed, onMounted, ref } from 'vue';
|
|||
import { entities } from 'misskey-js';
|
||||
import XItem from './system-webhook.item.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import XHeader from '@/pages/admin/_header_.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { showSystemWebhookEditorDialog } from '@/components/MkSystemWebhookEditor.impl.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ onMounted(async () => {
|
|||
await fetchWebhooks();
|
||||
});
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: 'SystemWebhook',
|
||||
icon: 'ti ti-webhook',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_gaps">
|
||||
<div :class="$style.inputs">
|
||||
<MkButton style="margin-left: auto" @click="resetQuery">{{ i18n.ts.reset }}</MkButton>
|
||||
</div>
|
||||
<div :class="$style.inputs">
|
||||
<MkSelect v-model="sort" style="flex: 1;">
|
||||
<template #label>{{ i18n.ts.sort }}</template>
|
||||
|
|
@ -58,25 +61,36 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, shallowRef, ref } from 'vue';
|
||||
import { computed, useTemplateRef, ref, watchEffect } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import { defaultMemoryStorage } from '@/memory-storage';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
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/admin-lookup.js';
|
||||
import { lookupUser } from '@/utility/admin-lookup.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import { dateString } from '@/filters/date.js';
|
||||
|
||||
const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
type SearchQuery = {
|
||||
sort?: string;
|
||||
state?: string;
|
||||
origin?: string;
|
||||
username?: string;
|
||||
hostname?: string;
|
||||
};
|
||||
|
||||
const sort = ref('+createdAt');
|
||||
const state = ref('all');
|
||||
const origin = ref('local');
|
||||
const searchUsername = ref('');
|
||||
const searchHost = ref('');
|
||||
const paginationComponent = useTemplateRef('paginationComponent');
|
||||
const storedQuery = JSON.parse(defaultMemoryStorage.getItem('admin-users-query') ?? '{}') as SearchQuery;
|
||||
|
||||
const sort = ref(storedQuery.sort ?? '+createdAt');
|
||||
const state = ref(storedQuery.state ?? 'all');
|
||||
const origin = ref(storedQuery.origin ?? 'local');
|
||||
const searchUsername = ref(storedQuery.username ?? '');
|
||||
const searchHost = ref(storedQuery.hostname ?? '');
|
||||
const pagination = {
|
||||
endpoint: 'admin/show-users' as const,
|
||||
limit: 10,
|
||||
|
|
@ -120,6 +134,14 @@ function show(user) {
|
|||
os.pageWindow(`/admin/user/${user.id}`);
|
||||
}
|
||||
|
||||
function resetQuery() {
|
||||
sort.value = '+createdAt';
|
||||
state.value = 'all';
|
||||
origin.value = 'local';
|
||||
searchUsername.value = '';
|
||||
searchHost.value = '';
|
||||
}
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
icon: 'ti ti-search',
|
||||
text: i18n.ts.search,
|
||||
|
|
@ -138,7 +160,17 @@ const headerActions = computed(() => [{
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
watchEffect(() => {
|
||||
defaultMemoryStorage.setItem('admin-users-query', JSON.stringify({
|
||||
sort: sort.value,
|
||||
state: state.value,
|
||||
origin: origin.value,
|
||||
username: searchUsername.value,
|
||||
hostname: searchHost.value,
|
||||
}));
|
||||
});
|
||||
|
||||
definePage(() => ({
|
||||
title: i18n.ts.users,
|
||||
icon: 'ti ti-users',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -4,23 +4,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
|
||||
<PageWithHeader>
|
||||
<MkSpacer :contentMax="500">
|
||||
<div class="_gaps">
|
||||
<MkAd v-for="ad in instance.ads" :key="ad.id" :specify="ad"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.ads,
|
||||
icon: 'ti ti-ad',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -4,14 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
||||
<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 : ''"
|
||||
:enterActiveClass="prefer.s.animation ? $style.fadeEnterActive : ''"
|
||||
:leaveActiveClass="prefer.s.animation ? $style.fadeLeaveActive : ''"
|
||||
:enterFromClass="prefer.s.animation ? $style.fadeEnterFrom : ''"
|
||||
:leaveToClass="prefer.s.animation ? $style.fadeLeaveTo : ''"
|
||||
mode="out-in"
|
||||
>
|
||||
<div v-if="announcement" :key="announcement.id" class="_panel" :class="$style.announcement">
|
||||
|
|
@ -44,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkLoading v-else/>
|
||||
</Transition>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -52,11 +51,12 @@ 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 { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { $i, updateAccountPartial } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { updateCurrentAccountPartial } from '@/accounts.js';
|
||||
|
||||
const props = defineProps<{
|
||||
announcementId: string;
|
||||
|
|
@ -90,7 +90,7 @@ async function read(target: Misskey.entities.Announcement): Promise<void> {
|
|||
target.isRead = true;
|
||||
await misskeyApi('i/read-announcement', { announcementId: target.id });
|
||||
if ($i) {
|
||||
updateAccountPartial({
|
||||
updateCurrentAccountPartial({
|
||||
unreadAnnouncements: $i.unreadAnnouncements.filter((a: { id: string; }) => a.id !== target.id),
|
||||
});
|
||||
}
|
||||
|
|
@ -102,7 +102,7 @@ const headerActions = computed(() => []);
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: announcement.value ? announcement.value.title : i18n.ts.announcements,
|
||||
icon: 'ti ti-speakerphone',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
|
||||
<MkSpacer :contentMax="800">
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<div :key="tab" class="_gaps">
|
||||
<div class="_gaps">
|
||||
<MkInfo v-if="$i && $i.hasUnreadAnnouncement && tab === 'current'" warn>{{ i18n.ts.youHaveUnreadAnnouncements }}</MkInfo>
|
||||
<MkPagination ref="paginationEl" :key="tab" v-slot="{items}" :pagination="tab === 'current' ? paginationCurrent : paginationPast" class="_gaps">
|
||||
<section v-for="announcement in items" :key="announcement.id" class="_panel" :class="$style.announcement">
|
||||
|
|
@ -43,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</MkHorizontalSwipe>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -53,10 +52,11 @@ import MkButton from '@/components/MkButton.vue';
|
|||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { $i, updateAccountPartial } from '@/account.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { updateCurrentAccountPartial } from '@/accounts.js';
|
||||
|
||||
const paginationCurrent = {
|
||||
endpoint: 'announcements' as const,
|
||||
|
|
@ -94,7 +94,7 @@ async function read(target) {
|
|||
return a;
|
||||
});
|
||||
misskeyApi('i/read-announcement', { announcementId: target.id });
|
||||
updateAccountPartial({
|
||||
updateCurrentAccountPartial({
|
||||
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id),
|
||||
});
|
||||
}
|
||||
|
|
@ -111,7 +111,7 @@ const headerTabs = computed(() => [{
|
|||
icon: 'ti ti-point',
|
||||
}]);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.announcements,
|
||||
icon: 'ti ti-speakerphone',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :displayBackButton="true" :tabs="headerTabs"/></template>
|
||||
<PageWithHeader :actions="headerActions" :displayBackButton="true" :tabs="headerTabs">
|
||||
<MkSpacer :contentMax="800">
|
||||
<div ref="rootEl">
|
||||
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
|
||||
|
|
@ -20,19 +19,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref, shallowRef } from 'vue';
|
||||
import { computed, watch, ref, useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkTimeline from '@/components/MkTimeline.vue';
|
||||
import { scroll } from '@@/js/scroll.js';
|
||||
import MkTimeline from '@/components/MkTimeline.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
@ -42,8 +41,8 @@ const props = defineProps<{
|
|||
|
||||
const antenna = ref<Misskey.entities.Antenna | null>(null);
|
||||
const queue = ref(0);
|
||||
const rootEl = shallowRef<HTMLElement>();
|
||||
const tlEl = shallowRef<InstanceType<typeof MkTimeline>>();
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
const tlEl = useTemplateRef('tlEl');
|
||||
|
||||
function queueUpdated(q) {
|
||||
queue.value = q;
|
||||
|
|
@ -88,7 +87,7 @@ const headerActions = computed(() => antenna.value ? [{
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: antenna.value ? antenna.value.name : i18n.ts.antennas,
|
||||
icon: 'ti ti-antenna',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
||||
<MkSpacer :contentMax="700">
|
||||
<div class="_gaps_m">
|
||||
<div class="_gaps_m">
|
||||
|
|
@ -30,19 +29,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import JSON5 from 'json5';
|
||||
import { Endpoints } from 'misskey-js';
|
||||
import type { Endpoints } from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { definePage } from '@/page.js';
|
||||
|
||||
const body = ref('{}');
|
||||
const endpoint = ref('');
|
||||
|
|
@ -87,7 +86,7 @@ const headerActions = computed(() => []);
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: 'API console',
|
||||
icon: 'ti ti-terminal-2',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
@ -38,7 +38,7 @@ const emit = defineEmits<{
|
|||
const app = computed(() => props.session.app);
|
||||
|
||||
const name = computed(() => {
|
||||
const el = document.createElement('div');
|
||||
const el = window.document.createElement('div');
|
||||
el.textContent = app.value.name;
|
||||
return el.innerHTML;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
||||
<MkSpacer :contentMax="500">
|
||||
<div v-if="state == 'fetch-session-error'">
|
||||
<p>{{ i18n.ts.somethingHappened }}</p>
|
||||
|
|
@ -38,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSignin @login="onLogin"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -46,10 +45,11 @@ import { onMounted, ref, computed } from 'vue';
|
|||
import * as Misskey from 'misskey-js';
|
||||
import XForm from './auth.form.vue';
|
||||
import MkSignin from '@/components/MkSignin.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { $i, login } from '@/account.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { login } from '@/accounts.js';
|
||||
|
||||
const props = defineProps<{
|
||||
token: string;
|
||||
|
|
@ -84,7 +84,7 @@ function accepted() {
|
|||
} else if (session.value && session.value.app.callbackUrl) {
|
||||
const url = new URL(session.value.app.callbackUrl);
|
||||
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(url.protocol)) throw new Error('invalid url');
|
||||
location.href = `${session.value.app.callbackUrl}?token=${session.value.token}`;
|
||||
window.location.href = `${session.value.app.callbackUrl}?token=${session.value.token}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ const headerActions = computed(() => []);
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts._auth.shareAccessTitle,
|
||||
icon: 'ti ti-apps',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -68,14 +68,14 @@ import MkInput from '@/components/MkInput.vue';
|
|||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import { signinRequired } from '@/account.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
|
||||
const $i = signinRequired();
|
||||
const $i = ensureSignin();
|
||||
|
||||
const props = defineProps<{
|
||||
avatarDecoration?: any,
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_gaps">
|
||||
<div :class="$style.decorations">
|
||||
|
|
@ -22,19 +21,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, defineAsyncComponent } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { signinRequired } from '@/account.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
|
||||
const $i = signinRequired();
|
||||
const $i = ensureSignin();
|
||||
|
||||
const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]);
|
||||
|
||||
|
|
@ -86,7 +85,7 @@ const headerActions = computed(() => [{
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.avatarDecorations,
|
||||
icon: 'ti ti-sparkles',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
||||
<MkSpacer :contentMax="700">
|
||||
<div v-if="channelId == null || channel != null" class="_gaps_m">
|
||||
<MkInput v-model="name">
|
||||
|
|
@ -65,7 +64,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -74,15 +73,15 @@ import * as Misskey from 'misskey-js';
|
|||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkColorInput from '@/components/MkColorInput.vue';
|
||||
import { selectFile } from '@/scripts/select-file.js';
|
||||
import { selectFile } from '@/utility/select-file.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
|
|
@ -202,7 +201,7 @@ const headerActions = computed(() => []);
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: props.channelId ? i18n.ts._channel.edit : i18n.ts._channel.create,
|
||||
icon: 'ti ti-device-tv',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="700" :class="$style.main">
|
||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
|
||||
<MkSpacer :contentMax="700">
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<div v-if="channel && tab === 'overview'" key="overview" class="_gaps">
|
||||
<div v-if="channel && tab === 'overview'" class="_gaps">
|
||||
<div class="_panel" :class="$style.bannerContainer">
|
||||
<XChannelFollowButton :channel="channel" :full="true" :class="$style.subscribe"/>
|
||||
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ti ti-star"></i></MkButton>
|
||||
|
|
@ -33,18 +32,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
<div v-if="channel && tab === 'timeline'" key="timeline" class="_gaps">
|
||||
<div v-if="channel && tab === 'timeline'" class="_gaps">
|
||||
<MkInfo v-if="channel.isArchived" warn>{{ i18n.ts.thisChannelArchived }}</MkInfo>
|
||||
|
||||
<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
|
||||
<MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
|
||||
<MkPostForm v-if="$i && prefer.r.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
|
||||
|
||||
<MkTimeline :key="channelId + withRenotes + onlyFiles" src="channel" :channel="channelId" :withRenotes="withRenotes" :onlyFiles="onlyFiles" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'featured'" key="featured">
|
||||
<div v-else-if="tab === 'featured'">
|
||||
<MkNotes :pagination="featuredPagination"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'search'" key="search">
|
||||
<div v-else-if="tab === 'search'">
|
||||
<div v-if="notesSearchAvailable" class="_gaps">
|
||||
<div>
|
||||
<MkInput v-model="searchQuery" @enter="search()">
|
||||
|
|
@ -69,38 +68,38 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { url } from '@@/js/config.js';
|
||||
import type { PageHeaderItem } from '@/types/page-header.js';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
import MkTimeline from '@/components/MkTimeline.vue';
|
||||
import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { $i, iAmModerator } from '@/account.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { $i, iAmModerator } from '@/i.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { deviceKind } from '@/utility/device-kind.js';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import { url } from '@@/js/config.js';
|
||||
import { favoritedChannelsCache } from '@/cache.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import MkNote from '@/components/MkNote.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { PageHeaderItem } from '@/types/page-header.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { notesSearchAvailable } from '@/scripts/check-permissions.js';
|
||||
import { isSupportShare } from '@/utility/navigator.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import { notesSearchAvailable } from '@/utility/check-permissions.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { deepMerge } from '@/scripts/merge.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { deepMerge } from '@/utility/merge.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
@ -241,7 +240,6 @@ const headerActions = computed(() => {
|
|||
return;
|
||||
}
|
||||
copyToClipboard(`${url}/channels/${channel.value.id}`);
|
||||
os.success();
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -296,17 +294,13 @@ const headerTabs = computed(() => [{
|
|||
icon: 'ti ti-search',
|
||||
}]);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: channel.value ? channel.value.name : i18n.ts.channel,
|
||||
icon: 'ti ti-device-tv',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.main {
|
||||
min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px)));
|
||||
}
|
||||
|
||||
.footer {
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
|
||||
<MkSpacer :contentMax="1200">
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<div v-if="tab === 'search'" key="search" :class="$style.searchRoot">
|
||||
<div v-if="tab === 'search'" :class="$style.searchRoot">
|
||||
<div class="_gaps">
|
||||
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
|
|
@ -25,28 +24,28 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkChannelList :key="key" :pagination="channelPagination"/>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
<div v-if="tab === 'featured'" key="featured">
|
||||
<div v-if="tab === 'featured'">
|
||||
<MkPagination v-slot="{items}" :pagination="featuredPagination">
|
||||
<div :class="$style.root">
|
||||
<MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'favorites'" key="favorites">
|
||||
<div v-else-if="tab === 'favorites'">
|
||||
<MkPagination v-slot="{items}" :pagination="favoritesPagination">
|
||||
<div :class="$style.root">
|
||||
<MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'following'" key="following">
|
||||
<div v-else-if="tab === 'following'">
|
||||
<MkPagination v-slot="{items}" :pagination="followingPagination">
|
||||
<div :class="$style.root">
|
||||
<MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'owned'" key="owned">
|
||||
<div v-else-if="tab === 'owned'">
|
||||
<MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton>
|
||||
<MkPagination v-slot="{items}" :pagination="ownedPagination">
|
||||
<div :class="$style.root">
|
||||
|
|
@ -56,7 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</MkHorizontalSwipe>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -69,9 +68,9 @@ import MkRadios from '@/components/MkRadios.vue';
|
|||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
@ -161,7 +160,7 @@ const headerTabs = computed(() => [{
|
|||
icon: 'ti ti-edit',
|
||||
}]);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.channel,
|
||||
icon: 'ti ti-device-tv',
|
||||
}));
|
||||
|
|
|
|||
245
packages/frontend/src/pages/chat/XMessage.vue
Normal file
245
packages/frontend/src/pages/chat/XMessage.vue
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[$style.root, { [$style.isMe]: isMe }]">
|
||||
<MkAvatar :class="$style.avatar" :user="message.fromUser" :link="!isMe" :preview="false"/>
|
||||
<div :class="$style.body">
|
||||
<MkFukidashi :class="$style.fukidashi" :tail="isMe ? 'right' : 'left'" :accented="isMe">
|
||||
<div v-if="!message.isDeleted" :class="$style.content">
|
||||
<Mfm v-if="message.text" ref="text" class="_selectable" :text="message.text" :i="$i"/>
|
||||
<MkMediaList v-if="message.file" :mediaList="[message.file]" :class="$style.file"/>
|
||||
</div>
|
||||
<div v-else :class="$style.content">
|
||||
<p>{{ i18n.ts.deleted }}</p>
|
||||
</div>
|
||||
</MkFukidashi>
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" style="margin: 8px 0;"/>
|
||||
<div :class="$style.footer">
|
||||
<button class="_textButton" style="color: currentColor;" @click="showMenu"><i class="ti ti-dots-circle-horizontal"></i></button>
|
||||
<MkTime :class="$style.time" :time="message.createdAt"/>
|
||||
<MkA v-if="isSearchResult && message.toRoomId" :to="`/chat/room/${message.toRoomId}`">{{ message.toRoom.name }}</MkA>
|
||||
<MkA v-if="isSearchResult && message.toUserId && isMe" :to="`/chat/user/${message.toUserId}`">@{{ message.toUser.username }}</MkA>
|
||||
</div>
|
||||
<TransitionGroup
|
||||
:enterActiveClass="prefer.s.animation ? $style.transition_reaction_enterActive : ''"
|
||||
:leaveActiveClass="prefer.s.animation ? $style.transition_reaction_leaveActive : ''"
|
||||
:enterFromClass="prefer.s.animation ? $style.transition_reaction_enterFrom : ''"
|
||||
:leaveToClass="prefer.s.animation ? $style.transition_reaction_leaveTo : ''"
|
||||
:moveClass="prefer.s.animation ? $style.transition_reaction_move : ''"
|
||||
tag="div" :class="$style.reactions"
|
||||
>
|
||||
<div v-for="record in message.reactions" :key="record.reaction + record.user.id" :class="$style.reaction">
|
||||
<MkAvatar :user="record.user" :link="false" :class="$style.reactionAvatar"/>
|
||||
<MkReactionIcon
|
||||
:withTooltip="true"
|
||||
:reaction="record.reaction.replace(/^:(\w+):$/, ':$1@.:')"
|
||||
:noStyle="true"
|
||||
:class="$style.reactionIcon"
|
||||
/>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent } from 'vue';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { url } from '@@/js/config.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkFukidashi from '@/components/MkFukidashi.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import MkMediaList from '@/components/MkMediaList.vue';
|
||||
import { reactionPicker } from '@/utility/reaction-picker.js';
|
||||
import * as sound from '@/utility/sound.js';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const props = defineProps<{
|
||||
message: Misskey.entities.ChatMessageLite | Misskey.entities.ChatMessage;
|
||||
isSearchResult?: boolean;
|
||||
}>();
|
||||
|
||||
const isMe = computed(() => props.message.fromUserId === $i.id);
|
||||
const urls = computed(() => props.message.text ? extractUrlFromMfm(mfm.parse(props.message.text)) : []);
|
||||
|
||||
function react(ev: MouseEvent) {
|
||||
reactionPicker.show(ev.currentTarget ?? ev.target, null, async (reaction) => {
|
||||
sound.playMisskeySfx('reaction');
|
||||
|
||||
misskeyApi('chat/messages/react', {
|
||||
messageId: props.message.id,
|
||||
reaction: reaction,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
const menu: MenuItem[] = [];
|
||||
|
||||
if (!isMe.value) {
|
||||
menu.push({
|
||||
text: i18n.ts.reaction,
|
||||
icon: 'ti ti-mood-plus',
|
||||
action: (ev) => {
|
||||
react(ev);
|
||||
},
|
||||
});
|
||||
|
||||
menu.push({
|
||||
type: 'divider',
|
||||
});
|
||||
}
|
||||
|
||||
menu.push({
|
||||
text: i18n.ts.copyContent,
|
||||
icon: 'ti ti-copy',
|
||||
action: () => {
|
||||
copyToClipboard(props.message.text);
|
||||
},
|
||||
});
|
||||
|
||||
menu.push({
|
||||
type: 'divider',
|
||||
});
|
||||
|
||||
if (isMe.value) {
|
||||
menu.push({
|
||||
text: i18n.ts.delete,
|
||||
icon: 'ti ti-trash',
|
||||
danger: true,
|
||||
action: () => {
|
||||
misskeyApi('chat/messages/delete', {
|
||||
messageId: props.message.id,
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
menu.push({
|
||||
text: i18n.ts.reportAbuse,
|
||||
icon: 'ti ti-exclamation-circle',
|
||||
action: () => {
|
||||
const localUrl = `${url}/chat/messages/${props.message.id}`;
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
|
||||
user: props.message.fromUser,
|
||||
initialComment: `${localUrl}\n-----\n`,
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
os.popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transition_reaction_move,
|
||||
.transition_reaction_enterActive,
|
||||
.transition_reaction_leaveActive {
|
||||
transition: opacity 0.2s cubic-bezier(0,.5,.5,1), transform 0.2s cubic-bezier(0,.5,.5,1) !important;
|
||||
}
|
||||
.transition_reaction_enterFrom,
|
||||
.transition_reaction_leaveTo {
|
||||
opacity: 0;
|
||||
transform: scale(0.7);
|
||||
}
|
||||
.transition_reaction_leaveActive {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
&.isMe {
|
||||
flex-direction: row-reverse;
|
||||
text-align: right;
|
||||
|
||||
.content {
|
||||
color: var(--MI_THEME-fgOnAccent);
|
||||
}
|
||||
|
||||
.footer {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: sticky;
|
||||
top: calc(16px + var(--MI-stickyTop, 0px));
|
||||
display: block;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow: clip;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.file {
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
margin-top: 4px;
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
.time {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.reactions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.reaction {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: solid 1px var(--MI_THEME-divider);
|
||||
border-radius: 999px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.reactionAvatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.reactionIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
</style>
|
||||
41
packages/frontend/src/pages/chat/XRoom.vue
Normal file
41
packages/frontend/src/pages/chat/XRoom.vue
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkA :to="`/chat/room/${room.id}`" class="_panel _gaps_s" :class="$style.root">
|
||||
<div :class="$style.header">
|
||||
<div style="font-weight: bold;">{{ room.name }}</div>
|
||||
<MkAvatar :user="room.owner" :link="false" :class="$style.headerAvatar"/>
|
||||
</div>
|
||||
<hr>
|
||||
<div>{{ room.description }}</div>
|
||||
</MkA>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
const props = defineProps<{
|
||||
room: Misskey.entities.ChatRoom;
|
||||
}>();
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.headerAvatar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
252
packages/frontend/src/pages/chat/home.home.vue
Normal file
252
packages/frontend/src/pages/chat/home.home.vue
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<MkButton primary gradate rounded :class="$style.start" @click="start"><i class="ti ti-plus"></i> {{ i18n.ts.startChat }}</MkButton>
|
||||
|
||||
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
|
||||
|
||||
<MkInput
|
||||
v-model="searchQuery"
|
||||
:placeholder="i18n.ts._chat.searchMessages"
|
||||
type="search"
|
||||
>
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
</MkInput>
|
||||
|
||||
<MkButton v-if="searchQuery.length > 0" primary rounded @click="search">{{ i18n.ts.search }}</MkButton>
|
||||
|
||||
<MkFoldableSection v-if="searched">
|
||||
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<div v-for="message in searchResults" :key="message.id" :class="$style.searchResultItem">
|
||||
<XMessage :message="message" :isSearchResult="true"/>
|
||||
</div>
|
||||
</div>
|
||||
</MkFoldableSection>
|
||||
|
||||
<MkFoldableSection>
|
||||
<template #header>{{ i18n.ts._chat.history }}</template>
|
||||
|
||||
<div v-if="history.length > 0" class="_gaps_s">
|
||||
<MkA
|
||||
v-for="item in history"
|
||||
:key="item.id"
|
||||
:class="[$style.message, { [$style.isMe]: item.isMe, [$style.isRead]: item.message.isRead }]"
|
||||
class="_panel"
|
||||
:to="item.message.toRoomId ? `/chat/room/${item.message.toRoomId}` : `/chat/user/${item.other!.id}`"
|
||||
>
|
||||
<MkAvatar v-if="item.other" :class="$style.messageAvatar" :user="item.other" indicator :preview="false"/>
|
||||
<div :class="$style.messageBody">
|
||||
<header v-if="item.message.toRoom" :class="$style.messageHeader">
|
||||
<span :class="$style.messageHeaderName">{{ item.message.toRoom.name }}</span>
|
||||
<MkTime :time="item.message.createdAt" :class="$style.messageHeaderTime"/>
|
||||
</header>
|
||||
<header v-else :class="$style.messageHeader">
|
||||
<MkUserName :class="$style.messageHeaderName" :user="item.other!"/>
|
||||
<MkAcct :class="$style.messageHeaderUsername" :user="item.other!"/>
|
||||
<MkTime :time="item.message.createdAt" :class="$style.messageHeaderTime"/>
|
||||
</header>
|
||||
<div :class="$style.messageBodyText"><span v-if="item.isMe" :class="$style.youSaid">{{ i18n.ts.you }}:</span>{{ item.message.text }}</div>
|
||||
</div>
|
||||
</MkA>
|
||||
</div>
|
||||
<div v-if="!fetching && history.length == 0" class="_fullinfo">
|
||||
<div>{{ i18n.ts._chat.noHistory }}</div>
|
||||
</div>
|
||||
<MkLoading v-if="fetching"/>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XMessage from './XMessage.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import * as os from '@/os.js';
|
||||
import { updateCurrentAccountPartial } from '@/accounts.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const fetching = ref(true);
|
||||
const history = ref<{
|
||||
id: string;
|
||||
message: Misskey.entities.ChatMessage;
|
||||
other: Misskey.entities.ChatMessage['fromUser'] | Misskey.entities.ChatMessage['toUser'] | null;
|
||||
isMe: boolean;
|
||||
}[]>([]);
|
||||
|
||||
const searchQuery = ref('');
|
||||
const searched = ref(false);
|
||||
const searchResults = ref<Misskey.entities.ChatMessage[]>([]);
|
||||
|
||||
function start(ev: MouseEvent) {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts._chat.individualChat,
|
||||
caption: i18n.ts._chat.individualChat_description,
|
||||
icon: 'ti ti-user',
|
||||
action: () => { startUser(); },
|
||||
}, { type: 'divider' }, {
|
||||
type: 'parent',
|
||||
text: i18n.ts._chat.roomChat,
|
||||
caption: i18n.ts._chat.roomChat_description,
|
||||
icon: 'ti ti-users-group',
|
||||
children: [{
|
||||
text: i18n.ts._chat.createRoom,
|
||||
icon: 'ti ti-plus',
|
||||
action: () => { createRoom(); },
|
||||
}],
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
async function startUser() {
|
||||
os.selectUser().then(user => {
|
||||
router.push(`/chat/user/${user.id}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function createRoom() {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: i18n.ts.name,
|
||||
minLength: 1,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
const room = await misskeyApi('chat/rooms/create', {
|
||||
name: result,
|
||||
});
|
||||
|
||||
router.push(`/chat/room/${room.id}`);
|
||||
}
|
||||
|
||||
async function search() {
|
||||
const res = await misskeyApi('chat/messages/search', {
|
||||
query: searchQuery.value,
|
||||
});
|
||||
|
||||
searchResults.value = res;
|
||||
searched.value = true;
|
||||
}
|
||||
|
||||
async function fetchHistory() {
|
||||
fetching.value = true;
|
||||
|
||||
const [userMessages, roomMessages] = await Promise.all([
|
||||
misskeyApi('chat/history', { room: false }),
|
||||
misskeyApi('chat/history', { room: true }),
|
||||
]);
|
||||
|
||||
history.value = [...userMessages, ...roomMessages]
|
||||
.toSorted((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
.map(m => ({
|
||||
id: m.id,
|
||||
message: m,
|
||||
other: m.room == null ? (m.fromUserId === $i.id ? m.toUser : m.fromUser) : null,
|
||||
isMe: m.fromUserId === $i.id,
|
||||
}));
|
||||
|
||||
fetching.value = false;
|
||||
|
||||
updateCurrentAccountPartial({ hasUnreadChatMessages: false });
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchHistory();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.start {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.message {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 16px 24px;
|
||||
|
||||
&.isRead,
|
||||
&.isMe {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&:not(.isMe):not(.isRead) {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
background-color: var(--MI_THEME-accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messageAvatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin: 0 16px 0 0;
|
||||
}
|
||||
|
||||
.messageBody {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.messageHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.messageHeaderName {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.messageHeaderUsername {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.messageHeaderTime {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.messageBodyText {
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.youSaid {
|
||||
font-weight: bold;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.searchResultItem {
|
||||
padding: 12px;
|
||||
border: solid 1px var(--MI_THEME-divider);
|
||||
border-radius: 12px;
|
||||
}
|
||||
</style>
|
||||
98
packages/frontend/src/pages/chat/home.invitations.vue
Normal file
98
packages/frontend/src/pages/chat/home.invitations.vue
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<div v-if="invitations.length > 0" class="_gaps_s">
|
||||
<MkFolder v-for="invitation in invitations" :key="invitation.id" :defaultOpen="true">
|
||||
<template #icon><i class="ti ti-users-group"></i></template>
|
||||
<template #label>{{ invitation.room.name }}</template>
|
||||
<template #suffix><MkTime :time="invitation.createdAt"/></template>
|
||||
<template #footer>
|
||||
<div class="_buttons">
|
||||
<MkButton primary @click="join(invitation)"><i class="ti ti-plus"></i> {{ i18n.ts._chat.join }}</MkButton>
|
||||
<MkButton danger @click="ignore(invitation)"><i class="ti ti-x"></i> {{ i18n.ts._chat.ignore }}</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div :class="$style.invitationBody">
|
||||
<MkAvatar :user="invitation.room.owner" :class="$style.invitationBodyAvatar" link/>
|
||||
<div style="flex: 1;" class="_gaps_s">
|
||||
<MkUserName :user="invitation.room.owner"/>
|
||||
<hr>
|
||||
<div>{{ invitation.room.description === '' ? i18n.ts.noDescription : invitation.room.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
<div v-if="!fetching && invitations.length == 0" class="_fullinfo">
|
||||
<div>{{ i18n.ts._chat.noInvitations }}</div>
|
||||
</div>
|
||||
<MkLoading v-if="fetching"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import * as os from '@/os.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const fetching = ref(true);
|
||||
const invitations = ref<Misskey.entities.ChatRoomInvitation[]>([]);
|
||||
|
||||
async function fetchInvitations() {
|
||||
fetching.value = true;
|
||||
|
||||
const res = await misskeyApi('chat/rooms/invitations/inbox', {
|
||||
});
|
||||
|
||||
invitations.value = res;
|
||||
|
||||
fetching.value = false;
|
||||
}
|
||||
|
||||
async function join(invitation: Misskey.entities.ChatRoomInvitation) {
|
||||
await misskeyApi('chat/rooms/join', {
|
||||
roomId: invitation.room.id,
|
||||
});
|
||||
|
||||
router.push(`/chat/room/${invitation.room.id}`);
|
||||
}
|
||||
|
||||
async function ignore(invitation: Misskey.entities.ChatRoomInvitation) {
|
||||
await misskeyApi('chat/rooms/invitations/ignore', {
|
||||
roomId: invitation.room.id,
|
||||
});
|
||||
|
||||
invitations.value = invitations.value.filter(i => i.id !== invitation.id);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchInvitations();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.invitationBody {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.invitationBodyAvatar {
|
||||
margin-right: 12px;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
}
|
||||
</style>
|
||||
54
packages/frontend/src/pages/chat/home.joiningRooms.vue
Normal file
54
packages/frontend/src/pages/chat/home.joiningRooms.vue
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<div v-if="memberships.length > 0" class="_gaps_s">
|
||||
<XRoom v-for="membership in memberships" :key="membership.id" :room="membership.room"/>
|
||||
</div>
|
||||
<div v-if="!fetching && memberships.length == 0" class="_fullinfo">
|
||||
<div>{{ i18n.ts._chat.noRooms }}</div>
|
||||
</div>
|
||||
<MkLoading v-if="fetching"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XRoom from './XRoom.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const fetching = ref(true);
|
||||
const memberships = ref<Misskey.entities.ChatRoomMembership[]>([]);
|
||||
|
||||
async function fetchRooms() {
|
||||
fetching.value = true;
|
||||
|
||||
const res = await misskeyApi('chat/rooms/joining', {
|
||||
});
|
||||
|
||||
memberships.value = res;
|
||||
|
||||
fetching.value = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchRooms();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
</style>
|
||||
54
packages/frontend/src/pages/chat/home.ownedRooms.vue
Normal file
54
packages/frontend/src/pages/chat/home.ownedRooms.vue
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<div v-if="rooms.length > 0" class="_gaps_s">
|
||||
<XRoom v-for="room in rooms" :key="room.id" :room="room"/>
|
||||
</div>
|
||||
<div v-if="!fetching && rooms.length == 0" class="_fullinfo">
|
||||
<div>{{ i18n.ts._chat.noRooms }}</div>
|
||||
</div>
|
||||
<MkLoading v-if="fetching"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XRoom from './XRoom.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const fetching = ref(true);
|
||||
const rooms = ref<Misskey.entities.ChatRoom[]>([]);
|
||||
|
||||
async function fetchRooms() {
|
||||
fetching.value = true;
|
||||
|
||||
const res = await misskeyApi('chat/rooms/owned', {
|
||||
});
|
||||
|
||||
rooms.value = res;
|
||||
|
||||
fetching.value = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchRooms();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
</style>
|
||||
60
packages/frontend/src/pages/chat/home.vue
Normal file
60
packages/frontend/src/pages/chat/home.vue
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
|
||||
<MkPolkadots v-if="tab === 'home'" accented/>
|
||||
<MkSpacer :contentMax="700">
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<XHome v-if="tab === 'home'"/>
|
||||
<XInvitations v-else-if="tab === 'invitations'"/>
|
||||
<XJoiningRooms v-else-if="tab === 'joiningRooms'"/>
|
||||
<XOwnedRooms v-else-if="tab === 'ownedRooms'"/>
|
||||
</MkHorizontalSwipe>
|
||||
</MkSpacer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import XHome from './home.home.vue';
|
||||
import XInvitations from './home.invitations.vue';
|
||||
import XJoiningRooms from './home.joiningRooms.vue';
|
||||
import XOwnedRooms from './home.ownedRooms.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import MkPolkadots from '@/components/MkPolkadots.vue';
|
||||
|
||||
const tab = ref('home');
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => [{
|
||||
key: 'home',
|
||||
title: i18n.ts._chat.home,
|
||||
icon: 'ti ti-home',
|
||||
}, {
|
||||
key: 'invitations',
|
||||
title: i18n.ts._chat.invitations,
|
||||
icon: 'ti ti-ticket',
|
||||
}, {
|
||||
key: 'joiningRooms',
|
||||
title: i18n.ts._chat.joiningRooms,
|
||||
icon: 'ti ti-users-group',
|
||||
}, {
|
||||
key: 'ownedRooms',
|
||||
title: i18n.ts._chat.yourRooms,
|
||||
icon: 'ti ti-settings',
|
||||
}]);
|
||||
|
||||
definePage(() => ({
|
||||
title: i18n.ts.chat + ' (beta)',
|
||||
icon: 'ti ti-message',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
</style>
|
||||
55
packages/frontend/src/pages/chat/message.vue
Normal file
55
packages/frontend/src/pages/chat/message.vue
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<PageWithHeader>
|
||||
<MkSpacer :contentMax="700">
|
||||
<div v-if="initializing">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<XMessage :message="message"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, useTemplateRef, computed, watch, onMounted, nextTick, onBeforeUnmount, onDeactivated, onActivated } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XMessage from './XMessage.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
messageId?: string;
|
||||
}>();
|
||||
|
||||
const initializing = ref(true);
|
||||
const message = ref<Misskey.entities.ChatMessage>();
|
||||
|
||||
async function initialize() {
|
||||
initializing.value = true;
|
||||
|
||||
message.value = await misskeyApi('chat/messages/show', {
|
||||
messageId: props.messageId,
|
||||
});
|
||||
|
||||
initializing.value = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initialize();
|
||||
});
|
||||
|
||||
definePage({
|
||||
title: i18n.ts.chat,
|
||||
});
|
||||
</script>
|
||||
333
packages/frontend/src/pages/chat/room.form.vue
Normal file
333
packages/frontend/src/pages/chat/room.form.vue
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="$style.root"
|
||||
@dragover.stop="onDragover"
|
||||
@drop.stop="onDrop"
|
||||
>
|
||||
<textarea
|
||||
ref="textareaEl"
|
||||
v-model="text"
|
||||
:class="$style.textarea"
|
||||
class="_acrylic"
|
||||
:placeholder="i18n.ts.inputMessageHere"
|
||||
:readonly="textareaReadOnly"
|
||||
@keydown="onKeydown"
|
||||
@paste="onPaste"
|
||||
></textarea>
|
||||
<footer :class="$style.footer">
|
||||
<div v-if="file" :class="$style.file" @click="file = null">{{ file.name }}</div>
|
||||
<div :class="$style.buttons">
|
||||
<button class="_button" :class="$style.button" @click="chooseFile"><i class="ti ti-photo-plus"></i></button>
|
||||
<button class="_button" :class="$style.button" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
|
||||
<button class="_button" :class="[$style.button, $style.send]" :disabled="!canSend || sending" :title="i18n.ts.send" @click="send">
|
||||
<template v-if="!sending"><i class="ti ti-send"></i></template><template v-if="sending"><MkLoading :em="true"/></template>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
<input ref="fileEl" style="display: none;" type="file" @change="onChangeFile"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, watch, ref, shallowRef, computed, nextTick, readonly } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
//import insertTextAtCursor from 'insert-text-at-cursor';
|
||||
import { throttle } from 'throttle-debounce';
|
||||
import { formatTimeString } from '@/utility/format-time-string.js';
|
||||
import { selectFile } from '@/utility/select-file.js';
|
||||
import * as os from '@/os.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { uploadFile } from '@/utility/upload.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { Autocomplete } from '@/utility/autocomplete.js';
|
||||
import { emojiPicker } from '@/utility/emoji-picker.js';
|
||||
|
||||
const props = defineProps<{
|
||||
user?: Misskey.entities.UserDetailed | null;
|
||||
room?: Misskey.entities.ChatRoom | null;
|
||||
}>();
|
||||
|
||||
const textareaEl = shallowRef<HTMLTextAreaElement>();
|
||||
const fileEl = shallowRef<HTMLInputElement>();
|
||||
|
||||
const text = ref<string>('');
|
||||
const file = ref<Misskey.entities.DriveFile | null>(null);
|
||||
const sending = ref(false);
|
||||
const textareaReadOnly = ref(false);
|
||||
|
||||
const canSend = computed(() => (text.value != null && text.value !== '') || file.value != null);
|
||||
|
||||
function getDraftKey() {
|
||||
return props.user ? 'user:' + props.user.id : 'room:' + props.room?.id;
|
||||
}
|
||||
|
||||
watch([text, file], saveDraft);
|
||||
|
||||
async function onPaste(ev: ClipboardEvent) {
|
||||
if (!ev.clipboardData) return;
|
||||
|
||||
const pastedFileName = 'yyyy-MM-dd HH-mm-ss [{{number}}]';
|
||||
|
||||
const clipboardData = ev.clipboardData;
|
||||
const items = clipboardData.items;
|
||||
|
||||
if (items.length === 1) {
|
||||
if (items[0].kind === 'file') {
|
||||
const pastedFile = items[0].getAsFile();
|
||||
if (!pastedFile) return;
|
||||
const lio = pastedFile.name.lastIndexOf('.');
|
||||
const ext = lio >= 0 ? pastedFile.name.slice(lio) : '';
|
||||
const formatted = formatTimeString(new Date(pastedFile.lastModified), pastedFileName).replace(/{{number}}/g, '1') + ext;
|
||||
if (formatted) upload(pastedFile, formatted);
|
||||
}
|
||||
} else {
|
||||
if (items[0].kind === 'file') {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.onlyOneFileCanBeAttached,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onDragover(ev: DragEvent) {
|
||||
if (!ev.dataTransfer) return;
|
||||
|
||||
const isFile = ev.dataTransfer.items[0].kind === 'file';
|
||||
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
|
||||
if (isFile || isDriveFile) {
|
||||
ev.preventDefault();
|
||||
switch (ev.dataTransfer.effectAllowed) {
|
||||
case 'all':
|
||||
case 'uninitialized':
|
||||
case 'copy':
|
||||
case 'copyLink':
|
||||
case 'copyMove':
|
||||
ev.dataTransfer.dropEffect = 'copy';
|
||||
break;
|
||||
case 'linkMove':
|
||||
case 'move':
|
||||
ev.dataTransfer.dropEffect = 'move';
|
||||
break;
|
||||
default:
|
||||
ev.dataTransfer.dropEffect = 'none';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onDrop(ev: DragEvent): void {
|
||||
if (!ev.dataTransfer) return;
|
||||
|
||||
// ファイルだったら
|
||||
if (ev.dataTransfer.files.length === 1) {
|
||||
ev.preventDefault();
|
||||
upload(ev.dataTransfer.files[0]);
|
||||
return;
|
||||
} else if (ev.dataTransfer.files.length > 1) {
|
||||
ev.preventDefault();
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.onlyOneFileCanBeAttached,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//#region ドライブのファイル
|
||||
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||
if (driveFile != null && driveFile !== '') {
|
||||
file.value = JSON.parse(driveFile);
|
||||
ev.preventDefault();
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
||||
function onKeydown(ev: KeyboardEvent) {
|
||||
if ((ev.key === 'Enter') && (ev.ctrlKey || ev.metaKey)) {
|
||||
send();
|
||||
}
|
||||
}
|
||||
|
||||
function chooseFile(ev: MouseEvent) {
|
||||
selectFile(ev.currentTarget ?? ev.target, i18n.ts.selectFile).then(selectedFile => {
|
||||
file.value = selectedFile;
|
||||
});
|
||||
}
|
||||
|
||||
function onChangeFile() {
|
||||
if (fileEl.value.files![0]) upload(fileEl.value.files[0]);
|
||||
}
|
||||
|
||||
function upload(fileToUpload: File, name?: string) {
|
||||
uploadFile(fileToUpload, prefer.s.uploadFolder, name).then(res => {
|
||||
file.value = res;
|
||||
});
|
||||
}
|
||||
|
||||
function send() {
|
||||
if (!canSend.value) return;
|
||||
|
||||
sending.value = true;
|
||||
|
||||
if (props.user) {
|
||||
misskeyApi('chat/messages/create-to-user', {
|
||||
toUserId: props.user.id,
|
||||
text: text.value ? text.value : undefined,
|
||||
fileId: file.value ? file.value.id : undefined,
|
||||
}).then(message => {
|
||||
clear();
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
}).then(() => {
|
||||
sending.value = false;
|
||||
});
|
||||
} else if (props.room) {
|
||||
misskeyApi('chat/messages/create-to-room', {
|
||||
toRoomId: props.room.id,
|
||||
text: text.value ? text.value : undefined,
|
||||
fileId: file.value ? file.value.id : undefined,
|
||||
}).then(message => {
|
||||
clear();
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
}).then(() => {
|
||||
sending.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
text.value = '';
|
||||
file.value = null;
|
||||
deleteDraft();
|
||||
}
|
||||
|
||||
function saveDraft() {
|
||||
const drafts = JSON.parse(miLocalStorage.getItem('chatMessageDrafts') || '{}');
|
||||
|
||||
drafts[getDraftKey()] = {
|
||||
updatedAt: new Date(),
|
||||
data: {
|
||||
text: text.value,
|
||||
file: file.value,
|
||||
},
|
||||
};
|
||||
|
||||
miLocalStorage.setItem('chatMessageDrafts', JSON.stringify(drafts));
|
||||
}
|
||||
|
||||
function deleteDraft() {
|
||||
const drafts = JSON.parse(miLocalStorage.getItem('chatMessageDrafts') || '{}');
|
||||
|
||||
delete drafts[getDraftKey()];
|
||||
|
||||
miLocalStorage.setItem('chatMessageDrafts', JSON.stringify(drafts));
|
||||
}
|
||||
|
||||
async function insertEmoji(ev: MouseEvent) {
|
||||
textareaReadOnly.value = true;
|
||||
const target = ev.currentTarget ?? ev.target;
|
||||
if (target == null) return;
|
||||
|
||||
// emojiPickerはダイアログが閉じずにtextareaとやりとりするので、
|
||||
// focustrapをかけているとinsertTextAtCursorが効かない
|
||||
// そのため、投稿フォームのテキストに直接注入する
|
||||
// See: https://github.com/misskey-dev/misskey/pull/14282
|
||||
// https://github.com/misskey-dev/misskey/issues/14274
|
||||
|
||||
let pos = textareaEl.value?.selectionStart ?? 0;
|
||||
let posEnd = textareaEl.value?.selectionEnd ?? text.value.length;
|
||||
emojiPicker.show(
|
||||
target as HTMLElement,
|
||||
emoji => {
|
||||
const textBefore = text.value.substring(0, pos);
|
||||
const textAfter = text.value.substring(posEnd);
|
||||
text.value = textBefore + emoji + textAfter;
|
||||
pos += emoji.length;
|
||||
posEnd += emoji.length;
|
||||
},
|
||||
() => {
|
||||
textareaReadOnly.value = false;
|
||||
nextTick(() => focus());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// TODO: detach when unmount
|
||||
new Autocomplete(textareaEl.value, text);
|
||||
|
||||
// 書きかけの投稿を復元
|
||||
const draft = JSON.parse(miLocalStorage.getItem('chatMessageDrafts') || '{}')[getDraftKey()];
|
||||
if (draft) {
|
||||
text.value = draft.data.text;
|
||||
file.value = draft.data.file;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
border-bottom: none;
|
||||
border-radius: 14px 14px 0 0;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
cursor: auto;
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 80px;
|
||||
margin: 0;
|
||||
padding: 16px 16px 0 16px;
|
||||
resize: none;
|
||||
font-size: 1em;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
box-sizing: border-box;
|
||||
color: var(--MI_THEME-fg);
|
||||
field-sizing: content;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background: var(--MI_THEME-panel);
|
||||
}
|
||||
|
||||
.file {
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.button {
|
||||
height: 50px;
|
||||
aspect-ratio: 1;
|
||||
|
||||
&:hover {
|
||||
color: var(--MI_THEME-accent);
|
||||
}
|
||||
}
|
||||
.send {
|
||||
margin-left: auto;
|
||||
color: var(--MI_THEME-accent);
|
||||
}
|
||||
</style>
|
||||
87
packages/frontend/src/pages/chat/room.info.vue
Normal file
87
packages/frontend/src/pages/chat/room.info.vue
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<MkInput v-model="name_" :disabled="!isOwner">
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkTextarea v-model="description_" :disabled="!isOwner">
|
||||
<template #label>{{ i18n.ts.description }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<MkButton v-if="isOwner" primary @click="save">{{ i18n.ts.save }}</MkButton>
|
||||
|
||||
<hr>
|
||||
|
||||
<MkSwitch v-if="!isOwner" v-model="isMuted">
|
||||
<template #label>{{ i18n.ts._chat.muteThisRoom }}</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import * as os from '@/os.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const props = defineProps<{
|
||||
room: Misskey.entities.ChatRoom;
|
||||
}>();
|
||||
|
||||
const isOwner = computed(() => {
|
||||
return props.room.ownerId === $i.id;
|
||||
});
|
||||
|
||||
const name_ = ref(props.room.name);
|
||||
const description_ = ref(props.room.description);
|
||||
|
||||
function save() {
|
||||
os.apiWithDialog('chat/rooms/update', {
|
||||
roomId: props.room.id,
|
||||
name: name_.value,
|
||||
description: description_.value,
|
||||
});
|
||||
}
|
||||
|
||||
const isMuted = ref(props.room.isMuted);
|
||||
|
||||
watch(isMuted, async () => {
|
||||
await os.apiWithDialog('chat/rooms/mute', {
|
||||
roomId: props.room.id,
|
||||
mute: isMuted.value,
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.membership {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.membershipBody {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 8px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
73
packages/frontend/src/pages/chat/room.members.vue
Normal file
73
packages/frontend/src/pages/chat/room.members.vue
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<MkButton v-if="isOwner" primary rounded style="margin: 0 auto;" @click="emit('inviteUser')"><i class="ti ti-plus"></i> {{ i18n.ts._chat.inviteUser }}</MkButton>
|
||||
|
||||
<MkA :class="$style.membershipBody" :to="`${userPage(room.owner)}`">
|
||||
<MkUserCardMini :user="room.owner"/>
|
||||
</MkA>
|
||||
|
||||
<hr>
|
||||
|
||||
<div v-for="membership in memberships" :key="membership.id" :class="$style.membership">
|
||||
<MkA :class="$style.membershipBody" :to="`${userPage(membership.user)}`">
|
||||
<MkUserCardMini :user="membership.user"/>
|
||||
</MkA>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import * as os from '@/os.js';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const props = defineProps<{
|
||||
room: Misskey.entities.ChatRoom;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'inviteUser'): void,
|
||||
}>();
|
||||
|
||||
const isOwner = computed(() => {
|
||||
return props.room.ownerId === $i.id;
|
||||
});
|
||||
|
||||
const memberships = ref<Misskey.entities.ChatRoomMembership[]>([]);
|
||||
|
||||
onMounted(async () => {
|
||||
memberships.value = await misskeyApi('chat/rooms/members', {
|
||||
roomId: props.room.id,
|
||||
limit: 50,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.membership {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.membershipBody {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 8px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
68
packages/frontend/src/pages/chat/room.search.vue
Normal file
68
packages/frontend/src/pages/chat/room.search.vue
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<MkInput
|
||||
v-model="searchQuery"
|
||||
:placeholder="i18n.ts._chat.searchMessages"
|
||||
type="search"
|
||||
>
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
</MkInput>
|
||||
|
||||
<MkButton v-if="searchQuery.length > 0" primary rounded @click="search">{{ i18n.ts.search }}</MkButton>
|
||||
|
||||
<MkFoldableSection v-if="searched">
|
||||
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<div v-for="message in searchResults" :key="message.id" :class="$style.searchResultItem">
|
||||
<XMessage :message="message" :user="message.fromUser" :isSearchResult="true"/>
|
||||
</div>
|
||||
</div>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XMessage from './XMessage.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import * as os from '@/os.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
userId?: string;
|
||||
roomId?: string;
|
||||
}>();
|
||||
|
||||
const searchQuery = ref('');
|
||||
const searched = ref(false);
|
||||
const searchResults = ref<Misskey.entities.ChatMessage[]>([]);
|
||||
|
||||
async function search() {
|
||||
const res = await misskeyApi('chat/messages/search', {
|
||||
query: searchQuery.value,
|
||||
roomId: props.roomId,
|
||||
userId: props.userId,
|
||||
});
|
||||
|
||||
searchResults.value = res;
|
||||
searched.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.searchResultItem {
|
||||
padding: 12px;
|
||||
border: solid 1px var(--MI_THEME-divider);
|
||||
border-radius: 12px;
|
||||
}
|
||||
</style>
|
||||
426
packages/frontend/src/pages/chat/room.vue
Normal file
426
packages/frontend/src/pages/chat/room.vue
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<PageWithHeader v-model:tab="tab" :reversed="tab === 'chat'" :tabs="headerTabs" :actions="headerActions">
|
||||
<MkSpacer v-if="tab === 'chat'" :contentMax="700">
|
||||
<div v-if="initializing">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="messages.length === 0">
|
||||
<div class="_gaps" style="text-align: center;">
|
||||
<div>{{ i18n.ts._chat.noMessagesYet }}</div>
|
||||
<template v-if="user">
|
||||
<div v-if="user.chatScope === 'followers'">{{ i18n.ts._chat.thisUserAllowsChatOnlyFromFollowers }}</div>
|
||||
<div v-else-if="user.chatScope === 'following'">{{ i18n.ts._chat.thisUserAllowsChatOnlyFromFollowing }}</div>
|
||||
<div v-else-if="user.chatScope === 'mutual'">{{ i18n.ts._chat.thisUserAllowsChatOnlyFromMutualFollowing }}</div>
|
||||
<div v-else>{{ i18n.ts._chat.thisUserNotAllowedChatAnyone }}</div>
|
||||
</template>
|
||||
<template v-else-if="room">
|
||||
<div>{{ i18n.ts._chat.inviteUserToChat }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="_gaps">
|
||||
<div v-if="canFetchMore">
|
||||
<MkButton :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMore">{{ i18n.ts.loadMore }}</MkButton>
|
||||
</div>
|
||||
|
||||
<TransitionGroup
|
||||
:enterActiveClass="prefer.s.animation ? $style.transition_x_enterActive : ''"
|
||||
:leaveActiveClass="prefer.s.animation ? $style.transition_x_leaveActive : ''"
|
||||
:enterFromClass="prefer.s.animation ? $style.transition_x_enterFrom : ''"
|
||||
:leaveToClass="prefer.s.animation ? $style.transition_x_leaveTo : ''"
|
||||
:moveClass="prefer.s.animation ? $style.transition_x_move : ''"
|
||||
tag="div" class="_gaps"
|
||||
>
|
||||
<XMessage v-for="message in messages.toReversed()" :key="message.id" :message="message"/>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
|
||||
<MkSpacer v-else-if="tab === 'search'" :contentMax="700">
|
||||
<XSearch :userId="userId" :roomId="roomId"/>
|
||||
</MkSpacer>
|
||||
|
||||
<MkSpacer v-else-if="tab === 'members'" :contentMax="700">
|
||||
<XMembers v-if="room != null" :room="room" @inviteUser="inviteUser"/>
|
||||
</MkSpacer>
|
||||
|
||||
<MkSpacer v-else-if="tab === 'info'" :contentMax="700">
|
||||
<XInfo v-if="room != null" :room="room"/>
|
||||
</MkSpacer>
|
||||
|
||||
<template #footer>
|
||||
<div v-if="tab === 'chat'" :class="$style.footer">
|
||||
<div class="_gaps">
|
||||
<Transition name="fade">
|
||||
<div v-show="showIndicator" :class="$style.new">
|
||||
<button class="_buttonPrimary" :class="$style.newButton" @click="onIndicatorClick">
|
||||
<i class="fas ti-fw fa-arrow-circle-down" :class="$style.newIcon"></i>{{ i18n.ts.newMessageExists }}
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
<XForm v-if="!initializing" :user="user" :room="room" :class="$style.form"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, useTemplateRef, computed, watch, onMounted, nextTick, onBeforeUnmount, onDeactivated, onActivated } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { isTailVisible } from '@@/js/scroll.js';
|
||||
import XMessage from './XMessage.vue';
|
||||
import XForm from './room.form.vue';
|
||||
import XSearch from './room.search.vue';
|
||||
import XMembers from './room.members.vue';
|
||||
import XInfo from './room.info.vue';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import * as os from '@/os.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import * as sound from '@/utility/sound.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
userId?: string;
|
||||
roomId?: string;
|
||||
}>();
|
||||
|
||||
const initializing = ref(true);
|
||||
const moreFetching = ref(false);
|
||||
const messages = ref<Misskey.entities.ChatMessage[]>([]);
|
||||
const canFetchMore = ref(false);
|
||||
const user = ref<Misskey.entities.UserDetailed | null>(null);
|
||||
const room = ref<Misskey.entities.ChatRoom | null>(null);
|
||||
const connection = ref<Misskey.ChannelConnection<Misskey.Channels['chatUser'] | Misskey.Channels['chatRoom']> | null>(null);
|
||||
const showIndicator = ref(false);
|
||||
|
||||
function normalizeMessage(message: Misskey.entities.ChatMessageLite | Misskey.entities.ChatMessage) {
|
||||
const reactions = [...message.reactions];
|
||||
for (const record of reactions) {
|
||||
if (room.value == null && record.user == null) { // 1on1の時はuserは省略される
|
||||
record.user = message.fromUserId === $i.id ? user.value : $i;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...message,
|
||||
fromUser: message.fromUser ?? (message.fromUserId === $i.id ? $i : user),
|
||||
reactions,
|
||||
};
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
const LIMIT = 20;
|
||||
|
||||
initializing.value = true;
|
||||
|
||||
if (props.userId) {
|
||||
const [u, m] = await Promise.all([
|
||||
misskeyApi('users/show', { userId: props.userId }),
|
||||
misskeyApi('chat/messages/user-timeline', { userId: props.userId, limit: LIMIT }),
|
||||
]);
|
||||
|
||||
user.value = u;
|
||||
messages.value = m.map(x => normalizeMessage(x));
|
||||
|
||||
if (messages.value.length === LIMIT) {
|
||||
canFetchMore.value = true;
|
||||
}
|
||||
|
||||
connection.value = useStream().useChannel('chatUser', {
|
||||
otherId: user.value.id,
|
||||
});
|
||||
connection.value.on('message', onMessage);
|
||||
connection.value.on('deleted', onDeleted);
|
||||
connection.value.on('react', onReact);
|
||||
} else {
|
||||
const [r, m] = await Promise.all([
|
||||
misskeyApi('chat/rooms/show', { roomId: props.roomId }),
|
||||
misskeyApi('chat/messages/room-timeline', { roomId: props.roomId, limit: LIMIT }),
|
||||
]);
|
||||
|
||||
room.value = r;
|
||||
messages.value = m.map(x => normalizeMessage(x));
|
||||
|
||||
if (messages.value.length === LIMIT) {
|
||||
canFetchMore.value = true;
|
||||
}
|
||||
|
||||
connection.value = useStream().useChannel('chatRoom', {
|
||||
roomId: room.value.id,
|
||||
});
|
||||
connection.value.on('message', onMessage);
|
||||
connection.value.on('deleted', onDeleted);
|
||||
connection.value.on('react', onReact);
|
||||
}
|
||||
|
||||
window.document.addEventListener('visibilitychange', onVisibilitychange);
|
||||
|
||||
initializing.value = false;
|
||||
}
|
||||
|
||||
let isActivated = true;
|
||||
|
||||
onActivated(() => {
|
||||
isActivated = true;
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
isActivated = false;
|
||||
});
|
||||
|
||||
async function fetchMore() {
|
||||
const LIMIT = 30;
|
||||
|
||||
moreFetching.value = true;
|
||||
|
||||
const newMessages = props.userId ? await misskeyApi('chat/messages/user-timeline', {
|
||||
userId: user.value.id,
|
||||
limit: LIMIT,
|
||||
untilId: messages.value[messages.value.length - 1].id,
|
||||
}) : await misskeyApi('chat/messages/room-timeline', {
|
||||
roomId: room.value.id,
|
||||
limit: LIMIT,
|
||||
untilId: messages.value[messages.value.length - 1].id,
|
||||
});
|
||||
|
||||
messages.value.push(...newMessages.map(x => normalizeMessage(x)));
|
||||
|
||||
canFetchMore.value = newMessages.length === LIMIT;
|
||||
moreFetching.value = false;
|
||||
}
|
||||
|
||||
function onMessage(message: Misskey.entities.ChatMessage) {
|
||||
sound.playMisskeySfx('chatMessage');
|
||||
|
||||
messages.value.unshift(normalizeMessage(message));
|
||||
|
||||
// TODO: DOM的にバックグラウンドになっていないかどうかも考慮する
|
||||
if (message.fromUserId !== $i.id && !window.document.hidden && isActivated) {
|
||||
connection.value?.send('read', {
|
||||
id: message.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (message.fromUserId !== $i.id) {
|
||||
//notifyNewMessage();
|
||||
}
|
||||
}
|
||||
|
||||
function onDeleted(id) {
|
||||
const index = messages.value.findIndex(m => m.id === id);
|
||||
if (index !== -1) {
|
||||
messages.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function onReact(ctx) {
|
||||
const message = messages.value.find(m => m.id === ctx.messageId);
|
||||
if (message) {
|
||||
if (room.value == null) { // 1on1の時はuserは省略される
|
||||
message.reactions.push({
|
||||
reaction: ctx.reaction,
|
||||
user: message.fromUserId === $i.id ? user : $i,
|
||||
});
|
||||
} else {
|
||||
message.reactions.push({
|
||||
reaction: ctx.reaction,
|
||||
user: ctx.user,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onIndicatorClick() {
|
||||
showIndicator.value = false;
|
||||
}
|
||||
|
||||
function notifyNewMessage() {
|
||||
showIndicator.value = true;
|
||||
}
|
||||
|
||||
function onVisibilitychange() {
|
||||
if (window.document.hidden) return;
|
||||
// TODO
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initialize();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
connection.value?.dispose();
|
||||
window.document.removeEventListener('visibilitychange', onVisibilitychange);
|
||||
});
|
||||
|
||||
async function inviteUser() {
|
||||
const invitee = await os.selectUser({ includeSelf: false, localOnly: true });
|
||||
os.apiWithDialog('chat/rooms/invitations/create', {
|
||||
roomId: room.value?.id,
|
||||
userId: invitee.id,
|
||||
});
|
||||
}
|
||||
|
||||
async function leaveRoom() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.areYouSure,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
misskeyApi('chat/rooms/leave', {
|
||||
roomId: room.value?.id,
|
||||
});
|
||||
router.push('/chat');
|
||||
}
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
if (room.value) {
|
||||
if (room.value.ownerId === $i.id) {
|
||||
menuItems.push({
|
||||
text: i18n.ts._chat.inviteUser,
|
||||
icon: 'ti ti-user-plus',
|
||||
action: () => {
|
||||
inviteUser();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
menuItems.push({
|
||||
text: i18n.ts._chat.leave,
|
||||
icon: 'ti ti-x',
|
||||
action: () => {
|
||||
leaveRoom();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
const tab = ref('chat');
|
||||
|
||||
const headerTabs = computed(() => room.value ? [{
|
||||
key: 'chat',
|
||||
title: i18n.ts.chat,
|
||||
icon: 'ti ti-messages',
|
||||
}, {
|
||||
key: 'members',
|
||||
title: i18n.ts._chat.members,
|
||||
icon: 'ti ti-users',
|
||||
}, {
|
||||
key: 'search',
|
||||
title: i18n.ts.search,
|
||||
icon: 'ti ti-search',
|
||||
}, {
|
||||
key: 'info',
|
||||
title: i18n.ts.info,
|
||||
icon: 'ti ti-info-circle',
|
||||
}] : [{
|
||||
key: 'chat',
|
||||
title: i18n.ts.chat,
|
||||
icon: 'ti ti-messages',
|
||||
}, {
|
||||
key: 'search',
|
||||
title: i18n.ts.search,
|
||||
icon: 'ti ti-search',
|
||||
}]);
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
icon: 'ti ti-dots',
|
||||
handler: showMenu,
|
||||
}]);
|
||||
|
||||
definePage(computed(() => !initializing.value ? user.value ? {
|
||||
userName: user,
|
||||
avatar: user,
|
||||
} : {
|
||||
title: room.value?.name,
|
||||
icon: 'ti ti-users',
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transition_x_move,
|
||||
.transition_x_enterActive,
|
||||
.transition_x_leaveActive {
|
||||
transition: opacity 0.2s cubic-bezier(0,.5,.5,1), transform 0.2s cubic-bezier(0,.5,.5,1) !important;
|
||||
}
|
||||
.transition_x_enterFrom,
|
||||
.transition_x_leaveTo {
|
||||
opacity: 0;
|
||||
transform: translateY(80px);
|
||||
}
|
||||
.transition_x_leaveActive {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.root {
|
||||
}
|
||||
|
||||
.more {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.new {
|
||||
width: 100%;
|
||||
padding-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.newButton {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0 12px;
|
||||
line-height: 32px;
|
||||
font-size: 12px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.newIcon {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
|
||||
}
|
||||
|
||||
.form {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
|
||||
.fade-enter-from, .fade-leave-to {
|
||||
transition: opacity 0.5s;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -4,19 +4,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<PageWithHeader>
|
||||
<MkSpacer :contentMax="800">
|
||||
<MkClickerGame/>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MkClickerGame from '@/components/MkClickerGame.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: '🍪👈',
|
||||
icon: 'ti ti-cookie',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions"/></template>
|
||||
<PageWithHeader :actions="headerActions">
|
||||
<MkSpacer :contentMax="800">
|
||||
<div v-if="clip" class="_gaps">
|
||||
<div class="_panel">
|
||||
|
|
@ -27,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkNotes :pagination="pagination" :detail="true"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -36,16 +35,16 @@ import * as Misskey from 'misskey-js';
|
|||
import { url } from '@@/js/config.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import { $i } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { clipsCache } from '@/cache.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { genEmbedCode } from '@/scripts/get-embed-code.js';
|
||||
import { isSupportShare } from '@/utility/navigator.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import { genEmbedCode } from '@/utility/get-embed-code.js';
|
||||
import { assertServerContext, serverContext } from '@/server-context.js';
|
||||
|
||||
// contextは非ログイン状態の情報しかないためログイン時は利用できない
|
||||
|
|
@ -148,7 +147,6 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
|
|||
text: i18n.ts.copyUrl,
|
||||
action: () => {
|
||||
copyToClipboard(`${url}/clips/${clip.value!.id}`);
|
||||
os.success();
|
||||
},
|
||||
}, {
|
||||
icon: 'ti ti-code',
|
||||
|
|
@ -193,7 +191,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
|
|||
},
|
||||
}] : null);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: clip.value ? clip.value.name : i18n.ts.clip,
|
||||
icon: 'ti ti-paperclip',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<PageWithHeader>
|
||||
<MkSpacer :contentMax="600" :marginMin="20">
|
||||
<div class="_gaps_m">
|
||||
<MkKeyValue :copy="instance.maintainerName">
|
||||
|
|
@ -31,17 +30,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkKeyValue>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.inquiry,
|
||||
icon: 'ti ti-help-circle',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -4,91 +4,88 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="ogwlenmc">
|
||||
<div v-if="tab === 'local'" class="local">
|
||||
<MkInput v-model="query" :debounce="true" type="search" autocapitalize="off">
|
||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="ogwlenmc">
|
||||
<div v-if="tab === 'local'" class="local">
|
||||
<MkInput v-model="query" :debounce="true" type="search" autocapitalize="off">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
<template #label>{{ i18n.ts.search }}</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-model="selectMode" style="margin: 8px 0;">
|
||||
<template #label>Select mode</template>
|
||||
</MkSwitch>
|
||||
<div v-if="selectMode" class="_buttons">
|
||||
<MkButton inline @click="selectAll">Select all</MkButton>
|
||||
<MkButton inline @click="setCategoryBulk">Set category</MkButton>
|
||||
<MkButton inline @click="setTagBulk">Set tag</MkButton>
|
||||
<MkButton inline @click="addTagBulk">Add tag</MkButton>
|
||||
<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
|
||||
<MkButton inline @click="setLicenseBulk">Set License</MkButton>
|
||||
<MkButton inline danger @click="delBulk">Delete</MkButton>
|
||||
</div>
|
||||
<MkPagination ref="emojisPaginationComponent" :pagination="pagination" :displayLimit="50">
|
||||
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
||||
<template #default="{items}">
|
||||
<div class="ldhfsamy">
|
||||
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
|
||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
<div class="name _monospace">{{ emoji.name }}</div>
|
||||
<div class="info">{{ emoji.category }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'remote'" class="remote">
|
||||
<FormSplit>
|
||||
<MkInput v-model="queryRemote" :debounce="true" type="search" autocapitalize="off">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
<template #label>{{ i18n.ts.search }}</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-model="selectMode" style="margin: 8px 0;">
|
||||
<template #label>Select mode</template>
|
||||
</MkSwitch>
|
||||
<div v-if="selectMode" class="_buttons">
|
||||
<MkButton inline @click="selectAll">Select all</MkButton>
|
||||
<MkButton inline @click="setCategoryBulk">Set category</MkButton>
|
||||
<MkButton inline @click="setTagBulk">Set tag</MkButton>
|
||||
<MkButton inline @click="addTagBulk">Add tag</MkButton>
|
||||
<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
|
||||
<MkButton inline @click="setLicenseBulk">Set License</MkButton>
|
||||
<MkButton inline danger @click="delBulk">Delete</MkButton>
|
||||
</div>
|
||||
<MkPagination ref="emojisPaginationComponent" :pagination="pagination" :displayLimit="50">
|
||||
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
||||
<template #default="{items}">
|
||||
<div class="ldhfsamy">
|
||||
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
|
||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
<div class="name _monospace">{{ emoji.name }}</div>
|
||||
<div class="info">{{ emoji.category }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'remote'" class="remote">
|
||||
<FormSplit>
|
||||
<MkInput v-model="queryRemote" :debounce="true" type="search" autocapitalize="off">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
<template #label>{{ i18n.ts.search }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="host" :debounce="true">
|
||||
<template #label>{{ i18n.ts.host }}</template>
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
<MkPagination :pagination="remotePagination" :displayLimit="50">
|
||||
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
||||
<template #default="{items}">
|
||||
<div class="ldhfsamy">
|
||||
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
|
||||
<img :src="getProxiedImageUrl(emoji.url, 'emoji')" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
<div class="name _monospace">{{ emoji.name }}</div>
|
||||
<div class="info">{{ emoji.host }}</div>
|
||||
</div>
|
||||
<MkInput v-model="host" :debounce="true">
|
||||
<template #label>{{ i18n.ts.host }}</template>
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
<MkPagination :pagination="remotePagination" :displayLimit="50">
|
||||
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
||||
<template #default="{items}">
|
||||
<div class="ldhfsamy">
|
||||
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
|
||||
<img :src="getProxiedImageUrl(emoji.url, 'emoji')" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
<div class="name _monospace">{{ emoji.name }}</div>
|
||||
<div class="info">{{ emoji.host }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, ref, shallowRef } from 'vue';
|
||||
import { computed, defineAsyncComponent, ref, useTemplateRef } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkRemoteEmojiEditDialog from '@/components/MkRemoteEmojiEditDialog.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import { selectFile } from '@/scripts/select-file.js';
|
||||
import { selectFile } from '@/utility/select-file.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { getProxiedImageUrl } from '@/utility/media-proxy.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
|
||||
const emojisPaginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
const emojisPaginationComponent = useTemplateRef('emojisPaginationComponent');
|
||||
|
||||
const tab = ref('local');
|
||||
const query = ref<string | null>(null);
|
||||
|
|
@ -337,7 +334,7 @@ const headerTabs = computed(() => [{
|
|||
title: i18n.ts.remote,
|
||||
}]);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.customEmojis,
|
||||
icon: 'ph-smiley ph-bold ph-lg',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
<div v-else class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -85,8 +85,8 @@ import bytes from '@/filters/bytes.js';
|
|||
import { infoImageUrl } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { Paging } from '@/components/MkPagination.vue';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<MkSpacer v-if="tab === 'info'" key="info" :contentMax="800">
|
||||
<MkSpacer v-if="tab === 'info'" :contentMax="800">
|
||||
<XFileInfo :fileId="fileId"/>
|
||||
</MkSpacer>
|
||||
|
||||
<MkSpacer v-else-if="tab === 'notes'" key="notes" :contentMax="800">
|
||||
<MkSpacer v-else-if="tab === 'notes'" :contentMax="800">
|
||||
<XNotes :fileId="fileId"/>
|
||||
</MkSpacer>
|
||||
</MkHorizontalSwipe>
|
||||
|
|
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { computed, ref, defineAsyncComponent } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
@ -48,7 +48,7 @@ const headerTabs = computed(() => [{
|
|||
icon: 'ti ti-pencil',
|
||||
}]);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts._fileViewer.title,
|
||||
icon: 'ti ti-file',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { computed, ref } from 'vue';
|
|||
import * as Misskey from 'misskey-js';
|
||||
import XDrive from '@/components/MkDrive.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
|
||||
const folder = ref<Misskey.entities.DriveFolder | null>(null);
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ const headerActions = computed(() => []);
|
|||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: folder.value ? folder.value.name : i18n.ts.drive,
|
||||
icon: 'ti ti-cloud',
|
||||
hideHeader: true,
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
|
||||
<div ref="containerEl" :class="[$style.gameContainer, { [$style.gameOver]: isGameOver && !replaying }]" @contextmenu.stop.prevent @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
|
||||
<img v-if="defaultStore.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
|
||||
<img v-if="store.s.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
|
||||
<img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/>
|
||||
<canvas ref="canvasEl" :class="$style.canvas"/>
|
||||
<Transition
|
||||
|
|
@ -191,26 +191,28 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onDeactivated, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
|
||||
import { computed, onDeactivated, onMounted, onUnmounted, ref, shallowRef, watch, useTemplateRef } from 'vue';
|
||||
import * as Matter from 'matter-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { DropAndFusionGame, Mono } from 'misskey-bubble-game';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { DropAndFusionGame } from 'misskey-bubble-game';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
import { apiUrl } from '@@/js/config.js';
|
||||
import type { Mono } from 'misskey-bubble-game';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import * as os from '@/os.js';
|
||||
import MkNumber from '@/components/MkNumber.vue';
|
||||
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { claimAchievement } from '@/utility/achievements.js';
|
||||
import { store } from '@/store.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
import { apiUrl } from '@@/js/config.js';
|
||||
import { $i } from '@/account.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { $i } from '@/i.js';
|
||||
import * as sound from '@/utility/sound.js';
|
||||
import MkRange from '@/components/MkRange.vue';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
type FrontendMonoDefinition = {
|
||||
id: string;
|
||||
|
|
@ -565,8 +567,8 @@ let game = new DropAndFusionGame({
|
|||
});
|
||||
attachGameEvents();
|
||||
|
||||
const containerEl = shallowRef<HTMLElement>();
|
||||
const canvasEl = shallowRef<HTMLCanvasElement>();
|
||||
const containerEl = useTemplateRef('containerEl');
|
||||
const canvasEl = useTemplateRef('canvasEl');
|
||||
const dropperX = ref(0);
|
||||
const currentPick = shallowRef<{ id: string; mono: Mono } | null>(null);
|
||||
const stock = shallowRef<{ id: string; mono: Mono }[]>([]);
|
||||
|
|
@ -585,8 +587,8 @@ const showConfig = ref(false);
|
|||
const replaying = ref(false);
|
||||
const replayPlaybackRate = ref(1);
|
||||
const currentFrame = ref(0);
|
||||
const bgmVolume = ref(defaultStore.state.dropAndFusion.bgmVolume);
|
||||
const sfxVolume = ref(defaultStore.state.dropAndFusion.sfxVolume);
|
||||
const bgmVolume = ref(prefer.s['game.dropAndFusion'].bgmVolume);
|
||||
const sfxVolume = ref(prefer.s['game.dropAndFusion'].sfxVolume);
|
||||
|
||||
watch(replayPlaybackRate, (newValue) => {
|
||||
game.replayPlaybackRate = newValue;
|
||||
|
|
@ -622,7 +624,7 @@ function loadMonoTextures() {
|
|||
if (renderer.textures[mono.img]) return;
|
||||
|
||||
let src = mono.img;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
|
||||
if (monoTextureUrls[mono.img]) {
|
||||
src = monoTextureUrls[mono.img];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
|
|
@ -630,7 +632,7 @@ function loadMonoTextures() {
|
|||
src = URL.createObjectURL(monoTextures[mono.img]);
|
||||
monoTextureUrls[mono.img] = src;
|
||||
} else {
|
||||
const res = await fetch(mono.img);
|
||||
const res = await window.fetch(mono.img);
|
||||
const blob = await res.blob();
|
||||
monoTextures[mono.img] = blob;
|
||||
src = URL.createObjectURL(blob);
|
||||
|
|
@ -648,7 +650,6 @@ function loadMonoTextures() {
|
|||
function getTextureImageUrl(mono: Mono) {
|
||||
const def = monoDefinitions.value.find(x => x.id === mono.id)!;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (monoTextureUrls[def.img]) {
|
||||
return monoTextureUrls[def.img];
|
||||
|
||||
|
|
@ -849,17 +850,16 @@ function exportLog() {
|
|||
l: DropAndFusionGame.serializeLogs(logs),
|
||||
});
|
||||
copyToClipboard(data);
|
||||
os.success();
|
||||
}
|
||||
|
||||
function updateSettings<
|
||||
K extends keyof typeof defaultStore.state.dropAndFusion,
|
||||
V extends typeof defaultStore.state.dropAndFusion[K],
|
||||
K extends keyof typeof prefer.s['game.dropAndFusion'],
|
||||
V extends typeof prefer.s['game.dropAndFusion'][K],
|
||||
>(key: K, value: V) {
|
||||
const changes: { [P in K]?: V } = {};
|
||||
changes[key] = value;
|
||||
defaultStore.set('dropAndFusion', {
|
||||
...defaultStore.state.dropAndFusion,
|
||||
prefer.commit('game.dropAndFusion', {
|
||||
...prefer.s['game.dropAndFusion'],
|
||||
...changes,
|
||||
});
|
||||
}
|
||||
|
|
@ -876,7 +876,7 @@ function loadImage(url: string) {
|
|||
|
||||
function getGameImageDriveFile() {
|
||||
return new Promise<Misskey.entities.DriveFile | null>(res => {
|
||||
const dcanvas = document.createElement('canvas');
|
||||
const dcanvas = window.document.createElement('canvas');
|
||||
dcanvas.width = game.GAME_WIDTH;
|
||||
dcanvas.height = game.GAME_HEIGHT;
|
||||
const ctx = dcanvas.getContext('2d');
|
||||
|
|
@ -909,8 +909,8 @@ function getGameImageDriveFile() {
|
|||
formData.append('name', `bubble-game-${Date.now()}.png`);
|
||||
formData.append('isSensitive', 'false');
|
||||
formData.append('i', $i.token);
|
||||
if (defaultStore.state.uploadFolder) {
|
||||
formData.append('folderId', defaultStore.state.uploadFolder);
|
||||
if (prefer.s.uploadFolder) {
|
||||
formData.append('folderId', prefer.s.uploadFolder);
|
||||
}
|
||||
|
||||
window.fetch(apiUrl + '/drive/files/create', {
|
||||
|
|
@ -1229,7 +1229,7 @@ onDeactivated(() => {
|
|||
bgmNodes?.soundSource.stop();
|
||||
});
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.bubbleGame,
|
||||
icon: 'ti ti-apple',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -88,12 +88,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import XGame from './drop-and-fusion.game.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApiGet } from '@/utility/misskey-api.js';
|
||||
|
||||
const gameMode = ref<'normal' | 'square' | 'yen' | 'sweets' | 'space'>('normal');
|
||||
const gameStarted = ref(false);
|
||||
|
|
@ -121,7 +121,7 @@ function onGameEnd() {
|
|||
gameStarted.value = false;
|
||||
}
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.bubbleGame,
|
||||
icon: 'ti ti-device-gamepad',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -87,11 +87,11 @@ import MkInput from '@/components/MkInput.vue';
|
|||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { customEmojiCategories } from '@/custom-emojis.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import { selectFile } from '@/scripts/select-file.js';
|
||||
import { selectFile } from '@/utility/select-file.js';
|
||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ import * as Misskey from 'misskey-js';
|
|||
import { defineAsyncComponent } from 'vue';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { misskeyApiGet } from '@/utility/misskey-api.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
|
||||
import { $i } from '@/account.js';
|
||||
import { $i } from '@/i.js';
|
||||
|
||||
const props = defineProps<{
|
||||
emoji: Misskey.entities.EmojiSimple;
|
||||
emoji: Misskey.entities.EmojiSimple;
|
||||
}>();
|
||||
|
||||
function menu(ev) {
|
||||
|
|
@ -38,7 +38,6 @@ function menu(ev) {
|
|||
icon: 'ti ti-copy',
|
||||
action: () => {
|
||||
copyToClipboard(`:${props.emoji.name}:`);
|
||||
os.success();
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.info,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
|
||||
const roles = ref<Misskey.entities.Role[] | null>(null);
|
||||
|
||||
|
|
|
|||
|
|
@ -63,12 +63,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref, shallowRef, computed } from 'vue';
|
||||
import { watch, ref, useTemplateRef, computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkUserList from '@/components/MkUserList.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const origin = ref('local');
|
||||
const tagsEl = shallowRef<InstanceType<typeof MkFoldableSection>>();
|
||||
const tagsEl = useTemplateRef('tagsEl');
|
||||
const tagsLocal = ref<Misskey.entities.Hashtag[]>([]);
|
||||
const tagsRemote = ref<Misskey.entities.Hashtag[]>([]);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,30 +4,29 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<div v-if="tab === 'featured'" key="featured">
|
||||
<div v-if="tab === 'featured'">
|
||||
<XFeatured/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'users'" key="users">
|
||||
<div v-else-if="tab === 'users'">
|
||||
<XUsers/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'roles'" key="roles">
|
||||
<div v-else-if="tab === 'roles'">
|
||||
<XRoles/>
|
||||
</div>
|
||||
</MkHorizontalSwipe>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref, shallowRef } from 'vue';
|
||||
import { computed, watch, ref, useTemplateRef } from 'vue';
|
||||
import XFeatured from './explore.featured.vue';
|
||||
import XUsers from './explore.users.vue';
|
||||
import XRoles from './explore.roles.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
|
@ -38,7 +37,7 @@ const props = withDefaults(defineProps<{
|
|||
});
|
||||
|
||||
const tab = ref(props.initialTab);
|
||||
const tagsEl = shallowRef<InstanceType<typeof MkFoldableSection>>();
|
||||
const tagsEl = useTemplateRef('tagsEl');
|
||||
|
||||
watch(() => props.tag, () => {
|
||||
if (tagsEl.value) tagsEl.value.toggleContent(props.tag == null);
|
||||
|
|
@ -60,7 +59,7 @@ const headerTabs = computed(() => [{
|
|||
title: i18n.ts.roles,
|
||||
}]);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.explore,
|
||||
icon: 'ti ti-hash',
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -4,13 +4,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<PageWithHeader>
|
||||
<MkSpacer :contentMax="800">
|
||||
<MkPagination :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.noNotes }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -22,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
</MkPagination>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -30,7 +29,7 @@ import MkPagination from '@/components/MkPagination.vue';
|
|||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
|
|
@ -45,7 +44,7 @@ const pagination = {
|
|||
limit: 10,
|
||||
};
|
||||
|
||||
definePageMetadata(() => ({
|
||||
definePage(() => ({
|
||||
title: i18n.ts.favorites,
|
||||
icon: 'ti ti-star',
|
||||
}));
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue