diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml new file mode 100644 index 000000000000..e8da5f5e276a --- /dev/null +++ b/.config/cypress-devcontainer.yml @@ -0,0 +1,203 @@ +#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# Misskey configuration +#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +# ┌─────┐ +#───┘ URL └───────────────────────────────────────────────────── + +# Final accessible URL seen by a user. +url: 'http://misskey.local' + +# 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: 61812 + +# ┌──────────────────────────┐ +#───┘ PostgreSQL configuration └──────────────────────────────── + +db: + host: db + port: 5432 + + # Database name + db: misskey + + # Auth + user: postgres + pass: postgres + + # Whether disable Caching queries + #disableCache: true + + # Extra Connection options + #extra: + # ssl: true + +dbReplications: false + +# You can configure any number of replicas here +#dbSlaves: +# - +# host: +# port: +# db: +# user: +# pass: +# - +# host: +# port: +# db: +# user: +# pass: + +# ┌─────────────────────┐ +#───┘ Redis configuration └───────────────────────────────────── + +redis: + host: redis + port: 6379 + #family: 0 # 0=Both, 4=IPv4, 6=IPv6 + #pass: example-pass + #prefix: example-prefix + #db: 1 + +#redisForPubsub: +# host: redis +# port: 6379 +# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 +# #pass: example-pass +# #prefix: example-prefix +# #db: 1 + +#redisForJobQueue: +# host: redis +# port: 6379 +# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 +# #pass: example-pass +# #prefix: example-prefix +# #db: 1 + +#redisForTimelines: +# host: redis +# port: 6379 +# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 +# #pass: example-pass +# #prefix: example-prefix +# #db: 1 + +# ┌───────────────────────────┐ +#───┘ MeiliSearch configuration └───────────────────────────── + +#meilisearch: +# host: meilisearch +# port: 7700 +# apiKey: '' +# ssl: true +# index: '' + +# ┌───────────────┐ +#───┘ 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 +# aidx ... 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: 'aidx' + +# ┌────────────────┐ +#───┘ Error tracking └────────────────────────────────────────── + +# Sentry is available for error tracking. +# See the Sentry documentation for more details on options. + +#sentryForBackend: +# enableNodeProfiling: true +# options: +# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' + +#sentryForFrontend: +# options: +# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' + +# ┌─────────────────────┐ +#───┘ 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: 32 + +# Job attempts +# deliverJobMaxAttempts: 12 +# inboxJobMaxAttempts: 8 + +# IP address family used for outgoing request (ipv4, ipv6 or dual) +#outgoingAddressFamily: ipv4 + +# 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: true) +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 diff --git a/.devcontainer/init.sh b/.devcontainer/init.sh index 55fb1e6fa687..e02a533c1591 100755 --- a/.devcontainer/init.sh +++ b/.devcontainer/init.sh @@ -3,6 +3,8 @@ set -xe sudo chown node node_modules +sudo apt-get update +sudo apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2 libxtst6 xauth xvfb git config --global --add safe.directory /workspace git submodule update --init corepack install @@ -12,3 +14,4 @@ pnpm install --frozen-lockfile cp .devcontainer/devcontainer.yml .config/default.yml pnpm build pnpm migrate +pnpm exec cypress install diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml deleted file mode 100644 index d4e99f966ef0..000000000000 --- a/.github/workflows/changelog-check.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Check the description in CHANGELOG.md - -on: - pull_request: - branches: - - master - - develop - -jobs: - check-changelog: - runs-on: ubuntu-latest - - steps: - - name: Checkout head - uses: actions/checkout@v4.1.1 - - name: Setup Node.js - uses: actions/setup-node@v4.0.3 - with: - node-version-file: '.node-version' - - - name: Checkout base - run: | - mkdir _base - cp -r .git _base/.git - cd _base - git fetch --depth 1 origin ${{ github.base_ref }} - git checkout origin/${{ github.base_ref }} CHANGELOG.md - - - name: Copy to Checker directory for CHANGELOG-base.md - run: cp _base/CHANGELOG.md scripts/changelog-checker/CHANGELOG-base.md - - name: Copy to Checker directory for CHANGELOG-head.md - run: cp CHANGELOG.md scripts/changelog-checker/CHANGELOG-head.md - - name: diff - continue-on-error: true - run: diff -u CHANGELOG-base.md CHANGELOG-head.md - working-directory: scripts/changelog-checker - - - name: Setup Checker - run: npm install - working-directory: scripts/changelog-checker - - name: Run Checker - run: npm run run - working-directory: scripts/changelog-checker diff --git a/.github/workflows/check-misskey-js-autogen.yml b/.github/workflows/check-misskey-js-autogen.yml index 3a2a2d5f8dd7..2c656278a1dc 100644 --- a/.github/workflows/check-misskey-js-autogen.yml +++ b/.github/workflows/check-misskey-js-autogen.yml @@ -3,8 +3,7 @@ name: Check Misskey JS autogen on: pull_request_target: branches: - - master - - develop + - hanami - improve-misskey-js-autogen-check paths: - packages/backend/** diff --git a/.github/workflows/check-misskey-js-version.yml b/.github/workflows/check-misskey-js-version.yml index 99c29ac974cf..053a0d24b39f 100644 --- a/.github/workflows/check-misskey-js-version.yml +++ b/.github/workflows/check-misskey-js-version.yml @@ -2,13 +2,13 @@ name: Check Misskey JS version on: push: - branches: [ develop ] + branches: [ hanami ] paths: - packages/misskey-js/package.json - package.json - .github/workflows/check-misskey-js-version.yml pull_request: - branches: [ develop ] + branches: [ hanami ] paths: - packages/misskey-js/package.json - package.json diff --git a/.github/workflows/check_copyright_year.yml b/.github/workflows/check_copyright_year.yml deleted file mode 100644 index 03dfcd0a0b21..000000000000 --- a/.github/workflows/check_copyright_year.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Check copyright year - -on: - push: - branches: - - master - - develop - -jobs: - check_copyright_year: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4.1.1 - - run: | - if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then - echo "Please change copyright year!" - exit 1 - fi diff --git a/.github/workflows/deploy-test-environment.yml b/.github/workflows/deploy-test-environment.yml deleted file mode 100644 index 66b15beb91ea..000000000000 --- a/.github/workflows/deploy-test-environment.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: deploy-test-environment - -on: - issue_comment: - types: [created] - workflow_dispatch: - inputs: - repository: - description: 'Repository to deploy (optional, use the repository where this workflow is stored by default)' - required: false - default: '' - branch_or_hash: - description: 'Branch or Commit hash to deploy (optional, use the branch where this workflow is stored by default)' - required: false - default: '' - wait_time: - description: 'Time to wait in seconds (optional, 1800 seconds by default)' - required: false - default: '' - -jobs: - get-pr-ref: - runs-on: ubuntu-latest - if: github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/preview') - outputs: - is-allowed-user: ${{ steps.check-allowed-users.outputs.is-allowed-user }} - pr-ref: ${{ steps.get-ref.outputs.pr-ref }} - wait_time: ${{ steps.get-wait-time.outputs.wait_time }} - steps: - - name: Checkout - uses: actions/checkout@v4.1.1 - - - name: Check allowed users - id: check-allowed-users - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ORG_ID: ${{ github.repository_owner_id }} - COMMENT_AUTHOR: ${{ github.event.comment.user.login }} - run: | - MEMBERSHIP_STATUS=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/organizations/$ORG_ID/public_members/$COMMENT_AUTHOR" \ - -o /dev/null -w '%{http_code}\n' -s) - if [ "$MEMBERSHIP_STATUS" -eq 204 ]; then - echo "is-allowed-user=true" > $GITHUB_OUTPUT - else - echo "is-allowed-user=false" > $GITHUB_OUTPUT - fi - - - name: Get PR ref - id: get-ref - run: | - PR_REF="refs/pull/${{ github.event.issue.number }}/head" - echo "pr-ref=$PR_REF" >> $GITHUB_OUTPUT - - - name: Extract wait time - id: get-wait-time - env: - COMMENT_BODY: ${{ github.event.comment.body }} - run: | - WAIT_TIME=$(echo "$COMMENT_BODY" | grep -oP '(?<=/preview\s)\d+' || echo "1800") - echo "wait_time=$WAIT_TIME" > $GITHUB_OUTPUT - - deploy-test-environment-pr-comment: - needs: get-pr-ref - if: needs.get-pr-ref.outputs.is-allowed-user == 'true' - uses: joinmisskey/misskey-tga/.github/workflows/deploy-test-environment.yml@main - with: - repository: ${{ github.repository }} - branch_or_hash: ${{ needs.get-pr-ref.outputs.pr-ref }} - wait_time: ${{ needs.get-pr-ref.outputs.wait_time }} - secrets: - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} - - deploy-test-environment-wd: - if: github.event_name == 'workflow_dispatch' - uses: joinmisskey/misskey-tga/.github/workflows/deploy-test-environment.yml@main - with: - repository: ${{ inputs.repository || github.repository }} - branch_or_hash: ${{ inputs.branch_or_hash || github.ref_name }} - wait_time: ${{ inputs.wait_time || '1800' }} - secrets: - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml deleted file mode 100644 index db899ba386be..000000000000 --- a/.github/workflows/docker.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: Publish Docker image - -on: - release: - types: [published] - workflow_dispatch: - -env: - REGISTRY_IMAGE: 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}} - -jobs: - # see https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners - build: - name: Build - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - platform: - - linux/amd64 - - linux/arm64 - steps: - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - name: Check out the repo - uses: actions/checkout@v4.1.1 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY_IMAGE }} - tags: ${{ env.TAGS }} - - 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 - id: build - uses: docker/build-push-action@v6 - with: - context: . - push: true - platforms: ${{ matrix.platform }} - provenance: false - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - merge: - runs-on: ubuntu-latest - needs: - - build - steps: - - name: Download digests - uses: actions/download-artifact@v4 - with: - path: /tmp/digests - pattern: digests-* - merge-multiple: true - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY_IMAGE }} - tags: ${{ env.TAGS }} - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }} diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml index c3dba4213d31..e9aed57c0e8e 100644 --- a/.github/workflows/dockle.yml +++ b/.github/workflows/dockle.yml @@ -4,8 +4,7 @@ name: Dockle on: push: branches: - - master - - develop + - hanami pull_request: jobs: diff --git a/.github/workflows/get-api-diff.yml b/.github/workflows/get-api-diff.yml index 81e8134fb741..994ae11faf7a 100644 --- a/.github/workflows/get-api-diff.yml +++ b/.github/workflows/get-api-diff.yml @@ -4,8 +4,7 @@ name: Get api.json from Misskey on: pull_request: branches: - - master - - develop + - hanami paths: - packages/backend/** - .github/workflows/get-api-diff.yml diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml deleted file mode 100644 index 88e2aceaed6b..000000000000 --- a/.github/workflows/labeler.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: "Pull Request Labeler" -on: - pull_request_target: - branches-ignore: - - 'l10n_develop' - -jobs: - triage: - permissions: - contents: read - pull-requests: write - runs-on: ubuntu-latest - steps: - - uses: actions/labeler@v5 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1f13f4fa2fb1..3529e0260e33 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,8 +3,7 @@ name: Lint on: push: branches: - - master - - develop + - hanami paths: - packages/backend/** - packages/frontend/** @@ -40,8 +39,6 @@ jobs: needs: [pnpm_install] runs-on: ubuntu-latest continue-on-error: true - env: - eslint-cache-version: v1 strategy: matrix: workspace: @@ -49,6 +46,9 @@ jobs: - frontend - sw - misskey-js + env: + eslint-cache-version: v1 + eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }} steps: - uses: actions/checkout@v4.1.1 with: @@ -64,11 +64,10 @@ jobs: - name: Restore eslint cache uses: actions/cache@v4.0.2 with: - path: node_modules/.cache/eslint - key: eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }} - restore-keys: | - eslint-${{ env.eslint-cache-version }}-${{ hashFiles('/pnpm-lock.yaml') }}- - - run: pnpm --filter ${{ matrix.workspace }} run eslint --cache --cache-location node_modules/.cache/eslint --cache-strategy content + path: ${{ env.eslint-cache-path }} + key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }} + restore-keys: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}- + - run: pnpm --filter ${{ matrix.workspace }} run eslint --cache --cache-location ${{ env.eslint-cache-path }} --cache-strategy content typecheck: needs: [pnpm_install] diff --git a/.github/workflows/on-release-created.yml b/.github/workflows/on-release-created.yml deleted file mode 100644 index 8dd9ed2513f5..000000000000 --- a/.github/workflows/on-release-created.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: On Release Created (Publish misskey-js) - -on: - release: - types: [created] - - workflow_dispatch: - -jobs: - publish-misskey-js: - name: Publish misskey-js - runs-on: ubuntu-latest - - permissions: - contents: read - id-token: write - - strategy: - matrix: - node-version: [20.16.0] - - steps: - - uses: actions/checkout@v4.1.1 - with: - submodules: true - - name: Install pnpm - uses: pnpm/action-setup@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.0.3 - with: - node-version: ${{ matrix.node-version }} - cache: 'pnpm' - registry-url: 'https://registry.npmjs.org' - - name: Publish package - run: | - corepack enable - pnpm i --frozen-lockfile - pnpm build - pnpm --filter misskey-js publish --access public --no-git-checks --provenance - env: - NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} - NPM_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} diff --git a/.github/workflows/release-edit-with-push.yml b/.github/workflows/release-edit-with-push.yml deleted file mode 100644 index 57657a4ba7b5..000000000000 --- a/.github/workflows/release-edit-with-push.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: "Release Manager: sync changelog with PR" - -on: - push: - branches: - - develop - paths: - - 'CHANGELOG.md' - -env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -permissions: - contents: write - issues: write - pull-requests: write - -jobs: - edit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - # headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得 - - name: Get PR - run: | - echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT - id: get_pr - env: - STABLE_BRANCH: ${{ vars.STABLE_BRANCH }} - - name: Get target version - if: steps.get_pr.outputs.pr_number != '' - uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v2 - id: v - # CHANGELOG.mdの内容を取得 - - name: Get changelog - if: steps.get_pr.outputs.pr_number != '' - uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v2 - with: - version: ${{ steps.v.outputs.target_version }} - id: changelog - # PRのnotesを更新 - - name: Update PR - if: steps.get_pr.outputs.pr_number != '' - run: | - gh pr edit "$PR_NUMBER" --body "$CHANGELOG" - env: - PR_NUMBER: ${{ steps.get_pr.outputs.pr_number }} - CHANGELOG: ${{ steps.changelog.outputs.changelog }} diff --git a/.github/workflows/release-with-dispatch.yml b/.github/workflows/release-with-dispatch.yml deleted file mode 100644 index ed2f822269a1..000000000000 --- a/.github/workflows/release-with-dispatch.yml +++ /dev/null @@ -1,135 +0,0 @@ -name: "Release Manager [Dispatch]" - -on: - workflow_dispatch: - inputs: - ## Specify the type of the next release. - #version_increment_type: - # type: choice - # description: 'VERSION INCREMENT TYPE' - # default: 'patch' - # required: false - # options: - # - 'major' - # - 'minor' - # - 'patch' - merge: - type: boolean - description: 'MERGE RELEASE BRANCH TO MAIN' - default: false - start-rc: - type: boolean - description: 'Start Release Candidate' - default: false - -env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -permissions: - contents: write - issues: write - pull-requests: write - -jobs: - get-pr: - runs-on: ubuntu-latest - outputs: - pr_number: ${{ steps.get_pr.outputs.pr_number }} - steps: - - uses: actions/checkout@v4 - # headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得 - - name: Get PRs - run: | - echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT - id: get_pr - env: - STABLE_BRANCH: ${{ vars.STABLE_BRANCH }} - - merge: - uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v2 - needs: get-pr - if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge == true }} - with: - pr_number: ${{ needs.get-pr.outputs.pr_number }} - user: 'github-actions[bot]' - package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} - # Text to prepend to the changelog - # The first line must be `## Unreleased` - changes_template: | - ## Unreleased - - ### General - - - - ### Client - - - - ### Server - - - - use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} - indent: ${{ vars.INDENT }} - secrets: - RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} - RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - - create-prerelease: - uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2 - needs: get-pr - if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge != true }} - with: - pr_number: ${{ needs.get-pr.outputs.pr_number }} - user: 'github-actions[bot]' - package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} - use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} - indent: ${{ vars.INDENT }} - draft_prerelease_channel: alpha - ready_start_prerelease_channel: beta - prerelease_channel: ${{ inputs.start-rc && 'rc' || '' }} - secrets: - RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} - RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - - create-target: - uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v2 - needs: get-pr - if: ${{ needs.get-pr.outputs.pr_number == '' }} - with: - user: 'github-actions[bot]' - # The script for version increment. - # process.env.CURRENT_VERSION: The current version. - # - # Misskey calender versioning (yyyy.MM.patch) example - version_increment_script: | - const now = new Date(); - const year = now.toLocaleDateString('en-US', { year: 'numeric', timeZone: 'Asia/Tokyo' }); - const month = now.toLocaleDateString('en-US', { month: 'numeric', timeZone: 'Asia/Tokyo' }); - const [major, minor, _patch] = process.env.CURRENT_VERSION.split('.'); - const patch = Number(_patch.split('-')[0]); - if (Number.isNaN(patch)) { - console.error('Invalid patch version', year, month, process.env.CURRENT_VERSION, major, minor, _patch); - throw new Error('Invalid patch version'); - } - if (year !== major || month !== minor) { - return `${year}.${month}.0`; - } else { - return `${major}.${minor}.${patch + 1}`; - } - ##Semver example - #version_increment_script: | - # const [major, minor, patch] = process.env.CURRENT_VERSION.split('.'); - # if ("${{ inputs.version_increment_type }}" === "major") { - # return `${Number(major) + 1}.0.0`; - # } else if ("${{ inputs.version_increment_type }}" === "minor") { - # return `${major}.${Number(minor) + 1}.0`; - # } else { - # return `${major}.${minor}.${Number(patch) + 1}`; - # } - package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} - use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} - indent: ${{ vars.INDENT }} - stable_branch: ${{ vars.STABLE_BRANCH }} - draft_prerelease_channel: alpha - secrets: - RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} - RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} diff --git a/.github/workflows/release-with-ready.yml b/.github/workflows/release-with-ready.yml deleted file mode 100644 index e863b5e2e822..000000000000 --- a/.github/workflows/release-with-ready.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: "Release Manager: release RC when ready for review" - -on: - pull_request: - types: [ready_for_review] - -env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -permissions: - contents: write - issues: write - pull-requests: write - -jobs: - check: - runs-on: ubuntu-latest - outputs: - head: ${{ steps.get_pr.outputs.head }} - base: ${{ steps.get_pr.outputs.base }} - steps: - - uses: actions/checkout@v4 - # PR情報を取得 - - name: Get PR - run: | - pr_json=$(gh pr view "$PR_NUMBER" --json isDraft,headRefName,baseRefName) - echo "head=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT - echo "base=$(echo $pr_json | jq -r '.baseRefName')" >> $GITHUB_OUTPUT - id: get_pr - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - release: - uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2 - needs: check - if: needs.check.outputs.head == github.event.repository.default_branch && needs.check.outputs.base == vars.STABLE_BRANCH - with: - pr_number: ${{ github.event.pull_request.number }} - user: 'github-actions[bot]' - package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} - use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} - indent: ${{ vars.INDENT }} - draft_prerelease_channel: alpha - ready_start_prerelease_channel: beta - secrets: - RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} - RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 026550025c95..0fce7d468c81 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -3,8 +3,7 @@ name: Test (backend) on: push: branches: - - master - - develop + - hanami paths: - packages/backend/** # for permissions diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml index fcaef529695f..86a4791388f9 100644 --- a/.github/workflows/test-frontend.yml +++ b/.github/workflows/test-frontend.yml @@ -3,8 +3,7 @@ name: Test (frontend) on: push: branches: - - master - - develop + - hanami paths: - packages/frontend/** # for permissions diff --git a/.github/workflows/test-misskey-js.yml b/.github/workflows/test-misskey-js.yml index 9ad71919df15..2698ce0fa19a 100644 --- a/.github/workflows/test-misskey-js.yml +++ b/.github/workflows/test-misskey-js.yml @@ -5,12 +5,12 @@ name: Test (misskey.js) on: push: - branches: [ develop ] + branches: [ hanami ] paths: - packages/misskey-js/** - .github/workflows/test-misskey-js.yml pull_request: - branches: [ develop ] + branches: [ hanami ] paths: - packages/misskey-js/** - .github/workflows/test-misskey-js.yml diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml index 8ad8a6476696..c68e46888089 100644 --- a/.github/workflows/test-production.yml +++ b/.github/workflows/test-production.yml @@ -3,8 +3,7 @@ name: Test (production install and build) on: push: branches: - - master - - develop + - hanami pull_request: env: diff --git a/.github/workflows/validate-api-json.yml b/.github/workflows/validate-api-json.yml index 06e987f27e4f..3f8301278878 100644 --- a/.github/workflows/validate-api-json.yml +++ b/.github/workflows/validate-api-json.yml @@ -3,8 +3,7 @@ name: Test (backend) on: push: branches: - - master - - develop + - hanami paths: - packages/backend/** - .github/workflows/validate-api-json.yml diff --git a/.gitignore b/.gitignore index e33383571553..4d5bd1ce0819 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ coverage !/.config/example.yml !/.config/docker_example.yml !/.config/docker_example.env +!/.config/cypress-devcontainer.yml docker-compose.yml compose.yml .devcontainer/compose.yml @@ -44,6 +45,7 @@ compose.yml /build built built-test +js-built /data /.cache-loader /db diff --git a/CHANGELOG.md b/CHANGELOG.md index fe61d698230b..398134436bab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Client - サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように +- Enhance: アイコンデコレーション管理画面にプレビューを追加 - Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正 ### Server diff --git a/cypress/e2e/basic.cy.ts b/cypress/e2e/basic.cy.ts index 02d882fdb659..87213669cc6e 100644 --- a/cypress/e2e/basic.cy.ts +++ b/cypress/e2e/basic.cy.ts @@ -125,6 +125,10 @@ describe('After setup instance', () => { cy.get('[data-cy-user-setup-next]').click(); cy.wait(1000); + // 【設定】センシティブなメディアに関する設定 + cy.get('[data-cy-user-setup-next]').click(); + cy.wait(1000); + // 完了(「ホーム画面に進む」ボタン) cy.get('[data-cy-user-setup-complete] a').click(); diff --git a/locales/en-US.yml b/locales/en-US.yml index 6c0b6732a6ab..f953759dd917 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -2658,12 +2658,14 @@ _contextMenu: appWithShift: "Application with shift key" native: "Native" _hana: + emojiRemakrs: "remarks" hanaSettings: "HanaMisskey Settings" hanaMode: "Hana Mode" hanaModeShort: "Hana" hanaModeTutorialDescription: "One of HanaMisskey's key unique features is Hana Mode, and your social networking experience with HanaMisskey will vary greatly depending on whether you enable Hana Mode or not. Below are the main differences and recommended use cases, so please select either one and proceed." flowerEffect: "Show flurries all year round" migrateFromBackspaceKey: "Migrate from BackspaceKey" + searchIsInBeta: "Our own search engine is currently under development. Currently you are using the same search engine (Meilisearch) as regular Misskey. Stay tuned to see how the search is optimized in the future!" _inDevelopment: title: "This feature is under development" description: "HanaMisskey is packed with new features currently in development! Stay tuned to see what exciting features will be implemented." @@ -2721,8 +2723,8 @@ _hana: generateImage: "Generate Image" imageGenerated: "Your \"Just Joined HanaMisskey!\" card is ready!" imageGeneratedDescription: "Download this card and share it on existing social media platforms like X (formerly Twitter) 🎉" - shareText: "Just joined the brand-new social media \"HanaMisskey\"! Sign up and follow me!\n{url}" - shareTextForX: "Just joined a new Misskey-based social media \"HanaMisskey\"! Sign up and follow me!\n{url}" + shareText: "Just joined the brand-new social media \"HanaMisskey\"! Sign up and follow me! #はなみすきー #はなみすきーはじめましたカード\n{url}" + shareTextForX: "Just joined a new Misskey-based social media \"HanaMisskey\"! Sign up and follow me! #はなみすきー #はなみすきーはじめましたカード\n{url}" shareToX: "Post on X" shareWarning: "Due to technical limitations, the \"Just Joined HanaMisskey!\" card will not be attached automatically when posting on X.\nPlease save the image manually and upload it to the post form." _welcomeCardGenPopup: diff --git a/locales/index.d.ts b/locales/index.d.ts index 43e60ad1030e..f2582e10f6e3 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -10336,6 +10336,10 @@ export interface Locale extends ILocale { "native": string; }; "_hana": { + /** + * 備考 + */ + "emojiRemarks": string; /** * はなみすきー設定 */ @@ -10364,6 +10368,10 @@ export interface Locale extends ILocale { * BackspaceKeyからの移行 */ "migrateFromBackspaceKey": string; + /** + * 独自検索エンジンは現在開発中です。今ご利用いただけるのは通常のMisskeyと同じ検索エンジン(Meilisearch)です。今後の進化にご期待ください! + */ + "searchIsInBeta": string; /** * 特定商取引法に基づく表記 */ @@ -10467,7 +10475,7 @@ export interface Locale extends ILocale { }; "_cta": { /** - * 「はな」のあるSNS体験を楽しもう + * はなみすきーで、かわいいSNS体験を */ "title": string; }; @@ -10656,12 +10664,12 @@ export interface Locale extends ILocale { */ "imageGeneratedDescription": string; /** - * あたらしいSNS「はなみすきー」をはじめました!登録してフォローしてね! + * あたらしいSNS「 #はなみすきー 」をはじめました!登録してフォローしてね! #はなみすきーはじめましたカード * {url} */ "shareText": ParameterizedString<"url">; /** - * あたらしいMisskey系SNS「はなみすきー」をはじめました!登録してフォローしてね! + * あたらしいMisskey系SNS「 #はなみすきー 」をはじめました!登録してフォローしてね! #はなみすきーはじめましたカード * {url} */ "shareTextForX": ParameterizedString<"url">; @@ -10711,7 +10719,7 @@ export interface Locale extends ILocale { */ "bskArchives": string; /** - * 運営メンバー + * チームメンバー */ "teamMembers": string; /** @@ -10727,6 +10735,42 @@ export interface Locale extends ILocale { */ "morePatrons": string; }; + "_visitorLoginPopup": { + /** + * はなみすきーで、かわいいSNS体験を + */ + "title": string; + /** + * リアクションで楽しく。はなモードであなたらしく。あたらしいSNSをあなたも体験してみませんか。 + */ + "description": string; + }; + "_tutorialMinorSettings": { + /** + * センシティブなコンテンツに関する設定 + */ + "title": string; + /** + * センシティブなコンテンツをどのように表示するかを設定できます。 + */ + "descriotion": string; + /** + * センシティブなメディアを含むノートをミュート + */ + "muteSensitive": string; + /** + * センシティブな画像・動画・音声などを含むノートをすべて最小化した状態で表示します。クリックすると開いて中身を見ることができますが、開いたとしてもファイル自体にはぼかしがかかった状態で表示されます。 + */ + "muteSensitiveDescription": string; + /** + * センシティブなコンテンツはぼかしがかかった状態で表示されますが、誤操作防止のために、中身をクリックして開く際に追加で確認ダイアログを表示させることができます。 + */ + "confirmWhenRevealingSensitiveMediaDescription": string; + /** + * 未成年の方は、おうちの方といっしょに確認してください。この設定は後からいつでも変更できます。 + */ + "forMinor": string; + }; }; } declare const locales: { diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 23e1dc8c3e1a..e6dab5d97cac 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2757,6 +2757,7 @@ _contextMenu: native: "ブラウザのUI" _hana: + emojiRemarks: "備考" hanaSettings: "はなみすきー設定" hanaMisskey: "はなみすきー" hanaMode: "はなモード" @@ -2764,6 +2765,7 @@ _hana: hanaModeTutorialDescription: "はなみすきーの主要な独自機能として「はなモード」があります。はなモードを有効にするかどうかで、はなみすきーでのSNS体験は大きく変わってきます。以下に主な違いとおすすめのユースケースを挙げますので、どちらか選択して進んでください。" flowerEffect: "いつでも花びらを降らせる" migrateFromBackspaceKey: "BackspaceKeyからの移行" + searchIsInBeta: "独自検索エンジンは現在開発中です。今ご利用いただけるのは通常のMisskeyと同じ検索エンジン(Meilisearch)です。今後の進化にご期待ください!" commerceDisclosure: "特定商取引法に基づく表記" commerceDisclosureUrl: "特定商取引法に基づく表記URL" subscription: "サブスクリプション" @@ -2794,7 +2796,7 @@ _hana: title: "最新技術を活用した高精度な検索" description: "機械学習や最新の学術研究をもとに、はなみすきーのためにチューニングされた高精度で高速な検索機能を利用できます。また、はなみすきーの検索機能そのものが学術研究プロジェクトとなっているため、検索精度は日進月歩となることが期待できます。" _cta: - title: "「はな」のあるSNS体験を楽しもう" + title: "はなみすきーで、かわいいSNS体験を" _hanaModeSwitcher: recomenddedFor: "こんな方におすすめ" normal: "通常" @@ -2843,8 +2845,8 @@ _hana: generateImage: "画像を出力" imageGenerated: "はなみすきーはじめましたカードが完成しました" imageGeneratedDescription: "このカードをダウンロードして、X (Twitter)などの既存のSNSでシェアしてください🎉" - shareText: "あたらしいSNS「はなみすきー」をはじめました!登録してフォローしてね!\n{url}" - shareTextForX: "あたらしいMisskey系SNS「はなみすきー」をはじめました!登録してフォローしてね!\n{url}" + shareText: "あたらしいSNS「 #はなみすきー 」をはじめました!登録してフォローしてね! #はなみすきーはじめましたカード\n{url}" + shareTextForX: "あたらしいMisskey系SNS「 #はなみすきー 」をはじめました!登録してフォローしてね! #はなみすきーはじめましたカード\n{url}" shareToX: "Xでポスト" shareWarning: "技術的制約により、Xにポストする際、はじめましたカード自動では添付されません。\nお手数ですが、手動で画像を保存して、Xの投稿フォームにアップロードしていただきますようお願いします。" _welcomeCardGenPopup: @@ -2857,7 +2859,17 @@ _hana: documentation: "ドキュメント" serviceStatus: "サービス状況" bskArchives: "BackspaceKeyアーカイブ(準備中)" - teamMembers: "運営メンバー" + teamMembers: "チームメンバー" bskPatrons: "BackspaceKey時代の支援者" bskDescription: "はなみすきーの前身はBackspaceKeyというMisskeyサーバーでした。古くからのご支援に感謝します😊" morePatrons: "ほかにも多くの方にご支援いただきました。ありがとうございます😊" + _visitorLoginPopup: + title: "はなみすきーで、かわいいSNS体験を" + description: "リアクションで楽しく。はなモードであなたらしく。あたらしいSNSをあなたも体験してみませんか。" + _tutorialMinorSettings: + title: "センシティブなコンテンツに関する設定" + descriotion: "センシティブなコンテンツをどのように表示するかを設定できます。" + muteSensitive: "センシティブなメディアを含むノートをミュート" + muteSensitiveDescription: "センシティブな画像・動画・音声などを含むノートをすべて最小化した状態で表示します。クリックすると開いて中身を見ることができますが、開いたとしてもファイル自体にはぼかしがかかった状態で表示されます。" + confirmWhenRevealingSensitiveMediaDescription: "センシティブなコンテンツはぼかしがかかった状態で表示されますが、誤操作防止のために、中身をクリックして開く際に追加で確認ダイアログを表示させることができます。" + forMinor: "未成年の方は、おうちの方といっしょに確認してください。この設定は後からいつでも変更できます。" diff --git a/package.json b/package.json index 310ea9821473..3b130fdd69e4 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts", "cy:run": "pnpm cypress run", "e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run", + "e2e-dev-container": "cp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run", "jest": "cd packages/backend && pnpm jest", "jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage", "test": "pnpm -r test", diff --git a/packages/backend/assets/LICENSE-MISSKEYFLOWERS b/packages/backend/assets/LICENSE-MISSKEYFLOWERS new file mode 100644 index 000000000000..e55ebf702df2 --- /dev/null +++ b/packages/backend/assets/LICENSE-MISSKEYFLOWERS @@ -0,0 +1,22 @@ +LICENSE +------- + +The following files: + +- favicon.png +- favicon.ico + +are owned by Misskey.flowers Project and are subject to copyright, with all rights reserved by Misskey.flowers Project. +These files may not be redistributed, modified, or reused in any way without explicit permission from Misskey.flowers Project. + +Copyright (c) 2024 Misskey.flowers Project + +All rights reserved. + +This digital asset and related documentation are the property of Misskey.flowers Project. + +Permission is hereby granted to the user of this digital asset and related documentation to use, copy, and publish the asset for any purpose, provided that this notice and disclaimer of warranty appears in all copies. The user is expressly prohibited from distributing, sublicensing, selling, reselling, or transferring the asset for profit. Any form of modification or tampering with the asset is strictly prohibited. + +IN NO EVENT SHALL MISSKEY.FLOWERS PROJECT BE LIABLE FOR ANY DAMAGES WHATSOEVER, INCLUDING BUT NOT LIMITED TO DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES, ARISING OUT OF THE USE, COPYING, PUBLISHING, OR MODIFICATION OF THIS ASSET. + +NO WARRANTIES OF ANY KIND ARE OFFERED FOR THIS ASSET. THIS ASSET IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. diff --git a/packages/backend/assets/favicon.ico b/packages/backend/assets/favicon.ico index 9be1ff62956c..0cc5b6a47d52 100644 Binary files a/packages/backend/assets/favicon.ico and b/packages/backend/assets/favicon.ico differ diff --git a/packages/backend/assets/favicon.png b/packages/backend/assets/favicon.png index b4eb18a5cb8d..d3b60672956f 100644 Binary files a/packages/backend/assets/favicon.png and b/packages/backend/assets/favicon.png differ diff --git a/packages/backend/migration/1726415450771-addEmojiRemarks.js b/packages/backend/migration/1726415450771-addEmojiRemarks.js new file mode 100644 index 000000000000..60a54fc3639b --- /dev/null +++ b/packages/backend/migration/1726415450771-addEmojiRemarks.js @@ -0,0 +1,11 @@ +export class AddEmojiRemarks1726415450771 { + name = 'AddEmojiRemarks1726415450771'; + + async up(queryRunner) { + await queryRunner.query('ALTER TABLE "emoji" ADD "remarks" character varying(1024)'); + } + + async down(queryRunner) { + await queryRunner.query('ALTER TABLE "emoji" DROP COLUMN "remarks"'); + } +} diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 5db3c5b98035..2c974175f412 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -67,6 +67,7 @@ export class CustomEmojiService implements OnApplicationShutdown { isSensitive: boolean; localOnly: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][]; + remarks: string | null; }, moderator?: MiUser): Promise { const emoji = await this.emojisRepository.insertOne({ id: this.idService.gen(), @@ -82,6 +83,7 @@ export class CustomEmojiService implements OnApplicationShutdown { isSensitive: data.isSensitive, localOnly: data.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction, + remarks: data.remarks, }); if (data.host == null) { @@ -112,6 +114,7 @@ export class CustomEmojiService implements OnApplicationShutdown { isSensitive?: boolean; localOnly?: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][]; + remarks?: string | null; }, moderator?: MiUser): Promise { const emoji = await this.emojisRepository.findOneByOrFail({ id: id }); const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() }); @@ -129,6 +132,7 @@ export class CustomEmojiService implements OnApplicationShutdown { publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined, type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined, roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined, + remarks: data.remarks, }); this.localEmojisCache.refresh(); diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 358c31d3c2f6..c30153713e47 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -958,8 +958,6 @@ export class NoteCreateService implements OnApplicationShutdown { this.pushToTl(note, user, ['localTimeline', 'homeTimeline', 'userListTimeline', 'antennaTimeline']); - this.antennaService.addNoteToAntennas(note, user); - if (data.reply) { this.saveReply(data.reply, note); } diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 841bd731c0cb..d7348dc6c5c4 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -62,6 +62,7 @@ export class EmojiEntityService { isSensitive: emoji.isSensitive, localOnly: emoji.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, + remarks: emoji.remarks, }; } diff --git a/packages/backend/src/models/Emoji.ts b/packages/backend/src/models/Emoji.ts index d62b6e9f6f19..de655422aa7e 100644 --- a/packages/backend/src/models/Emoji.ts +++ b/packages/backend/src/models/Emoji.ts @@ -81,4 +81,9 @@ export class MiEmoji { array: true, length: 128, default: '{}', }) public roleIdsThatCanBeUsedThisEmojiAsReaction: string[]; + + @Column('varchar', { + length: 1024, nullable: true, + }) + public remarks: string | null; } diff --git a/packages/backend/src/models/json-schema/emoji.ts b/packages/backend/src/models/json-schema/emoji.ts index 62686ad5ae62..bdb06ede3d66 100644 --- a/packages/backend/src/models/json-schema/emoji.ts +++ b/packages/backend/src/models/json-schema/emoji.ts @@ -102,5 +102,9 @@ export const packedEmojiDetailedSchema = { format: 'id', }, }, + remarks: { + type: 'string', + optional: false, nullable: true, + }, }, } as const; diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 32f851324ba7..411396036a8e 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -225,6 +225,7 @@ export class QueueProcessorService implements OnApplicationShutdown { } }, { ...baseQueueOptions(this.config, QUEUE.DB), + concurrency: 10, autorun: false, }); diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index 171809d25c3a..d92955cebd13 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -103,6 +103,7 @@ export class ImportCustomEmojisProcessorService { isSensitive: emojiInfo.isSensitive, localOnly: emojiInfo.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: [], + remarks: null, }); } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 796f273330fb..eceaeba13670 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -56,6 +56,7 @@ export const paramDef = { roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { type: 'string', } }, + remarks: { type: 'string', nullable: true }, }, required: ['name', 'fileId'], } as const; @@ -88,6 +89,7 @@ export default class extends Endpoint { // eslint- isSensitive: ps.isSensitive ?? false, localOnly: ps.localOnly ?? false, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [], + remarks: ps.remarks ?? null, }, me); return this.emojiEntityService.packDetailed(emoji); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index 975f892df9b6..40f3c72e7bb9 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -95,6 +95,7 @@ export default class extends Endpoint { // eslint- isSensitive: emoji.isSensitive, localOnly: emoji.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, + remarks: null, }, me); return this.emojiEntityService.packDetailed(addedEmoji); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index ffb5dbf4b536..a96e1cf487be 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -102,7 +102,8 @@ export default class extends Endpoint { // eslint- emojis = emojis.filter(emoji => emoji.name.includes(ps.query!) || emoji.aliases.some(a => a.includes(ps.query!)) || - emoji.category?.includes(ps.query!)); + emoji.category?.includes(ps.query!) || + emoji.license?.includes(ps.query!)); } emojis.splice(ps.limit + 1); } else { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 22609a16a39a..595cc53fd8fc 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -51,6 +51,7 @@ export const paramDef = { type: 'string', } }, license: { type: 'string', nullable: true }, + remarks: { type: 'string', nullable: true }, isSensitive: { type: 'boolean' }, localOnly: { type: 'boolean' }, roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { @@ -103,6 +104,7 @@ export default class extends Endpoint { // eslint- isSensitive: ps.isSensitive, localOnly: ps.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction, + remarks: ps.remarks, }, me); }); } diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts index 62f8f97047c1..3fa4b68f9d4c 100644 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ b/packages/backend/src/server/api/endpoints/notes/featured.ts @@ -15,8 +15,10 @@ import { CacheService } from '@/core/CacheService.js'; export const meta = { tags: ['notes'], - requireCredential: false, - allowGet: true, + requireCredential: true, + kind: 'read:account', + + allowGet: false, cacheSec: 3600, res: { @@ -61,7 +63,7 @@ export default class extends Endpoint { // eslint- if (this.globalNotesRankingCacheLastFetchedAt !== 0 && (Date.now() - this.globalNotesRankingCacheLastFetchedAt < 1000 * 60 * 30)) { noteIds = this.globalNotesRankingCache; } else { - noteIds = await this.featuredService.getGlobalNotesRanking(100); + noteIds = await this.featuredService.getGlobalNotesRanking(500); this.globalNotesRankingCache = noteIds; this.globalNotesRankingCacheLastFetchedAt = Date.now(); } diff --git a/packages/backend/src/server/api/endpoints/notes/hanami-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hanami-timeline.ts index 457137f1293d..041522c48b9f 100644 --- a/packages/backend/src/server/api/endpoints/notes/hanami-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hanami-timeline.ts @@ -50,6 +50,11 @@ export const meta = { code: 'HanamiTL_DISABLED', id: 'ffa57e0f-d14e-48d6-a64c-8fbcba5635ab', }, + FttDisabled: { + message: 'Fanout timeline has been disabled.', + code: 'FTT_DISABLED', + id: '31f4d555-f46a-cae8-8e45-9a17740748e8', + }, }, } as const; @@ -106,160 +111,115 @@ export default class extends Endpoint { // eslint- const serverSettings = await this.metaService.fetch(); if (!serverSettings.enableFanoutTimeline) { - const timeline = await this.getFromDb({ - untilId, - sinceId, - limit: ps.limit, - includeMyRenotes: ps.includeMyRenotes, - includeRenotedMyNotes: ps.includeRenotedMyNotes, - includeLocalRenotes: ps.includeLocalRenotes, - withFiles: ps.withFiles, - withRenotes: ps.withRenotes, - }, me); - - process.nextTick(() => { - this.activeUsersChart.read(me); - }); - - return await this.noteEntityService.packMany(timeline, me); + throw new ApiError(meta.errors.FttDisabled); } - const [ - followings, - ] = await Promise.all([ - this.cacheService.userFollowingsCache.fetch(me.id), - ]); - - const packedHomeTimelineNotes = await this.fanoutTimelineEndpointService.timeline({ - untilId, - sinceId, - limit: ps.limit, - allowPartial: ps.allowPartial, - me, - useDbFallback: serverSettings.enableFanoutTimelineDbFallback, - redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`], - alwaysIncludeMyNotes: true, - excludePureRenotes: !ps.withRenotes, - noteFilter: note => { - if (note.reply && note.reply.visibility === 'followers') { - if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false; - } - - return true; - }, - dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ + const followingsPromise = this.cacheService.userFollowingsCache.fetch(me.id); + const [packedHomeTimelineNotes, feauturedNotes] = await Promise.all([ + (async () => { + const followings = await followingsPromise; + return this.fanoutTimelineEndpointService.timeline({ + untilId, + sinceId, + limit: ps.limit, + allowPartial: ps.allowPartial, + me, + useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`], + alwaysIncludeMyNotes: true, + excludePureRenotes: !ps.withRenotes, + noteFilter: note => { + if (note.reply && note.reply.visibility === 'followers') { + if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false; + } + return true; + }, + dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ + untilId, + sinceId, + limit, + includeMyRenotes: ps.includeMyRenotes, + includeRenotedMyNotes: ps.includeRenotedMyNotes, + includeLocalRenotes: ps.includeLocalRenotes, + withFiles: ps.withFiles, + withRenotes: ps.withRenotes, + }, me), + }); + })(), + this.getFeaturedNotes({ untilId, sinceId, - limit, - includeMyRenotes: ps.includeMyRenotes, - includeRenotedMyNotes: ps.includeRenotedMyNotes, - includeLocalRenotes: ps.includeLocalRenotes, - withFiles: ps.withFiles, - withRenotes: ps.withRenotes, + limit: ps.limit, }, me), - }); - - process.nextTick(() => { - this.activeUsersChart.read(me); - }); - - // 3日経っていないことを確認 - if (ps.untilId) { - if (this.idService.parse(ps.untilId).date.getTime() < Date.now() - 1000 * 60 * 60 * 24 * 3 ) { - return packedHomeTimelineNotes; - } - } - - let feauturedNoteIds: string[]; - if (this.globalNotesRankingCacheLastFetchedAt !== 0 && (Date.now() - this.globalNotesRankingCacheLastFetchedAt < 1000 * 60 * 30)) { - feauturedNoteIds = this.globalNotesRankingCache; - } else { - feauturedNoteIds = await this.featuredService.getGlobalNotesRanking(100); - this.globalNotesRankingCache = feauturedNoteIds; - this.globalNotesRankingCacheLastFetchedAt = Date.now(); - } - - // feauturedのノート数が0でないことを確認 - if (feauturedNoteIds.length === 0) { - return packedHomeTimelineNotes; - } - - const [ - userIdsWhoMeMuting, - userIdsWhoBlockingMe, - userMutedInstances, - ] = await Promise.all([ - this.cacheService.userMutingsCache.fetch(me.id), - this.cacheService.userBlockedCache.fetch(me.id), - this.cacheService.userProfileCache.fetch(me.id).then(p => new Set(p.mutedInstances)), ]); - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .where('note.id IN (:...noteIds)', { noteIds: feauturedNoteIds }) - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('note.reply', 'reply') - .leftJoinAndSelect('note.renote', 'renote') - .leftJoinAndSelect('reply.user', 'replyUser') - .leftJoinAndSelect('renote.user', 'renoteUser') - .leftJoinAndSelect('note.channel', 'channel'); - - const feauturedNotes = (await query.getMany()).filter(note => { - if (isUserRelated(note, userIdsWhoBlockingMe)) return false; - if (isUserRelated(note, userIdsWhoMeMuting)) return false; - if (isInstanceMuted(note, userMutedInstances)) return false; - - return true; - }); - if (feauturedNotes.length === 0) { return packedHomeTimelineNotes; } - const packedFeauturedNotes = await this.noteEntityService.packMany(feauturedNotes, me); if (packedHomeTimelineNotes.length === 0) { - return packedFeauturedNotes; + return packedFeauturedNotes.sort((a, b) => a.id > b.id ? -1 : 1).slice(0, ps.limit); ; } + // TODO 重複の考慮 let allNotes; - if (!ps.sinceId && !ps.untilId) { - // 最初の読み込みのトップに人気投稿を入れる - const sortedFeaturedNotes = packedFeauturedNotes - .slice(0, 5) - .sort((a, b) => a.id > b.id ? -1 : 1); + // フィーチャーされた投稿の上位20件をシャッフル + const top20FeaturedNotes = packedFeauturedNotes.slice(0, 20); + for (let i = top20FeaturedNotes.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [top20FeaturedNotes[i], top20FeaturedNotes[j]] = [top20FeaturedNotes[j], top20FeaturedNotes[i]]; + } + + const [ + featuredTop, + remainingFeaturedNotes, + homeTimelineTop2, + remainingHomeTimelineNotes, + remainingFeaturedNotesFromFullList, + ] = await Promise.all([ + (async () => top20FeaturedNotes.slice(0, 4))(), + (async () => top20FeaturedNotes.slice(4))(), + (async () => packedHomeTimelineNotes.slice(0, 2))(), + (async () => packedHomeTimelineNotes.slice(2))(), + (async () => packedFeauturedNotes.slice(20))(), + ]); + + const mixedTop = [...homeTimelineTop2, ...featuredTop]; const remainingNotes = [ - ...packedFeauturedNotes.slice(5), - ...packedHomeTimelineNotes, + ...remainingFeaturedNotes, + ...remainingHomeTimelineNotes, + ...remainingFeaturedNotesFromFullList, ].sort((a, b) => a.id > b.id ? -1 : 1); - allNotes = [ - ...sortedFeaturedNotes, // 先頭5件を追加 - ...remainingNotes, - ]; + allNotes = [...mixedTop, ...remainingNotes]; } else { - allNotes = [ - ...packedHomeTimelineNotes, - ...packedFeauturedNotes, - ].sort((a, b) => a.id > b.id ? -1 : 1); + allNotes = [...packedHomeTimelineNotes, ...packedFeauturedNotes].sort((a, b) => a.id > b.id ? -1 : 1); } // 重複を排除 - allNotes = allNotes.filter((note, index, self) => - index === self.findIndex(n => n.id === note.id), - ); + const seenIds = new Set(); + allNotes = allNotes.filter(note => { + if (seenIds.has(note.id)) { + return false; + } + seenIds.add(note.id); + return true; + }); // リミットを適用(リミットはあくまで最大であってそれより少なくなってもいいため) const limitedNotes = allNotes.slice(0, ps.limit); - const homeTimelineIds = packedHomeTimelineNotes.map(note => note.id); + const homeTimelineIdsSet = new Set(packedHomeTimelineNotes.map(note => note.id)); // ホームタイムラインからのノートが存在し、かつリミット適用後の最小IDがホームタイムラインに由来しない場合 let minNoteId: string | null = limitedNotes.length > 0 ? limitedNotes[limitedNotes.length - 1].id : null; - if (homeTimelineIds.length > 0 && minNoteId && !homeTimelineIds.includes(minNoteId)) { + + if (homeTimelineIdsSet.size > 0 && limitedNotes.some(note => homeTimelineIdsSet.has(note.id))) { // 最小IDがホームタイムラインに由来しない場合、最小のノートを削除していく // これによってホームタイムラインの取得漏れが消える - while (minNoteId && !homeTimelineIds.includes(minNoteId)) { + while (minNoteId && !homeTimelineIdsSet.has(minNoteId)) { limitedNotes.pop(); minNoteId = limitedNotes.length > 0 ? limitedNotes[limitedNotes.length - 1].id : null; } @@ -268,6 +228,50 @@ export default class extends Endpoint { // eslint- }); } + private async getFeaturedNotes(ps: { untilId: string | null; sinceId: string | null; limit: number; }, me: MiLocalUser) { + let feauturedNoteIds: string[]; + if (this.globalNotesRankingCacheLastFetchedAt !== 0 && (Date.now() - this.globalNotesRankingCacheLastFetchedAt < 1000 * 60 * 30)) { + feauturedNoteIds = this.globalNotesRankingCache; + } else { + feauturedNoteIds = await this.featuredService.getGlobalNotesRanking(100); + this.globalNotesRankingCache = feauturedNoteIds; + this.globalNotesRankingCacheLastFetchedAt = Date.now(); + } + + if (feauturedNoteIds.length === 0) { + return []; + } + + const [ + userIdsWhoMeMuting, + userIdsWhoBlockingMe, + userMutedInstances, + ] = await Promise.all([ + this.cacheService.userMutingsCache.fetch(me.id), + this.cacheService.userBlockedCache.fetch(me.id), + this.cacheService.userProfileCache.fetch(me.id).then(p => new Set(p.mutedInstances)), + ]); + + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .where('note.id IN (:...noteIds)', { noteIds: feauturedNoteIds }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel'); + + const feauturedNotes = (await query.getMany()).filter(note => { + if (isUserRelated(note, userIdsWhoBlockingMe)) return false; + if (isUserRelated(note, userIdsWhoMeMuting)) return false; + if (isInstanceMuted(note, userMutedInstances)) return false; + + return true; + }); + + return feauturedNotes; + } + private async getFromDb(ps: { untilId: string | null; sinceId: string | null; limit: number; includeMyRenotes: boolean; includeRenotedMyNotes: boolean; includeLocalRenotes: boolean; withFiles: boolean; withRenotes: boolean; }, me: MiLocalUser) { const followees = await this.userFollowingService.getFollowees(me.id); const followingChannels = await this.channelFollowingsRepository.find({ diff --git a/packages/backend/src/server/api/stream/channels/hanami-timeline.ts b/packages/backend/src/server/api/stream/channels/hanami-timeline.ts index 367affa3d3a8..ab078e9bf1d6 100644 --- a/packages/backend/src/server/api/stream/channels/hanami-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hanami-timeline.ts @@ -10,6 +10,7 @@ import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { JsonObject } from '@/misc/json-value.js'; +import { FeaturedService } from '@/core/FeaturedService.js'; import Channel, { type MiChannelService } from '../channel.js'; class HanamiTimelineChannel extends Channel { @@ -20,9 +21,14 @@ class HanamiTimelineChannel extends Channel { private withRenotes: boolean; private withFiles: boolean; + private featuredNoteIds: string[]; + private globalNotesRankingCache: string[] = []; + private globalNotesRankingCacheLastFetchedAt = 0; + constructor( private noteEntityService: NoteEntityService, private roleService: RoleService, + private featuredService: FeaturedService, id: string, connection: Channel['connection'], @@ -36,6 +42,18 @@ class HanamiTimelineChannel extends Channel { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.hanamiTlAvailable) return; + const shouldUseCache = this.globalNotesRankingCacheLastFetchedAt !== 0 + && (Date.now() - this.globalNotesRankingCacheLastFetchedAt < 1000 * 60 * 30); + + this.featuredNoteIds = shouldUseCache + ? this.globalNotesRankingCache + : await this.featuredService.getGlobalNotesRanking(100); + + if (!shouldUseCache) { + this.globalNotesRankingCache = this.featuredNoteIds; + this.globalNotesRankingCacheLastFetchedAt = Date.now(); + } + this.withRenotes = !!(params.withRenotes ?? true); this.withFiles = !!(params.withFiles ?? false); @@ -48,24 +66,39 @@ class HanamiTimelineChannel extends Channel { if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; - if (note.channelId) { - if (!this.followingChannels.has(note.channelId)) return; + const followingSet = new Set(Object.keys(this.following)); + + if (isRenotePacked(note) && this.featuredNoteIds.includes(note.renoteId) && note.visibility === 'public') { + // セルフリノートは弾く + if (note.userId === note.renote?.userId) { + return; + } + // 20% の確率でタイムラインに表示 + const randomChance = Math.random(); + if (randomChance > 0.2) { + return; + } } else { // その投稿のユーザーをフォローしていなかったら弾く - if (!isMe && !Object.hasOwn(this.following, note.userId)) return; + if (note.channelId) { + if (!this.followingChannels.has(note.channelId)) return; + } else { + if (!isMe && !followingSet.has(note.userId)) return; + } } if (note.visibility === 'followers') { - if (!isMe && !Object.hasOwn(this.following, note.userId)) return; + if (!isMe && !followingSet.has(note.userId)) return; } else if (note.visibility === 'specified') { - if (!isMe && !note.visibleUserIds!.includes(this.user!.id)) return; + const visibleUserIdsSet = new Set(note.visibleUserIds ?? []); // null または undefined の場合に空配列 + if (!isMe && !visibleUserIdsSet.has(this.user!.id)) return; } if (note.reply) { const reply = note.reply; if (this.following[note.userId]?.withReplies) { // 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く - if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; + if (reply.visibility === 'followers' && !followingSet.has(reply.userId) && reply.userId !== this.user!.id) return; } else { // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return; @@ -78,7 +111,7 @@ class HanamiTimelineChannel extends Channel { if (note.renote.reply) { const reply = note.renote.reply; // 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く - if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; + if (reply.visibility === 'followers' && !followingSet.has(reply.userId) && reply.userId !== this.user!.id) return; } } @@ -112,6 +145,7 @@ export class HanamiTimelineChannelService implements MiChannelService { constructor( private noteEntityService: NoteEntityService, private roleService: RoleService, + private featuredService: FeaturedService, ) { } @@ -120,6 +154,7 @@ export class HanamiTimelineChannelService implements MiChannelService { return new HanamiTimelineChannel( this.noteEntityService, this.roleService, + this.featuredService, id, connection, ); diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 4a056f598a2e..ef7de75eab89 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -335,6 +335,10 @@ export async function mainBoot() { main.on('myTokenRegenerated', () => { signout(); }); + } else if (location.pathname !== '/' && !instance.disableRegistration) { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/HanaVisitorLoginPopup.vue')), {}, { + closed: () => dispose(), + }); } // shortcut diff --git a/packages/frontend/src/components/HanaVisitorLoginPopup.vue b/packages/frontend/src/components/HanaVisitorLoginPopup.vue new file mode 100644 index 000000000000..f8951c18fb63 --- /dev/null +++ b/packages/frontend/src/components/HanaVisitorLoginPopup.vue @@ -0,0 +1,106 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue index 4b2456224991..57d325b11ad3 100644 --- a/packages/frontend/src/components/MkChart.vue +++ b/packages/frontend/src/components/MkChart.vue @@ -13,29 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only - + + + + diff --git a/packages/frontend/src/components/MkTutorial.vue b/packages/frontend/src/components/MkTutorial.vue index 51e22709e2e5..1bb1019624c4 100644 --- a/packages/frontend/src/components/MkTutorial.vue +++ b/packages/frontend/src/components/MkTutorial.vue @@ -146,6 +146,10 @@ export const tutorialBodyPagesDef = [{ icon: 'ti ti-lock', type: 'setup', title: i18n.ts._initialTutorial._privacySettings.title, +}, { + icon: 'ti ti-shield-checkered', + type: 'setup', + title: i18n.ts._hana._tutorialMinorSettings.title, }] as const satisfies TutorialPage[]; export const MAX_PAGE = tutorialBodyPagesDef.length + 1; // 0始まりにするために +2 - 1 = +1 @@ -162,6 +166,7 @@ import XFollowUsers from '@/components/MkTutorial.FollowUsers.vue'; import XPostNote from '@/components/MkTutorial.PostNote.vue'; import XSensitive from '@/components/MkTutorial.Sensitive.vue'; import XPrivacySettings from '@/components/MkTutorial.PrivacySettings.vue'; +import XMinorSettings from '@/components/MkTutorial.MinorSettings.vue'; import MkAnimBg from '@/components/MkAnimBg.vue'; import { instance } from '@/instance.js'; import { host } from '@/config.js'; @@ -207,6 +212,7 @@ const componentsDef: Tuple = [ { component: XPostNote }, { component: XSensitive }, { component: XPrivacySettings }, + { component: XMinorSettings }, ]; // eslint-disable-next-line vue/no-setup-props-destructure diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue index 188cc37f4140..553717a29e47 100644 --- a/packages/frontend/src/components/MkUpdated.vue +++ b/packages/frontend/src/components/MkUpdated.vue @@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.misskeyUpdated }}
✨{{ version }}🚀
{{ i18n.ts.whatIsNew }} + {{ i18n.ts.whatIsNew }} ({{ i18n.ts._hana.hanaMisskey }}) {{ i18n.ts.gotIt }} @@ -30,6 +31,11 @@ function whatIsNew() { window.open(`https://misskey-hub.net/docs/releases/#_${version.replace(/\./g, '')}`, '_blank'); } +function whatIsNewHana() { + modal.value?.close(); + window.open(`https://docs.misskey.flowers/changelog/web#_${version.replace(/\./g, '-')}`, '_blank'); +} + onMounted(() => { confetti({ duration: 1000 * 3, diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue index 303e49de00fa..26ba598498d7 100644 --- a/packages/frontend/src/components/MkWindow.vue +++ b/packages/frontend/src/components/MkWindow.vue @@ -508,10 +508,6 @@ defineExpose({ .header { --height: 39px; - &.mini { - --height: 32px; - } - display: flex; position: relative; z-index: 1; @@ -524,6 +520,10 @@ defineExpose({ //border-bottom: solid 1px var(--divider); font-size: 90%; font-weight: bold; + + &.mini { + --height: 32px; + } } .headerButton { diff --git a/packages/frontend/src/pages/about-hanamisskey.vue b/packages/frontend/src/pages/about-hanamisskey.vue index 5f8a6fa06f8b..14f74097976a 100644 --- a/packages/frontend/src/pages/about-hanamisskey.vue +++ b/packages/frontend/src/pages/about-hanamisskey.vue @@ -21,17 +21,14 @@ {{ i18n.ts._hana._aboutHanaMisskey.documentation }} - {{ i18n.ts._hana._aboutHanaMisskey.serviceStatus }} - {{ i18n.ts._hana._aboutHanaMisskey.bskArchives }} - @@ -60,6 +57,22 @@ + + +
+ + + + +
+
@@ -86,13 +99,13 @@ import { ref, computed } from 'vue'; import FormLink from '@/components/form/link.vue'; import FormSection from '@/components/form/section.vue'; +import MkKeyValue from '@/components/MkKeyValue.vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { $i } from '@/account.js'; import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js'; -import { instance } from '@/instance.js'; const patronsWithIcon = [{ name: 'masnolia', @@ -150,110 +163,114 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts._hana.aboutHanaMisskey, - icon: null, + icon: 'ti ti-hanamisskey-hanamode', })); - +} + - +.patronIcon { + width: 24px; + border-radius: 100%; +} + +.patronName { + margin-left: 12px; +} + diff --git a/packages/frontend/src/pages/about.overview.vue b/packages/frontend/src/pages/about.overview.vue index e102dc9dc962..097bc39713d6 100644 --- a/packages/frontend/src/pages/about.overview.vue +++ b/packages/frontend/src/pages/about.overview.vue @@ -27,14 +27,22 @@ SPDX-License-Identifier: AGPL-3.0-only
- - - {{ i18n.ts._hana.aboutHanaMisskey }} - +
+ + + {{ i18n.ts._hana.aboutHanaMisskey }} + + + + {{ i18n.ts.aboutMisskey }} + +
+ {{ i18n.ts.sourceCode }} diff --git a/packages/frontend/src/pages/admin/overview.users.vue b/packages/frontend/src/pages/admin/overview.users.vue index 408be88d4792..a7dd4c0a485f 100644 --- a/packages/frontend/src/pages/admin/overview.users.vue +++ b/packages/frontend/src/pages/admin/overview.users.vue @@ -47,14 +47,14 @@ useInterval(fetch, 1000 * 60, { .root { &:global { > .users { - .chart-move { - transition: transform 1s ease; - } - display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); grid-gap: 12px; + .chart-move { + transition: transform 1s ease; + } + > .user:hover { text-decoration: none; } diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue index ad9ec3c4eece..b377314856a5 100644 --- a/packages/frontend/src/pages/avatar-decorations.vue +++ b/packages/frontend/src/pages/avatar-decorations.vue @@ -12,19 +12,31 @@ SPDX-License-Identifier: AGPL-3.0-only -
- - - - - - - - - -
- {{ i18n.ts.save }} - {{ i18n.ts.delete }} +
+
+
+
+ +
+
+ +
+
+
+ + + + + + + + + +
+ {{ i18n.ts.save }} + {{ i18n.ts.delete }} +
+
@@ -39,6 +51,7 @@ import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; +import { signinRequired } from '@/account.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; @@ -47,6 +60,8 @@ import MkFolder from '@/components/MkFolder.vue'; const avatarDecorations = ref([]); +const $i = signinRequired(); + function add() { avatarDecorations.value.unshift({ _id: Math.random().toString(36), @@ -99,3 +114,55 @@ definePageMetadata(() => ({ icon: 'ti ti-sparkles', })); + + diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index 853c1d6b0b5d..8fe2b9929041 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -66,6 +66,9 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn }}
+ + + isSensitive {{ i18n.ts.localOnly }} {{ i18n.ts.delete }} @@ -93,6 +96,7 @@ import { customEmojiCategories } from '@/custom-emojis.js'; import MkSwitch from '@/components/MkSwitch.vue'; import { selectFile } from '@/scripts/select-file.js'; import MkRolePreview from '@/components/MkRolePreview.vue'; +import MkTextarea from '@/components/MkTextarea.vue'; const props = defineProps<{ emoji?: any, @@ -108,6 +112,7 @@ const localOnly = ref(props.emoji ? props.emoji.localOnly : false); const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []); const rolesThatCanBeUsedThisEmojiAsReaction = ref([]); const file = ref(); +const remarks = ref(props.emoji ? props.emoji.remarks : ''); watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => { rolesThatCanBeUsedThisEmojiAsReaction.value = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.value.map((id) => misskeyApi('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null); @@ -153,6 +158,7 @@ async function done() { isSensitive: isSensitive.value, localOnly: localOnly.value, roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id), + remarks: remarks.value === '' ? null : remarks.value, }; if (file.value) { diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 4ba428d53630..c69530b34349 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -134,7 +134,7 @@ SPDX-License-Identifier: AGPL-3.0-only