Merge branch 'develop' into pag-back
This commit is contained in:
@@ -115,3 +115,27 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
|
||||
url: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function inviteCode(isUsed = false, hasExpiration = false, isExpired = false, isCreatedBySystem = false) {
|
||||
const date = new Date();
|
||||
const createdAt = new Date();
|
||||
createdAt.setDate(date.getDate() - 1)
|
||||
const expiresAt = new Date();
|
||||
|
||||
if (isExpired) {
|
||||
expiresAt.setHours(date.getHours() - 1)
|
||||
} else {
|
||||
expiresAt.setHours(date.getHours() + 1)
|
||||
}
|
||||
|
||||
return {
|
||||
id: "9gyqzizw77",
|
||||
code: "SLF3JKF7UV2H9",
|
||||
expiresAt: hasExpiration ? expiresAt.toISOString() : null,
|
||||
createdAt: createdAt.toISOString(),
|
||||
createdBy: isCreatedBySystem ? null : userDetailed('8i3rvznx32'),
|
||||
usedBy: isUsed ? userDetailed('3i3r2znx1v') : null,
|
||||
usedAt: isUsed ? date.toISOString() : null,
|
||||
used: isUsed,
|
||||
}
|
||||
}
|
||||
|
@@ -403,6 +403,7 @@ function toStories(component: string): Promise<string> {
|
||||
glob('src/components/MkSignupServerRules.vue'),
|
||||
glob('src/components/MkUserSetupDialog.vue'),
|
||||
glob('src/components/MkUserSetupDialog.*.vue'),
|
||||
glob('src/components/MkInviteCode.vue'),
|
||||
glob('src/pages/user/home.vue'),
|
||||
]);
|
||||
const components = globs.flat();
|
||||
|
@@ -20,9 +20,9 @@
|
||||
"@rollup/plugin-replace": "5.0.2",
|
||||
"@rollup/pluginutils": "5.0.2",
|
||||
"@syuilo/aiscript": "0.13.3",
|
||||
"@tabler/icons-webfont": "2.24.0",
|
||||
"@tabler/icons-webfont": "2.25.0",
|
||||
"@vitejs/plugin-vue": "4.2.3",
|
||||
"@vue-macros/reactivity-transform": "0.3.11",
|
||||
"@vue-macros/reactivity-transform": "0.3.14",
|
||||
"@vue/compiler-sfc": "3.3.4",
|
||||
"astring": "1.8.6",
|
||||
"autosize": "6.0.1",
|
||||
@@ -70,30 +70,30 @@
|
||||
"typescript": "5.1.6",
|
||||
"uuid": "9.0.0",
|
||||
"vanilla-tilt": "1.8.0",
|
||||
"vite": "4.4.1",
|
||||
"vite": "4.4.4",
|
||||
"vue": "3.3.4",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "7.0.26",
|
||||
"@storybook/addon-essentials": "7.0.26",
|
||||
"@storybook/addon-interactions": "7.0.26",
|
||||
"@storybook/addon-links": "7.0.26",
|
||||
"@storybook/addon-storysource": "7.0.26",
|
||||
"@storybook/addons": "7.0.26",
|
||||
"@storybook/blocks": "7.0.26",
|
||||
"@storybook/core-events": "7.0.26",
|
||||
"@storybook/addon-actions": "7.0.27",
|
||||
"@storybook/addon-essentials": "7.0.27",
|
||||
"@storybook/addon-interactions": "7.0.27",
|
||||
"@storybook/addon-links": "7.0.27",
|
||||
"@storybook/addon-storysource": "7.0.27",
|
||||
"@storybook/addons": "7.0.27",
|
||||
"@storybook/blocks": "7.0.27",
|
||||
"@storybook/core-events": "7.0.27",
|
||||
"@storybook/jest": "0.1.0",
|
||||
"@storybook/manager-api": "7.0.26",
|
||||
"@storybook/preview-api": "7.0.26",
|
||||
"@storybook/react": "7.0.26",
|
||||
"@storybook/react-vite": "7.0.26",
|
||||
"@storybook/manager-api": "7.0.27",
|
||||
"@storybook/preview-api": "7.0.27",
|
||||
"@storybook/react": "7.0.27",
|
||||
"@storybook/react-vite": "7.0.27",
|
||||
"@storybook/testing-library": "0.2.0",
|
||||
"@storybook/theming": "7.0.26",
|
||||
"@storybook/types": "7.0.26",
|
||||
"@storybook/vue3": "7.0.26",
|
||||
"@storybook/vue3-vite": "7.0.26",
|
||||
"@storybook/theming": "7.0.27",
|
||||
"@storybook/types": "7.0.27",
|
||||
"@storybook/vue3": "7.0.27",
|
||||
"@storybook/vue3-vite": "7.0.27",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/vue": "7.0.0",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
@@ -102,10 +102,10 @@
|
||||
"@types/gulp-rename": "2.0.2",
|
||||
"@types/matter-js": "0.18.5",
|
||||
"@types/micromatch": "4.0.2",
|
||||
"@types/node": "20.4.0",
|
||||
"@types/node": "20.4.2",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/sanitize-html": "2.9.0",
|
||||
"@types/testing-library__jest-dom": "5.14.7",
|
||||
"@types/testing-library__jest-dom": "5.14.8",
|
||||
"@types/throttle-debounce": "5.0.0",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"@types/uuid": "9.0.2",
|
||||
@@ -118,8 +118,8 @@
|
||||
"acorn": "8.10.0",
|
||||
"chokidar-cli": "3.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.17.0",
|
||||
"eslint": "8.44.0",
|
||||
"cypress": "12.17.1",
|
||||
"eslint": "8.45.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-vue": "9.15.1",
|
||||
"fast-glob": "3.3.0",
|
||||
@@ -131,13 +131,13 @@
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"start-server-and-test": "2.0.0",
|
||||
"storybook": "7.0.26",
|
||||
"storybook": "7.0.27",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"vite-plugin-turbosnap": "1.0.2",
|
||||
"vitest": "0.33.0",
|
||||
"vitest-fetch-mock": "0.2.2",
|
||||
"vue-eslint-parser": "9.3.1",
|
||||
"vue-tsc": "1.8.4"
|
||||
"vue-tsc": "1.8.5"
|
||||
}
|
||||
}
|
||||
|
@@ -13,10 +13,11 @@ import { miLocalStorage } from '@/local-storage';
|
||||
import { claimAchievement, claimedAchievements } from '@/scripts/achievements';
|
||||
import { mainRouter } from '@/router';
|
||||
import { initializeSw } from '@/scripts/initialize-sw';
|
||||
import { deckStore } from '@/ui/deck/deck-store';
|
||||
|
||||
export async function mainBoot() {
|
||||
const { isClientUpdated } = await common(() => createApp(
|
||||
new URLSearchParams(window.location.search).has('zen') || (ui === 'deck' && location.pathname !== '/') ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
|
||||
new URLSearchParams(window.location.search).has('zen') || (ui === 'deck' && deckStore.state.useSimpleUiForNonRootPages && location.pathname !== '/') ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
|
||||
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
|
||||
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
|
||||
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
|
||||
|
@@ -356,9 +356,7 @@ onMounted(() => {
|
||||
|
||||
props.textarea.addEventListener('keydown', onKeydown);
|
||||
|
||||
for (const el of Array.from(document.querySelectorAll('body *'))) {
|
||||
el.addEventListener('mousedown', onMousedown);
|
||||
}
|
||||
document.body.addEventListener('mousedown', onMousedown);
|
||||
|
||||
nextTick(() => {
|
||||
exec();
|
||||
@@ -374,9 +372,7 @@ onMounted(() => {
|
||||
onBeforeUnmount(() => {
|
||||
props.textarea.removeEventListener('keydown', onKeydown);
|
||||
|
||||
for (const el of Array.from(document.querySelectorAll('body *'))) {
|
||||
el.removeEventListener('mousedown', onMousedown);
|
||||
}
|
||||
document.body.removeEventListener('mousedown', onMousedown);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@@ -61,15 +61,11 @@ onMounted(() => {
|
||||
rootEl.style.top = `${top}px`;
|
||||
rootEl.style.left = `${left}px`;
|
||||
|
||||
for (const el of Array.from(document.querySelectorAll('body *'))) {
|
||||
el.addEventListener('mousedown', onMousedown);
|
||||
}
|
||||
document.body.addEventListener('mousedown', onMousedown);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
for (const el of Array.from(document.querySelectorAll('body *'))) {
|
||||
el.removeEventListener('mousedown', onMousedown);
|
||||
}
|
||||
document.body.removeEventListener('mousedown', onMousedown);
|
||||
});
|
||||
|
||||
function onMousedown(evt: Event) {
|
||||
|
@@ -0,0 +1,60 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { rest } from 'msw';
|
||||
import { userDetailed, inviteCode } from '../../.storybook/fakes';
|
||||
import { commonHandlers } from '../../.storybook/mocks';
|
||||
import MkInviteCode from './MkInviteCode.vue';
|
||||
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkInviteCode,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkInviteCode v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
invite: inviteCode() as any,
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
msw: {
|
||||
handlers: [
|
||||
...commonHandlers,
|
||||
rest.post('/api/users/show', (req, res, ctx) => {
|
||||
return res(ctx.json(userDetailed(req.params.userId as string)));
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
decorators: [() => ({
|
||||
template: '<div style="width:100cqmin"><story/></div>',
|
||||
})],
|
||||
} satisfies StoryObj<typeof MkInviteCode>;
|
||||
|
||||
export const Used = {
|
||||
...Default,
|
||||
args: {
|
||||
invite: inviteCode(true) as any
|
||||
},
|
||||
} satisfies StoryObj<typeof MkInviteCode>;
|
||||
|
||||
export const Expired = {
|
||||
...Default,
|
||||
args: {
|
||||
invite: inviteCode(false, true, true) as any
|
||||
},
|
||||
} satisfies StoryObj<typeof MkInviteCode>;
|
124
packages/frontend/src/components/MkInviteCode.vue
Normal file
124
packages/frontend/src/components/MkInviteCode.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<MkFolder>
|
||||
<template #label>{{ invite.code }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="invite.used">{{ i18n.ts.used }}</span>
|
||||
<span v-else-if="isExpired" style="color: var(--error)">{{ i18n.ts.expired }}</span>
|
||||
<span v-else style="color: var(--success)">{{ i18n.ts.unused }}</span>
|
||||
</template>
|
||||
|
||||
<div class="_gaps_s" :class="$style.root">
|
||||
<div :class="$style.items">
|
||||
<div>
|
||||
<div :class="$style.label">{{ i18n.ts.invitationCode }}</div>
|
||||
<div>{{ invite.code }}</div>
|
||||
</div>
|
||||
<div v-if="moderator">
|
||||
<div :class="$style.label">{{ i18n.ts.inviteCodeCreator }}</div>
|
||||
<div v-if="invite.createdBy" :class="$style.user">
|
||||
<MkAvatar :user="invite.createdBy" :class="$style.avatar" link preview/>
|
||||
<MkUserName :user="invite.createdBy" :nowrap="false"/>
|
||||
<div v-if="moderator">({{ invite.createdBy.id }})</div>
|
||||
</div>
|
||||
<div v-else>system</div>
|
||||
</div>
|
||||
<div v-if="invite.used">
|
||||
<div :class="$style.label">{{ i18n.ts.registeredUserUsingInviteCode }}</div>
|
||||
<div v-if="invite.usedBy" :class="$style.user">
|
||||
<MkAvatar :user="invite.usedBy" :class="$style.avatar" link preview/>
|
||||
<MkUserName :user="invite.usedBy" :nowrap="false"/>
|
||||
<div v-if="moderator">({{ invite.usedBy.id }})</div>
|
||||
</div>
|
||||
<div v-else>{{ i18n.ts.unknown }} ({{ i18n.ts.waitingForMailAuth }})</div>
|
||||
</div>
|
||||
<div v-if="invite.expiresAt && !invite.used">
|
||||
<div :class="$style.label">{{ i18n.ts.expirationDate }}</div>
|
||||
<div><MkTime :time="invite.expiresAt" mode="absolute"/></div>
|
||||
</div>
|
||||
<div v-if="invite.usedAt">
|
||||
<div :class="$style.label">{{ i18n.ts.inviteCodeUsedAt }}</div>
|
||||
<div><MkTime :time="invite.usedAt" mode="absolute"/></div>
|
||||
</div>
|
||||
<div v-if="moderator">
|
||||
<div :class="$style.label">{{ i18n.ts.createdAt }}</div>
|
||||
<div><MkTime :time="invite.createdAt" mode="absolute"/></div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.buttons">
|
||||
<MkButton v-if="!invite.used && !isExpired" primary rounded @click="copyInviteCode()"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
|
||||
<MkButton v-if="!invite.used || moderator" danger rounded @click="deleteCode()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
import { i18n } from '@/i18n';
|
||||
import * as os from '@/os';
|
||||
|
||||
const props = defineProps<{
|
||||
invite: misskey.entities.Invite;
|
||||
moderator?: boolean;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'deleted', value: string): void;
|
||||
}>();
|
||||
|
||||
const isExpired = computed(() => {
|
||||
return props.invite.expiresAt && new Date(props.invite.expiresAt) < new Date();
|
||||
});
|
||||
|
||||
function deleteCode() {
|
||||
os.apiWithDialog('invite/delete', {
|
||||
inviteId: props.invite.id,
|
||||
});
|
||||
emits('deleted', props.invite.id);
|
||||
}
|
||||
|
||||
function copyInviteCode() {
|
||||
copyToClipboard(props.invite.code);
|
||||
os.success();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
grid-gap: 12px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.85em;
|
||||
padding: 0 0 8px 0;
|
||||
user-select: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
--height: 24px;
|
||||
width: var(--height);
|
||||
height: var(--height);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
@@ -256,7 +256,7 @@ export default function(props: {
|
||||
case 'mention': {
|
||||
return [h(MkMention, {
|
||||
key: Math.random(),
|
||||
host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) || host,
|
||||
host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host,
|
||||
username: token.props.username,
|
||||
})];
|
||||
}
|
||||
|
@@ -57,6 +57,9 @@ export const ROLE_POLICIES = [
|
||||
'ltlAvailable',
|
||||
'canPublicNote',
|
||||
'canInvite',
|
||||
'inviteLimit',
|
||||
'inviteLimitCycle',
|
||||
'inviteExpirationTime',
|
||||
'canManageCustomEmojis',
|
||||
'canSearchNotes',
|
||||
'canHideAds',
|
||||
|
@@ -167,6 +167,18 @@ const patronsWithIcon = [{
|
||||
}, {
|
||||
name: 'Nagi8410',
|
||||
icon: 'https://misskey-hub.net/patrons/31b102ab4fc540ed806b0461575d38be.jpg',
|
||||
}, {
|
||||
name: '山岡士郎',
|
||||
icon: 'https://misskey-hub.net/patrons/84b9056341684266bb1eda3e680d094d.jpg',
|
||||
}, {
|
||||
name: 'よもやまたろう',
|
||||
icon: 'https://misskey-hub.net/patrons/4273c9cce50d445f8f7d0f16113d6d7f.jpg',
|
||||
}, {
|
||||
name: '花咲ももか',
|
||||
icon: 'https://misskey-hub.net/patrons/8c9b2b9128cb4fee99f04bb4f86f2efa.jpg',
|
||||
}, {
|
||||
name: 'カガミ',
|
||||
icon: 'https://misskey-hub.net/patrons/226ea3a4617749548580ec2d9a263e24.jpg',
|
||||
}];
|
||||
|
||||
const patrons = [
|
||||
@@ -263,6 +275,7 @@ const patrons = [
|
||||
'渡志郎',
|
||||
'ぷーざ',
|
||||
'越貝鯛丸',
|
||||
'Nick / pprmint.',
|
||||
];
|
||||
|
||||
let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure'));
|
||||
|
@@ -80,7 +80,7 @@ const menuDef = $computed(() => [{
|
||||
}, ...(instance.disableRegistration ? [{
|
||||
type: 'button',
|
||||
icon: 'ti ti-user-plus',
|
||||
text: i18n.ts.invite,
|
||||
text: i18n.ts.createInviteCode,
|
||||
action: invite,
|
||||
}] : [])],
|
||||
}, {
|
||||
@@ -95,6 +95,11 @@ const menuDef = $computed(() => [{
|
||||
text: i18n.ts.users,
|
||||
to: '/admin/users',
|
||||
active: currentPage?.route.name === 'users',
|
||||
}, {
|
||||
icon: 'ti ti-user-plus',
|
||||
text: i18n.ts.invite,
|
||||
to: '/admin/invites',
|
||||
active: currentPage?.route.name === 'invites',
|
||||
}, {
|
||||
icon: 'ti ti-badges',
|
||||
text: i18n.ts.roles,
|
||||
@@ -240,10 +245,10 @@ provideMetadataReceiver((info) => {
|
||||
});
|
||||
|
||||
const invite = () => {
|
||||
os.api('invite').then(x => {
|
||||
os.api('admin/invite/create').then(x => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: x.code,
|
||||
text: x?.[0].code,
|
||||
});
|
||||
}).catch(err => {
|
||||
os.alert({
|
||||
|
126
packages/frontend/src/pages/admin/invites.vue
Normal file
126
packages/frontend/src/pages/admin/invites.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<div class="_gaps_m">
|
||||
<MkFolder :expanded="false">
|
||||
<template #icon><i class="ti ti-plus"></i></template>
|
||||
<template #label>{{ i18n.ts.createInviteCode }}</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkSwitch v-model="noExpirationDate">
|
||||
<template #label>{{ i18n.ts.noExpirationDate }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-if="!noExpirationDate" v-model="expiresAt" type="datetime-local">
|
||||
<template #label>{{ i18n.ts.expirationDate }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="createCount" type="number">
|
||||
<template #label>{{ i18n.ts.createCount }}</template>
|
||||
</MkInput>
|
||||
<MkButton primary rounded @click="createWithOptions">{{ i18n.ts.create }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<div :class="$style.inputs">
|
||||
<MkSelect v-model="type" :class="$style.input">
|
||||
<template #label>{{ i18n.ts.state }}</template>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="unused">{{ i18n.ts.unused }}</option>
|
||||
<option value="used">{{ i18n.ts.used }}</option>
|
||||
<option value="expired">{{ i18n.ts.expired }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="sort" :class="$style.input">
|
||||
<template #label>{{ i18n.ts.sort }}</template>
|
||||
<option value="+createdAt">{{ i18n.ts.createdAt }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="-createdAt">{{ i18n.ts.createdAt }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
<option value="+usedAt">{{ i18n.ts.usedAt }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||
<option value="-usedAt">{{ i18n.ts.usedAt }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps_s">
|
||||
<MkInviteCode v-for="item in items" :key="item.id" :invite="(item as any)" :onDeleted="deleted" moderator/>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, shallowRef } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import * as os from '@/os';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||
import MkInviteCode from '@/components/MkInviteCode.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
||||
let type = ref('all');
|
||||
let sort = ref('+createdAt');
|
||||
|
||||
const pagination: Paging = {
|
||||
endpoint: 'admin/invite/list' as const,
|
||||
limit: 10,
|
||||
params: computed(() => ({
|
||||
type: type.value,
|
||||
sort: sort.value,
|
||||
})),
|
||||
offsetMode: true,
|
||||
};
|
||||
|
||||
const expiresAt = ref('');
|
||||
const noExpirationDate = ref(true);
|
||||
const createCount = ref(1);
|
||||
|
||||
async function createWithOptions() {
|
||||
const options = {
|
||||
expiresAt: noExpirationDate.value ? null : expiresAt.value,
|
||||
count: createCount.value,
|
||||
};
|
||||
|
||||
const tickets = await os.api('admin/invite/create', options);
|
||||
os.alert({
|
||||
type: 'success',
|
||||
title: i18n.ts.inviteCodeCreated,
|
||||
text: tickets?.map(x => x.code).join('\n'),
|
||||
});
|
||||
|
||||
tickets?.forEach(ticket => pagingComponent.value?.prepend(ticket));
|
||||
}
|
||||
|
||||
function deleted(id: string) {
|
||||
if (pagingComponent.value) {
|
||||
pagingComponent.value.items.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.invite,
|
||||
icon: 'ti ti-user-plus',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.inputs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<MkInput v-if="readonly" :modelValue="role.id" :readonly="true">
|
||||
<template #label>ID</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="role.name" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.name }}</template>
|
||||
</MkInput>
|
||||
@@ -171,6 +175,65 @@
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimit, 'inviteLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.inviteLimit }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.inviteLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.inviteLimit.value }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.inviteLimit)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.inviteLimit.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="role.policies.inviteLimit.value" :disabled="role.policies.inviteLimit.useDefault" type="number" :readonly="readonly">
|
||||
</MkInput>
|
||||
<MkRange v-model="role.policies.inviteLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimitCycle, 'inviteLimitCycle'])">
|
||||
<template #label>{{ i18n.ts._role._options.inviteLimitCycle }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.inviteLimitCycle.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.inviteLimitCycle.value + i18n.ts._time.minute }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.inviteLimitCycle)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.inviteLimitCycle.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="role.policies.inviteLimitCycle.value" :disabled="role.policies.inviteLimitCycle.useDefault" type="number" :readonly="readonly">
|
||||
<template #suffix>{{ i18n.ts._time.minute }}</template>
|
||||
</MkInput>
|
||||
<MkRange v-model="role.policies.inviteLimitCycle.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteExpirationTime, 'inviteExpirationTime'])">
|
||||
<template #label>{{ i18n.ts._role._options.inviteExpirationTime }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.inviteExpirationTime.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.inviteExpirationTime.value + i18n.ts._time.minute }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.inviteExpirationTime)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.inviteExpirationTime.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkInput v-model="role.policies.inviteExpirationTime.value" :disabled="role.policies.inviteExpirationTime.useDefault" type="number" :readonly="readonly">
|
||||
<template #suffix>{{ i18n.ts._time.minute }}</template>
|
||||
</MkInput>
|
||||
<MkRange v-model="role.policies.inviteExpirationTime.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])">
|
||||
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
||||
<template #suffix>
|
||||
|
@@ -51,6 +51,29 @@
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimit, 'inviteLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.inviteLimit }}</template>
|
||||
<template #suffix>{{ policies.inviteLimit }}</template>
|
||||
<MkInput v-model="policies.inviteLimit" type="number">
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimitCycle, 'inviteLimitCycle'])">
|
||||
<template #label>{{ i18n.ts._role._options.inviteLimitCycle }}</template>
|
||||
<template #suffix>{{ policies.inviteLimitCycle + i18n.ts._time.minute }}</template>
|
||||
<MkInput v-model="policies.inviteLimitCycle" type="number">
|
||||
<template #suffix>{{ i18n.ts._time.minute }}</template>
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteExpirationTime, 'inviteExpirationTime'])">
|
||||
<template #label>{{ i18n.ts._role._options.inviteExpirationTime }}</template>
|
||||
<template #suffix>{{ policies.inviteExpirationTime + i18n.ts._time.minute }}</template>
|
||||
<MkInput v-model="policies.inviteExpirationTime" type="number">
|
||||
<template #suffix>{{ i18n.ts._time.minute }}</template>
|
||||
</MkInput>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])">
|
||||
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
||||
<template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
|
@@ -37,6 +37,13 @@
|
||||
<template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
|
||||
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<template v-if="cacheRemoteFiles">
|
||||
<MkSwitch v-model="cacheRemoteSensitiveFiles">
|
||||
<template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}</template>
|
||||
<template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
@@ -104,7 +111,6 @@ import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkColorInput from '@/components/MkColorInput.vue';
|
||||
|
||||
let name: string | null = $ref(null);
|
||||
let description: string | null = $ref(null);
|
||||
@@ -112,13 +118,14 @@ let maintainerName: string | null = $ref(null);
|
||||
let maintainerEmail: string | null = $ref(null);
|
||||
let pinnedUsers: string = $ref('');
|
||||
let cacheRemoteFiles: boolean = $ref(false);
|
||||
let cacheRemoteSensitiveFiles: boolean = $ref(false);
|
||||
let enableServiceWorker: boolean = $ref(false);
|
||||
let swPublicKey: any = $ref(null);
|
||||
let swPrivateKey: any = $ref(null);
|
||||
let deeplAuthKey: string = $ref('');
|
||||
let deeplIsPro: boolean = $ref(false);
|
||||
|
||||
async function init() {
|
||||
async function init(): Promise<void> {
|
||||
const meta = await os.api('admin/meta');
|
||||
name = meta.name;
|
||||
description = meta.description;
|
||||
@@ -126,6 +133,7 @@ async function init() {
|
||||
maintainerEmail = meta.maintainerEmail;
|
||||
pinnedUsers = meta.pinnedUsers.join('\n');
|
||||
cacheRemoteFiles = meta.cacheRemoteFiles;
|
||||
cacheRemoteSensitiveFiles = meta.cacheRemoteSensitiveFiles;
|
||||
enableServiceWorker = meta.enableServiceWorker;
|
||||
swPublicKey = meta.swPublickey;
|
||||
swPrivateKey = meta.swPrivateKey;
|
||||
@@ -133,7 +141,7 @@ async function init() {
|
||||
deeplIsPro = meta.deeplIsPro;
|
||||
}
|
||||
|
||||
function save() {
|
||||
function save(): void {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
name,
|
||||
description,
|
||||
@@ -141,6 +149,7 @@ function save() {
|
||||
maintainerEmail,
|
||||
pinnedUsers: pinnedUsers.split('\n'),
|
||||
cacheRemoteFiles,
|
||||
cacheRemoteSensitiveFiles,
|
||||
enableServiceWorker,
|
||||
swPublicKey,
|
||||
swPrivateKey,
|
||||
|
114
packages/frontend/src/pages/invite.vue
Normal file
114
packages/frontend/src/pages/invite.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<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">
|
||||
<i class="ti ti-alert-triangle"></i>
|
||||
{{ i18n.ts.nothing }}
|
||||
</div>
|
||||
</div>
|
||||
</MKSpacer>
|
||||
<MkSpacer v-else :contentMax="800">
|
||||
<div class="_gaps_m" style="text-align: center;">
|
||||
<div v-if="resetCycle && inviteLimit">{{ i18n.t('inviteLimitResetCycle', { time: resetCycle, limit: inviteLimit }) }}</div>
|
||||
<MkButton inline primary rounded :disabled="currentInviteLimit !== null && currentInviteLimit <= 0" @click="create"><i class="ti ti-user-plus"></i> {{ i18n.ts.createInviteCode }}</MkButton>
|
||||
<div v-if="currentInviteLimit !== null">{{ i18n.t('createLimitRemaining', { limit: currentInviteLimit }) }}</div>
|
||||
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps_s">
|
||||
<MkInviteCode v-for="item in (items as Invite[])" :key="item.id" :invite="item" :onDeleted="deleted"/>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, shallowRef } from 'vue';
|
||||
import type { Invite } from 'misskey-js/built/entities';
|
||||
import { i18n } from '@/i18n';
|
||||
import * as os from '@/os';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||
import MkInviteCode from '@/components/MkInviteCode.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { serverErrorImageUrl, instance } from '@/instance';
|
||||
import { $i } from '@/account';
|
||||
|
||||
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
const currentInviteLimit = ref<null | number>(null);
|
||||
const inviteLimit = (($i != null && $i.policies.inviteLimit) || (($i == null && instance.policies.inviteLimit))) as number;
|
||||
const inviteLimitCycle = (($i != null && $i.policies.inviteLimitCycle) || ($i == null && instance.policies.inviteLimitCycle)) as number;
|
||||
|
||||
const pagination: Paging = {
|
||||
endpoint: 'invite/list' as const,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
const resetCycle = computed<null | string>(() => {
|
||||
if (!inviteLimitCycle) return null;
|
||||
|
||||
const minutes = inviteLimitCycle;
|
||||
if (minutes < 60) return minutes + i18n.ts._time.minute;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return hours + i18n.ts._time.hour;
|
||||
return Math.floor(hours / 24) + i18n.ts._time.day;
|
||||
});
|
||||
|
||||
async function create() {
|
||||
const ticket = await os.api('invite/create');
|
||||
os.alert({
|
||||
type: 'success',
|
||||
title: i18n.ts.inviteCodeCreated,
|
||||
text: ticket.code,
|
||||
});
|
||||
|
||||
pagingComponent.value?.prepend(ticket);
|
||||
update();
|
||||
}
|
||||
|
||||
function deleted(id: string) {
|
||||
if (pagingComponent.value) {
|
||||
pagingComponent.value.items.delete(id);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
async function update() {
|
||||
currentInviteLimit.value = (await os.api('invite/limit')).remaining;
|
||||
}
|
||||
|
||||
update();
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.invite,
|
||||
icon: 'ti ti-user-plus',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.img {
|
||||
vertical-align: bottom;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
</style>
|
@@ -14,7 +14,7 @@
|
||||
|
||||
<div v-if="items.length > 0" class="_gaps">
|
||||
<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`">
|
||||
<div style="margin-bottom: 4px;">{{ list.name }}</div>
|
||||
<div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i?.policies['userEachUserListsLimit']}` }) }})</span></div>
|
||||
<MkAvatars :userIds="list.userIds" :limit="10"/>
|
||||
</MkA>
|
||||
</div>
|
||||
@@ -32,6 +32,7 @@ import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { userListsCache } from '@/cache';
|
||||
import { infoImageUrl } from '@/instance';
|
||||
import { $i } from '@/account';
|
||||
|
||||
const items = $computed(() => userListsCache.value.value ?? []);
|
||||
|
||||
@@ -66,10 +67,6 @@ const headerTabs = $computed(() => []);
|
||||
definePageMetadata({
|
||||
title: i18n.ts.manageLists,
|
||||
icon: 'ti ti-list',
|
||||
action: {
|
||||
icon: 'ti ti-plus',
|
||||
handler: create,
|
||||
},
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
@@ -90,4 +87,9 @@ onActivated(() => {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nUsers {
|
||||
font-size: .9em;
|
||||
opacity: .7;
|
||||
}
|
||||
</style>
|
||||
|
@@ -20,6 +20,7 @@
|
||||
|
||||
<MkFolder defaultOpen>
|
||||
<template #label>{{ i18n.ts.members }}</template>
|
||||
<template #caption>{{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i?.policies['userEachUserListsLimit']}` }) }}</template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton>
|
||||
@@ -29,6 +30,10 @@
|
||||
</MkA>
|
||||
<button class="_button" :class="$style.remove" @click="removeUser(user, $event)"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
<MkButton v-if="!fetching && queueUserIds.length !== 0" v-appear="enableInfiniteScroll ? fetchMoreUsers : null" :class="$style.more" :style="{ cursor: 'pointer' }" primary rounded @click="fetchMoreUsers">
|
||||
{{ i18n.ts.loadMore }}
|
||||
</MkButton>
|
||||
<MkLoading v-if="fetching" class="loading"/>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
@@ -49,34 +54,57 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import { userListsCache } from '@/cache';
|
||||
import { UserList, UserLite } from 'misskey-js/built/entities';
|
||||
import { $i } from '@/account';
|
||||
import { defaultStore } from '@/store';
|
||||
const {
|
||||
enableInfiniteScroll,
|
||||
} = defaultStore.reactiveState;
|
||||
|
||||
const props = defineProps<{
|
||||
listId: string;
|
||||
}>();
|
||||
|
||||
let list = $ref(null);
|
||||
let users = $ref([]);
|
||||
const FETCH_USERS_LIMIT = 20;
|
||||
|
||||
let list = $ref<UserList | null>(null);
|
||||
let users = $ref<UserLite[]>([]);
|
||||
let queueUserIds = $ref<string[]>([]);
|
||||
let fetching = $ref(true);
|
||||
const isPublic = ref(false);
|
||||
const name = ref('');
|
||||
|
||||
function fetchList() {
|
||||
fetching = true;
|
||||
os.api('users/lists/show', {
|
||||
listId: props.listId,
|
||||
}).then(_list => {
|
||||
list = _list;
|
||||
name.value = list.name;
|
||||
isPublic.value = list.isPublic;
|
||||
queueUserIds = list.userIds;
|
||||
|
||||
os.api('users/show', {
|
||||
userIds: list.userIds,
|
||||
}).then(_users => {
|
||||
users = _users;
|
||||
});
|
||||
return fetchMoreUsers();
|
||||
});
|
||||
}
|
||||
|
||||
function fetchMoreUsers() {
|
||||
if (!list) return;
|
||||
if (fetching && users.length !== 0) return; // fetchingがtrueならやめるが、usersが空なら続行
|
||||
fetching = true;
|
||||
os.api('users/show', {
|
||||
userIds: queueUserIds.slice(0, FETCH_USERS_LIMIT),
|
||||
}).then(_users => {
|
||||
users = users.concat(_users);
|
||||
queueUserIds = queueUserIds.slice(FETCH_USERS_LIMIT);
|
||||
}).finally(() => {
|
||||
fetching = false;
|
||||
});
|
||||
}
|
||||
|
||||
function addUser() {
|
||||
os.selectUser().then(user => {
|
||||
if (!list) return;
|
||||
os.apiWithDialog('users/lists/push', {
|
||||
listId: list.id,
|
||||
userId: user.id,
|
||||
@@ -92,6 +120,7 @@ async function removeUser(user, ev) {
|
||||
icon: 'ti ti-x',
|
||||
danger: true,
|
||||
action: async () => {
|
||||
if (!list) return;
|
||||
os.api('users/lists/pull', {
|
||||
listId: list.id,
|
||||
userId: user.id,
|
||||
@@ -103,6 +132,7 @@ async function removeUser(user, ev) {
|
||||
}
|
||||
|
||||
async function deleteList() {
|
||||
if (!list) return;
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('removeAreYouSure', { x: list.name }),
|
||||
@@ -117,6 +147,7 @@ async function deleteList() {
|
||||
}
|
||||
|
||||
async function updateSettings() {
|
||||
if (!list) return;
|
||||
await os.apiWithDialog('users/lists/update', {
|
||||
listId: list.id,
|
||||
name: name.value,
|
||||
@@ -166,6 +197,11 @@ definePageMetadata(computed(() => list ? {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.more {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
|
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<div class="_gaps_m">
|
||||
<MkSwitch v-model="useSimpleUiForNonRootPages">{{ i18n.ts._deck.useSimpleUiForNonRootPages }}</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="navWindow">{{ i18n.ts.defaultNavigationBehaviour }}: {{ i18n.ts.openInWindow }}</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="alwaysShowMainColumn">{{ i18n.ts._deck.alwaysShowMainColumn }}</MkSwitch>
|
||||
@@ -21,6 +23,7 @@ import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const navWindow = computed(deckStore.makeGetterSetter('navWindow'));
|
||||
const useSimpleUiForNonRootPages = computed(deckStore.makeGetterSetter('useSimpleUiForNonRootPages'));
|
||||
const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn'));
|
||||
const columnAlign = computed(deckStore.makeGetterSetter('columnAlign'));
|
||||
|
||||
|
@@ -201,6 +201,10 @@ export const routes = [{
|
||||
}, {
|
||||
path: '/about-misskey',
|
||||
component: page(() => import('./pages/about-misskey.vue')),
|
||||
}, {
|
||||
path: '/invite',
|
||||
name: 'invite',
|
||||
component: page(() => import('./pages/invite.vue')),
|
||||
}, {
|
||||
path: '/ads',
|
||||
component: page(() => import('./pages/ads.vue')),
|
||||
@@ -428,6 +432,10 @@ export const routes = [{
|
||||
path: '/server-rules',
|
||||
name: 'server-rules',
|
||||
component: page(() => import('./pages/admin/server-rules.vue')),
|
||||
}, {
|
||||
path: '/invites',
|
||||
name: 'invites',
|
||||
component: page(() => import('./pages/admin/invites.vue')),
|
||||
}, {
|
||||
path: '/',
|
||||
component: page(() => import('./pages/_empty_.vue')),
|
||||
|
@@ -132,9 +132,7 @@ export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notifica
|
||||
}
|
||||
|
||||
export function playFile(file: string, volume: number) {
|
||||
const masterVolume = soundConfigStore.state.sound_masterVolume;
|
||||
if (masterVolume === 0) return;
|
||||
|
||||
const audio = setVolume(getAudio(file), volume);
|
||||
if (audio.volume === 0) return;
|
||||
audio.play();
|
||||
}
|
||||
|
@@ -33,7 +33,12 @@ export function openInstanceMenu(ev: MouseEvent) {
|
||||
text: i18n.ts.ads,
|
||||
icon: 'ti ti-ad',
|
||||
to: '/ads',
|
||||
}, {
|
||||
}, ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) ? {
|
||||
type: 'link',
|
||||
to: '/invite',
|
||||
text: i18n.ts.invite,
|
||||
icon: 'ti ti-user-plus',
|
||||
} : undefined, {
|
||||
type: 'parent',
|
||||
text: i18n.ts.tools,
|
||||
icon: 'ti ti-tool',
|
||||
@@ -52,23 +57,7 @@ export function openInstanceMenu(ev: MouseEvent) {
|
||||
to: '/clicker',
|
||||
text: '🍪👈',
|
||||
icon: 'ti ti-cookie',
|
||||
}, ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) ? {
|
||||
text: i18n.ts.invite,
|
||||
icon: 'ti ti-user-plus',
|
||||
action: () => {
|
||||
os.api('invite').then(x => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: x.code,
|
||||
});
|
||||
}).catch(err => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err,
|
||||
});
|
||||
});
|
||||
},
|
||||
} : undefined, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? {
|
||||
}, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? {
|
||||
type: 'link',
|
||||
to: '/custom-emojis-manager',
|
||||
text: i18n.ts.manageCustomEmojis,
|
||||
|
@@ -52,6 +52,10 @@ export const deckStore = markRaw(new Storage('deck', {
|
||||
where: 'deviceAccount',
|
||||
default: true,
|
||||
},
|
||||
useSimpleUiForNonRootPages: {
|
||||
where: 'deviceAccount',
|
||||
default: true,
|
||||
},
|
||||
}));
|
||||
|
||||
export const loadDeck = async () => {
|
||||
|
Reference in New Issue
Block a user