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 | ||||
|         submodules: true | ||||
|     - name: Checkout HEAD | ||||
|       if: github.event_name == 'pull_request' | ||||
|       if: github.event_name == 'pull_request_target' | ||||
|       run: git checkout ${{ github.head_ref }} | ||||
|     - name: Install pnpm | ||||
|       uses: pnpm/action-setup@v2 | ||||
| @@ -41,12 +41,12 @@ jobs: | ||||
|     - name: Build storybook | ||||
|       run: pnpm --filter frontend build-storybook | ||||
|     - 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 | ||||
|       env: | ||||
|         CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} | ||||
|     - 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 | ||||
|       run: | | ||||
|         DIFF="${{ github.event.before }} HEAD" | ||||
| @@ -61,7 +61,7 @@ jobs: | ||||
|       env: | ||||
|         CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} | ||||
|     - name: Publish to Chromatic | ||||
|       if: github.event_name == 'pull_request' | ||||
|       if: github.event_name == 'pull_request_target' | ||||
|       id: chromatic_pull_request | ||||
|       run: | | ||||
|         DIFF="${{ github.base_ref }} HEAD" | ||||
| @@ -77,7 +77,7 @@ jobs: | ||||
|         CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} | ||||
|     - name: Notify that Chromatic will skip testing | ||||
|       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: | ||||
|         github-token: ${{ secrets.GITHUB_TOKEN }} | ||||
|         script: | | ||||
|   | ||||
| @@ -12,7 +12,7 @@ | ||||
|  | ||||
| --> | ||||
|  | ||||
| ## 13.x.x (unreleased) | ||||
| ## 13.12.2 | ||||
|  | ||||
| ### General | ||||
| - 投稿したコンテンツのAIによる学習を軽減するオプションを追加 | ||||
| @@ -21,7 +21,7 @@ | ||||
| - Fix: ブラーエフェクトを有効にしている状態で高負荷になる問題を修正 | ||||
|  | ||||
| ### Server | ||||
| - | ||||
| - センシティブワードの登録にAnd、正規表現が使用できるようになりました。 | ||||
|  | ||||
| ## 13.12.1 | ||||
|  | ||||
|   | ||||
| @@ -1038,6 +1038,8 @@ thisChannelArchived: "Dieser Kanal wurde archiviert." | ||||
| displayOfNote: "Anzeige von Notizen" | ||||
| initialAccountSetting: "Kontoeinrichtung" | ||||
| 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: | ||||
|   accountCreated: "Dein Konto wurde erfolgreich erstellt!" | ||||
|   letsStartAccountSetup: "Lass uns nun dein Konto einrichten." | ||||
|   | ||||
| @@ -1038,6 +1038,8 @@ thisChannelArchived: "This channel has been archived." | ||||
| displayOfNote: "Note display" | ||||
| initialAccountSetting: "Profile setup" | ||||
| 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: | ||||
|   accountCreated: "Your account was successfully created!" | ||||
|   letsStartAccountSetup: "For starters, let's set up your profile." | ||||
|   | ||||
| @@ -990,6 +990,7 @@ rolesAssignedToMe: "自分に割り当てられたロール" | ||||
| resetPasswordConfirm: "パスワードリセットしますか?" | ||||
| sensitiveWords: "センシティブワード" | ||||
| sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。" | ||||
| sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。" | ||||
| notesSearchNotAvailable: "ノート検索は利用できません。" | ||||
| license: "ライセンス" | ||||
| unfavoriteConfirm: "お気に入り解除しますか?" | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
| 	"name": "misskey", | ||||
| 	"version": "13.12.1", | ||||
| 	"version": "13.12.2", | ||||
| 	"codename": "nasubi", | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import * as mfm from 'mfm-js'; | ||||
| import { In, DataSource } from 'typeorm'; | ||||
| import * as Redis from 'ioredis'; | ||||
| import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; | ||||
| import RE2 from 're2'; | ||||
| import { extractMentions } from '@/misc/extract-mentions.js'; | ||||
| import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.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.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'; | ||||
| 			} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { | ||||
| 				data.visibility = 'home'; | ||||
| @@ -670,6 +672,31 @@ export class NoteCreateService implements OnApplicationShutdown { | ||||
| 		// Register to search database | ||||
| 		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 | ||||
| 	private incRenoteCount(renote: Note) { | ||||
|   | ||||
| @@ -541,6 +541,61 @@ describe('Note', () => { | ||||
|  | ||||
| 			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', () => { | ||||
|   | ||||
| @@ -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> | ||||
| <div | ||||
| 	v-adaptive-border | ||||
| 	class="novjtctn" | ||||
| 	:class="{ disabled, checked }" | ||||
| 	:class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]" | ||||
| 	:aria-checked="checked" | ||||
| 	:aria-disabled="disabled" | ||||
| 	@click="toggle" | ||||
| @@ -10,11 +9,12 @@ | ||||
| 	<input | ||||
| 		type="radio" | ||||
| 		:disabled="disabled" | ||||
| 		:class="$style.input" | ||||
| 	> | ||||
| 	<span class="button"> | ||||
| 	<span :class="$style.button"> | ||||
| 		<span></span> | ||||
| 	</span> | ||||
| 	<span class="label"><slot></slot></span> | ||||
| 	<span :class="$style.label"><slot></slot></span> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| @@ -39,8 +39,8 @@ function toggle(): void { | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .novjtctn { | ||||
| <style lang="scss" module> | ||||
| .root { | ||||
| 	position: relative; | ||||
| 	display: inline-block; | ||||
| 	text-align: left; | ||||
| @@ -53,17 +53,11 @@ function toggle(): void { | ||||
| 	border-radius: 6px; | ||||
| 	font-size: 90%; | ||||
| 	transition: all 0.2s; | ||||
|  | ||||
| 	> * { | ||||
| 		user-select: none; | ||||
| 	} | ||||
| 	user-select: none; | ||||
|  | ||||
| 	&.disabled { | ||||
| 		opacity: 0.6; | ||||
|  | ||||
| 		&, * { | ||||
| 			cursor: not-allowed !important; | ||||
| 		} | ||||
| 		cursor: not-allowed !important; | ||||
| 	} | ||||
|  | ||||
| 	&:hover { | ||||
| @@ -74,10 +68,7 @@ function toggle(): void { | ||||
| 		background-color: var(--accentedBg) !important; | ||||
| 		border-color: var(--accentedBg) !important; | ||||
| 		color: var(--accent); | ||||
|  | ||||
| 		&, * { | ||||
| 			cursor: default !important; | ||||
| 		} | ||||
| 		cursor: default !important; | ||||
|  | ||||
| 		> .button { | ||||
| 			border-color: var(--accent); | ||||
| @@ -89,44 +80,44 @@ function toggle(): void { | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 	> input { | ||||
| 		position: absolute; | ||||
| 		width: 0; | ||||
| 		height: 0; | ||||
| 		opacity: 0; | ||||
| 		margin: 0; | ||||
| 	} | ||||
| .input { | ||||
| 	position: absolute; | ||||
| 	width: 0; | ||||
| 	height: 0; | ||||
| 	opacity: 0; | ||||
| 	margin: 0; | ||||
| } | ||||
|  | ||||
| 	> .button { | ||||
| 		position: absolute; | ||||
| 		width: 14px; | ||||
| 		height: 14px; | ||||
| 		background: none; | ||||
| 		border: solid 2px var(--inputBorder); | ||||
| 		border-radius: 100%; | ||||
| 		transition: inherit; | ||||
| .button { | ||||
| 	position: absolute; | ||||
| 	width: 14px; | ||||
| 	height: 14px; | ||||
| 	background: none; | ||||
| 	border: solid 2px var(--inputBorder); | ||||
| 	border-radius: 100%; | ||||
| 	transition: inherit; | ||||
|  | ||||
| 		&:after { | ||||
| 			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; | ||||
| 	&:after { | ||||
| 		content: ''; | ||||
| 		display: block; | ||||
| 		line-height: 20px; | ||||
| 		cursor: pointer; | ||||
| 		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; | ||||
| 	line-height: 20px; | ||||
| 	cursor: pointer; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,21 +1,19 @@ | ||||
| <template> | ||||
| <div | ||||
| 	class="ziffeomt" | ||||
| 	:class="{ disabled, checked }" | ||||
| > | ||||
| <div :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"> | ||||
| 	<input | ||||
| 		ref="input" | ||||
| 		type="checkbox" | ||||
| 		:disabled="disabled" | ||||
| 		:class="$style.input" | ||||
| 		@keydown.enter="toggle" | ||||
| 	> | ||||
| 	<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" data-cy-switch-toggle @click.prevent="toggle"> | ||||
| 		<div class="knob"></div> | ||||
| 	<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" :class="$style.button" data-cy-switch-toggle @click.prevent="toggle"> | ||||
| 		<div :class="$style.knob"></div> | ||||
| 	</span> | ||||
| 	<span class="label"> | ||||
| 	<span :class="$style.body"> | ||||
| 		<!-- TODO: 無名slotの方は廃止 --> | ||||
| 		<span @click="toggle"><slot name="label"></slot><slot></slot></span> | ||||
| 		<p class="caption"><slot name="caption"></slot></p> | ||||
| 		<span :class="$style.label" @click="toggle"><slot name="label"></slot><slot></slot></span> | ||||
| 		<p :class="$style.caption"><slot name="caption"></slot></p> | ||||
| 	</span> | ||||
| </div> | ||||
| </template> | ||||
| @@ -45,52 +43,12 @@ const toggle = () => { | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .ziffeomt { | ||||
| <style lang="scss" module> | ||||
| .root { | ||||
| 	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: 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; | ||||
| 		} | ||||
| 	} | ||||
| 	user-select: none; | ||||
|  | ||||
| 	&:hover { | ||||
| 		> .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 { | ||||
| 		opacity: 0.6; | ||||
| 		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> | ||||
|   | ||||
| @@ -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)"/> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| export type Widget = { | ||||
| 	name: string; | ||||
| @@ -42,6 +43,7 @@ export type DefaultStoredWidget = { | ||||
| 	place: string | null; | ||||
| } & Widget; | ||||
| </script> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { defineAsyncComponent, ref } from 'vue'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|   | ||||
| @@ -27,7 +27,7 @@ | ||||
| 					 | ||||
| 					<MkTextarea v-model="sensitiveWords"> | ||||
| 						<template #label>{{ i18n.ts.sensitiveWords }}</template> | ||||
| 						<template #caption>{{ i18n.ts.sensitiveWordsDescription }}</template> | ||||
| 						<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template> | ||||
| 					</MkTextarea> | ||||
| 				</div> | ||||
| 			</FormSuspense> | ||||
|   | ||||
| @@ -28,9 +28,9 @@ | ||||
| 		<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> | ||||
|  | ||||
| 			<div class="vxjfqztj"> | ||||
| 				<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 tagsRemote" :key="'remote:' + tag.tag" :to="`/user-tags/${tag.tag}`">{{ tag.tag }}</MkA> | ||||
| 			<div> | ||||
| 				<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}`" style="margin-right: 16px;">{{ tag.tag }}</MkA> | ||||
| 			</div> | ||||
| 		</MkFoldableSection> | ||||
|  | ||||
| @@ -132,15 +132,3 @@ os.api('hashtags/list', { | ||||
| 	tagsRemote = tags; | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .vxjfqztj { | ||||
| 	> * { | ||||
| 		margin-right: 16px; | ||||
|  | ||||
| 		&.local { | ||||
| 			font-weight: bold; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 tamaina
					tamaina