Merge branch 'io' into merge-upstream
This commit is contained in:
commit
50e811e862
|
@ -72,7 +72,7 @@ dbReplications: false
|
||||||
#───┘ Redis configuration └─────────────────────────────────────
|
#───┘ Redis configuration └─────────────────────────────────────
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
host: redis
|
host: keydb
|
||||||
port: 6379
|
port: 6379
|
||||||
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
#pass: example-pass
|
#pass: example-pass
|
||||||
|
@ -80,7 +80,7 @@ redis:
|
||||||
#db: 1
|
#db: 1
|
||||||
|
|
||||||
#redisForPubsub:
|
#redisForPubsub:
|
||||||
# host: redis
|
# host: keydb
|
||||||
# port: 6379
|
# port: 6379
|
||||||
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
# #pass: example-pass
|
# #pass: example-pass
|
||||||
|
@ -88,7 +88,7 @@ redis:
|
||||||
# #db: 1
|
# #db: 1
|
||||||
|
|
||||||
#redisForJobQueue:
|
#redisForJobQueue:
|
||||||
# host: redis
|
# host: keydb
|
||||||
# port: 6379
|
# port: 6379
|
||||||
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
# #pass: example-pass
|
# #pass: example-pass
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
"version": "8.9.2"
|
"version": "8.9.2"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"version": "18.18.0"
|
"version": "20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"forwardPorts": [3000],
|
"forwardPorts": [3000],
|
||||||
|
|
|
@ -72,7 +72,7 @@ dbReplications: false
|
||||||
#───┘ Redis configuration └─────────────────────────────────────
|
#───┘ Redis configuration └─────────────────────────────────────
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
host: redis
|
host: keydb
|
||||||
port: 6379
|
port: 6379
|
||||||
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
#pass: example-pass
|
#pass: example-pass
|
||||||
|
@ -80,7 +80,7 @@ redis:
|
||||||
#db: 1
|
#db: 1
|
||||||
|
|
||||||
#redisForPubsub:
|
#redisForPubsub:
|
||||||
# host: redis
|
# host: keydb
|
||||||
# port: 6379
|
# port: 6379
|
||||||
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
# #pass: example-pass
|
# #pass: example-pass
|
||||||
|
@ -88,7 +88,7 @@ redis:
|
||||||
# #db: 1
|
# #db: 1
|
||||||
|
|
||||||
#redisForJobQueue:
|
#redisForJobQueue:
|
||||||
# host: redis
|
# host: keydb
|
||||||
# port: 6379
|
# port: 6379
|
||||||
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
# #pass: example-pass
|
# #pass: example-pass
|
||||||
|
|
|
@ -15,15 +15,15 @@ services:
|
||||||
- internal_network
|
- internal_network
|
||||||
- external_network
|
- external_network
|
||||||
|
|
||||||
redis:
|
keydb:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
image: redis:7-alpine
|
image: eqalpha/keydb:latest
|
||||||
networks:
|
networks:
|
||||||
- internal_network
|
- internal_network
|
||||||
volumes:
|
volumes:
|
||||||
- redis-data:/data
|
- keydb-data:/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: "redis-cli ping"
|
test: "keydb-cli ping"
|
||||||
interval: 5s
|
interval: 5s
|
||||||
retries: 20
|
retries: 20
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ services:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
redis-data:
|
keydb-data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
internal_network:
|
internal_network:
|
||||||
|
|
|
@ -11,6 +11,7 @@ docker-compose.yml
|
||||||
node_modules/
|
node_modules/
|
||||||
packages/*/node_modules
|
packages/*/node_modules
|
||||||
redis/
|
redis/
|
||||||
|
keydb/
|
||||||
files/
|
files/
|
||||||
misskey-assets/
|
misskey-assets/
|
||||||
fluent-emojis/
|
fluent-emojis/
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
name: Publish Docker image (develop)
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
push_to_registry:
|
|
||||||
name: Push Docker image to Docker Hub
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.repository == 'misskey-dev/misskey'
|
|
||||||
steps:
|
|
||||||
- name: Check out the repo
|
|
||||||
uses: actions/checkout@v4.1.1
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
id: buildx
|
|
||||||
uses: docker/setup-buildx-action@v3.0.0
|
|
||||||
with:
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: misskey/misskey
|
|
||||||
- name: Log in to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
- name: Build and Push to Docker Hub
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
platforms: ${{ steps.buildx.outputs.platforms }}
|
|
||||||
provenance: false
|
|
||||||
tags: misskey/misskey:develop
|
|
||||||
labels: develop
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
|
@ -43,8 +43,8 @@ jobs:
|
||||||
platforms: ${{ steps.buildx.outputs.platforms }}
|
platforms: ${{ steps.buildx.outputs.platforms }}
|
||||||
provenance: false
|
provenance: false
|
||||||
labels: ${{ env.FORMATTED_BRANCH_NAME }}
|
labels: ${{ env.FORMATTED_BRANCH_NAME }}
|
||||||
cache-from: type=gha
|
cache-from: type=registry,ref=ghcr.io/misskeyio/misskey:io-buildcache
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=registry,ref=ghcr.io/misskeyio/misskey:io-buildcache,mode=max
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/misskeyio/misskey:latest
|
ghcr.io/misskeyio/misskey:latest
|
||||||
ghcr.io/misskeyio/misskey:${{ env.FORMATTED_BRANCH_NAME }}
|
ghcr.io/misskeyio/misskey:${{ env.FORMATTED_BRANCH_NAME }}
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
name: Publish Docker image
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
push_to_registry:
|
|
||||||
name: Push Docker image to Docker Hub
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out the repo
|
|
||||||
uses: actions/checkout@v4.1.1
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
id: buildx
|
|
||||||
uses: docker/setup-buildx-action@v3.0.0
|
|
||||||
with:
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: misskey/misskey
|
|
||||||
tags: |
|
|
||||||
type=edge
|
|
||||||
type=ref,event=pr
|
|
||||||
type=ref,event=branch
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
type=semver,pattern={{major}}
|
|
||||||
- name: Log in to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
- name: Build and Push to Docker Hub
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
platforms: ${{ steps.buildx.outputs.platforms }}
|
|
||||||
provenance: false
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
|
@ -1,4 +1,3 @@
|
||||||
---
|
|
||||||
name: Dockle
|
name: Dockle
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
@ -11,20 +10,23 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
dockle:
|
dockle:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
|
||||||
DOCKER_CONTENT_TRUST: 1
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.1
|
- name: Checkout code
|
||||||
- run: |
|
uses: actions/checkout@v4
|
||||||
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb"
|
- name: Build an image from Dockerfile
|
||||||
sudo dpkg -i dockle.deb
|
uses: docker/build-push-action@v5
|
||||||
- run: |
|
with:
|
||||||
cp .config/docker_example.env .config/docker.env
|
context: .
|
||||||
cp ./docker-compose.yml.example ./docker-compose.yml
|
push: false
|
||||||
- run: |
|
provenance: false
|
||||||
docker compose up -d web
|
cache-from: type=registry,ref=ghcr.io/misskeyio/misskey:io-buildcache
|
||||||
docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest
|
tags: |
|
||||||
- run: |
|
misskey:scan
|
||||||
cmd="dockle --exit-code 1 misskey-web:latest ${image_name}"
|
- name: Run dockle
|
||||||
echo "> ${cmd}"
|
uses: goodwithtech/dockle-action@main
|
||||||
eval "${cmd}"
|
with:
|
||||||
|
image: 'misskey:scan'
|
||||||
|
format: 'list'
|
||||||
|
exit-code: '1'
|
||||||
|
exit-level: 'warn'
|
||||||
|
ignore: 'CIS-DI-0005,CIS-DI-0010'
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
# If someone with write access comments "/ok-to-test" on a pull request, emit a repository_dispatch event
|
|
||||||
name: Ok To Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
ok-to-test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# Only run for PRs, not issue comments
|
|
||||||
if: ${{ github.event.issue.pull_request }}
|
|
||||||
steps:
|
|
||||||
# Generate a GitHub App installation access token from an App ID and private key
|
|
||||||
# To create a new GitHub App:
|
|
||||||
# https://developer.github.com/apps/building-github-apps/creating-a-github-app/
|
|
||||||
# See app.yml for an example app manifest
|
|
||||||
- name: Generate token
|
|
||||||
id: generate_token
|
|
||||||
uses: tibdex/github-app-token@v2
|
|
||||||
with:
|
|
||||||
app_id: ${{ secrets.DEPLOYBOT_APP_ID }}
|
|
||||||
private_key: ${{ secrets.DEPLOYBOT_PRIVATE_KEY }}
|
|
||||||
|
|
||||||
- name: Slash Command Dispatch
|
|
||||||
uses: peter-evans/slash-command-dispatch@v3
|
|
||||||
env:
|
|
||||||
TOKEN: ${{ steps.generate_token.outputs.token }}
|
|
||||||
with:
|
|
||||||
token: ${{ env.TOKEN }} # GitHub App installation access token
|
|
||||||
# token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # PAT or OAuth token will also work
|
|
||||||
reaction-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
issue-type: pull-request
|
|
||||||
commands: deploy
|
|
||||||
named-args: true
|
|
||||||
permission: write
|
|
|
@ -1,92 +0,0 @@
|
||||||
# Run secret-dependent integration tests only after /deploy approval
|
|
||||||
on:
|
|
||||||
repository_dispatch:
|
|
||||||
types: [deploy-command]
|
|
||||||
|
|
||||||
name: Deploy preview environment
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# Repo owner has commented /deploy on a (fork-based) pull request
|
|
||||||
deploy-preview-environment:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if:
|
|
||||||
github.event.client_payload.slash_command.sha != '' &&
|
|
||||||
contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha)
|
|
||||||
steps:
|
|
||||||
- uses: actions/github-script@v6.3.3
|
|
||||||
id: check-id
|
|
||||||
env:
|
|
||||||
number: ${{ github.event.client_payload.pull_request.number }}
|
|
||||||
job: ${{ github.job }}
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
result-encoding: string
|
|
||||||
script: |
|
|
||||||
const { data: pull } = await github.rest.pulls.get({
|
|
||||||
...context.repo,
|
|
||||||
pull_number: process.env.number
|
|
||||||
});
|
|
||||||
const ref = pull.head.sha;
|
|
||||||
|
|
||||||
const { data: checks } = await github.rest.checks.listForRef({
|
|
||||||
...context.repo,
|
|
||||||
ref
|
|
||||||
});
|
|
||||||
|
|
||||||
const check = checks.check_runs.filter(c => c.name === process.env.job);
|
|
||||||
|
|
||||||
return check[0].id;
|
|
||||||
|
|
||||||
- uses: actions/github-script@v6.3.3
|
|
||||||
env:
|
|
||||||
check_id: ${{ steps.check-id.outputs.result }}
|
|
||||||
details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }}
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
await github.rest.checks.update({
|
|
||||||
...context.repo,
|
|
||||||
check_run_id: process.env.check_id,
|
|
||||||
status: 'in_progress',
|
|
||||||
details_url: process.env.details_url
|
|
||||||
});
|
|
||||||
|
|
||||||
# Check out merge commit
|
|
||||||
- name: Fork based /deploy checkout
|
|
||||||
uses: actions/checkout@v4.1.1
|
|
||||||
with:
|
|
||||||
ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge'
|
|
||||||
|
|
||||||
# <insert integration tests needing secrets>
|
|
||||||
- name: Context
|
|
||||||
uses: okteto/context@latest
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.OKTETO_TOKEN }}
|
|
||||||
|
|
||||||
- name: Deploy preview environment
|
|
||||||
uses: ikuradon/deploy-preview@latest
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
name: pr-${{ github.event.client_payload.pull_request.number }}-syuilo
|
|
||||||
timeout: 15m
|
|
||||||
|
|
||||||
# Update check run called "integration-fork"
|
|
||||||
- uses: actions/github-script@v6.3.3
|
|
||||||
id: update-check-run
|
|
||||||
if: ${{ always() }}
|
|
||||||
env:
|
|
||||||
# Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run
|
|
||||||
conclusion: ${{ job.status }}
|
|
||||||
check_id: ${{ steps.check-id.outputs.result }}
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
const { data: result } = await github.rest.checks.update({
|
|
||||||
...context.repo,
|
|
||||||
check_run_id: process.env.check_id,
|
|
||||||
status: 'completed',
|
|
||||||
conclusion: process.env.conclusion
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
|
@ -1,54 +0,0 @@
|
||||||
# file: .github/workflows/preview-closed.yaml
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types:
|
|
||||||
- closed
|
|
||||||
|
|
||||||
name: Destroy preview environment
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
destroy-preview-environment:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/github-script@v6.3.3
|
|
||||||
id: check-conclusion
|
|
||||||
env:
|
|
||||||
number: ${{ github.event.number }}
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
result-encoding: string
|
|
||||||
script: |
|
|
||||||
const { data: pull } = await github.rest.pulls.get({
|
|
||||||
...context.repo,
|
|
||||||
pull_number: process.env.number
|
|
||||||
});
|
|
||||||
const ref = pull.head.sha;
|
|
||||||
|
|
||||||
const { data: checks } = await github.rest.checks.listForRef({
|
|
||||||
...context.repo,
|
|
||||||
ref
|
|
||||||
});
|
|
||||||
|
|
||||||
const check = checks.check_runs.filter(c => c.name === 'deploy-preview-environment');
|
|
||||||
|
|
||||||
if (check.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: result } = await github.rest.checks.get({
|
|
||||||
...context.repo,
|
|
||||||
check_run_id: check[0].id,
|
|
||||||
});
|
|
||||||
|
|
||||||
return result.conclusion;
|
|
||||||
- name: Context
|
|
||||||
if: steps.check-conclusion.outputs.result == 'success'
|
|
||||||
uses: okteto/context@latest
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.OKTETO_TOKEN }}
|
|
||||||
|
|
||||||
- name: Destroy preview environment
|
|
||||||
if: steps.check-conclusion.outputs.result == 'success'
|
|
||||||
uses: okteto/destroy-preview@latest
|
|
||||||
with:
|
|
||||||
name: pr-${{ github.event.number }}-syuilo
|
|
|
@ -13,18 +13,18 @@ jobs:
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [18.x]
|
node-version: [20.x]
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:13
|
image: postgres:15
|
||||||
ports:
|
ports:
|
||||||
- 54312:5432
|
- 54312:5432
|
||||||
env:
|
env:
|
||||||
POSTGRES_DB: test-misskey
|
POSTGRES_DB: test-misskey
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
redis:
|
keydb:
|
||||||
image: redis:7
|
image: eqalpha/keydb:latest
|
||||||
ports:
|
ports:
|
||||||
- 56312:6379
|
- 56312:6379
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ jobs:
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [18.x]
|
node-version: [20.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.1
|
- uses: actions/checkout@v4.1.1
|
||||||
|
@ -51,19 +51,19 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [18.x]
|
node-version: [20.x]
|
||||||
browser: [chrome]
|
browser: [chrome]
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:13
|
image: postgres:15
|
||||||
ports:
|
ports:
|
||||||
- 54312:5432
|
- 54312:5432
|
||||||
env:
|
env:
|
||||||
POSTGRES_DB: test-misskey
|
POSTGRES_DB: test-misskey
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
redis:
|
keydb:
|
||||||
image: redis:7
|
image: eqalpha/keydb:latest
|
||||||
ports:
|
ports:
|
||||||
- 56312:6379
|
- 56312:6379
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ jobs:
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [18.x]
|
node-version: [20.x]
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
|
@ -16,7 +16,7 @@ jobs:
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [18.x]
|
node-version: [20.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.1
|
- uses: actions/checkout@v4.1.1
|
||||||
|
|
|
@ -37,6 +37,7 @@ coverage
|
||||||
!/.config/docker_example.env
|
!/.config/docker_example.env
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
!/.devcontainer/docker-compose.yml
|
!/.devcontainer/docker-compose.yml
|
||||||
|
!/packages/backend/test/docker-compose.yml
|
||||||
|
|
||||||
# misskey
|
# misskey
|
||||||
/build
|
/build
|
||||||
|
@ -51,6 +52,7 @@ run.bat
|
||||||
api-docs.json
|
api-docs.json
|
||||||
*.log
|
*.log
|
||||||
/redis
|
/redis
|
||||||
|
/keydb
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/files
|
/files
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
18.18.0
|
20
|
||||||
|
|
20
Dockerfile
20
Dockerfile
|
@ -1,6 +1,6 @@
|
||||||
# syntax = docker/dockerfile:1.4
|
# syntax = docker/dockerfile:1.4
|
||||||
|
|
||||||
ARG NODE_VERSION=18.18.0-bullseye
|
ARG NODE_VERSION=20
|
||||||
|
|
||||||
# build assets & compile TypeScript
|
# build assets & compile TypeScript
|
||||||
|
|
||||||
|
@ -18,15 +18,18 @@ RUN corepack enable
|
||||||
|
|
||||||
WORKDIR /misskey
|
WORKDIR /misskey
|
||||||
|
|
||||||
COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"]
|
COPY --link pnpm-lock.yaml ./
|
||||||
|
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
|
||||||
|
pnpm fetch
|
||||||
|
|
||||||
|
COPY --link ["pnpm-workspace.yaml", "package.json", "./"]
|
||||||
COPY --link ["scripts", "./scripts"]
|
COPY --link ["scripts", "./scripts"]
|
||||||
COPY --link ["packages/backend/package.json", "./packages/backend/"]
|
COPY --link ["packages/backend/package.json", "./packages/backend/"]
|
||||||
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
|
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
|
||||||
COPY --link ["packages/sw/package.json", "./packages/sw/"]
|
COPY --link ["packages/sw/package.json", "./packages/sw/"]
|
||||||
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
|
RUN pnpm i --frozen-lockfile --aggregate-output --offline
|
||||||
pnpm i --frozen-lockfile --aggregate-output
|
|
||||||
|
|
||||||
COPY --link . ./
|
COPY --link . ./
|
||||||
|
|
||||||
|
@ -48,12 +51,15 @@ RUN corepack enable
|
||||||
|
|
||||||
WORKDIR /misskey
|
WORKDIR /misskey
|
||||||
|
|
||||||
COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"]
|
COPY --link pnpm-lock.yaml ./
|
||||||
|
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
|
||||||
|
pnpm fetch
|
||||||
|
|
||||||
|
COPY --link ["pnpm-workspace.yaml", "package.json", "./"]
|
||||||
COPY --link ["scripts", "./scripts"]
|
COPY --link ["scripts", "./scripts"]
|
||||||
COPY --link ["packages/backend/package.json", "./packages/backend/"]
|
COPY --link ["packages/backend/package.json", "./packages/backend/"]
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
|
RUN pnpm i --frozen-lockfile --aggregate-output --offline
|
||||||
pnpm i --frozen-lockfile --aggregate-output
|
|
||||||
|
|
||||||
FROM --platform=$TARGETPLATFORM node:${NODE_VERSION}-slim AS runner
|
FROM --platform=$TARGETPLATFORM node:${NODE_VERSION}-slim AS runner
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ spec:
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 3000
|
- containerPort: 3000
|
||||||
- name: postgres
|
- name: postgres
|
||||||
image: postgres:14-alpine
|
image: postgres:15-alpine
|
||||||
env:
|
env:
|
||||||
- name: POSTGRES_USER
|
- name: POSTGRES_USER
|
||||||
value: "example-misskey-user"
|
value: "example-misskey-user"
|
||||||
|
@ -37,8 +37,8 @@ spec:
|
||||||
value: "misskey"
|
value: "misskey"
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 5432
|
- containerPort: 5432
|
||||||
- name: redis
|
- name: keydb
|
||||||
image: redis:alpine
|
image: eqalpha/keydb:latest
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 6379
|
- containerPort: 6379
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
@ -6,12 +6,12 @@ services:
|
||||||
restart: always
|
restart: always
|
||||||
links:
|
links:
|
||||||
- db
|
- db
|
||||||
- redis
|
- keydb
|
||||||
# - meilisearch
|
# - meilisearch
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
redis:
|
keydb:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
|
@ -22,15 +22,15 @@ services:
|
||||||
- ./files:/misskey/files
|
- ./files:/misskey/files
|
||||||
- ./.config:/misskey/.config:ro
|
- ./.config:/misskey/.config:ro
|
||||||
|
|
||||||
redis:
|
keydb:
|
||||||
restart: always
|
restart: always
|
||||||
image: redis:7-alpine
|
image: eqalpha/keydb:latest
|
||||||
networks:
|
networks:
|
||||||
- internal_network
|
- internal_network
|
||||||
volumes:
|
volumes:
|
||||||
- ./redis:/data
|
- ./keydb:/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: "redis-cli ping"
|
test: "keydb-cli ping"
|
||||||
interval: 5s
|
interval: 5s
|
||||||
retries: 20
|
retries: 20
|
||||||
|
|
||||||
|
|
|
@ -312,6 +312,7 @@ folderName: "Folder name"
|
||||||
createFolder: "Create a folder"
|
createFolder: "Create a folder"
|
||||||
renameFolder: "Rename this folder"
|
renameFolder: "Rename this folder"
|
||||||
deleteFolder: "Delete this folder"
|
deleteFolder: "Delete this folder"
|
||||||
|
folder: "Folder"
|
||||||
addFile: "Add a file"
|
addFile: "Add a file"
|
||||||
emptyDrive: "Your Drive is empty"
|
emptyDrive: "Your Drive is empty"
|
||||||
emptyFolder: "This folder is empty"
|
emptyFolder: "This folder is empty"
|
||||||
|
@ -565,6 +566,10 @@ output: "Output"
|
||||||
script: "Script"
|
script: "Script"
|
||||||
disablePagesScript: "Disable AiScript on Pages"
|
disablePagesScript: "Disable AiScript on Pages"
|
||||||
updateRemoteUser: "Update remote user information"
|
updateRemoteUser: "Update remote user information"
|
||||||
|
deleteUserAvatar: "Delete user icon"
|
||||||
|
deleteUserAvatarConfirm: "Are you sure that you want to delete this user's icon?"
|
||||||
|
deleteUserBanner: "Delete user banner"
|
||||||
|
deleteUserBannerConfirm: "Are you sure that you want to delete this user's banner?"
|
||||||
deleteAllFiles: "Delete all files"
|
deleteAllFiles: "Delete all files"
|
||||||
deleteAllFilesConfirm: "Are you sure that you want to delete all files?"
|
deleteAllFilesConfirm: "Are you sure that you want to delete all files?"
|
||||||
removeAllFollowing: "Unfollow all followed users"
|
removeAllFollowing: "Unfollow all followed users"
|
||||||
|
|
|
@ -315,6 +315,7 @@ export interface Locale {
|
||||||
"createFolder": string;
|
"createFolder": string;
|
||||||
"renameFolder": string;
|
"renameFolder": string;
|
||||||
"deleteFolder": string;
|
"deleteFolder": string;
|
||||||
|
"folder": string;
|
||||||
"addFile": string;
|
"addFile": string;
|
||||||
"emptyDrive": string;
|
"emptyDrive": string;
|
||||||
"emptyFolder": string;
|
"emptyFolder": string;
|
||||||
|
@ -568,6 +569,10 @@ export interface Locale {
|
||||||
"script": string;
|
"script": string;
|
||||||
"disablePagesScript": string;
|
"disablePagesScript": string;
|
||||||
"updateRemoteUser": string;
|
"updateRemoteUser": string;
|
||||||
|
"deleteUserAvatar": string;
|
||||||
|
"deleteUserAvatarConfirm": string;
|
||||||
|
"deleteUserBanner": string;
|
||||||
|
"deleteUserBannerConfirm": string;
|
||||||
"deleteAllFiles": string;
|
"deleteAllFiles": string;
|
||||||
"deleteAllFilesConfirm": string;
|
"deleteAllFilesConfirm": string;
|
||||||
"removeAllFollowing": string;
|
"removeAllFollowing": string;
|
||||||
|
@ -1164,6 +1169,8 @@ export interface Locale {
|
||||||
"signupPendingError": string;
|
"signupPendingError": string;
|
||||||
"cwNotationRequired": string;
|
"cwNotationRequired": string;
|
||||||
"doReaction": string;
|
"doReaction": string;
|
||||||
|
"urlPreviewDenyList": string;
|
||||||
|
"urlPreviewDenyListDescription": string;
|
||||||
"_announcement": {
|
"_announcement": {
|
||||||
"forExistingUsers": string;
|
"forExistingUsers": string;
|
||||||
"forExistingUsersDescription": string;
|
"forExistingUsersDescription": string;
|
||||||
|
|
|
@ -312,6 +312,7 @@ folderName: "フォルダー名"
|
||||||
createFolder: "フォルダーを作成"
|
createFolder: "フォルダーを作成"
|
||||||
renameFolder: "フォルダー名を変更"
|
renameFolder: "フォルダー名を変更"
|
||||||
deleteFolder: "フォルダーを削除"
|
deleteFolder: "フォルダーを削除"
|
||||||
|
folder: "フォルダー"
|
||||||
addFile: "ファイルを追加"
|
addFile: "ファイルを追加"
|
||||||
emptyDrive: "ドライブは空です"
|
emptyDrive: "ドライブは空です"
|
||||||
emptyFolder: "フォルダーは空です"
|
emptyFolder: "フォルダーは空です"
|
||||||
|
@ -565,6 +566,10 @@ output: "出力"
|
||||||
script: "スクリプト"
|
script: "スクリプト"
|
||||||
disablePagesScript: "Pagesのスクリプトを無効にする"
|
disablePagesScript: "Pagesのスクリプトを無効にする"
|
||||||
updateRemoteUser: "リモートユーザー情報の更新"
|
updateRemoteUser: "リモートユーザー情報の更新"
|
||||||
|
deleteUserAvatar: "アイコンを削除"
|
||||||
|
deleteUserAvatarConfirm: "アイコンを削除しますか?"
|
||||||
|
deleteUserBanner: "バナーを削除"
|
||||||
|
deleteUserBannerConfirm: "バナーを削除しますか?"
|
||||||
deleteAllFiles: "すべてのファイルを削除"
|
deleteAllFiles: "すべてのファイルを削除"
|
||||||
deleteAllFilesConfirm: "すべてのファイルを削除しますか?"
|
deleteAllFilesConfirm: "すべてのファイルを削除しますか?"
|
||||||
removeAllFollowing: "フォローを全解除"
|
removeAllFollowing: "フォローを全解除"
|
||||||
|
@ -1161,6 +1166,8 @@ useGroupedNotifications: "通知をグルーピングして表示する"
|
||||||
signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。"
|
signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。"
|
||||||
cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。"
|
cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。"
|
||||||
doReaction: "リアクションする"
|
doReaction: "リアクションする"
|
||||||
|
urlPreviewDenyList: "サムネイルの表示を制限するURL"
|
||||||
|
urlPreviewDenyListDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。一致した場合、サムネイルがぼかされて表示されます。"
|
||||||
|
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "既存ユーザーのみ"
|
forExistingUsers: "既存ユーザーのみ"
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class UrlPreviewDenyList1699284486293 {
|
||||||
|
name = 'UrlPreviewDenyList1699284486293'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "urlPreviewDenyList" character varying(3072) array NOT NULL DEFAULT '{}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "urlPreviewDenyList"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,7 +38,7 @@ export class FeaturedService {
|
||||||
redisTransaction.expire(
|
redisTransaction.expire(
|
||||||
`${name}:${currentWindow}`,
|
`${name}:${currentWindow}`,
|
||||||
(windowRange * 3) / 1000,
|
(windowRange * 3) / 1000,
|
||||||
'NX'); // "NX -- Set expiry only when the key has no expiry" = 有効期限がないときだけ設定
|
);
|
||||||
await redisTransaction.exec();
|
await redisTransaction.exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,10 +48,10 @@ export class FeaturedService {
|
||||||
const previousWindow = currentWindow - 1;
|
const previousWindow = currentWindow - 1;
|
||||||
|
|
||||||
const redisPipeline = this.redisClient.pipeline();
|
const redisPipeline = this.redisClient.pipeline();
|
||||||
redisPipeline.zrange(
|
redisPipeline.zrevrange(
|
||||||
`${name}:${currentWindow}`, 0, threshold, 'REV', 'WITHSCORES');
|
`${name}:${currentWindow}`, 0, threshold, 'WITHSCORES');
|
||||||
redisPipeline.zrange(
|
redisPipeline.zrevrange(
|
||||||
`${name}:${previousWindow}`, 0, threshold, 'REV', 'WITHSCORES');
|
`${name}:${previousWindow}`, 0, threshold, 'WITHSCORES');
|
||||||
const [currentRankingResult, previousRankingResult] = await redisPipeline.exec().then(result => result ? result.map(r => (r[1] ?? []) as string[]) : [[], []]);
|
const [currentRankingResult, previousRankingResult] = await redisPipeline.exec().then(result => result ? result.map(r => (r[1] ?? []) as string[]) : [[], []]);
|
||||||
|
|
||||||
const ranking = new Map<string, number>();
|
const ranking = new Map<string, number>();
|
||||||
|
|
|
@ -52,20 +52,20 @@ export class FetchInstanceMetadataService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async tryLock(host: string): Promise<boolean> {
|
public async tryLock(host: string): Promise<boolean> {
|
||||||
const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'EX', 60 * 5, 'NX', 'GET');
|
const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, Date.now(), 'EX', 60 * 5, 'NX');
|
||||||
return mutex !== '1';
|
return mutex !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public unlock(host: string): Promise<number> {
|
public unlock(host: string): Promise<number> {
|
||||||
return this.redisClient.del(`fetchInstanceMetadata:mutex:${host}`);
|
return this.redisClient.unlink(`fetchInstanceMetadata:mutex:${host}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise<void> {
|
public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise<void> {
|
||||||
const host = instance.host;
|
const host = instance.host;
|
||||||
// Acquire mutex to ensure no parallel runs
|
// Acquire mutex to ensure no parallel runs
|
||||||
if (!await this.tryLock(host)) return;
|
if (!await this.tryLock(host) && !force) return;
|
||||||
try {
|
try {
|
||||||
if (!force) {
|
if (!force) {
|
||||||
const _instance = await this.federatedInstanceService.fetch(host);
|
const _instance = await this.federatedInstanceService.fetch(host);
|
||||||
|
|
|
@ -7,7 +7,8 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import {
|
import {
|
||||||
generateAuthenticationOptions,
|
generateAuthenticationOptions,
|
||||||
generateRegistrationOptions, verifyAuthenticationResponse,
|
generateRegistrationOptions,
|
||||||
|
verifyAuthenticationResponse,
|
||||||
verifyRegistrationResponse,
|
verifyRegistrationResponse,
|
||||||
} from '@simplewebauthn/server';
|
} from '@simplewebauthn/server';
|
||||||
import { AttestationFormat, isoCBOR } from '@simplewebauthn/server/helpers';
|
import { AttestationFormat, isoCBOR } from '@simplewebauthn/server/helpers';
|
||||||
|
|
|
@ -518,4 +518,9 @@ export class MiMeta {
|
||||||
default: 0,
|
default: 0,
|
||||||
})
|
})
|
||||||
public notesPerOneAd: number;
|
public notesPerOneAd: number;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 3072, array: true, default: '{}',
|
||||||
|
})
|
||||||
|
public urlPreviewDenyList: string[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,8 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d
|
||||||
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
|
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
|
||||||
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
|
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
|
||||||
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
|
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
|
||||||
|
import * as ep___admin_deleteUserAvatar from './endpoints/admin/delete-user-avatar.js';
|
||||||
|
import * as ep___admin_deleteUserBanner from './endpoints/admin/delete-user-banner.js';
|
||||||
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
|
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
|
||||||
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
|
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
|
||||||
import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
|
import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
|
||||||
|
@ -389,6 +391,8 @@ const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-de
|
||||||
const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default };
|
const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default };
|
||||||
const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default };
|
const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default };
|
||||||
const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default };
|
const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default };
|
||||||
|
const $admin_deleteUserAvatar: Provider = { provide: 'ep:admin/delete-user-avatar', useClass: ep___admin_deleteUserAvatar.default };
|
||||||
|
const $admin_deleteUserBanner: Provider = { provide: 'ep:admin/delete-user-banner', useClass: ep___admin_deleteUserBanner.default };
|
||||||
const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
|
const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
|
||||||
const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
|
const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
|
||||||
const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default };
|
const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default };
|
||||||
|
@ -755,6 +759,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$admin_avatarDecorations_list,
|
$admin_avatarDecorations_list,
|
||||||
$admin_avatarDecorations_update,
|
$admin_avatarDecorations_update,
|
||||||
$admin_deleteAllFilesOfAUser,
|
$admin_deleteAllFilesOfAUser,
|
||||||
|
$admin_deleteUserAvatar,
|
||||||
|
$admin_deleteUserBanner,
|
||||||
$admin_drive_cleanRemoteFiles,
|
$admin_drive_cleanRemoteFiles,
|
||||||
$admin_drive_cleanup,
|
$admin_drive_cleanup,
|
||||||
$admin_drive_files,
|
$admin_drive_files,
|
||||||
|
@ -1115,6 +1121,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$admin_avatarDecorations_list,
|
$admin_avatarDecorations_list,
|
||||||
$admin_avatarDecorations_update,
|
$admin_avatarDecorations_update,
|
||||||
$admin_deleteAllFilesOfAUser,
|
$admin_deleteAllFilesOfAUser,
|
||||||
|
$admin_deleteUserAvatar,
|
||||||
|
$admin_deleteUserBanner,
|
||||||
$admin_drive_cleanRemoteFiles,
|
$admin_drive_cleanRemoteFiles,
|
||||||
$admin_drive_cleanup,
|
$admin_drive_cleanup,
|
||||||
$admin_drive_files,
|
$admin_drive_files,
|
||||||
|
|
|
@ -27,6 +27,8 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d
|
||||||
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
|
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
|
||||||
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
|
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
|
||||||
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
|
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
|
||||||
|
import * as ep___admin_deleteUserAvatar from './endpoints/admin/delete-user-avatar.js';
|
||||||
|
import * as ep___admin_deleteUserBanner from './endpoints/admin/delete-user-banner.js';
|
||||||
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
|
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
|
||||||
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
|
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
|
||||||
import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
|
import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
|
||||||
|
@ -387,6 +389,8 @@ const eps = [
|
||||||
['admin/avatar-decorations/list', ep___admin_avatarDecorations_list],
|
['admin/avatar-decorations/list', ep___admin_avatarDecorations_list],
|
||||||
['admin/avatar-decorations/update', ep___admin_avatarDecorations_update],
|
['admin/avatar-decorations/update', ep___admin_avatarDecorations_update],
|
||||||
['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
|
['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
|
||||||
|
['admin/delete-user-avatar', ep___admin_deleteUserAvatar],
|
||||||
|
['admin/delete-user-banner', ep___admin_deleteUserBanner],
|
||||||
['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
|
['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
|
||||||
['admin/drive/cleanup', ep___admin_drive_cleanup],
|
['admin/drive/cleanup', ep___admin_drive_cleanup],
|
||||||
['admin/drive/files', ep___admin_drive_files],
|
['admin/drive/files', ep___admin_drive_files],
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type { UsersRepository } from '@/models/index.js';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
userId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['userId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
throw new Error('user not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.usersRepository.update(user.id, {
|
||||||
|
avatar: null,
|
||||||
|
avatarId: null,
|
||||||
|
avatarUrl: null,
|
||||||
|
avatarBlurhash: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type { UsersRepository } from '@/models/index.js';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
userId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['userId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
throw new Error('user not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.usersRepository.update(user.id, {
|
||||||
|
banner: null,
|
||||||
|
bannerId: null,
|
||||||
|
bannerUrl: null,
|
||||||
|
bannerBlurhash: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -315,6 +315,14 @@ export const meta = {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
urlPreviewDenyList: {
|
||||||
|
type: 'array',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -429,6 +437,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
|
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
|
||||||
perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax,
|
perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax,
|
||||||
notesPerOneAd: instance.notesPerOneAd,
|
notesPerOneAd: instance.notesPerOneAd,
|
||||||
|
urlPreviewDenyList: instance.urlPreviewDenyList,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,6 +133,9 @@ export const paramDef = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
urlPreviewDenyList: { type: 'array', nullable: true, items: {
|
||||||
|
type: 'string',
|
||||||
|
} },
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -173,6 +176,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
return h !== '' && h !== lv && !set.blockedHosts?.includes(h);
|
return h !== '' && h !== lv && !set.blockedHosts?.includes(h);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(ps.urlPreviewDenyList)) {
|
||||||
|
set.urlPreviewDenyList = ps.urlPreviewDenyList.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.themeColor !== undefined) {
|
if (ps.themeColor !== undefined) {
|
||||||
set.themeColor = ps.themeColor;
|
set.themeColor = ps.themeColor;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { summaly } from 'summaly';
|
import { summaly } from 'summaly';
|
||||||
|
import RE2 from 're2';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
@ -94,6 +95,23 @@ export class UrlPreviewService {
|
||||||
summary.icon = this.wrap(summary.icon);
|
summary.icon = this.wrap(summary.icon);
|
||||||
summary.thumbnail = this.wrap(summary.thumbnail);
|
summary.thumbnail = this.wrap(summary.thumbnail);
|
||||||
|
|
||||||
|
const includeDenyList = meta.urlPreviewDenyList.some(filter => {
|
||||||
|
// represents RegExp
|
||||||
|
const regexp = /^\/(.+)\/(.*)$/.exec(filter);
|
||||||
|
// This should never happen due to input sanitisation.
|
||||||
|
if (!regexp) {
|
||||||
|
const words = filter.split(' ');
|
||||||
|
return words.every(keyword => summary.url.includes(keyword));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new RE2(regexp[1], regexp[2]).test(summary.url);
|
||||||
|
} catch (err) {
|
||||||
|
// This should never happen due to input sanitisation.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (includeDenyList) summary.sensitive = true;
|
||||||
|
|
||||||
// Cache 7days
|
// Cache 7days
|
||||||
reply.header('Cache-Control', 'max-age=604800, immutable');
|
reply.header('Cache-Control', 'max-age=604800, immutable');
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
version: "3"
|
version: "3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
redistest:
|
keydbtest:
|
||||||
image: redis:7
|
image: eqalpha/keydb:latest
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:56312:6379"
|
- "127.0.0.1:56312:6379"
|
||||||
|
|
||||||
dbtest:
|
dbtest:
|
||||||
image: postgres:13
|
image: postgres:15
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:54312:5432"
|
- "127.0.0.1:54312:5432"
|
||||||
environment:
|
environment:
|
||||||
|
|
|
@ -21,9 +21,10 @@ import type { TestingModule } from '@nestjs/testing';
|
||||||
function mockRedis() {
|
function mockRedis() {
|
||||||
const hash = {};
|
const hash = {};
|
||||||
const set = jest.fn((key, value) => {
|
const set = jest.fn((key, value) => {
|
||||||
const ret = hash[key];
|
// このテストで呼び出すSETにはNXオプションが付いてる
|
||||||
|
if (hash[key]) return null;
|
||||||
hash[key] = value;
|
hash[key] = value;
|
||||||
return ret;
|
return 'OK';
|
||||||
});
|
});
|
||||||
return set;
|
return set;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,8 +24,16 @@ const props = withDefaults(defineProps<{
|
||||||
function loadShader(gl, type, source) {
|
function loadShader(gl, type, source) {
|
||||||
const shader = gl.createShader(type);
|
const shader = gl.createShader(type);
|
||||||
|
|
||||||
gl.shaderSource(shader, source);
|
try {
|
||||||
gl.compileShader(shader);
|
gl.shaderSource(shader, source);
|
||||||
|
gl.compileShader(shader);
|
||||||
|
} catch (error) {
|
||||||
|
alert(
|
||||||
|
`failed to compile shader: ${error} ${gl.getShaderInfoLog(shader)}`,
|
||||||
|
);
|
||||||
|
gl.deleteShader(shader);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||||
alert(
|
alert(
|
||||||
|
|
|
@ -5,9 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと -->
|
<!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと -->
|
||||||
<section>
|
<!-- フォルダの中にはカスタム絵文字だけ(Unicode絵文字もこっち) -->
|
||||||
|
<section v-if="!hasChildSection" v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
|
||||||
<header class="_acrylic" @click="shown = !shown">
|
<header class="_acrylic" @click="shown = !shown">
|
||||||
<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> ({{ emojis.length }})
|
<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-icons"></i>:{{ emojis.length }})
|
||||||
</header>
|
</header>
|
||||||
<div v-if="shown" class="body">
|
<div v-if="shown" class="body">
|
||||||
<button
|
<button
|
||||||
|
@ -23,15 +24,52 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<!-- フォルダの中にはカスタム絵文字やフォルダがある -->
|
||||||
|
<section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
|
||||||
|
<header class="_acrylic" @click="shown = !shown">
|
||||||
|
<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder"></i>:{{ customEmojiTree.length }} <i class="ti ti-icons"></i>:{{ emojis.length }})
|
||||||
|
</header>
|
||||||
|
<div v-if="shown" style="padding-left: 9px;">
|
||||||
|
<MkEmojiPickerSection
|
||||||
|
v-for="child in customEmojiTree"
|
||||||
|
:key="`custom:${child.category}`"
|
||||||
|
:initialShown="initialShown"
|
||||||
|
:emojis="computed(() => customEmojis.filter(e => e.category === child.category).map(e => `:${e.name}:`))"
|
||||||
|
:hasChildSection="child.children.length !== 0"
|
||||||
|
:customEmojiTree="child.children"
|
||||||
|
@chosen="nestedChosen"
|
||||||
|
>
|
||||||
|
{{ child.category || i18n.ts.other }}
|
||||||
|
</MkEmojiPickerSection>
|
||||||
|
</div>
|
||||||
|
<div v-if="shown" class="body">
|
||||||
|
<button
|
||||||
|
v-for="emoji in emojis"
|
||||||
|
:key="emoji"
|
||||||
|
:data-emoji="emoji"
|
||||||
|
class="_button item"
|
||||||
|
@pointerenter="computeButtonTitle"
|
||||||
|
@click="emit('chosen', emoji, $event)"
|
||||||
|
>
|
||||||
|
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
|
||||||
|
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, Ref } from 'vue';
|
import { ref, computed, Ref } from 'vue';
|
||||||
import { getEmojiName } from '@/scripts/emojilist.js';
|
import { CustomEmojiFolderTree, getEmojiName } from '@/scripts/emojilist.js';
|
||||||
|
import { i18n } from '../i18n.js';
|
||||||
|
import { customEmojis } from '@/custom-emojis.js';
|
||||||
|
import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
emojis: string[] | Ref<string[]>;
|
emojis: string[] | Ref<string[]>;
|
||||||
initialShown?: boolean;
|
initialShown?: boolean;
|
||||||
|
hasChildSection?: boolean;
|
||||||
|
customEmojiTree?: CustomEmojiFolderTree[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -49,4 +87,7 @@ function computeButtonTitle(ev: MouseEvent): void {
|
||||||
elm.title = getEmojiName(emoji) ?? emoji;
|
elm.title = getEmojiName(emoji) ?? emoji;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function nestedChosen(emoji: any, ev?: MouseEvent) {
|
||||||
|
emit('chosen', emoji, ev);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -73,18 +73,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-once class="group">
|
<div v-once class="group">
|
||||||
<header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
|
<header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
|
||||||
<XSection
|
<XSection
|
||||||
v-for="category in customEmojiCategories"
|
v-for="child in customEmojiFolderRoot.children"
|
||||||
:key="`custom:${category}`"
|
:key="`custom:${child.category}`"
|
||||||
:initialShown="false"
|
:initialShown="false"
|
||||||
:emojis="computed(() => customEmojis.filter(e => category === null ? (e.category === 'null' || !e.category) : e.category === category).filter(filterAvailable).map(e => `:${e.name}:`))"
|
:emojis="computed(() => customEmojis.filter(e => child.category === '' ? (e.category === 'null' || !e.category) : e.category === child.category).filter(filterAvailable).map(e => `:${e.name}:`))"
|
||||||
|
:hasChildSection="child.children.length !== 0"
|
||||||
|
:customEmojiTree="child.children"
|
||||||
@chosen="chosen"
|
@chosen="chosen"
|
||||||
>
|
>
|
||||||
{{ category || i18n.ts.other }}
|
{{ child.category || i18n.ts.other }}
|
||||||
</XSection>
|
</XSection>
|
||||||
</div>
|
</div>
|
||||||
<div v-once class="group">
|
<div v-once class="group">
|
||||||
<header class="_acrylic">{{ i18n.ts.emoji }}</header>
|
<header class="_acrylic">{{ i18n.ts.emoji }}</header>
|
||||||
<XSection v-for="category in categories" :key="category" :emojis="emojiCharByCategory.get(category) ?? []" @chosen="chosen">{{ category }}</XSection>
|
<XSection v-for="category in categories" :key="category" :emojis="emojiCharByCategory.get(category) ?? []" :hasChildSection="false" @chosen="chosen">{{ category }}</XSection>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
|
@ -100,7 +102,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { ref, shallowRef, computed, watch, onMounted } from 'vue';
|
import { ref, shallowRef, computed, watch, onMounted } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import XSection from '@/components/MkEmojiPicker.section.vue';
|
import XSection from '@/components/MkEmojiPicker.section.vue';
|
||||||
import { emojilist, emojiCharByCategory, UnicodeEmojiDef, unicodeEmojiCategories as categories, getEmojiName } from '@/scripts/emojilist.js';
|
import { emojilist, emojiCharByCategory, UnicodeEmojiDef, unicodeEmojiCategories as categories, getEmojiName, CustomEmojiFolderTree } from '@/scripts/emojilist.js';
|
||||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { isTouchUsing } from '@/scripts/touch.js';
|
import { isTouchUsing } from '@/scripts/touch.js';
|
||||||
|
@ -144,6 +146,39 @@ const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
|
||||||
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
|
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
|
||||||
const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
|
const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
|
||||||
|
|
||||||
|
const customEmojiFolderRoot: CustomEmojiFolderTree = { category: "", children: [] };
|
||||||
|
|
||||||
|
function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): CustomEmojiFolderTree {
|
||||||
|
const parts = (input && input !== 'null' ? input : '').split(' / ');
|
||||||
|
let currentNode: CustomEmojiFolderTree = root;
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
const path = currentNode.category ? `${currentNode.category} / ${part}` : part;
|
||||||
|
|
||||||
|
let existingNode = currentNode.children.find((node) => node.category === path);
|
||||||
|
if (!existingNode) {
|
||||||
|
const newNode: CustomEmojiFolderTree = {
|
||||||
|
category: path,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
currentNode.children.push(newNode);
|
||||||
|
existingNode = newNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentNode = existingNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
customEmojiCategories.value.forEach(ec => {
|
||||||
|
if (ec !== null) {
|
||||||
|
parseAndMergeCategories(ec, customEmojiFolderRoot);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
parseAndMergeCategories('', customEmojiFolderRoot);
|
||||||
|
|
||||||
watch(q, () => {
|
watch(q, () => {
|
||||||
if (emojisEl.value) emojisEl.value.scrollTop = 0;
|
if (emojisEl.value) emojisEl.value.scrollTop = 0;
|
||||||
|
|
||||||
|
@ -573,8 +608,7 @@ defineExpose({
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 32px;
|
line-height: 28px;
|
||||||
line-height: 32px;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
|
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue';
|
import { onUnmounted, onActivated, onMounted, computed, shallowRef } from 'vue';
|
||||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||||
import XNotification from '@/components/MkNotification.vue';
|
import XNotification from '@/components/MkNotification.vue';
|
||||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||||
|
@ -64,7 +64,7 @@ function onNotification(notification) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isMuted) {
|
if (!isMuted) {
|
||||||
pagingComponent.value.prepend(notification);
|
pagingComponent.value?.prepend(notification);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,16 +85,14 @@ onMounted(() => {
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
pagingComponent.value?.reload();
|
pagingComponent.value?.reload();
|
||||||
connection = useStream().useChannel('main');
|
|
||||||
connection.on('notification', onNotification);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (connection) connection.dispose();
|
if (connection) connection.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
onDeactivated(() => {
|
defineExpose({
|
||||||
if (connection) connection.dispose();
|
reload,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import MkLoading from '@/components/global/MkLoading.vue';
|
||||||
import { onMounted, onUnmounted, watch } from 'vue';
|
import { onMounted, onUnmounted, watch } from 'vue';
|
||||||
import { deviceKind } from '@/scripts/device-kind.js';
|
import { deviceKind } from '@/scripts/device-kind.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
@ -31,6 +32,7 @@ import { getScrollContainer } from '@/scripts/scroll.js';
|
||||||
const SCROLL_STOP = 10;
|
const SCROLL_STOP = 10;
|
||||||
const MAX_PULL_DISTANCE = Infinity;
|
const MAX_PULL_DISTANCE = Infinity;
|
||||||
const FIRE_THRESHOLD = 230;
|
const FIRE_THRESHOLD = 230;
|
||||||
|
const FIRE_THRESHOLD_RATIO = 1.1;
|
||||||
const RELEASE_TRANSITION_DURATION = 200;
|
const RELEASE_TRANSITION_DURATION = 200;
|
||||||
const PULL_BRAKE_BASE = 1.5;
|
const PULL_BRAKE_BASE = 1.5;
|
||||||
const PULL_BRAKE_FACTOR = 170;
|
const PULL_BRAKE_FACTOR = 170;
|
||||||
|
@ -39,9 +41,11 @@ let isPullStart = $ref(false);
|
||||||
let isPullEnd = $ref(false);
|
let isPullEnd = $ref(false);
|
||||||
let isRefreshing = $ref(false);
|
let isRefreshing = $ref(false);
|
||||||
let pullDistance = $ref(0);
|
let pullDistance = $ref(0);
|
||||||
|
let moveRatio = $ref(0);
|
||||||
|
|
||||||
let supportPointerDesktop = false;
|
let supportPointerDesktop = false;
|
||||||
let startScreenY: number | null = null;
|
let startScreenY: number | null = null;
|
||||||
|
let startClientX: number | null = null;
|
||||||
|
|
||||||
const rootEl = $shallowRef<HTMLDivElement>();
|
const rootEl = $shallowRef<HTMLDivElement>();
|
||||||
let scrollEl: HTMLElement | null = null;
|
let scrollEl: HTMLElement | null = null;
|
||||||
|
@ -65,11 +69,20 @@ function getScreenY(event) {
|
||||||
return event.touches[0].screenY;
|
return event.touches[0].screenY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getClientX(event) {
|
||||||
|
if (supportPointerDesktop) {
|
||||||
|
return event.clientX;
|
||||||
|
}
|
||||||
|
return event.touches[0].clientX;
|
||||||
|
}
|
||||||
|
|
||||||
function moveStart(event) {
|
function moveStart(event) {
|
||||||
if (!isPullStart && !isRefreshing && !disabled) {
|
if (!isPullStart && !isRefreshing && !disabled && scrollEl?.scrollTop === 0) {
|
||||||
isPullStart = true;
|
isPullStart = true;
|
||||||
startScreenY = getScreenY(event);
|
startScreenY = getScreenY(event);
|
||||||
|
startClientX = getClientX(event);
|
||||||
pullDistance = 0;
|
pullDistance = 0;
|
||||||
|
moveRatio = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,6 +125,7 @@ async function closeContent() {
|
||||||
function moveEnd() {
|
function moveEnd() {
|
||||||
if (isPullStart && !isRefreshing) {
|
if (isPullStart && !isRefreshing) {
|
||||||
startScreenY = null;
|
startScreenY = null;
|
||||||
|
startClientX = null;
|
||||||
if (isPullEnd) {
|
if (isPullEnd) {
|
||||||
isPullEnd = false;
|
isPullEnd = false;
|
||||||
isRefreshing = true;
|
isRefreshing = true;
|
||||||
|
@ -128,6 +142,7 @@ function moveEnd() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function moving(event: TouchEvent | PointerEvent) {
|
function moving(event: TouchEvent | PointerEvent) {
|
||||||
|
if (!isPullStart && scrollEl?.scrollTop === 0) moveStart(event);
|
||||||
if (!isPullStart || isRefreshing || disabled) return;
|
if (!isPullStart || isRefreshing || disabled) return;
|
||||||
|
|
||||||
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance)) {
|
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance)) {
|
||||||
|
@ -137,19 +152,23 @@ function moving(event: TouchEvent | PointerEvent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startScreenY === null) {
|
if (startScreenY === null || startClientX === null) {
|
||||||
startScreenY = getScreenY(event);
|
startScreenY = getScreenY(event);
|
||||||
|
startClientX = getClientX(event);
|
||||||
}
|
}
|
||||||
const moveScreenY = getScreenY(event);
|
const moveScreenY = getScreenY(event);
|
||||||
|
const moveClientX = getClientX(event);
|
||||||
|
|
||||||
const moveHeight = moveScreenY - startScreenY!;
|
const moveHeight = moveScreenY - startScreenY!;
|
||||||
|
const moveWidth = moveClientX - startClientX!;
|
||||||
pullDistance = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
|
pullDistance = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
|
||||||
|
moveRatio = Math.max(Math.abs(moveHeight), 1) / Math.max(Math.abs(moveWidth), 1);
|
||||||
|
|
||||||
if (pullDistance > 0) {
|
if (pullDistance > 0 && moveRatio > FIRE_THRESHOLD_RATIO) {
|
||||||
if (event.cancelable) event.preventDefault();
|
if (event.cancelable) event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
isPullEnd = pullDistance >= FIRE_THRESHOLD;
|
isPullEnd = pullDistance >= FIRE_THRESHOLD && moveRatio > FIRE_THRESHOLD_RATIO;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -169,47 +188,30 @@ function setDisabled(value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onScrollContainerScroll() {
|
function onScrollContainerScroll() {
|
||||||
const scrollPos = scrollEl!.scrollTop;
|
|
||||||
|
|
||||||
// When at the top of the page, disable vertical overscroll so passive touch listeners can take over.
|
// When at the top of the page, disable vertical overscroll so passive touch listeners can take over.
|
||||||
if (scrollPos === 0) {
|
if (scrollEl?.scrollTop === 0) {
|
||||||
scrollEl!.style.touchAction = 'pan-x pan-down pinch-zoom';
|
scrollEl!.style.touchAction = 'pan-x pan-down pinch-zoom';
|
||||||
registerEventListenersForReadyToPull();
|
|
||||||
} else {
|
} else {
|
||||||
scrollEl!.style.touchAction = 'auto';
|
scrollEl!.style.touchAction = 'auto';
|
||||||
unregisterEventListenersForReadyToPull();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerEventListenersForReadyToPull() {
|
|
||||||
if (rootEl == null) return;
|
|
||||||
rootEl.addEventListener('touchstart', moveStart, { passive: true });
|
|
||||||
rootEl.addEventListener('touchmove', moving, { passive: false }); // passive: falseにしないとpreventDefaultが使えない
|
|
||||||
}
|
|
||||||
|
|
||||||
function unregisterEventListenersForReadyToPull() {
|
|
||||||
if (rootEl == null) return;
|
|
||||||
rootEl.removeEventListener('touchstart', moveStart);
|
|
||||||
rootEl.removeEventListener('touchmove', moving);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (rootEl == null) return;
|
if (rootEl == null) return;
|
||||||
|
|
||||||
scrollEl = getScrollContainer(rootEl);
|
scrollEl = getScrollContainer(rootEl);
|
||||||
if (scrollEl == null) return;
|
if (scrollEl == null) return;
|
||||||
|
|
||||||
scrollEl.addEventListener('scroll', onScrollContainerScroll, { passive: true });
|
scrollEl.addEventListener('scroll', onScrollContainerScroll, { passive: true });
|
||||||
|
rootEl.addEventListener('touchstart', moveStart, { passive: true });
|
||||||
|
rootEl.addEventListener('touchmove', moving, { passive: false }); // passive: falseにしないとpreventDefaultが使えない
|
||||||
rootEl.addEventListener('touchend', moveEnd, { passive: true });
|
rootEl.addEventListener('touchend', moveEnd, { passive: true });
|
||||||
|
|
||||||
registerEventListenersForReadyToPull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (scrollEl) scrollEl.removeEventListener('scroll', onScrollContainerScroll);
|
if (scrollEl) scrollEl.removeEventListener('scroll', onScrollContainerScroll);
|
||||||
|
if (rootEl == null) return;
|
||||||
unregisterEventListenersForReadyToPull();
|
rootEl.removeEventListener('touchstart', moveStart);
|
||||||
|
rootEl.removeEventListener('touchmove', moving);
|
||||||
|
rootEl.removeEventListener('touchend', moveEnd);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
|
|
@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
|
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
|
||||||
<div v-if="thumbnail" :class="$style.thumbnail" :style="defaultStore.state.enableDataSaverMode ? '' : `background-image: url('${thumbnail}')`">
|
<div v-if="thumbnail" :class="[$style.thumbnail, { [$style.thumbnailBlur]: sensitive }]" :style="defaultStore.state.enableDataSaverMode ? '' : `background-image: url('${thumbnail}')`">
|
||||||
</div>
|
</div>
|
||||||
<article :class="$style.body">
|
<article :class="$style.body">
|
||||||
<header :class="$style.header">
|
<header :class="$style.header">
|
||||||
|
@ -118,6 +118,7 @@ let description = $ref<string | null>(null);
|
||||||
let thumbnail = $ref<string | null>(null);
|
let thumbnail = $ref<string | null>(null);
|
||||||
let icon = $ref<string | null>(null);
|
let icon = $ref<string | null>(null);
|
||||||
let sitename = $ref<string | null>(null);
|
let sitename = $ref<string | null>(null);
|
||||||
|
let sensitive = $ref<boolean | undefined>(undefined);
|
||||||
let player = $ref({
|
let player = $ref({
|
||||||
url: null,
|
url: null,
|
||||||
width: null,
|
width: null,
|
||||||
|
@ -170,6 +171,7 @@ window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLa
|
||||||
icon = info.icon;
|
icon = info.icon;
|
||||||
sitename = info.sitename;
|
sitename = info.sitename;
|
||||||
player = info.player;
|
player = info.player;
|
||||||
|
sensitive = info.sensitive;
|
||||||
});
|
});
|
||||||
|
|
||||||
function adjustTweetHeight(message: any) {
|
function adjustTweetHeight(message: any) {
|
||||||
|
@ -320,6 +322,11 @@ onUnmounted(() => {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thumbnailBlur {
|
||||||
|
filter: blur(8px);
|
||||||
|
clip-path: inset(0);
|
||||||
|
}
|
||||||
|
|
||||||
@container (max-width: 400px) {
|
@container (max-width: 400px) {
|
||||||
.link {
|
.link {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
|
@ -124,6 +124,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
|
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="deleteUserAvatar"><i class="ti ti-user-circle"></i> {{ i18n.ts.deleteUserAvatar }}</MkButton>
|
||||||
|
<MkButton v-if="iAmModerator" inline danger @click="deleteUserBanner"><i class="ti ti-photo"></i> {{ i18n.ts.deleteUserBanner }}</MkButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
</div>
|
</div>
|
||||||
|
@ -325,6 +330,44 @@ async function toggleSuspend(v) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteUserAvatar() {
|
||||||
|
const confirm = await os.confirm({
|
||||||
|
type: 'warning',
|
||||||
|
text: i18n.ts.deleteUserAvatarConfirm,
|
||||||
|
});
|
||||||
|
if (confirm.canceled) return;
|
||||||
|
const process = async () => {
|
||||||
|
await os.api('admin/delete-user-avatar', { userId: user.id });
|
||||||
|
os.success();
|
||||||
|
};
|
||||||
|
await process().catch(err => {
|
||||||
|
os.alert({
|
||||||
|
type: 'error',
|
||||||
|
text: err.toString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
refreshUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteUserBanner() {
|
||||||
|
const confirm = await os.confirm({
|
||||||
|
type: 'warning',
|
||||||
|
text: i18n.ts.deleteUserBannerConfirm,
|
||||||
|
});
|
||||||
|
if (confirm.canceled) return;
|
||||||
|
const process = async () => {
|
||||||
|
await os.api('admin/delete-user-banner', { userId: user.id });
|
||||||
|
os.success();
|
||||||
|
};
|
||||||
|
await process().catch(err => {
|
||||||
|
os.alert({
|
||||||
|
type: 'error',
|
||||||
|
text: err.toString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
refreshUser();
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteAllFiles() {
|
async function deleteAllFiles() {
|
||||||
const confirm = await os.confirm({
|
const confirm = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
|
|
@ -39,6 +39,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts.sensitiveWords }}</template>
|
<template #label>{{ i18n.ts.sensitiveWords }}</template>
|
||||||
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
|
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
|
|
||||||
|
<MkTextarea v-model="urlPreviewDenyList">
|
||||||
|
<template #label>{{ i18n.ts.urlPreviewDenyList }}</template>
|
||||||
|
<template #caption>{{ i18n.ts.urlPreviewDenyListDescription }}</template>
|
||||||
|
</MkTextarea>
|
||||||
</div>
|
</div>
|
||||||
</FormSuspense>
|
</FormSuspense>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
|
@ -75,6 +80,7 @@ let sensitiveWords: string = $ref('');
|
||||||
let preservedUsernames: string = $ref('');
|
let preservedUsernames: string = $ref('');
|
||||||
let tosUrl: string | null = $ref(null);
|
let tosUrl: string | null = $ref(null);
|
||||||
let privacyPolicyUrl: string | null = $ref(null);
|
let privacyPolicyUrl: string | null = $ref(null);
|
||||||
|
let urlPreviewDenyList: string = $ref('');
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const meta = await os.api('admin/meta');
|
const meta = await os.api('admin/meta');
|
||||||
|
@ -84,6 +90,7 @@ async function init() {
|
||||||
preservedUsernames = meta.preservedUsernames.join('\n');
|
preservedUsernames = meta.preservedUsernames.join('\n');
|
||||||
tosUrl = meta.tosUrl;
|
tosUrl = meta.tosUrl;
|
||||||
privacyPolicyUrl = meta.privacyPolicyUrl;
|
privacyPolicyUrl = meta.privacyPolicyUrl;
|
||||||
|
urlPreviewDenyList = meta.urlPreviewDenyList.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
|
@ -94,6 +101,7 @@ function save() {
|
||||||
privacyPolicyUrl,
|
privacyPolicyUrl,
|
||||||
sensitiveWords: sensitiveWords.split('\n'),
|
sensitiveWords: sensitiveWords.split('\n'),
|
||||||
preservedUsernames: preservedUsernames.split('\n'),
|
preservedUsernames: preservedUsernames.split('\n'),
|
||||||
|
urlPreviewDenyList: urlPreviewDenyList.split('\n'),
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
fetchInstance();
|
fetchInstance();
|
||||||
});
|
});
|
||||||
|
|
|
@ -43,3 +43,8 @@ export function getEmojiName(char: string): string | null {
|
||||||
return emojilist[idx].name;
|
return emojilist[idx].name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CustomEmojiFolderTree {
|
||||||
|
category: string;
|
||||||
|
children: CustomEmojiFolderTree[];
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked">
|
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
|
||||||
<template #header>
|
<template #header>
|
||||||
<i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
<i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked">
|
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
|
||||||
<template #header>
|
<template #header>
|
||||||
<i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
<i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -57,6 +57,7 @@ const props = withDefaults(defineProps<{
|
||||||
isStacked?: boolean;
|
isStacked?: boolean;
|
||||||
naked?: boolean;
|
naked?: boolean;
|
||||||
menu?: MenuItem[];
|
menu?: MenuItem[];
|
||||||
|
refresher?: () => Promise<void>;
|
||||||
}>(), {
|
}>(), {
|
||||||
isStacked: false,
|
isStacked: false,
|
||||||
naked: false,
|
naked: false,
|
||||||
|
@ -178,6 +179,18 @@ function getMenu() {
|
||||||
},
|
},
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
if (props.refresher) {
|
||||||
|
items = [{
|
||||||
|
icon: 'ti ti-refresh',
|
||||||
|
text: i18n.ts.reload,
|
||||||
|
action: () => {
|
||||||
|
if (props.refresher) {
|
||||||
|
props.refresher();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, ...items];
|
||||||
|
}
|
||||||
|
|
||||||
if (props.menu) {
|
if (props.menu) {
|
||||||
items.unshift(null);
|
items.unshift(null);
|
||||||
items = props.menu.concat(items);
|
items = props.menu.concat(items);
|
||||||
|
|
|
@ -4,10 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<XColumn :column="column" :isStacked="isStacked">
|
<XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()">
|
||||||
<template #header><i class="ti ti-mail" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
<template #header><i class="ti ti-mail" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||||
|
|
||||||
<MkNotes :pagination="pagination"/>
|
<MkNotes ref="tlComponent" :pagination="pagination"/>
|
||||||
</XColumn>
|
</XColumn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import { } from 'vue';
|
||||||
import XColumn from './column.vue';
|
import XColumn from './column.vue';
|
||||||
import { Column } from './deck-store.js';
|
import { Column } from './deck-store.js';
|
||||||
import MkNotes from '@/components/MkNotes.vue';
|
import MkNotes from '@/components/MkNotes.vue';
|
||||||
|
import { reloadStream } from '@/stream.js';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
column: Column;
|
column: Column;
|
||||||
|
@ -29,4 +30,15 @@ const pagination = {
|
||||||
visibility: 'specified',
|
visibility: 'specified',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const tlComponent: InstanceType<typeof MkNotes> = $ref();
|
||||||
|
|
||||||
|
function reloadTimeline() {
|
||||||
|
return new Promise<void>((res) => {
|
||||||
|
tlComponent.pagingComponent?.reload().then(() => {
|
||||||
|
reloadStream();
|
||||||
|
res();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked">
|
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
|
||||||
<template #header>
|
<template #header>
|
||||||
<i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
<i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -4,10 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<XColumn :column="column" :isStacked="isStacked">
|
<XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()">
|
||||||
<template #header><i class="ti ti-at" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
<template #header><i class="ti ti-at" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||||
|
|
||||||
<MkNotes :pagination="pagination"/>
|
<MkNotes ref="tlComponent" :pagination="pagination"/>
|
||||||
</XColumn>
|
</XColumn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -16,12 +16,24 @@ import { } from 'vue';
|
||||||
import XColumn from './column.vue';
|
import XColumn from './column.vue';
|
||||||
import { Column } from './deck-store.js';
|
import { Column } from './deck-store.js';
|
||||||
import MkNotes from '@/components/MkNotes.vue';
|
import MkNotes from '@/components/MkNotes.vue';
|
||||||
|
import { reloadStream } from '@/stream.js';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
column: Column;
|
column: Column;
|
||||||
isStacked: boolean;
|
isStacked: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const tlComponent: InstanceType<typeof MkNotes> = $ref();
|
||||||
|
|
||||||
|
function reloadTimeline() {
|
||||||
|
return new Promise<void>((res) => {
|
||||||
|
tlComponent.pagingComponent?.reload().then(() => {
|
||||||
|
reloadStream();
|
||||||
|
res();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const pagination = {
|
const pagination = {
|
||||||
endpoint: 'notes/mentions' as const,
|
endpoint: 'notes/mentions' as const,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
|
|
@ -4,10 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<XColumn :column="column" :isStacked="isStacked" :menu="menu">
|
<XColumn :column="column" :isStacked="isStacked" :menu="menu" :refresher="() => notificationsComponent.reload()">
|
||||||
<template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
<template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||||
|
|
||||||
<XNotifications :excludeTypes="props.column.excludeTypes"/>
|
<XNotifications ref="notificationsComponent" :excludeTypes="props.column.excludeTypes"/>
|
||||||
</XColumn>
|
</XColumn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -24,6 +24,8 @@ const props = defineProps<{
|
||||||
isStacked: boolean;
|
isStacked: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
let notificationsComponent = $shallowRef<InstanceType<typeof XNotifications>>();
|
||||||
|
|
||||||
function func() {
|
function func() {
|
||||||
os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), {
|
os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), {
|
||||||
excludeTypes: props.column.excludeTypes,
|
excludeTypes: props.column.excludeTypes,
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked">
|
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
|
||||||
<template #header>
|
<template #header>
|
||||||
<i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
<i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked">
|
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
|
||||||
<template #header>
|
<template #header>
|
||||||
<i v-if="column.tl === 'home'" class="ti ti-home"></i>
|
<i v-if="column.tl === 'home'" class="ti ti-home"></i>
|
||||||
<i v-else-if="column.tl === 'local'" class="ti ti-planet"></i>
|
<i v-else-if="column.tl === 'local'" class="ti ti-planet"></i>
|
||||||
|
@ -49,6 +49,7 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let disabled = $ref(false);
|
let disabled = $ref(false);
|
||||||
|
let timeline = $shallowRef<InstanceType<typeof MkTimeline>>();
|
||||||
|
|
||||||
const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable));
|
const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable));
|
||||||
const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable));
|
const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable));
|
||||||
|
|
|
@ -33,6 +33,7 @@ type AdminInstanceMetadata = DetailedInstanceMetadata & {
|
||||||
app192IconUrl: string | null;
|
app192IconUrl: string | null;
|
||||||
app512IconUrl: string | null;
|
app512IconUrl: string | null;
|
||||||
manifestJsonOverride: string;
|
manifestJsonOverride: string;
|
||||||
|
urlPreviewDenyList: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
|
@ -350,6 +351,18 @@ export type Endpoints = {
|
||||||
};
|
};
|
||||||
res: null;
|
res: null;
|
||||||
};
|
};
|
||||||
|
'admin/delete-user-avatar': {
|
||||||
|
req: {
|
||||||
|
userId: User['id'];
|
||||||
|
};
|
||||||
|
res: null;
|
||||||
|
};
|
||||||
|
'admin/delete-user-banner': {
|
||||||
|
req: {
|
||||||
|
userId: User['id'];
|
||||||
|
};
|
||||||
|
res: null;
|
||||||
|
};
|
||||||
'admin/delete-logs': {
|
'admin/delete-logs': {
|
||||||
req: NoParams;
|
req: NoParams;
|
||||||
res: null;
|
res: null;
|
||||||
|
@ -3057,10 +3070,10 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
|
||||||
// Warnings were encountered during analysis:
|
// Warnings were encountered during analysis:
|
||||||
//
|
//
|
||||||
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
|
||||||
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:20:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
||||||
// src/api.types.ts:636:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:638:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
||||||
// src/entities.ts:119:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
|
// src/entities.ts:119:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
|
||||||
// src/entities.ts:635:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
// src/entities.ts:636:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||||
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
||||||
|
|
||||||
// (No @packageDocumentation comment for this package)
|
// (No @packageDocumentation comment for this package)
|
||||||
|
|
|
@ -15,6 +15,8 @@ export type Endpoints = {
|
||||||
// admin
|
// admin
|
||||||
'admin/abuse-user-reports': { req: TODO; res: TODO; };
|
'admin/abuse-user-reports': { req: TODO; res: TODO; };
|
||||||
'admin/delete-all-files-of-a-user': { req: { userId: User['id']; }; res: null; };
|
'admin/delete-all-files-of-a-user': { req: { userId: User['id']; }; res: null; };
|
||||||
|
'admin/delete-user-avatar': { req: { userId: User['id']; }; res: null; };
|
||||||
|
'admin/delete-user-banner': { req: { userId: User['id']; }; res: null; };
|
||||||
'admin/delete-logs': { req: NoParams; res: null; };
|
'admin/delete-logs': { req: NoParams; res: null; };
|
||||||
'admin/get-index-stats': { req: TODO; res: TODO; };
|
'admin/get-index-stats': { req: TODO; res: TODO; };
|
||||||
'admin/get-table-stats': { req: TODO; res: TODO; };
|
'admin/get-table-stats': { req: TODO; res: TODO; };
|
||||||
|
|
|
@ -406,6 +406,7 @@ export type AdminInstanceMetadata = DetailedInstanceMetadata & {
|
||||||
app192IconUrl: string | null;
|
app192IconUrl: string | null;
|
||||||
app512IconUrl: string | null;
|
app512IconUrl: string | null;
|
||||||
manifestJsonOverride: string;
|
manifestJsonOverride: string;
|
||||||
|
urlPreviewDenyList: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ServerInfo = {
|
export type ServerInfo = {
|
||||||
|
|
Loading…
Reference in New Issue