Merge pull request #3392 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 0b8ce7200aa3121d07aa3866ddfc99de3bcf319e
This commit is contained in:
commit
5bb2518888
2
.github/actions/setup-javascript/action.yml
vendored
2
.github/actions/setup-javascript/action.yml
vendored
@ -23,7 +23,7 @@ runs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
||||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
|
|||||||
8
.github/workflows/build-container-image.yml
vendored
8
.github/workflows/build-container-image.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
|||||||
- linux/arm64
|
- linux/arm64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
env:
|
env:
|
||||||
@ -100,7 +100,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
if: ${{ inputs.push_to_images != '' }}
|
if: ${{ inputs.push_to_images != '' }}
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||||
with:
|
with:
|
||||||
# `hashFiles` is used to disambiguate between streaming and non-streaming images
|
# `hashFiles` is used to disambiguate between streaming and non-streaming images
|
||||||
name: digests-${{ hashFiles(inputs.file_to_build) }}-${{ env.PLATFORM_PAIR }}
|
name: digests-${{ hashFiles(inputs.file_to_build) }}-${{ env.PLATFORM_PAIR }}
|
||||||
@ -119,10 +119,10 @@ jobs:
|
|||||||
PUSH_TO_IMAGES: ${{ inputs.push_to_images }}
|
PUSH_TO_IMAGES: ${{ inputs.push_to_images }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.temp }}/digests
|
path: ${{ runner.temp }}/digests
|
||||||
# `hashFiles` is used to disambiguate between streaming and non-streaming images
|
# `hashFiles` is used to disambiguate between streaming and non-streaming images
|
||||||
|
|||||||
2
.github/workflows/build-push-pr.yml
vendored
2
.github/workflows/build-push-pr.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Repository needs to be cloned so `git rev-parse` below works
|
# Repository needs to be cloned so `git rev-parse` below works
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
- id: version_vars
|
- id: version_vars
|
||||||
run: |
|
run: |
|
||||||
echo mastodon_version_metadata=pr-${{ github.event.pull_request.number }}-$(git rev-parse --short ${{github.event.pull_request.head.sha}}) >> $GITHUB_OUTPUT
|
echo mastodon_version_metadata=pr-${{ github.event.pull_request.number }}-$(git rev-parse --short ${{github.event.pull_request.head.sha}}) >> $GITHUB_OUTPUT
|
||||||
|
|||||||
2
.github/workflows/bundler-audit.yml
vendored
2
.github/workflows/bundler-audit.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1
|
uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1
|
||||||
|
|||||||
12
.github/workflows/bundlesize-compare.yml
vendored
12
.github/workflows/bundlesize-compare.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
ANALYZE_BUNDLE_SIZE: '1'
|
ANALYZE_BUNDLE_SIZE: '1'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
@ -26,7 +26,7 @@ jobs:
|
|||||||
run: yarn run build:production
|
run: yarn run build:production
|
||||||
|
|
||||||
- name: Upload stats.json
|
- name: Upload stats.json
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||||
with:
|
with:
|
||||||
name: head-stats
|
name: head-stats
|
||||||
path: ./stats.json
|
path: ./stats.json
|
||||||
@ -40,7 +40,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
ANALYZE_BUNDLE_SIZE: '1'
|
ANALYZE_BUNDLE_SIZE: '1'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.base_ref }}
|
ref: ${{ github.base_ref }}
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ jobs:
|
|||||||
run: yarn run build:production
|
run: yarn run build:production
|
||||||
|
|
||||||
- name: Upload stats.json
|
- name: Upload stats.json
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||||
with:
|
with:
|
||||||
name: base-stats
|
name: base-stats
|
||||||
path: ./stats.json
|
path: ./stats.json
|
||||||
@ -64,9 +64,9 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||||
|
|
||||||
- uses: twk3/rollup-size-compare-action@5d3e409fcfe15d8ebb0edfe87e772c04b287f660 # v1.0.0
|
- uses: twk3/rollup-size-compare-action@a1f8628fee0e40899ab2b46c1b6e14552b99281e # v1.2.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
current-stats-json-path: ./head-stats/stats.json
|
current-stats-json-path: ./head-stats/stats.json
|
||||||
|
|||||||
2
.github/workflows/check-i18n.yml
vendored
2
.github/workflows/check-i18n.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Set up Ruby environment
|
- name: Set up Ruby environment
|
||||||
uses: ./.github/actions/setup-ruby
|
uses: ./.github/actions/setup-ruby
|
||||||
|
|||||||
4
.github/workflows/chromatic.yml
vendored
4
.github/workflows/chromatic.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
changed: ${{ steps.filter.outputs.src }}
|
changed: ${{ steps.filter.outputs.src }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ jobs:
|
|||||||
if: github.repository == 'mastodon/mastodon' && needs.pathcheck.outputs.changed == 'true'
|
if: github.repository == 'mastodon/mastodon' && needs.pathcheck.outputs.changed == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
|
|||||||
@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Increase Git http.postBuffer
|
- name: Increase Git http.postBuffer
|
||||||
# This is needed due to a bug in Ubuntu's cURL version?
|
# This is needed due to a bug in Ubuntu's cURL version?
|
||||||
@ -51,7 +51,7 @@ jobs:
|
|||||||
|
|
||||||
# Create or update the pull request
|
# Create or update the pull request
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||||
with:
|
with:
|
||||||
commit-message: 'New Crowdin translations'
|
commit-message: 'New Crowdin translations'
|
||||||
title: 'New Crowdin Translations for ${{ github.base_ref || github.ref_name }} (automated)'
|
title: 'New Crowdin Translations for ${{ github.base_ref || github.ref_name }} (automated)'
|
||||||
|
|||||||
4
.github/workflows/crowdin-download.yml
vendored
4
.github/workflows/crowdin-download.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Increase Git http.postBuffer
|
- name: Increase Git http.postBuffer
|
||||||
# This is needed due to a bug in Ubuntu's cURL version?
|
# This is needed due to a bug in Ubuntu's cURL version?
|
||||||
@ -53,7 +53,7 @@ jobs:
|
|||||||
|
|
||||||
# Create or update the pull request
|
# Create or update the pull request
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7
|
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8
|
||||||
with:
|
with:
|
||||||
commit-message: 'New Crowdin translations'
|
commit-message: 'New Crowdin translations'
|
||||||
title: 'New Crowdin Translations (automated)'
|
title: 'New Crowdin Translations (automated)'
|
||||||
|
|||||||
2
.github/workflows/crowdin-upload.yml
vendored
2
.github/workflows/crowdin-upload.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: crowdin action
|
- name: crowdin action
|
||||||
uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2
|
uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2
|
||||||
|
|||||||
2
.github/workflows/format-check.yml
vendored
2
.github/workflows/format-check.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
|
|||||||
2
.github/workflows/lint-css.yml
vendored
2
.github/workflows/lint-css.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
|
|||||||
2
.github/workflows/lint-haml.yml
vendored
2
.github/workflows/lint-haml.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1
|
uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1
|
||||||
|
|||||||
2
.github/workflows/lint-js.yml
vendored
2
.github/workflows/lint-js.yml
vendored
@ -38,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
|
|||||||
2
.github/workflows/lint-ruby.yml
vendored
2
.github/workflows/lint-ruby.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1
|
uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1
|
||||||
|
|||||||
2
.github/workflows/test-js.yml
vendored
2
.github/workflows/test-js.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
|
|||||||
2
.github/workflows/test-migrations.yml
vendored
2
.github/workflows/test-migrations.yml
vendored
@ -72,7 +72,7 @@ jobs:
|
|||||||
BUNDLE_RETRY: 3
|
BUNDLE_RETRY: 3
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Set up Ruby environment
|
- name: Set up Ruby environment
|
||||||
uses: ./.github/actions/setup-ruby
|
uses: ./.github/actions/setup-ruby
|
||||||
|
|||||||
30
.github/workflows/test-ruby.yml
vendored
30
.github/workflows/test-ruby.yml
vendored
@ -32,7 +32,7 @@ jobs:
|
|||||||
SECRET_KEY_BASE_DUMMY: 1
|
SECRET_KEY_BASE_DUMMY: 1
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Set up Ruby environment
|
- name: Set up Ruby environment
|
||||||
uses: ./.github/actions/setup-ruby
|
uses: ./.github/actions/setup-ruby
|
||||||
@ -43,7 +43,7 @@ jobs:
|
|||||||
onlyProduction: 'true'
|
onlyProduction: 'true'
|
||||||
|
|
||||||
- name: Cache assets from compilation
|
- name: Cache assets from compilation
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
public/assets
|
public/assets
|
||||||
@ -65,7 +65,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* tmp/cache/vite/last-build*.json
|
tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* tmp/cache/vite/last-build*.json
|
||||||
|
|
||||||
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||||
if: matrix.mode == 'test'
|
if: matrix.mode == 'test'
|
||||||
with:
|
with:
|
||||||
path: |-
|
path: |-
|
||||||
@ -128,9 +128,9 @@ jobs:
|
|||||||
- '3.3'
|
- '3.3'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
|
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||||
with:
|
with:
|
||||||
path: './'
|
path: './'
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
@ -151,7 +151,7 @@ jobs:
|
|||||||
bin/flatware fan bin/rails db:test:prepare
|
bin/flatware fan bin/rails db:test:prepare
|
||||||
|
|
||||||
- name: Cache RSpec persistence file
|
- name: Cache RSpec persistence file
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
tmp/rspec/examples.txt
|
tmp/rspec/examples.txt
|
||||||
@ -222,9 +222,9 @@ jobs:
|
|||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
|
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||||
with:
|
with:
|
||||||
path: './'
|
path: './'
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
@ -247,7 +247,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Cache Playwright Chromium browser
|
- name: Cache Playwright Chromium browser
|
||||||
id: playwright-cache
|
id: playwright-cache
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/ms-playwright
|
path: ~/.cache/ms-playwright
|
||||||
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||||
@ -263,14 +263,14 @@ jobs:
|
|||||||
- run: bin/rspec spec/system --tag streaming --tag js
|
- run: bin/rspec spec/system --tag streaming --tag js
|
||||||
|
|
||||||
- name: Archive logs
|
- name: Archive logs
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: e2e-logs-${{ matrix.ruby-version }}
|
name: e2e-logs-${{ matrix.ruby-version }}
|
||||||
path: log/
|
path: log/
|
||||||
|
|
||||||
- name: Archive test screenshots
|
- name: Archive test screenshots
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: e2e-screenshots-${{ matrix.ruby-version }}
|
name: e2e-screenshots-${{ matrix.ruby-version }}
|
||||||
@ -360,9 +360,9 @@ jobs:
|
|||||||
search-image: opensearchproject/opensearch:2
|
search-image: opensearchproject/opensearch:2
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
|
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||||
with:
|
with:
|
||||||
path: './'
|
path: './'
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
@ -382,14 +382,14 @@ jobs:
|
|||||||
- run: bin/rspec --tag search
|
- run: bin/rspec --tag search
|
||||||
|
|
||||||
- name: Archive logs
|
- name: Archive logs
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: test-search-logs-${{ matrix.ruby-version }}
|
name: test-search-logs-${{ matrix.ruby-version }}
|
||||||
path: log/
|
path: log/
|
||||||
|
|
||||||
- name: Archive test screenshots
|
- name: Archive test screenshots
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: test-search-screenshots
|
name: test-search-screenshots
|
||||||
|
|||||||
@ -226,6 +226,72 @@ module JsonLdHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Iterate through the pages of an activitypub collection,
|
||||||
|
# returning the collected items and the number of pages that were fetched.
|
||||||
|
#
|
||||||
|
# @param collection_or_uri [String, Hash]
|
||||||
|
# either the URI or an already-fetched AP object
|
||||||
|
# @param max_pages [Integer, nil]
|
||||||
|
# Max pages to fetch, if nil, fetch until no more pages
|
||||||
|
# @param max_items [Integer, nil]
|
||||||
|
# Max items to fetch, if nil, fetch until no more items
|
||||||
|
# @param reference_uri [String, nil]
|
||||||
|
# If not nil, a URI to compare to the collection URI.
|
||||||
|
# If the host of the collection URI does not match the reference URI,
|
||||||
|
# do not fetch the collection page.
|
||||||
|
# @param on_behalf_of [Account, nil]
|
||||||
|
# Sign the request on behalf of the Account, if not nil
|
||||||
|
# @return [Array<Array<Hash>, Integer>, nil]
|
||||||
|
# The collection items and the number of pages fetched
|
||||||
|
def collection_items(collection_or_uri, max_pages: 1, max_items: nil, reference_uri: nil, on_behalf_of: nil)
|
||||||
|
collection = fetch_collection_page(collection_or_uri, reference_uri: reference_uri, on_behalf_of: on_behalf_of)
|
||||||
|
return unless collection.is_a?(Hash)
|
||||||
|
|
||||||
|
collection = fetch_collection_page(collection['first'], reference_uri: reference_uri, on_behalf_of: on_behalf_of) if collection['first'].present?
|
||||||
|
return unless collection.is_a?(Hash)
|
||||||
|
|
||||||
|
items = []
|
||||||
|
n_pages = 1
|
||||||
|
while collection.is_a?(Hash)
|
||||||
|
items.concat(as_array(collection_page_items(collection)))
|
||||||
|
|
||||||
|
break if !max_items.nil? && items.size >= max_items
|
||||||
|
break if !max_pages.nil? && n_pages >= max_pages
|
||||||
|
|
||||||
|
collection = collection['next'].present? ? fetch_collection_page(collection['next'], reference_uri: reference_uri, on_behalf_of: on_behalf_of) : nil
|
||||||
|
n_pages += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
[items, n_pages]
|
||||||
|
end
|
||||||
|
|
||||||
|
def collection_page_items(collection)
|
||||||
|
case collection['type']
|
||||||
|
when 'Collection', 'CollectionPage'
|
||||||
|
collection['items']
|
||||||
|
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||||
|
collection['orderedItems']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Fetch a single collection page
|
||||||
|
# To get the whole collection, use collection_items
|
||||||
|
#
|
||||||
|
# @param collection_or_uri [String, Hash]
|
||||||
|
# @param reference_uri [String, nil]
|
||||||
|
# If not nil, a URI to compare to the collection URI.
|
||||||
|
# If the host of the collection URI does not match the reference URI,
|
||||||
|
# do not fetch the collection page.
|
||||||
|
# @param on_behalf_of [Account, nil]
|
||||||
|
# Sign the request on behalf of the Account, if not nil
|
||||||
|
# @return [Hash, nil]
|
||||||
|
def fetch_collection_page(collection_or_uri, reference_uri: nil, on_behalf_of: nil)
|
||||||
|
return collection_or_uri if collection_or_uri.is_a?(Hash)
|
||||||
|
return if !reference_uri.nil? && non_matching_uri_hosts?(reference_uri, collection_or_uri)
|
||||||
|
|
||||||
|
fetch_resource_without_id_validation(collection_or_uri, on_behalf_of, raise_on_error: :temporary)
|
||||||
|
end
|
||||||
|
|
||||||
def valid_activitypub_content_type?(response)
|
def valid_activitypub_content_type?(response)
|
||||||
return true if response.mime_type == 'application/activity+json'
|
return true if response.mime_type == 'application/activity+json'
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { fetchRelationships } from './accounts';
|
|||||||
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||||
import { unreblog, reblog } from './interactions_typed';
|
import { unreblog, reblog } from './interactions_typed';
|
||||||
import { openModal } from './modal';
|
import { openModal } from './modal';
|
||||||
|
import { timelineExpandPinnedFromStatus } from './timelines_typed';
|
||||||
|
|
||||||
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
|
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
|
||||||
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
|
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
|
||||||
@ -368,6 +369,7 @@ export function pin(status) {
|
|||||||
api().post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
|
api().post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
|
||||||
dispatch(importFetchedStatus(response.data));
|
dispatch(importFetchedStatus(response.data));
|
||||||
dispatch(pinSuccess(status));
|
dispatch(pinSuccess(status));
|
||||||
|
dispatch(timelineExpandPinnedFromStatus(status));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(pinFail(status, error));
|
dispatch(pinFail(status, error));
|
||||||
});
|
});
|
||||||
@ -406,6 +408,7 @@ export function unpin (status) {
|
|||||||
api().post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
|
api().post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
|
||||||
dispatch(importFetchedStatus(response.data));
|
dispatch(importFetchedStatus(response.data));
|
||||||
dispatch(unpinSuccess(status));
|
dispatch(unpinSuccess(status));
|
||||||
|
dispatch(timelineExpandPinnedFromStatus(status));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(unpinFail(status, error));
|
dispatch(unpinFail(status, error));
|
||||||
});
|
});
|
||||||
|
|||||||
@ -57,4 +57,29 @@ describe('parseTimelineKey', () => {
|
|||||||
tagged: 'nature',
|
tagged: 'nature',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('parses legacy account timeline key with pinned correctly', () => {
|
||||||
|
const params = parseTimelineKey('account:789:pinned:nature');
|
||||||
|
expect(params).toEqual({
|
||||||
|
type: 'account',
|
||||||
|
userId: '789',
|
||||||
|
replies: false,
|
||||||
|
boosts: false,
|
||||||
|
media: false,
|
||||||
|
pinned: true,
|
||||||
|
tagged: 'nature',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses legacy account timeline key with media correctly', () => {
|
||||||
|
const params = parseTimelineKey('account:789:media');
|
||||||
|
expect(params).toEqual({
|
||||||
|
type: 'account',
|
||||||
|
userId: '789',
|
||||||
|
replies: false,
|
||||||
|
boosts: false,
|
||||||
|
media: true,
|
||||||
|
pinned: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
|
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
|
||||||
|
|
||||||
|
import type { Status } from '../models/status';
|
||||||
import { createAppThunk } from '../store/typed_functions';
|
import { createAppThunk } from '../store/typed_functions';
|
||||||
|
|
||||||
import { expandTimeline, TIMELINE_NON_STATUS_MARKERS } from './timelines';
|
import {
|
||||||
|
expandAccountFeaturedTimeline,
|
||||||
|
expandTimeline,
|
||||||
|
TIMELINE_NON_STATUS_MARKERS,
|
||||||
|
} from './timelines';
|
||||||
|
|
||||||
export const expandTimelineByKey = createAppThunk(
|
export const expandTimelineByKey = createAppThunk(
|
||||||
(args: { key: string; maxId?: number }, { dispatch }) => {
|
(args: { key: string; maxId?: number }, { dispatch }) => {
|
||||||
@ -119,8 +125,25 @@ export function parseTimelineKey(key: string): TimelineParams | null {
|
|||||||
type: 'account',
|
type: 'account',
|
||||||
userId,
|
userId,
|
||||||
tagged: segments[3],
|
tagged: segments[3],
|
||||||
|
pinned: false,
|
||||||
|
boosts: false,
|
||||||
|
replies: false,
|
||||||
|
media: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle legacy keys.
|
||||||
|
const flagsSegment = segments[2];
|
||||||
|
if (!flagsSegment || !/^[01]{4}$/.test(flagsSegment)) {
|
||||||
|
if (flagsSegment === 'pinned') {
|
||||||
|
parsed.pinned = true;
|
||||||
|
} else if (flagsSegment === 'with_replies') {
|
||||||
|
parsed.replies = true;
|
||||||
|
} else if (flagsSegment === 'media') {
|
||||||
|
parsed.media = true;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
const view = segments[2]?.split('') ?? [];
|
const view = segments[2]?.split('') ?? [];
|
||||||
for (let i = 0; i < view.length; i++) {
|
for (let i = 0; i < view.length; i++) {
|
||||||
const flagName = ACCOUNT_FILTERS[i];
|
const flagName = ACCOUNT_FILTERS[i];
|
||||||
@ -150,6 +173,11 @@ export function parseTimelineKey(key: string): TimelineParams | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isTimelineKeyPinned(key: string) {
|
||||||
|
const parsedKey = parseTimelineKey(key);
|
||||||
|
return parsedKey?.type === 'account' && parsedKey.pinned;
|
||||||
|
}
|
||||||
|
|
||||||
export function isNonStatusId(value: unknown) {
|
export function isNonStatusId(value: unknown) {
|
||||||
return TIMELINE_NON_STATUS_MARKERS.includes(value as string | null);
|
return TIMELINE_NON_STATUS_MARKERS.includes(value as string | null);
|
||||||
}
|
}
|
||||||
@ -170,3 +198,53 @@ export const timelineDelete = createAction<{
|
|||||||
references: string[];
|
references: string[];
|
||||||
reblogOf: string | null;
|
reblogOf: string | null;
|
||||||
}>('timelines/delete');
|
}>('timelines/delete');
|
||||||
|
|
||||||
|
export const timelineExpandPinnedFromStatus = createAppThunk(
|
||||||
|
(status: Status, { dispatch, getState }) => {
|
||||||
|
const accountId = status.getIn(['account', 'id']) as string;
|
||||||
|
if (!accountId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that any of the relevant timelines are actually expanded before dispatching, to avoid unnecessary API calls.
|
||||||
|
const timelines = getState().timelines as ImmutableMap<string, unknown>;
|
||||||
|
if (!timelines.some((_, key) => key.startsWith(`account:${accountId}:`))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispatch(
|
||||||
|
expandTimelineByParams({
|
||||||
|
type: 'account',
|
||||||
|
userId: accountId,
|
||||||
|
pinned: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
void dispatch(expandAccountFeaturedTimeline(accountId));
|
||||||
|
|
||||||
|
// Iterate over tags and clear those too.
|
||||||
|
const tags = status.get('tags') as
|
||||||
|
| ImmutableList<ImmutableMap<'name', string>> // We only care about the tag name.
|
||||||
|
| undefined;
|
||||||
|
if (!tags) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tags.forEach((tag) => {
|
||||||
|
const tagName = tag.get('name');
|
||||||
|
if (!tagName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispatch(
|
||||||
|
expandTimelineByParams({
|
||||||
|
type: 'account',
|
||||||
|
userId: accountId,
|
||||||
|
pinned: true,
|
||||||
|
tagged: tagName,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
void dispatch(
|
||||||
|
expandAccountFeaturedTimeline(accountId, { tagged: tagName }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||||
|
|
||||||
import { timelineDelete, isNonStatusId } from 'flavours/glitch/actions/timelines_typed';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
blockAccountSuccess,
|
blockAccountSuccess,
|
||||||
@ -21,6 +20,7 @@ import {
|
|||||||
TIMELINE_GAP,
|
TIMELINE_GAP,
|
||||||
disconnectTimeline,
|
disconnectTimeline,
|
||||||
} from '../actions/timelines';
|
} from '../actions/timelines';
|
||||||
|
import { timelineDelete, isTimelineKeyPinned, isNonStatusId } from '../actions/timelines_typed';
|
||||||
import { compareId } from '../compare_id';
|
import { compareId } from '../compare_id';
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
const initialState = ImmutableMap();
|
||||||
@ -50,7 +50,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
|
|||||||
|
|
||||||
if (!next && !isLoadingRecent) mMap.set('hasMore', false);
|
if (!next && !isLoadingRecent) mMap.set('hasMore', false);
|
||||||
|
|
||||||
if (timeline.endsWith(':pinned')) {
|
if (isTimelineKeyPinned(timeline)) {
|
||||||
mMap.set('items', statuses.map(status => status.get('id')));
|
mMap.set('items', statuses.map(status => status.get('id')));
|
||||||
} else if (!statuses.isEmpty()) {
|
} else if (!statuses.isEmpty()) {
|
||||||
usePendingItems = isLoadingRecent && (usePendingItems || !mMap.get('pendingItems').isEmpty());
|
usePendingItems = isLoadingRecent && (usePendingItems || !mMap.get('pendingItems').isEmpty());
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { fetchRelationships } from './accounts';
|
|||||||
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||||
import { unreblog, reblog } from './interactions_typed';
|
import { unreblog, reblog } from './interactions_typed';
|
||||||
import { openModal } from './modal';
|
import { openModal } from './modal';
|
||||||
|
import { timelineExpandPinnedFromStatus } from './timelines_typed';
|
||||||
|
|
||||||
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
|
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
|
||||||
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
|
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
|
||||||
@ -368,6 +369,7 @@ export function pin(status) {
|
|||||||
api().post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
|
api().post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
|
||||||
dispatch(importFetchedStatus(response.data));
|
dispatch(importFetchedStatus(response.data));
|
||||||
dispatch(pinSuccess(status));
|
dispatch(pinSuccess(status));
|
||||||
|
dispatch(timelineExpandPinnedFromStatus(status));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(pinFail(status, error));
|
dispatch(pinFail(status, error));
|
||||||
});
|
});
|
||||||
@ -406,6 +408,7 @@ export function unpin (status) {
|
|||||||
api().post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
|
api().post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
|
||||||
dispatch(importFetchedStatus(response.data));
|
dispatch(importFetchedStatus(response.data));
|
||||||
dispatch(unpinSuccess(status));
|
dispatch(unpinSuccess(status));
|
||||||
|
dispatch(timelineExpandPinnedFromStatus(status));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(unpinFail(status, error));
|
dispatch(unpinFail(status, error));
|
||||||
});
|
});
|
||||||
|
|||||||
@ -57,4 +57,29 @@ describe('parseTimelineKey', () => {
|
|||||||
tagged: 'nature',
|
tagged: 'nature',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('parses legacy account timeline key with pinned correctly', () => {
|
||||||
|
const params = parseTimelineKey('account:789:pinned:nature');
|
||||||
|
expect(params).toEqual({
|
||||||
|
type: 'account',
|
||||||
|
userId: '789',
|
||||||
|
replies: false,
|
||||||
|
boosts: false,
|
||||||
|
media: false,
|
||||||
|
pinned: true,
|
||||||
|
tagged: 'nature',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses legacy account timeline key with media correctly', () => {
|
||||||
|
const params = parseTimelineKey('account:789:media');
|
||||||
|
expect(params).toEqual({
|
||||||
|
type: 'account',
|
||||||
|
userId: '789',
|
||||||
|
replies: false,
|
||||||
|
boosts: false,
|
||||||
|
media: true,
|
||||||
|
pinned: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
|
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
|
||||||
|
|
||||||
|
import type { Status } from '../models/status';
|
||||||
import { createAppThunk } from '../store/typed_functions';
|
import { createAppThunk } from '../store/typed_functions';
|
||||||
|
|
||||||
import { expandTimeline, TIMELINE_NON_STATUS_MARKERS } from './timelines';
|
import {
|
||||||
|
expandAccountFeaturedTimeline,
|
||||||
|
expandTimeline,
|
||||||
|
TIMELINE_NON_STATUS_MARKERS,
|
||||||
|
} from './timelines';
|
||||||
|
|
||||||
export const expandTimelineByKey = createAppThunk(
|
export const expandTimelineByKey = createAppThunk(
|
||||||
(args: { key: string; maxId?: number }, { dispatch }) => {
|
(args: { key: string; maxId?: number }, { dispatch }) => {
|
||||||
@ -119,8 +125,25 @@ export function parseTimelineKey(key: string): TimelineParams | null {
|
|||||||
type: 'account',
|
type: 'account',
|
||||||
userId,
|
userId,
|
||||||
tagged: segments[3],
|
tagged: segments[3],
|
||||||
|
pinned: false,
|
||||||
|
boosts: false,
|
||||||
|
replies: false,
|
||||||
|
media: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle legacy keys.
|
||||||
|
const flagsSegment = segments[2];
|
||||||
|
if (!flagsSegment || !/^[01]{4}$/.test(flagsSegment)) {
|
||||||
|
if (flagsSegment === 'pinned') {
|
||||||
|
parsed.pinned = true;
|
||||||
|
} else if (flagsSegment === 'with_replies') {
|
||||||
|
parsed.replies = true;
|
||||||
|
} else if (flagsSegment === 'media') {
|
||||||
|
parsed.media = true;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
const view = segments[2]?.split('') ?? [];
|
const view = segments[2]?.split('') ?? [];
|
||||||
for (let i = 0; i < view.length; i++) {
|
for (let i = 0; i < view.length; i++) {
|
||||||
const flagName = ACCOUNT_FILTERS[i];
|
const flagName = ACCOUNT_FILTERS[i];
|
||||||
@ -150,6 +173,11 @@ export function parseTimelineKey(key: string): TimelineParams | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isTimelineKeyPinned(key: string) {
|
||||||
|
const parsedKey = parseTimelineKey(key);
|
||||||
|
return parsedKey?.type === 'account' && parsedKey.pinned;
|
||||||
|
}
|
||||||
|
|
||||||
export function isNonStatusId(value: unknown) {
|
export function isNonStatusId(value: unknown) {
|
||||||
return TIMELINE_NON_STATUS_MARKERS.includes(value as string | null);
|
return TIMELINE_NON_STATUS_MARKERS.includes(value as string | null);
|
||||||
}
|
}
|
||||||
@ -170,3 +198,53 @@ export const timelineDelete = createAction<{
|
|||||||
references: string[];
|
references: string[];
|
||||||
reblogOf: string | null;
|
reblogOf: string | null;
|
||||||
}>('timelines/delete');
|
}>('timelines/delete');
|
||||||
|
|
||||||
|
export const timelineExpandPinnedFromStatus = createAppThunk(
|
||||||
|
(status: Status, { dispatch, getState }) => {
|
||||||
|
const accountId = status.getIn(['account', 'id']) as string;
|
||||||
|
if (!accountId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that any of the relevant timelines are actually expanded before dispatching, to avoid unnecessary API calls.
|
||||||
|
const timelines = getState().timelines as ImmutableMap<string, unknown>;
|
||||||
|
if (!timelines.some((_, key) => key.startsWith(`account:${accountId}:`))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispatch(
|
||||||
|
expandTimelineByParams({
|
||||||
|
type: 'account',
|
||||||
|
userId: accountId,
|
||||||
|
pinned: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
void dispatch(expandAccountFeaturedTimeline(accountId));
|
||||||
|
|
||||||
|
// Iterate over tags and clear those too.
|
||||||
|
const tags = status.get('tags') as
|
||||||
|
| ImmutableList<ImmutableMap<'name', string>> // We only care about the tag name.
|
||||||
|
| undefined;
|
||||||
|
if (!tags) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tags.forEach((tag) => {
|
||||||
|
const tagName = tag.get('name');
|
||||||
|
if (!tagName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispatch(
|
||||||
|
expandTimelineByParams({
|
||||||
|
type: 'account',
|
||||||
|
userId: accountId,
|
||||||
|
pinned: true,
|
||||||
|
tagged: tagName,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
void dispatch(
|
||||||
|
expandAccountFeaturedTimeline(accountId, { tagged: tagName }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||||
|
|
||||||
import { timelineDelete, isNonStatusId } from 'mastodon/actions/timelines_typed';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
blockAccountSuccess,
|
blockAccountSuccess,
|
||||||
@ -21,6 +20,7 @@ import {
|
|||||||
TIMELINE_GAP,
|
TIMELINE_GAP,
|
||||||
disconnectTimeline,
|
disconnectTimeline,
|
||||||
} from '../actions/timelines';
|
} from '../actions/timelines';
|
||||||
|
import { timelineDelete, isTimelineKeyPinned, isNonStatusId } from '../actions/timelines_typed';
|
||||||
import { compareId } from '../compare_id';
|
import { compareId } from '../compare_id';
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
const initialState = ImmutableMap();
|
||||||
@ -50,7 +50,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
|
|||||||
|
|
||||||
if (!next && !isLoadingRecent) mMap.set('hasMore', false);
|
if (!next && !isLoadingRecent) mMap.set('hasMore', false);
|
||||||
|
|
||||||
if (timeline.endsWith(':pinned')) {
|
if (isTimelineKeyPinned(timeline)) {
|
||||||
mMap.set('items', statuses.map(status => status.get('id')));
|
mMap.set('items', statuses.map(status => status.get('id')));
|
||||||
} else if (!statuses.isEmpty()) {
|
} else if (!statuses.isEmpty()) {
|
||||||
usePendingItems = isLoadingRecent && (usePendingItems || !mMap.get('pendingItems').isEmpty());
|
usePendingItems = isLoadingRecent && (usePendingItems || !mMap.get('pendingItems').isEmpty());
|
||||||
|
|||||||
@ -8,33 +8,15 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService
|
|||||||
|
|
||||||
@account = account
|
@account = account
|
||||||
@options = options
|
@options = options
|
||||||
@json = fetch_collection(options[:collection].presence || @account.featured_collection_url)
|
@json = fetch_collection_page(options[:collection].presence || @account.featured_collection_url)
|
||||||
return if @json.blank?
|
return if @json.blank?
|
||||||
|
|
||||||
process_items(collection_items(@json))
|
@items, = collection_items(@json, max_pages: 1, reference_uri: @account.uri, on_behalf_of: local_follower)
|
||||||
|
process_items(@items)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def collection_items(collection)
|
|
||||||
collection = fetch_collection(collection['first']) if collection['first'].present?
|
|
||||||
return unless collection.is_a?(Hash)
|
|
||||||
|
|
||||||
case collection['type']
|
|
||||||
when 'Collection', 'CollectionPage'
|
|
||||||
as_array(collection['items'])
|
|
||||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
|
||||||
as_array(collection['orderedItems'])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_collection(collection_or_uri)
|
|
||||||
return collection_or_uri if collection_or_uri.is_a?(Hash)
|
|
||||||
return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
|
|
||||||
|
|
||||||
fetch_resource_without_id_validation(collection_or_uri, local_follower, raise_on_error: :temporary)
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_items(items)
|
def process_items(items)
|
||||||
return if items.nil?
|
return if items.nil?
|
||||||
|
|
||||||
|
|||||||
@ -11,43 +11,12 @@ class ActivityPub::FetchFeaturedTagsCollectionService < BaseService
|
|||||||
|
|
||||||
return unless supported_context?(@json)
|
return unless supported_context?(@json)
|
||||||
|
|
||||||
process_items(collection_items(@json))
|
@items, = collection_items(@json, max_items: FeaturedTag::LIMIT, max_pages: FeaturedTag::LIMIT, reference_uri: @account.uri, on_behalf_of: local_follower)
|
||||||
|
process_items(@items)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def collection_items(collection)
|
|
||||||
all_items = []
|
|
||||||
|
|
||||||
collection = fetch_collection(collection['first']) if collection['first'].present?
|
|
||||||
|
|
||||||
while collection.is_a?(Hash)
|
|
||||||
items = case collection['type']
|
|
||||||
when 'Collection', 'CollectionPage'
|
|
||||||
collection['items']
|
|
||||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
|
||||||
collection['orderedItems']
|
|
||||||
end
|
|
||||||
|
|
||||||
break if items.blank?
|
|
||||||
|
|
||||||
all_items.concat(items)
|
|
||||||
|
|
||||||
break if all_items.size >= FeaturedTag::LIMIT
|
|
||||||
|
|
||||||
collection = collection['next'].present? ? fetch_collection(collection['next']) : nil
|
|
||||||
end
|
|
||||||
|
|
||||||
all_items
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_collection(collection_or_uri)
|
|
||||||
return collection_or_uri if collection_or_uri.is_a?(Hash)
|
|
||||||
return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
|
|
||||||
|
|
||||||
fetch_resource_without_id_validation(collection_or_uri, local_follower, raise_on_error: :temporary)
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_items(items)
|
def process_items(items)
|
||||||
names = items.filter_map { |item| item['type'] == 'Hashtag' && item['name']&.delete_prefix('#') }.take(FeaturedTag::LIMIT)
|
names = items.filter_map { |item| item['type'] == 'Hashtag' && item['name']&.delete_prefix('#') }.take(FeaturedTag::LIMIT)
|
||||||
tags = names.index_by { |name| HashtagNormalizer.new.normalize(name) }
|
tags = names.index_by { |name| HashtagNormalizer.new.normalize(name) }
|
||||||
|
|||||||
@ -8,9 +8,13 @@ class ActivityPub::FetchRepliesService < BaseService
|
|||||||
|
|
||||||
def call(reference_uri, collection_or_uri, max_pages: 1, allow_synchronous_requests: true, batch_id: nil, request_id: nil)
|
def call(reference_uri, collection_or_uri, max_pages: 1, allow_synchronous_requests: true, batch_id: nil, request_id: nil)
|
||||||
@reference_uri = reference_uri
|
@reference_uri = reference_uri
|
||||||
@allow_synchronous_requests = allow_synchronous_requests
|
return if !allow_synchronous_requests && !collection_or_uri.is_a?(Hash)
|
||||||
|
|
||||||
@items, n_pages = collection_items(collection_or_uri, max_pages: max_pages)
|
# if given a prefetched collection while forbidding synchronous requests,
|
||||||
|
# process it and return without fetching additional pages
|
||||||
|
max_pages = 1 if !allow_synchronous_requests && collection_or_uri.is_a?(Hash)
|
||||||
|
|
||||||
|
@items, n_pages = collection_items(collection_or_uri, max_pages: max_pages, max_items: MAX_REPLIES, reference_uri: @reference_uri)
|
||||||
return if @items.nil?
|
return if @items.nil?
|
||||||
|
|
||||||
@items = filter_replies(@items)
|
@items = filter_replies(@items)
|
||||||
@ -26,45 +30,6 @@ class ActivityPub::FetchRepliesService < BaseService
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def collection_items(collection_or_uri, max_pages: 1)
|
|
||||||
collection = fetch_collection(collection_or_uri)
|
|
||||||
return unless collection.is_a?(Hash)
|
|
||||||
|
|
||||||
collection = fetch_collection(collection['first']) if collection['first'].present?
|
|
||||||
return unless collection.is_a?(Hash)
|
|
||||||
|
|
||||||
items = []
|
|
||||||
n_pages = 1
|
|
||||||
while collection.is_a?(Hash)
|
|
||||||
items.concat(as_array(collection_page_items(collection)))
|
|
||||||
|
|
||||||
break if items.size >= MAX_REPLIES
|
|
||||||
break if n_pages >= max_pages
|
|
||||||
|
|
||||||
collection = collection['next'].present? ? fetch_collection(collection['next']) : nil
|
|
||||||
n_pages += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
[items, n_pages]
|
|
||||||
end
|
|
||||||
|
|
||||||
def collection_page_items(collection)
|
|
||||||
case collection['type']
|
|
||||||
when 'Collection', 'CollectionPage'
|
|
||||||
collection['items']
|
|
||||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
|
||||||
collection['orderedItems']
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_collection(collection_or_uri)
|
|
||||||
return collection_or_uri if collection_or_uri.is_a?(Hash)
|
|
||||||
return unless @allow_synchronous_requests
|
|
||||||
return if non_matching_uri_hosts?(@reference_uri, collection_or_uri)
|
|
||||||
|
|
||||||
fetch_resource_without_id_validation(collection_or_uri, nil, raise_on_error: :temporary)
|
|
||||||
end
|
|
||||||
|
|
||||||
def filter_replies(items)
|
def filter_replies(items)
|
||||||
# Only fetch replies to the same server as the original status to avoid
|
# Only fetch replies to the same server as the original status to avoid
|
||||||
# amplification attacks.
|
# amplification attacks.
|
||||||
|
|||||||
@ -67,10 +67,10 @@ class ActivityPub::SynchronizeFollowersService < BaseService
|
|||||||
|
|
||||||
# Only returns true if the whole collection has been processed
|
# Only returns true if the whole collection has been processed
|
||||||
def process_collection!(collection_uri, max_pages: MAX_COLLECTION_PAGES)
|
def process_collection!(collection_uri, max_pages: MAX_COLLECTION_PAGES)
|
||||||
collection = fetch_collection(collection_uri)
|
collection = fetch_collection_page(collection_uri, reference_uri: @account.uri)
|
||||||
return false unless collection.is_a?(Hash)
|
return false unless collection.is_a?(Hash)
|
||||||
|
|
||||||
collection = fetch_collection(collection['first']) if collection['first'].present?
|
collection = fetch_collection_page(collection['first'], reference_uri: @account.uri) if collection['first'].present?
|
||||||
|
|
||||||
while collection.is_a?(Hash)
|
while collection.is_a?(Hash)
|
||||||
process_page!(as_array(collection_page_items(collection)))
|
process_page!(as_array(collection_page_items(collection)))
|
||||||
@ -80,25 +80,9 @@ class ActivityPub::SynchronizeFollowersService < BaseService
|
|||||||
return true if collection['next'].blank? # We reached the end of the collection
|
return true if collection['next'].blank? # We reached the end of the collection
|
||||||
return false if max_pages <= 0 # We reached our pages limit
|
return false if max_pages <= 0 # We reached our pages limit
|
||||||
|
|
||||||
collection = fetch_collection(collection['next'])
|
collection = fetch_collection_page(collection['next'])
|
||||||
end
|
end
|
||||||
|
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
def collection_page_items(collection)
|
|
||||||
case collection['type']
|
|
||||||
when 'Collection', 'CollectionPage'
|
|
||||||
collection['items']
|
|
||||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
|
||||||
collection['orderedItems']
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_collection(collection_or_uri)
|
|
||||||
return collection_or_uri if collection_or_uri.is_a?(Hash)
|
|
||||||
return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
|
|
||||||
|
|
||||||
fetch_resource_without_id_validation(collection_or_uri, nil, raise_on_error: :temporary)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@ -3,82 +3,44 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe ExistingUsernameValidator do
|
RSpec.describe ExistingUsernameValidator do
|
||||||
|
subject { record_class.new }
|
||||||
|
|
||||||
let(:record_class) do
|
let(:record_class) do
|
||||||
Class.new do
|
Class.new do
|
||||||
include ActiveModel::Validations
|
include ActiveModel::Validations
|
||||||
|
|
||||||
attr_accessor :contact, :friends
|
attr_accessor :contact, :friends
|
||||||
|
|
||||||
def self.name
|
def self.name = 'Record'
|
||||||
'Record'
|
|
||||||
end
|
|
||||||
|
|
||||||
validates :contact, existing_username: true
|
validates :contact, existing_username: true
|
||||||
validates :friends, existing_username: { multiple: true }
|
validates :friends, existing_username: { multiple: true }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
let(:record) { record_class.new }
|
|
||||||
|
|
||||||
describe '#validate_each' do
|
context 'with a nil value' do
|
||||||
context 'with a nil value' do
|
it { is_expected.to allow_value(nil).for(:contact) }
|
||||||
it 'does not add errors' do
|
end
|
||||||
record.contact = nil
|
|
||||||
|
|
||||||
expect(record).to be_valid
|
context 'when there are no accounts' do
|
||||||
expect(record.errors).to be_empty
|
it { is_expected.to_not allow_value('user@example.com').for(:contact).with_message(I18n.t('existing_username_validator.not_found')) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there are accounts' do
|
||||||
|
before { Fabricate(:account, domain: 'example.com', username: 'user') }
|
||||||
|
|
||||||
|
context 'when the value does not match' do
|
||||||
|
it { is_expected.to_not allow_value('friend@other.host').for(:contact).with_message(I18n.t('existing_username_validator.not_found')) }
|
||||||
|
|
||||||
|
context 'when multiple is true' do
|
||||||
|
it { is_expected.to_not allow_value('friend@other.host').for(:friends).with_message(I18n.t('existing_username_validator.not_found_multiple', usernames: 'friend@other.host')) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when there are no accounts' do
|
context 'when the value does match' do
|
||||||
it 'adds errors to the record' do
|
it { is_expected.to allow_value('user@example.com').for(:contact) }
|
||||||
record.contact = 'user@example.com'
|
|
||||||
|
|
||||||
expect(record).to_not be_valid
|
it { is_expected.to allow_value('user@example.com').for(:friends) }
|
||||||
expect(record.errors.first.attribute).to eq(:contact)
|
|
||||||
expect(record.errors.first.type).to eq I18n.t('existing_username_validator.not_found')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when there are accounts' do
|
|
||||||
before { Fabricate(:account, domain: 'example.com', username: 'user') }
|
|
||||||
|
|
||||||
context 'when the value does not match' do
|
|
||||||
it 'adds errors to the record' do
|
|
||||||
record.contact = 'friend@other.host'
|
|
||||||
|
|
||||||
expect(record).to_not be_valid
|
|
||||||
expect(record.errors.first.attribute).to eq(:contact)
|
|
||||||
expect(record.errors.first.type).to eq I18n.t('existing_username_validator.not_found')
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when multiple is true' do
|
|
||||||
it 'adds errors to the record' do
|
|
||||||
record.friends = 'friend@other.host'
|
|
||||||
|
|
||||||
expect(record).to_not be_valid
|
|
||||||
expect(record.errors.first.attribute).to eq(:friends)
|
|
||||||
expect(record.errors.first.type).to eq I18n.t('existing_username_validator.not_found_multiple', usernames: 'friend@other.host')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the value does match' do
|
|
||||||
it 'does not add errors to the record' do
|
|
||||||
record.contact = 'user@example.com'
|
|
||||||
|
|
||||||
expect(record).to be_valid
|
|
||||||
expect(record.errors).to be_empty
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when multiple is true' do
|
|
||||||
it 'does not add errors to the record' do
|
|
||||||
record.friends = 'user@example.com'
|
|
||||||
|
|
||||||
expect(record).to be_valid
|
|
||||||
expect(record.errors).to be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user