Merge branch 'develop' into mkjs-n
This commit is contained in:
commit
7d8fa48d47
14 changed files with 217 additions and 298 deletions
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';
|
||||
|
@ -671,6 +673,31 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
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) {
|
||||
this.notesRepository.createQueryBuilder().update()
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue