Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop
This commit is contained in:
		
						commit
						2fe4a51d26
					
				
					 28 changed files with 144 additions and 38 deletions
				
			
		|  | @ -203,6 +203,7 @@ done: "完了" | ||||||
| processing: "処理中" | processing: "処理中" | ||||||
| preview: "プレビュー" | preview: "プレビュー" | ||||||
| default: "デフォルト" | default: "デフォルト" | ||||||
|  | defaultValueIs: "デフォルト: {value}" | ||||||
| noCustomEmojis: "絵文字はありません" | noCustomEmojis: "絵文字はありません" | ||||||
| noJobs: "ジョブはありません" | noJobs: "ジョブはありません" | ||||||
| federating: "連合中" | federating: "連合中" | ||||||
|  | @ -855,6 +856,8 @@ noEmailServerWarning: "メールサーバーの設定がされていません。 | ||||||
| thereIsUnresolvedAbuseReportWarning: "未対応の通報があります。" | thereIsUnresolvedAbuseReportWarning: "未対応の通報があります。" | ||||||
| recommended: "推奨" | recommended: "推奨" | ||||||
| check: "チェック" | check: "チェック" | ||||||
|  | driveCapOverrideLabel: "このユーザーのドライブ容量上限を変更" | ||||||
|  | driveCapOverrideCaption: "0以下を指定すると解除されます。" | ||||||
| requireAdminForView: "閲覧するには管理者アカウントでログインしている必要があります。" | requireAdminForView: "閲覧するには管理者アカウントでログインしている必要があります。" | ||||||
| isSystemAccount: "システムにより自動で作成・管理されているアカウントです。" | isSystemAccount: "システムにより自動で作成・管理されているアカウントです。" | ||||||
| typeToConfirm: "この操作を行うには {x} と入力してください" | typeToConfirm: "この操作を行うには {x} と入力してください" | ||||||
|  |  | ||||||
|  | @ -0,0 +1,13 @@ | ||||||
|  | export class driveCapacityOverrideMb1655813815729 { | ||||||
|  |     name = 'driveCapacityOverrideMb1655813815729' | ||||||
|  | 
 | ||||||
|  |     async up(queryRunner) { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "user" ADD "driveCapacityOverrideMb" integer`); | ||||||
|  |         await queryRunner.query(`COMMENT ON COLUMN "user"."driveCapacityOverrideMb" IS 'Overrides user drive capacity limit'`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async down(queryRunner) { | ||||||
|  |         await queryRunner.query(`COMMENT ON COLUMN "user"."driveCapacityOverrideMb" IS 'Overrides user drive capacity limit'`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "driveCapacityOverrideMb"`); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -218,6 +218,12 @@ export class User { | ||||||
| 	}) | 	}) | ||||||
| 	public token: string | null; | 	public token: string | null; | ||||||
| 
 | 
 | ||||||
|  | 	@Column('integer', { | ||||||
|  | 		nullable: true, | ||||||
|  | 		comment: 'Overrides user drive capacity limit', | ||||||
|  | 	}) | ||||||
|  | 	public driveCapacityOverrideMb: number | null; | ||||||
|  | 
 | ||||||
| 	constructor(data: Partial<User>) { | 	constructor(data: Partial<User>) { | ||||||
| 		if (data == null) return; | 		if (data == null) return; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -315,6 +315,7 @@ export const UserRepository = db.getRepository(User).extend({ | ||||||
| 			} : undefined) : undefined, | 			} : undefined) : undefined, | ||||||
| 			emojis: populateEmojis(user.emojis, user.host), | 			emojis: populateEmojis(user.emojis, user.host), | ||||||
| 			onlineStatus: this.getOnlineStatus(user), | 			onlineStatus: this.getOnlineStatus(user), | ||||||
|  | 			driveCapacityOverrideMb: user.driveCapacityOverrideMb, | ||||||
| 
 | 
 | ||||||
| 			...(opts.detail ? { | 			...(opts.detail ? { | ||||||
| 				url: profile!.url, | 				url: profile!.url, | ||||||
|  |  | ||||||
|  | @ -314,6 +314,7 @@ import * as ep___users_search from './endpoints/users/search.js'; | ||||||
| import * as ep___users_show from './endpoints/users/show.js'; | import * as ep___users_show from './endpoints/users/show.js'; | ||||||
| import * as ep___users_stats from './endpoints/users/stats.js'; | import * as ep___users_stats from './endpoints/users/stats.js'; | ||||||
| import * as ep___fetchRss from './endpoints/fetch-rss.js'; | import * as ep___fetchRss from './endpoints/fetch-rss.js'; | ||||||
|  | import * as ep___admin_driveCapOverride from './endpoints/admin/drive-capacity-override.js'; | ||||||
| 
 | 
 | ||||||
| const eps = [ | const eps = [ | ||||||
| 	['admin/meta', ep___admin_meta], | 	['admin/meta', ep___admin_meta], | ||||||
|  | @ -629,6 +630,7 @@ const eps = [ | ||||||
| 	['users/search', ep___users_search], | 	['users/search', ep___users_search], | ||||||
| 	['users/show', ep___users_show], | 	['users/show', ep___users_show], | ||||||
| 	['users/stats', ep___users_stats], | 	['users/stats', ep___users_stats], | ||||||
|  | 	['admin/drive-capacity-override', ep___admin_driveCapOverride], | ||||||
| 	['fetch-rss', ep___fetchRss], | 	['fetch-rss', ep___fetchRss], | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,47 @@ | ||||||
|  | import define from '../../define.js'; | ||||||
|  | import { Users } from '@/models/index.js'; | ||||||
|  | import { User } from '@/models/entities/user.js'; | ||||||
|  | import { insertModerationLog } from '@/services/insert-moderation-log.js'; | ||||||
|  | export const meta = { | ||||||
|  | 	tags: ['admin'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 	requireModerator: true, | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | export const paramDef = { | ||||||
|  | 	type: 'object', | ||||||
|  | 	properties: { | ||||||
|  | 		userId: { type: 'string', format: 'misskey:id' }, | ||||||
|  | 		overrideMb: { type: 'number', nullable: true }, | ||||||
|  | 	}, | ||||||
|  | 	required: ['userId', 'overrideMb'], | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | // eslint-disable-next-line import/no-default-export
 | ||||||
|  | export default define(meta, paramDef, async (ps, me) => { | ||||||
|  | 	const user = await Users.findOneBy({ id: ps.userId }); | ||||||
|  | 
 | ||||||
|  | 	if (user == null) { | ||||||
|  | 		throw new Error('user not found'); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if (!Users.isLocalUser(user)) { | ||||||
|  | 		throw new Error('user is not local user'); | ||||||
|  | 	}  | ||||||
|  | 
 | ||||||
|  | 	/*if (user.isAdmin) { | ||||||
|  | 		throw new Error('cannot suspend admin'); | ||||||
|  | 	} | ||||||
|  | 	if (user.isModerator) { | ||||||
|  | 		throw new Error('cannot suspend moderator'); | ||||||
|  | 	}*/ | ||||||
|  | 
 | ||||||
|  | 	await Users.update(user.id, { | ||||||
|  | 		driveCapacityOverrideMb: ps.overrideMb, | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	insertModerationLog(me, 'change-drive-capacity-override', { | ||||||
|  | 		targetId: user.id, | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
|  | @ -39,7 +39,7 @@ export default define(meta, paramDef, async (ps, user) => { | ||||||
| 	const usage = await DriveFiles.calcDriveUsageOf(user.id); | 	const usage = await DriveFiles.calcDriveUsageOf(user.id); | ||||||
| 
 | 
 | ||||||
| 	return { | 	return { | ||||||
| 		capacity: 1024 * 1024 * instance.localDriveCapacityMb, | 		capacity: 1024 * 1024 * (user.driveCapacityOverrideMb || instance.localDriveCapacityMb), | ||||||
| 		usage: usage, | 		usage: usage, | ||||||
| 	}; | 	}; | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -307,7 +307,7 @@ async function deleteOldFile(user: IRemoteUser) { | ||||||
| 
 | 
 | ||||||
| type AddFileArgs = { | type AddFileArgs = { | ||||||
| 	/** User who wish to add file */ | 	/** User who wish to add file */ | ||||||
| 	user: { id: User['id']; host: User['host'] } | null; | 	user: { id: User['id']; host: User['host']; driveCapacityOverrideMb: User['driveCapacityOverrideMb'] } | null; | ||||||
| 	/** File path */ | 	/** File path */ | ||||||
| 	path: string; | 	path: string; | ||||||
| 	/** Name */ | 	/** Name */ | ||||||
|  | @ -371,9 +371,16 @@ export async function addFile({ | ||||||
| 	//#region Check drive usage
 | 	//#region Check drive usage
 | ||||||
| 	if (user && !isLink) { | 	if (user && !isLink) { | ||||||
| 		const usage = await DriveFiles.calcDriveUsageOf(user); | 		const usage = await DriveFiles.calcDriveUsageOf(user); | ||||||
|  | 		const u = await Users.findOneBy({ id: user.id }); | ||||||
| 
 | 
 | ||||||
| 		const instance = await fetchMeta(); | 		const instance = await fetchMeta(); | ||||||
| 		const driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); | 		let driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); | ||||||
|  | 
 | ||||||
|  | 		if (Users.isLocalUser(user) && u?.driveCapacityOverrideMb != null) { | ||||||
|  | 			driveCapacity = 1024 * 1024 * u.driveCapacityOverrideMb; | ||||||
|  | 			logger.debug('drive capacity override applied'); | ||||||
|  | 			logger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`); | ||||||
|  | 		} | ||||||
| 
 | 
 | ||||||
| 		logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); | 		logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
| import 'prismjs'; | import { Prism } from 'prismjs'; | ||||||
| import 'prismjs/themes/prism-okaidia.css'; | import 'prismjs/themes/prism-okaidia.css'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
|  |  | ||||||
|  | @ -98,7 +98,7 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| 	created() { | 	created() { | ||||||
| 		for (const item in this.form) { | 		for (const item in this.form) { | ||||||
| 			this.values[item] = this.form[item].hasOwnProperty('default') ? this.form[item].default : null; | 			this.values[item] = this.form[item].default ?? null; | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -13,9 +13,6 @@ const props = defineProps<{ | ||||||
| 	router?: Router; | 	router?: Router; | ||||||
| }>(); | }>(); | ||||||
| 
 | 
 | ||||||
| const emit = defineEmits<{ |  | ||||||
| }>(); |  | ||||||
| 
 |  | ||||||
| const router = props.router ?? inject('router'); | const router = props.router ?? inject('router'); | ||||||
| 
 | 
 | ||||||
| if (router == null) { | if (router == null) { | ||||||
|  |  | ||||||
|  | @ -13,8 +13,6 @@ | ||||||
| import { onMounted, ref, watch, PropType, onBeforeUnmount } from 'vue'; | import { onMounted, ref, watch, PropType, onBeforeUnmount } from 'vue'; | ||||||
| import tinycolor from 'tinycolor2'; | import tinycolor from 'tinycolor2'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{}>(); |  | ||||||
| 
 |  | ||||||
| const loaded = !!window.TagCanvas; | const loaded = !!window.TagCanvas; | ||||||
| const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz'; | const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz'; | ||||||
| const computedStyle = getComputedStyle(document.documentElement); | const computedStyle = getComputedStyle(document.documentElement); | ||||||
|  |  | ||||||
|  | @ -75,7 +75,6 @@ const hasTabs = computed(() => { | ||||||
| 
 | 
 | ||||||
| const showTabsPopup = (ev: MouseEvent) => { | const showTabsPopup = (ev: MouseEvent) => { | ||||||
| 	if (!hasTabs.value) return; | 	if (!hasTabs.value) return; | ||||||
| 	if (!narrow.value) return; |  | ||||||
| 	ev.preventDefault(); | 	ev.preventDefault(); | ||||||
| 	ev.stopPropagation(); | 	ev.stopPropagation(); | ||||||
| 	const menu = props.tabs.map(tab => ({ | 	const menu = props.tabs.map(tab => ({ | ||||||
|  |  | ||||||
|  | @ -61,27 +61,22 @@ let hcaptchaSecretKey: string | null = $ref(null); | ||||||
| let recaptchaSiteKey: string | null = $ref(null); | let recaptchaSiteKey: string | null = $ref(null); | ||||||
| let recaptchaSecretKey: string | null = $ref(null); | let recaptchaSecretKey: string | null = $ref(null); | ||||||
| 
 | 
 | ||||||
| const enableHcaptcha = $computed(() => provider === 'hcaptcha'); |  | ||||||
| const enableRecaptcha = $computed(() => provider === 'recaptcha'); |  | ||||||
| 
 |  | ||||||
| async function init() { | async function init() { | ||||||
| 	const meta = await os.api('admin/meta'); | 	const meta = await os.api('admin/meta'); | ||||||
| 	enableHcaptcha = meta.enableHcaptcha; |  | ||||||
| 	hcaptchaSiteKey = meta.hcaptchaSiteKey; | 	hcaptchaSiteKey = meta.hcaptchaSiteKey; | ||||||
| 	hcaptchaSecretKey = meta.hcaptchaSecretKey; | 	hcaptchaSecretKey = meta.hcaptchaSecretKey; | ||||||
| 	enableRecaptcha = meta.enableRecaptcha; |  | ||||||
| 	recaptchaSiteKey = meta.recaptchaSiteKey; | 	recaptchaSiteKey = meta.recaptchaSiteKey; | ||||||
| 	recaptchaSecretKey = meta.recaptchaSecretKey; | 	recaptchaSecretKey = meta.recaptchaSecretKey; | ||||||
| 
 | 
 | ||||||
| 	provider = enableHcaptcha ? 'hcaptcha' : enableRecaptcha ? 'recaptcha' : null; | 	provider = meta.enableHcaptcha ? 'hcaptcha' : meta.enableRecaptcha ? 'recaptcha' : null; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function save() { | function save() { | ||||||
| 	os.apiWithDialog('admin/update-meta', { | 	os.apiWithDialog('admin/update-meta', { | ||||||
| 		enableHcaptcha, | 		enableHcaptcha: provider === 'hcaptcha', | ||||||
| 		hcaptchaSiteKey, | 		hcaptchaSiteKey, | ||||||
| 		hcaptchaSecretKey, | 		hcaptchaSecretKey, | ||||||
| 		enableRecaptcha, | 		enableRecaptcha: provider === 'recaptcha', | ||||||
| 		recaptchaSiteKey, | 		recaptchaSiteKey, | ||||||
| 		recaptchaSecretKey, | 		recaptchaSecretKey, | ||||||
| 	}).then(() => { | 	}).then(() => { | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ const props = defineProps<{ | ||||||
| 	user: misskey.entities.User; | 	user: misskey.entities.User; | ||||||
| }>(); | }>(); | ||||||
| 
 | 
 | ||||||
| const chart = $ref(null); | let chart = $ref(null); | ||||||
| 
 | 
 | ||||||
| os.apiGet('charts/user/notes', { userId: props.user.id, limit: 16, span: 'day' }).then(res => { | os.apiGet('charts/user/notes', { userId: props.user.id, limit: 16, span: 'day' }).then(res => { | ||||||
| 	chart = res; | 	chart = res; | ||||||
|  |  | ||||||
|  | @ -74,8 +74,8 @@ const props = defineProps<{ | ||||||
| 	postId: string; | 	postId: string; | ||||||
| }>(); | }>(); | ||||||
| 
 | 
 | ||||||
| const post = $ref(null); | let post = $ref(null); | ||||||
| const error = $ref(null); | let error = $ref(null); | ||||||
| const otherPostsPagination = { | const otherPostsPagination = { | ||||||
| 	endpoint: 'users/gallery/posts' as const, | 	endpoint: 'users/gallery/posts' as const, | ||||||
| 	limit: 6, | 	limit: 6, | ||||||
|  |  | ||||||
|  | @ -46,6 +46,7 @@ | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { watch } from 'vue'; | import { watch } from 'vue'; | ||||||
|  | import * as Acct from 'misskey-js/built/acct'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import MkInput from '@/components/form/input.vue'; | import MkInput from '@/components/form/input.vue'; | ||||||
| import MkTextarea from '@/components/form/textarea.vue'; | import MkTextarea from '@/components/form/textarea.vue'; | ||||||
|  |  | ||||||
|  | @ -41,6 +41,7 @@ import MkButton from '@/components/ui/button.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { mainRouter } from '@/router'; | import { mainRouter } from '@/router'; | ||||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
|  | import { i18n } from '@/i18n'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	listId: string; | 	listId: string; | ||||||
|  |  | ||||||
|  | @ -78,6 +78,7 @@ import FormButton from '@/components/ui/button.vue'; | ||||||
| import FormTextarea from '@/components/form/textarea.vue'; | import FormTextarea from '@/components/form/textarea.vue'; | ||||||
| import FormFolder from '@/components/form/folder.vue'; | import FormFolder from '@/components/form/folder.vue'; | ||||||
| 
 | 
 | ||||||
|  | import { $i } from '@/account'; | ||||||
| import { Theme, applyTheme } from '@/scripts/theme'; | import { Theme, applyTheme } from '@/scripts/theme'; | ||||||
| import lightTheme from '@/themes/_light.json5'; | import lightTheme from '@/themes/_light.json5'; | ||||||
| import darkTheme from '@/themes/_dark.json5'; | import darkTheme from '@/themes/_dark.json5'; | ||||||
|  | @ -118,7 +119,7 @@ const fgColors = [ | ||||||
| 	{ color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' }, | 	{ color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' }, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| const theme = $ref<Partial<Theme>>({ | let theme = $ref<Partial<Theme>>({ | ||||||
| 	base: 'light', | 	base: 'light', | ||||||
| 	props: lightTheme.props, | 	props: lightTheme.props, | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -85,6 +85,17 @@ | ||||||
| 				</FormSection> | 				</FormSection> | ||||||
| 			</div> | 			</div> | ||||||
| 			<div v-else-if="tab === 'moderation'" class="_formRoot"> | 			<div v-else-if="tab === 'moderation'" class="_formRoot"> | ||||||
|  | 				<FormSection> | ||||||
|  | 					<template #label>Drive Capacity Override</template> | ||||||
|  | 
 | ||||||
|  | 					<FormInput v-if="user.host == null" v-model="driveCapacityOverrideMb" inline :manual-save="true" type="number" :placeholder="i18n.t('defaultValueIs', { value: instance.driveCapacityPerLocalUserMb })" @update:model-value="applyDriveCapacityOverride"> | ||||||
|  | 						<template #label>{{ i18n.ts.driveCapOverrideLabel }}</template> | ||||||
|  | 						<template #suffix>MB</template> | ||||||
|  | 						<template #caption> | ||||||
|  | 							{{ i18n.ts.driveCapOverrideCaption }} | ||||||
|  | 						</template> | ||||||
|  | 					</FormInput> | ||||||
|  | 				</FormSection> | ||||||
| 				<FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" v-model="moderator" class="_formBlock" @update:modelValue="toggleModerator">{{ $ts.moderator }}</FormSwitch> | 				<FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" v-model="moderator" class="_formBlock" @update:modelValue="toggleModerator">{{ $ts.moderator }}</FormSwitch> | ||||||
| 				<FormSwitch v-model="silenced" class="_formBlock" @update:modelValue="toggleSilence">{{ $ts.silence }}</FormSwitch> | 				<FormSwitch v-model="silenced" class="_formBlock" @update:modelValue="toggleSilence">{{ $ts.silence }}</FormSwitch> | ||||||
| 				<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.suspend }}</FormSwitch> | 				<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.suspend }}</FormSwitch> | ||||||
|  | @ -141,7 +152,7 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { computed, defineAsyncComponent, defineComponent, watch } from 'vue'; | import { computed, watch } from 'vue'; | ||||||
| import * as misskey from 'misskey-js'; | import * as misskey from 'misskey-js'; | ||||||
| import MkChart from '@/components/chart.vue'; | import MkChart from '@/components/chart.vue'; | ||||||
| import MkObjectView from '@/components/object-view.vue'; | import MkObjectView from '@/components/object-view.vue'; | ||||||
|  | @ -150,6 +161,8 @@ import FormSwitch from '@/components/form/switch.vue'; | ||||||
| import FormLink from '@/components/form/link.vue'; | import FormLink from '@/components/form/link.vue'; | ||||||
| import FormSection from '@/components/form/section.vue'; | import FormSection from '@/components/form/section.vue'; | ||||||
| import FormButton from '@/components/ui/button.vue'; | import FormButton from '@/components/ui/button.vue'; | ||||||
|  | import FormInput from '@/components/form/input.vue'; | ||||||
|  | import FormSplit from '@/components/form/split.vue'; | ||||||
| import FormFolder from '@/components/form/folder.vue'; | import FormFolder from '@/components/form/folder.vue'; | ||||||
| import MkKeyValue from '@/components/key-value.vue'; | import MkKeyValue from '@/components/key-value.vue'; | ||||||
| import MkSelect from '@/components/form/select.vue'; | import MkSelect from '@/components/form/select.vue'; | ||||||
|  | @ -164,6 +177,7 @@ import { userPage, acct } from '@/filters/user'; | ||||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| import { iAmAdmin, iAmModerator } from '@/account'; | import { iAmAdmin, iAmModerator } from '@/account'; | ||||||
|  | import { instance } from '@/instance'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	userId: string; | 	userId: string; | ||||||
|  | @ -172,13 +186,14 @@ const props = defineProps<{ | ||||||
| let tab = $ref('overview'); | let tab = $ref('overview'); | ||||||
| let chartSrc = $ref('per-user-notes'); | let chartSrc = $ref('per-user-notes'); | ||||||
| let user = $ref<null | misskey.entities.UserDetailed>(); | let user = $ref<null | misskey.entities.UserDetailed>(); | ||||||
| let init = $ref(); | let init = $ref<ReturnType<typeof createFetcher>>(); | ||||||
| let info = $ref(); | let info = $ref(); | ||||||
| let ips = $ref(null); | let ips = $ref(null); | ||||||
| let ap = $ref(null); | let ap = $ref(null); | ||||||
| let moderator = $ref(false); | let moderator = $ref(false); | ||||||
| let silenced = $ref(false); | let silenced = $ref(false); | ||||||
| let suspended = $ref(false); | let suspended = $ref(false); | ||||||
|  | let driveCapacityOverrideMb: number | null = $ref(0); | ||||||
| let moderationNote = $ref(''); | let moderationNote = $ref(''); | ||||||
| const filesPagination = { | const filesPagination = { | ||||||
| 	endpoint: 'admin/drive/files' as const, | 	endpoint: 'admin/drive/files' as const, | ||||||
|  | @ -203,6 +218,7 @@ function createFetcher() { | ||||||
| 			moderator = info.isModerator; | 			moderator = info.isModerator; | ||||||
| 			silenced = info.isSilenced; | 			silenced = info.isSilenced; | ||||||
| 			suspended = info.isSuspended; | 			suspended = info.isSuspended; | ||||||
|  | 			driveCapacityOverrideMb = user.driveCapacityOverrideMb; | ||||||
| 			moderationNote = info.moderationNote; | 			moderationNote = info.moderationNote; | ||||||
| 
 | 
 | ||||||
| 			watch($$(moderationNote), async () => { | 			watch($$(moderationNote), async () => { | ||||||
|  | @ -289,6 +305,22 @@ async function deleteAllFiles() { | ||||||
| 	await refreshUser(); | 	await refreshUser(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | async function applyDriveCapacityOverride() { | ||||||
|  | 	let driveCapOrMb = driveCapacityOverrideMb; | ||||||
|  | 	if (driveCapacityOverrideMb && driveCapacityOverrideMb < 0) { | ||||||
|  | 		driveCapOrMb = null; | ||||||
|  | 	} | ||||||
|  | 	try { | ||||||
|  | 		await os.apiWithDialog('admin/drive-capacity-override', { userId: user.id, overrideMb: driveCapOrMb }); | ||||||
|  | 		await refreshUser(); | ||||||
|  | 	} catch (e) { | ||||||
|  | 		os.alert({ | ||||||
|  | 			type: 'error', | ||||||
|  | 			text: e.toString(), | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| async function deleteAccount() { | async function deleteAccount() { | ||||||
| 	const confirm = await os.confirm({ | 	const confirm = await os.confirm({ | ||||||
| 		type: 'warning', | 		type: 'warning', | ||||||
|  | @ -319,7 +351,7 @@ watch(() => props.userId, () => { | ||||||
| 	immediate: true, | 	immediate: true, | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| watch(() => user, () => { | watch($$(user), () => { | ||||||
| 	os.api('ap/get', { | 	os.api('ap/get', { | ||||||
| 		uri: user.uri ?? `${url}/users/${user.id}`, | 		uri: user.uri ?? `${url}/users/${user.id}`, | ||||||
| 	}).then(res => { | 	}).then(res => { | ||||||
|  |  | ||||||
|  | @ -38,7 +38,7 @@ export function install(plugin) { | ||||||
| function createPluginEnv(opts) { | function createPluginEnv(opts) { | ||||||
| 	const config = new Map(); | 	const config = new Map(); | ||||||
| 	for (const [k, v] of Object.entries(opts.plugin.config || {})) { | 	for (const [k, v] of Object.entries(opts.plugin.config || {})) { | ||||||
| 		config.set(k, jsToVal(opts.plugin.configData.hasOwnProperty(k) ? opts.plugin.configData[k] : v.default)); | 		config.set(k, jsToVal(typeof opts.plugin.configData[k] !== 'undefined' ? opts.plugin.configData[k] : v.default)); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return { | 	return { | ||||||
|  |  | ||||||
|  | @ -98,7 +98,7 @@ export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] { | ||||||
| export function groupByX<T>(collections: T[], keySelector: (x: T) => string) { | export function groupByX<T>(collections: T[], keySelector: (x: T) => string) { | ||||||
| 	return collections.reduce((obj: Record<string, T[]>, item: T) => { | 	return collections.reduce((obj: Record<string, T[]>, item: T) => { | ||||||
| 		const key = keySelector(item); | 		const key = keySelector(item); | ||||||
| 		if (!obj.hasOwnProperty(key)) { | 		if (typeof obj[key] === 'undefined') { | ||||||
| 			obj[key] = []; | 			obj[key] = []; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ export class Autocomplete { | ||||||
| 		x: Ref<number>; | 		x: Ref<number>; | ||||||
| 		y: Ref<number>; | 		y: Ref<number>; | ||||||
| 		q: Ref<string | null>; | 		q: Ref<string | null>; | ||||||
| 		close: Function; | 		close: () => void; | ||||||
| 	} | null; | 	} | null; | ||||||
| 	private textarea: HTMLInputElement | HTMLTextAreaElement; | 	private textarea: HTMLInputElement | HTMLTextAreaElement; | ||||||
| 	private currentType: string; | 	private currentType: string; | ||||||
|  |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| import keyCode from './keycode'; | import keyCode from './keycode'; | ||||||
| 
 | 
 | ||||||
| type Keymap = Record<string, Function>; | type Callback = (ev: KeyboardEvent) => void; | ||||||
|  | 
 | ||||||
|  | type Keymap = Record<string, Callback>; | ||||||
| 
 | 
 | ||||||
| type Pattern = { | type Pattern = { | ||||||
| 	which: string[]; | 	which: string[]; | ||||||
|  | @ -11,14 +13,14 @@ type Pattern = { | ||||||
| 
 | 
 | ||||||
| type Action = { | type Action = { | ||||||
| 	patterns: Pattern[]; | 	patterns: Pattern[]; | ||||||
| 	callback: Function; | 	callback: Callback; | ||||||
| 	allowRepeat: boolean; | 	allowRepeat: boolean; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => { | const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => { | ||||||
| 	const result = { | 	const result = { | ||||||
| 		patterns: [], | 		patterns: [], | ||||||
| 		callback: callback, | 		callback, | ||||||
| 		allowRepeat: true | 		allowRepeat: true | ||||||
| 	} as Action; | 	} as Action; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| export function query(obj: {}): string { | export function query(obj: Record<string, any>): string { | ||||||
| 	const params = Object.entries(obj) | 	const params = Object.entries(obj) | ||||||
| 		.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) | 		.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) | ||||||
| 		.reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>); | 		.reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>); | ||||||
|  |  | ||||||
|  | @ -60,8 +60,8 @@ const DESKTOP_THRESHOLD = 1100; | ||||||
| let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD); | let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD); | ||||||
| 
 | 
 | ||||||
| let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); | let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); | ||||||
| const widgetsShowing = $ref(false); | let widgetsShowing = $ref(false); | ||||||
| const fullView = $ref(false); | let fullView = $ref(false); | ||||||
| let globalHeaderHeight = $ref(0); | let globalHeaderHeight = $ref(0); | ||||||
| const wallpaper = localStorage.getItem('wallpaper') != null; | const wallpaper = localStorage.getItem('wallpaper') != null; | ||||||
| const showMenuOnTop = $computed(() => defaultStore.state.menuDisplay === 'top'); | const showMenuOnTop = $computed(() => defaultStore.state.menuDisplay === 'top'); | ||||||
|  |  | ||||||
|  | @ -53,7 +53,7 @@ function onContextmenu(ev: MouseEvent) { | ||||||
| 	if (isLink(ev.target as HTMLElement)) return; | 	if (isLink(ev.target as HTMLElement)) return; | ||||||
| 	if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes((ev.target as HTMLElement).tagName) || (ev.target as HTMLElement).attributes['contenteditable']) return; | 	if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes((ev.target as HTMLElement).tagName) || (ev.target as HTMLElement).attributes['contenteditable']) return; | ||||||
| 	if (window.getSelection()?.toString() !== '') return; | 	if (window.getSelection()?.toString() !== '') return; | ||||||
| 	const path = router.currentRoute.value.path; | 	const path = mainRouter.currentRoute.value.path; | ||||||
| 	os.contextMenu([{ | 	os.contextMenu([{ | ||||||
| 		type: 'label', | 		type: 'label', | ||||||
| 		text: path, | 		text: path, | ||||||
|  |  | ||||||
|  | @ -36,8 +36,9 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default: | ||||||
| 
 | 
 | ||||||
| 	const mergeProps = () => { | 	const mergeProps = () => { | ||||||
| 		for (const prop of Object.keys(propsDef)) { | 		for (const prop of Object.keys(propsDef)) { | ||||||
| 			if (widgetProps.hasOwnProperty(prop)) continue; | 			if (typeof widgetProps[prop] === 'undefined') { | ||||||
| 			widgetProps[prop] = propsDef[prop].default; | 				widgetProps[prop] = propsDef[prop].default; | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	}; | 	}; | ||||||
| 	watch(widgetProps, () => { | 	watch(widgetProps, () => { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue