Merge branch 'develop' into pag-back
This commit is contained in:
		| @@ -4,8 +4,9 @@ | ||||
| 	"scripts": { | ||||
| 		"watch": "vite", | ||||
| 		"build": "vite build", | ||||
| 		"storybook-dev": "chokidar 'src/**/*.{mdx,ts,vue}' -d 1000 -t 1000 --initial -i '**/*.stories.ts' -c 'pkill -f node_modules/storybook/index.js; node_modules/.bin/tsc -p .storybook && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js && node_modules/.bin/storybook dev -p 6006 --ci'", | ||||
| 		"build-storybook": "tsc -p .storybook && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js && storybook build", | ||||
| 		"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"", | ||||
| 		"build-storybook-pre": "tsc -p .storybook && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js", | ||||
| 		"build-storybook": "pnpm build-storybook-pre && storybook build", | ||||
| 		"chromatic": "chromatic", | ||||
| 		"test": "vitest --run", | ||||
| 		"test-and-coverage": "vitest --run --coverage", | ||||
| @@ -19,7 +20,7 @@ | ||||
| 		"@rollup/plugin-json": "6.0.0", | ||||
| 		"@rollup/plugin-replace": "5.0.2", | ||||
| 		"@rollup/pluginutils": "5.0.2", | ||||
| 		"@syuilo/aiscript": "0.14.0", | ||||
| 		"@syuilo/aiscript": "0.15.0", | ||||
| 		"@tabler/icons-webfont": "2.25.0", | ||||
| 		"@vitejs/plugin-vue": "4.2.3", | ||||
| 		"@vue-macros/reactivity-transform": "0.3.15", | ||||
| @@ -77,24 +78,24 @@ | ||||
| 		"vuedraggable": "next" | ||||
| 	}, | ||||
| 	"devDependencies": { | ||||
| 		"@storybook/addon-actions": "7.1.0", | ||||
| 		"@storybook/addon-essentials": "7.1.0", | ||||
| 		"@storybook/addon-interactions": "7.1.0", | ||||
| 		"@storybook/addon-links": "7.1.0", | ||||
| 		"@storybook/addon-storysource": "7.1.0", | ||||
| 		"@storybook/addons": "7.1.0", | ||||
| 		"@storybook/blocks": "7.1.0", | ||||
| 		"@storybook/core-events": "7.1.0", | ||||
| 		"@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.1.0", | ||||
| 		"@storybook/preview-api": "7.1.0", | ||||
| 		"@storybook/react": "7.1.0", | ||||
| 		"@storybook/react-vite": "7.1.0", | ||||
| 		"@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.1.0", | ||||
| 		"@storybook/types": "7.1.0", | ||||
| 		"@storybook/vue3": "7.1.0", | ||||
| 		"@storybook/vue3-vite": "7.1.0", | ||||
| 		"@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", | ||||
| @@ -117,7 +118,6 @@ | ||||
| 		"@vitest/coverage-v8": "0.33.0", | ||||
| 		"@vue/runtime-core": "3.3.4", | ||||
| 		"acorn": "8.10.0", | ||||
| 		"chokidar-cli": "3.0.0", | ||||
| 		"cross-env": "7.0.3", | ||||
| 		"cypress": "12.17.1", | ||||
| 		"eslint": "8.45.0", | ||||
| @@ -128,11 +128,12 @@ | ||||
| 		"micromatch": "4.0.5", | ||||
| 		"msw": "1.2.2", | ||||
| 		"msw-storybook-addon": "1.8.0", | ||||
| 		"nodemon": "3.0.1", | ||||
| 		"prettier": "3.0.0", | ||||
| 		"react": "18.2.0", | ||||
| 		"react-dom": "18.2.0", | ||||
| 		"start-server-and-test": "2.0.0", | ||||
| 		"storybook": "7.1.0", | ||||
| 		"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", | ||||
|   | ||||
| @@ -19,7 +19,7 @@ | ||||
| 		</div> | ||||
| 		<div v-if="file.isSensitive" :class="[$style.label, $style.red]"> | ||||
| 			<img :class="$style.labelImg" src="/client-assets/label-red.svg"/> | ||||
| 			<p :class="$style.labelText">NSFW</p> | ||||
| 			<p :class="$style.labelText">{{ i18n.ts.sensitive }}</p> | ||||
| 		</div> | ||||
|  | ||||
| 		<MkDriveFileThumbnail :class="$style.thumbnail" :file="file" fit="contain"/> | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
| 	<template v-if="hide"> | ||||
| 		<div :class="$style.hiddenText"> | ||||
| 			<div :class="$style.hiddenTextWrapper"> | ||||
| 				<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b> | ||||
| 				<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b> | ||||
| 				<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.enableDataSaverMode && image.size ? bytes(image.size) : i18n.ts.image }}</b> | ||||
| 				<span style="display: block;">{{ i18n.ts.clickToShow }}</span> | ||||
| 			</div> | ||||
| @@ -30,7 +30,7 @@ | ||||
| 		<div :class="$style.indicators"> | ||||
| 			<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div> | ||||
| 			<div v-if="image.comment" :class="$style.indicator">ALT</div> | ||||
| 			<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);">NSFW</div> | ||||
| 			<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div> | ||||
| 		</div> | ||||
| 		<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button> | ||||
| 		<i class="ti ti-eye-off" :class="$style.hide" @click.stop="hide = true"></i> | ||||
|   | ||||
| @@ -165,6 +165,7 @@ import { getNoteSummary } from '@/scripts/get-note-summary'; | ||||
| import { MenuItem } from '@/types/menu'; | ||||
| import MkRippleEffect from '@/components/MkRippleEffect.vue'; | ||||
| import { showMovedDialog } from '@/scripts/show-moved-dialog'; | ||||
| import { shouldCollapsed } from '@/scripts/collapsed'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	note: misskey.entities.Note; | ||||
| @@ -204,17 +205,7 @@ let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note | ||||
| const isMyRenote = $i && ($i.id === note.userId); | ||||
| const showContent = ref(false); | ||||
| const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null; | ||||
| const isLong = (appearNote.cw == null && appearNote.text != null && ( | ||||
| 	(appearNote.text.includes('$[x2')) || | ||||
| 	(appearNote.text.includes('$[x3')) || | ||||
| 	(appearNote.text.includes('$[x4')) || | ||||
| 	(appearNote.text.includes('$[scale')) || | ||||
| 	(appearNote.text.includes('$[position')) || | ||||
| 	(appearNote.text.split('\n').length > 9) || | ||||
| 	(appearNote.text.length > 500) || | ||||
| 	(appearNote.files.length >= 5) || | ||||
| 	(urls && urls.length >= 4) | ||||
| )); | ||||
| const isLong = shouldCollapsed(appearNote); | ||||
| const collapsed = ref(appearNote.cw == null && isLong); | ||||
| const isDeleted = ref(false); | ||||
| const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords)); | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| 			<div :class="$style.file" @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)"> | ||||
| 				<MkDriveFileThumbnail :data-id="element.id" :class="$style.thumbnail" :file="element" fit="cover"/> | ||||
| 				<div v-if="element.isSensitive" :class="$style.sensitive"> | ||||
| 					<i class="ti ti-alert-triangle" style="margin: auto;"></i> | ||||
| 					<i class="ti ti-eye-exclamation" style="margin: auto;"></i> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</template> | ||||
|   | ||||
| @@ -9,7 +9,10 @@ | ||||
| 				<MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo> | ||||
| 			</div> | ||||
|  | ||||
| 			<div style="text-align: center;">{{ i18n.ts.pleaseConfirmBelowBeforeSignup }}</div> | ||||
| 			<div style="text-align: center;"> | ||||
| 				<div>{{ i18n.ts.pleaseConfirmBelowBeforeSignup }}</div> | ||||
| 				<div style="font-weight: bold; margin-top: 0.5em;">{{ i18n.ts.beSureToReadThisAsItIsImportant }}</div> | ||||
| 			</div> | ||||
|  | ||||
| 			<MkFolder v-if="availableServerRules" :defaultOpen="true"> | ||||
| 				<template #label>{{ i18n.ts.serverRules }}</template> | ||||
| @@ -19,7 +22,7 @@ | ||||
| 					<li v-for="item in instance.serverRules" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li> | ||||
| 				</ol> | ||||
|  | ||||
| 				<MkSwitch v-model="agreeServerRules" style="margin-top: 16px;">{{ i18n.ts.agree }}</MkSwitch> | ||||
| 				<MkSwitch :modelValue="agreeServerRules" style="margin-top: 16px;" @update:modelValue="updateAgreeServerRules">{{ i18n.ts.agree }}</MkSwitch> | ||||
| 			</MkFolder> | ||||
|  | ||||
| 			<MkFolder v-if="availableTos" :defaultOpen="true"> | ||||
| @@ -28,7 +31,7 @@ | ||||
|  | ||||
| 				<a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a> | ||||
|  | ||||
| 				<MkSwitch v-model="agreeTos" style="margin-top: 16px;">{{ i18n.ts.agree }}</MkSwitch> | ||||
| 				<MkSwitch :modelValue="agreeTos" style="margin-top: 16px;" @update:modelValue="updateAgreeTos">{{ i18n.ts.agree }}</MkSwitch> | ||||
| 			</MkFolder> | ||||
|  | ||||
| 			<MkFolder :defaultOpen="true"> | ||||
| @@ -37,7 +40,7 @@ | ||||
|  | ||||
| 				<a href="https://misskey-hub.net/docs/notes.html" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a> | ||||
|  | ||||
| 				<MkSwitch v-model="agreeNote" style="margin-top: 16px;" data-cy-signup-rules-notes-agree>{{ i18n.ts.agree }}</MkSwitch> | ||||
| 				<MkSwitch :modelValue="agreeNote" style="margin-top: 16px;" data-cy-signup-rules-notes-agree @update:modelValue="updateAgreeNote">{{ i18n.ts.agree }}</MkSwitch> | ||||
| 			</MkFolder> | ||||
|  | ||||
| 			<div v-if="!agreed" style="text-align: center;">{{ i18n.ts.pleaseAgreeAllToContinue }}</div> | ||||
| @@ -52,13 +55,14 @@ | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { computed, ref } from 'vue'; | ||||
| import { computed, onMounted, ref, watch } from 'vue'; | ||||
| import { instance } from '@/instance'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| import MkFolder from '@/components/MkFolder.vue'; | ||||
| import MkSwitch from '@/components/MkSwitch.vue'; | ||||
| import MkInfo from '@/components/MkInfo.vue'; | ||||
| import * as os from '@/os'; | ||||
|  | ||||
| const availableServerRules = instance.serverRules.length > 0; | ||||
| const availableTos = instance.tosUrl != null; | ||||
| @@ -75,6 +79,48 @@ const emit = defineEmits<{ | ||||
| 	(ev: 'cancel'): void; | ||||
| 	(ev: 'done'): void; | ||||
| }>(); | ||||
|  | ||||
| async function updateAgreeServerRules(v: boolean) { | ||||
| 	if (v) { | ||||
| 		const confirm = await os.confirm({ | ||||
| 			type: 'question', | ||||
| 			title: i18n.ts.doYouAgree, | ||||
| 			text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.serverRules }), | ||||
| 		}); | ||||
| 		if (confirm.canceled) return; | ||||
| 		agreeServerRules.value = true; | ||||
| 	} else { | ||||
| 		agreeServerRules.value = false; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| async function updateAgreeTos(v: boolean) { | ||||
| 	if (v) { | ||||
| 		const confirm = await os.confirm({ | ||||
| 			type: 'question', | ||||
| 			title: i18n.ts.doYouAgree, | ||||
| 			text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.termsOfService }), | ||||
| 		}); | ||||
| 		if (confirm.canceled) return; | ||||
| 		agreeTos.value = true; | ||||
| 	} else { | ||||
| 		agreeTos.value = false; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| async function updateAgreeNote(v: boolean) { | ||||
| 	if (v) { | ||||
| 		const confirm = await os.confirm({ | ||||
| 			type: 'question', | ||||
| 			title: i18n.ts.doYouAgree, | ||||
| 			text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.basicNotesBeforeCreateAccount }), | ||||
| 		}); | ||||
| 		if (confirm.canceled) return; | ||||
| 		agreeNote.value = true; | ||||
| 	} else { | ||||
| 		agreeNote.value = false; | ||||
| 	} | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" module> | ||||
|   | ||||
| @@ -31,16 +31,13 @@ import MkMediaList from '@/components/MkMediaList.vue'; | ||||
| import MkPoll from '@/components/MkPoll.vue'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { $i } from '@/account'; | ||||
| import { shouldCollapsed } from '@/scripts/collapsed'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	note: misskey.entities.Note; | ||||
| }>(); | ||||
|  | ||||
| const isLong = | ||||
| 	props.note.cw == null && props.note.text != null && ( | ||||
| 		(props.note.text.split('\n').length > 9) || | ||||
| 		(props.note.text.length > 500) | ||||
| 	); | ||||
| const isLong = shouldCollapsed(props.note); | ||||
|  | ||||
| const collapsed = $ref(isLong); | ||||
| </script> | ||||
|   | ||||
| @@ -52,19 +52,21 @@ | ||||
| 			</footer> | ||||
| 		</article> | ||||
| 	</component> | ||||
| 	<div v-if="tweetId" :class="$style.action"> | ||||
| 		<MkButton :small="true" inline @click="tweetExpanded = true"> | ||||
| 			<i class="ti ti-brand-twitter"></i> {{ i18n.ts.expandTweet }} | ||||
| 		</MkButton> | ||||
| 	</div> | ||||
| 	<div v-if="!playerEnabled && player.url" :class="$style.action"> | ||||
| 		<MkButton :small="true" inline @click="playerEnabled = true"> | ||||
| 			<i class="ti ti-player-play"></i> {{ i18n.ts.enablePlayer }} | ||||
| 		</MkButton> | ||||
| 		<MkButton v-if="!isMobile" :small="true" inline @click="openPlayer()"> | ||||
| 			<i class="ti ti-picture-in-picture"></i> {{ i18n.ts.openInWindow }} | ||||
| 		</MkButton> | ||||
| 	</div> | ||||
| 	<template v-if="showActions"> | ||||
| 		<div v-if="tweetId" :class="$style.action"> | ||||
| 			<MkButton :small="true" inline @click="tweetExpanded = true"> | ||||
| 				<i class="ti ti-brand-twitter"></i> {{ i18n.ts.expandTweet }} | ||||
| 			</MkButton> | ||||
| 		</div> | ||||
| 		<div v-if="!playerEnabled && player.url" :class="$style.action"> | ||||
| 			<MkButton :small="true" inline @click="playerEnabled = true"> | ||||
| 				<i class="ti ti-player-play"></i> {{ i18n.ts.enablePlayer }} | ||||
| 			</MkButton> | ||||
| 			<MkButton v-if="!isMobile" :small="true" inline @click="openPlayer()"> | ||||
| 				<i class="ti ti-picture-in-picture"></i> {{ i18n.ts.openInWindow }} | ||||
| 			</MkButton> | ||||
| 		</div> | ||||
| 	</template> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| @@ -85,9 +87,11 @@ const props = withDefaults(defineProps<{ | ||||
| 	url: string; | ||||
| 	detail?: boolean; | ||||
| 	compact?: boolean; | ||||
| 	showActions?: boolean; | ||||
| }>(), { | ||||
| 	detail: false, | ||||
| 	compact: false, | ||||
| 	showActions: true, | ||||
| }); | ||||
|  | ||||
| const MOBILE_THRESHOLD = 500; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
| <div :class="$style.root" :style="{ zIndex, top: top + 'px', left: left + 'px' }"> | ||||
| 	<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" @afterLeave="emit('closed')"> | ||||
| 		<MkUrlPreview v-if="showing" class="_popup _shadow" :url="url"/> | ||||
| 		<MkUrlPreview v-if="showing" class="_popup _shadow" :url="url" :showActions="false"/> | ||||
| 	</Transition> | ||||
| </div> | ||||
| </template> | ||||
|   | ||||
| @@ -4,6 +4,9 @@ import { userEvent, waitFor, within } from '@storybook/testing-library'; | ||||
| import { StoryObj } from '@storybook/vue3'; | ||||
| import MkAd from './MkAd.vue'; | ||||
| import { i18n } from '@/i18n'; | ||||
|  | ||||
| let lock: Promise<undefined> | undefined; | ||||
|  | ||||
| const common = { | ||||
| 	render(args) { | ||||
| 		return { | ||||
| @@ -26,39 +29,53 @@ const common = { | ||||
| 		}; | ||||
| 	}, | ||||
| 	async play({ canvasElement, args }) { | ||||
| 		const canvas = within(canvasElement); | ||||
| 		const a = canvas.getByRole<HTMLAnchorElement>('link'); | ||||
| 		await expect(a.href).toMatch(/^https?:\/\/.*#test$/); | ||||
| 		const img = within(a).getByRole('img'); | ||||
| 		await expect(img).toBeInTheDocument(); | ||||
| 		let buttons = canvas.getAllByRole<HTMLButtonElement>('button'); | ||||
| 		await expect(buttons).toHaveLength(1); | ||||
| 		const i = buttons[0]; | ||||
| 		await expect(i).toBeInTheDocument(); | ||||
| 		await userEvent.click(i); | ||||
| 		await waitFor(() => expect(canvasElement).toHaveTextContent(i18n.ts._ad.back)); | ||||
| 		await expect(a).not.toBeInTheDocument(); | ||||
| 		await expect(i).not.toBeInTheDocument(); | ||||
| 		buttons = canvas.getAllByRole<HTMLButtonElement>('button'); | ||||
| 		await expect(buttons).toHaveLength(args.__hasReduce ? 2 : 1); | ||||
| 		const reduce = args.__hasReduce ? buttons[0] : null; | ||||
| 		const back = buttons[args.__hasReduce ? 1 : 0]; | ||||
| 		if (reduce) { | ||||
| 			await expect(reduce).toBeInTheDocument(); | ||||
| 			await expect(reduce).toHaveTextContent(i18n.ts._ad.reduceFrequencyOfThisAd); | ||||
| 		if (lock) { | ||||
| 			console.warn('This test is unexpectedly running twice in parallel, fix it!'); | ||||
| 			console.warn('See also: https://github.com/misskey-dev/misskey/issues/11267'); | ||||
| 			await lock; | ||||
| 		} | ||||
| 		await expect(back).toBeInTheDocument(); | ||||
| 		await expect(back).toHaveTextContent(i18n.ts._ad.back); | ||||
| 		await userEvent.click(back); | ||||
| 		await waitFor(() => expect(canvas.queryByRole('img')).toBeTruthy()); | ||||
| 		if (reduce) { | ||||
| 			await expect(reduce).not.toBeInTheDocument(); | ||||
|  | ||||
| 		let resolve: (value?: any) => void; | ||||
| 		lock = new Promise(r => resolve = r); | ||||
|  | ||||
| 		try { | ||||
| 			const canvas = within(canvasElement); | ||||
| 			const a = canvas.getByRole<HTMLAnchorElement>('link'); | ||||
| 			await expect(a.href).toMatch(/^https?:\/\/.*#test$/); | ||||
| 			const img = within(a).getByRole('img'); | ||||
| 			await expect(img).toBeInTheDocument(); | ||||
| 			let buttons = canvas.getAllByRole<HTMLButtonElement>('button'); | ||||
| 			await expect(buttons).toHaveLength(1); | ||||
| 			const i = buttons[0]; | ||||
| 			await expect(i).toBeInTheDocument(); | ||||
| 			await userEvent.click(i); | ||||
| 			await waitFor(() => expect(canvasElement).toHaveTextContent(i18n.ts._ad.back)); | ||||
| 			await expect(a).not.toBeInTheDocument(); | ||||
| 			await expect(i).not.toBeInTheDocument(); | ||||
| 			buttons = canvas.getAllByRole<HTMLButtonElement>('button'); | ||||
| 			await expect(buttons).toHaveLength(args.__hasReduce ? 2 : 1); | ||||
| 			const reduce = args.__hasReduce ? buttons[0] : null; | ||||
| 			const back = buttons[args.__hasReduce ? 1 : 0]; | ||||
| 			if (reduce) { | ||||
| 				await expect(reduce).toBeInTheDocument(); | ||||
| 				await expect(reduce).toHaveTextContent(i18n.ts._ad.reduceFrequencyOfThisAd); | ||||
| 			} | ||||
| 			await expect(back).toBeInTheDocument(); | ||||
| 			await expect(back).toHaveTextContent(i18n.ts._ad.back); | ||||
| 			await userEvent.click(back); | ||||
| 			await waitFor(() => expect(canvas.queryByRole('img')).toBeTruthy()); | ||||
| 			if (reduce) { | ||||
| 				await expect(reduce).not.toBeInTheDocument(); | ||||
| 			} | ||||
| 			await expect(back).not.toBeInTheDocument(); | ||||
| 			const aAgain = canvas.getByRole<HTMLAnchorElement>('link'); | ||||
| 			await expect(aAgain).toBeInTheDocument(); | ||||
| 			const imgAgain = within(aAgain).getByRole('img'); | ||||
| 			await expect(imgAgain).toBeInTheDocument(); | ||||
| 		} finally { | ||||
| 			resolve!(); | ||||
| 			lock = undefined; | ||||
| 		} | ||||
| 		await expect(back).not.toBeInTheDocument(); | ||||
| 		const aAgain = canvas.getByRole<HTMLAnchorElement>('link'); | ||||
| 		await expect(aAgain).toBeInTheDocument(); | ||||
| 		const imgAgain = within(aAgain).getByRole('img'); | ||||
| 		await expect(imgAgain).toBeInTheDocument(); | ||||
| 	}, | ||||
| 	args: { | ||||
| 		prefer: [], | ||||
|   | ||||
| @@ -179,6 +179,9 @@ const patronsWithIcon = [{ | ||||
| }, { | ||||
| 	name: 'カガミ', | ||||
| 	icon: 'https://misskey-hub.net/patrons/226ea3a4617749548580ec2d9a263e24.jpg', | ||||
| }, { | ||||
| 	name: 'フランギ・シュウ', | ||||
| 	icon: 'https://misskey-hub.net/patrons/3016d37e35f3430b90420176c912d304.jpg', | ||||
| }]; | ||||
|  | ||||
| const patrons = [ | ||||
| @@ -276,6 +279,7 @@ const patrons = [ | ||||
| 	'ぷーざ', | ||||
| 	'越貝鯛丸', | ||||
| 	'Nick / pprmint.', | ||||
| 	'kino3277', | ||||
| ]; | ||||
|  | ||||
| let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure')); | ||||
|   | ||||
| @@ -32,7 +32,7 @@ | ||||
| 				<MkUserCardMini :user="file.user"/> | ||||
| 			</MkA> | ||||
| 			<div> | ||||
| 				<MkSwitch v-model="isSensitive" @update:modelValue="toggleIsSensitive">NSFW</MkSwitch> | ||||
| 				<MkSwitch v-model="isSensitive" @update:modelValue="toggleIsSensitive">{{ i18n.ts.sensitive }}</MkSwitch> | ||||
| 			</div> | ||||
|  | ||||
| 			<div> | ||||
|   | ||||
| @@ -40,7 +40,7 @@ | ||||
| 										</div> | ||||
| 										<div v-if="expandedItems.includes(item.id)" :class="$style.userItemSub"> | ||||
| 											<div>Assigned: <MkTime :time="item.createdAt" mode="detail"/></div> | ||||
| 											<div v-if="item.expiresAt">Period: {{ item.expiresAt.toLocaleString() }}</div> | ||||
| 											<div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div> | ||||
| 											<div v-else>Period: {{ i18n.ts.indefinitely }}</div> | ||||
| 										</div> | ||||
| 									</div> | ||||
|   | ||||
| @@ -26,7 +26,7 @@ | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<MkButton rounded style="margin: 0 auto;" @click="changeImage">{{ i18n.ts.selectFile }}</MkButton> | ||||
| 				<MkInput v-model="name"> | ||||
| 				<MkInput v-model="name" pattern="[a-z0-9_]"> | ||||
| 					<template #label>{{ i18n.ts.name }}</template> | ||||
| 				</MkInput> | ||||
| 				<MkInput v-model="category" :datalist="customEmojiCategories"> | ||||
| @@ -70,6 +70,7 @@ | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { computed, watch } from 'vue'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import MkModalWindow from '@/components/MkModalWindow.vue'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| import MkInput from '@/components/MkInput.vue'; | ||||
| @@ -95,7 +96,7 @@ let isSensitive = $ref(props.emoji ? props.emoji.isSensitive : false); | ||||
| let localOnly = $ref(props.emoji ? props.emoji.localOnly : false); | ||||
| let roleIdsThatCanBeUsedThisEmojiAsReaction = $ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []); | ||||
| let rolesThatCanBeUsedThisEmojiAsReaction = $ref([]); | ||||
| let file = $ref(); | ||||
| let file = $ref<misskey.entities.DriveFile>(); | ||||
|  | ||||
| watch($$(roleIdsThatCanBeUsedThisEmojiAsReaction), async () => { | ||||
| 	rolesThatCanBeUsedThisEmojiAsReaction = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null); | ||||
| @@ -110,6 +111,10 @@ const emit = defineEmits<{ | ||||
|  | ||||
| async function changeImage(ev) { | ||||
| 	file = await selectFile(ev.currentTarget ?? ev.target, null); | ||||
| 	const candidate = file.name.replace(/\.(.+)$/, ''); | ||||
| 	if (candidate.match(/^[a-z0-9_]+$/)) { | ||||
| 		name = candidate; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| async function addRole() { | ||||
|   | ||||
| @@ -33,7 +33,7 @@ import MkTextarea from '@/components/MkTextarea.vue'; | ||||
| import MkInput from '@/components/MkInput.vue'; | ||||
| import { useRouter } from '@/router'; | ||||
|  | ||||
| const PRESET_DEFAULT = `/// @ 0.14.0 | ||||
| const PRESET_DEFAULT = `/// @ 0.15.0 | ||||
|  | ||||
| var name = "" | ||||
|  | ||||
| @@ -51,7 +51,7 @@ Ui:render([ | ||||
| ]) | ||||
| `; | ||||
|  | ||||
| const PRESET_OMIKUJI = `/// @ 0.14.0 | ||||
| const PRESET_OMIKUJI = `/// @ 0.15.0 | ||||
| // ユーザーごとに日替わりのおみくじのプリセット | ||||
|  | ||||
| // 選択肢 | ||||
| @@ -94,7 +94,7 @@ Ui:render([ | ||||
| ]) | ||||
| `; | ||||
|  | ||||
| const PRESET_SHUFFLE = `/// @ 0.14.0 | ||||
| const PRESET_SHUFFLE = `/// @ 0.15.0 | ||||
| // 巻き戻し可能な文字シャッフルのプリセット | ||||
|  | ||||
| let string = "ペペロンチーノ" | ||||
| @@ -173,7 +173,7 @@ var cursor = 0 | ||||
| do() | ||||
| `; | ||||
|  | ||||
| const PRESET_QUIZ = `/// @ 0.14.0 | ||||
| const PRESET_QUIZ = `/// @ 0.15.0 | ||||
| let title = '地理クイズ' | ||||
|  | ||||
| let qas = [{ | ||||
| @@ -286,7 +286,7 @@ qaEls.push(Ui:C:container({ | ||||
| Ui:render(qaEls) | ||||
| `; | ||||
|  | ||||
| const PRESET_TIMELINE = `/// @ 0.14.0 | ||||
| const PRESET_TIMELINE = `/// @ 0.15.0 | ||||
| // APIリクエストを行いローカルタイムラインを表示するプリセット | ||||
|  | ||||
| @fetch() { | ||||
|   | ||||
| @@ -236,6 +236,7 @@ definePageMetadata(computed(() => post ? { | ||||
| 			border-top: solid 0.5px var(--divider); | ||||
| 			display: flex; | ||||
| 			align-items: center; | ||||
| 			flex-wrap: wrap; | ||||
|  | ||||
| 			> .avatar { | ||||
| 				width: 52px; | ||||
|   | ||||
| @@ -55,7 +55,7 @@ | ||||
| 						</div> | ||||
| 						<div v-if="expandedMuteItems.includes(item.id)" :class="$style.userItemSub"> | ||||
| 							<div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div> | ||||
| 							<div v-if="item.expiresAt">Period: {{ item.expiresAt.toLocaleString() }}</div> | ||||
| 							<div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div> | ||||
| 							<div v-else>Period: {{ i18n.ts.indefinitely }}</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| @@ -85,7 +85,7 @@ | ||||
| 						</div> | ||||
| 						<div v-if="expandedBlockItems.includes(item.id)" :class="$style.userItemSub"> | ||||
| 							<div>Blocked at: <MkTime :time="item.createdAt" mode="detail"/></div> | ||||
| 							<div v-if="item.expiresAt">Period: {{ item.expiresAt.toLocaleString() }}</div> | ||||
| 							<div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div> | ||||
| 							<div v-else>Period: {{ i18n.ts.indefinitely }}</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
|   | ||||
| @@ -112,9 +112,17 @@ | ||||
| 						<MkButton v-if="user.host == null && iAmModerator" primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton> | ||||
|  | ||||
| 						<div v-for="role in info.roles" :key="role.id" :class="$style.roleItem"> | ||||
| 							<MkRolePreview :class="$style.role" :role="role" :forModeration="true"/> | ||||
| 							<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button> | ||||
| 							<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button> | ||||
| 							<div :class="$style.roleItemMain"> | ||||
| 								<MkRolePreview :class="$style.role" :role="role" :forModeration="true"/> | ||||
| 								<button class="_button" :class="$style.roleToggle" @click="toggleRoleItem(role)"><i class="ti ti-chevron-down"></i></button> | ||||
| 								<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button> | ||||
| 								<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button> | ||||
| 							</div> | ||||
| 							<div v-if="expandedRoles.includes(role.id)" :class="$style.roleItemSub"> | ||||
| 								<div>Assigned: <MkTime :time="info.roleAssigns.find(a => a.roleId === role.id).createdAt" mode="detail"/></div> | ||||
| 								<div v-if="info.roleAssigns.find(a => a.roleId === role.id).expiresAt">Period: {{ new Date(info.roleAssigns.find(a => a.roleId === role.id).expiresAt).toLocaleString() }}</div> | ||||
| 								<div v-else>Period: {{ i18n.ts.indefinitely }}</div> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</MkFolder> | ||||
| @@ -220,6 +228,7 @@ const filesPagination = { | ||||
| 		userId: props.userId, | ||||
| 	})), | ||||
| }; | ||||
| let expandedRoles = $ref([]); | ||||
|  | ||||
| function createFetcher() { | ||||
| 	if (iAmModerator) { | ||||
| @@ -384,6 +393,14 @@ async function unassignRole(role, ev) { | ||||
| 	}], ev.currentTarget ?? ev.target); | ||||
| } | ||||
|  | ||||
| function toggleRoleItem(role) { | ||||
| 	if (expandedRoles.includes(role.id)) { | ||||
| 		expandedRoles = expandedRoles.filter(x => x !== role.id); | ||||
| 	} else { | ||||
| 		expandedRoles.push(role.id); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| watch(() => props.userId, () => { | ||||
| 	init = createFetcher(); | ||||
| }, { | ||||
| @@ -523,11 +540,22 @@ definePageMetadata(computed(() => ({ | ||||
| } | ||||
|  | ||||
| .roleItem { | ||||
| } | ||||
|  | ||||
| .roleItemMain { | ||||
| 	display: flex; | ||||
| } | ||||
|  | ||||
| .role { | ||||
| 	flex: 1; | ||||
| 	min-width: 0; | ||||
| 	margin-right: 8px; | ||||
| } | ||||
|  | ||||
| .roleItemSub { | ||||
| 	padding: 6px 12px; | ||||
| 	font-size: 85%; | ||||
| 	color: var(--fgTransparentWeak); | ||||
| } | ||||
|  | ||||
| .roleUnassign { | ||||
|   | ||||
							
								
								
									
										19
									
								
								packages/frontend/src/scripts/collapsed.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								packages/frontend/src/scripts/collapsed.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| import * as mfm from 'mfm-js'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import { extractUrlFromMfm } from './extract-url-from-mfm'; | ||||
|  | ||||
| export function shouldCollapsed(note: misskey.entities.Note): boolean { | ||||
| 	const urls = note.text ? extractUrlFromMfm(mfm.parse(note.text)) : null; | ||||
| 	const collapsed = note.cw == null && note.text != null && ( | ||||
| 		(note.text.includes('$[x2')) || | ||||
| 		(note.text.includes('$[x3')) || | ||||
| 		(note.text.includes('$[x4')) || | ||||
| 		(note.text.includes('$[scale')) || | ||||
| 		(note.text.split('\n').length > 9) || | ||||
| 		(note.text.length > 500) || | ||||
| 		(note.files.length >= 5) || | ||||
| 		(!!urls && urls.length >= 4) | ||||
| 	); | ||||
|  | ||||
| 	return collapsed; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 tamaina
					tamaina