Compare commits

..

76 Commits

Author SHA1 Message Date
syuilo
d0157b3bfd 13.0.0-rc.8 2023-01-15 16:55:08 +09:00
syuilo
7fc8d2e6d5 ロールでレートリミットを調整できるように
Resolve #9584
2023-01-15 16:52:12 +09:00
Masaya Suzuki
fb0f9711ba Update actions/github-script (#9588) 2023-01-15 16:14:06 +09:00
syuilo
92136272b0 enhance(client): show readable error when rate limit exceeded 2023-01-15 16:13:57 +09:00
Masaya Suzuki
e1159e9ef2 Update actions/checkout (#9587) 2023-01-15 16:03:18 +09:00
Masaya Suzuki
a2e61c6708 CI Publish Docker image (develop) をforkしたリポジトリでは実行しない (#9585) 2023-01-15 15:59:01 +09:00
Masaya Suzuki
726959911c Update actions/setup-node (#9586) 2023-01-15 15:58:10 +09:00
syuilo
d59914b959 tweak style 2023-01-15 14:18:45 +09:00
syuilo
07025caee9 refactor(client): use css modules 2023-01-15 14:03:28 +09:00
syuilo
1c0289e490 Fix #9582 2023-01-15 13:46:09 +09:00
syuilo
275fcd8bbc tweak style 2023-01-15 13:39:06 +09:00
Masaya Suzuki
0c0aa93668 GitHub Actionsとpackages/swをDependabotによるアップデート対象にする (#9572) 2023-01-15 12:12:28 +09:00
Masaya Suzuki
bfcd5ea440 Dockerで構築する場合のconfigファイルの雛形追加 (#9577) 2023-01-15 12:11:38 +09:00
Masaya Suzuki
3ff43cca02 frontendに@type/nodeをインストールする (#9571) 2023-01-15 11:55:12 +09:00
Nya Candy
6bd536c526 feat: update year in COPYING file (#9578)
Happy new year :D
2023-01-15 11:53:22 +09:00
syuilo
7738a36014 refactor(client): use css modules 2023-01-15 11:30:40 +09:00
syuilo
daddec8362 refactor(client): use css modules 2023-01-15 11:22:58 +09:00
syuilo
a3832d73fd 13.0.0-rc.7 2023-01-15 09:19:11 +09:00
syuilo
cedb4267ba update deps 2023-01-15 09:19:02 +09:00
syuilo
9c6629d582 refactor(client): use css modules 2023-01-15 09:14:17 +09:00
syuilo
4ee4e70ee0 fix(client): Custom emojis in Inline emoji autocomplete(MkAutoComplete) is way too big
Fix #9570
2023-01-15 09:09:44 +09:00
syuilo
bb7867351c typo 2023-01-15 09:03:07 +09:00
syuilo
fea7460930 13.0.0-rc.6 2023-01-15 08:36:51 +09:00
syuilo
1bf2bf1773 New Crowdin updates (#9562)
* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)
2023-01-15 08:36:30 +09:00
syuilo
3d668ad10d Update CHANGELOG.md 2023-01-15 08:36:21 +09:00
Takuya Yoshida
2801338a3c Fix MODULE_NOT_FOUND (#9563) 2023-01-15 08:35:36 +09:00
syuilo
b66f4ebba1 chore: use computed 2023-01-15 08:35:03 +09:00
Masaya Suzuki
9ee1b5f30a Fix widget test (#9565) 2023-01-15 08:34:26 +09:00
Masaya Suzuki
0f31a0548c Fix import (#9566) 2023-01-15 08:33:07 +09:00
syuilo
ffc29aa6f5 refactor(client): use css modules 2023-01-15 08:32:20 +09:00
syuilo
d23aa94b41 refactor(client): use css modules 2023-01-15 08:30:29 +09:00
syuilo
c1b6378951 refactor(client): use css modules 2023-01-15 08:07:11 +09:00
syuilo
bb5d2bda51 refactor(client): use css modules 2023-01-15 08:01:46 +09:00
syuilo
d075471b2d fix(client): fix custom emoji rendering 2023-01-15 07:53:15 +09:00
Masaya Suzuki
199d98bf79 DB・Redis -> Misskeyの順に起動する (#9569) 2023-01-15 06:24:56 +09:00
syuilo
3ae798d526 13.0.0-rc.5 2023-01-14 21:09:38 +09:00
Takuya Yoshida
e1bd61c70e Change docker user to non-root (#9560) 2023-01-14 21:09:11 +09:00
syuilo
0296f841c3 New Crowdin updates (#9552)
* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (English)
2023-01-14 21:09:00 +09:00
syuilo
bd1f4b8d98 refactor 2023-01-14 21:04:30 +09:00
syuilo
dc19f20153 refactor 2023-01-14 21:04:20 +09:00
syuilo
f5cd809f62 refactor(client): use css modules 2023-01-14 20:51:07 +09:00
syuilo
09d5a7806a Update style.scss 2023-01-14 20:39:47 +09:00
syuilo
4606f23ed8 refactor(client): use css modules 2023-01-14 20:31:48 +09:00
syuilo
8451e08aaa Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-01-14 20:21:06 +09:00
syuilo
2047449294 enhance(server): add rate limits for some endpoints 2023-01-14 20:21:03 +09:00
Masaya Suzuki
d61eee695f forkしたリポジトリからのPRではCI Destroy preview environment が動作しないようにする (#9559) 2023-01-14 20:10:09 +09:00
Masaya Suzuki
73b62797cd CI Deploy preview environmentの不要なトリガー削除 (#9558)
* CI Deploy preview environmentの不要なトリガー削除

* 不要な条件削除
2023-01-14 19:51:34 +09:00
Masaya Suzuki
170cfc6a0e E2Eテスト "first widget should be removed" 修正 (#9556)
* Fix e2e test "first widget should be removed"

* E2Eテスト用クラス追加

* empty commit
2023-01-14 19:25:20 +09:00
syuilo
6bf1d7e398 13.0.0-rc.4 2023-01-14 18:50:25 +09:00
syuilo
e46e7f5252 ノートをピン留めできる数を設定可能に
Resolve #9555
2023-01-14 18:04:56 +09:00
syuilo
5952f1ac24 Update roles.editor.vue 2023-01-14 17:49:02 +09:00
syuilo
a08369fe36 enhance(client): 分かりやすいエラーメッセージを表示するように 2023-01-14 17:46:45 +09:00
syuilo
6cb9612943 fix import 2023-01-14 17:40:51 +09:00
syuilo
76c049522e enhance: ユーザーリストおよびユーザーリスト内のユーザーの作成可能数を設定可能に 2023-01-14 17:38:16 +09:00
syuilo
c41879c542 refactor(client): use css modules 2023-01-14 17:23:49 +09:00
syuilo
99bdb11d24 Update CHANGELOG.md 2023-01-14 17:02:49 +09:00
syuilo
c2009acb2d enhance: クリップおよびクリップ内のノートの作成可能数を設定可能に 2023-01-14 16:14:24 +09:00
syuilo
46d2a8726e fix missing import 2023-01-14 16:04:13 +09:00
syuilo
7df3ca7388 enhance(server): add rate limits for some endpoints 2023-01-14 15:59:15 +09:00
syuilo
51b8d4ae3e 13.0.0-rc.3 2023-01-14 15:08:30 +09:00
syuilo
ab1124abba refactor(client): use css modules 2023-01-14 15:05:23 +09:00
syuilo
3db84a2e8f refactor(client): use css modules 2023-01-14 15:02:14 +09:00
syuilo
9a78bbf0f1 refactor(client): use css modules 2023-01-14 15:01:28 +09:00
syuilo
efbec444e8 refactor(client): use css modules 2023-01-14 14:57:48 +09:00
syuilo
2f06f2a6da New Crowdin updates (#9544)
* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Traditional)
2023-01-14 14:20:09 +09:00
tamaina
b8da51e08c fix css module syntax error 2023-01-14 04:52:42 +00:00
syuilo
af6a578fa6 13.0.0-rc.2 2023-01-14 13:49:13 +09:00
tamaina
73d735a1f7 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-01-14 04:48:17 +00:00
tamaina
b8b1899a9f chore: fix ref name (pages/timeline.vue) 2023-01-14 04:47:59 +00:00
syuilo
d52f0617a1 fix(server): ドライブ容量超過時のエラーが適切にレスポンスされない問題を修正
Fix #9550
2023-01-14 13:41:53 +09:00
syuilo
c730973294 多分 fix #9551 2023-01-14 13:36:18 +09:00
syuilo
2c2e064871 refactor(client): use css modules 2023-01-14 13:29:41 +09:00
syuilo
e3c39d4b52 refactor(client): use css modules 2023-01-14 12:45:20 +09:00
syuilo
5da74897ae refactor(client): use css modules 2023-01-14 12:43:54 +09:00
syuilo
4b1009b34e refactor(client): use css modules 2023-01-14 12:30:32 +09:00
syuilo
203a7ad073 refactor(client): use css modules 2023-01-14 12:15:02 +09:00
124 changed files with 2696 additions and 1977 deletions

151
.config/docker_example.yml Normal file
View File

@@ -0,0 +1,151 @@
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Misskey configuration
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ┌─────┐
#───┘ URL └─────────────────────────────────────────────────────
# Final accessible URL seen by a user.
url: https://example.tld/
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
# URL SETTINGS AFTER THAT!
# ┌───────────────────────┐
#───┘ Port and TLS settings └───────────────────────────────────
#
# Misskey requires a reverse proxy to support HTTPS connections.
#
# +----- https://example.tld/ ------------+
# +------+ |+-------------+ +----------------+|
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
# +------+ |+-------------+ +----------------+|
# +---------------------------------------+
#
# You need to set up a reverse proxy. (e.g. nginx)
# An encrypted connection with HTTPS is highly recommended
# because tokens may be transferred in GET requests.
# The port that your Misskey server should listen on.
port: 3000
# ┌──────────────────────────┐
#───┘ PostgreSQL configuration └────────────────────────────────
db:
host: db
port: 5432
# Database name
db: misskey
# Auth
user: example-misskey-user
pass: example-misskey-pass
# Whether disable Caching queries
#disableCache: true
# Extra Connection options
#extra:
# ssl: true
# ┌─────────────────────┐
#───┘ Redis configuration └─────────────────────────────────────
redis:
host: redis
port: 6379
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
#pass: example-pass
#prefix: example-prefix
#db: 1
# ┌─────────────────────────────┐
#───┘ Elasticsearch configuration └─────────────────────────────
#elasticsearch:
# host: localhost
# port: 9200
# ssl: false
# user:
# pass:
# ┌───────────────┐
#───┘ ID generation └───────────────────────────────────────────
# You can select the ID generation method.
# You don't usually need to change this setting, but you can
# change it according to your preferences.
# Available methods:
# aid ... Short, Millisecond accuracy
# meid ... Similar to ObjectID, Millisecond accuracy
# ulid ... Millisecond accuracy
# objectid ... This is left for backward compatibility
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
# ID SETTINGS AFTER THAT!
id: 'aid'
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
# Whether disable HSTS
#disableHsts: true
# Number of worker processes
#clusterLimit: 1
# Job concurrency per worker
# deliverJobConcurrency: 128
# inboxJobConcurrency: 16
# Job rate limiter
# deliverJobPerSec: 128
# inboxJobPerSec: 16
# Job attempts
# deliverJobMaxAttempts: 12
# inboxJobMaxAttempts: 8
# IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4
# Syslog option
#syslog:
# host: localhost
# port: 514
# Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128
proxyBypassHosts:
- api.deepl.com
- api-free.deepl.com
- www.recaptcha.net
- hcaptcha.com
- challenges.cloudflare.com
# Proxy for SMTP/SMTPS
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT
#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
# Media Proxy
#mediaProxy: https://example.com/proxy
# Proxy remote files (default: false)
#proxyRemoteFiles: true
# Sign to ActivityPub GET request (default: true)
signToActivityPubGet: true
#allowedPrivateNetworks: [
# '127.0.0.1/32'
#]
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View File

@@ -5,6 +5,11 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 0
- package-ecosystem: npm - package-ecosystem: npm
directory: "/" directory: "/"
schedule: schedule:
@@ -20,3 +25,8 @@ updates:
schedule: schedule:
interval: daily interval: daily
open-pull-requests-limit: 0 open-pull-requests-limit: 0
- package-ecosystem: npm
directory: "/packages/sw"
schedule:
interval: daily
open-pull-requests-limit: 0

View File

@@ -10,10 +10,10 @@ jobs:
push_to_registry: push_to_registry:
name: Push Docker image to Docker Hub name: Push Docker image to Docker Hub
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'misskey-dev/misskey'
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v2 uses: actions/checkout@v3.3.0
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v3 uses: docker/metadata-action@v3

View File

@@ -12,7 +12,7 @@ jobs:
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v2 uses: actions/checkout@v3.3.0
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v3 uses: docker/metadata-action@v3

View File

@@ -11,11 +11,11 @@ jobs:
yarn_install: yarn_install:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3.3.0
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: actions/setup-node@v3.2.0 - uses: actions/setup-node@v3.6.0
with: with:
node-version: 18.x node-version: 18.x
cache: 'yarn' cache: 'yarn'
@@ -33,11 +33,11 @@ jobs:
- frontend - frontend
- sw - sw
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3.3.0
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: actions/setup-node@v3.2.0 - uses: actions/setup-node@v3.6.0
with: with:
node-version: 18.x node-version: 18.x
cache: 'yarn' cache: 'yarn'

View File

@@ -1,7 +1,5 @@
# Run secret-dependent integration tests only after /deploy approval # Run secret-dependent integration tests only after /deploy approval
on: on:
pull_request:
types: [opened, reopened, synchronize]
repository_dispatch: repository_dispatch:
types: [deploy-command] types: [deploy-command]
@@ -12,11 +10,10 @@ jobs:
deploy-preview-environment: deploy-preview-environment:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: if:
github.event_name == 'repository_dispatch' &&
github.event.client_payload.slash_command.sha != '' && github.event.client_payload.slash_command.sha != '' &&
contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha) contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha)
steps: steps:
- uses: actions/github-script@v5 - uses: actions/github-script@v6.3.3
id: check-id id: check-id
env: env:
number: ${{ github.event.client_payload.pull_request.number }} number: ${{ github.event.client_payload.pull_request.number }}
@@ -40,7 +37,7 @@ jobs:
return check[0].id; return check[0].id;
- uses: actions/github-script@v5 - uses: actions/github-script@v6.3.3
env: env:
check_id: ${{ steps.check-id.outputs.result }} check_id: ${{ steps.check-id.outputs.result }}
details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }} details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }}
@@ -56,7 +53,7 @@ jobs:
# Check out merge commit # Check out merge commit
- name: Fork based /deploy checkout - name: Fork based /deploy checkout
uses: actions/checkout@v2 uses: actions/checkout@v3.3.0
with: with:
ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge' ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge'
@@ -75,7 +72,7 @@ jobs:
timeout: 15m timeout: 15m
# Update check run called "integration-fork" # Update check run called "integration-fork"
- uses: actions/github-script@v5 - uses: actions/github-script@v6.3.3
id: update-check-run id: update-check-run
if: ${{ always() }} if: ${{ always() }}
env: env:

View File

@@ -9,6 +9,7 @@ name: Destroy preview environment
jobs: jobs:
destroy-preview-environment: destroy-preview-environment:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == github.event.pull_request.head.repo.full_name
steps: steps:
- name: Context - name: Context
uses: okteto/context@latest uses: okteto/context@latest

View File

@@ -30,11 +30,11 @@ jobs:
- 56312:6379 - 56312:6379
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3.3.0
with: with:
submodules: true submodules: true
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.2.0 uses: actions/setup-node@v3.6.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'yarn' cache: 'yarn'
@@ -77,7 +77,7 @@ jobs:
- 56312:6379 - 56312:6379
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3.3.0
with: with:
submodules: true submodules: true
# https://github.com/cypress-io/cypress-docker-images/issues/150 # https://github.com/cypress-io/cypress-docker-images/issues/150
@@ -87,7 +87,7 @@ jobs:
#- uses: browser-actions/setup-firefox@latest #- uses: browser-actions/setup-firefox@latest
# if: ${{ matrix.browser == 'firefox' }} # if: ${{ matrix.browser == 'firefox' }}
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.2.0 uses: actions/setup-node@v3.6.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'yarn' cache: 'yarn'

1
.gitignore vendored
View File

@@ -30,6 +30,7 @@ coverage
# config # config
/.config/* /.config/*
!/.config/example.yml !/.config/example.yml
!/.config/docker_example.yml
!/.config/docker_example.env !/.config/docker_example.env
# misskey # misskey

View File

@@ -52,6 +52,7 @@ You should also include the user name that made the change.
- 0.12.x未満のプラグインは読み込むことはできません - 0.12.x未満のプラグインは読み込むことはできません
- iOS15以下のデバイスはサポートされなくなりました - iOS15以下のデバイスはサポートされなくなりました
- Firefox110以下はサポートされなくなりました - Firefox110以下はサポートされなくなりました
- 109でもContainerQueriesのフラグを有効にする事で問題なく使用できます
#### For app developers #### For app developers
- API: metaのレスポンスに`emojis`プロパティが含まれなくなりました - API: metaのレスポンスに`emojis`プロパティが含まれなくなりました
@@ -73,16 +74,21 @@ You should also include the user name that made the change.
- Push notification of Antenna note @tamaina - Push notification of Antenna note @tamaina
- AVIF support @tamaina - AVIF support @tamaina
- Add Cloudflare Turnstile CAPTCHA support @CyberRex0 - Add Cloudflare Turnstile CAPTCHA support @CyberRex0
- レートリミットをユーザーごとに調整可能に @syuilo
- 非モデレーターでも、権限を持つロールをアサインされたユーザーはインスタンスの招待コードを発行できるように @syuilo - 非モデレーターでも、権限を持つロールをアサインされたユーザーはインスタンスの招待コードを発行できるように @syuilo
- 非モデレーターでも、権限を持つロールをアサインされたユーザーはカスタム絵文字の追加、編集、削除を行えるように @syuilo - 非モデレーターでも、権限を持つロールをアサインされたユーザーはカスタム絵文字の追加、編集、削除を行えるように @syuilo
- クリップおよびクリップ内のノートの作成可能数を設定可能に @syuilo
- ユーザーリストおよびユーザーリスト内のユーザーの作成可能数を設定可能に @syuilo
- ハードワードミュートの最大文字数を設定可能に @syuilo - ハードワードミュートの最大文字数を設定可能に @syuilo
- Webhookの作成可能数を設定可能に @syuilo - Webhookの作成可能数を設定可能に @syuilo
- ノートをピン留めできる数を設定可能に @syuilo
- Server: signToActivityPubGet is set to true by default @syuilo - Server: signToActivityPubGet is set to true by default @syuilo
- Server: improve syslog performance @syuilo - Server: improve syslog performance @syuilo
- Server: Use undici instead of node-fetch and got @tamaina - Server: Use undici instead of node-fetch and got @tamaina
- Server: Judge instance block by endsWith @tamaina - Server: Judge instance block by endsWith @tamaina
- Server: improve note scoring for featured notes @CyberRex0 - Server: improve note scoring for featured notes @CyberRex0
- Server: アンケート選択肢の文字数制限を緩和 @syuilo - Server: アンケート選択肢の文字数制限を緩和 @syuilo
- Server: add rate limits for some endpoints @syuilo
- Server: improve stats api performance @syuilo - Server: improve stats api performance @syuilo
- Server: improve nodeinfo performance @syuilo - Server: improve nodeinfo performance @syuilo
- Server: delete outdated notifications regularly to improve db performance @syuilo - Server: delete outdated notifications regularly to improve db performance @syuilo
@@ -94,6 +100,7 @@ You should also include the user name that made the change.
- Client: Add link to user RSS feed in profile menu @ssmucny - Client: Add link to user RSS feed in profile menu @ssmucny
- Client: Compress non-animated PNG files @saschanaz - Client: Compress non-animated PNG files @saschanaz
- Client: YouTube window player @sim1222 - Client: YouTube window player @sim1222
- Client: show readable error when rate limit exceeded @syuilo
- Client: enhance dashboard of control panel @syuilo - Client: enhance dashboard of control panel @syuilo
- Client: Vite is upgraded to v4 @syuilo, @tamaina - Client: Vite is upgraded to v4 @syuilo, @tamaina
- Client: HMR is available while yarn dev @tamaina - Client: HMR is available while yarn dev @tamaina
@@ -120,6 +127,7 @@ You should also include the user name that made the change.
- Client: clicker game @syuilo - Client: clicker game @syuilo
### Bugfixes ### Bugfixes
- Server: Fix @tensorflow/tfjs-core's MODULE_NOT_FOUND error @ikuradon
- Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468 - Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468
- Server: Bug fix for Pinned Users lookup on instance @squidicuzz - Server: Bug fix for Pinned Users lookup on instance @squidicuzz
- Server: Fix peers API returning suspended instances @ineffyble - Server: Fix peers API returning suspended instances @ineffyble
@@ -133,6 +141,7 @@ You should also include the user name that made the change.
- Server: 非公開のクリップのURLでOGPレンダリングされる問題を修正 @syuilo - Server: 非公開のクリップのURLでOGPレンダリングされる問題を修正 @syuilo
- Server: アンテナタイムライン(ストリーミング)が、フォローしていないユーザーの鍵投稿も拾ってしまう @syuilo - Server: アンテナタイムライン(ストリーミング)が、フォローしていないユーザーの鍵投稿も拾ってしまう @syuilo
- Server: follow request list api pagination @sim1222 - Server: follow request list api pagination @sim1222
- Server: ドライブ容量超過時のエラーが適切にレスポンスされない問題を修正 @syuilo
- Client: パスワードマネージャーなどでユーザー名がオートコンプリートされない問題を修正 @massongit - Client: パスワードマネージャーなどでユーザー名がオートコンプリートされない問題を修正 @massongit
- Client: 日付形式の文字列などがカスタム絵文字として表示されるのを修正 @syuilo - Client: 日付形式の文字列などがカスタム絵文字として表示されるのを修正 @syuilo
- Client: case insensitive emoji search @saschanaz - Client: case insensitive emoji search @saschanaz

View File

@@ -1,5 +1,5 @@
Unless otherwise stated this repository is Unless otherwise stated this repository is
Copyright © 2014-2022 syuilo and contributers Copyright © 2014-2023 syuilo and contributers
And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE. And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE.

View File

@@ -1,4 +1,6 @@
FROM node:18.13.0-bullseye AS builder ARG NODE_VERSION=18.13.0-bullseye
FROM node:${NODE_VERSION} AS builder
ARG NODE_ENV=production ARG NODE_ENV=production
@@ -22,23 +24,29 @@ COPY . ./
RUN git submodule update --init RUN git submodule update --init
RUN yarn build RUN yarn build
FROM node:18.13.0-bullseye-slim AS runner FROM node:${NODE_VERSION}-slim AS runner
WORKDIR /misskey ARG UID="991"
ARG GID="991"
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends \
ffmpeg tini \ ffmpeg tini \
&& apt-get -y clean \ && apt-get -y clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/* \
&& groupadd -g "${GID}" misskey \
&& useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey
COPY --from=builder /misskey/.yarn/install-state.gz ./.yarn/install-state.gz USER misskey
COPY --from=builder /misskey/node_modules ./node_modules WORKDIR /misskey
COPY --from=builder /misskey/built ./built
COPY --from=builder /misskey/packages/backend/node_modules ./packages/backend/node_modules COPY --chown=misskey:misskey --from=builder /misskey/.yarn/install-state.gz ./.yarn/install-state.gz
COPY --from=builder /misskey/packages/backend/built ./packages/backend/built COPY --chown=misskey:misskey --from=builder /misskey/node_modules ./node_modules
COPY --from=builder /misskey/packages/frontend/node_modules ./packages/frontend/node_modules COPY --chown=misskey:misskey --from=builder /misskey/built ./built
COPY . ./ COPY --chown=misskey:misskey --from=builder /misskey/packages/backend/node_modules ./packages/backend/node_modules
COPY --chown=misskey:misskey --from=builder /misskey/packages/backend/built ./packages/backend/built
COPY --chown=misskey:misskey --from=builder /misskey/packages/frontend/node_modules ./packages/frontend/node_modules
COPY --chown=misskey:misskey . ./
ENV NODE_ENV=production ENV NODE_ENV=production
ENTRYPOINT ["/usr/bin/tini", "--"] ENTRYPOINT ["/usr/bin/tini", "--"]

View File

@@ -29,15 +29,15 @@ describe('After user signed in', () => {
it('first widget should be removed', () => { it('first widget should be removed', () => {
cy.get('.mk-widget-edit').click(); cy.get('.mk-widget-edit').click();
cy.get('.customize-container:first-child .remove._button').click(); cy.get('.data-cy-customize-container:first-child .data-cy-customize-container-remove._button').click();
cy.get('.customize-container').should('have.length', 2); cy.get('.data-cy-customize-container').should('have.length', 2);
}); });
function buildWidgetTest(widgetName) { function buildWidgetTest(widgetName) {
it(`${widgetName} widget should get added`, () => { it(`${widgetName} widget should get added`, () => {
cy.get('.mk-widget-edit').click(); cy.get('.mk-widget-edit').click();
cy.get('.mk-widget-select select').select(widgetName, { force: true }); cy.get('.mk-widget-select select').select(widgetName, { force: true });
cy.get('.bg._modalBg.transparent').click({ multiple: true, force: true }); cy.get('.data-cy-bg._modalBg.data-cy-transparent').click({ multiple: true, force: true });
cy.get('.mk-widget-add').click({ force: true }); cy.get('.mk-widget-add').click({ force: true });
cy.get(`.mkw-${widgetName}`).should('exist'); cy.get(`.mkw-${widgetName}`).should('exist');
}); });

View File

@@ -8,6 +8,11 @@ services:
- db - db
- redis - redis
# - es # - es
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
ports: ports:
- "3000:3000" - "3000:3000"
networks: networks:
@@ -24,6 +29,10 @@ services:
- internal_network - internal_network
volumes: volumes:
- ./redis:/data - ./redis:/data
healthcheck:
test: "redis-cli ping"
interval: 5s
retries: 20
db: db:
restart: always restart: always
@@ -34,6 +43,10 @@ services:
- .config/docker.env - .config/docker.env
volumes: volumes:
- ./db:/var/lib/postgresql/data - ./db:/var/lib/postgresql/data
healthcheck:
test: "pg_isready"
interval: 5s
retries: 20
# es: # es:
# restart: always # restart: always

View File

@@ -931,12 +931,21 @@ undefined: "Undefiniert"
assign: "Zuweisen" assign: "Zuweisen"
unassign: "Entfernen" unassign: "Entfernen"
color: "Farbe" color: "Farbe"
manageCustomEmojis: "Benutzerdefinierte Emojis verwalten"
youCannotCreateAnymore: "Du hast das Erstellungslimit erreicht."
_role: _role:
new: "Rolle erstellen" new: "Rolle erstellen"
edit: "Rolle bearbeiten" edit: "Rolle bearbeiten"
name: "Rollenname" name: "Rollenname"
description: "Rollenbeschreibung" description: "Rollenbeschreibung"
permission: "Rollenberechtigungen" permission: "Rollenberechtigungen"
descriptionOfPermission: "<b>Moderatoren</b> können grundlegende Verwaltungsaufgaben erledigen.\n<b>Administratoren</b> können alle Einstellungen der Instanz verwalten."
assignTarget: "Zuweisungsart"
descriptionOfAssignTarget: "<b>Manuell</b> bedeutet, dass die Liste der Benutzer einer Rolle manuell verwaltet wird.\n<b>Konditionell</b> bedeutet, dass die Liste der Benutzer einer Rolle durch eine Bedingung automatisch verwaltet wird."
manual: "Manuell"
conditional: "Konditional"
condition: "Bedingung"
isConditionalRole: "Dies ist eine konditionale Rolle."
isPublic: "Öffentliche Rolle" isPublic: "Öffentliche Rolle"
descriptionOfIsPublic: "Ist dies aktiviert, so kann jeder die Liste der Benutzer, die dieser Rolle zugewiesen sind, einsehen. Zusätzlich wird diese Rolle im Profil zugewiesener Benutzer angezeigt." descriptionOfIsPublic: "Ist dies aktiviert, so kann jeder die Liste der Benutzer, die dieser Rolle zugewiesen sind, einsehen. Zusätzlich wird diese Rolle im Profil zugewiesener Benutzer angezeigt."
options: "Optionen" options: "Optionen"
@@ -949,8 +958,29 @@ _role:
gtlAvailable: "Kann auf die globale Chronik zugreifen" gtlAvailable: "Kann auf die globale Chronik zugreifen"
ltlAvailable: "Kann auf die lokale Chronik zugreifen" ltlAvailable: "Kann auf die lokale Chronik zugreifen"
canPublicNote: "Kann öffentliche Notizen erstellen" canPublicNote: "Kann öffentliche Notizen erstellen"
canInvite: "Einladungscodes für diese Instanz erstellen"
canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten"
driveCapacity: "Drive-Kapazität" driveCapacity: "Drive-Kapazität"
pinMax: "Maximale Anzahl an angehefteten Notizen"
antennaMax: "Maximale Anzahl an Antennen" antennaMax: "Maximale Anzahl an Antennen"
wordMuteMax: "Maximale Zeichenlänge für Wortstummschaltungen"
webhookMax: "Maximale Anzahl an Webhooks"
clipMax: "Maximale Anzahl an Clips"
noteEachClipsMax: "Maximale Anzahl an Notizen innerhalb eines Clips"
userListMax: "Maximale Anzahl an Benutzern in einer Benutzerliste"
userEachUserListsMax: "Maximale Anzahl an Benutzerlisten"
_condition:
isLocal: "Lokaler Benutzer"
isRemote: "Benutzer fremder Instanz"
createdLessThan: "Kontoerstellung liegt weniger als X zurück"
createdMoreThan: "Kontoerstellung liegt mehr als X zurück"
followersLessThanOrEq: "Hat X oder weniger Follower"
followersMoreThanOrEq: "Hat X oder mehr Follower"
followingLessThanOrEq: "Folgt X oder weniger Benutzern"
followingMoreThanOrEq: "Folgt X oder mehr Benutzern"
and: "UND-Bedingung"
or: "ODER-Bedingung"
not: "NICHT-Bedingung"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht." description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht."
sensitivity: "Erkennungssensitivität" sensitivity: "Erkennungssensitivität"

View File

@@ -931,12 +931,21 @@ undefined: "Undefined"
assign: "Assign" assign: "Assign"
unassign: "Unassign" unassign: "Unassign"
color: "Color" color: "Color"
manageCustomEmojis: "Manage Custom Emojis"
youCannotCreateAnymore: "You've hit the creation limit."
_role: _role:
new: "New role" new: "New role"
edit: "Edit role" edit: "Edit role"
name: "Role name" name: "Role name"
description: "Role description" description: "Role description"
permission: "Role permissions" permission: "Role permissions"
descriptionOfPermission: "<b>Moderators</b> can perform basic moderation operations.\n<b>Administrators</b> can change all settings of the instance."
assignTarget: "Assignment type"
descriptionOfAssignTarget: "<b>Manual</b> to manually change who is part of this role and who is not.\n<b>Conditional</b> to have users be automatically assigned and removed from this role based on a condition."
manual: "Manual"
conditional: "Conditional"
condition: "Condition"
isConditionalRole: "This is a conditional role."
isPublic: "Public role" isPublic: "Public role"
descriptionOfIsPublic: "Anyone will be able to view a list of users assigned to this role. In addition, this role will be displayed in the profiles of assigned users." descriptionOfIsPublic: "Anyone will be able to view a list of users assigned to this role. In addition, this role will be displayed in the profiles of assigned users."
options: "Role options" options: "Role options"
@@ -949,8 +958,29 @@ _role:
gtlAvailable: "Viewing the global timeline" gtlAvailable: "Viewing the global timeline"
ltlAvailable: "Viewing the local timeline" ltlAvailable: "Viewing the local timeline"
canPublicNote: "Can send public notes" canPublicNote: "Can send public notes"
canInvite: "Create instance invite codes"
canManageCustomEmojis: "Manage Custom Emojis"
driveCapacity: "Drive capacity" driveCapacity: "Drive capacity"
pinMax: "Maximum number of pinned notes"
antennaMax: "Maximum number of antennas" antennaMax: "Maximum number of antennas"
wordMuteMax: "Maximum number of characters allowed in word mutes"
webhookMax: "Maximum number of Webhooks"
clipMax: "Maximum number of Clips"
noteEachClipsMax: "Maximum number of notes within a clip"
userListMax: "Maximum number of user lists"
userEachUserListsMax: "Maximum number of users within a user list"
_condition:
isLocal: "Local user"
isRemote: "Remote user"
createdLessThan: "Less than X has passed since account creation"
createdMoreThan: "More than X has passed since account creation"
followersLessThanOrEq: "Has X or fewer followers"
followersMoreThanOrEq: "Has X or more followers"
followingLessThanOrEq: "Follows X or fewer accounts"
followingMoreThanOrEq: "Follows X or more accounts"
and: "AND-Condition"
or: "OR-Condition"
not: "NOT-Condition"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server." description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
sensitivity: "Detection sensitivity" sensitivity: "Detection sensitivity"

View File

@@ -924,7 +924,51 @@ neverShow: "Non mostrare più"
remindMeLater: "Rimanda" remindMeLater: "Rimanda"
didYouLikeMisskey: "Ti piace Misskey?" didYouLikeMisskey: "Ti piace Misskey?"
pleaseDonate: "Misskey è il software libero utilizzato su {host}. Offrendo una donazione è più facile continuare a svilupparlo!" pleaseDonate: "Misskey è il software libero utilizzato su {host}. Offrendo una donazione è più facile continuare a svilupparlo!"
roles: "Ruoli"
role: "Ruolo"
normalUser: "Profilo standard"
undefined: "Indefinito"
assign: "Assegna"
unassign: "Disassegna"
color: "Colore" color: "Colore"
manageCustomEmojis: "Gestisci le emoji personalizzate"
_role:
new: "Nuovo ruolo"
edit: "Modifica ruolo"
name: "Nome del ruolo"
description: "Descrizione del ruolo"
permission: "Permessi del ruolo"
descriptionOfPermission: "<b>Moderatori</b> possono svolgere le attività di moderazione basilari.\n<b>Amministratori</b> possono modificare la configurazione dell'istanza."
assignTarget: "Assegna il target"
descriptionOfAssignTarget: "<b>Manuale</b> per assegnare manualmente questo ruolo ai profili.\n<b>Condizionale</b> per assegnare o rimuovere automaticamente questo ruolo ai profili, secondo determinate condizioni."
manual: "Manuale"
conditional: "Condizionale"
condition: "Condizioni"
isConditionalRole: "Questo è un ruolo condizionato"
isPublic: "Ruolo pubblico"
descriptionOfIsPublic: "La lista di profili assegnati a questo ruolo è visibile a chiunque. Inoltre, il ruolo verrà mostrato nei relativi profili."
options: "Opzioni del ruolo"
baseRole: "Ruolo di base"
useBaseValue: "Eredita dal ruolo base"
chooseRoleToAssign: "Seleziona il ruolo da assegnare"
canEditMembersByModerator: "Consenti ai Moderatori di modificare i membri di questo ruolo"
descriptionOfCanEditMembersByModerator: "Se attivo, anche i Moderatori potranno assegnare o togliere questo ruolo. Altrimenti, se disattivo, potranno solo gli Amministratori."
_options:
gtlAvailable: "Disponibilità della Timeline Federata"
ltlAvailable: "Disponibilità della Timeline Locale"
canPublicNote: "Può scrivere Note con Visibilità Pubblica"
canInvite: "Genera codici di invito all'istanza"
canManageCustomEmojis: "Gestire le emoji personalizzate"
driveCapacity: "Capienza del Drive"
antennaMax: "Numero massimo di Antenne"
_condition:
isLocal: "Profilo locale"
isRemote: "Profilo remoto"
createdLessThan: "Creato meno di"
createdMoreThan: "Creato più di"
and: "E"
or: "O"
not: "NON"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "L'apprendimento automatico può essere utilizzato per individuare automaticamente i media sensibili da moderare. Il carico del server aumenta leggermente." description: "L'apprendimento automatico può essere utilizzato per individuare automaticamente i media sensibili da moderare. Il carico del server aumenta leggermente."
sensitivity: "Sensibilità di rilevamento" sensitivity: "Sensibilità di rilevamento"

View File

@@ -932,6 +932,9 @@ assign: "アサイン"
unassign: "アサインを解除" unassign: "アサインを解除"
color: "色" color: "色"
manageCustomEmojis: "カスタム絵文字の管理" manageCustomEmojis: "カスタム絵文字の管理"
youCannotCreateAnymore: "これ以上作成することはできません。"
cannotPerformTemporary: "一時的に利用できません"
cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。"
_role: _role:
new: "ロールの作成" new: "ロールの作成"
@@ -961,9 +964,16 @@ _role:
canInvite: "インスタンス招待コードの発行" canInvite: "インスタンス招待コードの発行"
canManageCustomEmojis: "カスタム絵文字の管理" canManageCustomEmojis: "カスタム絵文字の管理"
driveCapacity: "ドライブ容量" driveCapacity: "ドライブ容量"
pinMax: "ノートのピン留めの最大数"
antennaMax: "アンテナの作成可能数" antennaMax: "アンテナの作成可能数"
wordMuteMax: "ワードミュートの最大文字数" wordMuteMax: "ワードミュートの最大文字数"
webhookMax: "Webhookの作成可能数" webhookMax: "Webhookの作成可能数"
clipMax: "クリップの作成可能数"
noteEachClipsMax: "クリップ内のノートの最大数"
userListMax: "ユーザーリストの作成可能数"
userEachUserListsMax: "ユーザーリスト内のユーザーの最大数"
rateLimitFactor: "レートリミット"
descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。"
_condition: _condition:
isLocal: "ローカルユーザー" isLocal: "ローカルユーザー"
isRemote: "リモートユーザー" isRemote: "リモートユーザー"

View File

@@ -907,7 +907,7 @@ subscribePushNotification: "푸시 알림 켜기"
unsubscribePushNotification: "푸시 알림 끄기" unsubscribePushNotification: "푸시 알림 끄기"
pushNotificationAlreadySubscribed: "푸시 알림이 이미 켜져 있습니다" pushNotificationAlreadySubscribed: "푸시 알림이 이미 켜져 있습니다"
pushNotificationNotSupported: "브라우저나 인스턴스에서 푸시 알림이 지원되지 않습니다" pushNotificationNotSupported: "브라우저나 인스턴스에서 푸시 알림이 지원되지 않습니다"
sendPushNotificationReadMessage: "푸시 알림이 메시지를 읽으면 푸시 알림을 삭제합니다" sendPushNotificationReadMessage: "푸시 알림이 메시지를 읽은 뒤 푸시 알림을 삭제"
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」이라는 알림이 잠깐 표시됩니다. 기기의 전력 소비량이 증가할 수 있습니다." sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」이라는 알림이 잠깐 표시됩니다. 기기의 전력 소비량이 증가할 수 있습니다."
windowMaximize: "최대화" windowMaximize: "최대화"
windowRestore: "복구" windowRestore: "복구"
@@ -926,15 +926,26 @@ didYouLikeMisskey: "Misskey가 마음에 드시나요?"
pleaseDonate: "{host}은(는) 무료 소프트웨어 Misskey를 사용합니다. 후원을 통해 저희의 개발이 이어질 수 있게 도와주세요!" pleaseDonate: "{host}은(는) 무료 소프트웨어 Misskey를 사용합니다. 후원을 통해 저희의 개발이 이어질 수 있게 도와주세요!"
roles: "역할" roles: "역할"
role: "역할" role: "역할"
normalUser: "일반 사용자"
undefined: "정의되지 않음" undefined: "정의되지 않음"
assign: "할당" assign: "할당"
unassign: "할당 취소" unassign: "할당 취소"
color: "색" color: "색"
manageCustomEmojis: "커스텀 이모지 관리"
youCannotCreateAnymore: "더 이상 생성할 수 없습니다."
_role: _role:
new: "새 역할 생성" new: "새 역할 생성"
edit: "역할 수정" edit: "역할 수정"
name: "역할 이름" name: "역할 이름"
description: "역할 설명" description: "역할 설명"
permission: "역할의 권한"
descriptionOfPermission: "<b>모더레이터</b>는 기본적인 중재와 관련된 작업을 수행할 수 있습니다.\n<b>관리자</b>는 인스턴스의 모든 설정을 변경할 수 있습니다."
assignTarget: "할당 대상"
descriptionOfAssignTarget: "<b>수동</b>을 선택하면 누가 이 역할에 포함되는지를 수동으로 관리할 수 있습니다.\n<b>조건부</b>를 선택하면 조건을 설정해 일치하는 사용자를 자동으로 포함되게 할 수 있습니다."
manual: "수동"
conditional: "조건부"
condition: "조건"
isConditionalRole: "조건부 역할입니다."
isPublic: "공개 역할" isPublic: "공개 역할"
descriptionOfIsPublic: "역할에 할당된 사용자를 누구나 볼 수 있습니다. 또한 사용자 프로필에 이 역할이 표시됩니다." descriptionOfIsPublic: "역할에 할당된 사용자를 누구나 볼 수 있습니다. 또한 사용자 프로필에 이 역할이 표시됩니다."
options: "옵션" options: "옵션"
@@ -947,8 +958,29 @@ _role:
gtlAvailable: "글로벌 타임라인 보이기" gtlAvailable: "글로벌 타임라인 보이기"
ltlAvailable: "로컬 타임라인 보이기" ltlAvailable: "로컬 타임라인 보이기"
canPublicNote: "공개 노트 허용" canPublicNote: "공개 노트 허용"
canInvite: "인스턴스 초대 코드 발행"
canManageCustomEmojis: "커스텀 이모지 관리"
driveCapacity: "드라이브 용량" driveCapacity: "드라이브 용량"
pinMax: "고정할 수 있는 노트 수"
antennaMax: "최대 안테나 생성 허용 수" antennaMax: "최대 안테나 생성 허용 수"
wordMuteMax: "뮤트할 수 있는 단어의 수"
webhookMax: "생성할 수 있는 WebHook의 수"
clipMax: "생성할 수 있는 클립 수"
noteEachClipsMax: "각 클립에 추가할 수 있는 노트 수"
userListMax: "생성할 수 있는 리스트 수"
userEachUserListsMax: "리스트당 최대 사용자 수"
_condition:
isLocal: "로컬 사용자"
isRemote: "리모트 사용자"
createdLessThan: "다음 일수 이내에 가입한 유저"
createdMoreThan: "다음 일수 이상 활동한 유저"
followersLessThanOrEq: "팔로워 수가 다음 이하인 유저"
followersMoreThanOrEq: "팔로워 수가 다음 이상인 유저"
followingLessThanOrEq: "팔로잉 수가 다음 이하인 유저"
followingMoreThanOrEq: "팔로잉 수가 다음 이상인 유저"
and: "다음을 모두 만족"
or: "다음을 하나라도 만족"
not: "다음을 만족하지 않음"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다." description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다."
sensitivity: "탐지 민감도" sensitivity: "탐지 민감도"
@@ -1352,7 +1384,7 @@ _widgets:
aiscript: "AiScript 콘솔" aiscript: "AiScript 콘솔"
aiscriptApp: "AiScript 앱" aiscriptApp: "AiScript 앱"
aichan: "아이" aichan: "아이"
userList: "사용자 목록" userList: "유저 리스트"
_userList: _userList:
chooseList: "리스트 선택" chooseList: "리스트 선택"
clicker: "클리커" clicker: "클리커"

View File

@@ -926,15 +926,25 @@ didYouLikeMisskey: "คุณเคยชอบ Misskey ไหม?"
pleaseDonate: "{host} ใช้ซอฟต์แวร์ฟรี Misskey เราขอขอบคุณการบริจาคของคุณอย่างสูงเพื่อให้การพัฒนา Misskey สามารถดำเนินต่อไปได้นะ!" pleaseDonate: "{host} ใช้ซอฟต์แวร์ฟรี Misskey เราขอขอบคุณการบริจาคของคุณอย่างสูงเพื่อให้การพัฒนา Misskey สามารถดำเนินต่อไปได้นะ!"
roles: "บทบาท" roles: "บทบาท"
role: "บทบาท" role: "บทบาท"
normalUser: "ผู้ใช้มาตรฐาน"
undefined: "ไม่ได้กำหนด" undefined: "ไม่ได้กำหนด"
assign: "กำหนด" assign: "กำหนด"
unassign: "ยังไม่มอบหมาย" unassign: "ยังไม่มอบหมาย"
color: "สี" color: "สี"
manageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง"
_role: _role:
new: "บทบาทใหม่" new: "บทบาทใหม่"
edit: "แก้ไขบทบาท" edit: "แก้ไขบทบาท"
name: "ชื่อบทบาท" name: "ชื่อบทบาท"
description: "คำอธิบายบทบาท" description: "คำอธิบายบทบาท"
permission: "สิทธิ์ตามบทบาท"
descriptionOfPermission: "<b>ผู้ดูแลกลั่นกรองเนื้อหา</b> สามารถดำเนินการดูแลขั้นพื้นฐานได้นะ\n<b>ผู้ดูแลระบบ</b> สามารถเปลี่ยนการตั้งค่าทั้งหมดของอินสแตนซ์ได้นะ"
assignTarget: "กำหนดเป้าหมาย"
descriptionOfAssignTarget: "<b>แมนนวล</b> เพื่อเปลี่ยนผู้ที่เป็นส่วนหนึ่งของบทบาทนี้และใครที่ไม่ใช่ด้วยตนเอง\n<b>เงื่อนไข</b> เพื่อให้ผู้ใช้ได้รับการกำหนดและนำออกจากบทบาทนี้โดยอัตโนมัติตามเงื่อนไขชุดหนึ่ง"
manual: "ปรับเอง"
conditional: "มีเงื่อนไข"
condition: "เงื่อนไข"
isConditionalRole: "นี่คือบทบาทที่มีเงื่อนไข"
isPublic: "บทบาทสาธารณะ" isPublic: "บทบาทสาธารณะ"
descriptionOfIsPublic: "ทุกคนสามารถดูได้ว่าผู้ใช้งานนั้นได้รับมอบหมายบทบาทด้วยหรือไม่ \n\nบทบาทจะแสดงในโปรไฟล์ของผู้ใช้ด้วย" descriptionOfIsPublic: "ทุกคนสามารถดูได้ว่าผู้ใช้งานนั้นได้รับมอบหมายบทบาทด้วยหรือไม่ \n\nบทบาทจะแสดงในโปรไฟล์ของผู้ใช้ด้วย"
options: "ตัวเลือกบทบาท" options: "ตัวเลือกบทบาท"
@@ -947,8 +957,22 @@ _role:
gtlAvailable: "การดูไทม์ไลน์ทั่วโลก" gtlAvailable: "การดูไทม์ไลน์ทั่วโลก"
ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น" ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น"
canPublicNote: "สามารถส่งโน้ตสาธารณะ" canPublicNote: "สามารถส่งโน้ตสาธารณะ"
canInvite: "สร้างรหัสเชิญอินสแตนซ์"
canManageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง"
driveCapacity: "ความจุของไดรฟ์" driveCapacity: "ความจุของไดรฟ์"
antennaMax: "จำนวนสูงสุดของเสาอากาศ" antennaMax: "จำนวนสูงสุดของเสาอากาศ"
_condition:
isLocal: "ผู้ใช้ภายใน"
isRemote: "ผู้ใช้ระยะไกล"
createdLessThan: "สร้างน้อยกว่า"
createdMoreThan: "สร้างมากกว่า"
followersLessThanOrEq: "จำนวนผู้ติดตามน้อยกว่าหรือเท่ากับ\n"
followersMoreThanOrEq: "จำนวนผู้ติดตามมากกว่าหรือเท่ากับ\n"
followingLessThanOrEq: "จำนวนบัญชีต่อไปนี้คือ น้อยกว่าหรือเท่ากับ"
followingMoreThanOrEq: "จำนวนบัญชีต่อไปนี้คือ มากกว่าหรือเท่ากับ"
and: "และ"
or: "หรือ"
not: "ไม่"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย" description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย"
sensitivity: "การตรวจจับความไว" sensitivity: "การตรวจจับความไว"

View File

@@ -931,41 +931,56 @@ undefined: "未定义"
assign: "分配" assign: "分配"
unassign: "取消分配" unassign: "取消分配"
color: "颜色" color: "颜色"
manageCustomEmojis: "管理自定义表情符号"
youCannotCreateAnymore: "抱歉,您无法再创建更多了。"
_role: _role:
new: "创建角色" new: "创建角色"
edit: "编辑角色" edit: "编辑角色"
name: "用户组名称" name: "角色名称"
description: "用户组的描述" description: "角色描述"
permission: "用户组的权限" permission: "角色权限"
descriptionOfPermission: "<b>监察员</b>可以执行基本的审核操作。\n<b>管理员</b>可以更改实例的所有设置。" descriptionOfPermission: "<b>监察员</b>可以执行基本的审核操作。\n<b>管理员</b>可以更改实例的所有设置。"
assignTarget: "授权对象" assignTarget: "授权对象"
descriptionOfAssignTarget: "<b>手动</b>指手动选择谁被包括在这个用户组中。\n<b>符合条件</b>指设置条件以自动包括符合条件的用户。" descriptionOfAssignTarget: "<b>手动</b>指手动选择谁被包括在这个角色中。\n<b>符合条件</b>指设置条件以自动包括符合条件的用户。"
manual: "手动" manual: "手动"
conditional: "符合条件" conditional: "符合条件"
condition: "条件" condition: "条件"
isConditionalRole: "这是一个条件控制的用户组。" isConditionalRole: "这是一个条件控制的角色。"
isPublic: "公开用户组" isPublic: "角色公开"
descriptionOfIsPublic: "任何人都可以看到分配该用户组的用户用户的个人资料也将显示该用户组。" descriptionOfIsPublic: "任何人都可以看到分配该角色的用户。而用户的个人资料也将显示该角色。"
options: "选项" options: "选项"
baseRole: "基本角色" baseRole: "基本角色"
useBaseValue: "使用基本角色的值" useBaseValue: "使用基本角色的值"
chooseRoleToAssign: "选择要分配的角色" chooseRoleToAssign: "选择要分配的角色"
canEditMembersByModerator: "允许版主编辑成员" canEditMembersByModerator: "允许监察者编辑成员"
descriptionOfCanEditMembersByModerator: "如果选中,版主和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。" descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
_options: _options:
gtlAvailable: "查看全局时间线" gtlAvailable: "查看全局时间线"
ltlAvailable: "查看本地时间线" ltlAvailable: "查看本地时间线"
canPublicNote: "允许公开发帖" canPublicNote: "允许公开发帖"
canInvite: "发放实例邀请码"
canManageCustomEmojis: "管理自定义表情符号"
driveCapacity: "网盘容量" driveCapacity: "网盘容量"
pinMax: "帖子置顶数量限制"
antennaMax: "可创建的最大天线数量" antennaMax: "可创建的最大天线数量"
wordMuteMax: "屏蔽词的字数限制"
webhookMax: "Webhook 创建数量限制"
clipMax: "便签创建数量限制"
noteEachClipsMax: "单个便签内的贴文数量限制"
userListMax: "用户列表创建数量限制"
userEachUserListsMax: "单个用户列表内用户数量限制"
_condition: _condition:
isLocal: "是本地用户" isLocal: "是本地用户"
isRemote: "是远程用户" isRemote: "是远程用户"
createdLessThan: "账户创建时间少于" createdLessThan: "账户创建时间少于"
createdMoreThan: "账户创建时间超过" createdMoreThan: "账户创建时间超过"
and: "全部符合" followersLessThanOrEq: "关注者不多于"
or: "任一符合" followersMoreThanOrEq: "关注者不少于"
not: "不符合" followingLessThanOrEq: "关注中不多于"
followingMoreThanOrEq: "关注中不少于"
and: "符合以下全部条件"
or: "符合以下任一条件"
not: "不符合以下任何条件"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。" description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。"
sensitivity: "检测敏感度" sensitivity: "检测敏感度"

View File

@@ -324,7 +324,7 @@ integration: "整合"
connectService: "己連結" connectService: "己連結"
disconnectService: "己斷開 " disconnectService: "己斷開 "
enableLocalTimeline: "開啟本地時間軸" enableLocalTimeline: "開啟本地時間軸"
enableGlobalTimeline: "啟用公開時間軸" enableGlobalTimeline: "啟用全域時間軸"
disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審核員仍可以繼續使用。" disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審核員仍可以繼續使用。"
registration: "註冊" registration: "註冊"
enableRegistration: "開啟新使用者註冊" enableRegistration: "開啟新使用者註冊"
@@ -388,7 +388,7 @@ aboutMisskey: "關於 Misskey"
administrator: "管理員" administrator: "管理員"
token: "權杖" token: "權杖"
twoStepAuthentication: "兩階段驗證" twoStepAuthentication: "兩階段驗證"
moderator: "審核員" moderator: "監察員"
moderation: "言論調節" moderation: "言論調節"
nUsersMentioned: "提到了{n}" nUsersMentioned: "提到了{n}"
securityKey: "安全金鑰" securityKey: "安全金鑰"
@@ -869,7 +869,7 @@ recommended: "推薦"
check: "檢查" check: "檢查"
driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限" driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限"
driveCapOverrideCaption: "如果指定0以下的值就會被取消。" driveCapOverrideCaption: "如果指定0以下的值就會被取消。"
requireAdminForView: "必須以管理帳號登入才可以檢視。" requireAdminForView: "必須以管理帳號登入才可以檢視。"
isSystemAccount: "由系統自動建立與管理的帳號。" isSystemAccount: "由系統自動建立與管理的帳號。"
typeToConfirm: "要執行這項操作,請輸入 {x} " typeToConfirm: "要執行這項操作,請輸入 {x} "
deleteAccount: "刪除帳號" deleteAccount: "刪除帳號"
@@ -931,29 +931,51 @@ undefined: "未定義"
assign: "指派" assign: "指派"
unassign: "取消指派" unassign: "取消指派"
color: "顏色" color: "顏色"
manageCustomEmojis: "管理自訂表情符號"
_role: _role:
new: "建立角色" new: "建立角色"
edit: "編輯角色" edit: "編輯角色"
name: "角色名稱" name: "角色名稱"
description: "角色描述 " description: "角色描述 "
permission: "角色的權限" permission: "角色的權限"
descriptionOfPermission: "<b>審核員</b>執行與審核相關的基本操作。\n<b>管理</b>能變更實例的全部設定。" descriptionOfPermission: "<b>審核員</b>執行與審核相關的基本操作。\n<b>管理</b>能變更實例的全部設定。"
assignTarget: "指派目標" assignTarget: "指派目標"
descriptionOfAssignTarget: "<b>手動</b>是以手動管理這個角色包含的人員。\n<b>符合條件</b>是設定條件以自動包含符合條件的使用者。"
manual: "手動" manual: "手動"
conditional: "符合條件"
condition: "條件" condition: "條件"
isConditionalRole: "這是條件角色。" isConditionalRole: "這是條件角色。"
isPublic: "角色為公開" isPublic: "角色為公開"
descriptionOfIsPublic: "任何人都可以看到被指派了角色的使用者。此外,使用者的個人檔案將顯示這個角色。"
options: "選項" options: "選項"
baseRole: "基本角色" baseRole: "基本角色"
useBaseValue: "使用基本角色的值" useBaseValue: "使用基本角色的值"
chooseRoleToAssign: "選擇要指派的角色" chooseRoleToAssign: "選擇要指派的角色"
canEditMembersByModerator: "允許編輯監察員的成員"
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與監察員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
_options: _options:
gtlAvailable: "瀏覽全域時間軸"
ltlAvailable: "瀏覽本地時間軸"
canPublicNote: "允許公開貼文"
canInvite: "發行實例邀請碼"
canManageCustomEmojis: "管理自訂表情符號"
driveCapacity: "雲端硬碟容量" driveCapacity: "雲端硬碟容量"
pinMax: "置頂貼文的最大數量"
antennaMax: "可建立的天線數量"
webhookMax: "可建立的Webhook數量"
clipMax: "可建立的摘錄數量"
_condition: _condition:
isLocal: "本地使用者" isLocal: "本地使用者"
isRemote: "遠端使用者" isRemote: "遠端使用者"
createdLessThan: "自建立帳戶開始~以內" createdLessThan: "自建立帳戶開始~以內"
createdMoreThan: "自建立帳戶開始~經過" createdMoreThan: "自建立帳戶開始~經過"
followersLessThanOrEq: "追隨者人數在~以下"
followersMoreThanOrEq: "追隨者人數在~以上"
followingLessThanOrEq: "追隨人數在~以下"
followingMoreThanOrEq: "追隨人數在~以上"
and: "~和~"
or: "~或~"
not: "~否"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。" description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
sensitivity: "檢測敏感度" sensitivity: "檢測敏感度"

View File

@@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.0.0-rc.1", "version": "13.0.0-rc.8",
"codename": "indigo", "codename": "indigo",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -53,12 +53,15 @@
"devDependencies": { "devDependencies": {
"@types/gulp": "4.0.10", "@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1", "@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.48.0", "@typescript-eslint/eslint-plugin": "5.48.1",
"@typescript-eslint/parser": "5.48.0", "@typescript-eslint/parser": "5.48.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.3.0", "cypress": "12.3.0",
"eslint": "^8.31.0", "eslint": "^8.31.0",
"start-server-and-test": "1.15.2", "start-server-and-test": "1.15.2",
"typescript": "4.9.4" "typescript": "4.9.4"
},
"optionalDependencies": {
"@tensorflow/tfjs-core": "^4.2.0"
} }
} }

View File

@@ -21,17 +21,17 @@
"@tensorflow/tfjs-node": "4.1.0" "@tensorflow/tfjs-node": "4.1.0"
}, },
"dependencies": { "dependencies": {
"@bull-board/api": "^4.10.1", "@bull-board/api": "^4.10.2",
"@bull-board/fastify": "^4.10.1", "@bull-board/fastify": "^4.10.2",
"@bull-board/ui": "^4.10.1", "@bull-board/ui": "^4.10.2",
"@discordapp/twemoji": "14.0.2", "@discordapp/twemoji": "14.0.2",
"@fastify/accepts": "4.1.0", "@fastify/accepts": "4.1.0",
"@fastify/cookie": "^8.3.0", "@fastify/cookie": "^8.3.0",
"@fastify/cors": "8.2.0", "@fastify/cors": "8.2.0",
"@fastify/http-proxy": "^8.4.0", "@fastify/http-proxy": "^8.4.0",
"@fastify/multipart": "7.3.0", "@fastify/multipart": "7.4.0",
"@fastify/static": "6.6.0", "@fastify/static": "6.6.1",
"@fastify/view": "7.3.0", "@fastify/view": "7.4.0",
"@nestjs/common": "9.2.1", "@nestjs/common": "9.2.1",
"@nestjs/core": "9.2.1", "@nestjs/core": "9.2.1",
"@nestjs/testing": "9.2.1", "@nestjs/testing": "9.2.1",
@@ -41,7 +41,7 @@
"ajv": "8.12.0", "ajv": "8.12.0",
"archiver": "5.3.1", "archiver": "5.3.1",
"autwh": "0.1.0", "autwh": "0.1.0",
"aws-sdk": "2.1289.0", "aws-sdk": "2.1295.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.4", "blurhash": "2.0.4",
"bull": "4.10.2", "bull": "4.10.2",
@@ -58,7 +58,7 @@
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"fastify": "4.11.0", "fastify": "4.11.0",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "18.0.0", "file-type": "18.1.0",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"got": "12.5.3", "got": "12.5.3",
@@ -67,7 +67,7 @@
"ip-cidr": "3.0.11", "ip-cidr": "3.0.11",
"is-svg": "4.3.2", "is-svg": "4.3.2",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "20.0.3", "jsdom": "21.0.0",
"json5": "2.2.3", "json5": "2.2.3",
"json5-loader": "4.0.1", "json5-loader": "4.0.1",
"jsonld": "8.1.0", "jsonld": "8.1.0",
@@ -77,7 +77,7 @@
"misskey-js": "0.0.14", "misskey-js": "0.0.14",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"nodemailer": "6.8.0", "nodemailer": "6.9.0",
"nsfwjs": "2.4.2", "nsfwjs": "2.4.2",
"oauth": "^0.10.0", "oauth": "^0.10.0",
"os-utils": "0.0.14", "os-utils": "0.0.14",
@@ -87,7 +87,7 @@
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
"pug": "3.0.2", "pug": "3.0.2",
"punycode": "2.1.1", "punycode": "2.2.0",
"pureimage": "0.3.15", "pureimage": "0.3.15",
"qrcode": "1.5.1", "qrcode": "1.5.1",
"random-seed": "0.3.0", "random-seed": "0.3.0",
@@ -109,7 +109,7 @@
"stringz": "2.1.0", "stringz": "2.1.0",
"summaly": "2.7.0", "summaly": "2.7.0",
"syslog-pro": "git+https://github.com/misskey-dev/SyslogPro#0.2.9-misskey.2", "syslog-pro": "git+https://github.com/misskey-dev/SyslogPro#0.2.9-misskey.2",
"systeminformation": "5.17.1", "systeminformation": "5.17.3",
"tinycolor2": "1.5.2", "tinycolor2": "1.5.2",
"tmp": "0.2.1", "tmp": "0.2.1",
"tsc-alias": "1.8.2", "tsc-alias": "1.8.2",
@@ -117,18 +117,18 @@
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typeorm": "0.3.11", "typeorm": "0.3.11",
"ulid": "2.3.0", "ulid": "2.3.0",
"undici": "^5.14.0", "undici": "^5.15.0",
"unzipper": "0.10.11", "unzipper": "0.10.11",
"uuid": "9.0.0", "uuid": "9.0.0",
"vary": "1.1.2", "vary": "1.1.2",
"web-push": "3.5.0", "web-push": "3.5.0",
"websocket": "1.0.34", "websocket": "1.0.34",
"ws": "8.11.0", "ws": "8.12.0",
"xev": "3.0.2" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@redocly/openapi-core": "1.0.0-beta.117", "@redocly/openapi-core": "1.0.0-beta.120",
"@swc/core": "1.3.25", "@swc/core": "1.3.26",
"@swc/jest": "0.2.24", "@swc/jest": "0.2.24",
"@types/accepts": "1.3.5", "@types/accepts": "1.3.5",
"@types/archiver": "5.3.1", "@types/archiver": "5.3.1",
@@ -172,11 +172,11 @@
"@types/web-push": "3.3.2", "@types/web-push": "3.3.2",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.4", "@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.48.0", "@typescript-eslint/eslint-plugin": "5.48.1",
"@typescript-eslint/parser": "5.48.0", "@typescript-eslint/parser": "5.48.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.31.0", "eslint": "8.31.0",
"eslint-plugin-import": "2.26.0", "eslint-plugin-import": "2.27.4",
"execa": "6.1.0", "execa": "6.1.0",
"jest": "29.3.1", "jest": "29.3.1",
"jest-mock": "^29.3.1", "jest-mock": "^29.3.1",

View File

@@ -12,6 +12,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
@Injectable() @Injectable()
export class NotePiningService { export class NotePiningService {
@@ -30,6 +31,7 @@ export class NotePiningService {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private roleService: RoleService,
private relayService: RelayService, private relayService: RelayService,
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
@@ -55,7 +57,7 @@ export class NotePiningService {
const pinings = await this.userNotePiningsRepository.findBy({ userId: user.id }); const pinings = await this.userNotePiningsRepository.findBy({ userId: user.id });
if (pinings.length >= 5) { if (pinings.length >= (await this.roleService.getUserRoleOptions(user.id)).pinLimit) {
throw new IdentifiableError('15a018eb-58e5-4da1-93be-330fcc5e4e1a', 'You can not pin notes any more.'); throw new IdentifiableError('15a018eb-58e5-4da1-93be-330fcc5e4e1a', 'You can not pin notes any more.');
} }

View File

@@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { UserCacheService } from '@/core/UserCacheService.js'; import { UserCacheService } from '@/core/UserCacheService.js';
import { RoleCondFormulaValue } from '@/models/entities/Role.js'; import type { RoleCondFormulaValue } from '@/models/entities/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { StreamMessages } from '@/server/api/stream/types.js'; import { StreamMessages } from '@/server/api/stream/types.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
@@ -20,9 +20,15 @@ export type RoleOptions = {
canInvite: boolean; canInvite: boolean;
canManageCustomEmojis: boolean; canManageCustomEmojis: boolean;
driveCapacityMb: number; driveCapacityMb: number;
pinLimit: number;
antennaLimit: number; antennaLimit: number;
wordMuteLimit: number; wordMuteLimit: number;
webhookLimit: number; webhookLimit: number;
clipLimit: number;
noteEachClipsLimit: number;
userListLimit: number;
userEachUserListsLimit: number;
rateLimitFactor: number;
}; };
export const DEFAULT_ROLE: RoleOptions = { export const DEFAULT_ROLE: RoleOptions = {
@@ -32,9 +38,15 @@ export const DEFAULT_ROLE: RoleOptions = {
canInvite: false, canInvite: false,
canManageCustomEmojis: false, canManageCustomEmojis: false,
driveCapacityMb: 100, driveCapacityMb: 100,
pinLimit: 5,
antennaLimit: 5, antennaLimit: 5,
wordMuteLimit: 200, wordMuteLimit: 200,
webhookLimit: 3, webhookLimit: 3,
clipLimit: 10,
noteEachClipsLimit: 200,
userListLimit: 10,
userEachUserListsLimit: 50,
rateLimitFactor: 1,
}; };
@Injectable() @Injectable()
@@ -203,9 +215,15 @@ export class RoleService implements OnApplicationShutdown {
canInvite: getOptionValues('canInvite').some(x => x === true), canInvite: getOptionValues('canInvite').some(x => x === true),
canManageCustomEmojis: getOptionValues('canManageCustomEmojis').some(x => x === true), canManageCustomEmojis: getOptionValues('canManageCustomEmojis').some(x => x === true),
driveCapacityMb: Math.max(...getOptionValues('driveCapacityMb')), driveCapacityMb: Math.max(...getOptionValues('driveCapacityMb')),
pinLimit: Math.max(...getOptionValues('pinLimit')),
antennaLimit: Math.max(...getOptionValues('antennaLimit')), antennaLimit: Math.max(...getOptionValues('antennaLimit')),
wordMuteLimit: Math.max(...getOptionValues('wordMuteLimit')), wordMuteLimit: Math.max(...getOptionValues('wordMuteLimit')),
webhookLimit: Math.max(...getOptionValues('webhookLimit')), webhookLimit: Math.max(...getOptionValues('webhookLimit')),
clipLimit: Math.max(...getOptionValues('clipLimit')),
noteEachClipsLimit: Math.max(...getOptionValues('noteEachClipsLimit')),
userListLimit: Math.max(...getOptionValues('userListLimit')),
userEachUserListsLimit: Math.max(...getOptionValues('userEachUserListsLimit')),
rateLimitFactor: Math.max(...getOptionValues('rateLimitFactor')),
}; };
} }

View File

@@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ProxyAccountService } from '@/core/ProxyAccountService.js'; import { ProxyAccountService } from '@/core/ProxyAccountService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
@Injectable() @Injectable()
export class UserListService { export class UserListService {
@@ -23,13 +24,21 @@ export class UserListService {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private userFollowingService: UserFollowingService, private userFollowingService: UserFollowingService,
private roleService: RoleService,
private globalEventServie: GlobalEventService, private globalEventServie: GlobalEventService,
private proxyAccountService: ProxyAccountService, private proxyAccountService: ProxyAccountService,
) { ) {
} }
@bindThis @bindThis
public async push(target: User, list: UserList) { public async push(target: User, list: UserList, me: User) {
const currentCount = await this.userListJoiningsRepository.countBy({
userListId: list.id,
});
if (currentCount > (await this.roleService.getUserRoleOptions(me.id)).userEachUserListsLimit) {
throw new Error('Too many users');
}
await this.userListJoiningsRepository.insert({ await this.userListJoiningsRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),

View File

@@ -10,10 +10,10 @@ import { DownloadService } from '@/core/DownloadService.js';
import { UserListService } from '@/core/UserListService.js'; import { UserListService } from '@/core/UserListService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type Bull from 'bull'; import type Bull from 'bull';
import type { DbUserImportJobData } from '../types.js'; import type { DbUserImportJobData } from '../types.js';
import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class ImportUserListsProcessorService { export class ImportUserListsProcessorService {
@@ -102,7 +102,7 @@ export class ImportUserListsProcessorService {
if (await this.userListJoiningsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue; if (await this.userListJoiningsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
this.userListService.push(target, list!); this.userListService.push(target, list!, user);
} catch (e) { } catch (e) {
this.logger.warn(`Error in line:${linenum} ${e}`); this.logger.warn(`Error in line:${linenum} ${e}`);
} }

View File

@@ -224,8 +224,11 @@ export class ApiCallService implements OnApplicationShutdown {
limit.key = ep.name; limit.key = ep.name;
} }
// TODO: 毎リクエスト計算するのもあれだしキャッシュしたい
const factor = user ? (await this.roleService.getUserRoleOptions(user.id)).rateLimitFactor : 1;
// Rate limit // Rate limit
await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor).catch(err => { await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => {
throw new ApiError({ throw new ApiError({
message: 'Rate limit exceeded. Please try again later.', message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED', code: 'RATE_LIMIT_EXCEEDED',

View File

@@ -26,7 +26,7 @@ export class RateLimiterService {
} }
@bindThis @bindThis
public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string) { public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) {
return new Promise<void>((ok, reject) => { return new Promise<void>((ok, reject) => {
if (this.disabled) ok(); if (this.disabled) ok();
@@ -34,7 +34,7 @@ export class RateLimiterService {
const min = (): void => { const min = (): void => {
const minIntervalLimiter = new Limiter({ const minIntervalLimiter = new Limiter({
id: `${actor}:${limitation.key}:min`, id: `${actor}:${limitation.key}:min`,
duration: limitation.minInterval, duration: limitation.minInterval * factor,
max: 1, max: 1,
db: this.redisClient, db: this.redisClient,
}); });
@@ -62,8 +62,8 @@ export class RateLimiterService {
const max = (): void => { const max = (): void => {
const limiter = new Limiter({ const limiter = new Limiter({
id: `${actor}:${limitation.key}`, id: `${actor}:${limitation.key}`,
duration: limitation.duration, duration: limitation.duration * factor,
max: limitation.max, max: limitation.max / factor,
db: this.redisClient, db: this.redisClient,
}); });

View File

@@ -5,15 +5,15 @@ import type { UsersRepository, BlockingsRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = { export const meta = {
tags: ['account'], tags: ['account'],
limit: { limit: {
duration: ms('1hour'), duration: ms('1hour'),
max: 100, max: 20,
}, },
requireCredential: true, requireCredential: true,

View File

@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelsRepository, DriveFilesRepository } from '@/models/index.js'; import type { ChannelsRepository, DriveFilesRepository } from '@/models/index.js';
import type { Channel } from '@/models/entities/Channel.js'; import type { Channel } from '@/models/entities/Channel.js';
@@ -14,6 +15,11 @@ export const meta = {
kind: 'write:channels', kind: 'write:channels',
limit: {
duration: ms('1hour'),
max: 10,
},
res: { res: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,

View File

@@ -1,10 +1,12 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js'; import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js';
import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
export const meta = { export const meta = {
tags: ['account', 'notes', 'clips'], tags: ['account', 'notes', 'clips'],
@@ -13,6 +15,11 @@ export const meta = {
kind: 'write:account', kind: 'write:account',
limit: {
duration: ms('1hour'),
max: 20,
},
errors: { errors: {
noSuchClip: { noSuchClip: {
message: 'No such clip.', message: 'No such clip.',
@@ -31,6 +38,12 @@ export const meta = {
code: 'ALREADY_CLIPPED', code: 'ALREADY_CLIPPED',
id: '734806c4-542c-463a-9311-15c512803965', id: '734806c4-542c-463a-9311-15c512803965',
}, },
tooManyClipNotes: {
message: 'You cannot add notes to the clip any more.',
code: 'TOO_MANY_CLIP_NOTES',
id: 'f0dba960-ff73-4615-8df4-d6ac5d9dc118',
},
}, },
} as const; } as const;
@@ -54,6 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private clipNotesRepository: ClipNotesRepository, private clipNotesRepository: ClipNotesRepository,
private idService: IdService, private idService: IdService,
private roleService: RoleService,
private getterService: GetterService, private getterService: GetterService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
@@ -80,6 +94,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.alreadyClipped); throw new ApiError(meta.errors.alreadyClipped);
} }
const currentCount = await this.clipNotesRepository.countBy({
clipId: clip.id,
});
if (currentCount > (await this.roleService.getUserRoleOptions(me.id)).noteEachClipsLimit) {
throw new ApiError(meta.errors.tooManyClipNotes);
}
await this.clipNotesRepository.insert({ await this.clipNotesRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
noteId: note.id, noteId: note.id,

View File

@@ -4,6 +4,8 @@ import { IdService } from '@/core/IdService.js';
import type { ClipsRepository } from '@/models/index.js'; import type { ClipsRepository } from '@/models/index.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '@/server/api/error.js';
export const meta = { export const meta = {
tags: ['clips'], tags: ['clips'],
@@ -17,6 +19,14 @@ export const meta = {
optional: false, nullable: false, optional: false, nullable: false,
ref: 'Clip', ref: 'Clip',
}, },
errors: {
tooManyClips: {
message: 'You cannot create clip any more.',
code: 'TOO_MANY_CLIPS',
id: '920f7c2d-6208-4b76-8082-e632020f5883',
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@@ -37,9 +47,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private clipsRepository: ClipsRepository, private clipsRepository: ClipsRepository,
private clipEntityService: ClipEntityService, private clipEntityService: ClipEntityService,
private roleService: RoleService,
private idService: IdService, private idService: IdService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const currentCount = await this.clipsRepository.countBy({
userId: me.id,
});
if (currentCount > (await this.roleService.getUserRoleOptions(me.id)).clipLimit) {
throw new ApiError(meta.errors.tooManyClips);
}
const clip = await this.clipsRepository.insert({ const clip = await this.clipsRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),

View File

@@ -90,7 +90,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
} }
} }
const meta = await this.metaService.fetch(); const instance = await this.metaService.fetch();
try { try {
// Create file // Create file
@@ -102,8 +102,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
folderId: ps.folderId, folderId: ps.folderId,
force: ps.force, force: ps.force,
sensitive: ps.isSensitive, sensitive: ps.isSensitive,
requestIp: meta.enableIpLogging ? ip : null, requestIp: instance.enableIpLogging ? ip : null,
requestHeaders: meta.enableIpLogging ? headers : null, requestHeaders: instance.enableIpLogging ? headers : null,
}); });
return await this.driveFileEntityService.pack(driveFile, { self: true }); return await this.driveFileEntityService.pack(driveFile, { self: true });
} catch (err) { } catch (err) {
@@ -116,7 +116,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
} }
throw new ApiError(); throw new ApiError();
} finally { } finally {
cleanup!(); cleanup!();
} }
}); });
} }

View File

@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFoldersRepository } from '@/models/index.js'; import type { DriveFoldersRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
@@ -14,6 +15,11 @@ export const meta = {
kind: 'write:drive', kind: 'write:drive',
limit: {
duration: ms('1hour'),
max: 10,
},
errors: { errors: {
noSuchFolder: { noSuchFolder: {
message: 'No such folder.', message: 'No such folder.',

View File

@@ -6,15 +6,15 @@ import { IdentifiableError } from '@/misc/identifiable-error.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = { export const meta = {
tags: ['following', 'users'], tags: ['following', 'users'],
limit: { limit: {
duration: ms('1hour'), duration: ms('1hour'),
max: 100, max: 50,
}, },
requireCredential: true, requireCredential: true,

View File

@@ -60,7 +60,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private queryService: QueryService, private queryService: QueryService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId); const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId)
.andWhere('request.followeeId = :meId', { meId: me.id });
const requests = await query const requests = await query
.take(ps.limit) .take(ps.limit)

View File

@@ -18,7 +18,7 @@ export const meta = {
limit: { limit: {
duration: ms('1hour'), duration: ms('1hour'),
max: 300, max: 20,
}, },
res: { res: {

View File

@@ -6,6 +6,7 @@ import { webhookEventTypes } from '@/models/entities/Webhook.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '@/server/api/error.js';
export const meta = { export const meta = {
tags: ['webhooks'], tags: ['webhooks'],

View File

@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { BlockingsRepository, UserGroupJoiningsRepository, DriveFilesRepository, UserGroupsRepository } from '@/models/index.js'; import type { BlockingsRepository, UserGroupJoiningsRepository, DriveFilesRepository, UserGroupsRepository } from '@/models/index.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
@@ -15,6 +16,11 @@ export const meta = {
kind: 'write:messaging', kind: 'write:messaging',
limit: {
duration: ms('1hour'),
max: 120,
},
res: { res: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,

View File

@@ -1,12 +1,13 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { MutingsRepository } from '@/models/index.js'; import type { MutingsRepository } from '@/models/index.js';
import type { Muting } from '@/models/entities/Muting.js'; import type { Muting } from '@/models/entities/Muting.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = { export const meta = {
tags: ['account'], tags: ['account'],
@@ -15,6 +16,11 @@ export const meta = {
kind: 'write:mutes', kind: 'write:mutes',
limit: {
duration: ms('1hour'),
max: 20,
},
errors: { errors: {
noSuchUser: { noSuchUser: {
message: 'No such user.', message: 'No such user.',

View File

@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import type { NoteFavoritesRepository } from '@/models/index.js'; import type { NoteFavoritesRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -13,6 +14,11 @@ export const meta = {
kind: 'write:favorites', kind: 'write:favorites',
limit: {
duration: ms('1hour'),
max: 20,
},
errors: { errors: {
noSuchNote: { noSuchNote: {
message: 'No such note.', message: 'No such note.',

View File

@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import type { NotesRepository, NoteThreadMutingsRepository } from '@/models/index.js'; import type { NotesRepository, NoteThreadMutingsRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -14,6 +15,11 @@ export const meta = {
kind: 'write:account', kind: 'write:account',
limit: {
duration: ms('1hour'),
max: 10,
},
errors: { errors: {
noSuchNote: { noSuchNote: {
message: 'No such note.', message: 'No such note.',

View File

@@ -17,7 +17,7 @@ export const meta = {
limit: { limit: {
duration: ms('1hour'), duration: ms('1hour'),
max: 300, max: 10,
}, },
res: { res: {

View File

@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js'; import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { UserGroup } from '@/models/entities/UserGroup.js'; import type { UserGroup } from '@/models/entities/UserGroup.js';
@@ -16,6 +17,11 @@ export const meta = {
description: 'Create a new group.', description: 'Create a new group.',
limit: {
duration: ms('1hour'),
max: 10,
},
res: { res: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,

View File

@@ -5,6 +5,8 @@ import type { UserList } from '@/models/entities/UserList.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { RoleService } from '@/core/RoleService.js';
export const meta = { export const meta = {
tags: ['lists'], tags: ['lists'],
@@ -20,6 +22,14 @@ export const meta = {
optional: false, nullable: false, optional: false, nullable: false,
ref: 'UserList', ref: 'UserList',
}, },
errors: {
tooManyUserLists: {
message: 'You cannot create user list any more.',
code: 'TOO_MANY_USERLISTS',
id: '0cf21a28-7715-4f39-a20d-777bfdb8d138',
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@@ -39,8 +49,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private userListEntityService: UserListEntityService, private userListEntityService: UserListEntityService,
private idService: IdService, private idService: IdService,
private roleService: RoleService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const currentCount = await this.userListsRepository.countBy({
userId: me.id,
});
if (currentCount > (await this.roleService.getUserRoleOptions(me.id)).userListLimit) {
throw new ApiError(meta.errors.tooManyUserLists);
}
const userList = await this.userListsRepository.insert({ const userList = await this.userListsRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),

View File

@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/index.js'; import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
@@ -15,6 +16,11 @@ export const meta = {
description: 'Add a user to an existing list.', description: 'Add a user to an existing list.',
limit: {
duration: ms('1hour'),
max: 30,
},
errors: { errors: {
noSuchList: { noSuchList: {
message: 'No such list.', message: 'No such list.',
@@ -105,7 +111,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
} }
// Push the user // Push the user
await this.userListService.push(user, userList); await this.userListService.push(user, userList, me);
}); });
} }
} }

View File

@@ -18,7 +18,7 @@
"autobind-decorator": "2.4.0", "autobind-decorator": "2.4.0",
"autosize": "5.0.2", "autosize": "5.0.2",
"blurhash": "2.0.4", "blurhash": "2.0.4",
"broadcast-channel": "4.19.1", "broadcast-channel": "4.20.1",
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3", "browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
"canvas-confetti": "^1.6.0", "canvas-confetti": "^1.6.0",
"chart.js": "4.1.2", "chart.js": "4.1.2",
@@ -41,10 +41,10 @@
"misskey-js": "0.0.14", "misskey-js": "0.0.14",
"photoswipe": "5.3.4", "photoswipe": "5.3.4",
"prismjs": "1.29.0", "prismjs": "1.29.0",
"punycode": "2.1.1", "punycode": "2.2.0",
"querystring": "0.2.1", "querystring": "0.2.1",
"rndstr": "1.0.0", "rndstr": "1.0.0",
"rollup": "3.9.1", "rollup": "3.10.0",
"s-age": "1.1.2", "s-age": "1.1.2",
"sanitize-html": "^2.8.1", "sanitize-html": "^2.8.1",
"sass": "1.57.1", "sass": "1.57.1",
@@ -73,6 +73,7 @@
"@types/gulp": "4.0.10", "@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1", "@types/gulp-rename": "2.0.1",
"@types/matter-js": "0.18.2", "@types/matter-js": "0.18.2",
"@types/node": "^18.11.18",
"@types/punycode": "2.1.0", "@types/punycode": "2.1.0",
"@types/sanitize-html": "^2.8.0", "@types/sanitize-html": "^2.8.0",
"@types/seedrandom": "3.0.4", "@types/seedrandom": "3.0.4",
@@ -81,16 +82,16 @@
"@types/uuid": "9.0.0", "@types/uuid": "9.0.0",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.4", "@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.48.0", "@typescript-eslint/eslint-plugin": "5.48.1",
"@typescript-eslint/parser": "5.48.0", "@typescript-eslint/parser": "5.48.1",
"@vue/runtime-core": "3.2.45", "@vue/runtime-core": "3.2.45",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.3.0", "cypress": "12.3.0",
"eslint": "8.31.0", "eslint": "8.31.0",
"eslint-plugin-import": "2.26.0", "eslint-plugin-import": "2.27.4",
"eslint-plugin-vue": "9.8.0", "eslint-plugin-vue": "9.9.0",
"start-server-and-test": "1.15.2", "start-server-and-test": "1.15.2",
"vue-eslint-parser": "^9.1.0", "vue-eslint-parser": "^9.1.0",
"vue-tsc": "^1.0.22" "vue-tsc": "^1.0.24"
} }
} }

View File

@@ -1,34 +1,32 @@
<template> <template>
<div ref="rootEl" class="swhvrteh _popup _shadow" :style="{ zIndex }" @contextmenu.prevent="() => {}"> <div ref="rootEl" :class="$style.root" class="_popup _shadow" :style="{ zIndex }" @contextmenu.prevent="() => {}">
<ol v-if="type === 'user'" ref="suggests" class="users"> <ol v-if="type === 'user'" ref="suggests" :class="$style.list">
<li v-for="user in users" tabindex="-1" class="user" @click="complete(type, user)" @keydown="onKeydown"> <li v-for="user in users" tabindex="-1" :class="$style.item" @click="complete(type, user)" @keydown="onKeydown">
<img class="avatar" :src="user.avatarUrl"/> <img :class="$style.avatar" :src="user.avatarUrl"/>
<span class="name"> <span :class="$style.userName">
<MkUserName :key="user.id" :user="user"/> <MkUserName :key="user.id" :user="user"/>
</span> </span>
<span class="username">@{{ acct(user) }}</span> <span>@{{ acct(user) }}</span>
</li> </li>
<li tabindex="-1" class="choose" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li> <li tabindex="-1" :class="$style.item" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li>
</ol> </ol>
<ol v-else-if="hashtags.length > 0" ref="suggests" class="hashtags"> <ol v-else-if="hashtags.length > 0" ref="suggests" :class="[$style.list, $style.hashtags]">
<li v-for="hashtag in hashtags" tabindex="-1" @click="complete(type, hashtag)" @keydown="onKeydown"> <li v-for="hashtag in hashtags" tabindex="-1" :class="$style.item" @click="complete(type, hashtag)" @keydown="onKeydown">
<span class="name">{{ hashtag }}</span> <span class="name">{{ hashtag }}</span>
</li> </li>
</ol> </ol>
<ol v-else-if="emojis.length > 0" ref="suggests" class="emojis"> <ol v-else-if="emojis.length > 0" ref="suggests" :class="$style.list">
<li v-for="emoji in emojis" :key="emoji.emoji" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown"> <li v-for="emoji in emojis" :key="emoji.emoji" :class="$style.item" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown">
<div class="emoji"> <MkEmoji :emoji="emoji.emoji" :class="$style.emoji"/>
<MkEmoji :emoji="emoji.emoji"/>
</div>
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<span v-if="q" class="name" v-html="sanitizeHtml(emoji.name.replace(q, `<b>${q}</b>`))"></span> <span v-if="q" :class="$style.emojiName" v-html="sanitizeHtml(emoji.name.replace(q, `<b>${q}</b>`))"></span>
<span v-else v-text="emoji.name"></span> <span v-else v-text="emoji.name"></span>
<span v-if="emoji.aliasOf" class="alias">({{ emoji.aliasOf }})</span> <span v-if="emoji.aliasOf" :class="$style.emojiAlias">({{ emoji.aliasOf }})</span>
</li> </li>
</ol> </ol>
<ol v-else-if="mfmTags.length > 0" ref="suggests" class="mfmTags"> <ol v-else-if="mfmTags.length > 0" ref="suggests" :class="$style.list">
<li v-for="tag in mfmTags" tabindex="-1" @click="complete(type, tag)" @keydown="onKeydown"> <li v-for="tag in mfmTags" tabindex="-1" :class="$style.item" @click="complete(type, tag)" @keydown="onKeydown">
<span class="tag">{{ tag }}</span> <span>{{ tag }}</span>
</li> </li>
</ol> </ol>
</div> </div>
@@ -379,113 +377,89 @@ onBeforeUnmount(() => {
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.swhvrteh { .root {
position: fixed; position: fixed;
max-width: 100%; max-width: 100%;
margin-top: calc(1em + 8px); margin-top: calc(1em + 8px);
overflow: clip; overflow: clip;
transition: top 0.1s ease, left 0.1s ease; transition: top 0.1s ease, left 0.1s ease;
}
> ol { .list {
display: block; display: block;
margin: 0; margin: 0;
padding: 4px 0; padding: 4px 0;
max-height: 190px; max-height: 190px;
max-width: 500px; max-width: 500px;
overflow: auto; overflow: auto;
list-style: none; list-style: none;
}
> li { .item {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 4px 12px; padding: 4px 12px;
white-space: nowrap; white-space: nowrap;
overflow: clip; overflow: clip;
font-size: 0.9em; font-size: 0.9em;
cursor: default; cursor: default;
user-select: none;
overflow: hidden;
text-overflow: ellipsis;
&, * { &:hover {
user-select: none; background: var(--X3);
}
* {
overflow: hidden;
text-overflow: ellipsis;
}
&:hover {
background: var(--X3);
}
&[data-selected='true'] {
background: var(--accent);
&, * {
color: #fff !important;
}
}
&:active {
background: var(--accentDarken);
&, * {
color: #fff !important;
}
}
}
} }
> .users > li { &[data-selected='true'] {
background: var(--accent);
.avatar { color: #fff !important;
min-width: 28px;
min-height: 28px;
max-width: 28px;
max-height: 28px;
margin: 0 8px 0 0;
border-radius: 100%;
}
.name {
margin: 0 8px 0 0;
}
} }
> .emojis > li { &:active {
background: var(--accentDarken);
.emoji { color: #fff !important;
flex-shrink: 0;
display: flex;
margin: 0 4px 0 0;
height: 24px;
width: 24px;
justify-content: center;
align-items: center;
font-size: 20px;
> img {
height: 24px;
width: 24px;
object-fit: scale-down;
}
}
.name {
flex-shrink: 1;
}
.alias {
flex-shrink: 9999999;
margin: 0 0 0 8px;
}
}
> .mfmTags > li {
.name {
}
} }
} }
.avatar {
min-width: 28px;
min-height: 28px;
max-width: 28px;
max-height: 28px;
margin: 0 8px 0 0;
border-radius: 100%;
}
.userName {
margin: 0 8px 0 0;
}
.emoji {
flex-shrink: 0 !important;
display: flex !important;
margin: 0 4px 0 0 !important;
height: 24px !important;
width: 24px !important;
justify-content: center !important;
align-items: center !important;
font-size: 20px !important;
pointer-events: none !important;
}
.emojiImg {
height: 24px;
width: 24px;
object-fit: scale-down;
}
.emojiName {
flex-shrink: 1;
}
.emojiAlias {
flex-shrink: 9999999;
margin: 0 0 0 8px;
}
</style> </style>

View File

@@ -1,32 +1,34 @@
<template> <template>
<button <button
v-if="!link" v-if="!link"
ref="el" class="bghgjjyj _button" ref="el" class="_button"
:class="{ inline, primary, gradate, danger, rounded, full, small, large, asLike }" :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.asLike]: asLike }]"
:type="type" :type="type"
@click="emit('click', $event)" @click="emit('click', $event)"
@mousedown="onMousedown" @mousedown="onMousedown"
> >
<div ref="ripples" class="ripples"></div> <div ref="ripples" :class="$style.ripples"></div>
<div class="content"> <div :class="$style.content">
<slot></slot> <slot></slot>
</div> </div>
</button> </button>
<MkA <MkA
v-else class="bghgjjyj _button" v-else class="_button"
:class="{ inline, primary, gradate, danger, rounded, full, small }" :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.asLike]: asLike }]"
:to="to" :to="to"
@mousedown="onMousedown" @mousedown="onMousedown"
> >
<div ref="ripples" class="ripples"></div> <div ref="ripples" :class="$style.ripples"></div>
<div class="content"> <div :class="$style.content">
<slot></slot> <slot></slot>
</div> </div>
</MkA> </MkA>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onMounted } from 'vue'; import { nextTick, onMounted, useCssModule } from 'vue';
const $style = useCssModule();
const props = defineProps<{ const props = defineProps<{
type?: 'button' | 'submit' | 'reset'; type?: 'button' | 'submit' | 'reset';
@@ -78,6 +80,7 @@ function onMousedown(evt: MouseEvent): void {
const rect = target.getBoundingClientRect(); const rect = target.getBoundingClientRect();
const ripple = document.createElement('div'); const ripple = document.createElement('div');
ripple.classList.add($style.ripple);
ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px'; ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px';
ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px'; ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px';
@@ -101,8 +104,8 @@ function onMousedown(evt: MouseEvent): void {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.bghgjjyj { .root {
position: relative; position: relative;
z-index: 1; // 他コンポーネントのbox-shadowに隠されないようにするため z-index: 1; // 他コンポーネントのbox-shadowに隠されないようにするため
display: block; display: block;
@@ -173,7 +176,7 @@ function onMousedown(evt: MouseEvent): void {
} }
> .ripples { > .ripples {
::v-deep(div) { > .ripple {
background: rgba(255, 60, 106, 0.15); background: rgba(255, 60, 106, 0.15);
} }
} }
@@ -237,35 +240,37 @@ function onMousedown(evt: MouseEvent): void {
min-width: 100px; min-width: 100px;
} }
> .ripples { &.primary > .ripples > .ripple {
position: absolute;
z-index: 0;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 6px;
overflow: clip;
::v-deep(div) {
position: absolute;
width: 2px;
height: 2px;
border-radius: 100%;
background: rgba(0, 0, 0, 0.1);
opacity: 1;
transform: scale(1);
transition: all 0.5s cubic-bezier(0,.5,0,1);
}
}
&.primary > .ripples ::v-deep(div) {
background: rgba(0, 0, 0, 0.15); background: rgba(0, 0, 0, 0.15);
} }
}
> .content { .ripples {
position: relative; position: absolute;
z-index: 1; z-index: 0;
} top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 6px;
overflow: clip;
pointer-events: none;
}
.ripple {
position: absolute;
width: 2px;
height: 2px;
border-radius: 100%;
background: rgba(0, 0, 0, 0.1);
opacity: 1;
transform: scale(1);
transition: all 0.5s cubic-bezier(0,.5,0,1);
}
.content {
position: relative;
z-index: 1;
pointer-events: none;
} }
</style> </style>

View File

@@ -1,10 +1,10 @@
<template> <template>
<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" :direction="'top'" :inner-margin="16" @closed="emit('closed')"> <MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" :direction="'top'" :inner-margin="16" @closed="emit('closed')">
<div v-if="title || series" class="qpcyisrl"> <div v-if="title || series">
<div v-if="title" class="title">{{ title }}</div> <div v-if="title" :class="$style.title">{{ title }}</div>
<template v-if="series"> <template v-if="series">
<div v-for="x in series" class="series"> <div v-for="x in series">
<span class="color" :style="{ background: x.backgroundColor, borderColor: x.borderColor }"></span> <span :class="$style.color" :style="{ background: x.backgroundColor, borderColor: x.borderColor }"></span>
<span>{{ x.text }}</span> <span>{{ x.text }}</span>
</div> </div>
</template> </template>
@@ -33,21 +33,17 @@ const emit = defineEmits<{
}>(); }>();
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.qpcyisrl { .title {
> .title { margin-bottom: 4px;
margin-bottom: 4px; }
}
> .series { .color {
> .color { display: inline-block;
display: inline-block; width: 8px;
width: 8px; height: 8px;
height: 8px; border-width: 1px;
border-width: 1px; border-style: solid;
border-style: solid; margin-right: 8px;
margin-right: 8px;
}
}
} }
</style> </style>

View File

@@ -1,26 +1,32 @@
<template> <template>
<div class="ukygtjoj _panel" :class="{ naked, thin, hideHeader: !showHeader, scrollable, closed: !showBody }"> <div class="_panel" :class="[$style.root, { [$style.naked]: naked, [$style.thin]: thin, [$style.hideHeader]: !showHeader, [$style.scrollable]: scrollable, [$style.closed]: !showBody }]">
<header v-if="showHeader" ref="header"> <header v-if="showHeader" ref="header" :class="$style.header">
<div class="title"><slot name="header"></slot></div> <div :class="$style.title">
<div class="sub"> <span :class="$style.titleIcon"><slot name="icon"></slot></span>
<slot name="func"></slot> <slot name="header"></slot>
<button v-if="foldable" class="_button" @click="() => showBody = !showBody"> </div>
<div :class="$style.headerSub">
<slot name="func" :button-style-class="$style.headerButton"></slot>
<button v-if="foldable" :class="$style.headerButton" class="_button" @click="() => showBody = !showBody">
<template v-if="showBody"><i class="ti ti-chevron-up"></i></template> <template v-if="showBody"><i class="ti ti-chevron-up"></i></template>
<template v-else><i class="ti ti-chevron-down"></i></template> <template v-else><i class="ti ti-chevron-down"></i></template>
</button> </button>
</div> </div>
</header> </header>
<Transition <Transition
:name="$store.state.animation ? 'container-toggle' : ''" :enter-active-class="$store.state.animation ? $style.transition_toggle_enterActive : ''"
:leave-active-class="$store.state.animation ? $style.transition_toggle_leaveActive : ''"
:enter-from-class="$store.state.animation ? $style.transition_toggle_enterFrom : ''"
:leave-to-class="$store.state.animation ? $style.transition_toggle_leaveTo : ''"
@enter="enter" @enter="enter"
@after-enter="afterEnter" @after-enter="afterEnter"
@leave="leave" @leave="leave"
@after-leave="afterLeave" @after-leave="afterLeave"
> >
<div v-show="showBody" ref="content" class="content" :class="{ omitted }"> <div v-show="showBody" ref="content" :class="[$style.content, { omitted }]">
<slot></slot> <slot></slot>
<button v-if="omitted" class="fade _button" @click="() => { ignoreOmit = true; omitted = false; }"> <button v-if="omitted" :class="$style.fade" class="_button" @click="() => { ignoreOmit = true; omitted = false; }">
<span>{{ $ts.showMore }}</span> <span :class="$style.fadeLabel">{{ $ts.showMore }}</span>
</button> </button>
</div> </div>
</Transition> </Transition>
@@ -129,19 +135,18 @@ export default defineComponent({
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.container-toggle-enter-active, .container-toggle-leave-active { .transition_toggle_enterActive,
.transition_toggle_leaveActive {
overflow-y: clip; overflow-y: clip;
transition: opacity 0.5s, height 0.5s !important; transition: opacity 0.5s, height 0.5s !important;
} }
.container-toggle-enter-from { .transition_toggle_enterFrom,
opacity: 0; .transition_toggle_leaveTo {
}
.container-toggle-leave-to {
opacity: 0; opacity: 0;
} }
.ukygtjoj { .root {
position: relative; position: relative;
overflow: clip; overflow: clip;
contain: content; contain: content;
@@ -160,116 +165,93 @@ export default defineComponent({
} }
} }
> header {
position: sticky;
top: var(--stickyTop, 0px);
left: 0;
color: var(--panelHeaderFg);
background: var(--panelHeaderBg);
border-bottom: solid 0.5px var(--panelHeaderDivider);
z-index: 2;
line-height: 1.4em;
> .title {
margin: 0;
padding: 12px 16px;
> ::v-deep(i) {
margin-right: 6px;
}
&:empty {
display: none;
}
}
> .sub {
position: absolute;
z-index: 2;
top: 0;
right: 0;
height: 100%;
> ::v-deep(button) {
width: 42px;
height: 100%;
}
}
}
> .content {
--stickyTop: 0px;
&.omitted {
position: relative;
max-height: var(--maxHeight);
overflow: hidden;
> .fade {
display: block;
position: absolute;
z-index: 10;
bottom: 0;
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15));
> span {
display: inline-block;
background: var(--panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
}
&:hover {
> span {
background: var(--panelHighlight);
}
}
}
}
}
&.thin { &.thin {
> header { > .header {
> .title { > .title {
padding: 8px 10px; padding: 8px 10px;
font-size: 0.9em; font-size: 0.9em;
} }
} }
}
}
> .content { .header {
position: sticky;
top: var(--stickyTop, 0px);
left: 0;
color: var(--panelHeaderFg);
background: var(--panelHeaderBg);
border-bottom: solid 0.5px var(--panelHeaderDivider);
z-index: 2;
line-height: 1.4em;
}
.title {
margin: 0;
padding: 12px 16px;
&:empty {
display: none;
}
}
.titleIcon {
margin-right: 6px;
}
.headerSub {
position: absolute;
z-index: 2;
top: 0;
right: 0;
height: 100%;
}
.headerButton {
width: 42px;
height: 100%;
}
.content {
--stickyTop: 0px;
&.omitted {
position: relative;
max-height: var(--maxHeight);
overflow: hidden;
> .fade {
display: block;
position: absolute;
z-index: 10;
bottom: 0;
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15));
> .fadeLabel {
display: inline-block;
background: var(--panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
}
&:hover {
> .fadeLabel {
background: var(--panelHighlight);
}
}
} }
} }
} }
@container (max-width: 380px) { @container (max-width: 380px) {
.ukygtjoj { .title {
> header { padding: 8px 10px;
> .title { font-size: 0.9em;
padding: 8px 10px;
font-size: 0.9em;
}
}
}
}
._forceContainerFull_ .ukygtjoj {
> header {
> .title {
padding: 12px 16px !important;
}
}
}
._forceContainerFull_.ukygtjoj {
> header {
> .title {
padding: 12px 16px !important;
}
} }
} }
</style> </style>

View File

@@ -1,6 +1,12 @@
<template> <template>
<Transition :name="$store.state.animation ? 'fade' : ''" appear> <Transition
<div ref="rootEl" class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}"> appear
:enter-active-class="$store.state.animation ? $style.transition_fade_enterActive : ''"
:leave-active-class="$store.state.animation ? $style.transition_fade_leaveActive : ''"
:enter-from-class="$store.state.animation ? $style.transition_fade_enterFrom : ''"
:leave-to-class="$store.state.animation ? $style.transition_fade_leaveTo : ''"
>
<div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
<MkMenu :items="items" :align="'left'" @close="$emit('closed')"/> <MkMenu :items="items" :align="'left'" @close="$emit('closed')"/>
</div> </div>
</Transition> </Transition>
@@ -68,18 +74,19 @@ function onMousedown(evt: Event) {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.nvlagfpb { .transition_fade_enterActive,
position: absolute; .transition_fade_leaveActive {
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1), transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1), transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
transform-origin: left top; transform-origin: left top;
} }
.transition_fade_enterFrom,
.fade-enter-from, .fade-leave-to { .transition_fade_leaveTo {
opacity: 0; opacity: 0;
transform: scale(0.9); transform: scale(0.9);
} }
.root {
position: absolute;
}
</style> </style>

View File

@@ -146,6 +146,7 @@ onBeforeUnmount(() => {
<style lang="scss" module> <style lang="scss" module>
.root { .root {
position: relative; position: relative;
margin: auto;
padding: 32px; padding: 32px;
min-width: 320px; min-width: 320px;
max-width: 480px; max-width: 480px;

View File

@@ -1,11 +1,11 @@
<template> <template>
<span class="zjobosdg"> <span>
<span v-text="hh"></span> <span v-text="hh"></span>
<span class="colon" :class="{ showColon }">:</span> <span :class="[$style.colon, { [$style.showColon]: showColon }]">:</span>
<span v-text="mm"></span> <span v-text="mm"></span>
<span v-if="showS" class="colon" :class="{ showColon }">:</span> <span v-if="showS" :class="[$style.colon, { [$style.showColon]: showColon }]">:</span>
<span v-if="showS" v-text="ss"></span> <span v-if="showS" v-text="ss"></span>
<span v-if="showMs" class="colon" :class="{ showColon }">:</span> <span v-if="showMs" :class="[$style.colon, { [$style.showColon]: showColon }]">:</span>
<span v-if="showMs" v-text="ms"></span> <span v-if="showMs" v-text="ms"></span>
</span> </span>
</template> </template>
@@ -62,16 +62,14 @@ onUnmounted(() => {
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.zjobosdg { .colon {
> .colon { opacity: 0;
opacity: 0; transition: opacity 1s ease;
transition: opacity 1s ease;
&.showColon { &.showColon {
opacity: 1; opacity: 1;
transition: opacity 0s; transition: opacity 0s;
}
} }
} }
</style> </style>

View File

@@ -31,6 +31,3 @@ const emit = defineEmits<{
const shown = ref(!!props.initialShown); const shown = ref(!!props.initialShown);
</script> </script>
<style lang="scss" scoped>
</style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div v-if="meta" class="xfbouadm" :style="{ backgroundImage: `url(${ meta.backgroundImageUrl })` }"></div> <div v-if="meta" :class="$style.root" :style="{ backgroundImage: `url(${ meta.backgroundImageUrl })` }"></div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -14,8 +14,8 @@ os.api('meta', { detail: true }).then(gotMeta => {
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.xfbouadm { .root {
background-position: center; background-position: center;
background-size: cover; background-size: cover;
} }

View File

@@ -15,7 +15,7 @@
</template> </template>
<MkSpacer :margin-min="20" :margin-max="32"> <MkSpacer :margin-min="20" :margin-max="32">
<div class="xkpnjxcv _gaps_m"> <div class="_gaps_m">
<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)"> <template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
<MkInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1"> <MkInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
@@ -119,9 +119,3 @@ export default defineComponent({
}, },
}); });
</script> </script>
<style lang="scss" scoped>
.xkpnjxcv {
}
</style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="mk-google"> <div :class="$style.root">
<input v-model="query" type="search" :placeholder="q"> <input v-model="query" :class="$style.input" type="search" :placeholder="q">
<button @click="search"><i class="ti ti-search"></i> {{ $ts.searchByGoogle }}</button> <button :class="$style.button" @click="search"><i class="ti ti-search"></i> {{ $ts.searchByGoogle }}</button>
</div> </div>
</template> </template>
@@ -19,33 +19,33 @@ const search = () => {
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.mk-google { .root {
display: flex; display: flex;
margin: 8px 0; margin: 8px 0;
}
> input { .input {
flex-shrink: 1; flex-shrink: 1;
padding: 10px; padding: 10px;
width: 100%; width: 100%;
height: 40px; height: 40px;
font-size: 16px; font-size: 16px;
border: solid 1px var(--divider); border: solid 1px var(--divider);
border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px;
-webkit-appearance: textfield; -webkit-appearance: textfield;
} }
> button { .button {
flex-shrink: 0; flex-shrink: 0;
margin: 0; margin: 0;
padding: 0 16px; padding: 0 16px;
border: solid 1px var(--divider); border: solid 1px var(--divider);
border-left: none; border-left: none;
border-radius: 0 4px 4px 0; border-radius: 0 4px 4px 0;
&:active { &:active {
box-shadow: 0 2px 4px rgba(#000, 0.15) inset; box-shadow: 0 2px 4px rgba(#000, 0.15) inset;
}
} }
} }
</style> </style>

View File

@@ -33,6 +33,7 @@ const modal = $shallowRef<InstanceType<typeof MkModal>>();
<style lang="scss" scoped> <style lang="scss" scoped>
.xubzgfga { .xubzgfga {
margin: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="xubzgfgb" :class="{ cover }" :title="title"> <div :class="[$style.root, { [$style.cover]: cover }]" :title="title">
<canvas v-if="!loaded" ref="canvas" :width="size" :height="size" :title="title"/> <canvas v-if="!loaded" ref="canvas" :class="$style.canvas" :width="size" :height="size" :title="title"/>
<img v-if="src" :src="src" :title="title" :alt="alt" @load="onLoad"/> <img v-if="src" :class="$style.img" :src="src" :title="title" :alt="alt" @load="onLoad"/>
</div> </div>
</template> </template>
@@ -45,32 +45,32 @@ onMounted(() => {
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.xubzgfgb { .root {
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
> canvas,
> img {
display: block;
width: 100%;
height: 100%;
}
> canvas {
position: absolute;
object-fit: cover;
}
> img {
object-fit: contain;
}
&.cover { &.cover {
> img { > .img {
object-fit: cover; object-fit: cover;
} }
} }
} }
.canvas,
.img {
display: block;
width: 100%;
height: 100%;
}
.canvas {
position: absolute;
object-fit: cover;
}
.img {
object-fit: contain;
}
</style> </style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="fpezltsf" :class="{ warn }"> <div :class="[$style.root, { [$style.warn]: warn }]">
<i v-if="warn" class="ti ti-alert-triangle"></i> <i v-if="warn" class="ti ti-alert-triangle" :class="$style.i"></i>
<i v-else class="ti ti-info-circle"></i> <i v-else class="ti ti-info-circle" :class="$style.i"></i>
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
@@ -14,8 +14,8 @@ const props = defineProps<{
}>(); }>();
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.fpezltsf { .root {
padding: 12px 14px; padding: 12px 14px;
font-size: 90%; font-size: 90%;
background: var(--infoBg); background: var(--infoBg);
@@ -26,9 +26,9 @@ const props = defineProps<{
background: var(--infoWarnBg); background: var(--infoWarnBg);
color: var(--infoWarnFg); color: var(--infoWarnFg);
} }
}
> i { .i {
margin-right: 4px; margin-right: 4px;
}
} }
</style> </style>

View File

@@ -78,9 +78,9 @@ const inputEl = shallowRef<HTMLElement>();
const prefixEl = shallowRef<HTMLElement>(); const prefixEl = shallowRef<HTMLElement>();
const suffixEl = shallowRef<HTMLElement>(); const suffixEl = shallowRef<HTMLElement>();
const height = const height =
props.small ? 34 : props.small ? 33 :
props.large ? 40 : props.large ? 39 :
37; 36;
const focus = () => inputEl.value.focus(); const focus = () => inputEl.value.focus();
const onInput = (ev: KeyboardEvent) => { const onInput = (ev: KeyboardEvent) => {

View File

@@ -3,12 +3,12 @@
<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }"> <div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
<div class="main"> <div class="main">
<template v-for="item in items"> <template v-for="item in items">
<button v-if="item.action" v-click-anime class="_button" @click="$event => { item.action($event); close(); }"> <button v-if="item.action" v-click-anime class="_button item" @click="$event => { item.action($event); close(); }">
<i class="icon" :class="item.icon"></i> <i class="icon" :class="item.icon"></i>
<div class="text">{{ item.text }}</div> <div class="text">{{ item.text }}</div>
<span v-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span> <span v-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
</button> </button>
<MkA v-else v-click-anime :to="item.to" @click.passive="close()"> <MkA v-else v-click-anime :to="item.to" class="item" @click.passive="close()">
<i class="icon" :class="item.icon"></i> <i class="icon" :class="item.icon"></i>
<div class="text">{{ item.text }}</div> <div class="text">{{ item.text }}</div>
<span v-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span> <span v-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
@@ -66,6 +66,7 @@ function close() {
.szkkfdyq { .szkkfdyq {
max-height: 100%; max-height: 100%;
width: min(460px, 100vw); width: min(460px, 100vw);
margin: auto;
padding: 24px; padding: 24px;
box-sizing: border-box; box-sizing: border-box;
overflow: auto; overflow: auto;
@@ -82,11 +83,11 @@ function close() {
text-align: center; text-align: center;
} }
> .main, > .sub { > .main {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
> * { > .item {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -128,11 +129,5 @@ function close() {
} }
} }
} }
> .sub {
margin-top: 8px;
padding-top: 8px;
border-top: solid 0.5px var(--divider);
}
} }
</style> </style>

View File

@@ -1,17 +1,11 @@
<template> <template>
<MkA v-if="url.startsWith('/')" v-user-preview="canonical" class="akbvjaqn" :class="{ isMe }" :to="url" :style="{ background: bgCss }"> <MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }">
<img class="icon" :src="`/avatar/@${username}@${host}`" alt=""> <img :class="$style.icon" :src="`/avatar/@${username}@${host}`" alt="">
<span class="main"> <span>
<span class="username">@{{ username }}</span> <span :class="$style.username">@{{ username }}</span>
<span v-if="(host != localHost) || $store.state.showFullAcct" class="host">@{{ toUnicode(host) }}</span> <span v-if="(host != localHost) || $store.state.showFullAcct" :class="$style.host">@{{ toUnicode(host) }}</span>
</span> </span>
</MkA> </MkA>
<a v-else class="akbvjaqn" :href="url" target="_blank" rel="noopener" :style="{ background: bgCss }">
<span class="main">
<span class="username">@{{ username }}</span>
<span class="host">@{{ toUnicode(host) }}</span>
</span>
</a>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -39,8 +33,8 @@ bg.setAlpha(0.1);
const bgCss = bg.toRgbString(); const bgCss = bg.toRgbString();
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.akbvjaqn { .root {
display: inline-block; display: inline-block;
padding: 4px 8px 4px 4px; padding: 4px 8px 4px 4px;
border-radius: 999px; border-radius: 999px;
@@ -49,18 +43,18 @@ const bgCss = bg.toRgbString();
&.isMe { &.isMe {
color: var(--mentionMe); color: var(--mentionMe);
} }
}
> .icon { .icon {
width: 1.5em; width: 1.5em;
height: 1.5em; height: 1.5em;
object-fit: cover; object-fit: cover;
margin: 0 0.2em 0 0; margin: 0 0.2em 0 0;
vertical-align: bottom; vertical-align: bottom;
border-radius: 100%; border-radius: 100%;
} }
> .main > .host { .host {
opacity: 0.5; opacity: 0.5;
}
} }
</style> </style>

View File

@@ -1,8 +1,15 @@
<template> <template>
<Transition :name="transitionName" :duration="transitionDuration" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened"> <Transition
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> :name="transitionName"
<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div> :enter-active-class="$style['transition_' + transitionName + '_enterActive']"
<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick"> :leave-active-class="$style['transition_' + transitionName + '_leaveActive']"
:enter-from-class="$style['transition_' + transitionName + '_enterFrom']"
:leave-to-class="$style['transition_' + transitionName + '_leaveTo']"
:duration="transitionDuration" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened"
>
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
<div class="_modalBg data-cy-bg" :class="[$style.bg, { [$style.bgTransparent]: isEnableBgTransparent, 'data-cy-transparent': isEnableBgTransparent }]" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
<div ref="content" :class="[$style.content, { [$style.fixed]: fixed }]" :style="{ zIndex }" @click.self="onBgClick">
<slot :max-height="maxHeight" :type="type"></slot> <slot :max-height="maxHeight" :type="type"></slot>
</div> </div>
</div> </div>
@@ -26,7 +33,7 @@ function getFixedContainer(el: Element | null): Element | null {
} }
} }
type ModalTypes = 'popup' | 'dialog' | 'dialog:top' | 'drawer'; type ModalTypes = 'popup' | 'dialog' | 'drawer';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
manualShowing?: boolean | null; manualShowing?: boolean | null;
@@ -75,6 +82,7 @@ const type = $computed<ModalTypes>(() => {
return props.preferType!; return props.preferType!;
} }
}); });
const isEnableBgTransparent = $computed(() => props.transparentBg && (type === 'popup'));
let transitionName = $computed((() => let transitionName = $computed((() =>
defaultStore.state.animation defaultStore.state.animation
? useSendAnime ? useSendAnime
@@ -264,7 +272,7 @@ onMounted(() => {
fixed = (type === 'drawer') || (getFixedContainer(props.src) != null); fixed = (type === 'drawer') || (getFixedContainer(props.src) != null);
await nextTick(); await nextTick();
align(); align();
}, { immediate: true }); }, { immediate: true });
@@ -280,8 +288,9 @@ defineExpose({
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.send-enter-active, .send-leave-active { .transition_send_enterActive,
.transition_send_leaveActive {
> .bg { > .bg {
transition: opacity 0.3s !important; transition: opacity 0.3s !important;
} }
@@ -291,7 +300,8 @@ defineExpose({
transition: opacity 0.3s ease-in, transform 0.3s cubic-bezier(.5,-0.5,1,.5) !important; transition: opacity 0.3s ease-in, transform 0.3s cubic-bezier(.5,-0.5,1,.5) !important;
} }
} }
.send-enter-from, .send-leave-to { .transition_send_enterFrom,
.transition_send_leaveTo {
> .bg { > .bg {
opacity: 0; opacity: 0;
} }
@@ -303,7 +313,8 @@ defineExpose({
} }
} }
.modal-enter-active, .modal-leave-active { .transition_modal_enterActive,
.transition_modal_leaveActive {
> .bg { > .bg {
transition: opacity 0.2s !important; transition: opacity 0.2s !important;
} }
@@ -313,7 +324,8 @@ defineExpose({
transition: opacity 0.2s, transform 0.2s !important; transition: opacity 0.2s, transform 0.2s !important;
} }
} }
.modal-enter-from, .modal-leave-to { .transition_modal_enterFrom,
.transition_modal_leaveTo {
> .bg { > .bg {
opacity: 0; opacity: 0;
} }
@@ -326,7 +338,8 @@ defineExpose({
} }
} }
.modal-popup-enter-active, .modal-popup-leave-active { .transition_modal-popup_enterActive,
.transition_modal-popup_leaveActive {
> .bg { > .bg {
transition: opacity 0.1s !important; transition: opacity 0.1s !important;
} }
@@ -336,7 +349,8 @@ defineExpose({
transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1), transform 0.1s cubic-bezier(0, 0, 0.2, 1) !important; transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1), transform 0.1s cubic-bezier(0, 0, 0.2, 1) !important;
} }
} }
.modal-popup-enter-from, .modal-popup-leave-to { .transition_modal-popup_enterFrom,
.transition_modal-popup_leaveTo {
> .bg { > .bg {
opacity: 0; opacity: 0;
} }
@@ -349,7 +363,7 @@ defineExpose({
} }
} }
.modal-drawer-enter-active { .transition_modal-drawer_enterActive {
> .bg { > .bg {
transition: opacity 0.2s !important; transition: opacity 0.2s !important;
} }
@@ -358,7 +372,7 @@ defineExpose({
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important; transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
} }
} }
.modal-drawer-leave-active { .transition_modal-drawer_leaveActive {
> .bg { > .bg {
transition: opacity 0.2s !important; transition: opacity 0.2s !important;
} }
@@ -367,7 +381,8 @@ defineExpose({
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important; transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
} }
} }
.modal-drawer-enter-from, .modal-drawer-leave-to { .transition_modal-drawer_enterFrom,
.transition_modal-drawer_leaveTo {
> .bg { > .bg {
opacity: 0; opacity: 0;
} }
@@ -378,15 +393,7 @@ defineExpose({
} }
} }
.qzhlnise { .root {
> .bg {
&.transparent {
background: transparent;
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
}
&.dialog { &.dialog {
> .content { > .content {
position: fixed; position: fixed;
@@ -407,16 +414,6 @@ defineExpose({
-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%); -webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%);
mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%); mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%);
} }
> ::v-deep(*) {
margin: auto;
}
&.top {
> ::v-deep(*) {
margin-top: 0;
}
}
} }
} }
@@ -444,12 +441,15 @@ defineExpose({
left: 0; left: 0;
right: 0; right: 0;
margin: auto; margin: auto;
> ::v-deep(*) {
margin: auto;
}
} }
} }
}
.bg {
&.bgTransparent {
background: transparent;
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
} }
</style> </style>

View File

@@ -117,6 +117,7 @@ function onContextmenu(ev: MouseEvent) {
<style lang="scss" scoped> <style lang="scss" scoped>
.hrmcaedk { .hrmcaedk {
margin: auto;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -85,6 +85,7 @@ defineExpose({
<style lang="scss" scoped> <style lang="scss" scoped>
.ebkgoccj { .ebkgoccj {
margin: auto;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -493,7 +493,7 @@ function readPromo() {
bottom: 1em; bottom: 1em;
} }
.howLessLabel { .showLessLabel {
display: inline-block; display: inline-block;
background: var(--popup); background: var(--popup);
padding: 6px 10px; padding: 6px 10px;

View File

@@ -1,12 +1,12 @@
<template> <template>
<div class="fefdfafb"> <div :class="$style.root">
<MkAvatar class="avatar" :user="$i"/> <MkAvatar :class="$style.avatar" :user="$i"/>
<div class="main"> <div :class="$style.main">
<div class="header"> <div :class="$style.header">
<MkUserName :user="$i"/> <MkUserName :user="$i"/>
</div> </div>
<div class="body"> <div>
<div class="content"> <div :class="$style.content">
<Mfm :text="text.trim()" :author="$i" :i="$i"/> <Mfm :text="text.trim()" :author="$i" :i="$i"/>
</div> </div>
</div> </div>
@@ -22,75 +22,48 @@ const props = defineProps<{
}>(); }>();
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.fefdfafb { .root {
display: flex; display: flex;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: clip; overflow: clip;
font-size: 0.95em; font-size: 0.95em;
}
> .avatar { .avatar {
flex-shrink: 0; flex-shrink: 0 !important;
display: block; display: block !important;
margin: 0 10px 0 0; margin: 0 10px 0 0 !important;
width: 40px; width: 40px !important;
height: 40px; height: 40px !important;
border-radius: 8px; border-radius: 8px !important;
pointer-events: none; pointer-events: none !important;
} }
> .main { .main {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
}
> .header { .header {
margin-bottom: 2px; margin-bottom: 2px;
font-weight: bold; font-weight: bold;
}
> .body {
> .cw {
cursor: default;
display: block;
margin: 0;
padding: 0;
overflow-wrap: break-word;
> .text {
margin-right: 8px;
}
}
> .content {
> .text {
cursor: default;
margin: 0;
padding: 0;
}
}
}
}
} }
@container (min-width: 350px) { @container (min-width: 350px) {
.fefdfafb { .avatar {
> .avatar { margin: 0 10px 0 0 !important;
margin: 0 10px 0 0; width: 44px !important;
width: 44px; height: 44px !important;
height: 44px;
}
} }
} }
@container (min-width: 500px) { @container (min-width: 500px) {
.fefdfafb { .avatar {
> .avatar { margin: 0 12px 0 0 !important;
margin: 0 12px 0 0; width: 48px !important;
width: 48px; height: 48px !important;
height: 48px;
}
} }
} }
</style> </style>

View File

@@ -8,7 +8,7 @@
</template> </template>
<template #default="{ items: notifications }"> <template #default="{ items: notifications }">
<MkDateSeparatedList v-slot="{ item: notification }" class="elsfgstc" :items="notifications" :no-gap="true"> <MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :no-gap="true">
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/> <XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/> <XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/>
</MkDateSeparatedList> </MkDateSeparatedList>
@@ -97,8 +97,8 @@ onUnmounted(() => {
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.elsfgstc { .list {
background: var(--panel); background: var(--panel);
} }
</style> </style>

View File

@@ -17,7 +17,7 @@
</template> </template>
</template> </template>
<div class="yrolvcoq" :style="{ background: pageMetadata?.value?.bg }" style="container-type: inline-size;"> <div :class="$style.root" :style="{ background: pageMetadata?.value?.bg }" style="container-type: inline-size;">
<RouterView :router="router"/> <RouterView :router="router"/>
</div> </div>
</MkWindow> </MkWindow>
@@ -133,8 +133,8 @@ defineExpose({
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.yrolvcoq { .root {
min-height: 100%; min-height: 100%;
background: var(--bg); background: var(--bg);

View File

@@ -1,5 +1,11 @@
<template> <template>
<Transition :name="$store.state.animation ? 'fade' : ''" mode="out-in"> <Transition
:enter-active-class="$store.state.animation ? $style.transition_fade_enterActive : ''"
:leave-active-class="$store.state.animation ? $style.transition_fade_leaveActive : ''"
:enter-from-class="$store.state.animation ? $style.transition_fade_enterFrom : ''"
:leave-to-class="$store.state.animation ? $style.transition_fade_leaveTo : ''"
mode="out-in"
>
<MkLoading v-if="fetching"/> <MkLoading v-if="fetching"/>
<MkError v-else-if="error" @retry="init()"/> <MkError v-else-if="error" @retry="init()"/>
@@ -14,15 +20,15 @@
</div> </div>
<div v-else ref="rootEl"> <div v-else ref="rootEl">
<div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _margin"> <div v-show="pagination.reversed && more" key="_more_" class="_margin">
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore"> <MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMore : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
{{ i18n.ts.loadMore }} {{ i18n.ts.loadMore }}
</MkButton> </MkButton>
<MkLoading v-else class="loading"/> <MkLoading v-else class="loading"/>
</div> </div>
<slot :items="items" :fetching="fetching || moreFetching"></slot> <slot :items="items" :fetching="fetching || moreFetching"></slot>
<div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _margin"> <div v-show="!pagination.reversed && more" key="_more_" class="_margin">
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore"> <MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMore : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
{{ i18n.ts.loadMore }} {{ i18n.ts.loadMore }}
</MkButton> </MkButton>
<MkLoading v-else class="loading"/> <MkLoading v-else class="loading"/>
@@ -95,7 +101,7 @@ const isBackTop = ref(false);
const empty = computed(() => items.value.length === 0); const empty = computed(() => items.value.length === 0);
const error = ref(false); const error = ref(false);
const { const {
enableInfiniteScroll enableInfiniteScroll,
} = defaultStore.reactiveState; } = defaultStore.reactiveState;
const contentEl = $computed(() => props.pagination.pageEl || rootEl); const contentEl = $computed(() => props.pagination.pageEl || rootEl);
@@ -292,7 +298,7 @@ const prepend = (item: MisskeyEntity): void => {
function unshiftItems(newItems: MisskeyEntity[]) { function unshiftItems(newItems: MisskeyEntity[]) {
const length = newItems.length + items.value.length; const length = newItems.length + items.value.length;
items.value = [ ...newItems, ...items.value ].slice(0, props.displayLimit); items.value = [...newItems, ...items.value].slice(0, props.displayLimit);
if (length >= props.displayLimit) more.value = true; if (length >= props.displayLimit) more.value = true;
} }
@@ -331,7 +337,7 @@ onActivated(() => {
}); });
onDeactivated(() => { onDeactivated(() => {
isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl ? rootEl?.scrollHeight - window.innerHeight : 0) : window.scrollY === 0; isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl ? rootEl.scrollHeight - window.innerHeight : 0) : window.scrollY === 0;
}); });
function toBottom() { function toBottom() {
@@ -372,20 +378,18 @@ defineExpose({
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.fade-enter-active, .transition_fade_enterActive,
.fade-leave-active { .transition_fade_leaveActive {
transition: opacity 0.125s ease; transition: opacity 0.125s ease;
} }
.fade-enter-from, .transition_fade_enterFrom,
.fade-leave-to { .transition_fade_leaveTo {
opacity: 0; opacity: 0;
} }
.cxiknjgy { .more {
> .button { margin-left: auto;
margin-left: auto; margin-right: auto;
margin-right: auto;
}
} }
</style> </style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @close="emit('closing')" @closed="emit('closed')"> <MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @close="emit('closing')" @closed="emit('closed')">
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/> <MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="modal.close()"/>
</MkModal> </MkModal>
</template> </template>
@@ -26,12 +26,10 @@ const emit = defineEmits<{
let modal = $shallowRef<InstanceType<typeof MkModal>>(); let modal = $shallowRef<InstanceType<typeof MkModal>>();
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.sfhdhdhq { .drawer {
&.drawer { border-radius: 24px;
border-radius: 24px; border-bottom-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-left-radius: 0;
border-bottom-left-radius: 0;
}
} }
</style> </style>

View File

@@ -1,66 +1,65 @@
<template> <template>
<div <div
class="gafaadew" :class="[$style.root, { [$style.modal]: modal, _popup: modal }]"
:class="{ modal, _popup: modal }"
@dragover.stop="onDragover" @dragover.stop="onDragover"
@dragenter="onDragenter" @dragenter="onDragenter"
@dragleave="onDragleave" @dragleave="onDragleave"
@drop.stop="onDrop" @drop.stop="onDrop"
> >
<header> <header :class="$style.header">
<button v-if="!fixed" class="cancel _button" @click="cancel"><i class="ti ti-x"></i></button> <button v-if="!fixed" :class="$style.cancel" class="_button" @click="cancel"><i class="ti ti-x"></i></button>
<button v-click-anime v-tooltip="i18n.ts.switchAccount" class="account _button" @click="openAccountMenu"> <button v-click-anime v-tooltip="i18n.ts.switchAccount" :class="$style.account" class="_button" @click="openAccountMenu">
<MkAvatar :user="postAccount ?? $i" class="avatar"/> <MkAvatar :user="postAccount ?? $i" :class="$style.avatar"/>
</button> </button>
<div class="right"> <div :class="$style.headerRight">
<span class="text-count" :class="{ over: textLength > maxTextLength }">{{ maxTextLength - textLength }}</span> <span :class="[$style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</span>
<span v-if="localOnly" class="local-only"><i class="ti ti-world-off"></i></span> <span v-if="localOnly" :class="$style.localOnly"><i class="ti ti-world-off"></i></span>
<button ref="visibilityButton" v-tooltip="i18n.ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility"> <button ref="visibilityButton" v-tooltip="i18n.ts.visibility" class="_button" :class="$style.visibility" :disabled="channel != null" @click="setVisibility">
<span v-if="visibility === 'public'"><i class="ti ti-world"></i></span> <span v-if="visibility === 'public'"><i class="ti ti-world"></i></span>
<span v-if="visibility === 'home'"><i class="ti ti-home"></i></span> <span v-if="visibility === 'home'"><i class="ti ti-home"></i></span>
<span v-if="visibility === 'followers'"><i class="ti ti-lock"></i></span> <span v-if="visibility === 'followers'"><i class="ti ti-lock"></i></span>
<span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span> <span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span>
</button> </button>
<button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button> <button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.previewButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>
<button v-click-anime class="submit _button" :class="{ posting }" :disabled="!canPost" data-cy-open-post-form-submit @click="post"> <button v-click-anime class="_button" :class="[$style.submit, { [$style.submitPosting]: posting }]" :disabled="!canPost" data-cy-open-post-form-submit @click="post">
<div class="inner"> <div :class="$style.submitInner">
<template v-if="posted"></template> <template v-if="posted"></template>
<template v-else-if="posting"><MkEllipsis/></template> <template v-else-if="posting"><MkEllipsis/></template>
<template v-else>{{ submitText }}</template> <template v-else>{{ submitText }}</template>
<i :class="posted ? 'ti ti-check' : reply ? 'ti ti-arrow-back-up' : renote ? 'ti ti-quote' : 'ti ti-send'"></i> <i style="margin-left: 6px;" :class="posted ? 'ti ti-check' : reply ? 'ti ti-arrow-back-up' : renote ? 'ti ti-quote' : 'ti ti-send'"></i>
</div> </div>
</button> </button>
</div> </div>
</header> </header>
<div class="form" :class="{ fixed }"> <div :class="[$style.form]">
<MkNoteSimple v-if="reply" class="preview" :note="reply"/> <MkNoteSimple v-if="reply" :class="$style.targetNote" :note="reply"/>
<MkNoteSimple v-if="renote" class="preview" :note="renote"/> <MkNoteSimple v-if="renote" :class="$style.targetNote" :note="renote"/>
<div v-if="quoteId" class="with-quote"><i class="ti ti-quote"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ti ti-x"></i></button></div> <div v-if="quoteId" :class="$style.withQuote"><i class="ti ti-quote"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ti ti-x"></i></button></div>
<div v-if="visibility === 'specified'" class="to-specified"> <div v-if="visibility === 'specified'" :class="$style.toSpecified">
<span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span> <span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span>
<div class="visibleUsers"> <div :class="$style.visibleUsers">
<span v-for="u in visibleUsers" :key="u.id"> <span v-for="u in visibleUsers" :key="u.id" :class="$style.visibleUser">
<MkAcct :user="u"/> <MkAcct :user="u"/>
<button class="_button" @click="removeVisibleUser(u)"><i class="ti ti-x"></i></button> <button class="_button" style="padding: 4px 8px;" @click="removeVisibleUser(u)"><i class="ti ti-x"></i></button>
</span> </span>
<button class="_buttonPrimary" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button> <button class="_buttonPrimary" style="padding: 4px; border-radius: 8px;" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button>
</div> </div>
</div> </div>
<MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo> <MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
<input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown"> <input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
<textarea ref="textareaEl" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting || posted" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> <textarea ref="textareaEl" v-model="text" :class="[$style.text, { [$style.withCw]: useCw }]" :disabled="posting || posted" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
<XPostFormAttaches v-model="files" class="attaches" @detach="detachFile" @change-sensitive="updateFileSensitive" @change-name="updateFileName"/> <XPostFormAttaches v-model="files" :class="$style.attaches" @detach="detachFile" @change-sensitive="updateFileSensitive" @change-name="updateFileName"/>
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> <MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
<XNotePreview v-if="showPreview" class="preview" :text="text"/> <XNotePreview v-if="showPreview" :class="$style.preview" :text="text"/>
<footer> <footer :class="$style.footer">
<button v-tooltip="i18n.ts.attachFile" class="_button" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button> <button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button>
<button v-tooltip="i18n.ts.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button> <button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
<button v-tooltip="i18n.ts.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button> <button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
<button v-tooltip="i18n.ts.mention" class="_button" @click="insertMention"><i class="ti ti-at"></i></button> <button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button> <button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
<button v-tooltip="i18n.ts.emoji" class="_button" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button> <button v-tooltip="i18n.ts.emoji" class="_button" :class="$style.footerButton" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" @click="showActions"><i class="ti ti-plug"></i></button> <button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
</footer> </footer>
<datalist id="hashtags"> <datalist id="hashtags">
<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/> <option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/>
@@ -742,306 +741,275 @@ defineExpose({
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.gafaadew { .root {
position: relative; position: relative;
&.modal { &.modal {
width: 100%; width: 100%;
max-width: 520px; max-width: 520px;
} }
}
> header { .header {
z-index: 1000; z-index: 1000;
height: 66px; height: 66px;
}
> .cancel { .cancel {
padding: 0; padding: 0;
font-size: 1em; font-size: 1em;
width: 64px; width: 64px;
line-height: 66px; line-height: 66px;
} }
> .account { .account {
height: 100%; height: 100%;
aspect-ratio: 1/1; aspect-ratio: 1/1;
display: inline-flex; display: inline-flex;
vertical-align: bottom; vertical-align: bottom;
}
> .avatar { .avatar {
width: 28px; width: 28px;
height: 28px; height: 28px;
margin: auto; margin: auto;
} }
}
> .right { .headerRight {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
}
> .text-count { .textCount {
opacity: 0.7; opacity: 0.7;
line-height: 66px; line-height: 66px;
} }
> .visibility { .visibility {
height: 34px; height: 34px;
width: 34px; width: 34px;
margin: 0 0 0 8px; margin: 0 0 0 8px;
& + .localOnly { & + .localOnly {
margin-left: 0 !important; margin-left: 0 !important;
} }
} }
> .local-only {
margin: 0 0 0 12px;
opacity: 0.7;
}
> .preview { .localOnly {
display: inline-block; margin: 0 0 0 12px;
padding: 0; opacity: 0.7;
margin: 0 8px 0 0; }
font-size: 16px;
width: 34px;
height: 34px;
border-radius: 6px;
&:hover { .previewButton {
background: var(--X5); display: inline-block;
} padding: 0;
margin: 0 8px 0 0;
font-size: 16px;
width: 34px;
height: 34px;
border-radius: 6px;
&.active { &:hover {
color: var(--accent); background: var(--X5);
} }
}
> .submit { &.previewButtonActive {
margin: 16px 16px 16px 0; color: var(--accent);
vertical-align: bottom; }
}
&:disabled { .submit {
opacity: 0.7; margin: 16px 16px 16px 0;
} vertical-align: bottom;
&.posting { &:disabled {
cursor: wait; opacity: 0.7;
} }
&:not(:disabled):hover { &.posting {
> .inner { cursor: wait;
background: linear-gradient(90deg, var(--X8), var(--X8)); }
}
}
&:not(:disabled):active { &:not(:disabled):hover {
> .inner { > .inner {
background: linear-gradient(90deg, var(--X8), var(--X8)); background: linear-gradient(90deg, var(--X8), var(--X8));
}
}
> .inner {
padding: 0 12px;
line-height: 34px;
font-weight: bold;
border-radius: 4px;
font-size: 0.9em;
min-width: 90px;
box-sizing: border-box;
color: var(--fgOnAccent);
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
> i {
margin-left: 6px;
}
}
}
} }
} }
> .form { &:not(:disabled):active {
> .preview { > .inner {
padding: 16px; background: linear-gradient(90deg, var(--X8), var(--X8));
} }
}
}
> .with-quote { .submitInner {
margin: 0 0 8px 0; padding: 0 12px;
color: var(--accent); line-height: 34px;
font-weight: bold;
border-radius: 4px;
font-size: 0.9em;
min-width: 90px;
box-sizing: border-box;
color: var(--fgOnAccent);
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
}
> button { .form {
padding: 4px 8px; }
color: var(--accentAlpha04);
&:hover { .preview {
color: var(--accentAlpha06); padding: 16px 20px 0 20px;
} }
&:active { .targetNote {
color: var(--accentDarken30); padding: 0 20px 16px 20px;
} }
}
}
> .to-specified { .withQuote {
padding: 6px 24px; margin: 0 0 8px 0;
margin-bottom: 8px; color: var(--accent);
overflow: auto; }
white-space: nowrap;
> .visibleUsers { .toSpecified {
display: inline; padding: 6px 24px;
top: -1px; margin-bottom: 8px;
font-size: 14px; overflow: auto;
white-space: nowrap;
}
> button { .visibleUsers {
padding: 4px; display: inline;
border-radius: 8px; top: -1px;
} font-size: 14px;
}
> span { .visibleUser {
margin-right: 14px; margin-right: 14px;
padding: 8px 0 8px 8px; padding: 8px 0 8px 8px;
border-radius: 8px; border-radius: 8px;
background: var(--X4); background: var(--X4);
}
> button { .hasNotSpecifiedMentions {
padding: 4px 8px; margin: 0 20px 16px 20px;
} }
}
}
}
> .hasNotSpecifiedMentions { .cw,
margin: 0 20px 16px 20px; .hashtags,
} .text {
display: block;
box-sizing: border-box;
padding: 0 24px;
margin: 0;
width: 100%;
font-size: 16px;
border: none;
border-radius: 0;
background: transparent;
color: var(--fg);
font-family: inherit;
> .cw, &:focus {
> .hashtags, outline: none;
> .text { }
display: block;
box-sizing: border-box;
padding: 0 24px;
margin: 0;
width: 100%;
font-size: 16px;
border: none;
border-radius: 0;
background: transparent;
color: var(--fg);
font-family: inherit;
&:focus { &:disabled {
outline: none; opacity: 0.5;
} }
}
&:disabled { .cw {
opacity: 0.5; z-index: 1;
} padding-bottom: 8px;
} border-bottom: solid 0.5px var(--divider);
}
> .cw { .hashtags {
z-index: 1; z-index: 1;
padding-bottom: 8px; padding-top: 8px;
border-bottom: solid 0.5px var(--divider); padding-bottom: 8px;
} border-top: solid 0.5px var(--divider);
}
> .hashtags { .text {
z-index: 1; max-width: 100%;
padding-top: 8px; min-width: 100%;
padding-bottom: 8px; min-height: 90px;
border-top: solid 0.5px var(--divider);
}
> .text { &.withCw {
max-width: 100%; padding-top: 8px;
min-width: 100%; }
min-height: 90px; }
&.withCw { .footer {
padding-top: 8px; padding: 0 16px 16px 16px;
} }
}
> footer { .footerButton {
padding: 0 16px 16px 16px; display: inline-block;
padding: 0;
margin: 0;
font-size: 1em;
width: 46px;
height: 46px;
border-radius: 6px;
> button { &:hover {
display: inline-block; background: var(--X5);
padding: 0; }
margin: 0;
font-size: 1em;
width: 46px;
height: 46px;
border-radius: 6px;
&:hover { &.footerButtonActive {
background: var(--X5); color: var(--accent);
}
&.active {
color: var(--accent);
}
}
}
} }
} }
@container (max-width: 500px) { @container (max-width: 500px) {
.gafaadew { .header {
> header { height: 50px;
height: 50px;
> .cancel { > .cancel {
width: 50px; width: 50px;
line-height: 50px;
}
> .headerRight {
> .textCount {
line-height: 50px; line-height: 50px;
} }
> .right { > .submit {
> .text-count { margin: 8px;
line-height: 50px;
}
> .submit {
margin: 8px;
}
} }
} }
}
> .form { .toSpecified {
> .to-specified { padding: 6px 16px;
padding: 6px 16px; }
}
> .cw, .cw,
> .hashtags, .hashtags,
> .text { .text {
padding: 0 16px; padding: 0 16px;
} }
> .text { .text {
min-height: 80px; min-height: 80px;
} }
> footer { .footer {
padding: 0 8px 8px 8px; padding: 0 8px 8px 8px;
}
}
} }
} }
@container (max-width: 310px) { @container (max-width: 310px) {
.gafaadew { .footerButton {
> .form { font-size: 14px;
> footer { width: 44px;
> button { height: 44px;
font-size: 14px;
width: 44px;
height: 44px;
}
}
}
} }
} }
</style> </style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<MkModal ref="modal" :prefer-type="'dialog:top'" @click="modal.close()" @closed="onModalClosed()"> <MkModal ref="modal" :prefer-type="'dialog'" @click="modal.close()" @closed="onModalClosed()">
<MkPostForm ref="form" v-bind="props" autofocus freeze-after-posted @posted="onPosted" @cancel="modal.close()" @esc="modal.close()"/> <MkPostForm ref="form" style="margin: 0 auto auto auto;" v-bind="props" autofocus freeze-after-posted @posted="onPosted" @cancel="modal.close()" @esc="modal.close()"/>
</MkModal> </MkModal>
</template> </template>

View File

@@ -173,7 +173,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
$thumbWidth: 20px; $thumbWidth: 20px;
> .body { > .body {
padding: 8px 12px; padding: 7px 12px;
background: var(--panel); background: var(--panel);
border: solid 1px var(--panel); border: solid 1px var(--panel);
border-radius: 6px; border-radius: 6px;

View File

@@ -1,6 +1,6 @@
<template> <template>
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }"> <div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
<span class="text" :class="{ up }"> <span :class="[$style.text, { [$style.up]: up }]">
<MkReactionIcon class="icon" :reaction="reaction"/> <MkReactionIcon class="icon" :reaction="reaction"/>
</span> </span>
</div> </div>
@@ -43,30 +43,28 @@ onMounted(() => {
position: fixed; position: fixed;
width: 128px; width: 128px;
height: 128px; height: 128px;
}
&:global { .text {
> .text { display: block;
display: block; height: 1em;
height: 1em; text-align: center;
text-align: center; position: absolute;
position: absolute; top: 0;
top: 0; left: 0;
left: 0; right: 0;
right: 0; bottom: 0;
bottom: 0; margin: auto;
margin: auto; color: var(--accent);
color: var(--accent); font-size: 18px;
font-size: 18px; font-weight: bold;
font-weight: bold; transform: translateY(-30px);
transform: translateY(-30px); transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5);
transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5); will-change: opacity, transform;
will-change: opacity, transform;
&.up { &.up {
opacity: 0; opacity: 0;
transform: translateY(-50px) rotateZ(v-bind(angle)); transform: translateY(-50px) rotateZ(v-bind(angle));
}
}
} }
} }
</style> </style>

View File

@@ -1,8 +1,8 @@
<template> <template>
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> <MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
<div class="beeadbfb"> <div :class="$style.root">
<MkReactionIcon :reaction="reaction" class="icon" :no-style="true"/> <MkReactionIcon :reaction="reaction" :class="$style.icon" :no-style="true"/>
<div class="name">{{ reaction.replace('@.', '') }}</div> <div :class="$style.name">{{ reaction.replace('@.', '') }}</div>
</div> </div>
</MkTooltip> </MkTooltip>
</template> </template>
@@ -23,20 +23,20 @@ const emit = defineEmits<{
}>(); }>();
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.beeadbfb { .root {
text-align: center; text-align: center;
}
> .icon { .icon {
display: block; display: block;
width: 60px; width: 60px;
font-size: 60px; // unicodeな絵文字についてはwidthが効かないため font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
margin: 0 auto; margin: 0 auto;
object-fit: contain; object-fit: contain;
} }
> .name { .name {
font-size: 0.9em; font-size: 0.9em;
}
} }
</style> </style>

View File

@@ -1,16 +1,16 @@
<template> <template>
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> <MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
<div class="bqxuuuey"> <div :class="$style.root">
<div class="reaction"> <div :class="$style.reaction">
<MkReactionIcon :reaction="reaction" class="icon" :no-style="true"/> <MkReactionIcon :reaction="reaction" :class="$style.reactionIcon" :no-style="true"/>
<div class="name">{{ getReactionName(reaction) }}</div> <div :class="$style.reactionName">{{ getReactionName(reaction) }}</div>
</div> </div>
<div class="users"> <div :class="$style.users">
<div v-for="u in users" :key="u.id" class="user"> <div v-for="u in users" :key="u.id" :class="$style.user">
<MkAvatar class="avatar" :user="u"/> <MkAvatar :class="$style.avatar" :user="u"/>
<MkUserName class="name" :user="u" :nowrap="true"/> <MkUserName :user="u" :nowrap="true"/>
</div> </div>
<div v-if="users.length > 10" class="omitted">+{{ count - 10 }}</div> <div v-if="users.length > 10">+{{ count - 10 }}</div>
</div> </div>
</div> </div>
</MkTooltip> </MkTooltip>
@@ -43,53 +43,53 @@ function getReactionName(reaction: string): string {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.bqxuuuey { .root {
display: flex; display: flex;
}
> .reaction { .reaction {
max-width: 100px; max-width: 100px;
text-align: center; text-align: center;
}
> .icon { .reactionIcon {
display: block; display: block;
width: 60px; width: 60px;
font-size: 60px; // unicodeな絵文字についてはwidthが効かないため font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
object-fit: contain; object-fit: contain;
margin: 0 auto; margin: 0 auto;
} }
> .name { .reactionName {
font-size: 1em; font-size: 1em;
} }
}
> .users { .users {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
font-size: 0.95em; font-size: 0.95em;
border-left: solid 0.5px var(--divider); border-left: solid 0.5px var(--divider);
padding-left: 10px; padding-left: 10px;
margin-left: 10px; margin-left: 10px;
margin-right: 14px; margin-right: 14px;
text-align: left; text-align: left;
}
> .user { .user {
line-height: 24px; line-height: 24px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
&:not(:last-child) { &:not(:last-child) {
margin-bottom: 3px; margin-bottom: 3px;
}
> .avatar {
width: 24px;
height: 24px;
margin-right: 3px;
}
}
} }
} }
.avatar {
width: 24px;
height: 24px;
margin-right: 3px;
}
</style> </style>

View File

@@ -2,12 +2,12 @@
<button <button
ref="buttonEl" ref="buttonEl"
v-ripple="canToggle" v-ripple="canToggle"
class="hkzvhatu _button" class="_button"
:class="{ reacted: note.myReaction == reaction, canToggle }" :class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle }]"
@click="toggleReaction()" @click="toggleReaction()"
> >
<MkReactionIcon class="icon" :reaction="reaction"/> <MkReactionIcon :class="$style.icon" :reaction="reaction"/>
<span class="count">{{ count }}</span> <span :class="$style.count">{{ count }}</span>
</button> </button>
</template> </template>
@@ -92,8 +92,8 @@ useTooltip(buttonEl, async (showing) => {
}, 100); }, 100);
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.hkzvhatu { .root {
display: inline-block; display: inline-block;
height: 32px; height: 32px;
margin: 2px; margin: 2px;
@@ -127,11 +127,11 @@ useTooltip(buttonEl, async (showing) => {
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5)); filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5));
} }
} }
}
> .count { .count {
font-size: 0.9em; font-size: 0.9em;
line-height: 32px; line-height: 32px;
margin: 0 0 0 4px; margin: 0 0 0 4px;
}
} }
</style> </style>

View File

@@ -1,5 +1,12 @@
<template> <template>
<TransitionGroup :name="$store.state.animation ? 'x' : ''" tag="div" class="tdflqwzn" :class="{ isMe }"> <TransitionGroup
:enter-active-class="$store.state.animation ? $style.transition_x_enterActive : ''"
:leave-active-class="$store.state.animation ? $style.transition_x_leaveActive : ''"
:enter-from-class="$store.state.animation ? $style.transition_x_enterFrom : ''"
:leave-to-class="$store.state.animation ? $style.transition_x_leaveTo : ''"
:move-class="$store.state.animation ? $style.transition_x_move : ''"
tag="div" :class="$style.root"
>
<XReaction v-for="(count, reaction) in note.reactions" :key="reaction" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note"/> <XReaction v-for="(count, reaction) in note.reactions" :key="reaction" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note"/>
</TransitionGroup> </TransitionGroup>
</template> </template>
@@ -19,29 +26,26 @@ const initialReactions = new Set(Object.keys(props.note.reactions));
const isMe = computed(() => $i && $i.id === props.note.userId); const isMe = computed(() => $i && $i.id === props.note.userId);
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.x-move, .x-enter-active, .x-leave-active { .transition_x_move,
.transition_x_enterActive,
.transition_x_leaveActive {
transition: opacity 0.2s cubic-bezier(0,.5,.5,1), transform 0.2s cubic-bezier(0,.5,.5,1) !important; transition: opacity 0.2s cubic-bezier(0,.5,.5,1), transform 0.2s cubic-bezier(0,.5,.5,1) !important;
} }
.x-enter-from, .x-leave-to { .transition_x_enterFrom,
.transition_x_leaveTo {
opacity: 0; opacity: 0;
transform: scale(0.7); transform: scale(0.7);
} }
.x-leave-active { .transition_x_leaveActive {
position: absolute; position: absolute;
} }
.tdflqwzn { .root {
margin: 4px -2px 0 -2px; margin: 4px -2px 0 -2px;
&:empty { &:empty {
display: none; display: none;
} }
&.isMe {
> span {
cursor: default !important;
}
}
} }
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="jmgmzlwq"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserCaution }}<a class="link" :href="href" rel="nofollow noopener" target="_blank">{{ i18n.ts.showOnRemote }}</a></div> <div :class="$style.root"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserCaution }}<a :class="$style.link" :href="href" rel="nofollow noopener" target="_blank">{{ i18n.ts.showOnRemote }}</a></div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -10,18 +10,18 @@ defineProps<{
}>(); }>();
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.jmgmzlwq { .root {
font-size: 0.8em; font-size: 0.8em;
padding: 16px; padding: 16px;
background: var(--infoWarnBg); background: var(--infoWarnBg);
color: var(--infoWarnFg); color: var(--infoWarnFg);
border-radius: var(--radius); border-radius: var(--radius);
overflow: clip; overflow: clip;
}
> .link { .link {
margin-left: 4px; margin-left: 4px;
color: var(--accent); color: var(--accent);
}
} }
</style> </style>

View File

@@ -65,9 +65,9 @@ const prefixEl = ref(null);
const suffixEl = ref(null); const suffixEl = ref(null);
const container = ref(null); const container = ref(null);
const height = const height =
props.small ? 34 : props.small ? 33 :
props.large ? 40 : props.large ? 39 :
37; 36;
const focus = () => inputEl.value.focus(); const focus = () => inputEl.value.focus();
const onInput = (ev) => { const onInput = (ev) => {

View File

@@ -1,6 +1,6 @@
<template> <template>
<span class="mk-sparkle"> <span :class="$style.root">
<span ref="el"> <span ref="el" style="display: inline-block;">
<slot></slot> <slot></slot>
</span> </span>
<!-- なぜか path に対する key が機能しないため <!-- なぜか path に対する key が機能しないため
@@ -32,7 +32,7 @@
</path> </path>
</svg> </svg>
--> -->
<svg v-for="particle in particles" :key="particle.id" :width="width" :height="height" :viewBox="`0 0 ${width} ${height}`" xmlns="http://www.w3.org/2000/svg"> <svg v-for="particle in particles" :key="particle.id" :width="width" :height="height" :viewBox="`0 0 ${width} ${height}`" xmlns="http://www.w3.org/2000/svg" style="position: absolute; top: -32px; left: -32px;">
<path <path
style="transform-origin: center; transform-box: fill-box;" style="transform-origin: center; transform-box: fill-box;"
:transform="`translate(${particle.x} ${particle.y})`" :transform="`translate(${particle.x} ${particle.y})`"
@@ -111,20 +111,10 @@ onUnmounted(() => {
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.mk-sparkle { .root {
position: relative; position: relative;
display: inline-block; display: inline-block;
pointer-events: none;
> span {
display: inline-block;
}
> svg {
position: absolute;
top: -32px;
left: -32px;
pointer-events: none;
}
} }
</style> </style>

View File

@@ -1,11 +1,11 @@
<template> <template>
<div class="wrmlmaau" :class="{ collapsed }"> <div :class="[$style.root, { [$style.collapsed]: collapsed }]">
<div class="body"> <div :class="$style.body">
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span> <span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> <MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="note.text" v-once :text="note.text" :author="note.user" :i="$i"/> <Mfm v-if="note.text" v-once :text="note.text" :author="note.user" :i="$i"/>
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> <MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
</div> </div>
<details v-if="note.files.length > 0"> <details v-if="note.files.length > 0">
<summary>({{ $t('withNFiles', { n: note.files.length }) }})</summary> <summary>({{ $t('withNFiles', { n: note.files.length }) }})</summary>
@@ -15,8 +15,8 @@
<summary>{{ i18n.ts.poll }}</summary> <summary>{{ i18n.ts.poll }}</summary>
<MkPoll :note="note"/> <MkPoll :note="note"/>
</details> </details>
<button v-if="collapsed" class="fade _button" @click="collapsed = false"> <button v-if="collapsed" :class="$style.fade" class="_button" @click="collapsed = false">
<span>{{ i18n.ts.showMore }}</span> <span :class="$style.fadeLabel">{{ i18n.ts.showMore }}</span>
</button> </button>
</div> </div>
</template> </template>
@@ -39,23 +39,10 @@ const collapsed = $ref(
)); ));
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.wrmlmaau { .root {
overflow-wrap: break-word; overflow-wrap: break-word;
> .body {
> .reply {
margin-right: 6px;
color: var(--accent);
}
> .rp {
margin-left: 4px;
font-style: oblique;
color: var(--renote);
}
}
&.collapsed { &.collapsed {
position: relative; position: relative;
max-height: 9em; max-height: 9em;
@@ -70,7 +57,7 @@ const collapsed = $ref(
height: 64px; height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15)); background: linear-gradient(0deg, var(--panel), var(--X15));
> span { > .fadeLabel {
display: inline-block; display: inline-block;
background: var(--panel); background: var(--panel);
padding: 6px 10px; padding: 6px 10px;
@@ -80,11 +67,26 @@ const collapsed = $ref(
} }
&:hover { &:hover {
> span { > .fadeLabel {
background: var(--panelHighlight); background: var(--panelHighlight);
} }
} }
} }
} }
} }
.body {
}
.reply {
margin-right: 6px;
color: var(--accent);
}
.rp {
margin-left: 4px;
font-style: oblique;
color: var(--renote);
}
</style> </style>

View File

@@ -49,7 +49,7 @@ onMounted(() => {
transform: translateY(-100%); transform: translateY(-100%);
} }
> .root { .root {
position: fixed; position: fixed;
left: 0; left: 0;
right: 0; right: 0;

View File

@@ -34,6 +34,7 @@ onMounted(() => {
<style lang="scss" module> <style lang="scss" module>
.root { .root {
margin: auto;
position: relative; position: relative;
padding: 32px; padding: 32px;
min-width: 320px; min-width: 320px;

View File

@@ -1,5 +1,5 @@
<template> <template>
<div v-tooltip="text" class="fzgwjkgc" :class="user.onlineStatus"></div> <div v-tooltip="text" :class="[$style.root, $style['status_' + user.onlineStatus]]"></div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -21,24 +21,24 @@ const text = $computed(() => {
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.fzgwjkgc { .root {
box-shadow: 0 0 0 3px var(--panel); box-shadow: 0 0 0 3px var(--panel);
border-radius: 120%; // Blinkのバグか知らんけど、100%ぴったりにすると何故か若干楕円でレンダリングされる border-radius: 120%; // Blinkのバグか知らんけど、100%ぴったりにすると何故か若干楕円でレンダリングされる
&.online { &.status_online {
background: #58d4c9; background: #58d4c9;
} }
&.active { &.status_active {
background: #e4bc48; background: #e4bc48;
} }
&.offline { &.status_offline {
background: #ea5353; background: #ea5353;
} }
&.unknown { &.status_unknown {
background: #888; background: #888;
} }
} }

View File

@@ -1,42 +1,42 @@
<template> <template>
<MkModal ref="modal" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')"> <MkModal ref="modal" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
<div class="gqyayizv _popup"> <div class="_popup" :class="$style.root">
<button key="public" class="_button item" :class="{ active: v === 'public' }" data-index="1" @click="choose('public')"> <button key="public" class="_button" :class="[$style.item, { [$style.active]: v === 'public' }]" data-index="1" @click="choose('public')">
<div class="icon"><i class="ti ti-world"></i></div> <div :class="$style.icon"><i class="ti ti-world"></i></div>
<div class="body"> <div :class="$style.body">
<span>{{ i18n.ts._visibility.public }}</span> <span :class="$style.itemTitle">{{ i18n.ts._visibility.public }}</span>
<span>{{ i18n.ts._visibility.publicDescription }}</span> <span :class="$style.itemDescription">{{ i18n.ts._visibility.publicDescription }}</span>
</div> </div>
</button> </button>
<button key="home" class="_button item" :class="{ active: v === 'home' }" data-index="2" @click="choose('home')"> <button key="home" class="_button" :class="[$style.item, { [$style.active]: v === 'home' }]" data-index="2" @click="choose('home')">
<div class="icon"><i class="ti ti-home"></i></div> <div :class="$style.icon"><i class="ti ti-home"></i></div>
<div class="body"> <div :class="$style.body">
<span>{{ i18n.ts._visibility.home }}</span> <span :class="$style.itemTitle">{{ i18n.ts._visibility.home }}</span>
<span>{{ i18n.ts._visibility.homeDescription }}</span> <span :class="$style.itemDescription">{{ i18n.ts._visibility.homeDescription }}</span>
</div> </div>
</button> </button>
<button key="followers" class="_button item" :class="{ active: v === 'followers' }" data-index="3" @click="choose('followers')"> <button key="followers" class="_button" :class="[$style.item, { [$style.active]: v === 'followers' }]" data-index="3" @click="choose('followers')">
<div class="icon"><i class="ti ti-lock"></i></div> <div :class="$style.icon"><i class="ti ti-lock"></i></div>
<div class="body"> <div :class="$style.body">
<span>{{ i18n.ts._visibility.followers }}</span> <span :class="$style.itemTitle">{{ i18n.ts._visibility.followers }}</span>
<span>{{ i18n.ts._visibility.followersDescription }}</span> <span :class="$style.itemDescription">{{ i18n.ts._visibility.followersDescription }}</span>
</div> </div>
</button> </button>
<button key="specified" :disabled="localOnly" class="_button item" :class="{ active: v === 'specified' }" data-index="4" @click="choose('specified')"> <button key="specified" :disabled="localOnly" class="_button" :class="[$style.item, { [$style.active]: v === 'specified' }]" data-index="4" @click="choose('specified')">
<div class="icon"><i class="ti ti-mail"></i></div> <div :class="$style.icon"><i class="ti ti-mail"></i></div>
<div class="body"> <div :class="$style.body">
<span>{{ i18n.ts._visibility.specified }}</span> <span :class="$style.itemTitle">{{ i18n.ts._visibility.specified }}</span>
<span>{{ i18n.ts._visibility.specifiedDescription }}</span> <span :class="$style.itemDescription">{{ i18n.ts._visibility.specifiedDescription }}</span>
</div> </div>
</button> </button>
<div class="divider"></div> <div :class="$style.divider"></div>
<button key="localOnly" class="_button item localOnly" :class="{ active: localOnly }" data-index="5" @click="localOnly = !localOnly"> <button key="localOnly" class="_button" :class="[$style.item, $style.localOnly, { [$style.active]: localOnly }]" data-index="5" @click="localOnly = !localOnly">
<div class="icon"><i class="ti ti-world-off"></i></div> <div :class="$style.icon"><i class="ti ti-world-off"></i></div>
<div class="body"> <div :class="$style.body">
<span>{{ i18n.ts._visibility.localOnly }}</span> <span :class="$style.itemTitle">{{ i18n.ts._visibility.localOnly }}</span>
<span>{{ i18n.ts._visibility.localOnlyDescription }}</span> <span :class="$style.itemDescription">{{ i18n.ts._visibility.localOnlyDescription }}</span>
</div> </div>
<div class="toggle"><i :class="localOnly ? 'ti ti-toggle-right' : 'ti ti-toggle-left'"></i></div> <div :class="$style.toggle"><i :class="localOnly ? 'ti ti-toggle-right' : 'ti ti-toggle-left'"></i></div>
</button> </button>
</div> </div>
</MkModal> </MkModal>
@@ -79,81 +79,81 @@ function choose(visibility: typeof misskey.noteVisibilities[number]): void {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.gqyayizv { .root {
width: 240px; width: 240px;
padding: 8px 0; padding: 8px 0;
}
> .divider { .divider {
margin: 8px 0; margin: 8px 0;
border-top: solid 0.5px var(--divider); border-top: solid 0.5px var(--divider);
}
.item {
display: flex;
padding: 8px 14px;
font-size: 12px;
text-align: left;
width: 100%;
box-sizing: border-box;
&:hover {
background: rgba(0, 0, 0, 0.05);
} }
> .item { &:active {
display: flex; background: rgba(0, 0, 0, 0.1);
padding: 8px 14px; }
font-size: 12px;
text-align: left;
width: 100%;
box-sizing: border-box;
&:hover { &.active {
background: rgba(0, 0, 0, 0.05); color: var(--fgOnAccent);
} background: var(--accent);
}
&:active { &.localOnly.active {
background: rgba(0, 0, 0, 0.1); color: var(--accent);
} background: inherit;
&.active {
color: var(--fgOnAccent);
background: var(--accent);
}
&.localOnly.active {
color: var(--accent);
background: inherit;
}
> .icon {
display: flex;
justify-content: center;
align-items: center;
margin-right: 10px;
width: 16px;
top: 0;
bottom: 0;
margin-top: auto;
margin-bottom: auto;
}
> .body {
flex: 1 1 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
> span:first-child {
display: block;
font-weight: bold;
}
> span:last-child:not(:first-child) {
opacity: 0.6;
}
}
> .toggle {
display: flex;
justify-content: center;
align-items: center;
margin-left: 10px;
width: 16px;
top: 0;
bottom: 0;
margin-top: auto;
margin-bottom: auto;
}
} }
} }
.icon {
display: flex;
justify-content: center;
align-items: center;
margin-right: 10px;
width: 16px;
top: 0;
bottom: 0;
margin-top: auto;
margin-bottom: auto;
}
.body {
flex: 1 1 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.itemTitle {
display: block;
font-weight: bold;
}
.itemDescription {
opacity: 0.6;
}
.toggle {
display: flex;
justify-content: center;
align-items: center;
margin-left: 10px;
width: 16px;
top: 0;
bottom: 0;
margin-top: auto;
margin-bottom: auto;
}
</style> </style>

View File

@@ -1,9 +1,9 @@
<template> <template>
<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="success ? done() : () => {}" @closed="emit('closed')"> <MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="success ? done() : () => {}" @closed="emit('closed')">
<div class="iuyakobc" :class="{ iconOnly: (text == null) || success }"> <div :class="[$style.root, { [$style.iconOnly]: (text == null) || success }]">
<i v-if="success" class="ti ti-check icon success"></i> <i v-if="success" :class="[$style.icon, $style.success]" class="ti ti-check"></i>
<MkLoading v-else class="icon waiting" :em="true"/> <MkLoading v-else :class="[$style.icon, $style.waiting]" :em="true"/>
<div v-if="text && !success" class="text">{{ text }}<MkEllipsis/></div> <div v-if="text && !success" :class="$style.text">{{ text }}<MkEllipsis/></div>
</div> </div>
</MkModal> </MkModal>
</template> </template>
@@ -35,8 +35,9 @@ watch(() => props.showing, () => {
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.iuyakobc { .root {
margin: auto;
position: relative; position: relative;
padding: 32px; padding: 32px;
box-sizing: border-box; box-sizing: border-box;
@@ -53,21 +54,21 @@ watch(() => props.showing, () => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
}
> .icon { .icon {
font-size: 32px; font-size: 32px;
&.success { &.success {
color: var(--accent); color: var(--accent);
}
&.waiting {
opacity: 0.7;
}
} }
> .text { &.waiting {
margin-top: 16px; opacity: 0.7;
} }
} }
.text {
margin-top: 16px;
}
</style> </style>

View File

@@ -19,9 +19,9 @@
@update:model-value="v => emit('updateWidgets', v)" @update:model-value="v => emit('updateWidgets', v)"
> >
<template #item="{element}"> <template #item="{element}">
<div :class="[$style.widget, $style['customize-container']]"> <div :class="[$style.widget, $style['customize-container']]" class="data-cy-customize-container">
<button :class="$style['customize-container-config']" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button> <button :class="$style['customize-container-config']" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button>
<button :class="$style['customize-container-remove']" class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button> <button :class="$style['customize-container-remove']" class="_button data-cy-customize-container-remove" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button>
<div class="handle"> <div class="handle">
<component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style['customize-container-handle-widget']" :widget="element" @update-props="updateWidget(element.id, $event)"/> <component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style['customize-container-handle-widget']" :widget="element" @update-props="updateWidget(element.id, $event)"/>
</div> </div>
@@ -171,7 +171,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
&-widget { &-widget {
pointer-events: none; pointer-events: none;
} }
} }
} }

View File

@@ -1,27 +1,27 @@
<template> <template>
<div class="terlnhxf"> <div :class="$style.root">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { provide } from 'vue';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
minWidth?: number; minWidth?: number;
}>(), { }>(), {
minWidth: 210, minWidth: 210,
}); });
provide('splited', true);
const minWidth = props.minWidth + 'px'; const minWidth = props.minWidth + 'px';
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.terlnhxf { .root {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(v-bind('minWidth'), 1fr)); grid-template-columns: repeat(auto-fill, minmax(v-bind('minWidth'), 1fr));
grid-gap: 12px; grid-gap: 12px;
> ::v-deep(*) {
margin: 0 !important;
}
} }
</style> </style>

View File

@@ -1,16 +1,16 @@
<template> <template>
<div v-if="chosen" class="qiivuoyo"> <div v-if="chosen" :class="$style.root">
<div v-if="!showMenu" class="main" :class="chosen.place"> <div v-if="!showMenu" :class="[$style.main, $style['form_' + chosen.place]]">
<a :href="chosen.url" target="_blank"> <a :href="chosen.url" target="_blank" :class="$style.link">
<img :src="chosen.imageUrl"> <img :src="chosen.imageUrl" :class="$style.img">
<button class="_button menu" @click.prevent.stop="toggleMenu"><span class="ti ti-info-circle info-circle"></span></button> <button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button>
</a> </a>
</div> </div>
<div v-else class="menu"> <div v-else :class="$style.menu">
<div class="body"> <div :class="$style.menuContainer">
<div>Ads by {{ host }}</div> <div>Ads by {{ host }}</div>
<!--<MkButton class="button" primary>{{ $ts._ad.like }}</MkButton>--> <!--<MkButton class="button" primary>{{ $ts._ad.like }}</MkButton>-->
<MkButton v-if="chosen.ratio !== 0" class="button" @click="reduceFrequency">{{ $ts._ad.reduceFrequencyOfThisAd }}</MkButton> <MkButton v-if="chosen.ratio !== 0" :class="$style.menuButton" @click="reduceFrequency">{{ $ts._ad.reduceFrequencyOfThisAd }}</MkButton>
<button class="_textButton" @click="toggleMenu">{{ $ts._ad.back }}</button> <button class="_textButton" @click="toggleMenu">{{ $ts._ad.back }}</button>
</div> </div>
</div> </div>
@@ -92,95 +92,99 @@ function reduceFrequency(): void {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.qiivuoyo { .root {
background-size: auto auto; background-size: auto auto;
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--ad) 8px, var(--ad) 14px ); background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--ad) 8px, var(--ad) 14px );
}
> .main { .main {
text-align: center; text-align: center;
> a { &.form_square {
display: inline-block; > .link,
position: relative; > .link > .img {
vertical-align: bottom; max-width: min(300px, 100%);
max-height: 300px;
&:hover {
> img {
filter: contrast(120%);
}
}
> img {
display: block;
object-fit: contain;
margin: auto;
border-radius: 5px;
}
> .menu {
position: absolute;
top: 1px;
right: 1px;
> .info-circle {
border: 3px solid var(--panel);
border-radius: 50%;
background: var(--panel);
}
}
}
&.square {
> a ,
> a > img {
max-width: min(300px, 100%);
max-height: 300px;
}
}
&.horizontal {
padding: 8px;
> a ,
> a > img {
max-width: min(600px, 100%);
max-height: 80px;
}
}
&.horizontal-big {
padding: 8px;
> a ,
> a > img {
max-width: min(600px, 100%);
max-height: 250px;
}
}
&.vertical {
> a ,
> a > img {
max-width: min(100px, 100%);
}
} }
} }
> .menu { &.form_horizontal {
padding: 8px; padding: 8px;
text-align: center;
> .body { > .link,
padding: 8px; > .link > .img {
margin: 0 auto; max-width: min(600px, 100%);
max-width: 400px; max-height: 80px;
border: solid 1px var(--divider); }
}
> .button { &.form_horizontal-big {
margin: 8px auto; padding: 8px;
}
> .link,
> .link > .img {
max-width: min(600px, 100%);
max-height: 250px;
}
}
&.form_vertical {
> .link,
> .link > .img {
max-width: min(100px, 100%);
} }
} }
} }
.link {
display: inline-block;
position: relative;
vertical-align: bottom;
&:hover {
> .img {
filter: contrast(120%);
}
}
}
.img {
display: block;
object-fit: contain;
margin: auto;
border-radius: 5px;
}
.i {
position: absolute;
top: 1px;
right: 1px;
display: grid;
place-content: center;
background: var(--panel);
border-radius: 100%;
padding: 2px;
}
.iIcon {
font-size: 14px;
line-height: 17px;
}
.menu {
padding: 8px;
text-align: center;
}
.menuContainer {
padding: 8px;
margin: 0 auto;
max-width: 400px;
border: solid 1px var(--divider);
}
.menuButton {
margin: 8px auto;
}
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :is-note="isNote" class="havbbuyv" :class="{ nowrap }"/> <MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :is-note="isNote" :class="[$style.root, { [$style.nowrap]: nowrap }]"/>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -157,8 +157,8 @@ const props = withDefaults(defineProps<{
} }
</style> </style>
<style lang="scss" scoped> <style lang="scss" module>
.havbbuyv { .root {
white-space: pre-wrap; white-space: pre-wrap;
&.nowrap { &.nowrap {
@@ -167,24 +167,5 @@ const props = withDefaults(defineProps<{
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
::v-deep(.quote) {
display: block;
margin: 8px;
padding: 6px 0 6px 12px;
color: var(--fg);
border-left: solid 3px var(--fg);
opacity: 0.7;
}
::v-deep(pre) {
font-size: 0.8em;
}
> ::v-deep(code) {
font-size: 0.8em;
word-break: break-all;
padding: 4px 6px;
}
} }
</style> </style>

View File

@@ -1,36 +1,36 @@
<template> <template>
<div v-if="show" ref="el" class="fdidabkb" :class="{ slim: narrow, thin: thin_ }" :style="{ background: bg }" @click="onClick"> <div v-if="show" ref="el" :class="[$style.root, { [$style.slim]: narrow, [$style.thin]: thin_ }]" :style="{ background: bg }" @click="onClick">
<div v-if="narrow" class="buttons left"> <div v-if="narrow" :class="$style.buttonsLeft">
<MkAvatar v-if="props.displayMyAvatar && $i" class="avatar" :user="$i" :disable-preview="true"/> <MkAvatar v-if="props.displayMyAvatar && $i" :class="$style.avatar" :user="$i" :disable-preview="true"/>
</div> </div>
<template v-if="metadata"> <template v-if="metadata">
<div v-if="!hideTitle" class="titleContainer" @click="showTabsPopup"> <div v-if="!hideTitle" :class="$style.titleContainer" @click="showTabsPopup">
<MkAvatar v-if="metadata.avatar" class="avatar" :user="metadata.avatar" :disable-preview="true" :show-indicator="true"/> <MkAvatar v-if="metadata.avatar" :class="$style.titleAvatar" :user="metadata.avatar" :disable-preview="true" :show-indicator="true"/>
<i v-else-if="metadata.icon" class="icon" :class="metadata.icon"></i> <i v-else-if="metadata.icon" :class="[$style.titleIcon, metadata.icon]"></i>
<div class="title"> <div :class="$style.title">
<MkUserName v-if="metadata.userName" :user="metadata.userName" :nowrap="true" class="title"/> <MkUserName v-if="metadata.userName" :user="metadata.userName" :nowrap="true"/>
<div v-else-if="metadata.title" class="title">{{ metadata.title }}</div> <div v-else-if="metadata.title">{{ metadata.title }}</div>
<div v-if="!narrow && metadata.subtitle" class="subtitle"> <div v-if="!narrow && metadata.subtitle" :class="$style.subtitle">
{{ metadata.subtitle }} {{ metadata.subtitle }}
</div> </div>
<div v-if="narrow && hasTabs" class="subtitle activeTab"> <div v-if="narrow && hasTabs" :class="[$style.subtitle, $style.activeTab]">
{{ tabs.find(tab => tab.key === props.tab)?.title }} {{ tabs.find(tab => tab.key === props.tab)?.title }}
<i class="chevron ti ti-chevron-down"></i> <i class="ti ti-chevron-down" :class="$style.chevron"></i>
</div> </div>
</div> </div>
</div> </div>
<div v-if="!narrow || hideTitle" class="tabs"> <div v-if="!narrow || hideTitle" :class="$style.tabs">
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = (el as HTMLElement)" v-tooltip.noDelay="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)"> <button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = (el as HTMLElement)" v-tooltip.noDelay="tab.title" class="_button" :class="[$style.tab, { [$style.active]: tab.key != null && tab.key === props.tab }]" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
<i v-if="tab.icon" class="icon" :class="tab.icon"></i> <i v-if="tab.icon" :class="[$style.tabIcon, tab.icon]"></i>
<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span> <span v-if="!tab.iconOnly" :class="$style.tabTitle">{{ tab.title }}</span>
</button> </button>
<div ref="tabHighlightEl" class="highlight"></div> <div ref="tabHighlightEl" :class="$style.tabHighlight"></div>
</div> </div>
</template> </template>
<div class="buttons right"> <div :class="$style.buttonsRight">
<template v-for="action in actions"> <template v-for="action in actions">
<button v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button> <button v-tooltip.noDelay="action.text" class="_button" :class="[$style.button, { [$style.highlighted]: action.highlighted }]" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
</template> </template>
</div> </div>
</div> </div>
@@ -178,8 +178,8 @@ onUnmounted(() => {
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.fdidabkb { .root {
--height: 50px; --height: 50px;
display: flex; display: flex;
width: 100%; width: 100%;
@@ -215,154 +215,156 @@ onUnmounted(() => {
} }
} }
} }
}
> .buttons { .buttons {
--margin: 8px; --margin: 8px;
display: flex; display: flex;
align-items: center; align-items: center;
min-width: var(--height); min-width: var(--height);
height: var(--height); height: var(--height);
margin: 0 var(--margin); margin: 0 var(--margin);
&.left { &:empty {
margin-right: auto; width: var(--height);
}
}
> .avatar { .buttonsLeft {
$size: 32px; composes: buttons;
display: inline-block; margin-right: auto;
width: $size; }
height: $size;
vertical-align: bottom;
margin: 0 8px;
pointer-events: none;
}
}
&.right { .buttonsRight {
margin-left: auto; composes: buttons;
} margin-left: auto;
}
&:empty { .avatar {
width: var(--height); $size: 32px;
} display: inline-block;
width: $size;
height: $size;
vertical-align: bottom;
margin: 0 8px;
pointer-events: none;
}
> .button { .button {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: calc(var(--height) - (var(--margin) * 2)); height: calc(var(--height) - (var(--margin) * 2));
width: calc(var(--height) - (var(--margin) * 2)); width: calc(var(--height) - (var(--margin) * 2));
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
border-radius: 5px; border-radius: 5px;
&:hover { &:hover {
background: rgba(0, 0, 0, 0.05); background: rgba(0, 0, 0, 0.05);
}
&.highlighted {
color: var(--accent);
}
}
> .fullButton {
& + .fullButton {
margin-left: 12px;
}
}
} }
> .titleContainer { &.highlighted {
display: flex; color: var(--accent);
align-items: center;
max-width: 400px;
overflow: auto;
white-space: nowrap;
text-align: left;
font-weight: bold;
flex-shrink: 0;
margin-left: 24px;
> .avatar {
$size: 32px;
display: inline-block;
width: $size;
height: $size;
vertical-align: bottom;
margin: 0 8px;
pointer-events: none;
}
> .icon {
margin-right: 8px;
width: 16px;
text-align: center;
}
> .title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.1;
> .subtitle {
opacity: 0.6;
font-size: 0.8em;
font-weight: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.activeTab {
text-align: center;
> .chevron {
display: inline-block;
margin-left: 6px;
}
}
}
}
} }
}
> .tabs { .fullButton {
position: relative; & + .fullButton {
margin-left: 16px; margin-left: 12px;
font-size: 0.8em; }
overflow: auto; }
white-space: nowrap;
> .tab { .titleContainer {
display: flex;
align-items: center;
max-width: 400px;
overflow: auto;
white-space: nowrap;
text-align: left;
font-weight: bold;
flex-shrink: 0;
margin-left: 24px;
}
.titleAvatar {
$size: 32px;
display: inline-block;
width: $size;
height: $size;
vertical-align: bottom;
margin: 0 8px;
pointer-events: none;
}
.titleIcon {
margin-right: 8px;
width: 16px;
text-align: center;
}
.title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.1;
}
.subtitle {
opacity: 0.6;
font-size: 0.8em;
font-weight: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.activeTab {
text-align: center;
> .chevron {
display: inline-block; display: inline-block;
position: relative; margin-left: 6px;
padding: 0 10px;
height: 100%;
font-weight: normal;
opacity: 0.7;
&:hover {
opacity: 1;
}
&.active {
opacity: 1;
}
> .icon + .title {
margin-left: 8px;
}
}
> .highlight {
position: absolute;
bottom: 0;
height: 3px;
background: var(--accent);
border-radius: 999px;
transition: all 0.2s ease;
pointer-events: none;
} }
} }
} }
.tabs {
position: relative;
margin-left: 16px;
font-size: 0.8em;
overflow: auto;
white-space: nowrap;
}
.tab {
display: inline-block;
position: relative;
padding: 0 10px;
height: 100%;
font-weight: normal;
opacity: 0.7;
&:hover {
opacity: 1;
}
&.active {
opacity: 1;
}
}
.tabIcon + .tabTitle {
margin-left: 8px;
}
.tabHighlight {
position: absolute;
bottom: 0;
height: 3px;
background: var(--accent);
border-radius: 999px;
transition: all 0.2s ease;
pointer-events: none;
}
</style> </style>

View File

@@ -12,6 +12,15 @@ import MkA from '@/components/global/MkA.vue';
import { host } from '@/config'; import { host } from '@/config';
import { MFM_TAGS } from '@/scripts/mfm-tags'; import { MFM_TAGS } from '@/scripts/mfm-tags';
const QUOTE_STYLE = `
display: block;
margin: 8px;
padding: 6px 0 6px 12px;
color: var(--fg);
border-left: solid 3px var(--fg);
opacity: 0.7;
`.split('\n').join(' ');
export default defineComponent({ export default defineComponent({
props: { props: {
text: { text: {
@@ -276,11 +285,11 @@ export default defineComponent({
case 'quote': { case 'quote': {
if (!this.nowrap) { if (!this.nowrap) {
return [h('div', { return [h('div', {
class: 'quote', style: QUOTE_STYLE,
}, genEl(token.children))]; }, genEl(token.children))];
} else { } else {
return [h('span', { return [h('span', {
class: 'quote', style: QUOTE_STYLE,
}, genEl(token.children))]; }, genEl(token.children))];
} }
} }
@@ -290,7 +299,8 @@ export default defineComponent({
key: Math.random(), key: Math.random(),
emoji: `:${token.props.name}:`, emoji: `:${token.props.name}:`,
normal: this.plain, normal: this.plain,
host: this.author.host, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
host: this.author?.host,
})]; })];
} }

View File

@@ -4,6 +4,7 @@ import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue';
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import insertTextAtCursor from 'insert-text-at-cursor'; import insertTextAtCursor from 'insert-text-at-cursor';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { i18n } from './i18n';
import MkPostFormDialog from '@/components/MkPostFormDialog.vue'; import MkPostFormDialog from '@/components/MkPostFormDialog.vue';
import MkWaitingDialog from '@/components/MkWaitingDialog.vue'; import MkWaitingDialog from '@/components/MkWaitingDialog.vue';
import { MenuItem } from '@/types/menu'; import { MenuItem } from '@/types/menu';
@@ -17,9 +18,19 @@ export const apiWithDialog = ((
) => { ) => {
const promise = api(endpoint, data, token); const promise = api(endpoint, data, token);
promiseDialog(promise, null, (err) => { promiseDialog(promise, null, (err) => {
let title = null;
let text = err.message + '\n' + (err as any).id;
if (err.code === 'RATE_LIMIT_EXCEEDED') {
title = i18n.ts.cannotPerformTemporary;
text = i18n.ts.cannotPerformTemporaryDescription;
} else if (err.code.startsWith('TOO_MANY')) {
title = i18n.ts.youCannotCreateAnymore;
text = `${i18n.ts.error}: ${err.id}`;
}
alert({ alert({
type: 'error', type: 'error',
text: err.message + '\n' + (err as any).id, title,
text,
}); });
}); });

View File

@@ -38,6 +38,19 @@
<FormSlot> <FormSlot>
<template #label>{{ i18n.ts._role.options }}</template> <template #label>{{ i18n.ts._role.options }}</template>
<div class="_gaps_s"> <div class="_gaps_s">
<MkFolder>
<template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template>
<template #suffix>{{ options_rateLimitFactor_useDefault ? i18n.ts._role.useBaseValue : `${Math.floor(options_rateLimitFactor_value * 100)}%` }}</template>
<div class="_gaps">
<MkSwitch v-model="options_rateLimitFactor_useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkRange :model-value="options_rateLimitFactor_value * 100" :min="30" :max="300" :step="10" :text-converter="(v) => `${v}%`" @update:model-value="v => options_rateLimitFactor_value = (v / 100)">
<template #caption>{{ i18n.ts._role._options.descriptionOfRateLimitFactor }}</template>
</MkRange>
</div>
</MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template> <template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
<template #suffix>{{ options_gtlAvailable_useDefault ? i18n.ts._role.useBaseValue : (options_gtlAvailable_value ? i18n.ts.yes : i18n.ts.no) }}</template> <template #suffix>{{ options_gtlAvailable_useDefault ? i18n.ts._role.useBaseValue : (options_gtlAvailable_value ? i18n.ts.yes : i18n.ts.no) }}</template>
@@ -116,6 +129,18 @@
</div> </div>
</MkFolder> </MkFolder>
<MkFolder>
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
<template #suffix>{{ options_pinLimit_useDefault ? i18n.ts._role.useBaseValue : (options_pinLimit_value) }}</template>
<div class="_gaps">
<MkSwitch v-model="options_pinLimit_useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkInput v-model="options_pinLimit_value" :disabled="options_pinLimit_useDefault" type="number" :readonly="readonly">
</MkInput>
</div>
</MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.antennaMax }}</template> <template #label>{{ i18n.ts._role._options.antennaMax }}</template>
<template #suffix>{{ options_antennaLimit_useDefault ? i18n.ts._role.useBaseValue : (options_antennaLimit_value) }}</template> <template #suffix>{{ options_antennaLimit_useDefault ? i18n.ts._role.useBaseValue : (options_antennaLimit_value) }}</template>
@@ -152,6 +177,54 @@
</MkInput> </MkInput>
</div> </div>
</MkFolder> </MkFolder>
<MkFolder>
<template #label>{{ i18n.ts._role._options.clipMax }}</template>
<template #suffix>{{ options_clipLimit_useDefault ? i18n.ts._role.useBaseValue : (options_clipLimit_value) }}</template>
<div class="_gaps">
<MkSwitch v-model="options_clipLimit_useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkInput v-model="options_clipLimit_value" :disabled="options_clipLimit_useDefault" type="number" :readonly="readonly">
</MkInput>
</div>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts._role._options.noteEachClipsMax }}</template>
<template #suffix>{{ options_noteEachClipsLimit_useDefault ? i18n.ts._role.useBaseValue : (options_noteEachClipsLimit_value) }}</template>
<div class="_gaps">
<MkSwitch v-model="options_noteEachClipsLimit_useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkInput v-model="options_noteEachClipsLimit_value" :disabled="options_noteEachClipsLimit_useDefault" type="number" :readonly="readonly">
</MkInput>
</div>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts._role._options.userListMax }}</template>
<template #suffix>{{ options_userListLimit_useDefault ? i18n.ts._role.useBaseValue : (options_userListLimit_value) }}</template>
<div class="_gaps">
<MkSwitch v-model="options_userListLimit_useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkInput v-model="options_userListLimit_value" :disabled="options_userListLimit_useDefault" type="number" :readonly="readonly">
</MkInput>
</div>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts._role._options.userEachUserListsMax }}</template>
<template #suffix>{{ options_userEachUserListsLimit_useDefault ? i18n.ts._role.useBaseValue : (options_userEachUserListsLimit_value) }}</template>
<div class="_gaps">
<MkSwitch v-model="options_userEachUserListsLimit_useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkInput v-model="options_userEachUserListsLimit_value" :disabled="options_userEachUserListsLimit_useDefault" type="number" :readonly="readonly">
</MkInput>
</div>
</MkFolder>
</div> </div>
</FormSlot> </FormSlot>
@@ -181,9 +254,11 @@ import MkTextarea from '@/components/MkTextarea.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkRange from '@/components/MkRange.vue';
import FormSlot from '@/components/form/slot.vue'; import FormSlot from '@/components/form/slot.vue';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { instance } from '@/instance';
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'created', payload: any): void; (ev: 'created', payload: any): void;
@@ -206,23 +281,35 @@ let condFormula = $ref(role?.condFormula ?? { id: uuid(), type: 'isRemote' });
let isPublic = $ref(role?.isPublic ?? false); let isPublic = $ref(role?.isPublic ?? false);
let canEditMembersByModerator = $ref(role?.canEditMembersByModerator ?? false); let canEditMembersByModerator = $ref(role?.canEditMembersByModerator ?? false);
let options_gtlAvailable_useDefault = $ref(role?.options?.gtlAvailable?.useDefault ?? true); let options_gtlAvailable_useDefault = $ref(role?.options?.gtlAvailable?.useDefault ?? true);
let options_gtlAvailable_value = $ref(role?.options?.gtlAvailable?.value ?? false); let options_gtlAvailable_value = $ref(role?.options?.gtlAvailable?.value ?? instance.baseRole.gtlAvailable);
let options_ltlAvailable_useDefault = $ref(role?.options?.ltlAvailable?.useDefault ?? true); let options_ltlAvailable_useDefault = $ref(role?.options?.ltlAvailable?.useDefault ?? true);
let options_ltlAvailable_value = $ref(role?.options?.ltlAvailable?.value ?? false); let options_ltlAvailable_value = $ref(role?.options?.ltlAvailable?.value ?? instance.baseRole.ltlAvailable);
let options_canPublicNote_useDefault = $ref(role?.options?.canPublicNote?.useDefault ?? true); let options_canPublicNote_useDefault = $ref(role?.options?.canPublicNote?.useDefault ?? true);
let options_canPublicNote_value = $ref(role?.options?.canPublicNote?.value ?? false); let options_canPublicNote_value = $ref(role?.options?.canPublicNote?.value ?? instance.baseRole.canPublicNote);
let options_canInvite_useDefault = $ref(role?.options?.canInvite?.useDefault ?? true); let options_canInvite_useDefault = $ref(role?.options?.canInvite?.useDefault ?? true);
let options_canInvite_value = $ref(role?.options?.canInvite?.value ?? false); let options_canInvite_value = $ref(role?.options?.canInvite?.value ?? instance.baseRole.canInvite);
let options_canManageCustomEmojis_useDefault = $ref(role?.options?.canManageCustomEmojis?.useDefault ?? true); let options_canManageCustomEmojis_useDefault = $ref(role?.options?.canManageCustomEmojis?.useDefault ?? true);
let options_canManageCustomEmojis_value = $ref(role?.options?.canManageCustomEmojis?.value ?? false); let options_canManageCustomEmojis_value = $ref(role?.options?.canManageCustomEmojis?.value ?? instance.baseRole.canManageCustomEmojis);
let options_driveCapacityMb_useDefault = $ref(role?.options?.driveCapacityMb?.useDefault ?? true); let options_driveCapacityMb_useDefault = $ref(role?.options?.driveCapacityMb?.useDefault ?? true);
let options_driveCapacityMb_value = $ref(role?.options?.driveCapacityMb?.value ?? 0); let options_driveCapacityMb_value = $ref(role?.options?.driveCapacityMb?.value ?? instance.baseRole.driveCapacityMb);
let options_pinLimit_useDefault = $ref(role?.options?.pinLimit?.useDefault ?? true);
let options_pinLimit_value = $ref(role?.options?.pinLimit?.value ?? instance.baseRole.pinLimit);
let options_antennaLimit_useDefault = $ref(role?.options?.antennaLimit?.useDefault ?? true); let options_antennaLimit_useDefault = $ref(role?.options?.antennaLimit?.useDefault ?? true);
let options_antennaLimit_value = $ref(role?.options?.antennaLimit?.value ?? 0); let options_antennaLimit_value = $ref(role?.options?.antennaLimit?.value ?? instance.baseRole.antennaLimit);
let options_wordMuteLimit_useDefault = $ref(role?.options?.wordMuteLimit?.useDefault ?? true); let options_wordMuteLimit_useDefault = $ref(role?.options?.wordMuteLimit?.useDefault ?? true);
let options_wordMuteLimit_value = $ref(role?.options?.wordMuteLimit?.value ?? 0); let options_wordMuteLimit_value = $ref(role?.options?.wordMuteLimit?.value ?? instance.baseRole.wordMuteLimit);
let options_webhookLimit_useDefault = $ref(role?.options?.webhookLimit?.useDefault ?? true); let options_webhookLimit_useDefault = $ref(role?.options?.webhookLimit?.useDefault ?? true);
let options_webhookLimit_value = $ref(role?.options?.webhookLimit?.value ?? 0); let options_webhookLimit_value = $ref(role?.options?.webhookLimit?.value ?? instance.baseRole.webhookLimit);
let options_clipLimit_useDefault = $ref(role?.options?.clipLimit?.useDefault ?? true);
let options_clipLimit_value = $ref(role?.options?.clipLimit?.value ?? instance.baseRole.clipLimit);
let options_noteEachClipsLimit_useDefault = $ref(role?.options?.noteEachClipsLimit?.useDefault ?? true);
let options_noteEachClipsLimit_value = $ref(role?.options?.noteEachClipsLimit?.value ?? instance.baseRole.noteEachClipsLimit);
let options_userListLimit_useDefault = $ref(role?.options?.userListLimit?.useDefault ?? true);
let options_userListLimit_value = $ref(role?.options?.userListLimit?.value ?? instance.baseRole.userListLimit);
let options_userEachUserListsLimit_useDefault = $ref(role?.options?.userEachUserListsLimit?.useDefault ?? true);
let options_userEachUserListsLimit_value = $ref(role?.options?.userEachUserListsLimit?.value ?? instance.baseRole.userEachUserListsLimit);
let options_rateLimitFactor_useDefault = $ref(role?.options?.rateLimitFactor?.useDefault ?? true);
let options_rateLimitFactor_value = $ref(role?.options?.rateLimitFactor?.value ?? instance.baseRole.rateLimitFactor);
if (_DEV_) { if (_DEV_) {
watch($$(condFormula), () => { watch($$(condFormula), () => {
@@ -238,9 +325,15 @@ function getOptions() {
canInvite: { useDefault: options_canInvite_useDefault, value: options_canInvite_value }, canInvite: { useDefault: options_canInvite_useDefault, value: options_canInvite_value },
canManageCustomEmojis: { useDefault: options_canManageCustomEmojis_useDefault, value: options_canManageCustomEmojis_value }, canManageCustomEmojis: { useDefault: options_canManageCustomEmojis_useDefault, value: options_canManageCustomEmojis_value },
driveCapacityMb: { useDefault: options_driveCapacityMb_useDefault, value: options_driveCapacityMb_value }, driveCapacityMb: { useDefault: options_driveCapacityMb_useDefault, value: options_driveCapacityMb_value },
pinLimit: { useDefault: options_pinLimit_useDefault, value: options_pinLimit_value },
antennaLimit: { useDefault: options_antennaLimit_useDefault, value: options_antennaLimit_value }, antennaLimit: { useDefault: options_antennaLimit_useDefault, value: options_antennaLimit_value },
wordMuteLimit: { useDefault: options_wordMuteLimit_useDefault, value: options_wordMuteLimit_value }, wordMuteLimit: { useDefault: options_wordMuteLimit_useDefault, value: options_wordMuteLimit_value },
webhookLimit: { useDefault: options_webhookLimit_useDefault, value: options_webhookLimit_value }, webhookLimit: { useDefault: options_webhookLimit_useDefault, value: options_webhookLimit_value },
clipLimit: { useDefault: options_clipLimit_useDefault, value: options_clipLimit_value },
noteEachClipsLimit: { useDefault: options_noteEachClipsLimit_useDefault, value: options_noteEachClipsLimit_value },
userListLimit: { useDefault: options_userListLimit_useDefault, value: options_userListLimit_value },
userEachUserListsLimit: { useDefault: options_userEachUserListsLimit_useDefault, value: options_userEachUserListsLimit_value },
rateLimitFactor: { useDefault: options_rateLimitFactor_useDefault, value: options_rateLimitFactor_value },
}; };
} }

Some files were not shown because too many files have changed in this diff Show More