Merge branch 'develop' into pag-back

This commit is contained in:
tamaina
2023-07-17 07:29:32 +00:00
92 changed files with 3176 additions and 1027 deletions

View File

@@ -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,
}
}

View File

@@ -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();

View File

@@ -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"
}
}

View File

@@ -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')) :

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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>;

View 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>

View File

@@ -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,
})];
}

View File

@@ -57,6 +57,9 @@ export const ROLE_POLICIES = [
'ltlAvailable',
'canPublicNote',
'canInvite',
'inviteLimit',
'inviteLimitCycle',
'inviteExpirationTime',
'canManageCustomEmojis',
'canSearchNotes',
'canHideAds',

View File

@@ -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'));

View File

@@ -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({

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,

View 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>

View File

@@ -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>

View File

@@ -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));

View File

@@ -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'));

View File

@@ -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')),

View File

@@ -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();
}

View File

@@ -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,

View File

@@ -52,6 +52,10 @@ export const deckStore = markRaw(new Storage('deck', {
where: 'deviceAccount',
default: true,
},
useSimpleUiForNonRootPages: {
where: 'deviceAccount',
default: true,
},
}));
export const loadDeck = async () => {