Merge tag '2024.11.0' into feature/2024.10
This commit is contained in:
commit
bc816cb166
234 changed files with 6612 additions and 4634 deletions
|
|
@ -658,6 +658,7 @@ definePageMetadata(() => ({
|
|||
<style lang="scss" module>
|
||||
.ip {
|
||||
display: flex;
|
||||
word-break: break-all;
|
||||
|
||||
> :global(.date) {
|
||||
opacity: 0.7;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps_s">
|
||||
<MkInfo v-if="thereIsUnresolvedAbuseReport" warn class="info">{{ i18n.ts.thereIsUnresolvedAbuseReportWarning }} <MkA to="/admin/abuses" class="_link">{{ i18n.ts.check }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noMaintainerInformation" warn class="info">{{ i18n.ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noInquiryUrl" warn>{{ i18n.ts.noInquiryUrlWarning }} <MkA to="/admin/moderation" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noInquiryUrl" warn class="info">{{ i18n.ts.noInquiryUrlWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noBotProtection" warn class="info">{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noEmailServer" warn class="info">{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="pendingUserApprovals" warn class="info">{{ i18n.ts.pendingUserApprovals }} <MkA to="/admin/approvals" class="_link">{{ i18n.ts.check }}</MkA></MkInfo>
|
||||
|
|
|
|||
|
|
@ -10,9 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_gaps_m">
|
||||
<MkSwitch v-model="enableRegistration" @change="onChange_enableRegistration">
|
||||
<template #label>{{ i18n.ts.enableRegistration }}</template>
|
||||
<template #caption>{{ i18n.ts._serverSettings.thisSettingWillAutomaticallyOffWhenModeratorsInactive }}</template>
|
||||
<MkSwitch :modelValue="enableRegistration" @update:modelValue="onChange_enableRegistration">
|
||||
<template #label>{{ i18n.ts._serverSettings.openRegistration }}</template>
|
||||
<template #caption>
|
||||
<div>{{ i18n.ts._serverSettings.thisSettingWillAutomaticallyOffWhenModeratorsInactive }}</div>
|
||||
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._serverSettings.openRegistrationWarning }}</div>
|
||||
</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="emailRequiredForSignup" @change="onChange_emailRequiredForSignup">
|
||||
|
|
@ -200,7 +203,17 @@ async function init() {
|
|||
mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n');
|
||||
}
|
||||
|
||||
function onChange_enableRegistration(value: boolean) {
|
||||
async function onChange_enableRegistration(value: boolean) {
|
||||
if (value) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.acknowledgeNotesAndEnable,
|
||||
});
|
||||
if (canceled) return;
|
||||
}
|
||||
|
||||
enableRegistration.value = value;
|
||||
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
disableRegistration: !value,
|
||||
}).then(() => {
|
||||
|
|
|
|||
|
|
@ -100,19 +100,19 @@ async function addUser() {
|
|||
const { canceled: canceled1, result: username } = await os.inputText({
|
||||
title: i18n.ts.username,
|
||||
});
|
||||
if (canceled1) return;
|
||||
if (canceled1 || username == null) return;
|
||||
|
||||
const { canceled: canceled2, result: password } = await os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password',
|
||||
});
|
||||
if (canceled2) return;
|
||||
if (canceled2 || password == null) return;
|
||||
|
||||
os.apiWithDialog('admin/accounts/create', {
|
||||
username: username,
|
||||
password: password,
|
||||
}).then(res => {
|
||||
paginationComponent.value.reload();
|
||||
paginationComponent.value?.reload();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ import * as os from '@/os.js';
|
|||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { $i, updateAccount } from '@/account.js';
|
||||
import { $i, updateAccountPartial } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
@ -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) {
|
||||
updateAccount({
|
||||
updateAccountPartial({
|
||||
unreadAnnouncements: $i.unreadAnnouncements.filter((a: { id: string; }) => a.id !== target.id),
|
||||
});
|
||||
}
|
||||
|
|
@ -103,7 +103,7 @@ const headerActions = computed(() => []);
|
|||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
title: announcement.value ? `${i18n.ts.announcements}: ${announcement.value.title}` : i18n.ts.announcements,
|
||||
title: announcement.value ? announcement.value.title : i18n.ts.announcements,
|
||||
icon: 'ti ti-speakerphone',
|
||||
}));
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ import * as os from '@/os.js';
|
|||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { $i, updateAccount } from '@/account.js';
|
||||
import { $i, updateAccountPartial } from '@/account.js';
|
||||
|
||||
const paginationCurrent = {
|
||||
endpoint: 'announcements' as const,
|
||||
|
|
@ -94,7 +94,7 @@ async function read(target) {
|
|||
return a;
|
||||
});
|
||||
misskeyApi('i/read-announcement', { announcementId: target.id });
|
||||
updateAccount({
|
||||
updateAccountPartial({
|
||||
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ function accepted() {
|
|||
location.href = callbackUrl.toString();
|
||||
} else if (session.value && session.value.app.callbackUrl) {
|
||||
const url = new URL(session.value.app.callbackUrl);
|
||||
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(url.protocol)) throw new Error('invalid url');
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
220
packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
Normal file
220
packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkWindow
|
||||
ref="windowEl"
|
||||
:initialWidth="400"
|
||||
:initialHeight="500"
|
||||
:canResize="true"
|
||||
@close="windowEl?.close()"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template v-if="avatarDecoration" #header>{{ avatarDecoration.name }}</template>
|
||||
<template v-else #header>New decoration</template>
|
||||
|
||||
<div style="display: flex; flex-direction: column; min-height: 100%;">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;">
|
||||
<div class="_gaps_m">
|
||||
<div :class="$style.preview">
|
||||
<div :class="[$style.previewItem, $style.light]">
|
||||
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="url != '' ? [{ url }] : []" forceShowDecoration/>
|
||||
</div>
|
||||
<div :class="[$style.previewItem, $style.dark]">
|
||||
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="url != '' ? [{ url }] : []" forceShowDecoration/>
|
||||
</div>
|
||||
</div>
|
||||
<MkInput v-model="name">
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="url">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="description">
|
||||
<template #label>{{ i18n.ts.description }}</template>
|
||||
</MkTextarea>
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.availableRoles }}</template>
|
||||
<template #suffix>{{ rolesThatCanBeUsedThisDecoration.length === 0 ? i18n.ts.all : rolesThatCanBeUsedThisDecoration.length }}</template>
|
||||
|
||||
<div class="_gaps">
|
||||
<MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
|
||||
<div v-for="role in rolesThatCanBeUsedThisDecoration" :key="role.id" :class="$style.roleItem">
|
||||
<MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/>
|
||||
<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(role, $event)"><i class="ti ti-x"></i></button>
|
||||
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
<MkButton v-if="avatarDecoration" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<div :class="$style.footer">
|
||||
<MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.avatarDecoration ? i18n.ts.update : i18n.ts.create }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
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 { 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';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
const props = defineProps<{
|
||||
avatarDecoration?: any,
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
|
||||
(ev: 'closed'): void
|
||||
}>();
|
||||
|
||||
const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
|
||||
const url = ref<string>(props.avatarDecoration ? props.avatarDecoration.url : '');
|
||||
const name = ref<string>(props.avatarDecoration ? props.avatarDecoration.name : '');
|
||||
const description = ref<string>(props.avatarDecoration ? props.avatarDecoration.description : '');
|
||||
const roleIdsThatCanBeUsedThisDecoration = ref(props.avatarDecoration ? props.avatarDecoration.roleIdsThatCanBeUsedThisDecoration : []);
|
||||
const rolesThatCanBeUsedThisDecoration = ref<Misskey.entities.Role[]>([]);
|
||||
|
||||
watch(roleIdsThatCanBeUsedThisDecoration, async () => {
|
||||
rolesThatCanBeUsedThisDecoration.value = (await Promise.all(roleIdsThatCanBeUsedThisDecoration.value.map((id) => misskeyApi('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
|
||||
}, { immediate: true });
|
||||
|
||||
async function addRole() {
|
||||
const roles = await misskeyApi('admin/roles/list');
|
||||
const currentRoleIds = rolesThatCanBeUsedThisDecoration.value.map(x => x.id);
|
||||
|
||||
const { canceled, result: role } = await os.select({
|
||||
items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })),
|
||||
});
|
||||
if (canceled || role == null) return;
|
||||
|
||||
rolesThatCanBeUsedThisDecoration.value.push(role);
|
||||
}
|
||||
|
||||
async function removeRole(role, ev) {
|
||||
rolesThatCanBeUsedThisDecoration.value = rolesThatCanBeUsedThisDecoration.value.filter(x => x.id !== role.id);
|
||||
}
|
||||
|
||||
async function done() {
|
||||
const params = {
|
||||
url: url.value,
|
||||
name: name.value,
|
||||
description: description.value,
|
||||
roleIdsThatCanBeUsedThisDecoration: rolesThatCanBeUsedThisDecoration.value.map(x => x.id),
|
||||
};
|
||||
|
||||
if (props.avatarDecoration) {
|
||||
await os.apiWithDialog('admin/avatar-decorations/update', {
|
||||
id: props.avatarDecoration.id,
|
||||
...params,
|
||||
});
|
||||
|
||||
emit('done', {
|
||||
updated: {
|
||||
id: props.avatarDecoration.id,
|
||||
...params,
|
||||
},
|
||||
});
|
||||
|
||||
windowEl.value?.close();
|
||||
} else {
|
||||
const created = await os.apiWithDialog('admin/avatar-decorations/create', params);
|
||||
|
||||
emit('done', {
|
||||
created: created,
|
||||
});
|
||||
|
||||
windowEl.value?.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.tsx.removeAreYouSure({ x: name.value }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
misskeyApi('admin/avatar-decorations/delete', {
|
||||
id: props.avatarDecoration.id,
|
||||
}).then(() => {
|
||||
emit('done', {
|
||||
deleted: true,
|
||||
});
|
||||
windowEl.value?.close();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.preview {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: var(--MI-margin);
|
||||
}
|
||||
|
||||
.previewItem {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 160px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--MI-radius);
|
||||
|
||||
&.light {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background: #222;
|
||||
}
|
||||
}
|
||||
|
||||
.roleItem {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.role {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.roleUnassign {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-left: 8px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: sticky;
|
||||
z-index: 10000;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 12px;
|
||||
border-top: solid 0.5px var(--MI_THEME-divider);
|
||||
background: var(--MI_THEME-acrylicBg);
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
}
|
||||
</style>
|
||||
|
|
@ -5,92 +5,38 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_gaps">
|
||||
<MkFolder v-for="avatarDecoration in avatarDecorations" :key="avatarDecoration.id ?? avatarDecoration._id" :defaultOpen="avatarDecoration.id == null">
|
||||
<template #label>{{ avatarDecoration.name }}</template>
|
||||
<template #caption>{{ avatarDecoration.description }}</template>
|
||||
|
||||
<div :class="$style.editorRoot">
|
||||
<div :class="$style.editorWrapper">
|
||||
<div :class="$style.preview">
|
||||
<div :class="[$style.previewItem, $style.light]">
|
||||
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[avatarDecoration]" forceShowDecoration/>
|
||||
</div>
|
||||
<div :class="[$style.previewItem, $style.dark]">
|
||||
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[avatarDecoration]" forceShowDecoration/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="avatarDecoration.name">
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="avatarDecoration.description">
|
||||
<template #label>{{ i18n.ts.description }}</template>
|
||||
</MkTextarea>
|
||||
<MkInput v-model="avatarDecoration.url">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<div class="_buttons">
|
||||
<MkButton inline primary @click="save(avatarDecoration)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton v-if="avatarDecoration.id != null" inline danger @click="del(avatarDecoration)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.decorations">
|
||||
<div
|
||||
v-for="avatarDecoration in avatarDecorations"
|
||||
:key="avatarDecoration.id"
|
||||
v-panel
|
||||
:class="$style.decoration"
|
||||
@click="edit(avatarDecoration)"
|
||||
>
|
||||
<div :class="$style.decorationName"><MkCondensedLine :minScale="0.5">{{ avatarDecoration.name }}</MkCondensedLine></div>
|
||||
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: avatarDecoration.url }]" forceShowDecoration/>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, defineAsyncComponent } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import { signinRequired } from '@/account.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
||||
const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]);
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
function add() {
|
||||
avatarDecorations.value.unshift({
|
||||
_id: Math.random().toString(36),
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
url: '',
|
||||
});
|
||||
}
|
||||
|
||||
function del(avatarDecoration) {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.tsx.deleteAreYouSure({ x: avatarDecoration.name }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
avatarDecorations.value = avatarDecorations.value.filter(x => x !== avatarDecoration);
|
||||
misskeyApi('admin/avatar-decorations/delete', avatarDecoration);
|
||||
});
|
||||
}
|
||||
|
||||
async function save(avatarDecoration) {
|
||||
if (avatarDecoration.id == null) {
|
||||
await os.apiWithDialog('admin/avatar-decorations/create', avatarDecoration);
|
||||
load();
|
||||
} else {
|
||||
os.apiWithDialog('admin/avatar-decorations/update', avatarDecoration);
|
||||
}
|
||||
}
|
||||
const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]);
|
||||
|
||||
function load() {
|
||||
misskeyApi('admin/avatar-decorations/list').then(_avatarDecorations => {
|
||||
|
|
@ -100,6 +46,37 @@ function load() {
|
|||
|
||||
load();
|
||||
|
||||
async function add(ev: MouseEvent) {
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('./avatar-decoration-edit-dialog.vue')), {
|
||||
}, {
|
||||
done: result => {
|
||||
if (result.created) {
|
||||
avatarDecorations.value.unshift(result.created);
|
||||
}
|
||||
},
|
||||
closed: () => dispose(),
|
||||
});
|
||||
}
|
||||
|
||||
function edit(avatarDecoration) {
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('./avatar-decoration-edit-dialog.vue')), {
|
||||
avatarDecoration: avatarDecoration,
|
||||
}, {
|
||||
done: result => {
|
||||
if (result.updated) {
|
||||
const index = avatarDecorations.value.findIndex(x => x.id === avatarDecoration.id);
|
||||
avatarDecorations.value[index] = {
|
||||
...avatarDecorations.value[index],
|
||||
...result.updated,
|
||||
};
|
||||
} else if (result.deleted) {
|
||||
avatarDecorations.value = avatarDecorations.value.filter(x => x.id !== avatarDecoration.id);
|
||||
}
|
||||
},
|
||||
closed: () => dispose(),
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'ti ti-plus',
|
||||
|
|
@ -116,53 +93,26 @@ definePageMetadata(() => ({
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.editorRoot {
|
||||
container: editor / inline-size;
|
||||
}
|
||||
|
||||
.editorWrapper {
|
||||
.decorations {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
gap: var(--MI-margin);
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
grid-gap: 12px;
|
||||
}
|
||||
|
||||
.preview {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: var(--MI-margin);
|
||||
.decoration {
|
||||
cursor: pointer;
|
||||
padding: 16px 16px 28px 16px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 90%;
|
||||
overflow: clip;
|
||||
contain: content;
|
||||
}
|
||||
|
||||
.previewItem {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 160px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--MI-radius);
|
||||
|
||||
&.light {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background: #222;
|
||||
}
|
||||
}
|
||||
|
||||
@container editor (min-width: 600px) {
|
||||
.editorWrapper {
|
||||
grid-template-columns: 200px 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: calc(var(--MI-margin) * 2);
|
||||
}
|
||||
|
||||
.preview {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
}
|
||||
.decorationName {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -33,25 +33,28 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { computed, watch, provide, ref } from 'vue';
|
||||
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 { 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 { url } from '@@/js/config.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 type { MenuItem } from '@/types/menu.js';
|
||||
import { getServerContext } from '@/server-context.js';
|
||||
|
||||
const CTX_CLIP = getServerContext('clip');
|
||||
|
||||
const props = defineProps<{
|
||||
clipId: string,
|
||||
}>();
|
||||
|
||||
const clip = ref<Misskey.entities.Clip | null>(null);
|
||||
const clip = ref<Misskey.entities.Clip | null>(CTX_CLIP);
|
||||
const favorited = ref(false);
|
||||
const pagination = {
|
||||
endpoint: 'clips/notes' as const,
|
||||
|
|
@ -64,6 +67,11 @@ const pagination = {
|
|||
const isOwned = computed<boolean | null>(() => $i && clip.value && ($i.id === clip.value.userId));
|
||||
|
||||
watch(() => props.clipId, async () => {
|
||||
if (CTX_CLIP && CTX_CLIP.id === props.clipId) {
|
||||
clip.value = CTX_CLIP;
|
||||
return;
|
||||
}
|
||||
|
||||
clip.value = await misskeyApi('clips/show', {
|
||||
clipId: props.clipId,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ const selectAll = () => {
|
|||
if (selectedEmojis.value.length > 0) {
|
||||
selectedEmojis.value = [];
|
||||
} else {
|
||||
selectedEmojis.value = Array.from(emojisPaginationComponent.value.items.values(), item => item.id);
|
||||
selectedEmojis.value = Array.from(emojisPaginationComponent.value?.items.values(), item => item.id);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -136,7 +136,7 @@ const add = async (ev: MouseEvent) => {
|
|||
}, {
|
||||
done: result => {
|
||||
if (result.created) {
|
||||
emojisPaginationComponent.value.prepend(result.created);
|
||||
emojisPaginationComponent.value?.prepend(result.created);
|
||||
}
|
||||
},
|
||||
closed: () => dispose(),
|
||||
|
|
@ -149,12 +149,12 @@ const edit = (emoji) => {
|
|||
}, {
|
||||
done: result => {
|
||||
if (result.updated) {
|
||||
emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
|
||||
emojisPaginationComponent.value?.updateItem(result.updated.id, (oldEmoji) => ({
|
||||
...oldEmoji,
|
||||
...result.updated,
|
||||
}));
|
||||
} else if (result.deleted) {
|
||||
emojisPaginationComponent.value.removeItem(emoji.id);
|
||||
emojisPaginationComponent.value?.removeItem(emoji.id);
|
||||
}
|
||||
},
|
||||
closed: () => dispose(),
|
||||
|
|
@ -239,7 +239,7 @@ const setCategoryBulk = async () => {
|
|||
ids: selectedEmojis.value,
|
||||
category: result,
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
emojisPaginationComponent.value?.reload();
|
||||
};
|
||||
|
||||
const setLicenseBulk = async () => {
|
||||
|
|
@ -251,43 +251,43 @@ const setLicenseBulk = async () => {
|
|||
ids: selectedEmojis.value,
|
||||
license: result,
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
emojisPaginationComponent.value?.reload();
|
||||
};
|
||||
|
||||
const addTagBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'Tag',
|
||||
});
|
||||
if (canceled) return;
|
||||
if (canceled || result == null) return;
|
||||
await os.apiWithDialog('admin/emoji/add-aliases-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
aliases: result.split(' '),
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
emojisPaginationComponent.value?.reload();
|
||||
};
|
||||
|
||||
const removeTagBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'Tag',
|
||||
});
|
||||
if (canceled) return;
|
||||
if (canceled || result == null) return;
|
||||
await os.apiWithDialog('admin/emoji/remove-aliases-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
aliases: result.split(' '),
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
emojisPaginationComponent.value?.reload();
|
||||
};
|
||||
|
||||
const setTagBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'Tag',
|
||||
});
|
||||
if (canceled) return;
|
||||
if (canceled || result == null) return;
|
||||
await os.apiWithDialog('admin/emoji/set-aliases-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
aliases: result.split(' '),
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
emojisPaginationComponent.value?.reload();
|
||||
};
|
||||
|
||||
const delBulk = async () => {
|
||||
|
|
@ -299,7 +299,7 @@ const delBulk = async () => {
|
|||
await os.apiWithDialog('admin/emoji/delete-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
emojisPaginationComponent.value?.reload();
|
||||
};
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
ref="windowEl"
|
||||
:initialWidth="400"
|
||||
:initialHeight="500"
|
||||
:canResize="false"
|
||||
@close="windowEl.close()"
|
||||
@closed="$emit('closed')"
|
||||
:canResize="true"
|
||||
@close="windowEl?.close()"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template v-if="emoji" #header>:{{ emoji.name }}:</template>
|
||||
<template v-else #header>New emoji</template>
|
||||
|
|
@ -95,14 +95,19 @@ import { selectFile } from '@/scripts/select-file.js';
|
|||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
emoji?: any,
|
||||
emoji?: Misskey.entities.EmojiDetailed,
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: { deleted?: boolean; updated?: Misskey.entities.AdminEmojiUpdateRequest; created?: Misskey.entities.AdminEmojiUpdateRequest }): void,
|
||||
(ev: 'closed'): void
|
||||
}>();
|
||||
|
||||
const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
|
||||
const name = ref<string>(props.emoji ? props.emoji.name : '');
|
||||
const category = ref<string>(props.emoji ? props.emoji.category : '');
|
||||
const category = ref<string>(props.emoji?.category ? props.emoji.category : '');
|
||||
const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : '');
|
||||
const license = ref<string>(props.emoji ? (props.emoji.license ?? '') : '');
|
||||
const license = ref<string>(props.emoji?.license ? props.emoji.license : '');
|
||||
const isSensitive = ref(props.emoji ? props.emoji.isSensitive : false);
|
||||
const localOnly = ref(props.emoji ? props.emoji.localOnly : false);
|
||||
const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
|
||||
|
|
@ -115,12 +120,7 @@ watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
|
|||
|
||||
const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
|
||||
(ev: 'closed'): void
|
||||
}>();
|
||||
|
||||
async function changeImage(ev) {
|
||||
async function changeImage(ev: Event) {
|
||||
file.value = await selectFile(ev.currentTarget ?? ev.target, null);
|
||||
const candidate = file.value.name.replace(/\.(.+)$/, '');
|
||||
if (candidate.match(/^[a-z0-9_]+$/)) {
|
||||
|
|
@ -140,7 +140,7 @@ async function addRole() {
|
|||
rolesThatCanBeUsedThisEmojiAsReaction.value.push(role);
|
||||
}
|
||||
|
||||
async function removeRole(role, ev) {
|
||||
async function removeRole(role: Misskey.entities.RoleLite, ev: Event) {
|
||||
rolesThatCanBeUsedThisEmojiAsReaction.value = rolesThatCanBeUsedThisEmojiAsReaction.value.filter(x => x.id !== role.id);
|
||||
}
|
||||
|
||||
|
|
@ -172,7 +172,7 @@ async function done() {
|
|||
},
|
||||
});
|
||||
|
||||
windowEl.value.close();
|
||||
windowEl.value?.close();
|
||||
} else {
|
||||
const created = await os.apiWithDialog('admin/emoji/add', params);
|
||||
|
||||
|
|
@ -180,11 +180,12 @@ async function done() {
|
|||
created: created,
|
||||
});
|
||||
|
||||
windowEl.value.close();
|
||||
windowEl.value?.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
if (!props.emoji) return;
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.tsx.removeAreYouSure({ x: name.value }),
|
||||
|
|
@ -197,7 +198,7 @@ async function del() {
|
|||
emit('done', {
|
||||
deleted: true,
|
||||
});
|
||||
windowEl.value.close();
|
||||
windowEl.value?.close();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -15,18 +15,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
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 { i18n } from '@/i18n.js';
|
||||
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
|
||||
import { $i } from '@/account.js';
|
||||
|
||||
const props = defineProps<{
|
||||
emoji: Misskey.entities.EmojiSimple;
|
||||
}>();
|
||||
|
||||
function menu(ev) {
|
||||
os.popupMenu([{
|
||||
const menuItems: MenuItem[] = [];
|
||||
menuItems.push({
|
||||
type: 'label',
|
||||
text: ':' + props.emoji.name + ':',
|
||||
}, {
|
||||
|
|
@ -48,8 +52,28 @@ function menu(ev) {
|
|||
closed: () => dispose(),
|
||||
});
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
});
|
||||
|
||||
if ($i?.isModerator ?? $i?.isAdmin) {
|
||||
menuItems.push({
|
||||
text: i18n.ts.edit,
|
||||
icon: 'ti ti-pencil',
|
||||
action: () => {
|
||||
edit(props.emoji);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
const edit = async (emoji) => {
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/pages/emoji-edit-dialog.vue')), {
|
||||
emoji: emoji,
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
<template #default="{items}">
|
||||
<div class="mk-follow-requests">
|
||||
<div class="mk-follow-requests _gaps">
|
||||
<div v-for="req in items" :key="req.id" class="user _panel">
|
||||
<MkAvatar class="avatar" :user="displayUser(req)" indicator link preview/>
|
||||
<div class="body">
|
||||
|
|
@ -29,6 +29,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton class="command" rounded primary @click="accept(displayUser(req))"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
|
||||
<MkButton class="command" rounded danger @click="reject(displayUser(req))"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
|
||||
</div>
|
||||
<div v-else class="commands">
|
||||
<MkButton class="command" rounded danger @click="cancel(displayUser(req))"><i class="ti ti-x"/> {{ i18n.ts.cancel }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -41,38 +44,42 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { shallowRef, computed, ref } from 'vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkPagination, { type Paging } from '@/components/MkPagination.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { userPage, acct } from '@/filters/user.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { $i } from '@/account.js';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { $i } from '@/account';
|
||||
|
||||
const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
||||
const pagination = computed(() => tab.value === 'list'
|
||||
? {
|
||||
endpoint: 'following/requests/list' as const,
|
||||
limit: 10,
|
||||
}
|
||||
: {
|
||||
endpoint: 'following/requests/sent' as const,
|
||||
limit: 10,
|
||||
},
|
||||
);
|
||||
const pagination = computed<Paging>(() => tab.value === 'list' ? {
|
||||
endpoint: 'following/requests/list',
|
||||
limit: 10,
|
||||
} : {
|
||||
endpoint: 'following/requests/sent',
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
function accept(user) {
|
||||
misskeyApi('following/requests/accept', { userId: user.id }).then(() => {
|
||||
function accept(user: Misskey.entities.UserLite) {
|
||||
os.apiWithDialog('following/requests/accept', { userId: user.id }).then(() => {
|
||||
paginationComponent.value?.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function reject(user) {
|
||||
misskeyApi('following/requests/reject', { userId: user.id }).then(() => {
|
||||
function reject(user: Misskey.entities.UserLite) {
|
||||
os.apiWithDialog('following/requests/reject', { userId: user.id }).then(() => {
|
||||
paginationComponent.value?.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function cancel(user: Misskey.entities.UserLite) {
|
||||
os.apiWithDialog('following/requests/cancel', { userId: user.id }).then(() => {
|
||||
paginationComponent.value?.reload();
|
||||
});
|
||||
}
|
||||
|
|
@ -86,11 +93,11 @@ const headerActions = computed(() => []);
|
|||
const headerTabs = computed(() => [
|
||||
{
|
||||
key: 'list',
|
||||
title: i18n.ts.followRequests,
|
||||
title: i18n.ts._followRequest.recieved,
|
||||
icon: 'ph-envelope ph-bold ph-lg',
|
||||
}, {
|
||||
key: 'sent',
|
||||
title: i18n.ts.pendingFollowRequests,
|
||||
title: i18n.ts._followRequest.sent,
|
||||
icon: 'ph-paper-plane-tilt ph-bold ph-lg',
|
||||
},
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -5,10 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header>
|
||||
<MkPageHeader/>
|
||||
</template>
|
||||
<MKSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200">
|
||||
<template #header><MkPageHeader/></template>
|
||||
<MkSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200">
|
||||
<div :class="$style.root">
|
||||
<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
|
||||
<div :class="$style.text">
|
||||
|
|
@ -16,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
{{ i18n.ts.nothing }}
|
||||
</div>
|
||||
</div>
|
||||
</MKSpacer>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else :contentMax="800">
|
||||
<div class="_gaps_m" style="text-align: center;">
|
||||
<div v-if="resetCycle && inviteLimit">{{ i18n.tsx.inviteLimitResetCycle({ time: resetCycle, limit: inviteLimit }) }}</div>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200">
|
||||
<MkSpacer v-if="error != null" :contentMax="1200">
|
||||
<div :class="$style.root">
|
||||
<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
|
||||
<p :class="$style.text">
|
||||
|
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
{{ i18n.ts.nothing }}
|
||||
</p>
|
||||
</div>
|
||||
</MKSpacer>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="list" :contentMax="700" :class="$style.main">
|
||||
<div v-if="list" class="members _margin">
|
||||
<div :class="$style.member_text">{{ i18n.ts.members }}</div>
|
||||
|
|
@ -50,7 +50,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const list = ref<Misskey.entities.UserList | null>(null);
|
||||
const error = ref();
|
||||
const error = ref<unknown | null>(null);
|
||||
const users = ref<Misskey.entities.UserDetailed[]>([]);
|
||||
|
||||
function fetchList(): void {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ function fetch() {
|
|||
return;
|
||||
}
|
||||
|
||||
let promise: Promise<any>;
|
||||
let promise: Promise<unknown>;
|
||||
|
||||
if (uri.startsWith('https://')) {
|
||||
promise = misskeyApi('ap/show', {
|
||||
|
|
|
|||
|
|
@ -4,95 +4,79 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<div v-if="$i">
|
||||
<div v-if="state == 'waiting'">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
<div v-if="state == 'denied'">
|
||||
<p>{{ i18n.ts._auth.denied }}</p>
|
||||
</div>
|
||||
<div v-else-if="state == 'accepted'" class="accepted">
|
||||
<p v-if="callback">{{ i18n.ts._auth.callback }}<MkEllipsis/></p>
|
||||
<p v-else>{{ i18n.ts._auth.pleaseGoBack }}</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="_permissions.length > 0">
|
||||
<p v-if="name">{{ i18n.tsx._auth.permission({ name }) }}</p>
|
||||
<p v-else>{{ i18n.ts._auth.permissionAsk }}</p>
|
||||
<ul>
|
||||
<li v-for="p in _permissions" :key="p">{{ i18n.ts._permissions[p] }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div>
|
||||
<div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div>
|
||||
<div :class="$style.buttons">
|
||||
<MkButton inline @click="deny">{{ i18n.ts.cancel }}</MkButton>
|
||||
<MkButton inline primary @click="accept">{{ i18n.ts.accept }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<MkAnimBg style="position: fixed; top: 0;"/>
|
||||
<div :class="$style.formContainer">
|
||||
<div :class="$style.form">
|
||||
<MkAuthConfirm
|
||||
ref="authRoot"
|
||||
:name="name"
|
||||
:icon="icon || undefined"
|
||||
:permissions="_permissions"
|
||||
@accept="onAccept"
|
||||
@deny="onDeny"
|
||||
>
|
||||
<template #consentAdditionalInfo>
|
||||
<div v-if="callback != null" class="_gaps_s" :class="$style.redirectRoot">
|
||||
<div>{{ i18n.ts._auth.byClickingYouWillBeRedirectedToThisUrl }}</div>
|
||||
<div class="_monospace" :class="$style.redirectUrl">{{ callback }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkAuthConfirm>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p>
|
||||
<MkSignin @login="onLogin"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import MkSignin from '@/components/MkSignin.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { $i, login } from '@/account.js';
|
||||
import { computed, useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
import MkAnimBg from '@/components/MkAnimBg.vue';
|
||||
import MkAuthConfirm from '@/components/MkAuthConfirm.vue';
|
||||
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
const props = defineProps<{
|
||||
session: string;
|
||||
callback?: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
permission: string; // コンマ区切り
|
||||
name?: string;
|
||||
icon?: string;
|
||||
permission?: string; // コンマ区切り
|
||||
}>();
|
||||
|
||||
const _permissions = computed(() => props.permission ? props.permission.split(',') : []);
|
||||
const _permissions = computed(() => {
|
||||
return (props.permission ? props.permission.split(',').filter((p): p is typeof Misskey.permissions[number] => (Misskey.permissions as readonly string[]).includes(p)) : []);
|
||||
});
|
||||
|
||||
const state = ref<string | null>(null);
|
||||
const authRoot = useTemplateRef('authRoot');
|
||||
|
||||
async function accept(): Promise<void> {
|
||||
state.value = 'waiting';
|
||||
async function onAccept(token: string) {
|
||||
await misskeyApi('miauth/gen-token', {
|
||||
session: props.session,
|
||||
name: props.name,
|
||||
iconUrl: props.icon,
|
||||
permission: _permissions.value,
|
||||
}, token).catch(() => {
|
||||
authRoot.value?.showUI('failed');
|
||||
});
|
||||
|
||||
state.value = 'accepted';
|
||||
if (props.callback) {
|
||||
if (props.callback && props.callback !== '') {
|
||||
const cbUrl = new URL(props.callback);
|
||||
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url');
|
||||
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(cbUrl.protocol)) throw new Error('invalid url');
|
||||
cbUrl.searchParams.set('session', props.session);
|
||||
location.href = cbUrl.href;
|
||||
location.href = cbUrl.toString();
|
||||
} else {
|
||||
authRoot.value?.showUI('success');
|
||||
}
|
||||
}
|
||||
|
||||
function deny(): void {
|
||||
state.value = 'denied';
|
||||
function onDeny() {
|
||||
authRoot.value?.showUI('denied');
|
||||
}
|
||||
|
||||
function onLogin(res): void {
|
||||
login(res.i);
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
title: 'MiAuth',
|
||||
icon: 'ti ti-apps',
|
||||
|
|
@ -100,15 +84,38 @@ definePageMetadata(() => ({
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.buttons {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
.formContainer {
|
||||
min-height: 100svh;
|
||||
padding: 32px 32px calc(env(safe-area-inset-bottom, 0px) + 32px) 32px;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
.loginMessage {
|
||||
text-align: center;
|
||||
margin: 8px 0 24px;
|
||||
.form {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
border-radius: var(--MI-radius);
|
||||
background-color: var(--MI_THEME-panel);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
overflow: clip;
|
||||
max-width: 500px;
|
||||
width: calc(100vw - 64px);
|
||||
height: min(65svh, calc(100svh - calc(env(safe-area-inset-bottom, 0px) + 64px)));
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.redirectRoot {
|
||||
padding: 16px;
|
||||
border-radius: var(--MI-radius);
|
||||
background-color: var(--MI_THEME-bg);
|
||||
}
|
||||
|
||||
.redirectUrl {
|
||||
font-size: 90%;
|
||||
padding: 12px;
|
||||
border-radius: var(--MI-radius);
|
||||
background-color: var(--MI_THEME-panel);
|
||||
overflow-x: scroll;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -77,15 +77,15 @@ async function create() {
|
|||
|
||||
clipsCache.delete();
|
||||
|
||||
pagingComponent.value.reload();
|
||||
pagingComponent.value?.reload();
|
||||
}
|
||||
|
||||
function onClipCreated() {
|
||||
pagingComponent.value.reload();
|
||||
pagingComponent.value?.reload();
|
||||
}
|
||||
|
||||
function onClipDeleted() {
|
||||
pagingComponent.value.reload();
|
||||
pagingComponent.value?.reload();
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ function addUser() {
|
|||
listId: list.value.id,
|
||||
userId: user.id,
|
||||
}).then(() => {
|
||||
paginationEl.value.reload();
|
||||
paginationEl.value?.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -126,7 +126,7 @@ async function removeUser(item, ev) {
|
|||
listId: list.value.id,
|
||||
userId: item.userId,
|
||||
}).then(() => {
|
||||
paginationEl.value.removeItem(item.id);
|
||||
paginationEl.value?.removeItem(item.id);
|
||||
});
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
if (props.showLoginPopup) {
|
||||
pleaseLogin('/');
|
||||
pleaseLogin({ path: '/' });
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
|
|
|||
|
|
@ -60,6 +60,10 @@ import { i18n } from '@/i18n.js';
|
|||
import { dateString } from '@/filters/date.js';
|
||||
import MkClipPreview from '@/components/MkClipPreview.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import { getServerContext } from '@/server-context.js';
|
||||
|
||||
const CTX_NOTE = getServerContext('note');
|
||||
|
||||
const MkNoteDetailed = defineAsyncComponent(() =>
|
||||
(defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNoteDetailed.vue') :
|
||||
|
|
@ -72,7 +76,7 @@ const props = defineProps<{
|
|||
initialTab?: string;
|
||||
}>();
|
||||
|
||||
const note = ref<null | Misskey.entities.Note>();
|
||||
const note = ref<null | Misskey.entities.Note>(CTX_NOTE);
|
||||
const clips = ref<Misskey.entities.Clip[]>();
|
||||
const showPrev = ref<'user' | 'channel' | false>(false);
|
||||
const showNext = ref<'user' | 'channel' | false>(false);
|
||||
|
|
@ -121,6 +125,12 @@ function fetchNote() {
|
|||
showPrev.value = false;
|
||||
showNext.value = false;
|
||||
note.value = null;
|
||||
|
||||
if (CTX_NOTE && CTX_NOTE.id === props.noteId) {
|
||||
note.value = CTX_NOTE;
|
||||
return;
|
||||
}
|
||||
|
||||
misskeyApi('notes/show', {
|
||||
noteId: props.noteId,
|
||||
}).then(res => {
|
||||
|
|
@ -134,6 +144,11 @@ function fetchNote() {
|
|||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
if (err.id === '8e75455b-738c-471d-9f80-62693f33372e') {
|
||||
pleaseLogin({
|
||||
message: i18n.ts.thisContentsAreMarkedAsSigninRequiredByAuthor,
|
||||
});
|
||||
}
|
||||
error.value = err;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,40 +4,28 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<div v-if="$i">
|
||||
<div v-if="permissions.length > 0">
|
||||
<p v-if="name">{{ i18n.tsx._auth.permission({ name }) }}</p>
|
||||
<p v-else>{{ i18n.ts._auth.permissionAsk }}</p>
|
||||
<ul>
|
||||
<li v-for="p in permissions" :key="p">{{ i18n.ts._permissions[p] }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div>
|
||||
<div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div>
|
||||
<form :class="$style.buttons" action="/oauth/decision" accept-charset="utf-8" method="post">
|
||||
<input name="login_token" type="hidden" :value="$i.token"/>
|
||||
<input name="transaction_id" type="hidden" :value="transactionIdMeta?.content"/>
|
||||
<MkButton inline name="cancel" value="cancel">{{ i18n.ts.cancel }}</MkButton>
|
||||
<MkButton inline primary>{{ i18n.ts.accept }}</MkButton>
|
||||
</form>
|
||||
<div>
|
||||
<MkAnimBg style="position: fixed; top: 0;"/>
|
||||
<div :class="$style.formContainer">
|
||||
<div :class="$style.form">
|
||||
<MkAuthConfirm
|
||||
ref="authRoot"
|
||||
:name="name"
|
||||
:permissions="permissions"
|
||||
:waitOnDeny="true"
|
||||
@accept="onAccept"
|
||||
@deny="onDeny"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p>
|
||||
<MkSignin @login="onLogin"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MkSignin from '@/components/MkSignin.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { $i, login } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkAnimBg from '@/components/MkAnimBg.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkAuthConfirm from '@/components/MkAuthConfirm.vue';
|
||||
|
||||
const transactionIdMeta = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:transaction-id"]');
|
||||
if (transactionIdMeta) {
|
||||
|
|
@ -45,10 +33,44 @@ if (transactionIdMeta) {
|
|||
}
|
||||
|
||||
const name = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-name"]')?.content;
|
||||
const permissions = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:scope"]')?.content.split(' ') ?? [];
|
||||
const permissions = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:scope"]')?.content.split(' ').filter((p): p is typeof Misskey.permissions[number] => (Misskey.permissions as readonly string[]).includes(p)) ?? [];
|
||||
|
||||
function onLogin(res): void {
|
||||
login(res.i);
|
||||
function doPost(token: string, decision: 'accept' | 'deny') {
|
||||
const form = document.createElement('form');
|
||||
form.action = '/oauth/decision';
|
||||
form.method = 'post';
|
||||
form.acceptCharset = 'utf-8';
|
||||
|
||||
const loginToken = document.createElement('input');
|
||||
loginToken.type = 'hidden';
|
||||
loginToken.name = 'login_token';
|
||||
loginToken.value = token;
|
||||
form.appendChild(loginToken);
|
||||
|
||||
const transactionId = document.createElement('input');
|
||||
transactionId.type = 'hidden';
|
||||
transactionId.name = 'transaction_id';
|
||||
transactionId.value = transactionIdMeta?.content ?? '';
|
||||
form.appendChild(transactionId);
|
||||
|
||||
if (decision === 'deny') {
|
||||
const cancel = document.createElement('input');
|
||||
cancel.type = 'hidden';
|
||||
cancel.name = 'cancel';
|
||||
cancel.value = 'cancel';
|
||||
form.appendChild(cancel);
|
||||
}
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
function onAccept(token: string) {
|
||||
doPost(token, 'accept');
|
||||
}
|
||||
|
||||
function onDeny(token: string) {
|
||||
doPost(token, 'deny');
|
||||
}
|
||||
|
||||
definePageMetadata(() => ({
|
||||
|
|
@ -58,15 +80,24 @@ definePageMetadata(() => ({
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.buttons {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
.formContainer {
|
||||
min-height: 100svh;
|
||||
padding: 32px 32px calc(env(safe-area-inset-bottom, 0px) + 32px) 32px;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
.loginMessage {
|
||||
text-align: center;
|
||||
margin: 8px 0 24px;
|
||||
.form {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
border-radius: var(--MI-radius);
|
||||
background-color: var(--MI_THEME-panel);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
overflow: clip;
|
||||
max-width: 500px;
|
||||
width: calc(100vw - 64px);
|
||||
height: min(65svh, calc(100svh - calc(env(safe-area-inset-bottom, 0px) + 64px)));
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<XContainer :draggable="true" @remove="() => emit('remove')">
|
||||
<template #header><i class="ti ti-photo"></i> {{ i18n.ts._pages.blocks.image }}</template>
|
||||
<template #func>
|
||||
<button @click="choose()">
|
||||
|
|
@ -30,11 +30,12 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any
|
||||
modelValue: Misskey.entities.PageBlock & { type: 'image' };
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any): void;
|
||||
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'image' }): void;
|
||||
(ev: 'remove'): void;
|
||||
}>();
|
||||
|
||||
const file = ref<Misskey.entities.DriveFile | null>(null);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<XContainer :draggable="true" @remove="() => emit('remove')">
|
||||
<template #header><i class="ti ti-note"></i> {{ i18n.ts._pages.blocks.note }}</template>
|
||||
|
||||
<section style="padding: 16px;" class="_gaps_s">
|
||||
|
|
@ -34,19 +34,24 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any
|
||||
modelValue: Misskey.entities.PageBlock & { type: 'note' };
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any): void;
|
||||
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'note' }): void;
|
||||
}>();
|
||||
|
||||
const id = ref<any>(props.modelValue.note);
|
||||
const id = ref(props.modelValue.note);
|
||||
const note = ref<Misskey.entities.Note | null>(null);
|
||||
|
||||
watch(id, async () => {
|
||||
if (id.value && (id.value.startsWith('http://') || id.value.startsWith('https://'))) {
|
||||
id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop();
|
||||
id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop() ?? null;
|
||||
}
|
||||
|
||||
if (!id.value) {
|
||||
note.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
emit('update:modelValue', {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<XContainer :draggable="true" @remove="() => emit('remove')">
|
||||
<template #header><i class="ti ti-note"></i> {{ props.modelValue.title }}</template>
|
||||
<template #func>
|
||||
<button class="_button" @click="rename()">
|
||||
|
|
@ -21,8 +21,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/* eslint-disable vue/no-mutating-props */
|
||||
|
||||
import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import XContainer from '../page-editor.container.vue';
|
||||
import * as os from '@/os.js';
|
||||
|
|
@ -33,14 +34,13 @@ import { getPageBlockList } from '@/pages/page-editor/common.js';
|
|||
|
||||
const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue'));
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: any,
|
||||
}>(), {
|
||||
modelValue: {},
|
||||
});
|
||||
const props = defineProps<{
|
||||
modelValue: Misskey.entities.PageBlock & { type: 'section'; },
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any): void;
|
||||
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'section' }): void;
|
||||
(ev: 'remove'): void;
|
||||
}>();
|
||||
|
||||
const children = ref(deepClone(props.modelValue.children ?? []));
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<XContainer :draggable="true" @remove="() => emit('remove')">
|
||||
<template #header><i class="ti ti-align-left"></i> {{ i18n.ts._pages.blocks.text }}</template>
|
||||
|
||||
<section>
|
||||
|
|
@ -15,18 +15,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/* eslint-disable vue/no-mutating-props */
|
||||
|
||||
import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XContainer from '../page-editor.container.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { Autocomplete } from '@/scripts/autocomplete.js';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any
|
||||
modelValue: Misskey.entities.PageBlock & { type: 'text' }
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any): void;
|
||||
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'text' }): void;
|
||||
}>();
|
||||
|
||||
let autocomplete: Autocomplete;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => $emit('update:modelValue', v)">
|
||||
<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => emit('update:modelValue', v)">
|
||||
<template #item="{element}">
|
||||
<div :class="$style.item">
|
||||
<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ const props = defineProps<{
|
|||
|
||||
const scope = computed(() => props.path ? props.path.split('/') : []);
|
||||
|
||||
const keys = ref<any>(null);
|
||||
const keys = ref<[string, string][]>([]);
|
||||
|
||||
function fetchKeys() {
|
||||
misskeyApi('i/registry/keys-with-type', {
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.
|
|||
|
||||
const props = defineProps<{
|
||||
game: Misskey.entities.ReversiGameDetailed;
|
||||
connection: Misskey.ChannelConnection;
|
||||
connection: Misskey.ChannelConnection<Misskey.Channels['reversiGame']>;
|
||||
}>();
|
||||
|
||||
const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false });
|
||||
|
|
@ -217,14 +217,14 @@ function onChangeReadyStates(states) {
|
|||
game.value.user2Ready = states.user2;
|
||||
}
|
||||
|
||||
function updateSettings(key: keyof Misskey.entities.ReversiGameDetailed) {
|
||||
function updateSettings(key: typeof Misskey.reversiUpdateKeys[number]) {
|
||||
props.connection.send('updateSettings', {
|
||||
key: key,
|
||||
value: game.value[key],
|
||||
});
|
||||
}
|
||||
|
||||
function onUpdateSettings({ userId, key, value }: { userId: string; key: keyof Misskey.entities.ReversiGameDetailed; value: any; }) {
|
||||
function onUpdateSettings<K extends typeof Misskey.reversiUpdateKeys[number]>({ userId, key, value }: { userId: string; key: K; value: Misskey.entities.ReversiGameDetailed[K]; }) {
|
||||
if (userId === $i.id) return;
|
||||
if (game.value[key] === value) return;
|
||||
game.value[key] = value;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :displayBackButton="true" :tabs="headerTabs"/></template>
|
||||
<MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200">
|
||||
<MkSpacer v-if="error != null" :contentMax="1200">
|
||||
<div :class="$style.root">
|
||||
<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
|
||||
<p :class="$style.text">
|
||||
|
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
{{ error }}
|
||||
</p>
|
||||
</div>
|
||||
</MKSpacer>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'users'" :contentMax="1200">
|
||||
<div class="_gaps_s">
|
||||
<div v-if="role">{{ role.description }}</div>
|
||||
|
|
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'timeline'" :contentMax="700">
|
||||
<MkTimeline v-if="visible" ref="timeline" src="role" :role="props.role"/>
|
||||
<MkTimeline v-if="visible" ref="timeline" src="role" :role="props.roleId"/>
|
||||
<div v-else-if="!visible" class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
|
|
@ -47,23 +47,24 @@ import { instanceName } from '@@/js/config.js';
|
|||
import { serverErrorImageUrl, infoImageUrl } from '@/instance.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
role: string;
|
||||
roleId: string;
|
||||
initialTab?: string;
|
||||
}>(), {
|
||||
initialTab: 'users',
|
||||
});
|
||||
|
||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
||||
const tab = ref(props.initialTab);
|
||||
const role = ref<Misskey.entities.Role>();
|
||||
const error = ref();
|
||||
const role = ref<Misskey.entities.Role | null>(null);
|
||||
const error = ref<string | null>(null);
|
||||
const visible = ref(false);
|
||||
|
||||
watch(() => props.role, () => {
|
||||
watch(() => props.roleId, () => {
|
||||
misskeyApi('roles/show', {
|
||||
roleId: props.role,
|
||||
roleId: props.roleId,
|
||||
}).then(res => {
|
||||
role.value = res;
|
||||
document.title = `${role.value.name} | ${instanceName}`;
|
||||
error.value = null;
|
||||
visible.value = res.isExplorable && res.isPublic;
|
||||
}).catch((err) => {
|
||||
if (err.code === 'NO_SUCH_ROLE') {
|
||||
|
|
@ -71,7 +72,6 @@ watch(() => props.role, () => {
|
|||
} else {
|
||||
error.value = i18n.ts.somethingHappened;
|
||||
}
|
||||
document.title = `${error.value} | ${instanceName}`;
|
||||
});
|
||||
}, { immediate: true });
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ const users = computed(() => ({
|
|||
endpoint: 'roles/users' as const,
|
||||
limit: 30,
|
||||
params: {
|
||||
roleId: props.role,
|
||||
roleId: props.roleId,
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
@ -94,7 +94,7 @@ const headerTabs = computed(() => [{
|
|||
}]);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
title: role.value ? role.value.name : i18n.ts.role,
|
||||
title: role.value ? role.value.name : (error.value ?? i18n.ts.role),
|
||||
icon: 'ti ti-badge',
|
||||
}));
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -76,7 +76,11 @@ import { claimAchievement } from '@/scripts/achievements.js';
|
|||
const parser = new Parser();
|
||||
let aiscript: Interpreter;
|
||||
const code = ref('');
|
||||
const logs = ref<any[]>([]);
|
||||
const logs = ref<{
|
||||
id: number;
|
||||
text: string;
|
||||
print: boolean;
|
||||
}[]>([]);
|
||||
const root = ref<AsUiRoot>();
|
||||
const components = ref<Ref<AsUiComponent>[]>([]);
|
||||
const uiKey = ref(0);
|
||||
|
|
|
|||
|
|
@ -138,12 +138,13 @@ const token = ref<string | number | null>(null);
|
|||
const backupCodes = ref<string[]>();
|
||||
|
||||
function cancel() {
|
||||
dialog.value.close();
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
async function tokenDone() {
|
||||
if (token.value == null) return;
|
||||
const res = await os.apiWithDialog('i/2fa/done', {
|
||||
token: token.value.toString(),
|
||||
token: typeof token.value === 'string' ? token.value : token.value.toString(),
|
||||
});
|
||||
|
||||
backupCodes.value = res.backupCodes;
|
||||
|
|
@ -166,7 +167,7 @@ function downloadBackupCodes() {
|
|||
}
|
||||
|
||||
function allDone() {
|
||||
dialog.value.close();
|
||||
dialog.value?.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ import FormSection from '@/components/form/section.vue';
|
|||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { signinRequired, updateAccount } from '@/account.js';
|
||||
import { signinRequired, updateAccountPartial } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
|
@ -123,7 +123,7 @@ async function unregisterTOTP(): Promise<void> {
|
|||
password: auth.result.password,
|
||||
token: auth.result.token,
|
||||
}).then(res => {
|
||||
updateAccount({
|
||||
updateAccountPartial({
|
||||
twoFactorEnabled: false,
|
||||
});
|
||||
}).catch(error => {
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref, computed } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import type * as Misskey from 'misskey-js';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { getAccounts, addAccount as addAccounts, removeAccount as _removeAccount, login, $i } from '@/account.js';
|
||||
import { getAccounts, removeAccount as _removeAccount, login, $i, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
|
|
@ -74,27 +74,23 @@ async function removeAccount(account: Misskey.entities.UserDetailed) {
|
|||
}
|
||||
|
||||
function addExistingAccount() {
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
|
||||
done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
|
||||
await addAccounts(res.id, res.i);
|
||||
getAccountWithSigninDialog().then((res) => {
|
||||
if (res != null) {
|
||||
os.success();
|
||||
init();
|
||||
},
|
||||
closed: () => dispose(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createAccount() {
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
|
||||
done: async (res: Misskey.entities.SignupResponse) => {
|
||||
await addAccounts(res.id, res.token);
|
||||
getAccountWithSignupDialog().then((res) => {
|
||||
if (res != null) {
|
||||
switchAccountWithToken(res.token);
|
||||
},
|
||||
closed: () => dispose(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function switchAccount(account: any) {
|
||||
async function switchAccount(account: Misskey.entities.UserDetailed) {
|
||||
const fetchedAccounts = await getAccounts();
|
||||
const token = fetchedAccounts.find(x => x.id === account.id)!.token;
|
||||
switchAccountWithToken(token);
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import FormPagination from '@/components/MkPagination.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
@ -77,7 +78,7 @@ const pagination = {
|
|||
|
||||
function revoke(token) {
|
||||
misskeyApi('i/revoke-token', { tokenId: token.id }).then(() => {
|
||||
list.value.reload();
|
||||
list.value?.reload();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
>
|
||||
<div :class="$style.name"><MkCondensedLine :minScale="0.5">{{ decoration.name }}</MkCondensedLine></div>
|
||||
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: decoration.url, angle, flipH, offsetX, offsetY, showBelow }]" forceShowDecoration/>
|
||||
<i v-if="decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.lock" class="ti ti-lock"></i>
|
||||
<i v-if="locked" :class="$style.lock" class="ti ti-lock"></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { signinRequired } from '@/account.js';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
|
@ -38,6 +38,8 @@ const props = defineProps<{
|
|||
const emit = defineEmits<{
|
||||
(ev: 'click'): void;
|
||||
}>();
|
||||
|
||||
const locked = computed(() => props.decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => props.decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id)));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
@ -68,5 +70,6 @@ const emit = defineEmits<{
|
|||
position: absolute;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
color: var(--MI_THEME-warn);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.footer" class="_buttonsCenter">
|
||||
<MkButton v-if="usingIndex != null" primary rounded @click="update"><i class="ti ti-check"></i> {{ i18n.ts.update }}</MkButton>
|
||||
<MkButton v-if="usingIndex != null" rounded @click="detach"><i class="ti ti-x"></i> {{ i18n.ts.detach }}</MkButton>
|
||||
<MkButton v-else :disabled="exceeded" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton>
|
||||
<MkButton v-else :disabled="exceeded || locked" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
|
|
@ -64,6 +64,7 @@ const props = defineProps<{
|
|||
id: string;
|
||||
url: string;
|
||||
name: string;
|
||||
roleIdsThatCanBeUsedThisDecoration: string[];
|
||||
};
|
||||
}>();
|
||||
|
||||
|
|
@ -88,6 +89,7 @@ const emit = defineEmits<{
|
|||
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
const exceeded = computed(() => ($i.policies.avatarDecorationLimit - $i.avatarDecorations.length) <= 0);
|
||||
const locked = computed(() => props.decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => props.decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id)));
|
||||
const angle = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].angle : null) ?? 0);
|
||||
const flipH = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].flipH : null) ?? false);
|
||||
const offsetX = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetX : null) ?? 0);
|
||||
|
|
@ -115,7 +117,7 @@ const decorationsForPreview = computed(() => {
|
|||
});
|
||||
|
||||
function cancel() {
|
||||
dialog.value.close();
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
async function update() {
|
||||
|
|
@ -126,7 +128,7 @@ async function update() {
|
|||
offsetY: offsetY.value,
|
||||
showBelow: showBelow.value,
|
||||
});
|
||||
dialog.value.close();
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
async function attach() {
|
||||
|
|
@ -137,12 +139,12 @@ async function attach() {
|
|||
offsetY: offsetY.value,
|
||||
showBelow: showBelow.value,
|
||||
});
|
||||
dialog.value.close();
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
async function detach() {
|
||||
emit('detach');
|
||||
dialog.value.close();
|
||||
dialog.value?.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -100,10 +100,6 @@ function reset() {
|
|||
}));
|
||||
}
|
||||
|
||||
watch(menuDisplay, async () => {
|
||||
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
|
||||
});
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
|
|
|||
|
|
@ -6,13 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div class="_gaps_m">
|
||||
<MkSelect v-model="type">
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option v-if="hasSender" value="following">{{ i18n.ts.following }}</option>
|
||||
<option v-if="hasSender" value="follower">{{ i18n.ts.followers }}</option>
|
||||
<option v-if="hasSender" value="mutualFollow">{{ i18n.ts.mutualFollow }}</option>
|
||||
<option v-if="hasSender" value="followingOrFollower">{{ i18n.ts.followingOrFollower }}</option>
|
||||
<option v-if="hasSender" value="list">{{ i18n.ts.userList }}</option>
|
||||
<option value="never">{{ i18n.ts.none }}</option>
|
||||
<option v-for="type in props.configurableTypes ?? notificationConfigTypes" :key="type" :value="type">{{ notificationConfigTypesI18nMap[type] }}</option>
|
||||
</MkSelect>
|
||||
|
||||
<MkSelect v-if="type === 'list'" v-model="userListId">
|
||||
|
|
@ -21,34 +15,61 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkSelect>
|
||||
|
||||
<div class="_buttons">
|
||||
<MkButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton inline primary :disabled="type === 'list' && userListId === null" @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
const notificationConfigTypes = [
|
||||
'all',
|
||||
'following',
|
||||
'follower',
|
||||
'mutualFollow',
|
||||
'followingOrFollower',
|
||||
'list',
|
||||
'never'
|
||||
] as const;
|
||||
|
||||
export type NotificationConfig = {
|
||||
type: Exclude<typeof notificationConfigTypes[number], 'list'>;
|
||||
} | {
|
||||
type: 'list';
|
||||
userListId: string;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { ref } from 'vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
value: any;
|
||||
const props = defineProps<{
|
||||
value: NotificationConfig;
|
||||
userLists: Misskey.entities.UserList[];
|
||||
hasSender: boolean;
|
||||
}>(), {
|
||||
hasSender: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update', result: any): void;
|
||||
configurableTypes?: NotificationConfig['type'][]; // If not specified, all types are configurable
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update', result: NotificationConfig): void;
|
||||
}>();
|
||||
|
||||
const notificationConfigTypesI18nMap: Record<typeof notificationConfigTypes[number], string> = {
|
||||
all: i18n.ts.all,
|
||||
following: i18n.ts.following,
|
||||
follower: i18n.ts.followers,
|
||||
mutualFollow: i18n.ts.mutualFollow,
|
||||
followingOrFollower: i18n.ts.followingOrFollower,
|
||||
list: i18n.ts.userList,
|
||||
never: i18n.ts.none,
|
||||
};
|
||||
|
||||
const type = ref(props.value.type);
|
||||
const userListId = ref(props.value.userListId);
|
||||
const userListId = ref(props.value.type === 'list' ? props.value.userListId : null);
|
||||
|
||||
function save() {
|
||||
emit('update', { type: type.value, userListId: userListId.value });
|
||||
emit('update', type.value === 'list' ? { type: type.value, userListId: userListId.value! } : { type: type.value });
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
}}
|
||||
</template>
|
||||
|
||||
<XNotificationConfig :userLists="userLists" :value="$i.notificationRecieveConfig[type] ?? { type: 'all' }" :hasSender="!(notificationTypesWithoutSender.includes(type))" @update="(res) => updateReceiveConfig(type, res)"/>
|
||||
<XNotificationConfig
|
||||
:userLists="userLists"
|
||||
:value="$i.notificationRecieveConfig[type] ?? { type: 'all' }"
|
||||
:configurableTypes="onlyOnOrOffNotificationTypes.includes(type) ? ['all', 'never'] : undefined"
|
||||
@update="(res) => updateReceiveConfig(type, res)"
|
||||
/>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
|
@ -58,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef, computed } from 'vue';
|
||||
import XNotificationConfig from './notifications.notification-config.vue';
|
||||
import XNotificationConfig, { type NotificationConfig } from './notifications.notification-config.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
|
@ -73,8 +78,9 @@ import { notificationTypes } from '@@/js/const.js';
|
|||
|
||||
const $i = signinRequired();
|
||||
|
||||
const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] as const satisfies (typeof notificationTypes[number])[];
|
||||
const notificationTypesWithoutSender = ['achievementEarned'] as const satisfies (typeof notificationTypes[number])[];
|
||||
const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] satisfies (typeof notificationTypes[number])[] as string[];
|
||||
|
||||
const onlyOnOrOffNotificationTypes = ['app', 'achievementEarned', 'login'] satisfies (typeof notificationTypes[number])[] as string[];
|
||||
|
||||
const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
|
||||
const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
|
||||
|
|
@ -89,7 +95,7 @@ async function readAllNotifications() {
|
|||
await os.apiWithDialog('notifications/mark-all-as-read');
|
||||
}
|
||||
|
||||
async function updateReceiveConfig(type, value) {
|
||||
async function updateReceiveConfig(type: typeof notificationTypes[number], value: NotificationConfig) {
|
||||
await os.apiWithDialog('i/update', {
|
||||
notificationRecieveConfig: {
|
||||
...$i.notificationRecieveConfig,
|
||||
|
|
|
|||
|
|
@ -44,6 +44,93 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #caption>{{ i18n.ts.makeExplorableDescription }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.lockdown }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkSwitch :modelValue="requireSigninToViewContents" @update:modelValue="update_requireSigninToViewContents">
|
||||
{{ i18n.ts._accountSettings.requireSigninToViewContents }}
|
||||
<template #caption>
|
||||
<div>{{ i18n.ts._accountSettings.requireSigninToViewContentsDescription1 }}</div>
|
||||
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription2 }}</div>
|
||||
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription3 }}</div>
|
||||
</template>
|
||||
</MkSwitch>
|
||||
|
||||
<FormSlot>
|
||||
<template #label>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBefore }}</template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<MkSelect :modelValue="makeNotesFollowersOnlyBefore_type" @update:modelValue="makeNotesFollowersOnlyBefore = $event === 'relative' ? -604800 : $event === 'absolute' ? Math.floor(Date.now() / 1000) : null">
|
||||
<option :value="null">{{ i18n.ts.none }}</option>
|
||||
<option value="relative">{{ i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod }}</option>
|
||||
<option value="absolute">{{ i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime }}</option>
|
||||
</MkSelect>
|
||||
|
||||
<MkSelect v-if="makeNotesFollowersOnlyBefore_type === 'relative'" v-model="makeNotesFollowersOnlyBefore">
|
||||
<option :value="-3600">{{ i18n.ts.oneHour }}</option>
|
||||
<option :value="-86400">{{ i18n.ts.oneDay }}</option>
|
||||
<option :value="-259200">{{ i18n.ts.threeDays }}</option>
|
||||
<option :value="-604800">{{ i18n.ts.oneWeek }}</option>
|
||||
<option :value="-2592000">{{ i18n.ts.oneMonth }}</option>
|
||||
<option :value="-7776000">{{ i18n.ts.threeMonths }}</option>
|
||||
<option :value="-31104000">{{ i18n.ts.oneYear }}</option>
|
||||
</MkSelect>
|
||||
|
||||
<MkInput
|
||||
v-if="makeNotesFollowersOnlyBefore_type === 'absolute'"
|
||||
:modelValue="formatDateTimeString(new Date(makeNotesFollowersOnlyBefore * 1000), 'yyyy-MM-dd')"
|
||||
type="date"
|
||||
:manualSave="true"
|
||||
@update:modelValue="makeNotesFollowersOnlyBefore = Math.floor(new Date($event).getTime() / 1000)"
|
||||
>
|
||||
</MkInput>
|
||||
</div>
|
||||
|
||||
<template #caption>
|
||||
<div>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription }}</div>
|
||||
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
|
||||
</template>
|
||||
</FormSlot>
|
||||
|
||||
<FormSlot>
|
||||
<template #label>{{ i18n.ts._accountSettings.makeNotesHiddenBefore }}</template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<MkSelect :modelValue="makeNotesHiddenBefore_type" @update:modelValue="makeNotesHiddenBefore = $event === 'relative' ? -604800 : $event === 'absolute' ? Math.floor(Date.now() / 1000) : null">
|
||||
<option :value="null">{{ i18n.ts.none }}</option>
|
||||
<option value="relative">{{ i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod }}</option>
|
||||
<option value="absolute">{{ i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime }}</option>
|
||||
</MkSelect>
|
||||
|
||||
<MkSelect v-if="makeNotesHiddenBefore_type === 'relative'" v-model="makeNotesHiddenBefore">
|
||||
<option :value="-3600">{{ i18n.ts.oneHour }}</option>
|
||||
<option :value="-86400">{{ i18n.ts.oneDay }}</option>
|
||||
<option :value="-259200">{{ i18n.ts.threeDays }}</option>
|
||||
<option :value="-604800">{{ i18n.ts.oneWeek }}</option>
|
||||
<option :value="-2592000">{{ i18n.ts.oneMonth }}</option>
|
||||
<option :value="-7776000">{{ i18n.ts.threeMonths }}</option>
|
||||
<option :value="-31104000">{{ i18n.ts.oneYear }}</option>
|
||||
</MkSelect>
|
||||
|
||||
<MkInput
|
||||
v-if="makeNotesHiddenBefore_type === 'absolute'"
|
||||
:modelValue="formatDateTimeString(new Date(makeNotesHiddenBefore * 1000), 'yyyy-MM-dd')"
|
||||
type="date"
|
||||
:manualSave="true"
|
||||
@update:modelValue="makeNotesHiddenBefore = Math.floor(new Date($event).getTime() / 1000)"
|
||||
>
|
||||
</MkInput>
|
||||
</div>
|
||||
|
||||
<template #caption>
|
||||
<div>{{ i18n.ts._accountSettings.makeNotesHiddenBeforeDescription }}</div>
|
||||
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
|
||||
</template>
|
||||
</FormSlot>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<div class="_gaps_m">
|
||||
<MkSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">{{ i18n.ts.rememberNoteVisibility }}</MkSwitch>
|
||||
|
|
@ -72,7 +159,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
|
|
@ -82,6 +169,10 @@ import { defaultStore } from '@/store.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { signinRequired } from '@/account.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
import { formatDateTimeString } from '@/scripts/format-time-string.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
|
|
@ -90,6 +181,9 @@ const autoAcceptFollowed = ref($i.autoAcceptFollowed);
|
|||
const noCrawle = ref($i.noCrawle);
|
||||
const noindex = ref($i.noindex);
|
||||
const isExplorable = ref($i.isExplorable);
|
||||
const requireSigninToViewContents = ref($i.requireSigninToViewContents ?? false);
|
||||
const makeNotesFollowersOnlyBefore = ref($i.makeNotesFollowersOnlyBefore ?? null);
|
||||
const makeNotesHiddenBefore = ref($i.makeNotesHiddenBefore ?? null);
|
||||
const hideOnlineStatus = ref($i.hideOnlineStatus);
|
||||
const publicReactions = ref($i.publicReactions);
|
||||
const followingVisibility = ref($i.followingVisibility);
|
||||
|
|
@ -100,6 +194,43 @@ const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNote
|
|||
const rememberNoteVisibility = computed(defaultStore.makeGetterSetter('rememberNoteVisibility'));
|
||||
const keepCw = computed(defaultStore.makeGetterSetter('keepCw'));
|
||||
|
||||
const makeNotesFollowersOnlyBefore_type = computed(() => {
|
||||
if (makeNotesFollowersOnlyBefore.value == null) {
|
||||
return null;
|
||||
} else if (makeNotesFollowersOnlyBefore.value >= 0) {
|
||||
return 'absolute';
|
||||
} else {
|
||||
return 'relative';
|
||||
}
|
||||
});
|
||||
|
||||
const makeNotesHiddenBefore_type = computed(() => {
|
||||
if (makeNotesHiddenBefore.value == null) {
|
||||
return null;
|
||||
} else if (makeNotesHiddenBefore.value >= 0) {
|
||||
return 'absolute';
|
||||
} else {
|
||||
return 'relative';
|
||||
}
|
||||
});
|
||||
|
||||
watch([makeNotesFollowersOnlyBefore, makeNotesHiddenBefore], () => {
|
||||
save();
|
||||
});
|
||||
|
||||
async function update_requireSigninToViewContents(value: boolean) {
|
||||
if (value) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.acknowledgeNotesAndEnable,
|
||||
});
|
||||
if (canceled) return;
|
||||
}
|
||||
|
||||
requireSigninToViewContents.value = value;
|
||||
save();
|
||||
}
|
||||
|
||||
function save() {
|
||||
misskeyApi('i/update', {
|
||||
isLocked: !!isLocked.value,
|
||||
|
|
@ -107,6 +238,9 @@ function save() {
|
|||
noCrawle: !!noCrawle.value,
|
||||
noindex: !!noindex.value,
|
||||
isExplorable: !!isExplorable.value,
|
||||
requireSigninToViewContents: !!requireSigninToViewContents.value,
|
||||
makeNotesFollowersOnlyBefore: makeNotesFollowersOnlyBefore.value,
|
||||
makeNotesHiddenBefore: makeNotesHiddenBefore.value,
|
||||
hideOnlineStatus: !!hideOnlineStatus.value,
|
||||
publicReactions: !!publicReactions.value,
|
||||
followingVisibility: followingVisibility.value,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_renote)" @click="test('renote')"><i class="ti ti-send"></i></MkButton>
|
||||
</div>
|
||||
<div :class="$style.switchBox">
|
||||
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
|
||||
<MkSwitch v-model="event_reaction" :disabled="true">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
|
||||
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_reaction)" @click="test('reaction')"><i class="ti ti-send"></i></MkButton>
|
||||
</div>
|
||||
<div :class="$style.switchBox">
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch>
|
||||
<MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch>
|
||||
<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
|
||||
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
|
||||
<MkSwitch v-model="event_reaction" :disabled="true">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
|
||||
<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
|
|
|||
|
|
@ -17,11 +17,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.tl">
|
||||
<MkTimeline
|
||||
ref="tlComponent"
|
||||
:key="src + withRenotes + withBots + withReplies + onlyFiles"
|
||||
:key="src + withRenotes + withBoth + withReplies + onlyFiles + withSensitive"
|
||||
:src="src.split(':')[0]"
|
||||
:list="src.split(':')[1]"
|
||||
:withRenotes="withRenotes"
|
||||
:withReplies="withReplies"
|
||||
:withSensitive="withSensitive"
|
||||
:onlyFiles="onlyFiles"
|
||||
:withBots="withBots"
|
||||
:sound="true"
|
||||
|
|
@ -130,11 +131,6 @@ watch(src, () => {
|
|||
queue.value = 0;
|
||||
});
|
||||
|
||||
watch(withSensitive, () => {
|
||||
// これだけはクライアント側で完結する処理なので手動でリロード
|
||||
tlComponent.value?.reloadTimeline();
|
||||
});
|
||||
|
||||
function queueUpdated(q: number): void {
|
||||
queue.value = q;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -351,7 +351,7 @@ function parallaxLoop() {
|
|||
}
|
||||
|
||||
function parallax() {
|
||||
const banner = bannerEl.value as any;
|
||||
const banner = bannerEl.value;
|
||||
if (banner == null) return;
|
||||
|
||||
const top = getScrollPosition(rootEl.value);
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { getServerContext } from '@/server-context.js';
|
||||
|
||||
const XHome = defineAsyncComponent(() => import('./home.vue'));
|
||||
const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
|
||||
|
|
@ -52,6 +53,8 @@ const XFlashs = defineAsyncComponent(() => import('./flashs.vue'));
|
|||
const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
|
||||
const XRaw = defineAsyncComponent(() => import('./raw.vue'));
|
||||
|
||||
const CTX_USER = getServerContext('user');
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
acct: string;
|
||||
page?: string;
|
||||
|
|
@ -61,13 +64,24 @@ const props = withDefaults(defineProps<{
|
|||
|
||||
const tab = ref(props.page);
|
||||
|
||||
const user = ref<null | Misskey.entities.UserDetailed>(null);
|
||||
const user = ref<null | Misskey.entities.UserDetailed>(CTX_USER);
|
||||
const error = ref<any>(null);
|
||||
|
||||
function fetchUser(): void {
|
||||
if (props.acct == null) return;
|
||||
|
||||
const { username, host } = Misskey.acct.parse(props.acct);
|
||||
|
||||
if (CTX_USER && CTX_USER.username === username && CTX_USER.host === host) {
|
||||
user.value = CTX_USER;
|
||||
return;
|
||||
}
|
||||
|
||||
user.value = null;
|
||||
misskeyApi('users/show', Misskey.acct.parse(props.acct)).then(u => {
|
||||
misskeyApi('users/show', {
|
||||
username,
|
||||
host,
|
||||
}).then(u => {
|
||||
user.value = u;
|
||||
}).catch(err => {
|
||||
error.value = err;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue