mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
Compare commits
11 Commits
6c2d53372b
...
v4.5.0-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca0c5e7a79 | ||
|
|
10a81a2f43 | ||
|
|
9f8fce3c47 | ||
|
|
5f5e6ca031 | ||
|
|
af4c372ab2 | ||
|
|
aa579ce286 | ||
|
|
adfabf8c80 | ||
|
|
ea710df180 | ||
|
|
e1b6e28829 | ||
|
|
214d59bd37 | ||
|
|
e4291e9b05 |
@@ -73,7 +73,7 @@ services:
|
||||
hard: -1
|
||||
|
||||
libretranslate:
|
||||
image: libretranslate/libretranslate:v1.7.3
|
||||
image: libretranslate/libretranslate:v1.6.2
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- lt-data:/home/libretranslate/.local
|
||||
|
||||
2
.github/actions/setup-javascript/action.yml
vendored
2
.github/actions/setup-javascript/action.yml
vendored
@@ -9,7 +9,7 @@ runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
|
||||
3
.github/renovate.json5
vendored
3
.github/renovate.json5
vendored
@@ -5,6 +5,7 @@
|
||||
'customManagers:dockerfileVersions',
|
||||
':labels(dependencies)',
|
||||
':prConcurrentLimitNone', // Remove limit for open PRs at any time.
|
||||
':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour.
|
||||
':enableVulnerabilityAlertsWithLabel(security)',
|
||||
],
|
||||
rebaseWhen: 'conflicted',
|
||||
@@ -22,6 +23,8 @@
|
||||
// Require Dependency Dashboard Approval for major version bumps of these node packages
|
||||
matchManagers: ['npm'],
|
||||
matchPackageNames: [
|
||||
'tesseract.js', // Requires code changes
|
||||
|
||||
// react-router: Requires manual upgrade
|
||||
'history',
|
||||
'react-router-dom',
|
||||
|
||||
8
.github/workflows/build-container-image.yml
vendored
8
.github/workflows/build-container-image.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
- linux/arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare
|
||||
env:
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
|
||||
- name: Upload digest
|
||||
if: ${{ inputs.push_to_images != '' }}
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
# `hashFiles` is used to disambiguate between streaming and non-streaming images
|
||||
name: digests-${{ hashFiles(inputs.file_to_build) }}-${{ env.PLATFORM_PAIR }}
|
||||
@@ -119,10 +119,10 @@ jobs:
|
||||
PUSH_TO_IMAGES: ${{ inputs.push_to_images }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
# `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:
|
||||
# Repository needs to be cloned so `git rev-parse` below works
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
- id: version_vars
|
||||
run: |
|
||||
echo mastodon_version_metadata=pr-${{ github.event.pull_request.number }}-$(git rev-parse --short ${{github.event.pull_request.head.sha}}) >> $GITHUB_OUTPUT
|
||||
|
||||
42
.github/workflows/build-releases.yml
vendored
42
.github/workflows/build-releases.yml
vendored
@@ -9,44 +9,7 @@ permissions:
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
check-latest-stable:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
latest: ${{ steps.check.outputs.is_latest_stable }}
|
||||
steps:
|
||||
# Repository needs to be cloned to list branches
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check latest stable
|
||||
shell: bash
|
||||
id: check
|
||||
run: |
|
||||
ref="${GITHUB_REF#refs/tags/}"
|
||||
|
||||
if [[ "$ref" =~ ^v([0-9]+)\.([0-9]+)(\.[0-9]+)?$ ]]; then
|
||||
current="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
|
||||
else
|
||||
echo "tag $ref is not semver"
|
||||
echo "is_latest_stable=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
latest=$(git for-each-ref --format='%(refname:short)' "refs/remotes/origin/stable-*.*" \
|
||||
| sed -E 's#^origin/stable-##' \
|
||||
| sort -Vr \
|
||||
| head -n1)
|
||||
|
||||
if [[ "$current" == "$latest" ]]; then
|
||||
echo "is_latest_stable=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_latest_stable=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
build-image:
|
||||
needs: check-latest-stable
|
||||
uses: ./.github/workflows/build-container-image.yml
|
||||
with:
|
||||
file_to_build: Dockerfile
|
||||
@@ -57,14 +20,13 @@ jobs:
|
||||
# Only tag with latest when ran against the latest stable branch
|
||||
# This needs to be updated after each minor version release
|
||||
flavor: |
|
||||
latest=${{ needs.check-latest-stable.outputs.latest }}
|
||||
latest=${{ startsWith(github.ref, 'refs/tags/v4.3.') }}
|
||||
tags: |
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
secrets: inherit
|
||||
|
||||
build-image-streaming:
|
||||
needs: check-latest-stable
|
||||
uses: ./.github/workflows/build-container-image.yml
|
||||
with:
|
||||
file_to_build: streaming/Dockerfile
|
||||
@@ -75,7 +37,7 @@ jobs:
|
||||
# Only tag with latest when ran against the latest stable branch
|
||||
# This needs to be updated after each minor version release
|
||||
flavor: |
|
||||
latest=${{ needs.check-latest-stable.outputs.latest }}
|
||||
latest=${{ startsWith(github.ref, 'refs/tags/v4.3.') }}
|
||||
tags: |
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
|
||||
2
.github/workflows/bundler-audit.yml
vendored
2
.github/workflows/bundler-audit.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
|
||||
2
.github/workflows/check-i18n.yml
vendored
2
.github/workflows/check-i18n.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Ruby environment
|
||||
uses: ./.github/actions/setup-ruby
|
||||
|
||||
49
.github/workflows/chromatic.yml
vendored
49
.github/workflows/chromatic.yml
vendored
@@ -1,51 +1,31 @@
|
||||
name: 'Chromatic'
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- renovate/*
|
||||
- stable-*
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- '**/*.js'
|
||||
- '**/*.jsx'
|
||||
- '**/*.ts'
|
||||
- '**/*.tsx'
|
||||
- '**/*.css'
|
||||
- '**/*.scss'
|
||||
- '.github/workflows/chromatic.yml'
|
||||
|
||||
jobs:
|
||||
pathcheck:
|
||||
name: Check for relevant changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
changed: ${{ steps.filter.outputs.src }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
src:
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- '**/*.js'
|
||||
- '**/*.jsx'
|
||||
- '**/*.ts'
|
||||
- '**/*.tsx'
|
||||
- '**/*.css'
|
||||
- '**/*.scss'
|
||||
- '.github/workflows/chromatic.yml'
|
||||
|
||||
chromatic:
|
||||
name: Run Chromatic
|
||||
runs-on: ubuntu-latest
|
||||
needs: pathcheck
|
||||
if: github.repository == 'mastodon/mastodon' && needs.pathcheck.outputs.changed == 'true'
|
||||
if: github.repository == 'mastodon/mastodon'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Javascript environment
|
||||
uses: ./.github/actions/setup-javascript
|
||||
|
||||
@@ -53,10 +33,9 @@ jobs:
|
||||
run: yarn build-storybook
|
||||
|
||||
- name: Run Chromatic
|
||||
uses: chromaui/action@v13
|
||||
uses: chromaui/action@v12
|
||||
with:
|
||||
# ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret
|
||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
zip: true
|
||||
storybookBuildDir: 'storybook-static'
|
||||
exitZeroOnChanges: false # Fail workflow if changes are found
|
||||
autoAcceptChanges: 'main' # Auto-accept changes on main branch only
|
||||
|
||||
8
.github/workflows/codeql.yml
vendored
8
.github/workflows/codeql.yml
vendored
@@ -31,11 +31,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -61,6 +61,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Increase Git http.postBuffer
|
||||
# This is needed due to a bug in Ubuntu's cURL version?
|
||||
|
||||
2
.github/workflows/crowdin-download.yml
vendored
2
.github/workflows/crowdin-download.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Increase Git http.postBuffer
|
||||
# This is needed due to a bug in Ubuntu's cURL version?
|
||||
|
||||
2
.github/workflows/crowdin-upload.yml
vendored
2
.github/workflows/crowdin-upload.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@v2
|
||||
|
||||
2
.github/workflows/format-check.yml
vendored
2
.github/workflows/format-check.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Javascript environment
|
||||
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:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Javascript environment
|
||||
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:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
|
||||
2
.github/workflows/lint-js.yml
vendored
2
.github/workflows/lint-js.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Javascript environment
|
||||
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:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
|
||||
2
.github/workflows/test-js.yml
vendored
2
.github/workflows/test-js.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Javascript environment
|
||||
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
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Ruby environment
|
||||
uses: ./.github/actions/setup-ruby
|
||||
|
||||
28
.github/workflows/test-ruby.yml
vendored
28
.github/workflows/test-ruby.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
SECRET_KEY_BASE_DUMMY: 1
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Ruby environment
|
||||
uses: ./.github/actions/setup-ruby
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
run: |
|
||||
tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* tmp/cache/vite/last-build*.json
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: matrix.mode == 'test'
|
||||
with:
|
||||
path: |-
|
||||
@@ -128,9 +128,9 @@ jobs:
|
||||
- '3.3'
|
||||
- '.ruby-version'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: './'
|
||||
name: ${{ github.sha }}
|
||||
@@ -230,9 +230,9 @@ jobs:
|
||||
- '3.3'
|
||||
- '.ruby-version'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: './'
|
||||
name: ${{ github.sha }}
|
||||
@@ -309,9 +309,9 @@ jobs:
|
||||
- '.ruby-version'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: './'
|
||||
name: ${{ github.sha }}
|
||||
@@ -350,14 +350,14 @@ jobs:
|
||||
- run: bin/rspec spec/system --tag streaming --tag js
|
||||
|
||||
- name: Archive logs
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: e2e-logs-${{ matrix.ruby-version }}
|
||||
path: log/
|
||||
|
||||
- name: Archive test screenshots
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: e2e-screenshots-${{ matrix.ruby-version }}
|
||||
@@ -447,9 +447,9 @@ jobs:
|
||||
search-image: opensearchproject/opensearch:2
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v6
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: './'
|
||||
name: ${{ github.sha }}
|
||||
@@ -469,14 +469,14 @@ jobs:
|
||||
- run: bin/rspec --tag search
|
||||
|
||||
- name: Archive logs
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: test-search-logs-${{ matrix.ruby-version }}
|
||||
path: log/
|
||||
|
||||
- name: Archive test screenshots
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: test-search-screenshots
|
||||
|
||||
@@ -95,7 +95,6 @@ AUTHORS.md
|
||||
|
||||
# Ignore glitch-soc vendored CSS reset
|
||||
app/javascript/flavours/glitch/styles/reset.scss
|
||||
app/javascript/flavours/glitch/styles_new/mastodon/reset.scss
|
||||
|
||||
# Ignore win95 theme
|
||||
app/javascript/styles/win95.scss
|
||||
@@ -31,7 +31,7 @@ const config: StorybookConfig = {
|
||||
viteFinal(config) {
|
||||
// For an unknown reason, Storybook does not use the root
|
||||
// from the Vite config so we need to set it manually.
|
||||
config.root = resolve(import.meta.dirname, '../app/javascript');
|
||||
config.root = resolve(__dirname, '../app/javascript');
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
<html class="no-reduce-motion theme-light">
|
||||
<html class="no-reduce-motion">
|
||||
</html>
|
||||
@@ -11,11 +11,6 @@ import type { Preview } from '@storybook/react-vite';
|
||||
import { initialize, mswLoader } from 'msw-storybook-addon';
|
||||
import { action } from 'storybook/actions';
|
||||
|
||||
import {
|
||||
importCustomEmojiData,
|
||||
importLegacyShortcodes,
|
||||
importEmojiData,
|
||||
} from '@/mastodon/features/emoji/loader';
|
||||
import type { LocaleData } from '@/mastodon/locales';
|
||||
import { reducerWithInitialState } from '@/mastodon/reducers';
|
||||
import { defaultMiddleware } from '@/mastodon/store/store';
|
||||
@@ -55,31 +50,12 @@ const preview: Preview = {
|
||||
locale: 'en',
|
||||
},
|
||||
decorators: [
|
||||
(Story, { parameters, globals, args, argTypes }) => {
|
||||
(Story, { parameters, globals, args }) => {
|
||||
// Get the locale from the global toolbar
|
||||
// and merge it with any parameters or args state.
|
||||
const { locale } = globals as { locale: string };
|
||||
const { state = {} } = parameters;
|
||||
|
||||
const argsState: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(args)) {
|
||||
const argType = argTypes[key];
|
||||
if (argType?.reduxPath) {
|
||||
const reduxPath = Array.isArray(argType.reduxPath)
|
||||
? argType.reduxPath.map((p) => p.toString())
|
||||
: argType.reduxPath.split('.');
|
||||
|
||||
reduxPath.reduce((acc, key, i) => {
|
||||
if (acc[key] === undefined) {
|
||||
acc[key] = {};
|
||||
}
|
||||
if (i === reduxPath.length - 1) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc[key] as Record<string, unknown>;
|
||||
}, argsState);
|
||||
}
|
||||
}
|
||||
const { state: argsState = {} } = args;
|
||||
|
||||
const reducer = reducerWithInitialState(
|
||||
{
|
||||
@@ -88,7 +64,7 @@ const preview: Preview = {
|
||||
},
|
||||
},
|
||||
state as Record<string, unknown>,
|
||||
argsState,
|
||||
argsState as Record<string, unknown>,
|
||||
);
|
||||
|
||||
const store = configureStore({
|
||||
@@ -151,12 +127,7 @@ const preview: Preview = {
|
||||
</MemoryRouter>
|
||||
),
|
||||
],
|
||||
loaders: [
|
||||
mswLoader,
|
||||
importCustomEmojiData,
|
||||
importLegacyShortcodes,
|
||||
({ globals: { locale } }) => importEmojiData(locale as string),
|
||||
],
|
||||
loaders: [mswLoader],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* - Please do NOT modify this file.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '2.12.1'
|
||||
const PACKAGE_VERSION = '2.11.3'
|
||||
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
@@ -205,7 +205,6 @@ async function resolveMainClient(event) {
|
||||
* @param {FetchEvent} event
|
||||
* @param {Client | undefined} client
|
||||
* @param {string} requestId
|
||||
* @param {number} requestInterceptedAt
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async function getResponse(event, client, requestId, requestInterceptedAt) {
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
// The addon package.json incorrectly exports types, so we need to override them here.
|
||||
|
||||
import type { RootState } from '@/mastodon/store';
|
||||
|
||||
// See: https://github.com/storybookjs/storybook/blob/v9.0.4/code/addons/vitest/package.json#L70-L76
|
||||
declare module '@storybook/addon-vitest/vitest-plugin' {
|
||||
export * from '@storybook/addon-vitest/dist/vitest-plugin/index';
|
||||
}
|
||||
|
||||
type RootPathKeys = keyof RootState;
|
||||
|
||||
declare module 'storybook/internal/csf' {
|
||||
export interface InputType {
|
||||
reduxPath?:
|
||||
| `${RootPathKeys}.${string}`
|
||||
| [RootPathKeys, ...(string | number)[]];
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
13
.yarn/patches/babel-plugin-lodash-npm-3.3.4-c7161075b6.patch
Normal file
13
.yarn/patches/babel-plugin-lodash-npm-3.3.4-c7161075b6.patch
Normal file
@@ -0,0 +1,13 @@
|
||||
diff --git a/lib/index.js b/lib/index.js
|
||||
index 16ed6be8be8f555cc99096c2ff60954b42dc313d..d009c069770d066ad0db7ad02de1ea473a29334e 100644
|
||||
--- a/lib/index.js
|
||||
+++ b/lib/index.js
|
||||
@@ -99,7 +99,7 @@ function lodash(_ref) {
|
||||
|
||||
var node = _ref3;
|
||||
|
||||
- if ((0, _types.isModuleDeclaration)(node)) {
|
||||
+ if ((0, _types.isImportDeclaration)(node) || (0, _types.isExportDeclaration)(node)) {
|
||||
isModule = true;
|
||||
break;
|
||||
}
|
||||
@@ -538,7 +538,7 @@ and provided thanks to the work of the following contributors:
|
||||
* [Drew Schuster](mailto:dtschust@gmail.com)
|
||||
* [Dryusdan](mailto:dryusdan@dryusdan.fr)
|
||||
* [Eai](mailto:eai@mizle.net)
|
||||
* [Eashwar Ranganathan](mailto:eashwar@eashwar.com)
|
||||
* [Eashwar Ranganathan](mailto:eranganathan@lyft.com)
|
||||
* [Ed Knutson](mailto:knutsoned@gmail.com)
|
||||
* [Elizabeth Martín Campos](mailto:me@elizabeth.sh)
|
||||
* [Elizabeth Myers](mailto:elizabeth@interlinked.me)
|
||||
|
||||
77
CHANGELOG.md
77
CHANGELOG.md
@@ -2,74 +2,17 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [4.5.3] - 2025-12-08
|
||||
|
||||
### Security
|
||||
|
||||
- Fix inconsistent error handling leaking information on existence of private posts ([GHSA-gwhw-gcjx-72v8](https://github.com/mastodon/mastodon/security/advisories/GHSA-gwhw-gcjx-72v8))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix “Delete and Redraft” on a non-quote being treated as a quote post in some cases (#37140 by @ClearlyClaire)
|
||||
- Fix YouTube embeds by sending referer (#37126 by @ChaosExAnima)
|
||||
- Fix streamed quoted polls not being hydrated correctly (#37118 by @ClearlyClaire)
|
||||
- Fix creation of duplicate conversations (#37108 by @oneiros)
|
||||
- Fix extraneous `noreferrer` in external links (#37107 by @ChaosExAnima)
|
||||
- Fix edge case error handling in some database migrations (#37079 by @ClearlyClaire)
|
||||
- Fix error handling when re-fetching already-known statuses (#37077 by @ClearlyClaire)
|
||||
- Fix post navigation in single-column mode when Advanced UI is enabled (#37044 by @diondiondion)
|
||||
- Fix `tootctl status remove` removing quoted posts and remote quotes of local posts (#37009 by @ClearlyClaire)
|
||||
- Fix known expensive S3 batch delete operation failing because of short timeouts (#37004 by @ClearlyClaire)
|
||||
- Fix compose autosuggest always lowercasing input token (#36995 by @ClearlyClaire)
|
||||
|
||||
## [4.5.2] - 2025-11-20
|
||||
|
||||
### Changed
|
||||
|
||||
- Change private quote education modal to not show up on self-quotes (#36926 by @ClearlyClaire)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix missing fallback link in CW-only quote posts (#36963 by @ClearlyClaire)
|
||||
- Fix statuses without text being hidden while loading (#36962 by @ClearlyClaire)
|
||||
- Fix `g` + `h` keyboard shortcut not working when a post is focused (#36935 by @diondiondion)
|
||||
- Fix quoting overwriting current content warning (#36934 by @ClearlyClaire)
|
||||
- Fix scroll-to-status in threaded view being unreliable (#36927 by @ClearlyClaire)
|
||||
- Fix path resolution for emoji worker (#36897 by @ChaosExAnima)
|
||||
- Fix `tootctl upgrade storage-schema` failing with `ArgumentError` (#36914 by @shugo)
|
||||
- Fix cross-origin handling of CSS modules (#36890 by @ClearlyClaire)
|
||||
- Fix error with remote tags including percent signs (#36886 and #36925 by @ChaosExAnima and @ClearlyClaire)
|
||||
- Fix bogus quote approval policy not always being replaced correctly (#36885 by @ClearlyClaire)
|
||||
- Fix hashtag completion not being inserted correctly (#36884 by @ClearlyClaire)
|
||||
- Fix Cmd/Ctrl + Enter in the composer triggering confirmation dialog action (#36870 by @diondiondion)
|
||||
|
||||
## [4.5.1] - 2025-11-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Cmd/Ctrl + Enter not submitting Alt text modal on some browsers (#36866 by @diondiondion)
|
||||
- Fix posts coming from public/hashtag streaming being marked as unquotable (#36860 and #36869 by @ClearlyClaire)
|
||||
- Fix old previously-undiscovered posts being treated as new when receiving an `Update` (#36848 by @ClearlyClaire)
|
||||
- Fix blank screen in browsers that don't support `Intl.DisplayNames` (#36847 by @diondiondion)
|
||||
- Fix filters not being applied to quotes in detailed view (#36843 by @ClearlyClaire)
|
||||
- Fix scroll shift caused by fetch-all-replies alerts (#36807 by @diondiondion)
|
||||
- Fix dropdown menu not focusing first item when opened via keyboard (#36804 by @diondiondion)
|
||||
- Fix assets build issue on arch64 (#36781 by @ClearlyClaire)
|
||||
- Fix `/api/v1/statuses/:id/context` sometimes returing `Mastodon-Async-Refresh` without `result_count` (#36779 by @ClearlyClaire)
|
||||
- Fix prepared quote not being discarded with contents when replying (#36778 by @ClearlyClaire)
|
||||
|
||||
## [4.5.0] - 2025-11-06
|
||||
## [4.5.0] - UNRELEASED
|
||||
|
||||
### Added
|
||||
|
||||
- **Add support for allowing and authoring quotes** (#35355, #35578, #35614, #35618, #35624, #35626, #35652, #35629, #35665, #35653, #35670, #35677, #35690, #35697, #35689, #35699, #35700, #35701, #35709, #35714, #35713, #35715, #35725, #35749, #35769, #35780, #35762, #35804, #35808, #35805, #35819, #35824, #35828, #35822, #35835, #35865, #35860, #35832, #35891, #35894, #35895, #35820, #35917, #35924, #35925, #35914, #35930, #35941, #35939, #35948, #35955, #35967, #35990, #35991, #35975, #35971, #36002, #35986, #36031, #36034, #36038, #36054, #36052, #36055, #36065, #36068, #36083, #36087, #36080, #36091, #36090, #36118, #36119, #36128, #36094, #36129, #36138, #36132, #36151, #36158, #36171, #36194, #36220, #36169, #36130, #36249, #36153, #36299, #36291, #36301, #36315, #36317, #36364, #36383, #36381, #36459, #36464, #36461, #36516, #36528, #36549, #36550, #36559, #36693, #36704, #36690, #36689, #36696, #36721, #36695 and #36736 by @ChaosExAnima, @ClearlyClaire, @Lycolia, @diondiondion, and @tribela)\
|
||||
- **Add support for allowing and authoring quotes** (#35355, #35578, #35614, #35618, #35624, #35626, #35652, #35629, #35665, #35653, #35670, #35677, #35690, #35697, #35689, #35699, #35700, #35701, #35709, #35714, #35713, #35715, #35725, #35749, #35769, #35780, #35762, #35804, #35808, #35805, #35819, #35824, #35828, #35822, #35835, #35865, #35860, #35832, #35891, #35894, #35895, #35820, #35917, #35924, #35925, #35914, #35930, #35941, #35939, #35948, #35955, #35967, #35990, #35991, #35975, #35971, #36002, #35986, #36031, #36034, #36038, #36054, #36052, #36055, #36065, #36068, #36083, #36087, #36080, #36091, #36090, #36118, #36119, #36128, #36094, #36129, #36138, #36132, #36151, #36158, #36171, #36194, #36220, #36169, #36130, #36249, #36153, #36299, #36291, #36301, #36315, #36317, #36364, #36383, #36381, #36459, #36464, #36461, #36516, #36528, #36549, #36550 and #36559 by @ChaosExAnima, @ClearlyClaire, @Lycolia, @diondiondion, and @tribela)\
|
||||
This includes a revamp of the composer interface.\
|
||||
See https://blog.joinmastodon.org/2025/09/introducing-quote-posts/ for a user-centric overview of the feature, and https://docs.joinmastodon.org/client/quotes/ for API documentation.
|
||||
- **Add support for fetching and refreshing replies to the web UI** (#35210, #35496, #35575, #35500, #35577, #35602, #35603, #35654, #36141, #36237, #36172, #36256, #36271, #36334, #36382, #36239, #36484, #36481, #36583, #36627 and #36547 by @ClearlyClaire, @diondiondion, @Gargron and @renchap)
|
||||
- **Add ability to block words in usernames** (#35407, #35655, and #35806 by @ClearlyClaire and @Gargron)
|
||||
- Add ability to individually disable local or remote feeds for visitors or logged-in users `disabled` value to server setting for live and topic feeds, as well as user permission to bypass that (#36338, #36467, #36497, #36563, #36577, #36585, #36607 and #36703 by @ClearlyClaire)\
|
||||
This splits the `timeline_preview` setting into four more granular settings controlling live feeds and topic (hashtag, trending link) feeds.\
|
||||
The setting for local topic feeds has 2 values: `public` and `authenticated`. Every other setting has 3 values: `public`, `authenticated`, `disabled`.\
|
||||
- Add ability to individually disable local or remote feeds for visitors or logged-in users `disabled` value to server setting for live and topic feeds, as well as user permission to bypass that (#36338, #36467, #36497, #36563, #36577, #36585, and #36607 by @ClearlyClaire)\
|
||||
This splits the `timeline_preview` setting into four more granular settings controlling live feeds and topic (hashtag, trending link) feeds, with 3 values each: `public`, `authenticated`, `disabled`.\
|
||||
When `disabled`, users with the “View live and topic feeds” will still be able to view them.
|
||||
- Add support for displaying of quote posts in Moderator UI (#35964 by @ThisIsMissEm)
|
||||
- Add support for displaying link previews for Admin UI (#35958 by @ThisIsMissEm)
|
||||
@@ -77,22 +20,21 @@ All notable changes to this project will be documented in this file.
|
||||
- Add support for `Update` activities on converted object types (#36322 by @ClearlyClaire)
|
||||
- Add support for dynamic viewport height (#36272 by @e1berd)
|
||||
- Add support for numeric-based URIs for new local accounts (#32724, #36304, #36316, and #36365 by @ClearlyClaire)
|
||||
- Add default visualizer for audio upload without poster (#36734 by @ChaosExAnima)
|
||||
- Add Traditional Mongolian to posting languages (#36196 by @shimon1024)
|
||||
- Add example post with manual quote approval policy to `dev:populate_sample_data` (#36099 by @ClearlyClaire)
|
||||
- Add server-side support for handling posts with a quote policy allowing followers to quote (#36093 and #36127 by @ClearlyClaire)
|
||||
- Add schema.org markup to SEO-enabled posts (#36075 by @Gargron)
|
||||
- Add migration to fill unset default quote policy based on default post privacy (#36041 by @ClearlyClaire)
|
||||
- Add “Posting defaults” setting page, moving existing settings from “Other” (#35896, #36033, #35966, #35969, and #36084 by @ClearlyClaire and @diondiondion)
|
||||
- Added emoji from Twemoji v16 (#36501 and #36530 by @ChaosExAnima)
|
||||
- Add feature to select custom emoji rendering (#35229, #35282, #35253, #35424, #35473, #35483, #35505, #35568, #35605, #35659, #35664, #35739, #35985, #36051, #36071, #36137, #36165, #36248, #36262, #36275, #36293, #36341, #36342, #36366, #36377, #36378, #36385, #36393, #36397, #36403, #36413, #36410, #36454, #36402, #36503, #36502, #36532, #36603, #36409, #36638 and #36750 by @ChaosExAnima, @ClearlyClaire and @braddunbar)\
|
||||
This also completely reworks the processing and rendering of emojis and server-rendered HTML in statuses and other places.
|
||||
- Add support for exposing conversation context for new public conversations according to FEP-7888 (#35959 and #36064 by @ClearlyClaire and @jesseplusplus)
|
||||
- Add digest re-check before removing followers in synchronization mechanism (#34273 by @ClearlyClaire)
|
||||
- Add “Posting defaults” setting page, moving existing settings from “Other” (#35896, #36033, #35966, #35969, and #36084 by @ClearlyClaire and @diondiondion)
|
||||
- Add support for displaying Valkey version on admin dashboard (#35785 by @ykzts)
|
||||
- Add delivery failure tracking and handling to FASP jobs (#35625, #35628, and #35723 by @oneiros)
|
||||
- Add example of quote post with a preview card to development sample data (#35616 by @ClearlyClaire)
|
||||
- Add second set of blocked text that applies to accounts regardless of account age for spam-blocking (#35563 by @ClearlyClaire)
|
||||
- Added emoji from Twemoji v16 (#36501 and #36530 by @ChaosExAnima)
|
||||
- Add feature to select custom emoji rendering (#35229, #35282, #35253, #35424, #35473, #35483, #35505, #35568, #35605, #35659, #35664, #35739, #35985, #36051, #36071, #36137, #36165, #36248, #36262, #36275, #36293, #36341, #36342, #36366, #36377, #36378, #36385, #36393, #36397, #36403, #36413, #36410, #36454, #36402, #36503, #36502, #36532, #36603, #36409 and #36638 by @ChaosExAnima, @ClearlyClaire and @braddunbar)\
|
||||
This also completely reworks the processing and rendering of emojis and server-rendered HTML in statuses and other places.
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -102,8 +44,6 @@ All notable changes to this project will be documented in this file.
|
||||
- Change display of blocked and muted quoted users (#36619 by @ClearlyClaire)\
|
||||
This adds `blocked_account`, `blocked_domain` and `muted_account` values to the `state` attribute of `Quote` and `ShallowQuote` REST API entities.
|
||||
- Change submitting an empty post to show an error rather than failing silently (#36650 by @diondiondion)
|
||||
- Change "Privacy and reach" settings from "Public profile" to their own top-level category (#27294 by @ChaelCodes)
|
||||
- Change number of times quote verification is retried to better deal with temporary failures (#36698 by @ClearlyClaire)
|
||||
- Change display of content warnings in Admin UI (#35935 by @ThisIsMissEm)
|
||||
- Change styling of column banners (#36531 by @ClearlyClaire)
|
||||
- Change recommended Node version to 24 (LTS) (#36539 by @renchap)
|
||||
@@ -135,7 +75,6 @@ All notable changes to this project will be documented in this file.
|
||||
- Fix URL comparison for mentions in case of empty path (#36613 and #36626 by @ClearlyClaire)
|
||||
- Fix hashtags not being picked up when full-width hash sign is used (#36103 and #36625 by @ClearlyClaire and @Gargron)
|
||||
- Fix layout of severed relationships when purged events are listed (#36593 by @mejofi)
|
||||
- Fix Skeleton placeholders being animated when setting to reduce animations is enabled (#36716 by @ClearlyClaire)
|
||||
- Fix vacuum tasks being interrupted by a single batch failure (#36606 by @Gargron)
|
||||
- Fix handling of unreachable network error for search services (#36587 by @mjankowski)
|
||||
- Fix bookmarks export when a bookmarked status is soft-deleted (#36576 by @ClearlyClaire)
|
||||
|
||||
12
Gemfile
12
Gemfile
@@ -13,7 +13,7 @@ gem 'haml-rails', '~>3.0'
|
||||
gem 'pg', '~> 1.5'
|
||||
gem 'pghero'
|
||||
|
||||
gem 'aws-sdk-core', require: false
|
||||
gem 'aws-sdk-core', '< 3.216.0', require: false # TODO: https://github.com/mastodon/mastodon/pull/34173#issuecomment-2733378873
|
||||
gem 'aws-sdk-s3', '~> 1.123', require: false
|
||||
gem 'blurhash', '~> 0.1'
|
||||
gem 'fog-core', '<= 2.6.0'
|
||||
@@ -24,7 +24,7 @@ gem 'ruby-vips', '~> 2.2', require: false
|
||||
|
||||
gem 'active_model_serializers', '~> 0.10'
|
||||
gem 'addressable', '~> 2.8'
|
||||
gem 'bootsnap', '~> 1.19.0', require: false
|
||||
gem 'bootsnap', '~> 1.18.0', require: false
|
||||
gem 'browser'
|
||||
gem 'charlock_holmes', '~> 0.7.7'
|
||||
gem 'chewy', '~> 7.3'
|
||||
@@ -40,7 +40,7 @@ gem 'net-ldap', '~> 0.18'
|
||||
gem 'omniauth', '~> 2.0'
|
||||
gem 'omniauth-cas', '~> 3.0.0.beta.1'
|
||||
gem 'omniauth_openid_connect', '~> 0.8.0'
|
||||
gem 'omniauth-rails_csrf_protection', '~> 2.0'
|
||||
gem 'omniauth-rails_csrf_protection', '~> 1.0'
|
||||
gem 'omniauth-saml', '~> 2.0'
|
||||
|
||||
gem 'color_diff', '~> 0.1'
|
||||
@@ -71,7 +71,7 @@ gem 'oj', '~> 3.14'
|
||||
gem 'ox', '~> 2.14'
|
||||
gem 'parslet'
|
||||
gem 'premailer-rails'
|
||||
gem 'public_suffix', '~> 7.0'
|
||||
gem 'public_suffix', '~> 6.0'
|
||||
gem 'pundit', '~> 2.3'
|
||||
gem 'rack-attack', '~> 6.6'
|
||||
gem 'rack-cors', require: 'rack/cors'
|
||||
@@ -114,7 +114,7 @@ group :opentelemetry do
|
||||
gem 'opentelemetry-instrumentation-http', '~> 0.27.0', require: false
|
||||
gem 'opentelemetry-instrumentation-http_client', '~> 0.26.0', require: false
|
||||
gem 'opentelemetry-instrumentation-net_http', '~> 0.26.0', require: false
|
||||
gem 'opentelemetry-instrumentation-pg', '~> 0.34.0', require: false
|
||||
gem 'opentelemetry-instrumentation-pg', '~> 0.32.0', require: false
|
||||
gem 'opentelemetry-instrumentation-rack', '~> 0.29.0', require: false
|
||||
gem 'opentelemetry-instrumentation-rails', '~> 0.39.0', require: false
|
||||
gem 'opentelemetry-instrumentation-redis', '~> 0.28.0', require: false
|
||||
@@ -138,7 +138,7 @@ group :test do
|
||||
# Browser integration testing
|
||||
gem 'capybara', '~> 3.39'
|
||||
gem 'capybara-playwright-driver'
|
||||
gem 'playwright-ruby-client', '1.57.0', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package
|
||||
gem 'playwright-ruby-client', '1.55.0', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package
|
||||
|
||||
# Used to reset the database between system tests
|
||||
gem 'database_cleaner-active_record'
|
||||
|
||||
201
Gemfile.lock
201
Gemfile.lock
@@ -53,7 +53,7 @@ GEM
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
active_model_serializers (0.10.16)
|
||||
active_model_serializers (0.10.15)
|
||||
actionpack (>= 4.1)
|
||||
activemodel (>= 4.1)
|
||||
case_transform (>= 0.2)
|
||||
@@ -86,8 +86,8 @@ GEM
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
uri (>= 0.13.1)
|
||||
addressable (2.8.8)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
aes_key_wrap (1.1.0)
|
||||
android_key_attestation (0.3.0)
|
||||
annotaterb (4.20.0)
|
||||
@@ -96,20 +96,17 @@ GEM
|
||||
ast (2.4.3)
|
||||
attr_required (1.0.2)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1194.0)
|
||||
aws-sdk-core (3.239.2)
|
||||
aws-partitions (1.1168.0)
|
||||
aws-sdk-core (3.215.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.118.0)
|
||||
aws-sdk-core (~> 3, >= 3.239.1)
|
||||
aws-sdk-kms (1.96.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.207.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sdk-s3 (1.177.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
@@ -129,14 +126,14 @@ GEM
|
||||
binding_of_caller (1.0.1)
|
||||
debug_inspector (>= 1.2.0)
|
||||
blurhash (0.1.8)
|
||||
bootsnap (1.19.0)
|
||||
bootsnap (1.18.6)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (7.1.1)
|
||||
brakeman (7.0.2)
|
||||
racc
|
||||
browser (6.2.0)
|
||||
builder (3.3.0)
|
||||
bundler-audit (0.9.3)
|
||||
bundler (>= 1.2.0)
|
||||
bundler-audit (0.9.2)
|
||||
bundler (>= 1.2.0, < 3)
|
||||
thor (~> 1.0)
|
||||
capybara (3.40.0)
|
||||
addressable
|
||||
@@ -166,8 +163,8 @@ GEM
|
||||
climate_control (1.2.0)
|
||||
cocoon (1.2.15)
|
||||
color_diff (0.1)
|
||||
concurrent-ruby (1.3.6)
|
||||
connection_pool (2.5.5)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.4)
|
||||
cose (1.3.1)
|
||||
cbor (~> 0.5.9)
|
||||
openssl-signature_algorithm (~> 1.0)
|
||||
@@ -182,7 +179,7 @@ GEM
|
||||
activerecord (>= 5.a)
|
||||
database_cleaner-core (~> 2.0)
|
||||
database_cleaner-core (2.0.1)
|
||||
date (3.5.1)
|
||||
date (3.4.1)
|
||||
debug (1.11.0)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
@@ -208,7 +205,7 @@ GEM
|
||||
domain_name (0.6.20240107)
|
||||
doorkeeper (5.8.2)
|
||||
railties (>= 5)
|
||||
dotenv (3.2.0)
|
||||
dotenv (3.1.8)
|
||||
drb (2.2.3)
|
||||
dry-cli (1.3.0)
|
||||
elasticsearch (7.17.11)
|
||||
@@ -227,14 +224,14 @@ GEM
|
||||
mail (~> 2.7)
|
||||
email_validator (2.2.4)
|
||||
activemodel
|
||||
erb (6.0.1)
|
||||
erb (5.1.1)
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.4.0)
|
||||
tzinfo
|
||||
excon (1.3.2)
|
||||
excon (1.3.0)
|
||||
logger
|
||||
fabrication (3.0.0)
|
||||
faker (3.5.3)
|
||||
faker (3.5.2)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (2.14.0)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
@@ -244,8 +241,8 @@ GEM
|
||||
faraday (>= 1, < 3)
|
||||
faraday-httpclient (2.0.2)
|
||||
httpclient (>= 2.2)
|
||||
faraday-net_http (3.4.2)
|
||||
net-http (~> 0.5)
|
||||
faraday-net_http (3.4.1)
|
||||
net-http (>= 0.5.0)
|
||||
fast_blank (1.0.1)
|
||||
fastimage (2.4.0)
|
||||
ffi (1.17.2)
|
||||
@@ -269,20 +266,20 @@ GEM
|
||||
fog-openstack (1.1.5)
|
||||
fog-core (~> 2.1)
|
||||
fog-json (>= 1.0)
|
||||
formatador (1.2.3)
|
||||
formatador (1.2.1)
|
||||
reline
|
||||
forwardable (1.3.3)
|
||||
fugit (1.12.1)
|
||||
fugit (1.12.0)
|
||||
et-orbi (~> 1.4)
|
||||
raabro (~> 1.4)
|
||||
globalid (1.3.0)
|
||||
activesupport (>= 6.1)
|
||||
google-protobuf (4.33.2)
|
||||
google-protobuf (4.32.1)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
googleapis-common-protos-types (1.22.0)
|
||||
google-protobuf (~> 4.26)
|
||||
haml (7.1.0)
|
||||
haml (6.3.0)
|
||||
temple (>= 0.8.2)
|
||||
thor
|
||||
tilt
|
||||
@@ -291,7 +288,7 @@ GEM
|
||||
activesupport (>= 5.1)
|
||||
haml (>= 4.0.6)
|
||||
railties (>= 5.1)
|
||||
haml_lint (0.68.0)
|
||||
haml_lint (0.66.0)
|
||||
haml (>= 5.0)
|
||||
parallel (~> 1.10)
|
||||
rainbow
|
||||
@@ -304,8 +301,8 @@ GEM
|
||||
highline (3.1.2)
|
||||
reline
|
||||
hiredis (0.6.3)
|
||||
hiredis-client (0.26.2)
|
||||
redis-client (= 0.26.2)
|
||||
hiredis-client (0.26.1)
|
||||
redis-client (= 0.26.1)
|
||||
hkdf (0.3.0)
|
||||
htmlentities (4.3.4)
|
||||
http (5.3.1)
|
||||
@@ -324,14 +321,13 @@ GEM
|
||||
rainbow (>= 2.0.0)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-tasks (1.1.2)
|
||||
i18n-tasks (1.0.15)
|
||||
activesupport (>= 4.0.2)
|
||||
ast (>= 2.1.0)
|
||||
erubi
|
||||
highline (>= 3.0.0)
|
||||
highline (>= 2.0.0)
|
||||
i18n
|
||||
parser (>= 3.2.2.1)
|
||||
prism
|
||||
rails-i18n
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
ruby-progressbar (~> 1.8, >= 1.8.1)
|
||||
@@ -340,8 +336,8 @@ GEM
|
||||
inline_svg (1.10.0)
|
||||
activesupport (>= 3.0)
|
||||
nokogiri (>= 1.6)
|
||||
io-console (0.8.2)
|
||||
irb (1.15.3)
|
||||
io-console (0.8.1)
|
||||
irb (1.15.2)
|
||||
pp (>= 0.6.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
@@ -350,7 +346,7 @@ GEM
|
||||
azure-blob (~> 0.5.2)
|
||||
hashie (~> 5.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.18.0)
|
||||
json (2.15.1)
|
||||
json-canonicalization (1.0.0)
|
||||
json-jwt (1.17.0)
|
||||
activesupport (>= 4.2)
|
||||
@@ -447,13 +443,13 @@ GEM
|
||||
mime-types-data (3.2025.0924)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.9)
|
||||
minitest (5.27.0)
|
||||
minitest (5.26.0)
|
||||
msgpack (1.8.0)
|
||||
multi_json (1.18.0)
|
||||
multi_json (1.17.0)
|
||||
mutex_m (0.3.0)
|
||||
net-http (0.6.0)
|
||||
uri
|
||||
net-imap (0.6.0)
|
||||
net-imap (0.5.12)
|
||||
date
|
||||
net-protocol
|
||||
net-ldap (0.20.0)
|
||||
@@ -465,11 +461,11 @@ GEM
|
||||
timeout
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.5)
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.18.10)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
oj (3.16.13)
|
||||
oj (3.16.11)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
omniauth (2.1.4)
|
||||
@@ -481,7 +477,7 @@ GEM
|
||||
addressable (~> 2.8)
|
||||
nokogiri (~> 1.12)
|
||||
omniauth (~> 2.1)
|
||||
omniauth-rails_csrf_protection (2.0.1)
|
||||
omniauth-rails_csrf_protection (1.0.2)
|
||||
actionpack (>= 4.2)
|
||||
omniauth (~> 2.0)
|
||||
omniauth-saml (2.2.4)
|
||||
@@ -516,9 +512,9 @@ GEM
|
||||
opentelemetry-common (~> 0.20)
|
||||
opentelemetry-sdk (~> 1.10)
|
||||
opentelemetry-semantic_conventions
|
||||
opentelemetry-helpers-sql (0.3.0)
|
||||
opentelemetry-helpers-sql (0.2.0)
|
||||
opentelemetry-api (~> 1.7)
|
||||
opentelemetry-helpers-sql-processor (0.3.1)
|
||||
opentelemetry-helpers-sql-obfuscation (0.4.0)
|
||||
opentelemetry-common (~> 0.21)
|
||||
opentelemetry-instrumentation-action_mailer (0.6.1)
|
||||
opentelemetry-instrumentation-active_support (~> 0.10)
|
||||
@@ -542,19 +538,19 @@ GEM
|
||||
opentelemetry-registry (~> 0.1)
|
||||
opentelemetry-instrumentation-concurrent_ruby (0.24.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-excon (0.26.1)
|
||||
opentelemetry-instrumentation-excon (0.26.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-faraday (0.30.1)
|
||||
opentelemetry-instrumentation-faraday (0.30.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-http (0.27.1)
|
||||
opentelemetry-instrumentation-http (0.27.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-http_client (0.26.1)
|
||||
opentelemetry-instrumentation-http_client (0.26.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-net_http (0.26.1)
|
||||
opentelemetry-instrumentation-net_http (0.26.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-pg (0.34.1)
|
||||
opentelemetry-instrumentation-pg (0.32.0)
|
||||
opentelemetry-helpers-sql
|
||||
opentelemetry-helpers-sql-processor
|
||||
opentelemetry-helpers-sql-obfuscation
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-rack (0.29.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
@@ -569,7 +565,7 @@ GEM
|
||||
opentelemetry-instrumentation-concurrent_ruby (~> 0.23)
|
||||
opentelemetry-instrumentation-redis (0.28.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-instrumentation-sidekiq (0.28.1)
|
||||
opentelemetry-instrumentation-sidekiq (0.28.0)
|
||||
opentelemetry-instrumentation-base (~> 0.25)
|
||||
opentelemetry-registry (0.4.0)
|
||||
opentelemetry-api (~> 1.1)
|
||||
@@ -585,7 +581,7 @@ GEM
|
||||
ox (2.14.23)
|
||||
bigdecimal (>= 3.0)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.10.0)
|
||||
parser (3.3.9.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
parslet (2.0.0)
|
||||
@@ -594,7 +590,7 @@ GEM
|
||||
pg (1.6.2)
|
||||
pghero (3.7.0)
|
||||
activerecord (>= 7.1)
|
||||
playwright-ruby-client (1.57.0)
|
||||
playwright-ruby-client (1.55.0)
|
||||
concurrent-ruby (>= 1.1.6)
|
||||
mime-types (>= 3.0)
|
||||
pp (0.6.3)
|
||||
@@ -608,37 +604,37 @@ GEM
|
||||
net-smtp
|
||||
premailer (~> 1.7, >= 1.7.9)
|
||||
prettyprint (0.2.0)
|
||||
prism (1.6.0)
|
||||
prometheus_exporter (2.3.1)
|
||||
prism (1.5.2)
|
||||
prometheus_exporter (2.3.0)
|
||||
webrick
|
||||
propshaft (1.3.1)
|
||||
actionpack (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
rack
|
||||
psych (5.3.0)
|
||||
psych (5.2.6)
|
||||
date
|
||||
stringio
|
||||
public_suffix (7.0.0)
|
||||
public_suffix (6.0.2)
|
||||
puma (7.1.0)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.5.2)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.2.4)
|
||||
rack (3.2.3)
|
||||
rack-attack (6.8.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-cors (3.0.0)
|
||||
logger
|
||||
rack (>= 3.0.14)
|
||||
rack-oauth2 (2.3.0)
|
||||
rack-oauth2 (2.2.1)
|
||||
activesupport
|
||||
attr_required
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
json-jwt (>= 1.11.0)
|
||||
rack (>= 2.1.0)
|
||||
rack-protection (4.2.1)
|
||||
rack-protection (4.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
logger (>= 1.6.0)
|
||||
rack (>= 3.0.0, < 4)
|
||||
@@ -649,7 +645,7 @@ GEM
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.2.0)
|
||||
rack (>= 1.3)
|
||||
rackup (2.3.1)
|
||||
rackup (2.2.1)
|
||||
rack (>= 3)
|
||||
rails (8.0.3)
|
||||
actioncable (= 8.0.3)
|
||||
@@ -672,7 +668,7 @@ GEM
|
||||
rails-html-sanitizer (1.6.2)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
rails-i18n (8.1.0)
|
||||
rails-i18n (8.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 8.0.0, < 9)
|
||||
railties (8.0.3)
|
||||
@@ -685,7 +681,7 @@ GEM
|
||||
tsort (>= 0.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.3.1)
|
||||
rake (13.3.0)
|
||||
rdf (3.3.4)
|
||||
bcp47_spec (~> 0.2)
|
||||
bigdecimal (~> 3.1, >= 3.1.5)
|
||||
@@ -695,7 +691,7 @@ GEM
|
||||
readline (~> 0.0)
|
||||
rdf-normalize (0.7.0)
|
||||
rdf (~> 3.3)
|
||||
rdoc (6.17.0)
|
||||
rdoc (6.15.0)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
tsort
|
||||
@@ -703,10 +699,10 @@ GEM
|
||||
reline
|
||||
redcarpet (3.6.1)
|
||||
redis (4.8.1)
|
||||
redis-client (0.26.2)
|
||||
redis-client (0.26.1)
|
||||
connection_pool
|
||||
regexp_parser (2.11.3)
|
||||
reline (0.6.3)
|
||||
reline (0.6.2)
|
||||
io-console (~> 0.5)
|
||||
request_store (1.7.0)
|
||||
rack (>= 1.4)
|
||||
@@ -717,22 +713,22 @@ GEM
|
||||
rotp (6.3.0)
|
||||
rouge (4.6.1)
|
||||
rpam2 (4.0.2)
|
||||
rqrcode (3.1.1)
|
||||
rqrcode (3.1.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 2.0)
|
||||
rqrcode_core (2.0.1)
|
||||
rspec (3.13.2)
|
||||
rqrcode_core (2.0.0)
|
||||
rspec (3.13.1)
|
||||
rspec-core (~> 3.13.0)
|
||||
rspec-expectations (~> 3.13.0)
|
||||
rspec-mocks (~> 3.13.0)
|
||||
rspec-core (3.13.6)
|
||||
rspec-core (3.13.5)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.5)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-github (3.0.0)
|
||||
rspec-core (~> 3.0)
|
||||
rspec-mocks (3.13.7)
|
||||
rspec-mocks (3.13.5)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (8.0.2)
|
||||
@@ -749,7 +745,7 @@ GEM
|
||||
rspec-mocks (~> 3.0)
|
||||
sidekiq (>= 5, < 9)
|
||||
rspec-support (3.13.6)
|
||||
rubocop (1.81.7)
|
||||
rubocop (1.81.6)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
@@ -760,7 +756,7 @@ GEM
|
||||
rubocop-ast (>= 1.47.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.48.0)
|
||||
rubocop-ast (1.47.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.4)
|
||||
rubocop-capybara (2.22.1)
|
||||
@@ -773,16 +769,16 @@ GEM
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.47.1, < 2.0)
|
||||
rubocop-rails (2.34.2)
|
||||
rubocop-rails (2.33.4)
|
||||
activesupport (>= 4.2.0)
|
||||
lint_roller (~> 1.1)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
rubocop-rspec (3.8.0)
|
||||
rubocop-rspec (3.7.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.81)
|
||||
rubocop-rspec_rails (2.32.0)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
rubocop-rspec_rails (2.31.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
rubocop-rspec (~> 3.5)
|
||||
@@ -792,10 +788,10 @@ GEM
|
||||
ruby-saml (1.18.1)
|
||||
nokogiri (>= 1.13.10)
|
||||
rexml
|
||||
ruby-vips (2.3.0)
|
||||
ruby-vips (2.2.5)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
rubyzip (3.2.2)
|
||||
rubyzip (3.2.1)
|
||||
rufus-scheduler (3.9.2)
|
||||
fugit (~> 1.1, >= 1.11.1)
|
||||
safety_net_attestation (0.5.0)
|
||||
@@ -807,9 +803,9 @@ GEM
|
||||
activerecord (>= 4.0.0)
|
||||
railties (>= 4.0.0)
|
||||
securerandom (0.4.1)
|
||||
shoulda-matchers (7.0.1)
|
||||
activesupport (>= 7.1)
|
||||
sidekiq (8.0.10)
|
||||
shoulda-matchers (6.5.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (8.0.8)
|
||||
connection_pool (>= 2.5.0)
|
||||
json (>= 2.9.0)
|
||||
logger (>= 1.6.2)
|
||||
@@ -820,7 +816,7 @@ GEM
|
||||
sidekiq-scheduler (6.0.1)
|
||||
rufus-scheduler (~> 3.2)
|
||||
sidekiq (>= 7.3, < 9)
|
||||
sidekiq-unique-jobs (8.0.12)
|
||||
sidekiq-unique-jobs (8.0.11)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||
sidekiq (>= 7.0.0, < 9.0.0)
|
||||
thor (>= 1.0, < 3.0)
|
||||
@@ -839,10 +835,9 @@ GEM
|
||||
stackprof (0.2.27)
|
||||
starry (0.2.0)
|
||||
base64
|
||||
stoplight (5.7.0)
|
||||
concurrent-ruby
|
||||
stoplight (5.4.0)
|
||||
zeitwerk
|
||||
stringio (3.1.9)
|
||||
stringio (3.1.7)
|
||||
strong_migrations (2.5.1)
|
||||
activerecord (>= 7.1)
|
||||
swd (2.0.3)
|
||||
@@ -856,10 +851,10 @@ GEM
|
||||
unicode-display_width (>= 1.1.1, < 4)
|
||||
terrapin (1.1.1)
|
||||
climate_control
|
||||
test-prof (1.5.0)
|
||||
test-prof (1.4.4)
|
||||
thor (1.4.0)
|
||||
tilt (2.6.1)
|
||||
timeout (0.5.0)
|
||||
timeout (0.4.3)
|
||||
tpm-key_attestation (0.14.1)
|
||||
bindata (~> 2.4)
|
||||
openssl (> 2.0)
|
||||
@@ -880,7 +875,7 @@ GEM
|
||||
unf (~> 0.1.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2025.3)
|
||||
tzinfo-data (1.2025.2)
|
||||
tzinfo (>= 1.0.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
@@ -888,7 +883,7 @@ GEM
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.1.0)
|
||||
uri (1.1.1)
|
||||
uri (1.0.4)
|
||||
useragent (0.16.11)
|
||||
validate_url (1.0.15)
|
||||
activemodel (>= 3.0.0)
|
||||
@@ -916,11 +911,11 @@ GEM
|
||||
activesupport
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
webmock (3.26.1)
|
||||
webmock (3.26.0)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.9.2)
|
||||
webrick (1.9.1)
|
||||
websocket-driver (0.8.0)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
@@ -938,12 +933,12 @@ DEPENDENCIES
|
||||
active_model_serializers (~> 0.10)
|
||||
addressable (~> 2.8)
|
||||
annotaterb (~> 4.13)
|
||||
aws-sdk-core
|
||||
aws-sdk-core (< 3.216.0)
|
||||
aws-sdk-s3 (~> 1.123)
|
||||
better_errors (~> 2.9)
|
||||
binding_of_caller (~> 1.0)
|
||||
blurhash (~> 0.1)
|
||||
bootsnap (~> 1.19.0)
|
||||
bootsnap (~> 1.18.0)
|
||||
brakeman (~> 7.0)
|
||||
browser
|
||||
bundler-audit (~> 0.9)
|
||||
@@ -1010,7 +1005,7 @@ DEPENDENCIES
|
||||
oj (~> 3.14)
|
||||
omniauth (~> 2.0)
|
||||
omniauth-cas (~> 3.0.0.beta.1)
|
||||
omniauth-rails_csrf_protection (~> 2.0)
|
||||
omniauth-rails_csrf_protection (~> 1.0)
|
||||
omniauth-saml (~> 2.0)
|
||||
omniauth_openid_connect (~> 0.8.0)
|
||||
opentelemetry-api (~> 1.7.0)
|
||||
@@ -1023,7 +1018,7 @@ DEPENDENCIES
|
||||
opentelemetry-instrumentation-http (~> 0.27.0)
|
||||
opentelemetry-instrumentation-http_client (~> 0.26.0)
|
||||
opentelemetry-instrumentation-net_http (~> 0.26.0)
|
||||
opentelemetry-instrumentation-pg (~> 0.34.0)
|
||||
opentelemetry-instrumentation-pg (~> 0.32.0)
|
||||
opentelemetry-instrumentation-rack (~> 0.29.0)
|
||||
opentelemetry-instrumentation-rails (~> 0.39.0)
|
||||
opentelemetry-instrumentation-redis (~> 0.28.0)
|
||||
@@ -1033,11 +1028,11 @@ DEPENDENCIES
|
||||
parslet
|
||||
pg (~> 1.5)
|
||||
pghero
|
||||
playwright-ruby-client (= 1.57.0)
|
||||
playwright-ruby-client (= 1.55.0)
|
||||
premailer-rails
|
||||
prometheus_exporter (~> 2.2)
|
||||
propshaft
|
||||
public_suffix (~> 7.0)
|
||||
public_suffix (~> 6.0)
|
||||
puma (~> 7.0)
|
||||
pundit (~> 2.3)
|
||||
rack-attack (~> 6.6)
|
||||
@@ -1090,7 +1085,7 @@ DEPENDENCIES
|
||||
xorcist (~> 1.1)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.4.1p0
|
||||
ruby 3.4.1p0
|
||||
|
||||
BUNDLED WITH
|
||||
4.0.1
|
||||
2.7.2
|
||||
|
||||
@@ -15,8 +15,7 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ---------------- |
|
||||
| 4.5.x | Yes |
|
||||
| 4.4.x | Yes |
|
||||
| 4.3.x | Until 2026-05-06 |
|
||||
| 4.3.x | Yes |
|
||||
| 4.2.x | Until 2026-01-08 |
|
||||
| < 4.2 | No |
|
||||
|
||||
@@ -39,7 +39,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
||||
return @body if defined?(@body)
|
||||
|
||||
@body = request.body.read
|
||||
@body.presence&.force_encoding('UTF-8')
|
||||
@body.force_encoding('UTF-8') if @body.present?
|
||||
|
||||
request.body.rewind if request.body.respond_to?(:rewind)
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class ActivityPub::LikesController < ActivityPub::BaseController
|
||||
def set_status
|
||||
@status = @account.statuses.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController
|
||||
before_action :set_quote_authorization
|
||||
|
||||
def show
|
||||
expires_in 30.seconds, public: true if @quote.quoted_status.distributable? && public_fetch_mode?
|
||||
expires_in 30.seconds, public: true if @quote.status.distributable? && public_fetch_mode?
|
||||
render json: @quote, serializer: ActivityPub::QuoteAuthorizationSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||
end
|
||||
|
||||
@@ -23,8 +23,8 @@ class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController
|
||||
@quote = Quote.accepted.where(quoted_account: @account).find(params[:id])
|
||||
return not_found unless @quote.status.present? && @quote.quoted_status.present?
|
||||
|
||||
authorize @quote.quoted_status, :show?
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
authorize @quote.status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,7 +25,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
|
||||
def set_status
|
||||
@status = @account.statuses.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class ActivityPub::SharesController < ActivityPub::BaseController
|
||||
def set_status
|
||||
@status = @account.statuses.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -5,15 +5,6 @@ module Admin
|
||||
def index
|
||||
authorize :custom_emoji, :index?
|
||||
|
||||
# If filtering by local emojis, remove by_domain filter.
|
||||
params.delete(:by_domain) if params[:local].present?
|
||||
|
||||
# If filtering by domain, ensure remote filter is set.
|
||||
if params[:by_domain].present?
|
||||
params.delete(:local)
|
||||
params[:remote] = '1'
|
||||
end
|
||||
|
||||
@custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page])
|
||||
@form = Form::CustomEmojiBatch.new
|
||||
end
|
||||
|
||||
@@ -9,7 +9,7 @@ module Admin
|
||||
|
||||
@site_upload.destroy!
|
||||
|
||||
redirect_back_or_to admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
|
||||
redirect_back fallback_location: admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::AnnualReportsController < Api::BaseController
|
||||
include AsyncRefreshesConcern
|
||||
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, except: [:read, :generate]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:read, :generate]
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index
|
||||
before_action :require_user!
|
||||
before_action :set_annual_report, only: [:show, :read]
|
||||
before_action :set_annual_report, except: :index
|
||||
|
||||
def index
|
||||
with_read_replica do
|
||||
@@ -30,28 +28,6 @@ class Api::V1::AnnualReportsController < Api::BaseController
|
||||
relationships: @relationships
|
||||
end
|
||||
|
||||
def state
|
||||
render json: { state: report_state }
|
||||
end
|
||||
|
||||
def generate
|
||||
return render_empty unless year == AnnualReport.current_campaign
|
||||
return render_empty if GeneratedAnnualReport.exists?(account_id: current_account.id, year: year)
|
||||
|
||||
async_refresh = AsyncRefresh.new(refresh_key)
|
||||
|
||||
if async_refresh.running?
|
||||
add_async_refresh_header(async_refresh, retry_seconds: 2)
|
||||
return head 202
|
||||
end
|
||||
|
||||
add_async_refresh_header(AsyncRefresh.create(refresh_key), retry_seconds: 2)
|
||||
|
||||
GenerateAnnualReportWorker.perform_async(current_account.id, year)
|
||||
|
||||
head 202
|
||||
end
|
||||
|
||||
def read
|
||||
@annual_report.view!
|
||||
render_empty
|
||||
@@ -59,21 +35,7 @@ class Api::V1::AnnualReportsController < Api::BaseController
|
||||
|
||||
private
|
||||
|
||||
def report_state
|
||||
AnnualReport.new(current_account, year).state do |async_refresh|
|
||||
add_async_refresh_header(async_refresh, retry_seconds: 2)
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_key
|
||||
"wrapstodon:#{current_account.id}:#{year}"
|
||||
end
|
||||
|
||||
def year
|
||||
params[:id]&.to_i
|
||||
end
|
||||
|
||||
def set_annual_report
|
||||
@annual_report = GeneratedAnnualReport.find_by!(account_id: current_account.id, year: year)
|
||||
@annual_report = GeneratedAnnualReport.find_by!(account_id: current_account.id, year: params[:id])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,7 +17,7 @@ class Api::V1::Polls::VotesController < Api::BaseController
|
||||
def set_poll
|
||||
@poll = Poll.find(params[:poll_id])
|
||||
authorize @poll.status, :show?
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class Api::V1::PollsController < Api::BaseController
|
||||
def set_poll
|
||||
@poll = Poll.find(params[:id])
|
||||
authorize @poll.status, :show?
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class Api::V1::Statuses::BaseController < Api::BaseController
|
||||
def set_status
|
||||
@status = Status.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -23,7 +23,7 @@ class Api::V1::Statuses::BookmarksController < Api::V1::Statuses::BaseController
|
||||
bookmark&.destroy!
|
||||
|
||||
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, bookmarks_map: { @status.id => false })
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,7 +25,7 @@ class Api::V1::Statuses::FavouritesController < Api::V1::Statuses::BaseControlle
|
||||
|
||||
relationships = StatusRelationshipsPresenter.new([@status], current_account.id, favourites_map: { @status.id => false }, attributes_map: { @status.id => { favourites_count: count } })
|
||||
render json: @status, serializer: REST::StatusSerializer, relationships: relationships
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -41,8 +41,8 @@ class Api::V1::Statuses::QuotesController < Api::V1::Statuses::BaseController
|
||||
if current_account&.id != @status.account_id
|
||||
domains = @statuses.filter_map(&:account_domain).uniq
|
||||
account_ids = @statuses.map(&:account_id).uniq
|
||||
current_account&.preload_relations!(account_ids, domains)
|
||||
@statuses.reject! { |status| StatusFilter.new(status, current_account).filtered? }
|
||||
relations = current_account&.relations_map(account_ids, domains) || {}
|
||||
@statuses.reject! { |status| StatusFilter.new(status, current_account, relations).filtered? }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
|
||||
|
||||
relationships = StatusRelationshipsPresenter.new([@status], current_account.id, reblogs_map: { @reblog.id => false }, attributes_map: { @reblog.id => { reblogs_count: count } })
|
||||
render json: @reblog, serializer: REST::StatusSerializer, relationships: relationships
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
@@ -45,7 +45,7 @@ class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
|
||||
def set_reblog
|
||||
@reblog = Status.find(params[:status_id])
|
||||
authorize @reblog, :show?
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
if async_refresh.running?
|
||||
add_async_refresh_header(async_refresh)
|
||||
elsif !current_account.nil? && @status.should_fetch_replies?
|
||||
add_async_refresh_header(AsyncRefresh.create(refresh_key, count_results: true))
|
||||
add_async_refresh_header(AsyncRefresh.create(refresh_key))
|
||||
|
||||
WorkerBatch.new.within do |batch|
|
||||
batch.connect(refresh_key, threshold: 1.0)
|
||||
@@ -128,11 +128,10 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
@status = Status.where(account: current_account).find(params[:id])
|
||||
authorize @status, :destroy?
|
||||
|
||||
json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
|
||||
|
||||
@status.discard_with_reblogs
|
||||
StatusPin.find_by(status: @status)&.destroy
|
||||
@status.account.statuses_count = @status.account.statuses_count - 1
|
||||
json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
|
||||
|
||||
RemovalWorker.perform_async(@status.id, { 'redraft' => !truthy_param?(:delete_media) })
|
||||
|
||||
@@ -148,7 +147,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
def set_status
|
||||
@status = Status.find(params[:id])
|
||||
authorize @status, :show?
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1Alpha::CollectionItemsController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
before_action :check_feature_enabled
|
||||
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:collections' }
|
||||
|
||||
before_action :require_user!
|
||||
|
||||
before_action :set_collection
|
||||
before_action :set_account, only: [:create]
|
||||
before_action :set_collection_item, only: [:destroy]
|
||||
|
||||
after_action :verify_authorized
|
||||
|
||||
def create
|
||||
authorize @collection, :update?
|
||||
authorize @account, :feature?
|
||||
|
||||
@item = AddAccountToCollectionService.new.call(@collection, @account)
|
||||
|
||||
render json: @item, serializer: REST::CollectionItemSerializer
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @collection, :update?
|
||||
|
||||
@collection_item.destroy
|
||||
|
||||
head 200
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_collection
|
||||
@collection = Collection.find(params[:collection_id])
|
||||
end
|
||||
|
||||
def set_account
|
||||
return render(json: { error: '`account_id` parameter is missing' }, status: 422) if params[:account_id].blank?
|
||||
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
|
||||
def set_collection_item
|
||||
@collection_item = @collection.collection_items.find(params[:id])
|
||||
end
|
||||
|
||||
def check_feature_enabled
|
||||
raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled?
|
||||
end
|
||||
end
|
||||
@@ -1,114 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1Alpha::CollectionsController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
DEFAULT_COLLECTIONS_LIMIT = 40
|
||||
|
||||
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
|
||||
render json: { error: ValidationErrorFormatter.new(e).as_json }, status: 422
|
||||
end
|
||||
|
||||
before_action :check_feature_enabled
|
||||
|
||||
before_action -> { authorize_if_got_token! :read, :'read:collections' }, only: [:index, :show]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:collections' }, only: [:create, :update, :destroy]
|
||||
|
||||
before_action :require_user!, only: [:create, :update, :destroy]
|
||||
|
||||
before_action :set_account, only: [:index]
|
||||
before_action :set_collections, only: [:index]
|
||||
before_action :set_collection, only: [:show, :update, :destroy]
|
||||
|
||||
after_action :insert_pagination_headers, only: [:index]
|
||||
|
||||
after_action :verify_authorized
|
||||
|
||||
def index
|
||||
cache_if_unauthenticated!
|
||||
authorize Collection, :index?
|
||||
|
||||
render json: @collections, each_serializer: REST::BaseCollectionSerializer
|
||||
end
|
||||
|
||||
def show
|
||||
cache_if_unauthenticated!
|
||||
authorize @collection, :show?
|
||||
|
||||
render json: @collection, serializer: REST::CollectionSerializer
|
||||
end
|
||||
|
||||
def create
|
||||
authorize Collection, :create?
|
||||
|
||||
@collection = CreateCollectionService.new.call(collection_creation_params, current_user.account)
|
||||
|
||||
render json: @collection, serializer: REST::CollectionSerializer
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @collection, :update?
|
||||
|
||||
@collection.update!(collection_update_params) # TODO: Create a service for this to federate changes
|
||||
|
||||
render json: @collection, serializer: REST::CollectionSerializer
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @collection, :destroy?
|
||||
|
||||
@collection.destroy
|
||||
|
||||
head 200
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
|
||||
def set_collections
|
||||
@collections = @account.collections
|
||||
.with_tag
|
||||
.order(created_at: :desc)
|
||||
.offset(offset_param)
|
||||
.limit(limit_param(DEFAULT_COLLECTIONS_LIMIT))
|
||||
end
|
||||
|
||||
def set_collection
|
||||
@collection = Collection.find(params[:id])
|
||||
end
|
||||
|
||||
def collection_creation_params
|
||||
params.permit(:name, :description, :sensitive, :discoverable, :tag_name, account_ids: [])
|
||||
end
|
||||
|
||||
def collection_update_params
|
||||
params.permit(:name, :description, :sensitive, :discoverable, :tag_name)
|
||||
end
|
||||
|
||||
def check_feature_enabled
|
||||
raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled?
|
||||
end
|
||||
|
||||
def next_path
|
||||
return unless records_continue?
|
||||
|
||||
api_v1_alpha_account_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT)))
|
||||
end
|
||||
|
||||
def prev_path
|
||||
return if offset_param.zero?
|
||||
|
||||
api_v1_alpha_account_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT)))
|
||||
end
|
||||
|
||||
def records_continue?
|
||||
((offset_param * limit_param(DEFAULT_COLLECTIONS_LIMIT)) + @collections.size) < @account.collections.size
|
||||
end
|
||||
|
||||
def offset_param
|
||||
params[:offset].to_i
|
||||
end
|
||||
end
|
||||
@@ -30,7 +30,7 @@ class Api::Web::EmbedsController < Api::Web::BaseController
|
||||
def set_status
|
||||
@status = Status.find(params[:id])
|
||||
authorize @status, :show?
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
@@ -135,7 +135,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||
@accept_token = session[:accept_token] = SecureRandom.hex
|
||||
@invite_code = invite_code
|
||||
|
||||
render :rules
|
||||
set_locale { render :rules }
|
||||
end
|
||||
|
||||
def is_flashing_format? # rubocop:disable Naming/PredicatePrefix
|
||||
|
||||
@@ -21,7 +21,7 @@ class AuthorizeInteractionsController < ApplicationController
|
||||
def set_resource
|
||||
@resource = located_resource
|
||||
authorize(@resource, :show?) if @resource.is_a?(Status)
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -72,13 +72,10 @@ module SignatureVerification
|
||||
rescue Mastodon::SignatureVerificationError => e
|
||||
fail_with! e.message
|
||||
rescue *Mastodon::HTTP_CONNECTION_ERRORS => e
|
||||
@signature_verification_failure_code ||= 503
|
||||
fail_with! "Failed to fetch remote data: #{e.message}"
|
||||
rescue Mastodon::UnexpectedResponseError
|
||||
@signature_verification_failure_code ||= 503
|
||||
fail_with! 'Failed to fetch remote data (got unexpected reply from server)'
|
||||
rescue Stoplight::Error::RedLight
|
||||
@signature_verification_failure_code ||= 503
|
||||
fail_with! 'Fetching attempt skipped because of recent connection failure'
|
||||
end
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ class FollowerAccountsController < ApplicationController
|
||||
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
|
||||
|
||||
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :protect_hidden_collections, if: -> { request.format.json? }
|
||||
|
||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||
skip_before_action :require_functional!, unless: :limited_federation_mode?
|
||||
@@ -19,6 +18,8 @@ class FollowerAccountsController < ApplicationController
|
||||
end
|
||||
|
||||
format.json do
|
||||
raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
|
||||
|
||||
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
|
||||
|
||||
render json: collection_presenter,
|
||||
@@ -40,10 +41,6 @@ class FollowerAccountsController < ApplicationController
|
||||
@follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
|
||||
end
|
||||
|
||||
def protect_hidden_collections
|
||||
raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
|
||||
end
|
||||
|
||||
def page_requested?
|
||||
params[:page].present?
|
||||
end
|
||||
|
||||
@@ -7,7 +7,6 @@ class FollowingAccountsController < ApplicationController
|
||||
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
|
||||
|
||||
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :protect_hidden_collections, if: -> { request.format.json? }
|
||||
|
||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||
skip_before_action :require_functional!, unless: :limited_federation_mode?
|
||||
@@ -19,6 +18,11 @@ class FollowingAccountsController < ApplicationController
|
||||
end
|
||||
|
||||
format.json do
|
||||
if page_requested? && @account.hide_collections?
|
||||
forbidden
|
||||
next
|
||||
end
|
||||
|
||||
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
|
||||
|
||||
render json: collection_presenter,
|
||||
@@ -40,10 +44,6 @@ class FollowingAccountsController < ApplicationController
|
||||
@follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
|
||||
end
|
||||
|
||||
def protect_hidden_collections
|
||||
raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
|
||||
end
|
||||
|
||||
def page_requested?
|
||||
params[:page].present?
|
||||
end
|
||||
|
||||
@@ -34,7 +34,7 @@ class MediaController < ApplicationController
|
||||
|
||||
def verify_permitted_status!
|
||||
authorize @media_attachment.status, :show?
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ class StatusesController < ApplicationController
|
||||
def set_status
|
||||
@status = @account.statuses.find(params[:id])
|
||||
authorize @status, :show?
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WrapstodonController < ApplicationController
|
||||
include WebAppControllerConcern
|
||||
include Authorization
|
||||
include AccountOwnedConcern
|
||||
|
||||
vary_by 'Accept, Accept-Language, Cookie'
|
||||
|
||||
before_action :set_generated_annual_report
|
||||
|
||||
skip_before_action :require_functional!, only: :show, unless: :limited_federation_mode?
|
||||
|
||||
def show
|
||||
expires_in 10.minutes, public: true if current_account.nil?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_generated_annual_report
|
||||
@generated_annual_report = GeneratedAnnualReport.find_by!(account: @account, year: params[:year], share_key: params[:share_key])
|
||||
end
|
||||
end
|
||||
@@ -153,9 +153,11 @@ module ApplicationHelper
|
||||
tag.meta(content: content, property: property)
|
||||
end
|
||||
|
||||
def html_classes
|
||||
def body_classes
|
||||
output = []
|
||||
output << content_for(:html_classes)
|
||||
output << content_for(:body_classes)
|
||||
output << "flavour-#{current_flavour.parameterize}"
|
||||
output << "skin-#{current_skin.parameterize}"
|
||||
output << 'system-font' if current_account&.user&.setting_system_font_ui
|
||||
output << 'custom-scrollbars' unless current_account&.user&.setting_system_scrollbars_ui
|
||||
output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion')
|
||||
@@ -163,12 +165,6 @@ module ApplicationHelper
|
||||
output.compact_blank.join(' ')
|
||||
end
|
||||
|
||||
def body_classes
|
||||
output = []
|
||||
output << content_for(:body_classes)
|
||||
output.compact_blank.join(' ')
|
||||
end
|
||||
|
||||
def cdn_host
|
||||
Rails.configuration.action_controller.asset_host
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
module DatabaseHelper
|
||||
def replica_enabled?
|
||||
ENV['REPLICA_DB_NAME'] || ENV['REPLICA_DB_HOST'] || ENV.fetch('REPLICA_DATABASE_URL', nil)
|
||||
ENV['REPLICA_DB_NAME'] || ENV.fetch('REPLICA_DATABASE_URL', nil)
|
||||
end
|
||||
module_function :replica_enabled?
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ module LanguagesHelper
|
||||
cy: ['Welsh', 'Cymraeg'].freeze,
|
||||
da: ['Danish', 'dansk'].freeze,
|
||||
de: ['German', 'Deutsch'].freeze,
|
||||
dv: ['Divehi', 'ދިވެހި'].freeze,
|
||||
dv: ['Divehi', 'Dhivehi'].freeze,
|
||||
dz: ['Dzongkha', 'རྫོང་ཁ'].freeze,
|
||||
ee: ['Ewe', 'Eʋegbe'].freeze,
|
||||
el: ['Greek', 'Ελληνικά'].freeze,
|
||||
@@ -100,7 +100,7 @@ module LanguagesHelper
|
||||
lo: ['Lao', 'ລາວ'].freeze,
|
||||
lt: ['Lithuanian', 'lietuvių kalba'].freeze,
|
||||
lu: ['Luba-Katanga', 'Tshiluba'].freeze,
|
||||
lv: ['Latvian', 'Latviski'].freeze,
|
||||
lv: ['Latvian', 'latviešu valoda'].freeze,
|
||||
mg: ['Malagasy', 'fiteny malagasy'].freeze,
|
||||
mh: ['Marshallese', 'Kajin M̧ajeļ'].freeze,
|
||||
mi: ['Māori', 'te reo Māori'].freeze,
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WrapstodonHelper
|
||||
def render_wrapstodon_share_data(report)
|
||||
payload = ActiveModelSerializers::SerializableResource.new(
|
||||
AnnualReportsPresenter.new([report]),
|
||||
serializer: REST::AnnualReportsSerializer,
|
||||
scope: nil,
|
||||
scope_name: :current_user
|
||||
).as_json
|
||||
|
||||
payload[:me] = current_account.id.to_s if user_signed_in?
|
||||
payload[:domain] = Addressable::IDNA.to_unicode(Rails.configuration.x.local_domain)
|
||||
|
||||
json_string = payload.to_json
|
||||
|
||||
# rubocop:disable Rails/OutputSafety
|
||||
content_tag(:script, json_escape(json_string).html_safe, type: 'application/json', id: 'wrapstodon-data')
|
||||
# rubocop:enable Rails/OutputSafety
|
||||
end
|
||||
end
|
||||
@@ -1,49 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DateOfBirthInput < SimpleForm::Inputs::Base
|
||||
OPTIONS = {
|
||||
day: { autocomplete: 'bday-day', maxlength: 2, pattern: '[0-9]+', placeholder: 'DD' },
|
||||
month: { autocomplete: 'bday-month', maxlength: 2, pattern: '[0-9]+', placeholder: 'MM' },
|
||||
year: { autocomplete: 'bday-year', maxlength: 4, pattern: '[0-9]+', placeholder: 'YYYY' },
|
||||
}.freeze
|
||||
OPTIONS = [
|
||||
{ autocomplete: 'bday-day', maxlength: 2, pattern: '[0-9]+', placeholder: 'DD' }.freeze,
|
||||
{ autocomplete: 'bday-month', maxlength: 2, pattern: '[0-9]+', placeholder: 'MM' }.freeze,
|
||||
{ autocomplete: 'bday-year', maxlength: 4, pattern: '[0-9]+', placeholder: 'YYYY' }.freeze,
|
||||
].freeze
|
||||
|
||||
def input(wrapper_options = nil)
|
||||
merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)
|
||||
merged_input_options[:inputmode] = 'numeric'
|
||||
|
||||
safe_join(
|
||||
ordered_options.map do |option|
|
||||
options = merged_input_options
|
||||
.merge(OPTIONS[option])
|
||||
.merge(
|
||||
id: generate_id(option),
|
||||
'aria-label': I18n.t("simple_form.labels.user.date_of_birth_#{param_for(option)}"),
|
||||
value: values[option]
|
||||
)
|
||||
@builder.text_field("#{attribute_name}(#{param_for(option)})", options)
|
||||
end
|
||||
)
|
||||
values = (object.public_send(attribute_name) || '').split('.')
|
||||
|
||||
safe_join(Array.new(3) do |index|
|
||||
options = merged_input_options.merge(OPTIONS[index]).merge id: generate_id(index), 'aria-label': I18n.t("simple_form.labels.user.date_of_birth_#{index + 1}i"), value: values[index]
|
||||
@builder.text_field("#{attribute_name}(#{index + 1}i)", options)
|
||||
end)
|
||||
end
|
||||
|
||||
def label_target
|
||||
"#{attribute_name}_#{param_for(ordered_options.first)}"
|
||||
"#{attribute_name}_1i"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ordered_options
|
||||
I18n.t('date.order').map(&:to_sym)
|
||||
end
|
||||
|
||||
def generate_id(option)
|
||||
"#{object_name}_#{attribute_name}_#{param_for(option)}"
|
||||
end
|
||||
|
||||
def param_for(option)
|
||||
"#{ActionView::Helpers::DateTimeSelector::POSITION[option]}i"
|
||||
end
|
||||
|
||||
def values
|
||||
Date._parse((object.public_send(attribute_name) || '').to_s).transform_keys(mon: :month, mday: :day)
|
||||
def generate_id(index)
|
||||
"#{object_name}_#{attribute_name}_#{index + 1}i"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import Rails from '@rails/ujs';
|
||||
import { decode, ValidationError } from 'blurhash';
|
||||
import { on } from 'delegated-events';
|
||||
|
||||
import ready from '../mastodon/ready';
|
||||
|
||||
@@ -24,9 +24,10 @@ const setAnnouncementEndsAttributes = (target: HTMLInputElement) => {
|
||||
}
|
||||
};
|
||||
|
||||
on(
|
||||
'change',
|
||||
Rails.delegate(
|
||||
document,
|
||||
'input[type="datetime-local"]#announcement_starts_at',
|
||||
'change',
|
||||
({ target }) => {
|
||||
if (target instanceof HTMLInputElement)
|
||||
setAnnouncementEndsAttributes(target);
|
||||
@@ -62,7 +63,7 @@ const hideSelectAll = () => {
|
||||
if (hiddenField) hiddenField.value = '0';
|
||||
};
|
||||
|
||||
on('change', '#batch_checkbox_all', ({ target }) => {
|
||||
Rails.delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
|
||||
if (!(target instanceof HTMLInputElement)) return;
|
||||
|
||||
const selectAllMatchingElement = document.querySelector(
|
||||
@@ -84,7 +85,7 @@ on('change', '#batch_checkbox_all', ({ target }) => {
|
||||
}
|
||||
});
|
||||
|
||||
on('click', '.batch-table__select-all button', () => {
|
||||
Rails.delegate(document, '.batch-table__select-all button', 'click', () => {
|
||||
const hiddenField = document.querySelector<HTMLInputElement>(
|
||||
'#select_all_matching',
|
||||
);
|
||||
@@ -112,7 +113,7 @@ on('click', '.batch-table__select-all button', () => {
|
||||
}
|
||||
});
|
||||
|
||||
on('change', batchCheckboxClassName, () => {
|
||||
Rails.delegate(document, batchCheckboxClassName, 'change', () => {
|
||||
const checkAllElement = document.querySelector<HTMLInputElement>(
|
||||
'input#batch_checkbox_all',
|
||||
);
|
||||
@@ -139,9 +140,14 @@ on('change', batchCheckboxClassName, () => {
|
||||
}
|
||||
});
|
||||
|
||||
on('change', '.filter-subset--with-select select', ({ target }) => {
|
||||
if (target instanceof HTMLSelectElement) target.form?.submit();
|
||||
});
|
||||
Rails.delegate(
|
||||
document,
|
||||
'.filter-subset--with-select select',
|
||||
'change',
|
||||
({ target }) => {
|
||||
if (target instanceof HTMLSelectElement) target.form?.submit();
|
||||
},
|
||||
);
|
||||
|
||||
const onDomainBlockSeverityChange = (target: HTMLSelectElement) => {
|
||||
const rejectMediaDiv = document.querySelector(
|
||||
@@ -162,11 +168,11 @@ const onDomainBlockSeverityChange = (target: HTMLSelectElement) => {
|
||||
}
|
||||
};
|
||||
|
||||
on('change', '#domain_block_severity', ({ target }) => {
|
||||
Rails.delegate(document, '#domain_block_severity', 'change', ({ target }) => {
|
||||
if (target instanceof HTMLSelectElement) onDomainBlockSeverityChange(target);
|
||||
});
|
||||
|
||||
function onEnableBootstrapTimelineAccountsChange(target: HTMLInputElement) {
|
||||
const onEnableBootstrapTimelineAccountsChange = (target: HTMLInputElement) => {
|
||||
const bootstrapTimelineAccountsField =
|
||||
document.querySelector<HTMLInputElement>(
|
||||
'#form_admin_settings_bootstrap_timeline_accounts',
|
||||
@@ -188,11 +194,12 @@ function onEnableBootstrapTimelineAccountsChange(target: HTMLInputElement) {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
on(
|
||||
'change',
|
||||
Rails.delegate(
|
||||
document,
|
||||
'#form_admin_settings_enable_bootstrap_timeline_accounts',
|
||||
'change',
|
||||
({ target }) => {
|
||||
if (target instanceof HTMLInputElement)
|
||||
onEnableBootstrapTimelineAccountsChange(target);
|
||||
@@ -232,11 +239,11 @@ const onChangeRegistrationMode = (target: HTMLSelectElement) => {
|
||||
});
|
||||
};
|
||||
|
||||
function convertUTCDateTimeToLocal(value: string) {
|
||||
const convertUTCDateTimeToLocal = (value: string) => {
|
||||
const date = new Date(value + 'Z');
|
||||
const twoChars = (x: number) => x.toString().padStart(2, '0');
|
||||
return `${date.getFullYear()}-${twoChars(date.getMonth() + 1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`;
|
||||
}
|
||||
};
|
||||
|
||||
function convertLocalDatetimeToUTC(value: string) {
|
||||
const date = new Date(value);
|
||||
@@ -244,9 +251,14 @@ function convertLocalDatetimeToUTC(value: string) {
|
||||
return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6);
|
||||
}
|
||||
|
||||
on('change', '#form_admin_settings_registrations_mode', ({ target }) => {
|
||||
if (target instanceof HTMLSelectElement) onChangeRegistrationMode(target);
|
||||
});
|
||||
Rails.delegate(
|
||||
document,
|
||||
'#form_admin_settings_registrations_mode',
|
||||
'change',
|
||||
({ target }) => {
|
||||
if (target instanceof HTMLSelectElement) onChangeRegistrationMode(target);
|
||||
},
|
||||
);
|
||||
|
||||
async function mountReactComponent(element: Element) {
|
||||
const componentName = element.getAttribute('data-admin-component');
|
||||
@@ -256,8 +268,9 @@ async function mountReactComponent(element: Element) {
|
||||
|
||||
const componentProps = JSON.parse(stringProps) as object;
|
||||
|
||||
const { default: AdminComponent } =
|
||||
await import('@/mastodon/containers/admin_component');
|
||||
const { default: AdminComponent } = await import(
|
||||
'@/mastodon/containers/admin_component'
|
||||
);
|
||||
|
||||
const { default: Component } = (await import(
|
||||
`@/mastodon/components/admin/${componentName}.jsx`
|
||||
@@ -292,7 +305,7 @@ ready(() => {
|
||||
if (registrationMode) onChangeRegistrationMode(registrationMode);
|
||||
|
||||
const checkAllElement = document.querySelector<HTMLInputElement>(
|
||||
'#batch_checkbox_all',
|
||||
'input#batch_checkbox_all',
|
||||
);
|
||||
if (checkAllElement) {
|
||||
const allCheckboxes = Array.from(
|
||||
@@ -305,7 +318,7 @@ ready(() => {
|
||||
}
|
||||
|
||||
document
|
||||
.querySelector<HTMLAnchorElement>('a#add-instance-button')
|
||||
.querySelector('a#add-instance-button')
|
||||
?.addEventListener('click', (e) => {
|
||||
const domain = document.querySelector<HTMLInputElement>(
|
||||
'input[type="text"]#by_domain',
|
||||
@@ -329,7 +342,7 @@ ready(() => {
|
||||
}
|
||||
});
|
||||
|
||||
on('submit', 'form', ({ target }) => {
|
||||
Rails.delegate(document, 'form', 'submit', ({ target }) => {
|
||||
if (target instanceof HTMLFormElement)
|
||||
target
|
||||
.querySelectorAll<HTMLInputElement>('input[type="datetime-local"]')
|
||||
|
||||
@@ -4,8 +4,8 @@ import { IntlMessageFormat } from 'intl-messageformat';
|
||||
import type { MessageDescriptor, PrimitiveType } from 'react-intl';
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import Rails from '@rails/ujs';
|
||||
import axios from 'axios';
|
||||
import { on } from 'delegated-events';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
import { timeAgoString } from '../mastodon/components/relative_timestamp';
|
||||
@@ -175,9 +175,10 @@ function loaded() {
|
||||
});
|
||||
}
|
||||
|
||||
on(
|
||||
'input',
|
||||
Rails.delegate(
|
||||
document,
|
||||
'input#user_account_attributes_username',
|
||||
'input',
|
||||
throttle(
|
||||
({ target }) => {
|
||||
if (!(target instanceof HTMLInputElement)) return;
|
||||
@@ -201,47 +202,60 @@ function loaded() {
|
||||
),
|
||||
);
|
||||
|
||||
on('input', '#user_password,#user_password_confirmation', () => {
|
||||
const password = document.querySelector<HTMLInputElement>(
|
||||
'input#user_password',
|
||||
);
|
||||
const confirmation = document.querySelector<HTMLInputElement>(
|
||||
'input#user_password_confirmation',
|
||||
);
|
||||
if (!confirmation || !password) return;
|
||||
Rails.delegate(
|
||||
document,
|
||||
'#user_password,#user_password_confirmation',
|
||||
'input',
|
||||
() => {
|
||||
const password = document.querySelector<HTMLInputElement>(
|
||||
'input#user_password',
|
||||
);
|
||||
const confirmation = document.querySelector<HTMLInputElement>(
|
||||
'input#user_password_confirmation',
|
||||
);
|
||||
if (!confirmation || !password) return;
|
||||
|
||||
if (confirmation.value && confirmation.value.length > password.maxLength) {
|
||||
confirmation.setCustomValidity(
|
||||
formatMessage(messages.passwordExceedsLength),
|
||||
);
|
||||
} else if (password.value && password.value !== confirmation.value) {
|
||||
confirmation.setCustomValidity(
|
||||
formatMessage(messages.passwordDoesNotMatch),
|
||||
);
|
||||
} else {
|
||||
confirmation.setCustomValidity('');
|
||||
}
|
||||
});
|
||||
if (
|
||||
confirmation.value &&
|
||||
confirmation.value.length > password.maxLength
|
||||
) {
|
||||
confirmation.setCustomValidity(
|
||||
formatMessage(messages.passwordExceedsLength),
|
||||
);
|
||||
} else if (password.value && password.value !== confirmation.value) {
|
||||
confirmation.setCustomValidity(
|
||||
formatMessage(messages.passwordDoesNotMatch),
|
||||
);
|
||||
} else {
|
||||
confirmation.setCustomValidity('');
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
on('change', '#edit_profile input[type=file]', ({ target }) => {
|
||||
if (!(target instanceof HTMLInputElement)) return;
|
||||
Rails.delegate(
|
||||
document,
|
||||
'#edit_profile input[type=file]',
|
||||
'change',
|
||||
({ target }) => {
|
||||
if (!(target instanceof HTMLInputElement)) return;
|
||||
|
||||
const avatar = document.querySelector<HTMLImageElement>(
|
||||
`img#${target.id}-preview`,
|
||||
);
|
||||
const avatar = document.querySelector<HTMLImageElement>(
|
||||
`img#${target.id}-preview`,
|
||||
);
|
||||
|
||||
if (!avatar) return;
|
||||
if (!avatar) return;
|
||||
|
||||
let file: File | undefined;
|
||||
if (target.files) file = target.files[0];
|
||||
let file: File | undefined;
|
||||
if (target.files) file = target.files[0];
|
||||
|
||||
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
|
||||
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
|
||||
|
||||
if (url) avatar.src = url;
|
||||
});
|
||||
if (url) avatar.src = url;
|
||||
},
|
||||
);
|
||||
|
||||
on('click', '.input-copy input', ({ target }) => {
|
||||
Rails.delegate(document, '.input-copy input', 'click', ({ target }) => {
|
||||
if (!(target instanceof HTMLInputElement)) return;
|
||||
|
||||
target.focus();
|
||||
@@ -249,7 +263,7 @@ on('click', '.input-copy input', ({ target }) => {
|
||||
target.setSelectionRange(0, target.value.length);
|
||||
});
|
||||
|
||||
on('click', '.input-copy button', ({ target }) => {
|
||||
Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
|
||||
if (!(target instanceof HTMLButtonElement)) return;
|
||||
|
||||
const input = target.parentNode?.querySelector<HTMLInputElement>(
|
||||
@@ -298,22 +312,22 @@ const toggleSidebar = () => {
|
||||
sidebar.classList.toggle('visible');
|
||||
};
|
||||
|
||||
on('click', '.sidebar__toggle__icon', () => {
|
||||
Rails.delegate(document, '.sidebar__toggle__icon', 'click', () => {
|
||||
toggleSidebar();
|
||||
});
|
||||
|
||||
on('keydown', '.sidebar__toggle__icon', (e) => {
|
||||
Rails.delegate(document, '.sidebar__toggle__icon', 'keydown', (e) => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
on('mouseover', 'img.custom-emoji', ({ target }) => {
|
||||
Rails.delegate(document, 'img.custom-emoji', 'mouseover', ({ target }) => {
|
||||
if (target instanceof HTMLImageElement && target.dataset.original)
|
||||
target.src = target.dataset.original;
|
||||
});
|
||||
on('mouseout', 'img.custom-emoji', ({ target }) => {
|
||||
Rails.delegate(document, 'img.custom-emoji', 'mouseout', ({ target }) => {
|
||||
if (target instanceof HTMLImageElement && target.dataset.static)
|
||||
target.src = target.dataset.static;
|
||||
});
|
||||
@@ -362,17 +376,22 @@ const setInputHint = (
|
||||
}
|
||||
};
|
||||
|
||||
on('change', '#account_statuses_cleanup_policy_enabled', ({ target }) => {
|
||||
if (!(target instanceof HTMLInputElement) || !target.form) return;
|
||||
Rails.delegate(
|
||||
document,
|
||||
'#account_statuses_cleanup_policy_enabled',
|
||||
'change',
|
||||
({ target }) => {
|
||||
if (!(target instanceof HTMLInputElement) || !target.form) return;
|
||||
|
||||
target.form
|
||||
.querySelectorAll<
|
||||
HTMLInputElement | HTMLSelectElement
|
||||
>('input:not([type=hidden], #account_statuses_cleanup_policy_enabled), select')
|
||||
.forEach((input) => {
|
||||
setInputDisabled(input, !target.checked);
|
||||
});
|
||||
});
|
||||
target.form
|
||||
.querySelectorAll<
|
||||
HTMLInputElement | HTMLSelectElement
|
||||
>('input:not([type=hidden], #account_statuses_cleanup_policy_enabled), select')
|
||||
.forEach((input) => {
|
||||
setInputDisabled(input, !target.checked);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const updateDefaultQuotePrivacyFromPrivacy = (
|
||||
privacySelect: EventTarget | null,
|
||||
@@ -395,13 +414,18 @@ const updateDefaultQuotePrivacyFromPrivacy = (
|
||||
}
|
||||
};
|
||||
|
||||
on('change', '#user_settings_attributes_default_privacy', ({ target }) => {
|
||||
updateDefaultQuotePrivacyFromPrivacy(target);
|
||||
});
|
||||
Rails.delegate(
|
||||
document,
|
||||
'#user_settings_attributes_default_privacy',
|
||||
'change',
|
||||
({ target }) => {
|
||||
updateDefaultQuotePrivacyFromPrivacy(target);
|
||||
},
|
||||
);
|
||||
|
||||
// Empty the honeypot fields in JS in case something like an extension
|
||||
// automatically filled them.
|
||||
on('submit', '#registration_new_user,#new_user', () => {
|
||||
Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => {
|
||||
[
|
||||
'user_website',
|
||||
'user_confirm_password',
|
||||
@@ -415,7 +439,7 @@ on('submit', '#registration_new_user,#new_user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
on('click', '.rules-list button', ({ target }) => {
|
||||
Rails.delegate(document, '.rules-list button', 'click', ({ target }) => {
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
|
||||
import { importFetchedStatuses } from '@/mastodon/actions/importer';
|
||||
import { hydrateStore } from '@/mastodon/actions/store';
|
||||
import type { ApiAnnualReportResponse } from '@/mastodon/api/annual_report';
|
||||
import { Router } from '@/mastodon/components/router';
|
||||
import { WrapstodonSharedPage } from '@/mastodon/features/annual_report/shared_page';
|
||||
import { IntlProvider, loadLocale } from '@/mastodon/locales';
|
||||
import { loadPolyfills } from '@/mastodon/polyfills';
|
||||
import ready from '@/mastodon/ready';
|
||||
import { setReport } from '@/mastodon/reducers/slices/annual_report';
|
||||
import { store } from '@/mastodon/store';
|
||||
|
||||
function loaded() {
|
||||
const mountNode = document.getElementById('wrapstodon');
|
||||
if (!mountNode) {
|
||||
throw new Error('Mount node not found');
|
||||
}
|
||||
const propsNode = document.getElementById('wrapstodon-data');
|
||||
if (!propsNode) {
|
||||
throw new Error('Initial state prop not found');
|
||||
}
|
||||
|
||||
const initialState = JSON.parse(
|
||||
propsNode.textContent,
|
||||
) as ApiAnnualReportResponse & { me?: string; domain: string };
|
||||
|
||||
const report = initialState.annual_reports[0];
|
||||
if (!report) {
|
||||
throw new Error('Initial state report not found');
|
||||
}
|
||||
|
||||
// Set up store
|
||||
store.dispatch(
|
||||
hydrateStore({
|
||||
meta: {
|
||||
locale: document.documentElement.lang,
|
||||
me: initialState.me,
|
||||
domain: initialState.domain,
|
||||
},
|
||||
accounts: initialState.accounts,
|
||||
}),
|
||||
);
|
||||
store.dispatch(importFetchedStatuses(initialState.statuses));
|
||||
|
||||
store.dispatch(setReport(report));
|
||||
|
||||
const root = createRoot(mountNode);
|
||||
root.render(
|
||||
<IntlProvider>
|
||||
<ReduxProvider store={store}>
|
||||
<Router>
|
||||
<WrapstodonSharedPage />
|
||||
</Router>
|
||||
</ReduxProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
loadPolyfills()
|
||||
.then(loadLocale)
|
||||
.then(() => ready(loaded))
|
||||
.catch((err: unknown) => {
|
||||
console.error(err);
|
||||
});
|
||||
25
app/javascript/flavours/glitch/actions/bundles.js
Normal file
25
app/javascript/flavours/glitch/actions/bundles.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST';
|
||||
export const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS';
|
||||
export const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL';
|
||||
|
||||
export function fetchBundleRequest(skipLoading) {
|
||||
return {
|
||||
type: BUNDLE_FETCH_REQUEST,
|
||||
skipLoading,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchBundleSuccess(skipLoading) {
|
||||
return {
|
||||
type: BUNDLE_FETCH_SUCCESS,
|
||||
skipLoading,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchBundleFail(error, skipLoading) {
|
||||
return {
|
||||
type: BUNDLE_FETCH_FAIL,
|
||||
error,
|
||||
skipLoading,
|
||||
};
|
||||
}
|
||||
@@ -58,6 +58,7 @@ export const COMPOSE_ADVANCED_OPTIONS_CHANGE = 'COMPOSE_ADVANCED_OPTIONS_CHANGE'
|
||||
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
|
||||
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
||||
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
||||
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
|
||||
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
|
||||
export const COMPOSE_CONTENT_TYPE_CHANGE = 'COMPOSE_CONTENT_TYPE_CHANGE';
|
||||
export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE';
|
||||
@@ -709,8 +710,8 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
|
||||
|
||||
dispatch(useEmoji(suggestion));
|
||||
} else if (suggestion.type === 'hashtag') {
|
||||
completion = token + suggestion.name.slice(token.length - 1);
|
||||
startPosition = position - 1;
|
||||
completion = suggestion.name.slice(token.length - 1);
|
||||
startPosition = position + token.length;
|
||||
} else if (suggestion.type === 'account') {
|
||||
completion = `@${getState().getIn(['accounts', suggestion.id, 'acct'])}`;
|
||||
startPosition = position - 1;
|
||||
@@ -824,6 +825,13 @@ export function changeComposeSpoilerText(text) {
|
||||
};
|
||||
}
|
||||
|
||||
export function changeComposeVisibility(value) {
|
||||
return {
|
||||
type: COMPOSE_VISIBILITY_CHANGE,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
export function insertEmojiCompose(position, emoji, needsSpace) {
|
||||
return {
|
||||
type: COMPOSE_EMOJI_INSERT,
|
||||
|
||||
@@ -13,11 +13,10 @@ import {
|
||||
} from 'flavours/glitch/store/typed_functions';
|
||||
|
||||
import type { ApiQuotePolicy } from '../api_types/quotes';
|
||||
import type { Status, StatusVisibility } from '../models/status';
|
||||
import type { RootState } from '../store';
|
||||
import type { Status } from '../models/status';
|
||||
|
||||
import { showAlert } from './alerts';
|
||||
import { changeCompose, focusCompose } from './compose';
|
||||
import { focusCompose } from './compose';
|
||||
import { importFetchedStatuses } from './importer';
|
||||
import { openModal } from './modal';
|
||||
|
||||
@@ -42,10 +41,6 @@ const messages = defineMessages({
|
||||
id: 'quote_error.unauthorized',
|
||||
defaultMessage: 'You are not authorized to quote this post.',
|
||||
},
|
||||
quoteErrorPrivateMention: {
|
||||
id: 'quote_error.private_mentions',
|
||||
defaultMessage: 'Quoting is not allowed with direct mentions.',
|
||||
},
|
||||
});
|
||||
|
||||
type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & {
|
||||
@@ -72,39 +67,6 @@ const simulateModifiedApiResponse = (
|
||||
return data;
|
||||
};
|
||||
|
||||
export const changeComposeVisibility = createAppThunk(
|
||||
'compose/visibility_change',
|
||||
(visibility: StatusVisibility, { dispatch, getState }) => {
|
||||
if (visibility !== 'direct') {
|
||||
return visibility;
|
||||
}
|
||||
|
||||
const state = getState();
|
||||
const quotedStatusId = state.compose.get('quoted_status_id') as
|
||||
| string
|
||||
| null;
|
||||
if (!quotedStatusId) {
|
||||
return visibility;
|
||||
}
|
||||
|
||||
// Remove the quoted status
|
||||
dispatch(quoteComposeCancel());
|
||||
const quotedStatus = state.statuses.get(quotedStatusId) as Status | null;
|
||||
if (!quotedStatus) {
|
||||
return visibility;
|
||||
}
|
||||
|
||||
// Append the quoted status URL to the compose text
|
||||
const url = quotedStatus.get('url') as string;
|
||||
const text = state.compose.get('text') as string;
|
||||
if (!text.includes(url)) {
|
||||
const newText = text.trim() ? `${text}\n\n${url}` : url;
|
||||
dispatch(changeCompose(newText));
|
||||
}
|
||||
return visibility;
|
||||
},
|
||||
);
|
||||
|
||||
export const changeUploadCompose = createDataLoadingThunk(
|
||||
'compose/changeUpload',
|
||||
async (
|
||||
@@ -168,8 +130,6 @@ export const quoteComposeByStatus = createAppThunk(
|
||||
|
||||
if (composeState.get('id')) {
|
||||
dispatch(showAlert({ message: messages.quoteErrorEdit }));
|
||||
} else if (composeState.get('privacy') === 'direct') {
|
||||
dispatch(showAlert({ message: messages.quoteErrorPrivateMention }));
|
||||
} else if (composeState.get('poll')) {
|
||||
dispatch(showAlert({ message: messages.quoteErrorPoll }));
|
||||
} else if (
|
||||
@@ -213,17 +173,6 @@ export const quoteComposeById = createAppThunk(
|
||||
},
|
||||
);
|
||||
|
||||
const composeStateForbidsLink = (composeState: RootState['compose']) => {
|
||||
return (
|
||||
composeState.get('quoted_status_id') ||
|
||||
composeState.get('is_submitting') ||
|
||||
composeState.get('poll') ||
|
||||
composeState.get('is_uploading') ||
|
||||
composeState.get('id') ||
|
||||
composeState.get('privacy') === 'direct'
|
||||
);
|
||||
};
|
||||
|
||||
export const pasteLinkCompose = createDataLoadingThunk(
|
||||
'compose/pasteLink',
|
||||
async ({ url }: { url: string }) => {
|
||||
@@ -234,12 +183,15 @@ export const pasteLinkCompose = createDataLoadingThunk(
|
||||
limit: 2,
|
||||
});
|
||||
},
|
||||
(data, { dispatch, getState, requestId }) => {
|
||||
(data, { dispatch, getState }) => {
|
||||
const composeState = getState().compose;
|
||||
|
||||
if (
|
||||
composeStateForbidsLink(composeState) ||
|
||||
composeState.get('fetching_link') !== requestId // Request has been cancelled
|
||||
composeState.get('quoted_status_id') ||
|
||||
composeState.get('is_submitting') ||
|
||||
composeState.get('poll') ||
|
||||
composeState.get('is_uploading') ||
|
||||
composeState.get('id')
|
||||
)
|
||||
return;
|
||||
|
||||
@@ -255,17 +207,6 @@ export const pasteLinkCompose = createDataLoadingThunk(
|
||||
dispatch(quoteComposeById(data.statuses[0].id));
|
||||
}
|
||||
},
|
||||
{
|
||||
useLoadingBar: false,
|
||||
condition: (_, { getState }) =>
|
||||
!getState().compose.get('fetching_link') &&
|
||||
!composeStateForbidsLink(getState().compose),
|
||||
},
|
||||
);
|
||||
|
||||
// Ideally this would cancel the action and the HTTP request, but this is good enough
|
||||
export const cancelPasteLinkCompose = createAction(
|
||||
'compose/cancelPasteLinkCompose',
|
||||
);
|
||||
|
||||
export const quoteComposeCancel = createAction('compose/quoteComposeCancel');
|
||||
|
||||
@@ -46,11 +46,11 @@ export function importFetchedAccounts(accounts) {
|
||||
return importAccounts({ accounts: normalAccounts });
|
||||
}
|
||||
|
||||
export function importFetchedStatus(status, options = {}) {
|
||||
return importFetchedStatuses([status], options);
|
||||
export function importFetchedStatus(status) {
|
||||
return importFetchedStatuses([status]);
|
||||
}
|
||||
|
||||
export function importFetchedStatuses(statuses, options = {}) {
|
||||
export function importFetchedStatuses(statuses) {
|
||||
return (dispatch, getState) => {
|
||||
const accounts = [];
|
||||
const normalStatuses = [];
|
||||
@@ -58,7 +58,7 @@ export function importFetchedStatuses(statuses, options = {}) {
|
||||
const filters = [];
|
||||
|
||||
function processStatus(status) {
|
||||
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]), { ...options, settings: getState().get('local_settings') }));
|
||||
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]), getState().get('local_settings')));
|
||||
pushUnique(accounts, status.account);
|
||||
|
||||
if (status.filtered) {
|
||||
|
||||
@@ -27,12 +27,9 @@ function stripQuoteFallback(text) {
|
||||
return wrapper.innerHTML;
|
||||
}
|
||||
|
||||
export function normalizeStatus(status, normalOldStatus, { settings, bogusQuotePolicy = false }) {
|
||||
export function normalizeStatus(status, normalOldStatus, settings) {
|
||||
const normalStatus = { ...status };
|
||||
|
||||
if (bogusQuotePolicy)
|
||||
normalStatus.quote_approval = null;
|
||||
|
||||
normalStatus.account = status.account.id;
|
||||
|
||||
if (status.reblog && status.reblog.id) {
|
||||
@@ -104,8 +101,6 @@ export function normalizeStatus(status, normalOldStatus, { settings, bogusQuoteP
|
||||
}
|
||||
|
||||
if (normalOldStatus) {
|
||||
normalStatus.quote_approval ||= normalOldStatus.get('quote_approval');
|
||||
|
||||
const list = normalOldStatus.get('media_attachments');
|
||||
if (normalStatus.media_attachments && list) {
|
||||
normalStatus.media_attachments.forEach(item => {
|
||||
|
||||
@@ -147,7 +147,7 @@ export const hydrateSearch = createAppAsyncThunk(
|
||||
'search/hydrate',
|
||||
(_args, { dispatch, getState }) => {
|
||||
const me = getState().meta.get('me') as string;
|
||||
const history = searchHistory.get(me);
|
||||
const history = searchHistory.get(me) as RecentSearch[] | null;
|
||||
|
||||
if (history !== null) {
|
||||
dispatch(updateSearchHistory(history));
|
||||
|
||||
@@ -85,8 +85,6 @@ export function fetchStatus(id, {
|
||||
dispatch(fetchStatusSuccess(skipLoading));
|
||||
}).catch(error => {
|
||||
dispatch(fetchStatusFail(id, error, skipLoading, parentQuotePostId));
|
||||
if (error.status === 404)
|
||||
dispatch(deleteFromTimelines(id));
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -206,8 +204,8 @@ export function deleteStatusFail(id, error) {
|
||||
};
|
||||
}
|
||||
|
||||
export const updateStatus = (status, { bogusQuotePolicy }) => dispatch =>
|
||||
dispatch(importFetchedStatus(status, { bogusQuotePolicy }));
|
||||
export const updateStatus = status => dispatch =>
|
||||
dispatch(importFetchedStatus(status));
|
||||
|
||||
export function muteStatus(id) {
|
||||
return (dispatch) => {
|
||||
|
||||
@@ -37,9 +37,7 @@ export function hydrateStore(rawState) {
|
||||
|
||||
dispatch(hydrateCompose());
|
||||
dispatch(hydrateSearch());
|
||||
if (rawState.accounts) {
|
||||
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
|
||||
}
|
||||
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
|
||||
dispatch(saveSettings());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,9 +52,6 @@ const randomUpTo = max =>
|
||||
export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => {
|
||||
const { messages } = getLocale();
|
||||
|
||||
// Public streams are currently not returning personalized quote policies
|
||||
const bogusQuotePolicy = channelName.startsWith('public') || channelName.startsWith('hashtag');
|
||||
|
||||
return connectStream(channelName, params, (dispatch, getState) => {
|
||||
// @ts-ignore
|
||||
const locale = getState().getIn(['meta', 'locale']);
|
||||
@@ -100,11 +97,11 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
||||
switch (data.event) {
|
||||
case 'update':
|
||||
// @ts-expect-error
|
||||
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), { accept: options.accept, bogusQuotePolicy }));
|
||||
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
|
||||
break;
|
||||
case 'status.update':
|
||||
// @ts-expect-error
|
||||
dispatch(updateStatus(JSON.parse(data.payload), { bogusQuotePolicy }));
|
||||
dispatch(updateStatus(JSON.parse(data.payload)));
|
||||
break;
|
||||
case 'delete':
|
||||
dispatch(deleteFromTimelines(data.payload));
|
||||
|
||||
@@ -7,7 +7,7 @@ import { toServerSideType } from 'flavours/glitch/utils/filters';
|
||||
|
||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||
import { submitMarkers } from './markers';
|
||||
import { timelineDelete } from './timelines_typed';
|
||||
import {timelineDelete} from './timelines_typed';
|
||||
|
||||
export { disconnectTimeline } from './timelines_typed';
|
||||
|
||||
@@ -25,21 +25,15 @@ export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
|
||||
export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL';
|
||||
export const TIMELINE_INSERT = 'TIMELINE_INSERT';
|
||||
|
||||
// When adding new special markers here, make sure to update TIMELINE_NON_STATUS_MARKERS in actions/timelines_typed.js
|
||||
export const TIMELINE_SUGGESTIONS = 'inline-follow-suggestions';
|
||||
export const TIMELINE_GAP = null;
|
||||
|
||||
export const TIMELINE_NON_STATUS_MARKERS = [
|
||||
TIMELINE_GAP,
|
||||
TIMELINE_SUGGESTIONS,
|
||||
];
|
||||
|
||||
export const loadPending = timeline => ({
|
||||
type: TIMELINE_LOAD_PENDING,
|
||||
timeline,
|
||||
});
|
||||
|
||||
export function updateTimeline(timeline, status, { accept = undefined, bogusQuotePolicy = false } = {}) {
|
||||
export function updateTimeline(timeline, status, accept) {
|
||||
return (dispatch, getState) => {
|
||||
if (typeof accept === 'function' && !accept(status)) {
|
||||
return;
|
||||
@@ -61,7 +55,7 @@ export function updateTimeline(timeline, status, { accept = undefined, bogusQuot
|
||||
filtered = filters.length > 0;
|
||||
}
|
||||
|
||||
dispatch(importFetchedStatus(status, { bogusQuotePolicy }));
|
||||
dispatch(importFetchedStatus(status));
|
||||
|
||||
dispatch({
|
||||
type: TIMELINE_UPDATE,
|
||||
|
||||
@@ -2,12 +2,6 @@ import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
|
||||
|
||||
import { TIMELINE_NON_STATUS_MARKERS } from './timelines';
|
||||
|
||||
export function isNonStatusId(value: unknown) {
|
||||
return TIMELINE_NON_STATUS_MARKERS.includes(value as string | null);
|
||||
}
|
||||
|
||||
export const disconnectTimeline = createAction(
|
||||
'timeline/disconnect',
|
||||
({ timeline }: { timeline: string }) => ({
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import api, { apiRequestGet, getAsyncRefreshHeader } from '../api';
|
||||
import type { ApiAccountJSON } from '../api_types/accounts';
|
||||
import type { ApiStatusJSON } from '../api_types/statuses';
|
||||
import type { AnnualReport } from '../models/annual_report';
|
||||
|
||||
export type ApiAnnualReportState =
|
||||
| 'available'
|
||||
| 'generating'
|
||||
| 'eligible'
|
||||
| 'ineligible';
|
||||
|
||||
export const apiGetAnnualReportState = async (year: number) => {
|
||||
const response = await api().get<{ state: ApiAnnualReportState }>(
|
||||
`/api/v1/annual_reports/${year}/state`,
|
||||
);
|
||||
|
||||
return {
|
||||
state: response.data.state,
|
||||
refresh: getAsyncRefreshHeader(response),
|
||||
};
|
||||
};
|
||||
|
||||
export const apiRequestGenerateAnnualReport = async (year: number) => {
|
||||
const response = await api().post(`/api/v1/annual_reports/${year}/generate`);
|
||||
|
||||
return {
|
||||
refresh: getAsyncRefreshHeader(response),
|
||||
};
|
||||
};
|
||||
|
||||
export interface ApiAnnualReportResponse {
|
||||
annual_reports: AnnualReport[];
|
||||
accounts: ApiAccountJSON[];
|
||||
statuses: ApiStatusJSON[];
|
||||
}
|
||||
|
||||
export const apiGetAnnualReport = (year: number) =>
|
||||
apiRequestGet<ApiAnnualReportResponse>(`v1/annual_reports/${year}`);
|
||||
@@ -1,9 +1,8 @@
|
||||
// See app/serializers/rest/custom_emoji_serializer.rb
|
||||
// See app/serializers/rest/account_serializer.rb
|
||||
export interface ApiCustomEmojiJSON {
|
||||
shortcode: string;
|
||||
static_url: string;
|
||||
url: string;
|
||||
category?: string;
|
||||
featured?: boolean;
|
||||
visible_in_picker: boolean;
|
||||
}
|
||||
|
||||
@@ -102,7 +102,8 @@ export interface ApiAccountWarningJSON {
|
||||
appeal: unknown;
|
||||
}
|
||||
|
||||
interface ModerationWarningNotificationGroupJSON extends BaseNotificationGroupJSON {
|
||||
interface ModerationWarningNotificationGroupJSON
|
||||
extends BaseNotificationGroupJSON {
|
||||
type: 'moderation_warning';
|
||||
moderation_warning: ApiAccountWarningJSON;
|
||||
}
|
||||
@@ -122,12 +123,14 @@ export interface ApiAccountRelationshipSeveranceEventJSON {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface AccountRelationshipSeveranceNotificationGroupJSON extends BaseNotificationGroupJSON {
|
||||
interface AccountRelationshipSeveranceNotificationGroupJSON
|
||||
extends BaseNotificationGroupJSON {
|
||||
type: 'severed_relationships';
|
||||
event: ApiAccountRelationshipSeveranceEventJSON;
|
||||
}
|
||||
|
||||
interface AccountRelationshipSeveranceNotificationJSON extends BaseNotificationJSON {
|
||||
interface AccountRelationshipSeveranceNotificationJSON
|
||||
extends BaseNotificationJSON {
|
||||
type: 'severed_relationships';
|
||||
event: ApiAccountRelationshipSeveranceEventJSON;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export interface ApiPreviewCardJSON {
|
||||
html: string;
|
||||
width: number;
|
||||
height: number;
|
||||
image: string | null;
|
||||
image: string;
|
||||
image_description: string;
|
||||
embed_url: string;
|
||||
blurhash: string;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { setupLinkListeners } from './utils/links';
|
||||
import Rails from '@rails/ujs';
|
||||
|
||||
export function start() {
|
||||
setupLinkListeners();
|
||||
try {
|
||||
Rails.start();
|
||||
} catch {
|
||||
// If called twice
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,11 +49,7 @@ export const Alert: React.FC<{
|
||||
</span>
|
||||
|
||||
{hasAction && (
|
||||
<button
|
||||
className='notification-bar__action'
|
||||
onClick={onActionClick}
|
||||
type='button'
|
||||
>
|
||||
<button className='notification-bar__action' onClick={onActionClick}>
|
||||
{action}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -47,7 +47,7 @@ export const AltTextBadge: React.FC<{ description: string }> = ({
|
||||
rootClose
|
||||
onHide={handleClose}
|
||||
show={open}
|
||||
target={anchorRef}
|
||||
target={anchorRef.current}
|
||||
placement='top-end'
|
||||
flip
|
||||
offset={offset}
|
||||
|
||||
@@ -28,7 +28,7 @@ const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
word = word.trim();
|
||||
word = word.trim().toLowerCase();
|
||||
|
||||
if (word.length > 0) {
|
||||
return [left + 1, word];
|
||||
@@ -159,8 +159,8 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
||||
this.input.focus();
|
||||
};
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.suggestions !== this.props.suggestions && this.props.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
|
||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
|
||||
this.setState({ suggestionsHidden: false });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
word = word.trim();
|
||||
word = word.trim().toLowerCase();
|
||||
|
||||
if (word.length > 0) {
|
||||
return [left + 1, word];
|
||||
@@ -50,7 +50,6 @@ const AutosuggestTextarea = forwardRef(({
|
||||
onKeyUp,
|
||||
onKeyDown,
|
||||
onPaste,
|
||||
onDrop,
|
||||
onFocus,
|
||||
autoFocus = true,
|
||||
lang,
|
||||
@@ -154,12 +153,6 @@ const AutosuggestTextarea = forwardRef(({
|
||||
onPaste(e);
|
||||
}, [onPaste]);
|
||||
|
||||
const handleDrop = useCallback((e) => {
|
||||
if (onDrop) {
|
||||
onDrop(e);
|
||||
}
|
||||
}, [onDrop]);
|
||||
|
||||
// Show the suggestions again whenever they change and the textarea is focused
|
||||
useEffect(() => {
|
||||
if (suggestions.size > 0 && textareaRef.current === document.activeElement) {
|
||||
@@ -211,7 +204,6 @@ const AutosuggestTextarea = forwardRef(({
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onPaste={handlePaste}
|
||||
onDrop={handleDrop}
|
||||
dir='auto'
|
||||
aria-autocomplete='list'
|
||||
aria-label={placeholder}
|
||||
@@ -243,7 +235,6 @@ AutosuggestTextarea.propTypes = {
|
||||
onKeyUp: PropTypes.func,
|
||||
onKeyDown: PropTypes.func,
|
||||
onPaste: PropTypes.func.isRequired,
|
||||
onDrop: PropTypes.func,
|
||||
onFocus:PropTypes.func,
|
||||
autoFocus: PropTypes.bool,
|
||||
lang: PropTypes.string,
|
||||
|
||||
@@ -5,10 +5,8 @@ import classNames from 'classnames';
|
||||
|
||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||
|
||||
interface BaseProps extends Omit<
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
'children'
|
||||
> {
|
||||
interface BaseProps
|
||||
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
||||
block?: boolean;
|
||||
secondary?: boolean;
|
||||
plain?: boolean;
|
||||
@@ -80,7 +78,6 @@ export const Button: React.FC<Props> = ({
|
||||
aria-live={loading !== undefined ? 'polite' : undefined}
|
||||
onClick={handleClick}
|
||||
title={title}
|
||||
// eslint-disable-next-line react/button-has-type -- set correctly via TS
|
||||
type={type}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { fn, userEvent, expect } from 'storybook/test';
|
||||
|
||||
import type { CarouselProps } from './index';
|
||||
import { Carousel } from './index';
|
||||
|
||||
interface TestSlideProps {
|
||||
id: number;
|
||||
text: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const TestSlide: FC<TestSlideProps & { active: boolean }> = ({
|
||||
active,
|
||||
text,
|
||||
color,
|
||||
}) => (
|
||||
<div
|
||||
className='test-slide'
|
||||
style={{
|
||||
backgroundColor: active ? color : undefined,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
|
||||
const slides: TestSlideProps[] = [
|
||||
{
|
||||
id: 1,
|
||||
text: 'first',
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
text: 'second',
|
||||
color: 'pink',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
text: 'third',
|
||||
color: 'orange',
|
||||
},
|
||||
];
|
||||
|
||||
type StoryProps = Pick<
|
||||
CarouselProps<TestSlideProps>,
|
||||
'items' | 'renderItem' | 'emptyFallback' | 'onChangeSlide'
|
||||
>;
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Carousel',
|
||||
args: {
|
||||
items: slides,
|
||||
renderItem(item, active) {
|
||||
return <TestSlide {...item} active={active} key={item.id} />;
|
||||
},
|
||||
onChangeSlide: fn(),
|
||||
emptyFallback: 'No slides available',
|
||||
},
|
||||
render(args) {
|
||||
return (
|
||||
<>
|
||||
<Carousel {...args} />
|
||||
<style>
|
||||
{`.test-slide {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
min-height: 100px;
|
||||
transition: background-color 0.3s;
|
||||
background-color: black;
|
||||
}`}
|
||||
</style>
|
||||
</>
|
||||
);
|
||||
},
|
||||
argTypes: {
|
||||
emptyFallback: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
tags: ['test'],
|
||||
} satisfies Meta<StoryProps>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
async play({ args, canvas }) {
|
||||
const nextButton = await canvas.findByRole('button', { name: /next/i });
|
||||
const slides = await canvas.findAllByRole('group');
|
||||
await expect(slides).toHaveLength(slides.length);
|
||||
|
||||
await userEvent.click(nextButton);
|
||||
await expect(args.onChangeSlide).toHaveBeenCalledWith(1, slides[1]);
|
||||
|
||||
await userEvent.click(nextButton);
|
||||
await expect(args.onChangeSlide).toHaveBeenCalledWith(2, slides[2]);
|
||||
|
||||
// Wrap around
|
||||
await userEvent.click(nextButton);
|
||||
await expect(args.onChangeSlide).toHaveBeenCalledWith(0, slides[0]);
|
||||
},
|
||||
};
|
||||
|
||||
export const DifferentHeights: Story = {
|
||||
args: {
|
||||
items: slides.map((props, index) => ({
|
||||
...props,
|
||||
styles: { height: 100 + index * 100 },
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
export const NoSlides: Story = {
|
||||
args: {
|
||||
items: [],
|
||||
},
|
||||
};
|
||||
@@ -1,244 +0,0 @@
|
||||
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import type {
|
||||
ComponentPropsWithoutRef,
|
||||
ComponentType,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import type { MessageDescriptor } from 'react-intl';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { usePrevious } from '@dnd-kit/utilities';
|
||||
import { animated, useSpring } from '@react-spring/web';
|
||||
import { useDrag } from '@use-gesture/react';
|
||||
|
||||
import type { CarouselPaginationProps } from './pagination';
|
||||
import { CarouselPagination } from './pagination';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
const defaultMessages = defineMessages({
|
||||
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
|
||||
next: { id: 'lightbox.next', defaultMessage: 'Next' },
|
||||
current: {
|
||||
id: 'carousel.current',
|
||||
defaultMessage: '<sr>Slide</sr> {current, number} / {max, number}',
|
||||
},
|
||||
slide: {
|
||||
id: 'carousel.slide',
|
||||
defaultMessage: 'Slide {current, number} of {max, number}',
|
||||
},
|
||||
});
|
||||
|
||||
export type MessageKeys = keyof typeof defaultMessages;
|
||||
|
||||
export interface CarouselSlideProps {
|
||||
id: string | number;
|
||||
}
|
||||
|
||||
export type RenderSlideFn<
|
||||
SlideProps extends CarouselSlideProps = CarouselSlideProps,
|
||||
> = (item: SlideProps, active: boolean, index: number) => ReactElement;
|
||||
|
||||
export interface CarouselProps<
|
||||
SlideProps extends CarouselSlideProps = CarouselSlideProps,
|
||||
> {
|
||||
items: SlideProps[];
|
||||
renderItem: RenderSlideFn<SlideProps>;
|
||||
onChangeSlide?: (index: number, ref: Element) => void;
|
||||
paginationComponent?: ComponentType<CarouselPaginationProps> | null;
|
||||
paginationProps?: Partial<CarouselPaginationProps>;
|
||||
messages?: Record<MessageKeys, MessageDescriptor>;
|
||||
emptyFallback?: ReactNode;
|
||||
classNamePrefix?: string;
|
||||
slideClassName?: string;
|
||||
}
|
||||
|
||||
export const Carousel = <
|
||||
SlideProps extends CarouselSlideProps = CarouselSlideProps,
|
||||
>({
|
||||
items,
|
||||
renderItem,
|
||||
onChangeSlide,
|
||||
paginationComponent: Pagination = CarouselPagination,
|
||||
paginationProps = {},
|
||||
messages = defaultMessages,
|
||||
children,
|
||||
emptyFallback = null,
|
||||
className,
|
||||
classNamePrefix = 'carousel',
|
||||
slideClassName,
|
||||
...wrapperProps
|
||||
}: CarouselProps<SlideProps> & ComponentPropsWithoutRef<'div'>) => {
|
||||
// Handle slide change
|
||||
const [slideIndex, setSlideIndex] = useState(0);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
// Handle slide heights
|
||||
const [currentSlideHeight, setCurrentSlideHeight] = useState(
|
||||
() => wrapperRef.current?.scrollHeight ?? 0,
|
||||
);
|
||||
const previousSlideHeight = usePrevious(currentSlideHeight);
|
||||
const handleSlideChange = useCallback(
|
||||
(direction: number) => {
|
||||
setSlideIndex((prev) => {
|
||||
const max = items.length - 1;
|
||||
let newIndex = prev + direction;
|
||||
if (newIndex < 0) {
|
||||
newIndex = max;
|
||||
} else if (newIndex > max) {
|
||||
newIndex = 0;
|
||||
}
|
||||
|
||||
const slide = wrapperRef.current?.children[newIndex];
|
||||
if (slide) {
|
||||
setCurrentSlideHeight(slide.scrollHeight);
|
||||
if (slide instanceof HTMLElement) {
|
||||
onChangeSlide?.(newIndex, slide);
|
||||
}
|
||||
}
|
||||
|
||||
return newIndex;
|
||||
});
|
||||
},
|
||||
[items.length, onChangeSlide],
|
||||
);
|
||||
|
||||
const observerRef = useRef<ResizeObserver | null>(null);
|
||||
observerRef.current ??= new ResizeObserver(() => {
|
||||
handleSlideChange(0);
|
||||
});
|
||||
|
||||
const wrapperStyles = useSpring({
|
||||
x: `-${slideIndex * 100}%`,
|
||||
height: currentSlideHeight,
|
||||
// Don't animate from zero to the height of the initial slide
|
||||
immediate: !previousSlideHeight,
|
||||
});
|
||||
useLayoutEffect(() => {
|
||||
// Update slide height when the component mounts
|
||||
if (currentSlideHeight === 0) {
|
||||
handleSlideChange(0);
|
||||
}
|
||||
}, [currentSlideHeight, handleSlideChange]);
|
||||
|
||||
// Handle swiping animations
|
||||
const bind = useDrag(
|
||||
({ swipe: [swipeX] }) => {
|
||||
handleSlideChange(swipeX * -1); // Invert swipe as swiping left loads the next slide.
|
||||
},
|
||||
{ pointer: { capture: false } },
|
||||
);
|
||||
const handlePrev = useCallback(() => {
|
||||
handleSlideChange(-1);
|
||||
// We're focusing on the wrapper as the child slides can potentially be inert.
|
||||
// Because of that, only the active slide can be focused anyway.
|
||||
wrapperRef.current?.focus();
|
||||
}, [handleSlideChange]);
|
||||
const handleNext = useCallback(() => {
|
||||
handleSlideChange(1);
|
||||
wrapperRef.current?.focus();
|
||||
}, [handleSlideChange]);
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
if (items.length === 0) {
|
||||
return emptyFallback;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...bind()}
|
||||
aria-roledescription='carousel'
|
||||
role='region'
|
||||
className={classNames(classNamePrefix, className)}
|
||||
{...wrapperProps}
|
||||
>
|
||||
<div className={`${classNamePrefix}__header`}>
|
||||
{children}
|
||||
{Pagination && items.length > 1 && (
|
||||
<Pagination
|
||||
current={slideIndex}
|
||||
max={items.length}
|
||||
onNext={handleNext}
|
||||
onPrev={handlePrev}
|
||||
className={`${classNamePrefix}__pagination`}
|
||||
messages={messages}
|
||||
{...paginationProps}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<animated.div
|
||||
className={`${classNamePrefix}__slides`}
|
||||
ref={wrapperRef}
|
||||
style={wrapperStyles}
|
||||
aria-label={intl.formatMessage(messages.slide, {
|
||||
current: slideIndex + 1,
|
||||
max: items.length,
|
||||
})}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{items.map((itemsProps, index) => (
|
||||
<CarouselSlideWrapper<SlideProps>
|
||||
item={itemsProps}
|
||||
renderItem={renderItem}
|
||||
observer={observerRef.current}
|
||||
index={index}
|
||||
key={`slide-${itemsProps.id}`}
|
||||
className={classNames(`${classNamePrefix}__slide`, slideClassName, {
|
||||
active: index === slideIndex,
|
||||
})}
|
||||
active={index === slideIndex}
|
||||
/>
|
||||
))}
|
||||
</animated.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type CarouselSlideWrapperProps<SlideProps extends CarouselSlideProps> = {
|
||||
observer: ResizeObserver | null;
|
||||
className: string;
|
||||
active: boolean;
|
||||
item: SlideProps;
|
||||
index: number;
|
||||
} & Pick<CarouselProps<SlideProps>, 'renderItem'>;
|
||||
|
||||
const CarouselSlideWrapper = <SlideProps extends CarouselSlideProps>({
|
||||
observer,
|
||||
className,
|
||||
active,
|
||||
renderItem,
|
||||
item,
|
||||
index,
|
||||
}: CarouselSlideWrapperProps<SlideProps>) => {
|
||||
const handleRef = useCallback(
|
||||
(instance: HTMLDivElement | null) => {
|
||||
if (observer && instance) {
|
||||
observer.observe(instance);
|
||||
}
|
||||
},
|
||||
[observer],
|
||||
);
|
||||
|
||||
const children = useMemo(
|
||||
() => renderItem(item, active, index),
|
||||
[renderItem, item, active, index],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={handleRef}
|
||||
className={className}
|
||||
role='group'
|
||||
aria-roledescription='slide'
|
||||
inert={active ? undefined : ''}
|
||||
data-index={index}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
import type { FC, MouseEventHandler } from 'react';
|
||||
|
||||
import type { MessageDescriptor } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||
|
||||
import { IconButton } from '../icon_button';
|
||||
|
||||
import type { MessageKeys } from './index';
|
||||
|
||||
export interface CarouselPaginationProps {
|
||||
onNext: MouseEventHandler;
|
||||
onPrev: MouseEventHandler;
|
||||
current: number;
|
||||
max: number;
|
||||
className?: string;
|
||||
messages: Record<MessageKeys, MessageDescriptor>;
|
||||
}
|
||||
|
||||
export const CarouselPagination: FC<CarouselPaginationProps> = ({
|
||||
onNext,
|
||||
onPrev,
|
||||
current,
|
||||
max,
|
||||
className = '',
|
||||
messages,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<div className={className}>
|
||||
<IconButton
|
||||
title={intl.formatMessage(messages.previous)}
|
||||
icon='chevron-left'
|
||||
iconComponent={ChevronLeftIcon}
|
||||
onClick={onPrev}
|
||||
/>
|
||||
<span aria-live='polite'>
|
||||
{intl.formatMessage(messages.current, {
|
||||
current: current + 1,
|
||||
max,
|
||||
sr: (chunk) => <span className='sr-only'>{chunk}</span>,
|
||||
})}
|
||||
</span>
|
||||
<IconButton
|
||||
title={intl.formatMessage(messages.next)}
|
||||
icon='chevron-right'
|
||||
iconComponent={ChevronRightIcon}
|
||||
onClick={onNext}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
.carousel {
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
touch-action: pan-y;
|
||||
|
||||
&__header {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
&__pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__slides {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
&__slide {
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export const ColumnBackButton: React.FC<{ onClick?: OnClickCallback }> = ({
|
||||
const handleClick = useHandleClick(onClick);
|
||||
|
||||
const component = (
|
||||
<button onClick={handleClick} className='column-back-button' type='button'>
|
||||
<button onClick={handleClick} className='column-back-button'>
|
||||
<Icon
|
||||
id='chevron-left'
|
||||
icon={ArrowBackIcon}
|
||||
|
||||
@@ -53,7 +53,6 @@ const BackButton: React.FC<{
|
||||
compact: onlyIcon,
|
||||
})}
|
||||
aria-label={intl.formatMessage(messages.back)}
|
||||
type='button'
|
||||
>
|
||||
<Icon
|
||||
id='chevron-left'
|
||||
@@ -173,7 +172,6 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||
<button
|
||||
className='text-btn column-header__setting-btn'
|
||||
onClick={handlePin}
|
||||
type='button'
|
||||
>
|
||||
<Icon id='times' icon={CloseIcon} />{' '}
|
||||
<FormattedMessage id='column_header.unpin' defaultMessage='Unpin' />
|
||||
@@ -187,7 +185,6 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||
aria-label={intl.formatMessage(messages.moveLeft)}
|
||||
className='icon-button column-header__setting-btn'
|
||||
onClick={handleMoveLeft}
|
||||
type='button'
|
||||
>
|
||||
<Icon id='chevron-left' icon={ChevronLeftIcon} />
|
||||
</button>
|
||||
@@ -196,7 +193,6 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||
aria-label={intl.formatMessage(messages.moveRight)}
|
||||
className='icon-button column-header__setting-btn'
|
||||
onClick={handleMoveRight}
|
||||
type='button'
|
||||
>
|
||||
<Icon id='chevron-right' icon={ChevronRightIcon} />
|
||||
</button>
|
||||
@@ -207,7 +203,6 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||
<button
|
||||
className='text-btn column-header__setting-btn'
|
||||
onClick={handlePin}
|
||||
type='button'
|
||||
>
|
||||
<Icon id='plus' icon={AddIcon} />{' '}
|
||||
<FormattedMessage id='column_header.pin' defaultMessage='Pin' />
|
||||
@@ -242,7 +237,6 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||
collapsed ? messages.show : messages.hide,
|
||||
)}
|
||||
onClick={handleToggleClick}
|
||||
type='button'
|
||||
>
|
||||
<i className='icon-with-badge'>
|
||||
<Icon
|
||||
@@ -265,11 +259,7 @@ export const ColumnHeader: React.FC<Props> = ({
|
||||
<>
|
||||
{backButton}
|
||||
|
||||
<button
|
||||
onClick={handleTitleClick}
|
||||
className='column-header__title'
|
||||
type='button'
|
||||
>
|
||||
<button onClick={handleTitleClick} className='column-header__title'>
|
||||
{!backButton && (
|
||||
<Icon
|
||||
id={icon}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState, useRef } from 'react';
|
||||
import { useCallback, useState, useEffect, useRef } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
@@ -12,15 +12,11 @@ export const ColumnSearchHeader: React.FC<{
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
// Reset the component when it turns from active to inactive.
|
||||
// [More on this pattern](https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)
|
||||
const [previousActive, setPreviousActive] = useState(active);
|
||||
if (active !== previousActive) {
|
||||
setPreviousActive(active);
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
setValue('');
|
||||
}
|
||||
}
|
||||
}, [active]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
||||
@@ -74,7 +74,7 @@ export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
|
||||
<button className='button' onClick={handleButtonClick} type='button'>
|
||||
<button className='button' onClick={handleButtonClick}>
|
||||
<Icon id='copy' icon={ContentCopyIcon} />{' '}
|
||||
{copied ? (
|
||||
<FormattedMessage id='copypaste.copied' defaultMessage='Copied' />
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useCallback, useState, useEffect } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
|
||||
import { useDismissible } from '../hooks/useDismissible';
|
||||
import { changeSetting } from 'flavours/glitch/actions/settings';
|
||||
import { bannerSettings } from 'flavours/glitch/settings';
|
||||
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||
|
||||
import { IconButton } from './icon_button';
|
||||
|
||||
@@ -14,12 +16,48 @@ const messages = defineMessages({
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const DismissableBanner: FC<Props> = ({ id, children }) => {
|
||||
export function useDismissableBannerState({ id }: Props) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const dismissed: boolean = useAppSelector((state) =>
|
||||
/* eslint-disable-next-line */
|
||||
state.settings.getIn(['dismissed_banners', id], false),
|
||||
);
|
||||
|
||||
const [isVisible, setIsVisible] = useState(
|
||||
!bannerSettings.get(id) && !dismissed,
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const dismiss = useCallback(() => {
|
||||
setIsVisible(false);
|
||||
bannerSettings.set(id, true);
|
||||
dispatch(changeSetting(['dismissed_banners', id], true));
|
||||
}, [id, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
// Store legacy localStorage setting on server
|
||||
if (!isVisible && !dismissed) {
|
||||
dispatch(changeSetting(['dismissed_banners', id], true));
|
||||
}
|
||||
}, [id, dispatch, isVisible, dismissed]);
|
||||
|
||||
return {
|
||||
wasDismissed: !isVisible,
|
||||
dismiss,
|
||||
};
|
||||
}
|
||||
|
||||
export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
|
||||
id,
|
||||
children,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { wasDismissed, dismiss } = useDismissible(id);
|
||||
const { wasDismissed, dismiss } = useDismissableBannerState({
|
||||
id,
|
||||
});
|
||||
|
||||
if (wasDismissed) {
|
||||
return null;
|
||||
|
||||
@@ -109,7 +109,7 @@ export const Dropdown: FC<
|
||||
placement='bottom-start'
|
||||
onHide={handleClose}
|
||||
flip
|
||||
target={buttonRef}
|
||||
target={buttonRef.current}
|
||||
popperConfig={{
|
||||
strategy: 'fixed',
|
||||
modifiers: [matchWidth],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user