Merge branch 'develop' into mkjs-n
This commit is contained in:
		
							
								
								
									
										10
									
								
								.github/workflows/storybook.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/storybook.yml
									
									
									
									
										vendored
									
									
								
							| @@ -20,7 +20,7 @@ jobs: | |||||||
|         fetch-depth: 0 |         fetch-depth: 0 | ||||||
|         submodules: true |         submodules: true | ||||||
|     - name: Checkout HEAD |     - name: Checkout HEAD | ||||||
|       if: github.event_name == 'pull_request' |       if: github.event_name == 'pull_request_target' | ||||||
|       run: git checkout ${{ github.head_ref }} |       run: git checkout ${{ github.head_ref }} | ||||||
|     - name: Install pnpm |     - name: Install pnpm | ||||||
|       uses: pnpm/action-setup@v2 |       uses: pnpm/action-setup@v2 | ||||||
| @@ -41,12 +41,12 @@ jobs: | |||||||
|     - name: Build storybook |     - name: Build storybook | ||||||
|       run: pnpm --filter frontend build-storybook |       run: pnpm --filter frontend build-storybook | ||||||
|     - name: Publish to Chromatic |     - name: Publish to Chromatic | ||||||
|       if: github.event_name != 'pull_request' && github.ref == 'refs/heads/master' |       if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/master' | ||||||
|       run: pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static |       run: pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static | ||||||
|       env: |       env: | ||||||
|         CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} |         CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} | ||||||
|     - name: Publish to Chromatic |     - name: Publish to Chromatic | ||||||
|       if: github.event_name != 'pull_request' && github.ref != 'refs/heads/master' |       if: github.event_name != 'pull_request_target' && github.ref != 'refs/heads/master' | ||||||
|       id: chromatic_push |       id: chromatic_push | ||||||
|       run: | |       run: | | ||||||
|         DIFF="${{ github.event.before }} HEAD" |         DIFF="${{ github.event.before }} HEAD" | ||||||
| @@ -61,7 +61,7 @@ jobs: | |||||||
|       env: |       env: | ||||||
|         CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} |         CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} | ||||||
|     - name: Publish to Chromatic |     - name: Publish to Chromatic | ||||||
|       if: github.event_name == 'pull_request' |       if: github.event_name == 'pull_request_target' | ||||||
|       id: chromatic_pull_request |       id: chromatic_pull_request | ||||||
|       run: | |       run: | | ||||||
|         DIFF="${{ github.base_ref }} HEAD" |         DIFF="${{ github.base_ref }} HEAD" | ||||||
| @@ -77,7 +77,7 @@ jobs: | |||||||
|         CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} |         CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} | ||||||
|     - name: Notify that Chromatic will skip testing |     - name: Notify that Chromatic will skip testing | ||||||
|       uses: actions/github-script@v6.4.0 |       uses: actions/github-script@v6.4.0 | ||||||
|       if: github.event_name == 'pull_request' && steps.chromatic_pull_request.outputs.skip == 'true' |       if: github.event_name == 'pull_request_target' && steps.chromatic_pull_request.outputs.skip == 'true' | ||||||
|       with: |       with: | ||||||
|         github-token: ${{ secrets.GITHUB_TOKEN }} |         github-token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|         script: | |         script: | | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ | |||||||
|  |  | ||||||
| --> | --> | ||||||
|  |  | ||||||
| ## 13.x.x (unreleased) | ## 13.12.2 | ||||||
|  |  | ||||||
| ### General | ### General | ||||||
| - 投稿したコンテンツのAIによる学習を軽減するオプションを追加 | - 投稿したコンテンツのAIによる学習を軽減するオプションを追加 | ||||||
| @@ -21,7 +21,7 @@ | |||||||
| - Fix: ブラーエフェクトを有効にしている状態で高負荷になる問題を修正 | - Fix: ブラーエフェクトを有効にしている状態で高負荷になる問題を修正 | ||||||
|  |  | ||||||
| ### Server | ### Server | ||||||
| - | - センシティブワードの登録にAnd、正規表現が使用できるようになりました。 | ||||||
|  |  | ||||||
| ## 13.12.1 | ## 13.12.1 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1038,6 +1038,8 @@ thisChannelArchived: "Dieser Kanal wurde archiviert." | |||||||
| displayOfNote: "Anzeige von Notizen" | displayOfNote: "Anzeige von Notizen" | ||||||
| initialAccountSetting: "Kontoeinrichtung" | initialAccountSetting: "Kontoeinrichtung" | ||||||
| youFollowing: "Gefolgt" | youFollowing: "Gefolgt" | ||||||
|  | preventAiLarning: "Verwendung in machinellem Lernen (AI/KI) ablehnen" | ||||||
|  | preventAiLarningDescription: "Fordert Crawler auf, gepostetes Text- oder Bildmaterial usw. nicht in Datensätzen für maschinelles Lernen (AI/KI) zu verwenden. Dies wird durch das Hinzufügen eines \"noai\"-HTML-Tags an den jeweiligen Inhalt erreicht. Da dieser Tag jedoch ignoriert werden kann, ist eine vollständige Verhinderung hierdurch nicht möglich." | ||||||
| _initialAccountSetting: | _initialAccountSetting: | ||||||
|   accountCreated: "Dein Konto wurde erfolgreich erstellt!" |   accountCreated: "Dein Konto wurde erfolgreich erstellt!" | ||||||
|   letsStartAccountSetup: "Lass uns nun dein Konto einrichten." |   letsStartAccountSetup: "Lass uns nun dein Konto einrichten." | ||||||
|   | |||||||
| @@ -1038,6 +1038,8 @@ thisChannelArchived: "This channel has been archived." | |||||||
| displayOfNote: "Note display" | displayOfNote: "Note display" | ||||||
| initialAccountSetting: "Profile setup" | initialAccountSetting: "Profile setup" | ||||||
| youFollowing: "Followed" | youFollowing: "Followed" | ||||||
|  | preventAiLarning: "Reject usage in Machine Learning (AI)" | ||||||
|  | preventAiLarningDescription: "Requests crawlers to not use posted text or image material etc. in machine learning (AI) data sets. This is achieved by adding a \"noai\" HTML-Tag to the respective content. A complete prevention can however not be achieved through this tag, as it may simply be ignored." | ||||||
| _initialAccountSetting: | _initialAccountSetting: | ||||||
|   accountCreated: "Your account was successfully created!" |   accountCreated: "Your account was successfully created!" | ||||||
|   letsStartAccountSetup: "For starters, let's set up your profile." |   letsStartAccountSetup: "For starters, let's set up your profile." | ||||||
|   | |||||||
| @@ -990,6 +990,7 @@ rolesAssignedToMe: "自分に割り当てられたロール" | |||||||
| resetPasswordConfirm: "パスワードリセットしますか?" | resetPasswordConfirm: "パスワードリセットしますか?" | ||||||
| sensitiveWords: "センシティブワード" | sensitiveWords: "センシティブワード" | ||||||
| sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。" | sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。" | ||||||
|  | sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。" | ||||||
| notesSearchNotAvailable: "ノート検索は利用できません。" | notesSearchNotAvailable: "ノート検索は利用できません。" | ||||||
| license: "ライセンス" | license: "ライセンス" | ||||||
| unfavoriteConfirm: "お気に入り解除しますか?" | unfavoriteConfirm: "お気に入り解除しますか?" | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
| 	"name": "misskey", | 	"name": "misskey", | ||||||
| 	"version": "13.12.1", | 	"version": "13.12.2", | ||||||
| 	"codename": "nasubi", | 	"codename": "nasubi", | ||||||
| 	"repository": { | 	"repository": { | ||||||
| 		"type": "git", | 		"type": "git", | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import * as mfm from 'mfm-js'; | |||||||
| import { In, DataSource } from 'typeorm'; | import { In, DataSource } from 'typeorm'; | ||||||
| import * as Redis from 'ioredis'; | import * as Redis from 'ioredis'; | ||||||
| import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; | import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; | ||||||
|  | import RE2 from 're2'; | ||||||
| import { extractMentions } from '@/misc/extract-mentions.js'; | import { extractMentions } from '@/misc/extract-mentions.js'; | ||||||
| import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; | import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; | ||||||
| import { extractHashtags } from '@/misc/extract-hashtags.js'; | import { extractHashtags } from '@/misc/extract-hashtags.js'; | ||||||
| @@ -238,7 +239,8 @@ export class NoteCreateService implements OnApplicationShutdown { | |||||||
| 		if (data.channel != null) data.localOnly = true; | 		if (data.channel != null) data.localOnly = true; | ||||||
|  |  | ||||||
| 		if (data.visibility === 'public' && data.channel == null) { | 		if (data.visibility === 'public' && data.channel == null) { | ||||||
| 			if ((data.text != null) && (await this.metaService.fetch()).sensitiveWords.some(w => data.text!.includes(w))) { | 			const sensitiveWords = (await this.metaService.fetch()).sensitiveWords; | ||||||
|  | 			if (this.isSensitive(data, sensitiveWords)) { | ||||||
| 				data.visibility = 'home'; | 				data.visibility = 'home'; | ||||||
| 			} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { | 			} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { | ||||||
| 				data.visibility = 'home'; | 				data.visibility = 'home'; | ||||||
| @@ -671,6 +673,31 @@ export class NoteCreateService implements OnApplicationShutdown { | |||||||
| 		this.index(note); | 		this.index(note); | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
|  | 	@bindThis | ||||||
|  | 	private isSensitive(note: Option, sensitiveWord: string[]): boolean { | ||||||
|  | 		if (sensitiveWord.length > 0) { | ||||||
|  | 			const text = note.cw ?? note.text ?? ''; | ||||||
|  | 			if (text === '') return false; | ||||||
|  | 			const matched = sensitiveWord.some(filter => { | ||||||
|  | 				// represents RegExp | ||||||
|  | 				const regexp = filter.match(/^\/(.+)\/(.*)$/); | ||||||
|  | 				// This should never happen due to input sanitisation. | ||||||
|  | 				if (!regexp) { | ||||||
|  | 					const words = filter.split(' '); | ||||||
|  | 					return words.every(keyword => text.includes(keyword)); | ||||||
|  | 				} | ||||||
|  | 				try { | ||||||
|  | 					return new RE2(regexp[1], regexp[2]).test(text); | ||||||
|  | 				} catch (err) { | ||||||
|  | 					// This should never happen due to input sanitisation. | ||||||
|  | 					return false; | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 			if (matched) return true; | ||||||
|  | 		} | ||||||
|  | 		return false; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	@bindThis | 	@bindThis | ||||||
| 	private incRenoteCount(renote: Note) { | 	private incRenoteCount(renote: Note) { | ||||||
| 		this.notesRepository.createQueryBuilder().update() | 		this.notesRepository.createQueryBuilder().update() | ||||||
|   | |||||||
| @@ -541,6 +541,61 @@ describe('Note', () => { | |||||||
|  |  | ||||||
| 			assert.strictEqual(res.status, 400); | 			assert.strictEqual(res.status, 400); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
|  | 		test('センシティブな投稿はhomeになる (単語指定)', async () => { | ||||||
|  | 			const sensitive = await api('admin/update-meta', { | ||||||
|  | 				sensitiveWords: [ | ||||||
|  | 					"test", | ||||||
|  | 				] | ||||||
|  | 			}, alice); | ||||||
|  |  | ||||||
|  | 			assert.strictEqual(sensitive.status, 204); | ||||||
|  |  | ||||||
|  | 			await new Promise(x => setTimeout(x, 2)); | ||||||
|  |  | ||||||
|  | 			const note1 = await api('/notes/create', { | ||||||
|  | 				text: 'hogetesthuge', | ||||||
|  | 			}, alice); | ||||||
|  |  | ||||||
|  | 			assert.strictEqual(note1.status, 200); | ||||||
|  | 			assert.strictEqual(note1.body.createdNote.visibility, 'home'); | ||||||
|  |  | ||||||
|  | 		}); | ||||||
|  |  | ||||||
|  | 		test('センシティブな投稿はhomeになる (正規表現)', async () => { | ||||||
|  | 			const sensitive = await api('admin/update-meta', { | ||||||
|  | 				sensitiveWords: [ | ||||||
|  | 					"/Test/i", | ||||||
|  | 				] | ||||||
|  | 			}, alice); | ||||||
|  |  | ||||||
|  | 			assert.strictEqual(sensitive.status, 204); | ||||||
|  |  | ||||||
|  | 			const note2 = await api('/notes/create', { | ||||||
|  | 				text: 'hogetesthuge', | ||||||
|  | 			}, alice); | ||||||
|  |  | ||||||
|  | 			assert.strictEqual(note2.status, 200); | ||||||
|  | 			assert.strictEqual(note2.body.createdNote.visibility, 'home'); | ||||||
|  | 		}); | ||||||
|  |  | ||||||
|  | 		test('センシティブな投稿はhomeになる (スペースアンド)', async () => { | ||||||
|  | 			const sensitive = await api('admin/update-meta', { | ||||||
|  | 				sensitiveWords: [ | ||||||
|  | 					"Test hoge" | ||||||
|  | 				] | ||||||
|  | 			}, alice); | ||||||
|  |  | ||||||
|  | 			assert.strictEqual(sensitive.status, 204); | ||||||
|  |  | ||||||
|  | 			const note2 = await api('/notes/create', { | ||||||
|  | 				text: 'hogeTesthuge', | ||||||
|  | 			}, alice); | ||||||
|  |  | ||||||
|  | 			assert.strictEqual(note2.status, 200); | ||||||
|  | 			assert.strictEqual(note2.body.createdNote.visibility, 'home'); | ||||||
|  |  | ||||||
|  | 		}); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	describe('notes/delete', () => { | 	describe('notes/delete', () => { | ||||||
|   | |||||||
| @@ -1,144 +0,0 @@ | |||||||
| <template> |  | ||||||
| <div |  | ||||||
| 	class="ziffeoms" |  | ||||||
| 	:class="{ disabled, checked }" |  | ||||||
| > |  | ||||||
| 	<input |  | ||||||
| 		ref="input" |  | ||||||
| 		type="checkbox" |  | ||||||
| 		:disabled="disabled" |  | ||||||
| 		@keydown.enter="toggle" |  | ||||||
| 	> |  | ||||||
| 	<span ref="button" v-adaptive-border v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" @click.prevent="toggle"> |  | ||||||
| 		<i class="check ti ti-check"></i> |  | ||||||
| 	</span> |  | ||||||
| 	<span class="label"> |  | ||||||
| 		<!-- TODO: 無名slotの方は廃止 --> |  | ||||||
| 		<span @click="toggle"><slot name="label"></slot><slot></slot></span> |  | ||||||
| 		<p class="caption"><slot name="caption"></slot></p> |  | ||||||
| 	</span> |  | ||||||
| </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script lang="ts" setup> |  | ||||||
| import { toRefs, Ref } from 'vue'; |  | ||||||
| import * as os from '@/os'; |  | ||||||
| import MkRippleEffect from '@/components/MkRippleEffect.vue'; |  | ||||||
| import { i18n } from '@/i18n'; |  | ||||||
|  |  | ||||||
| const props = defineProps<{ |  | ||||||
| 	modelValue: boolean | Ref<boolean>; |  | ||||||
| 	disabled?: boolean; |  | ||||||
| }>(); |  | ||||||
|  |  | ||||||
| const emit = defineEmits<{ |  | ||||||
| 	(ev: 'update:modelValue', v: boolean): void; |  | ||||||
| }>(); |  | ||||||
|  |  | ||||||
| let button = $shallowRef<HTMLElement>(); |  | ||||||
| const checked = toRefs(props).modelValue; |  | ||||||
| const toggle = () => { |  | ||||||
| 	if (props.disabled) return; |  | ||||||
| 	emit('update:modelValue', !checked.value); |  | ||||||
|  |  | ||||||
| 	if (!checked.value) { |  | ||||||
| 		const rect = button.getBoundingClientRect(); |  | ||||||
| 		const x = rect.left + (button.offsetWidth / 2); |  | ||||||
| 		const y = rect.top + (button.offsetHeight / 2); |  | ||||||
| 		os.popup(MkRippleEffect, { x, y, particle: false }, {}, 'end'); |  | ||||||
| 	} |  | ||||||
| }; |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| .ziffeoms { |  | ||||||
| 	position: relative; |  | ||||||
| 	display: flex; |  | ||||||
| 	transition: all 0.2s ease; |  | ||||||
|  |  | ||||||
| 	> * { |  | ||||||
| 		user-select: none; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	> input { |  | ||||||
| 		position: absolute; |  | ||||||
| 		width: 0; |  | ||||||
| 		height: 0; |  | ||||||
| 		opacity: 0; |  | ||||||
| 		margin: 0; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	> .button { |  | ||||||
| 		position: relative; |  | ||||||
| 		display: inline-flex; |  | ||||||
| 		flex-shrink: 0; |  | ||||||
| 		margin: 0; |  | ||||||
| 		box-sizing: border-box; |  | ||||||
| 		width: 23px; |  | ||||||
| 		height: 23px; |  | ||||||
| 		outline: none; |  | ||||||
| 		background: var(--panel); |  | ||||||
| 		border: solid 1px var(--panel); |  | ||||||
| 		border-radius: 4px; |  | ||||||
| 		cursor: pointer; |  | ||||||
| 		transition: inherit; |  | ||||||
|  |  | ||||||
| 		> .check { |  | ||||||
| 			margin: auto; |  | ||||||
| 			opacity: 0; |  | ||||||
| 			color: var(--fgOnAccent); |  | ||||||
| 			font-size: 13px; |  | ||||||
| 			transform: scale(0.5); |  | ||||||
| 			transition: all 0.2s ease; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	&:hover { |  | ||||||
| 		> .button { |  | ||||||
| 			border-color: var(--inputBorderHover) !important; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	> .label { |  | ||||||
| 		margin-left: 12px; |  | ||||||
| 		margin-top: 2px; |  | ||||||
| 		display: block; |  | ||||||
| 		transition: inherit; |  | ||||||
| 		color: var(--fg); |  | ||||||
|  |  | ||||||
| 		> span { |  | ||||||
| 			display: block; |  | ||||||
| 			line-height: 20px; |  | ||||||
| 			cursor: pointer; |  | ||||||
| 			transition: inherit; |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		> .caption { |  | ||||||
| 			margin: 8px 0 0 0; |  | ||||||
| 			color: var(--fgTransparentWeak); |  | ||||||
| 			font-size: 0.85em; |  | ||||||
|  |  | ||||||
| 			&:empty { |  | ||||||
| 				display: none; |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	&.disabled { |  | ||||||
| 		opacity: 0.6; |  | ||||||
| 		cursor: not-allowed; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	&.checked { |  | ||||||
| 		> .button { |  | ||||||
| 			background-color: var(--accent) !important; |  | ||||||
| 			border-color: var(--accent) !important; |  | ||||||
|  |  | ||||||
| 			> .check { |  | ||||||
| 				opacity: 1; |  | ||||||
| 				transform: scale(1); |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -1,8 +1,7 @@ | |||||||
| <template> | <template> | ||||||
| <div | <div | ||||||
| 	v-adaptive-border | 	v-adaptive-border | ||||||
| 	class="novjtctn" | 	:class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]" | ||||||
| 	:class="{ disabled, checked }" |  | ||||||
| 	:aria-checked="checked" | 	:aria-checked="checked" | ||||||
| 	:aria-disabled="disabled" | 	:aria-disabled="disabled" | ||||||
| 	@click="toggle" | 	@click="toggle" | ||||||
| @@ -10,11 +9,12 @@ | |||||||
| 	<input | 	<input | ||||||
| 		type="radio" | 		type="radio" | ||||||
| 		:disabled="disabled" | 		:disabled="disabled" | ||||||
|  | 		:class="$style.input" | ||||||
| 	> | 	> | ||||||
| 	<span class="button"> | 	<span :class="$style.button"> | ||||||
| 		<span></span> | 		<span></span> | ||||||
| 	</span> | 	</span> | ||||||
| 	<span class="label"><slot></slot></span> | 	<span :class="$style.label"><slot></slot></span> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| @@ -39,8 +39,8 @@ function toggle(): void { | |||||||
| } | } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" module> | ||||||
| .novjtctn { | .root { | ||||||
| 	position: relative; | 	position: relative; | ||||||
| 	display: inline-block; | 	display: inline-block; | ||||||
| 	text-align: left; | 	text-align: left; | ||||||
| @@ -53,17 +53,11 @@ function toggle(): void { | |||||||
| 	border-radius: 6px; | 	border-radius: 6px; | ||||||
| 	font-size: 90%; | 	font-size: 90%; | ||||||
| 	transition: all 0.2s; | 	transition: all 0.2s; | ||||||
|  | 	user-select: none; | ||||||
| 	> * { |  | ||||||
| 		user-select: none; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	&.disabled { | 	&.disabled { | ||||||
| 		opacity: 0.6; | 		opacity: 0.6; | ||||||
|  | 		cursor: not-allowed !important; | ||||||
| 		&, * { |  | ||||||
| 			cursor: not-allowed !important; |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	&:hover { | 	&:hover { | ||||||
| @@ -74,10 +68,7 @@ function toggle(): void { | |||||||
| 		background-color: var(--accentedBg) !important; | 		background-color: var(--accentedBg) !important; | ||||||
| 		border-color: var(--accentedBg) !important; | 		border-color: var(--accentedBg) !important; | ||||||
| 		color: var(--accent); | 		color: var(--accent); | ||||||
|  | 		cursor: default !important; | ||||||
| 		&, * { |  | ||||||
| 			cursor: default !important; |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		> .button { | 		> .button { | ||||||
| 			border-color: var(--accent); | 			border-color: var(--accent); | ||||||
| @@ -89,44 +80,44 @@ function toggle(): void { | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| 	> input { | .input { | ||||||
| 		position: absolute; | 	position: absolute; | ||||||
| 		width: 0; | 	width: 0; | ||||||
| 		height: 0; | 	height: 0; | ||||||
| 		opacity: 0; | 	opacity: 0; | ||||||
| 		margin: 0; | 	margin: 0; | ||||||
| 	} | } | ||||||
|  |  | ||||||
| 	> .button { | .button { | ||||||
| 		position: absolute; | 	position: absolute; | ||||||
| 		width: 14px; | 	width: 14px; | ||||||
| 		height: 14px; | 	height: 14px; | ||||||
| 		background: none; | 	background: none; | ||||||
| 		border: solid 2px var(--inputBorder); | 	border: solid 2px var(--inputBorder); | ||||||
| 		border-radius: 100%; | 	border-radius: 100%; | ||||||
| 		transition: inherit; | 	transition: inherit; | ||||||
|  |  | ||||||
| 		&:after { | 	&:after { | ||||||
| 			content: ''; | 		content: ''; | ||||||
| 			display: block; |  | ||||||
| 			position: absolute; |  | ||||||
| 			top: 3px; |  | ||||||
| 			right: 3px; |  | ||||||
| 			bottom: 3px; |  | ||||||
| 			left: 3px; |  | ||||||
| 			border-radius: 100%; |  | ||||||
| 			opacity: 0; |  | ||||||
| 			transform: scale(0); |  | ||||||
| 			transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	> .label { |  | ||||||
| 		margin-left: 28px; |  | ||||||
| 		display: block; | 		display: block; | ||||||
| 		line-height: 20px; | 		position: absolute; | ||||||
| 		cursor: pointer; | 		top: 3px; | ||||||
|  | 		right: 3px; | ||||||
|  | 		bottom: 3px; | ||||||
|  | 		left: 3px; | ||||||
|  | 		border-radius: 100%; | ||||||
|  | 		opacity: 0; | ||||||
|  | 		transform: scale(0); | ||||||
|  | 		transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .label { | ||||||
|  | 	margin-left: 28px; | ||||||
|  | 	display: block; | ||||||
|  | 	line-height: 20px; | ||||||
|  | 	cursor: pointer; | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,21 +1,19 @@ | |||||||
| <template> | <template> | ||||||
| <div | <div :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"> | ||||||
| 	class="ziffeomt" |  | ||||||
| 	:class="{ disabled, checked }" |  | ||||||
| > |  | ||||||
| 	<input | 	<input | ||||||
| 		ref="input" | 		ref="input" | ||||||
| 		type="checkbox" | 		type="checkbox" | ||||||
| 		:disabled="disabled" | 		:disabled="disabled" | ||||||
|  | 		:class="$style.input" | ||||||
| 		@keydown.enter="toggle" | 		@keydown.enter="toggle" | ||||||
| 	> | 	> | ||||||
| 	<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" data-cy-switch-toggle @click.prevent="toggle"> | 	<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" :class="$style.button" data-cy-switch-toggle @click.prevent="toggle"> | ||||||
| 		<div class="knob"></div> | 		<div :class="$style.knob"></div> | ||||||
| 	</span> | 	</span> | ||||||
| 	<span class="label"> | 	<span :class="$style.body"> | ||||||
| 		<!-- TODO: 無名slotの方は廃止 --> | 		<!-- TODO: 無名slotの方は廃止 --> | ||||||
| 		<span @click="toggle"><slot name="label"></slot><slot></slot></span> | 		<span :class="$style.label" @click="toggle"><slot name="label"></slot><slot></slot></span> | ||||||
| 		<p class="caption"><slot name="caption"></slot></p> | 		<p :class="$style.caption"><slot name="caption"></slot></p> | ||||||
| 	</span> | 	</span> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| @@ -45,52 +43,12 @@ const toggle = () => { | |||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" module> | ||||||
| .ziffeomt { | .root { | ||||||
| 	position: relative; | 	position: relative; | ||||||
| 	display: flex; | 	display: flex; | ||||||
| 	transition: all 0.2s ease; | 	transition: all 0.2s ease; | ||||||
|  | 	user-select: none; | ||||||
| 	> * { |  | ||||||
| 		user-select: none; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	> input { |  | ||||||
| 		position: absolute; |  | ||||||
| 		width: 0; |  | ||||||
| 		height: 0; |  | ||||||
| 		opacity: 0; |  | ||||||
| 		margin: 0; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	> .button { |  | ||||||
| 		position: relative; |  | ||||||
| 		display: inline-flex; |  | ||||||
| 		flex-shrink: 0; |  | ||||||
| 		margin: 0; |  | ||||||
| 		box-sizing: border-box; |  | ||||||
| 		width: 32px; |  | ||||||
| 		height: 23px; |  | ||||||
| 		outline: none; |  | ||||||
| 		background: var(--switchOffBg); |  | ||||||
| 		background-clip: content-box; |  | ||||||
| 		border: solid 1px var(--switchOffBg); |  | ||||||
| 		border-radius: 999px; |  | ||||||
| 		cursor: pointer; |  | ||||||
| 		transition: inherit; |  | ||||||
| 		user-select: none; |  | ||||||
|  |  | ||||||
| 		> .knob { |  | ||||||
| 			position: absolute; |  | ||||||
| 			top: 3px; |  | ||||||
| 			left: 3px; |  | ||||||
| 			width: 15px; |  | ||||||
| 			height: 15px; |  | ||||||
| 			background: var(--switchOffFg); |  | ||||||
| 			border-radius: 999px; |  | ||||||
| 			transition: all 0.2s ease; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	&:hover { | 	&:hover { | ||||||
| 		> .button { | 		> .button { | ||||||
| @@ -98,31 +56,6 @@ const toggle = () => { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	> .label { |  | ||||||
| 		margin-left: 12px; |  | ||||||
| 		margin-top: 2px; |  | ||||||
| 		display: block; |  | ||||||
| 		transition: inherit; |  | ||||||
| 		color: var(--fg); |  | ||||||
|  |  | ||||||
| 		> span { |  | ||||||
| 			display: block; |  | ||||||
| 			line-height: 20px; |  | ||||||
| 			cursor: pointer; |  | ||||||
| 			transition: inherit; |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		> .caption { |  | ||||||
| 			margin: 8px 0 0 0; |  | ||||||
| 			color: var(--fgTransparentWeak); |  | ||||||
| 			font-size: 0.85em; |  | ||||||
|  |  | ||||||
| 			&:empty { |  | ||||||
| 				display: none; |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	&.disabled { | 	&.disabled { | ||||||
| 		opacity: 0.6; | 		opacity: 0.6; | ||||||
| 		cursor: not-allowed; | 		cursor: not-allowed; | ||||||
| @@ -140,4 +73,66 @@ const toggle = () => { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .input { | ||||||
|  | 	position: absolute; | ||||||
|  | 	width: 0; | ||||||
|  | 	height: 0; | ||||||
|  | 	opacity: 0; | ||||||
|  | 	margin: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .button { | ||||||
|  | 	position: relative; | ||||||
|  | 	display: inline-flex; | ||||||
|  | 	flex-shrink: 0; | ||||||
|  | 	margin: 0; | ||||||
|  | 	box-sizing: border-box; | ||||||
|  | 	width: 32px; | ||||||
|  | 	height: 23px; | ||||||
|  | 	outline: none; | ||||||
|  | 	background: var(--switchOffBg); | ||||||
|  | 	background-clip: content-box; | ||||||
|  | 	border: solid 1px var(--switchOffBg); | ||||||
|  | 	border-radius: 999px; | ||||||
|  | 	cursor: pointer; | ||||||
|  | 	transition: inherit; | ||||||
|  | 	user-select: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .knob { | ||||||
|  | 	position: absolute; | ||||||
|  | 	top: 3px; | ||||||
|  | 	left: 3px; | ||||||
|  | 	width: 15px; | ||||||
|  | 	height: 15px; | ||||||
|  | 	background: var(--switchOffFg); | ||||||
|  | 	border-radius: 999px; | ||||||
|  | 	transition: all 0.2s ease; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .body { | ||||||
|  | 	margin-left: 12px; | ||||||
|  | 	margin-top: 2px; | ||||||
|  | 	display: block; | ||||||
|  | 	transition: inherit; | ||||||
|  | 	color: var(--fg); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .label { | ||||||
|  | 	display: block; | ||||||
|  | 	line-height: 20px; | ||||||
|  | 	cursor: pointer; | ||||||
|  | 	transition: inherit; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .caption { | ||||||
|  | 	margin: 8px 0 0 0; | ||||||
|  | 	color: var(--fgTransparentWeak); | ||||||
|  | 	font-size: 0.85em; | ||||||
|  |  | ||||||
|  | 	&:empty { | ||||||
|  | 		display: none; | ||||||
|  | 	} | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -32,6 +32,7 @@ | |||||||
| 	<component :is="`widget-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @update-props="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/> | 	<component :is="`widget-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @update-props="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| export type Widget = { | export type Widget = { | ||||||
| 	name: string; | 	name: string; | ||||||
| @@ -42,6 +43,7 @@ export type DefaultStoredWidget = { | |||||||
| 	place: string | null; | 	place: string | null; | ||||||
| } & Widget; | } & Widget; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { defineAsyncComponent, ref } from 'vue'; | import { defineAsyncComponent, ref } from 'vue'; | ||||||
| import { v4 as uuid } from 'uuid'; | import { v4 as uuid } from 'uuid'; | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ | |||||||
| 					 | 					 | ||||||
| 					<MkTextarea v-model="sensitiveWords"> | 					<MkTextarea v-model="sensitiveWords"> | ||||||
| 						<template #label>{{ i18n.ts.sensitiveWords }}</template> | 						<template #label>{{ i18n.ts.sensitiveWords }}</template> | ||||||
| 						<template #caption>{{ i18n.ts.sensitiveWordsDescription }}</template> | 						<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template> | ||||||
| 					</MkTextarea> | 					</MkTextarea> | ||||||
| 				</div> | 				</div> | ||||||
| 			</FormSuspense> | 			</FormSuspense> | ||||||
|   | |||||||
| @@ -28,9 +28,9 @@ | |||||||
| 		<MkFoldableSection ref="tagsEl" :foldable="true" :expanded="false" class="_margin"> | 		<MkFoldableSection ref="tagsEl" :foldable="true" :expanded="false" class="_margin"> | ||||||
| 			<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template> | 			<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template> | ||||||
|  |  | ||||||
| 			<div class="vxjfqztj"> | 			<div> | ||||||
| 				<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/user-tags/${tag.tag}`" class="local">{{ tag.tag }}</MkA> | 				<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/user-tags/${tag.tag}`" style="margin-right: 16px; font-weight: bold;">{{ tag.tag }}</MkA> | ||||||
| 				<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/user-tags/${tag.tag}`">{{ tag.tag }}</MkA> | 				<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/user-tags/${tag.tag}`" style="margin-right: 16px;">{{ tag.tag }}</MkA> | ||||||
| 			</div> | 			</div> | ||||||
| 		</MkFoldableSection> | 		</MkFoldableSection> | ||||||
|  |  | ||||||
| @@ -132,15 +132,3 @@ os.api('hashtags/list', { | |||||||
| 	tagsRemote = tags; | 	tagsRemote = tags; | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| .vxjfqztj { |  | ||||||
| 	> * { |  | ||||||
| 		margin-right: 16px; |  | ||||||
|  |  | ||||||
| 		&.local { |  | ||||||
| 			font-weight: bold; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 tamaina
					tamaina