Compare commits
No commits in common. "main" and "origin/v4.1.1" have entirely different histories.
main
...
origin/v4.
225
.circleci/config.yml
Normal file
225
.circleci/config.yml
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
version: 2.1
|
||||||
|
|
||||||
|
orbs:
|
||||||
|
ruby: circleci/ruby@2.0.0
|
||||||
|
node: circleci/node@5.0.3
|
||||||
|
|
||||||
|
executors:
|
||||||
|
default:
|
||||||
|
parameters:
|
||||||
|
ruby-version:
|
||||||
|
type: string
|
||||||
|
docker:
|
||||||
|
- image: cimg/ruby:<< parameters.ruby-version >>
|
||||||
|
environment:
|
||||||
|
BUNDLE_JOBS: 3
|
||||||
|
BUNDLE_RETRY: 3
|
||||||
|
CONTINUOUS_INTEGRATION: true
|
||||||
|
DB_HOST: localhost
|
||||||
|
DB_USER: root
|
||||||
|
DISABLE_SIMPLECOV: true
|
||||||
|
RAILS_ENV: test
|
||||||
|
- image: cimg/postgres:14.5
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: root
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
- image: cimg/redis:7.0
|
||||||
|
|
||||||
|
commands:
|
||||||
|
install-system-dependencies:
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
name: Install system dependencies
|
||||||
|
command: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libicu-dev libidn11-dev
|
||||||
|
install-ruby-dependencies:
|
||||||
|
parameters:
|
||||||
|
ruby-version:
|
||||||
|
type: string
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
command: |
|
||||||
|
bundle config clean 'true'
|
||||||
|
bundle config frozen 'true'
|
||||||
|
bundle config without 'development production'
|
||||||
|
name: Set bundler settings
|
||||||
|
- ruby/install-deps:
|
||||||
|
bundler-version: '2.3.26'
|
||||||
|
key: ruby<< parameters.ruby-version >>-gems-v1
|
||||||
|
wait-db:
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
command: dockerize -wait tcp://localhost:5432 -wait tcp://localhost:6379 -timeout 1m
|
||||||
|
name: Wait for PostgreSQL and Redis
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
docker:
|
||||||
|
- image: cimg/ruby:3.0-node
|
||||||
|
environment:
|
||||||
|
RAILS_ENV: test
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- install-system-dependencies
|
||||||
|
- install-ruby-dependencies:
|
||||||
|
ruby-version: '3.0'
|
||||||
|
- node/install-packages:
|
||||||
|
cache-version: v1
|
||||||
|
pkg-manager: yarn
|
||||||
|
- run:
|
||||||
|
command: |
|
||||||
|
export NODE_OPTIONS=--openssl-legacy-provider
|
||||||
|
./bin/rails assets:precompile
|
||||||
|
name: Precompile assets
|
||||||
|
- persist_to_workspace:
|
||||||
|
paths:
|
||||||
|
- public/assets
|
||||||
|
- public/packs-test
|
||||||
|
root: .
|
||||||
|
|
||||||
|
test:
|
||||||
|
parameters:
|
||||||
|
ruby-version:
|
||||||
|
type: string
|
||||||
|
executor:
|
||||||
|
name: default
|
||||||
|
ruby-version: << parameters.ruby-version >>
|
||||||
|
environment:
|
||||||
|
ALLOW_NOPAM: true
|
||||||
|
PAM_ENABLED: true
|
||||||
|
PAM_DEFAULT_SERVICE: pam_test
|
||||||
|
PAM_CONTROLLED_SERVICE: pam_test_controlled
|
||||||
|
parallelism: 4
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- install-system-dependencies
|
||||||
|
- run:
|
||||||
|
command: sudo apt-get install -y ffmpeg imagemagick libpam-dev
|
||||||
|
name: Install additional system dependencies
|
||||||
|
- run:
|
||||||
|
command: bundle config with 'pam_authentication'
|
||||||
|
name: Enable PAM authentication
|
||||||
|
- install-ruby-dependencies:
|
||||||
|
ruby-version: << parameters.ruby-version >>
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- wait-db
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:create db:schema:load db:seed
|
||||||
|
name: Load database schema
|
||||||
|
- ruby/rspec-test
|
||||||
|
|
||||||
|
test-migrations:
|
||||||
|
executor:
|
||||||
|
name: default
|
||||||
|
ruby-version: '3.0'
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- install-system-dependencies
|
||||||
|
- install-ruby-dependencies:
|
||||||
|
ruby-version: '3.0'
|
||||||
|
- wait-db
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:create
|
||||||
|
name: Create database
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate VERSION=20171010025614
|
||||||
|
name: Run migrations up to v2.0.0
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails tests:migrations:populate_v2
|
||||||
|
name: Populate database with test data
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate VERSION=20180514140000
|
||||||
|
name: Run migrations up to v2.4.0
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails tests:migrations:populate_v2_4
|
||||||
|
name: Populate database with test data
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate VERSION=20180707154237
|
||||||
|
name: Run migrations up to v2.4.3
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails tests:migrations:populate_v2_4_3
|
||||||
|
name: Populate database with test data
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate
|
||||||
|
name: Run all remaining migrations
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails tests:migrations:check_database
|
||||||
|
name: Check migration result
|
||||||
|
|
||||||
|
test-two-step-migrations:
|
||||||
|
executor:
|
||||||
|
name: default
|
||||||
|
ruby-version: '3.0'
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- install-system-dependencies
|
||||||
|
- install-ruby-dependencies:
|
||||||
|
ruby-version: '3.0'
|
||||||
|
- wait-db
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:create
|
||||||
|
name: Create database
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate VERSION=20171010025614
|
||||||
|
name: Run migrations up to v2.0.0
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails tests:migrations:populate_v2
|
||||||
|
name: Populate database with test data
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate VERSION=20180514140000
|
||||||
|
name: Run pre-deployment migrations up to v2.4.0
|
||||||
|
environment:
|
||||||
|
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails tests:migrations:populate_v2_4
|
||||||
|
name: Populate database with test data
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate VERSION=20180707154237
|
||||||
|
name: Run migrations up to v2.4.3
|
||||||
|
environment:
|
||||||
|
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails tests:migrations:populate_v2_4_3
|
||||||
|
name: Populate database with test data
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate
|
||||||
|
name: Run all remaining pre-deployment migrations
|
||||||
|
environment:
|
||||||
|
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate
|
||||||
|
name: Run all post-deployment migrations
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails tests:migrations:check_database
|
||||||
|
name: Check migration result
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
build-and-test:
|
||||||
|
jobs:
|
||||||
|
- build
|
||||||
|
- test:
|
||||||
|
matrix:
|
||||||
|
parameters:
|
||||||
|
ruby-version:
|
||||||
|
- '2.7'
|
||||||
|
- '3.0'
|
||||||
|
name: test-ruby<< matrix.ruby-version >>
|
||||||
|
requires:
|
||||||
|
- build
|
||||||
|
- test-migrations:
|
||||||
|
requires:
|
||||||
|
- build
|
||||||
|
- test-two-step-migrations:
|
||||||
|
requires:
|
||||||
|
- build
|
||||||
|
- node/run:
|
||||||
|
cache-version: v1
|
||||||
|
name: test-webui
|
||||||
|
pkg-manager: yarn
|
||||||
|
requires:
|
||||||
|
- build
|
||||||
|
version: '16.18'
|
||||||
|
yarn-run: test:jest
|
30
.github/dependabot.yml
vendored
Normal file
30
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: '/'
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
open-pull-requests-limit: 99
|
||||||
|
allow:
|
||||||
|
- dependency-type: direct
|
||||||
|
|
||||||
|
- package-ecosystem: bundler
|
||||||
|
directory: '/'
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
open-pull-requests-limit: 99
|
||||||
|
allow:
|
||||||
|
- dependency-type: direct
|
||||||
|
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: '/'
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
open-pull-requests-limit: 99
|
||||||
|
allow:
|
||||||
|
- dependency-type: direct
|
92
.github/workflows/build-container-image.yml
vendored
92
.github/workflows/build-container-image.yml
vendored
@ -1,92 +0,0 @@
|
|||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
platforms:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
cache:
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
use_native_arm64_builder:
|
|
||||||
type: boolean
|
|
||||||
push_to_images:
|
|
||||||
type: string
|
|
||||||
flavor:
|
|
||||||
type: string
|
|
||||||
tags:
|
|
||||||
type: string
|
|
||||||
labels:
|
|
||||||
type: string
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-image:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- uses: docker/setup-qemu-action@v2
|
|
||||||
if: contains(inputs.platforms, 'linux/arm64') && !inputs.use_native_arm64_builder
|
|
||||||
|
|
||||||
- uses: docker/setup-buildx-action@v2
|
|
||||||
id: buildx
|
|
||||||
if: ${{ !(inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')) }}
|
|
||||||
|
|
||||||
- name: Start a local Docker Builder
|
|
||||||
if: inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')
|
|
||||||
run: |
|
|
||||||
docker run --rm -d --name buildkitd -p 1234:1234 --privileged moby/buildkit:latest --addr tcp://0.0.0.0:1234
|
|
||||||
|
|
||||||
- uses: docker/setup-buildx-action@v2
|
|
||||||
id: buildx-native
|
|
||||||
if: inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')
|
|
||||||
with:
|
|
||||||
driver: remote
|
|
||||||
endpoint: tcp://localhost:1234
|
|
||||||
platforms: linux/amd64
|
|
||||||
append: |
|
|
||||||
- endpoint: tcp://${{ vars.DOCKER_BUILDER_HETZNER_ARM64_01_HOST }}:13865
|
|
||||||
platforms: linux/arm64
|
|
||||||
name: mastodon-docker-builder-arm64-01
|
|
||||||
driver-opts:
|
|
||||||
- servername=mastodon-docker-builder-arm64-01
|
|
||||||
env:
|
|
||||||
BUILDER_NODE_1_AUTH_TLS_CACERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CACERT }}
|
|
||||||
BUILDER_NODE_1_AUTH_TLS_CERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CERT }}
|
|
||||||
BUILDER_NODE_1_AUTH_TLS_KEY: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_KEY }}
|
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
|
||||||
if: contains(inputs.push_to_images, 'tootsuite')
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Log in to the Github Container registry
|
|
||||||
if: contains(inputs.push_to_images, 'ghcr.io')
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- uses: docker/metadata-action@v4
|
|
||||||
id: meta
|
|
||||||
if: ${{ inputs.push_to_images != '' }}
|
|
||||||
with:
|
|
||||||
images: ${{ inputs.push_to_images }}
|
|
||||||
flavor: ${{ inputs.flavor }}
|
|
||||||
tags: ${{ inputs.tags }}
|
|
||||||
labels: ${{ inputs.labels }}
|
|
||||||
|
|
||||||
- uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: ${{ inputs.platforms }}
|
|
||||||
provenance: false
|
|
||||||
builder: ${{ steps.buildx.outputs.name || steps.buildx-native.outputs.name }}
|
|
||||||
push: ${{ inputs.push_to_images != '' }}
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: ${{ inputs.cache && 'type=gha' || '' }}
|
|
||||||
cache-to: ${{ inputs.cache && 'type=gha,mode=max' || '' }}
|
|
70
.github/workflows/build-image.yml
vendored
Normal file
70
.github/workflows/build-image.yml
vendored
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
name: Build container image
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/build-image.yml
|
||||||
|
- Dockerfile
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: hadolint/hadolint-action@v3.1.0
|
||||||
|
- uses: docker/setup-qemu-action@v2
|
||||||
|
- uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
if: github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request'
|
||||||
|
|
||||||
|
- name: Log in to the Github Container registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
if: github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request'
|
||||||
|
|
||||||
|
- uses: docker/metadata-action@v4
|
||||||
|
id: meta
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
tootsuite/mastodon
|
||||||
|
ghcr.io/mastodon/mastodon
|
||||||
|
flavor: |
|
||||||
|
latest=auto
|
||||||
|
tags: |
|
||||||
|
type=edge,branch=main
|
||||||
|
type=pep440,pattern={{raw}}
|
||||||
|
type=pep440,pattern=v{{major}}.{{minor}}
|
||||||
|
type=ref,event=pr
|
||||||
|
|
||||||
|
- uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
provenance: false
|
||||||
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
|
push: ${{ github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
27
.github/workflows/build-releases.yml
vendored
27
.github/workflows/build-releases.yml
vendored
@ -1,27 +0,0 @@
|
|||||||
name: Build container release images
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- '*'
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-image:
|
|
||||||
uses: ./.github/workflows/build-container-image.yml
|
|
||||||
with:
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
use_native_arm64_builder: true
|
|
||||||
push_to_images: |
|
|
||||||
tootsuite/mastodon
|
|
||||||
ghcr.io/mastodon/mastodon
|
|
||||||
# Do not use cache when building releases, so apt update is always ran and the release always contain the latest packages
|
|
||||||
cache: false
|
|
||||||
flavor: |
|
|
||||||
latest=false
|
|
||||||
tags: |
|
|
||||||
type=pep440,pattern={{raw}}
|
|
||||||
type=pep440,pattern=v{{major}}.{{minor}}
|
|
||||||
secrets: inherit
|
|
41
.github/workflows/lint-ruby.yml
vendored
Normal file
41
.github/workflows/lint-ruby.yml
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
name: Ruby Linting
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- 'dependabot/**'
|
||||||
|
paths:
|
||||||
|
- 'Gemfile*'
|
||||||
|
- '.rubocop.yml'
|
||||||
|
- '**/*.rb'
|
||||||
|
- '**/*.rake'
|
||||||
|
- '.github/workflows/lint-ruby.yml'
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'Gemfile*'
|
||||||
|
- '.rubocop.yml'
|
||||||
|
- '**/*.rb'
|
||||||
|
- '**/*.rake'
|
||||||
|
- '.github/workflows/lint-ruby.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set-up RuboCop Problem Mathcher
|
||||||
|
uses: r7kamura/rubocop-problem-matchers-action@v1
|
||||||
|
|
||||||
|
- name: Run rubocop
|
||||||
|
uses: github/super-linter@v4
|
||||||
|
env:
|
||||||
|
DEFAULT_BRANCH: main
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
LINTER_RULES_PATH: .
|
||||||
|
RUBY_CONFIG_FILE: .rubocop.yml
|
||||||
|
VALIDATE_ALL_CODEBASE: false
|
||||||
|
VALIDATE_RUBY: true
|
15
.github/workflows/test-image-build.yml
vendored
15
.github/workflows/test-image-build.yml
vendored
@ -1,15 +0,0 @@
|
|||||||
name: Test container image build
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-image:
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
uses: ./.github/workflows/build-container-image.yml
|
|
||||||
with:
|
|
||||||
platforms: linux/amd64 # Testing only on native platform so it is performant
|
|
153
.github/workflows/test-ruby.yml
vendored
153
.github/workflows/test-ruby.yml
vendored
@ -1,153 +0,0 @@
|
|||||||
name: Ruby Testing
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches-ignore:
|
|
||||||
- 'dependabot/**'
|
|
||||||
- 'renovate/**'
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
env:
|
|
||||||
BUNDLE_CLEAN: true
|
|
||||||
BUNDLE_FROZEN: true
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: true
|
|
||||||
matrix:
|
|
||||||
mode:
|
|
||||||
- production
|
|
||||||
- test
|
|
||||||
env:
|
|
||||||
RAILS_ENV: ${{ matrix.mode }}
|
|
||||||
BUNDLE_WITH: ${{ matrix.mode }}
|
|
||||||
OTP_SECRET: precompile_placeholder
|
|
||||||
SECRET_KEY_BASE: precompile_placeholder
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Node.js
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
cache: yarn
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
|
|
||||||
- name: Install native Ruby dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libicu-dev libidn11-dev
|
|
||||||
|
|
||||||
- name: Set up bundler cache
|
|
||||||
uses: ruby/setup-ruby@v1
|
|
||||||
with:
|
|
||||||
ruby-version: .ruby-version
|
|
||||||
bundler-cache: true
|
|
||||||
|
|
||||||
- run: yarn --frozen-lockfile --production
|
|
||||||
- name: Precompile assets
|
|
||||||
# Previously had set this, but it's not supported
|
|
||||||
# export NODE_OPTIONS=--openssl-legacy-provider
|
|
||||||
run: |-
|
|
||||||
./bin/rails assets:precompile
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
if: matrix.mode == 'test'
|
|
||||||
with:
|
|
||||||
path: |-
|
|
||||||
./public/assets
|
|
||||||
./public/packs-test
|
|
||||||
name: ${{ github.sha }}
|
|
||||||
retention-days: 0
|
|
||||||
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
needs:
|
|
||||||
- build
|
|
||||||
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:14-alpine
|
|
||||||
env:
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
options: >-
|
|
||||||
--health-cmd pg_isready
|
|
||||||
--health-interval 10s
|
|
||||||
--health-timeout 5s
|
|
||||||
--health-retries 5
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
options: >-
|
|
||||||
--health-cmd "redis-cli ping"
|
|
||||||
--health-interval 10s
|
|
||||||
--health-timeout 5s
|
|
||||||
--health-retries 5
|
|
||||||
ports:
|
|
||||||
- 6379:6379
|
|
||||||
|
|
||||||
env:
|
|
||||||
DB_HOST: localhost
|
|
||||||
DB_USER: postgres
|
|
||||||
DB_PASS: postgres
|
|
||||||
DISABLE_SIMPLECOV: true
|
|
||||||
RAILS_ENV: test
|
|
||||||
ALLOW_NOPAM: true
|
|
||||||
PAM_ENABLED: true
|
|
||||||
PAM_DEFAULT_SERVICE: pam_test
|
|
||||||
PAM_CONTROLLED_SERVICE: pam_test_controlled
|
|
||||||
OIDC_ENABLED: true
|
|
||||||
OIDC_SCOPE: read
|
|
||||||
SAML_ENABLED: true
|
|
||||||
CAS_ENABLED: true
|
|
||||||
BUNDLE_WITH: 'pam_authentication test'
|
|
||||||
CI_JOBS: ${{ matrix.ci_job }}/4
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
ruby-version:
|
|
||||||
- '.ruby-version'
|
|
||||||
ci_job:
|
|
||||||
- 1
|
|
||||||
- 2
|
|
||||||
- 3
|
|
||||||
- 4
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
path: './public'
|
|
||||||
name: ${{ github.sha }}
|
|
||||||
|
|
||||||
- name: Update package index
|
|
||||||
run: sudo apt-get update
|
|
||||||
|
|
||||||
- name: Install native Ruby dependencies
|
|
||||||
run: sudo apt-get install -y libicu-dev libidn11-dev
|
|
||||||
|
|
||||||
- name: Install additional system dependencies
|
|
||||||
run: sudo apt-get install -y ffmpeg imagemagick libpam-dev
|
|
||||||
|
|
||||||
- name: Set up bundler cache
|
|
||||||
uses: ruby/setup-ruby@v1
|
|
||||||
with:
|
|
||||||
ruby-version: ${{ matrix.ruby-version}}
|
|
||||||
bundler-cache: true
|
|
||||||
|
|
||||||
- name: Load database schema
|
|
||||||
run: './bin/rails db:create db:schema:load db:seed'
|
|
||||||
|
|
||||||
- run: bin/rspec
|
|
@ -1 +1 @@
|
|||||||
3.0.6
|
3.0.4
|
||||||
|
293
CHANGELOG.md
293
CHANGELOG.md
@ -3,299 +3,6 @@ Changelog
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## [4.1.18] - 2024-07-04
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Fix incorrect permission checking on multiple API endpoints ([GHSA-58x8-3qxw-6hm7](https://github.com/mastodon/mastodon/security/advisories/GHSA-58x8-3qxw-6hm7))
|
|
||||||
- Fix incorrect authorship checking when processing some activities (CVE-2024-37903, [GHSA-xjvf-fm67-4qc3](https://github.com/mastodon/mastodon/security/advisories/GHSA-xjvf-fm67-4qc3))
|
|
||||||
- Fix ongoing streaming sessions not being invalidated when application tokens get revoked ([GHSA-vp5r-5pgw-jwqx](https://github.com/mastodon/mastodon/security/advisories/GHSA-vp5r-5pgw-jwqx))
|
|
||||||
- Update dependencies
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Change preview cards generation to skip unusually long URLs ([oneiros](https://github.com/mastodon/mastodon/pull/30854))
|
|
||||||
- Change search modifiers to be case-insensitive ([Gargron](https://github.com/mastodon/mastodon/pull/30865))
|
|
||||||
- Change `STATSD_ADDR` handling to emit a warning rather than crashing if the address is unreachable ([timothyjrogers](https://github.com/mastodon/mastodon/pull/30691))
|
|
||||||
- Change PWA start URL from `/home` to `/` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27377))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix scheduled statuses scheduled in less than 5 minutes being immediately published ([danielmbrasil](https://github.com/mastodon/mastodon/pull/30584))
|
|
||||||
- Fix encoding detection for link cards ([oneiros](https://github.com/mastodon/mastodon/pull/30780))
|
|
||||||
- Fix `/admin/accounts/:account_id/statuses/:id` for edited posts with media attachments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30819))
|
|
||||||
|
|
||||||
## [4.1.17] - 2024-05-30
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Update dependencies
|
|
||||||
- Fix private mention filtering ([GHSA-5fq7-3p3j-9vrf](https://github.com/mastodon/mastodon/security/advisories/GHSA-5fq7-3p3j-9vrf))
|
|
||||||
- Fix password change endpoint not being rate-limited ([GHSA-q3rg-xx5v-4mxh](https://github.com/mastodon/mastodon/security/advisories/GHSA-q3rg-xx5v-4mxh))
|
|
||||||
- Add hardening around rate-limit bypass ([GHSA-c2r5-cfqr-c553](https://github.com/mastodon/mastodon/security/advisories/GHSA-c2r5-cfqr-c553))
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Add fallback redirection when getting a webfinger query `WEB_DOMAIN@WEB_DOMAIN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28592))
|
|
||||||
- Add `digest` attribute to `Admin::DomainBlock` entity in REST API ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/29092))
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- Remove superfluous application-level caching in some controllers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29862))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix leaking Elasticsearch connections in Sidekiq processes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30450))
|
|
||||||
- Fix language of remote posts not being recognized when using unusual casing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30403))
|
|
||||||
- Fix off-by-one in `tootctl media` commands ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30306))
|
|
||||||
- Fix removal of allowed domains (in `LIMITED_FEDERATION_MODE`) not being recorded in the audit log ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/30125))
|
|
||||||
- Fix not being able to block a subdomain of an already-blocked domain through the API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30119))
|
|
||||||
- Fix `Idempotency-Key` being ignored when scheduling a post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30084))
|
|
||||||
- Fix crash when supplying the `FFMPEG_BINARY` environment variable ([timothyjrogers](https://github.com/mastodon/mastodon/pull/30022))
|
|
||||||
- Fix improper email address validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29838))
|
|
||||||
- Fix results/query in `api/v1/featured_tags/suggestions` ([mjankowski](https://github.com/mastodon/mastodon/pull/29597))
|
|
||||||
- Fix unblocking internationalized domain names under certain conditions ([tribela](https://github.com/mastodon/mastodon/pull/29530))
|
|
||||||
- Fix admin account created by `mastodon:setup` not being auto-approved ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29379))
|
|
||||||
- Fix reference to non-existent var in CLI maintenance command ([mjankowski](https://github.com/mastodon/mastodon/pull/28363))
|
|
||||||
|
|
||||||
## [4.1.16] - 2024-02-23
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Add hourly task to automatically require approval for new registrations in the absence of moderators ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29318), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/29355))
|
|
||||||
In order to prevent future abandoned Mastodon servers from being used for spam, harassment and other malicious activity, Mastodon will now automatically switch new user registrations to require moderator approval whenever they are left open and no activity (including non-moderation actions from apps) from any logged-in user with permission to access moderation reports has been detected in a full week.
|
|
||||||
When this happens, users with the permission to change server settings will receive an email notification.
|
|
||||||
This feature is disabled when `EMAIL_DOMAIN_ALLOWLIST` is used, and can also be disabled with `DISABLE_AUTOMATIC_SWITCHING_TO_APPROVED_REGISTRATIONS=true`.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Change registrations to be closed by default on new installations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29280))
|
|
||||||
If you are running a server and never changed your registrations mode from the default, updating will automatically close your registrations.
|
|
||||||
Simply re-enable them through the administration interface or using `tootctl settings registrations open` if you want to enable them again.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix processing of remote ActivityPub actors making use of `Link` objects as `Image` `url` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29335))
|
|
||||||
- Fix link verifications when page size exceeds 1MB ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29358))
|
|
||||||
|
|
||||||
## [4.1.15] - 2024-02-16
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix OmniAuth tests and edge cases in error handling ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29201), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/29207))
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Fix insufficient checking of remote posts ([GHSA-jhrq-qvrm-qr36](https://github.com/mastodon/mastodon/security/advisories/GHSA-jhrq-qvrm-qr36))
|
|
||||||
|
|
||||||
## [4.1.14] - 2024-02-14
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Update the `sidekiq-unique-jobs` dependency (see [GHSA-cmh9-rx85-xj38](https://github.com/mhenrixon/sidekiq-unique-jobs/security/advisories/GHSA-cmh9-rx85-xj38))
|
|
||||||
In addition, we have disabled the web interface for `sidekiq-unique-jobs` out of caution.
|
|
||||||
If you need it, you can re-enable it by setting `ENABLE_SIDEKIQ_UNIQUE_JOBS_UI=true`.
|
|
||||||
If you only need to clear all locks, you can now use `bundle exec rake sidekiq_unique_jobs:delete_all_locks`.
|
|
||||||
- Update the `nokogiri` dependency (see [GHSA-xc9x-jj77-9p9j](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-xc9x-jj77-9p9j))
|
|
||||||
- Disable administrative Doorkeeper routes ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/29187))
|
|
||||||
- Fix ongoing streaming sessions not being invalidated when applications get deleted in some cases ([GHSA-7w3c-p9j8-mq3x](https://github.com/mastodon/mastodon/security/advisories/GHSA-7w3c-p9j8-mq3x))
|
|
||||||
In some rare cases, the streaming server was not notified of access tokens revocation on application deletion.
|
|
||||||
- Change external authentication behavior to never reattach a new identity to an existing user by default ([GHSA-vm39-j3vx-pch3](https://github.com/mastodon/mastodon/security/advisories/GHSA-vm39-j3vx-pch3))
|
|
||||||
Up until now, Mastodon has allowed new identities from external authentication providers to attach to an existing local user based on their verified e-mail address.
|
|
||||||
This allowed upgrading users from a database-stored password to an external authentication provider, or move from one authentication provider to another.
|
|
||||||
However, this behavior may be unexpected, and means that when multiple authentication providers are configured, the overall security would be that of the least secure authentication provider.
|
|
||||||
For these reasons, this behavior is now locked under the `ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH` environment variable.
|
|
||||||
In addition, regardless of this environment variable, Mastodon will refuse to attach two identities from the same authentication provider to the same account.
|
|
||||||
|
|
||||||
## [4.1.13] - 2024-02-01
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Fix insufficient origin validation (CVE-2024-23832, [GHSA-3fjr-858r-92rw](https://github.com/mastodon/mastodon/security/advisories/GHSA-3fjr-858r-92rw))
|
|
||||||
|
|
||||||
## [4.1.12] - 2024-01-24
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix error when processing remote files with unusually long names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28823))
|
|
||||||
- Fix processing of compacted single-item JSON-LD collections ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28816))
|
|
||||||
- Retry 401 errors on replies fetching ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/28788))
|
|
||||||
- Fix `RecordNotUnique` errors in LinkCrawlWorker ([tribela](https://github.com/mastodon/mastodon/pull/28748))
|
|
||||||
- Fix Mastodon not correctly processing HTTP Signatures with query strings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28443), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28476))
|
|
||||||
- Fix potential redirection loop of streaming endpoint ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28665))
|
|
||||||
- Fix streaming API redirection ignoring the port of `streaming_api_base_url` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28558))
|
|
||||||
- Fix `Undo Announce` activity not being sent to non-follower authors ([MitarashiDango](https://github.com/mastodon/mastodon/pull/18482))
|
|
||||||
- Fix `LinkCrawlWorker` error when encountering empty OEmbed response ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28268))
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Add rate-limit of TOTP authentication attempts at controller level ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28801))
|
|
||||||
|
|
||||||
## [4.1.11] - 2023-12-04
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Change GIF max matrix size error to explicitly mention GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27927))
|
|
||||||
- Change `Follow` activities delivery to bypass availability check ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/27586))
|
|
||||||
- Change Content-Security-Policy to be tighter on media paths ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26889))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix incoming status creation date not being restricted to standard ISO8601 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27655), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28081))
|
|
||||||
- Fix posts from force-sensitized accounts being able to trend ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27620))
|
|
||||||
- Fix processing LDSigned activities from actors with unknown public keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27474))
|
|
||||||
- Fix error and incorrect URLs in `/api/v1/accounts/:id/featured_tags` for remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27459))
|
|
||||||
- Fix report processing notice not mentioning the report number when performing a custom action ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27442))
|
|
||||||
- Fix some link anchors being recognized as hashtags ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27271), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27584))
|
|
||||||
|
|
||||||
## [4.1.10] - 2023-10-10
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Change some worker lock TTLs to be shorter-lived ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27246))
|
|
||||||
- Change user archive export allowed period from 7 days to 6 days ([suddjian](https://github.com/mastodon/mastodon/pull/27200))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix mentions being matched in some URL query strings ([mjankowski](https://github.com/mastodon/mastodon/pull/25656))
|
|
||||||
- Fix multiple instances of the trend refresh scheduler sometimes running at once ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27253))
|
|
||||||
- Fix importer returning negative row estimates ([jgillich](https://github.com/mastodon/mastodon/pull/27258))
|
|
||||||
- Fix filtering audit log for entries about disabling 2FA ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27186))
|
|
||||||
- Fix tIME chunk not being properly removed from PNG uploads ([TheEssem](https://github.com/mastodon/mastodon/pull/27111))
|
|
||||||
- Fix inefficient queries in “Follows and followers” as well as several admin pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27116), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27306))
|
|
||||||
|
|
||||||
## [4.1.9] - 2023-09-20
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix post translation erroring out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26990))
|
|
||||||
|
|
||||||
## [4.1.8] - 2023-09-19
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix post edits not being forwarded as expected ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26936))
|
|
||||||
- Fix moderator rights inconsistencies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26729))
|
|
||||||
- Fix crash when encountering invalid URL ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26814))
|
|
||||||
- Fix cached posts including stale stats ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26409))
|
|
||||||
- Fix uploading of video files for which `ffprobe` reports `0/0` average framerate ([NicolaiSoeborg](https://github.com/mastodon/mastodon/pull/26500))
|
|
||||||
- Fix unexpected audio stream transcoding when uploaded video is eligible to passthrough ([yufushiro](https://github.com/mastodon/mastodon/pull/26608))
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Fix missing HTML sanitization in translation API (CVE-2023-42452)
|
|
||||||
- Fix incorrect domain name normalization (CVE-2023-42451)
|
|
||||||
|
|
||||||
## [4.1.7] - 2023-09-05
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Change remote report processing to accept reports with long comments, but truncate them ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25028))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- **Fix blocking subdomains of an already-blocked domain** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26392))
|
|
||||||
- Fix `/api/v1/timelines/tag/:hashtag` allowing for unauthenticated access when public preview is disabled ([danielmbrasil](https://github.com/mastodon/mastodon/pull/26237))
|
|
||||||
- Fix inefficiencies in `PlainTextFormatter` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26727))
|
|
||||||
|
|
||||||
## [4.1.6] - 2023-07-31
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix memory leak in streaming server ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26228))
|
|
||||||
- Fix wrong filters sometimes applying in streaming ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26159), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26213), [renchap](https://github.com/mastodon/mastodon/pull/26233))
|
|
||||||
- Fix incorrect connect timeout in outgoing requests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26116))
|
|
||||||
|
|
||||||
## [4.1.5] - 2023-07-21
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Add check preventing Sidekiq workers from running with Makara configured ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25850))
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Change request timeout handling to use a longer deadline ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26055))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix moderation interface for remote instances with a .zip TLD ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25886))
|
|
||||||
- Fix remote accounts being possibly persisted to database with incomplete protocol values ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25886))
|
|
||||||
- Fix trending publishers table not rendering correctly on narrow screens ([vmstan](https://github.com/mastodon/mastodon/pull/25945))
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Fix CSP headers being unintentionally wide ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26105))
|
|
||||||
|
|
||||||
## [4.1.4] - 2023-07-07
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix branding:generate_app_icons failing because of disallowed ICO coder ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25794))
|
|
||||||
- Fix crash in admin interface when viewing a remote user with verified links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25796))
|
|
||||||
- Fix processing of media files with unusual names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25788))
|
|
||||||
|
|
||||||
## [4.1.3] - 2023-07-06
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Add fallback redirection when getting a webfinger query `LOCAL_DOMAIN@LOCAL_DOMAIN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23600))
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Change OpenGraph-based embeds to allow fullscreen ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25058))
|
|
||||||
- Change AccessTokensVacuum to also delete expired tokens ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24868))
|
|
||||||
- Change profile updates to be sent to recently-mentioned servers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24852))
|
|
||||||
- Change automatic post deletion thresholds and load detection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24614))
|
|
||||||
- Change `/api/v1/statuses/:id/history` to always return at least one item ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25510))
|
|
||||||
- Change auto-linking to allow carets in URL query params ([renchap](https://github.com/mastodon/mastodon/pull/25216))
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- Remove invalid `X-Frame-Options: ALLOWALL` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25070))
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix wrong view being displayed when a webhook fails validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25464))
|
|
||||||
- Fix soft-deleted post cleanup scheduler overwhelming the streaming server ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25519))
|
|
||||||
- Fix incorrect pagination headers in `/api/v2/admin/accounts` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25477))
|
|
||||||
- Fix multiple inefficiencies in automatic post cleanup worker ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24607), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24785), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24840))
|
|
||||||
- Fix performance of streaming by parsing message JSON once ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25278), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25361))
|
|
||||||
- Fix CSP headers when `S3_ALIAS_HOST` includes a path component ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25273))
|
|
||||||
- Fix `tootctl accounts approve --number N` not approving N earliest registrations ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24605))
|
|
||||||
- Fix reports not being closed when performing batch suspensions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24988))
|
|
||||||
- Fix being able to vote on your own polls ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25015))
|
|
||||||
- Fix race condition when reblogging a status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25016))
|
|
||||||
- Fix “Authorized applications” inefficiently and incorrectly getting last use date ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25060))
|
|
||||||
- Fix “Authorized applications” crashing when listing apps with certain admin API scopes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25713))
|
|
||||||
- Fix multiple N+1s in ConversationsController ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25134), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25399), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25499))
|
|
||||||
- Fix user archive takeouts when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24431))
|
|
||||||
- Fix searching for remote content by URL not working under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25637))
|
|
||||||
- Fix inefficiencies in indexing content for search ([VyrCossont](https://github.com/mastodon/mastodon/pull/24285), [VyrCossont](https://github.com/mastodon/mastodon/pull/24342))
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Add finer permission requirements for managing webhooks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25463))
|
|
||||||
- Update dependencies
|
|
||||||
- Add hardening headers for user-uploaded files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25756))
|
|
||||||
- Fix verified links possibly hiding important parts of the URL (CVE-2023-36462)
|
|
||||||
- Fix timeout handling of outbound HTTP requests (CVE-2023-36461)
|
|
||||||
- Fix arbitrary file creation through media processing (CVE-2023-36460)
|
|
||||||
- Fix possible XSS in preview cards (CVE-2023-36459)
|
|
||||||
|
|
||||||
## [4.1.2] - 2023-04-04
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix crash in `tootctl` commands making use of parallelization when Elasticsearch is enabled ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24182), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24377))
|
|
||||||
- Fix crash in `db:setup` when Elasticsearch is enabled ([rrgeorge](https://github.com/mastodon/mastodon/pull/24302))
|
|
||||||
- Fix user archive takeout when using OpenStack Swift or S3 providers with no ACL support ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24200))
|
|
||||||
- Fix invalid/expired invites being processed on sign-up ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24337))
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Update Ruby to 3.0.6 due to ReDoS vulnerabilities ([saizai](https://github.com/mastodon/mastodon/pull/24334))
|
|
||||||
- Fix unescaped user input in LDAP query ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24379))
|
|
||||||
|
|
||||||
## [4.1.1] - 2023-03-16
|
## [4.1.1] - 2023-03-16
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
# This needs to be bullseye-slim because the Ruby image is built on bullseye-slim
|
# This needs to be bullseye-slim because the Ruby image is built on bullseye-slim
|
||||||
ARG NODE_VERSION="16.18.1-bullseye-slim"
|
ARG NODE_VERSION="16.18.1-bullseye-slim"
|
||||||
|
|
||||||
FROM ghcr.io/moritzheiber/ruby-jemalloc:3.0.6-slim as ruby
|
FROM ghcr.io/moritzheiber/ruby-jemalloc:3.0.4-slim as ruby
|
||||||
FROM node:${NODE_VERSION} as build
|
FROM node:${NODE_VERSION} as build
|
||||||
|
|
||||||
COPY --link --from=ruby /opt/ruby /opt/ruby
|
COPY --link --from=ruby /opt/ruby /opt/ruby
|
||||||
@ -17,7 +17,6 @@ COPY Gemfile* package.json yarn.lock /opt/mastodon/
|
|||||||
|
|
||||||
# hadolint ignore=DL3008
|
# hadolint ignore=DL3008
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get -yq dist-upgrade && \
|
|
||||||
apt-get install -y --no-install-recommends build-essential \
|
apt-get install -y --no-install-recommends build-essential \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
git \
|
git \
|
||||||
|
1
Gemfile
1
Gemfile
@ -158,4 +158,3 @@ gem 'concurrent-ruby', require: false
|
|||||||
gem 'connection_pool', require: false
|
gem 'connection_pool', require: false
|
||||||
gem 'xorcist', '~> 1.1'
|
gem 'xorcist', '~> 1.1'
|
||||||
gem 'cocoon', '~> 1.2'
|
gem 'cocoon', '~> 1.2'
|
||||||
gem 'mail', '~> 2.8'
|
|
||||||
|
168
Gemfile.lock
168
Gemfile.lock
@ -10,40 +10,40 @@ GIT
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.1.7.8)
|
actioncable (6.1.7.2)
|
||||||
actionpack (= 6.1.7.8)
|
actionpack (= 6.1.7.2)
|
||||||
activesupport (= 6.1.7.8)
|
activesupport (= 6.1.7.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.1.7.8)
|
actionmailbox (6.1.7.2)
|
||||||
actionpack (= 6.1.7.8)
|
actionpack (= 6.1.7.2)
|
||||||
activejob (= 6.1.7.8)
|
activejob (= 6.1.7.2)
|
||||||
activerecord (= 6.1.7.8)
|
activerecord (= 6.1.7.2)
|
||||||
activestorage (= 6.1.7.8)
|
activestorage (= 6.1.7.2)
|
||||||
activesupport (= 6.1.7.8)
|
activesupport (= 6.1.7.2)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.1.7.8)
|
actionmailer (6.1.7.2)
|
||||||
actionpack (= 6.1.7.8)
|
actionpack (= 6.1.7.2)
|
||||||
actionview (= 6.1.7.8)
|
actionview (= 6.1.7.2)
|
||||||
activejob (= 6.1.7.8)
|
activejob (= 6.1.7.2)
|
||||||
activesupport (= 6.1.7.8)
|
activesupport (= 6.1.7.2)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (6.1.7.8)
|
actionpack (6.1.7.2)
|
||||||
actionview (= 6.1.7.8)
|
actionview (= 6.1.7.2)
|
||||||
activesupport (= 6.1.7.8)
|
activesupport (= 6.1.7.2)
|
||||||
rack (~> 2.0, >= 2.0.9)
|
rack (~> 2.0, >= 2.0.9)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (6.1.7.8)
|
actiontext (6.1.7.2)
|
||||||
actionpack (= 6.1.7.8)
|
actionpack (= 6.1.7.2)
|
||||||
activerecord (= 6.1.7.8)
|
activerecord (= 6.1.7.2)
|
||||||
activestorage (= 6.1.7.8)
|
activestorage (= 6.1.7.2)
|
||||||
activesupport (= 6.1.7.8)
|
activesupport (= 6.1.7.2)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.1.7.8)
|
actionview (6.1.7.2)
|
||||||
activesupport (= 6.1.7.8)
|
activesupport (= 6.1.7.2)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
@ -54,22 +54,22 @@ GEM
|
|||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||||
active_record_query_trace (1.8)
|
active_record_query_trace (1.8)
|
||||||
activejob (6.1.7.8)
|
activejob (6.1.7.2)
|
||||||
activesupport (= 6.1.7.8)
|
activesupport (= 6.1.7.2)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.1.7.8)
|
activemodel (6.1.7.2)
|
||||||
activesupport (= 6.1.7.8)
|
activesupport (= 6.1.7.2)
|
||||||
activerecord (6.1.7.8)
|
activerecord (6.1.7.2)
|
||||||
activemodel (= 6.1.7.8)
|
activemodel (= 6.1.7.2)
|
||||||
activesupport (= 6.1.7.8)
|
activesupport (= 6.1.7.2)
|
||||||
activestorage (6.1.7.8)
|
activestorage (6.1.7.2)
|
||||||
actionpack (= 6.1.7.8)
|
actionpack (= 6.1.7.2)
|
||||||
activejob (= 6.1.7.8)
|
activejob (= 6.1.7.2)
|
||||||
activerecord (= 6.1.7.8)
|
activerecord (= 6.1.7.2)
|
||||||
activesupport (= 6.1.7.8)
|
activesupport (= 6.1.7.2)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (6.1.7.8)
|
activesupport (6.1.7.2)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
@ -105,7 +105,6 @@ GEM
|
|||||||
aws-sigv4 (~> 1.4)
|
aws-sigv4 (~> 1.4)
|
||||||
aws-sigv4 (1.5.2)
|
aws-sigv4 (1.5.2)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
base64 (0.2.0)
|
|
||||||
bcrypt (3.1.17)
|
bcrypt (3.1.17)
|
||||||
better_errors (2.9.1)
|
better_errors (2.9.1)
|
||||||
coderay (>= 1.0.0)
|
coderay (>= 1.0.0)
|
||||||
@ -121,7 +120,8 @@ GEM
|
|||||||
bindata (2.4.14)
|
bindata (2.4.14)
|
||||||
binding_of_caller (1.0.0)
|
binding_of_caller (1.0.0)
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
blurhash (0.1.7)
|
blurhash (0.1.6)
|
||||||
|
ffi (~> 1.14)
|
||||||
bootsnap (1.16.0)
|
bootsnap (1.16.0)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
brakeman (5.4.0)
|
brakeman (5.4.0)
|
||||||
@ -174,7 +174,7 @@ GEM
|
|||||||
cocoon (1.2.15)
|
cocoon (1.2.15)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
color_diff (0.1)
|
color_diff (0.1)
|
||||||
concurrent-ruby (1.2.3)
|
concurrent-ruby (1.2.0)
|
||||||
connection_pool (2.3.0)
|
connection_pool (2.3.0)
|
||||||
cose (1.2.1)
|
cose (1.2.1)
|
||||||
cbor (~> 0.5.9)
|
cbor (~> 0.5.9)
|
||||||
@ -184,7 +184,7 @@ GEM
|
|||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
css_parser (1.12.0)
|
css_parser (1.12.0)
|
||||||
addressable
|
addressable
|
||||||
date (3.3.4)
|
date (3.3.3)
|
||||||
debug_inspector (1.0.0)
|
debug_inspector (1.0.0)
|
||||||
devise (4.8.1)
|
devise (4.8.1)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
@ -207,7 +207,7 @@ GEM
|
|||||||
docile (1.4.0)
|
docile (1.4.0)
|
||||||
domain_name (0.5.20190701)
|
domain_name (0.5.20190701)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
doorkeeper (5.6.6)
|
doorkeeper (5.6.4)
|
||||||
railties (>= 5)
|
railties (>= 5)
|
||||||
dotenv (2.8.1)
|
dotenv (2.8.1)
|
||||||
dotenv-rails (2.8.1)
|
dotenv-rails (2.8.1)
|
||||||
@ -331,7 +331,7 @@ GEM
|
|||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.6.3)
|
json (2.6.3)
|
||||||
json-canonicalization (0.3.0)
|
json-canonicalization (0.3.0)
|
||||||
json-jwt (1.15.3.1)
|
json-jwt (1.15.3)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
aes_key_wrap
|
aes_key_wrap
|
||||||
bindata
|
bindata
|
||||||
@ -389,14 +389,14 @@ GEM
|
|||||||
loofah (2.19.1)
|
loofah (2.19.1)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.8.1)
|
mail (2.8.0.1)
|
||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
makara (0.5.1)
|
makara (0.5.1)
|
||||||
activerecord (>= 5.2.0)
|
activerecord (>= 5.2.0)
|
||||||
marcel (1.0.4)
|
marcel (1.0.2)
|
||||||
mario-redis-lock (1.2.1)
|
mario-redis-lock (1.2.1)
|
||||||
redis (>= 3.0.5)
|
redis (>= 3.0.5)
|
||||||
matrix (0.4.2)
|
matrix (0.4.2)
|
||||||
@ -405,28 +405,28 @@ GEM
|
|||||||
mime-types (3.4.1)
|
mime-types (3.4.1)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2022.0105)
|
mime-types-data (3.2022.0105)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.2)
|
||||||
mini_portile2 (2.8.7)
|
mini_portile2 (2.8.1)
|
||||||
minitest (5.17.0)
|
minitest (5.17.0)
|
||||||
msgpack (1.6.0)
|
msgpack (1.6.0)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.1.1)
|
multipart-post (2.1.1)
|
||||||
net-imap (0.3.7)
|
net-imap (0.3.4)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-ldap (0.17.1)
|
net-ldap (0.17.1)
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.2)
|
net-protocol (0.2.1)
|
||||||
timeout
|
timeout
|
||||||
net-scp (4.0.0.rc1)
|
net-scp (4.0.0.rc1)
|
||||||
net-ssh (>= 2.6.5, < 8.0.0)
|
net-ssh (>= 2.6.5, < 8.0.0)
|
||||||
net-smtp (0.3.4)
|
net-smtp (0.3.3)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-ssh (7.0.1)
|
net-ssh (7.0.1)
|
||||||
nio4r (2.5.9)
|
nio4r (2.5.8)
|
||||||
nokogiri (1.16.6)
|
nokogiri (1.14.1)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.0)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nsa (0.2.8)
|
nsa (0.2.8)
|
||||||
activesupport (>= 4.2, < 7)
|
activesupport (>= 4.2, < 7)
|
||||||
@ -469,7 +469,7 @@ GEM
|
|||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.4.6)
|
pg (1.4.5)
|
||||||
pghero (3.1.0)
|
pghero (3.1.0)
|
||||||
activerecord (>= 6)
|
activerecord (>= 6)
|
||||||
pkg-config (1.5.1)
|
pkg-config (1.5.1)
|
||||||
@ -492,13 +492,13 @@ GEM
|
|||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (5.0.1)
|
public_suffix (5.0.1)
|
||||||
puma (5.6.8)
|
puma (5.6.5)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.3.0)
|
pundit (2.3.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.7.3)
|
racc (1.6.2)
|
||||||
rack (2.2.9)
|
rack (2.2.6.2)
|
||||||
rack-attack (6.6.1)
|
rack-attack (6.6.1)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rack-cors (1.1.1)
|
rack-cors (1.1.1)
|
||||||
@ -513,20 +513,20 @@ GEM
|
|||||||
rack
|
rack
|
||||||
rack-test (2.0.2)
|
rack-test (2.0.2)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rails (6.1.7.8)
|
rails (6.1.7.2)
|
||||||
actioncable (= 6.1.7.8)
|
actioncable (= 6.1.7.2)
|
||||||
actionmailbox (= 6.1.7.8)
|
actionmailbox (= 6.1.7.2)
|
||||||
actionmailer (= 6.1.7.8)
|
actionmailer (= 6.1.7.2)
|
||||||
actionpack (= 6.1.7.8)
|
actionpack (= 6.1.7.2)
|
||||||
actiontext (= 6.1.7.8)
|
actiontext (= 6.1.7.2)
|
||||||
actionview (= 6.1.7.8)
|
actionview (= 6.1.7.2)
|
||||||
activejob (= 6.1.7.8)
|
activejob (= 6.1.7.2)
|
||||||
activemodel (= 6.1.7.8)
|
activemodel (= 6.1.7.2)
|
||||||
activerecord (= 6.1.7.8)
|
activerecord (= 6.1.7.2)
|
||||||
activestorage (= 6.1.7.8)
|
activestorage (= 6.1.7.2)
|
||||||
activesupport (= 6.1.7.8)
|
activesupport (= 6.1.7.2)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.1.7.8)
|
railties (= 6.1.7.2)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-controller-testing (1.0.5)
|
rails-controller-testing (1.0.5)
|
||||||
actionpack (>= 5.0.1.rc1)
|
actionpack (>= 5.0.1.rc1)
|
||||||
@ -542,9 +542,9 @@ GEM
|
|||||||
railties (>= 6.0.0, < 7)
|
railties (>= 6.0.0, < 7)
|
||||||
rails-settings-cached (0.6.6)
|
rails-settings-cached (0.6.6)
|
||||||
rails (>= 4.2.0)
|
rails (>= 4.2.0)
|
||||||
railties (6.1.7.8)
|
railties (6.1.7.2)
|
||||||
actionpack (= 6.1.7.8)
|
actionpack (= 6.1.7.2)
|
||||||
activesupport (= 6.1.7.8)
|
activesupport (= 6.1.7.2)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
@ -566,8 +566,7 @@ GEM
|
|||||||
responders (3.0.1)
|
responders (3.0.1)
|
||||||
actionpack (>= 5.0)
|
actionpack (>= 5.0)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
rexml (3.2.8)
|
rexml (3.2.5)
|
||||||
strscan (>= 3.0.9)
|
|
||||||
rotp (6.2.0)
|
rotp (6.2.0)
|
||||||
rpam2 (4.0.2)
|
rpam2 (4.0.2)
|
||||||
rqrcode (2.1.2)
|
rqrcode (2.1.2)
|
||||||
@ -629,14 +628,14 @@ GEM
|
|||||||
fugit (~> 1.1, >= 1.1.6)
|
fugit (~> 1.1, >= 1.1.6)
|
||||||
safety_net_attestation (0.4.0)
|
safety_net_attestation (0.4.0)
|
||||||
jwt (~> 2.0)
|
jwt (~> 2.0)
|
||||||
sanitize (6.0.2)
|
sanitize (6.0.1)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
scenic (1.7.0)
|
scenic (1.7.0)
|
||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
railties (>= 4.0.0)
|
railties (>= 4.0.0)
|
||||||
semantic_range (3.0.0)
|
semantic_range (3.0.0)
|
||||||
sidekiq (6.5.12)
|
sidekiq (6.5.8)
|
||||||
connection_pool (>= 2.2.5, < 3)
|
connection_pool (>= 2.2.5, < 3)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
redis (>= 4.5.0, < 5)
|
redis (>= 4.5.0, < 5)
|
||||||
@ -647,7 +646,7 @@ GEM
|
|||||||
rufus-scheduler (~> 3.2)
|
rufus-scheduler (~> 3.2)
|
||||||
sidekiq (>= 4, < 7)
|
sidekiq (>= 4, < 7)
|
||||||
tilt (>= 1.4.0)
|
tilt (>= 1.4.0)
|
||||||
sidekiq-unique-jobs (7.1.33)
|
sidekiq-unique-jobs (7.1.29)
|
||||||
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
redis (< 5.0)
|
redis (< 5.0)
|
||||||
@ -665,8 +664,7 @@ GEM
|
|||||||
simplecov-html (0.12.3)
|
simplecov-html (0.12.3)
|
||||||
simplecov_json_formatter (0.1.4)
|
simplecov_json_formatter (0.1.4)
|
||||||
smart_properties (1.17.0)
|
smart_properties (1.17.0)
|
||||||
sprockets (3.7.3)
|
sprockets (3.7.2)
|
||||||
base64
|
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
rack (> 1, < 3)
|
rack (> 1, < 3)
|
||||||
sprockets-rails (3.4.2)
|
sprockets-rails (3.4.2)
|
||||||
@ -682,7 +680,6 @@ GEM
|
|||||||
redlock (~> 1.0)
|
redlock (~> 1.0)
|
||||||
strong_migrations (0.7.9)
|
strong_migrations (0.7.9)
|
||||||
activerecord (>= 5)
|
activerecord (>= 5)
|
||||||
strscan (3.1.0)
|
|
||||||
swd (1.3.0)
|
swd (1.3.0)
|
||||||
activesupport (>= 3)
|
activesupport (>= 3)
|
||||||
attr_required (>= 0.0.5)
|
attr_required (>= 0.0.5)
|
||||||
@ -692,9 +689,9 @@ GEM
|
|||||||
unicode-display_width (>= 1.1.1, < 3)
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
terrapin (0.6.0)
|
terrapin (0.6.0)
|
||||||
climate_control (>= 0.0.3, < 1.0)
|
climate_control (>= 0.0.3, < 1.0)
|
||||||
thor (1.2.2)
|
thor (1.2.1)
|
||||||
tilt (2.0.11)
|
tilt (2.0.11)
|
||||||
timeout (0.3.2)
|
timeout (0.3.1)
|
||||||
tpm-key_attestation (0.11.0)
|
tpm-key_attestation (0.11.0)
|
||||||
bindata (~> 2.4)
|
bindata (~> 2.4)
|
||||||
openssl (> 2.0, < 3.1)
|
openssl (> 2.0, < 3.1)
|
||||||
@ -750,14 +747,14 @@ GEM
|
|||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
semantic_range (>= 2.3.0)
|
semantic_range (>= 2.3.0)
|
||||||
websocket-driver (0.7.6)
|
websocket-driver (0.7.5)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
wisper (2.0.1)
|
wisper (2.0.1)
|
||||||
xorcist (1.1.3)
|
xorcist (1.1.3)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.6.16)
|
zeitwerk (2.6.6)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
@ -820,7 +817,6 @@ DEPENDENCIES
|
|||||||
letter_opener_web (~> 2.0)
|
letter_opener_web (~> 2.0)
|
||||||
link_header (~> 0.0)
|
link_header (~> 0.0)
|
||||||
lograge (~> 0.12)
|
lograge (~> 0.12)
|
||||||
mail (~> 2.8)
|
|
||||||
makara (~> 0.5)
|
makara (~> 0.5)
|
||||||
mario-redis-lock (~> 1.2)
|
mario-redis-lock (~> 1.2)
|
||||||
memory_profiler
|
memory_profiler
|
||||||
|
10
SECURITY.md
10
SECURITY.md
@ -10,8 +10,8 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
|
|||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | ---------------- |
|
| ------- | ----------|
|
||||||
| 4.2.x | Yes |
|
| 4.0.x | Yes |
|
||||||
| 4.1.x | Yes |
|
| 3.5.x | Yes |
|
||||||
| < 4.1 | No |
|
| < 3.5 | No |
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AccountsIndex < Chewy::Index
|
class AccountsIndex < Chewy::Index
|
||||||
include DatetimeClampingConcern
|
|
||||||
|
|
||||||
settings index: { refresh_interval: '30s' }, analysis: {
|
settings index: { refresh_interval: '30s' }, analysis: {
|
||||||
analyzer: {
|
analyzer: {
|
||||||
content: {
|
content: {
|
||||||
@ -40,6 +38,6 @@ class AccountsIndex < Chewy::Index
|
|||||||
|
|
||||||
field :following_count, type: 'long', value: ->(account) { account.following_count }
|
field :following_count, type: 'long', value: ->(account) { account.following_count }
|
||||||
field :followers_count, type: 'long', value: ->(account) { account.followers_count }
|
field :followers_count, type: 'long', value: ->(account) { account.followers_count }
|
||||||
field :last_status_at, type: 'date', value: ->(account) { clamp_date(account.last_status_at || account.created_at) }
|
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module DatetimeClampingConcern
|
|
||||||
extend ActiveSupport::Concern
|
|
||||||
|
|
||||||
MIN_ISO8601_DATETIME = '0000-01-01T00:00:00Z'.to_datetime.freeze
|
|
||||||
MAX_ISO8601_DATETIME = '9999-12-31T23:59:59Z'.to_datetime.freeze
|
|
||||||
|
|
||||||
class_methods do
|
|
||||||
def clamp_date(datetime)
|
|
||||||
datetime.clamp(MIN_ISO8601_DATETIME, MAX_ISO8601_DATETIME)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,8 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class TagsIndex < Chewy::Index
|
class TagsIndex < Chewy::Index
|
||||||
include DatetimeClampingConcern
|
|
||||||
|
|
||||||
settings index: { refresh_interval: '30s' }, analysis: {
|
settings index: { refresh_interval: '30s' }, analysis: {
|
||||||
analyzer: {
|
analyzer: {
|
||||||
content: {
|
content: {
|
||||||
@ -38,6 +36,6 @@ class TagsIndex < Chewy::Index
|
|||||||
|
|
||||||
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
|
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
|
||||||
field :usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts }
|
field :usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts }
|
||||||
field :last_status_at, type: 'date', value: ->(tag) { clamp_date(tag.last_status_at || tag.created_at) }
|
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -21,7 +21,7 @@ module Admin
|
|||||||
account_action.save!
|
account_action.save!
|
||||||
|
|
||||||
if account_action.with_report?
|
if account_action.with_report?
|
||||||
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: resource_params[:report_id])
|
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: params[:report_id])
|
||||||
else
|
else
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id)
|
||||||
end
|
end
|
||||||
|
@ -25,8 +25,6 @@ class Admin::DomainAllowsController < Admin::BaseController
|
|||||||
def destroy
|
def destroy
|
||||||
authorize @domain_allow, :destroy?
|
authorize @domain_allow, :destroy?
|
||||||
UnallowDomainService.new.call(@domain_allow)
|
UnallowDomainService.new.call(@domain_allow)
|
||||||
log_action :destroy, @domain_allow
|
|
||||||
|
|
||||||
redirect_to admin_instances_path, notice: I18n.t('admin.domain_allows.destroyed_msg')
|
redirect_to admin_instances_path, notice: I18n.t('admin.domain_allows.destroyed_msg')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ module Admin
|
|||||||
@domain_block.errors.delete(:domain)
|
@domain_block.errors.delete(:domain)
|
||||||
render :new
|
render :new
|
||||||
else
|
else
|
||||||
if existing_domain_block.present? && existing_domain_block.domain == TagManager.instance.normalize_domain(@domain_block.domain.strip)
|
if existing_domain_block.present?
|
||||||
@domain_block = existing_domain_block
|
@domain_block = existing_domain_block
|
||||||
@domain_block.update(resource_params)
|
@domain_block.update(resource_params)
|
||||||
end
|
end
|
||||||
|
@ -20,7 +20,6 @@ module Admin
|
|||||||
authorize :webhook, :create?
|
authorize :webhook, :create?
|
||||||
|
|
||||||
@webhook = Webhook.new(resource_params)
|
@webhook = Webhook.new(resource_params)
|
||||||
@webhook.current_account = current_account
|
|
||||||
|
|
||||||
if @webhook.save
|
if @webhook.save
|
||||||
redirect_to admin_webhook_path(@webhook)
|
redirect_to admin_webhook_path(@webhook)
|
||||||
@ -40,12 +39,10 @@ module Admin
|
|||||||
def update
|
def update
|
||||||
authorize @webhook, :update?
|
authorize @webhook, :update?
|
||||||
|
|
||||||
@webhook.current_account = current_account
|
|
||||||
|
|
||||||
if @webhook.update(resource_params)
|
if @webhook.update(resource_params)
|
||||||
redirect_to admin_webhook_path(@webhook)
|
redirect_to admin_webhook_path(@webhook)
|
||||||
else
|
else
|
||||||
render :edit
|
render :show
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -7,11 +7,8 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
|||||||
after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) }
|
after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) }
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@statuses = load_statuses
|
@statuses = load_statuses
|
||||||
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
|
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||||
render json: @statuses, each_serializer: REST::StatusSerializer,
|
|
||||||
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
|
|
||||||
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -19,11 +19,10 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
|
|||||||
def create
|
def create
|
||||||
authorize :domain_block, :create?
|
authorize :domain_block, :create?
|
||||||
|
|
||||||
@domain_block = DomainBlock.new(resource_params)
|
|
||||||
existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil
|
existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil
|
||||||
return render json: existing_domain_block, serializer: REST::Admin::ExistingDomainBlockErrorSerializer, status: 422 if conflicts_with_existing_block?(@domain_block, existing_domain_block)
|
return render json: existing_domain_block, serializer: REST::Admin::ExistingDomainBlockErrorSerializer, status: 422 if existing_domain_block.present?
|
||||||
|
|
||||||
@domain_block.save!
|
@domain_block = DomainBlock.create!(resource_params)
|
||||||
DomainBlockWorker.perform_async(@domain_block.id)
|
DomainBlockWorker.perform_async(@domain_block.id)
|
||||||
log_action :create, @domain_block
|
log_action :create, @domain_block
|
||||||
render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer
|
render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer
|
||||||
@ -56,10 +55,6 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def conflicts_with_existing_block?(domain_block, existing_domain_block)
|
|
||||||
existing_domain_block.present? && (existing_domain_block.domain == TagManager.instance.normalize_domain(domain_block.domain) || !domain_block.stricter_than?(existing_domain_block))
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_domain_blocks
|
def set_domain_blocks
|
||||||
@domain_blocks = filtered_domain_blocks.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
@domain_blocks = filtered_domain_blocks.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
end
|
end
|
||||||
|
@ -6,11 +6,8 @@ class Api::V1::BookmarksController < Api::BaseController
|
|||||||
after_action :insert_pagination_headers
|
after_action :insert_pagination_headers
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@statuses = load_statuses
|
@statuses = load_statuses
|
||||||
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
|
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||||
render json: @statuses, each_serializer: REST::StatusSerializer,
|
|
||||||
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
|
|
||||||
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -11,7 +11,7 @@ class Api::V1::ConversationsController < Api::BaseController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
@conversations = paginated_conversations
|
@conversations = paginated_conversations
|
||||||
render json: @conversations, each_serializer: REST::ConversationSerializer, relationships: StatusRelationshipsPresenter.new(@conversations.map(&:last_status), current_user&.account_id)
|
render json: @conversations, each_serializer: REST::ConversationSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def read
|
def read
|
||||||
@ -32,19 +32,6 @@ class Api::V1::ConversationsController < Api::BaseController
|
|||||||
|
|
||||||
def paginated_conversations
|
def paginated_conversations
|
||||||
AccountConversation.where(account: current_account)
|
AccountConversation.where(account: current_account)
|
||||||
.includes(
|
|
||||||
account: :account_stat,
|
|
||||||
last_status: [
|
|
||||||
:media_attachments,
|
|
||||||
:preview_cards,
|
|
||||||
:status_stat,
|
|
||||||
:tags,
|
|
||||||
{
|
|
||||||
active_mentions: [account: :account_stat],
|
|
||||||
account: :account_stat,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
)
|
|
||||||
.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -6,11 +6,8 @@ class Api::V1::FavouritesController < Api::BaseController
|
|||||||
after_action :insert_pagination_headers
|
after_action :insert_pagination_headers
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@statuses = load_statuses
|
@statuses = load_statuses
|
||||||
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
|
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||||
render json: @statuses, each_serializer: REST::StatusSerializer,
|
|
||||||
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
|
|
||||||
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -12,10 +12,6 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def set_recently_used_tags
|
def set_recently_used_tags
|
||||||
@recently_used_tags = Tag.recently_used(current_account).where.not(id: featured_tag_ids).limit(10)
|
@recently_used_tags = Tag.recently_used(current_account).where.not(id: current_account.featured_tags).limit(10)
|
||||||
end
|
|
||||||
|
|
||||||
def featured_tag_ids
|
|
||||||
current_account.featured_tags.pluck(:tag_id)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -6,7 +6,6 @@ class Api::V1::ScheduledStatusesController < Api::BaseController
|
|||||||
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, except: [:update, :destroy]
|
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, except: [:update, :destroy]
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:update, :destroy]
|
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:update, :destroy]
|
||||||
|
|
||||||
before_action :require_user!
|
|
||||||
before_action :set_statuses, only: :index
|
before_action :set_statuses, only: :index
|
||||||
before_action :set_status, except: :index
|
before_action :set_status, except: :index
|
||||||
|
|
||||||
|
@ -7,15 +7,11 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController
|
|||||||
before_action :set_status
|
before_action :set_status
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: status_edits, each_serializer: REST::StatusEditSerializer
|
render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def status_edits
|
|
||||||
@status.edits.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)]
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_status
|
def set_status
|
||||||
@status = Status.find(params[:status_id])
|
@status = Status.find(params[:status_id])
|
||||||
authorize @status, :show?
|
authorize @status, :show?
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
class Api::V1::Statuses::ReblogsController < Api::BaseController
|
class Api::V1::Statuses::ReblogsController < Api::BaseController
|
||||||
include Authorization
|
include Authorization
|
||||||
include Redisable
|
|
||||||
include Lockable
|
|
||||||
|
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
|
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
@ -12,9 +10,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
|
|||||||
override_rate_limit_headers :create, family: :statuses
|
override_rate_limit_headers :create, family: :statuses
|
||||||
|
|
||||||
def create
|
def create
|
||||||
with_lock("reblog:#{current_account.id}:#{@reblog.id}") do
|
@status = ReblogService.new.call(current_account, @reblog, reblog_params)
|
||||||
@status = ReblogService.new.call(current_account, @reblog, reblog_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: @status, serializer: REST::StatusSerializer
|
render json: @status, serializer: REST::StatusSerializer
|
||||||
end
|
end
|
||||||
|
@ -4,7 +4,6 @@ class Api::V1::Statuses::TranslationsController < Api::BaseController
|
|||||||
include Authorization
|
include Authorization
|
||||||
|
|
||||||
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
|
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
|
||||||
before_action :require_user!
|
|
||||||
before_action :set_status
|
before_action :set_status
|
||||||
before_action :set_translation
|
before_action :set_translation
|
||||||
|
|
||||||
|
@ -44,13 +44,10 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
loaded_ancestors = cache_collection(ancestors_results, Status)
|
loaded_ancestors = cache_collection(ancestors_results, Status)
|
||||||
loaded_descendants = cache_collection(descendants_results, Status)
|
loaded_descendants = cache_collection(descendants_results, Status)
|
||||||
|
|
||||||
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
|
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
|
||||||
statuses = [@status] + @context.ancestors + @context.descendants
|
statuses = [@status] + @context.ancestors + @context.descendants
|
||||||
account_ids = statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
|
|
||||||
|
|
||||||
render json: @context, serializer: REST::ContextSerializer,
|
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
|
||||||
relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id),
|
|
||||||
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@ -67,8 +64,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
application: doorkeeper_token.application,
|
application: doorkeeper_token.application,
|
||||||
poll: status_params[:poll],
|
poll: status_params[:poll],
|
||||||
idempotency: request.headers['Idempotency-Key'],
|
idempotency: request.headers['Idempotency-Key'],
|
||||||
with_rate_limit: true,
|
with_rate_limit: true
|
||||||
quote_id: status_params[:quote_id].presence,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
|
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
|
||||||
@ -132,7 +128,6 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
:visibility,
|
:visibility,
|
||||||
:language,
|
:language,
|
||||||
:scheduled_at,
|
:scheduled_at,
|
||||||
:quote_id,
|
|
||||||
media_ids: [],
|
media_ids: [],
|
||||||
media_attributes: [
|
media_attributes: [
|
||||||
:id,
|
:id,
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
class Api::V1::StreamingController < Api::BaseController
|
class Api::V1::StreamingController < Api::BaseController
|
||||||
def index
|
def index
|
||||||
if same_host?
|
if Rails.configuration.x.streaming_api_base_url == request.host
|
||||||
not_found
|
not_found
|
||||||
else
|
else
|
||||||
redirect_to streaming_api_url, status: 301
|
redirect_to streaming_api_url, status: 301
|
||||||
@ -11,16 +11,9 @@ class Api::V1::StreamingController < Api::BaseController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def same_host?
|
|
||||||
base_url = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url)
|
|
||||||
request.host == base_url.host && request.port == (base_url.port || 80)
|
|
||||||
end
|
|
||||||
|
|
||||||
def streaming_api_url
|
def streaming_api_url
|
||||||
Addressable::URI.parse(request.url).tap do |uri|
|
Addressable::URI.parse(request.url).tap do |uri|
|
||||||
base_url = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url)
|
uri.host = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url).host
|
||||||
uri.host = base_url.host
|
|
||||||
uri.port = base_url.port
|
|
||||||
end.to_s
|
end.to_s
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -6,13 +6,11 @@ class Api::V1::Timelines::HomeController < Api::BaseController
|
|||||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@statuses = load_statuses
|
@statuses = load_statuses
|
||||||
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
|
|
||||||
|
|
||||||
render json: @statuses,
|
render json: @statuses,
|
||||||
each_serializer: REST::StatusSerializer,
|
each_serializer: REST::StatusSerializer,
|
||||||
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
|
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
|
||||||
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id),
|
|
||||||
status: account_home_feed.regenerating? ? 206 : 200
|
status: account_home_feed.regenerating? ? 206 : 200
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -9,12 +9,9 @@ class Api::V1::Timelines::ListController < Api::BaseController
|
|||||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||||
|
|
||||||
def show
|
def show
|
||||||
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
|
|
||||||
|
|
||||||
render json: @statuses,
|
render json: @statuses,
|
||||||
each_serializer: REST::StatusSerializer,
|
each_serializer: REST::StatusSerializer,
|
||||||
relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id),
|
relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id)
|
||||||
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -1,17 +1,12 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Timelines::PublicController < Api::BaseController
|
class Api::V1::Timelines::PublicController < Api::BaseController
|
||||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
|
|
||||||
before_action :require_user!, only: [:show], if: :require_auth?
|
before_action :require_user!, only: [:show], if: :require_auth?
|
||||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@statuses = load_statuses
|
@statuses = load_statuses
|
||||||
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
|
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||||
|
|
||||||
render json: @statuses, each_serializer: REST::StatusSerializer,
|
|
||||||
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
|
|
||||||
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -1,26 +1,16 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Timelines::TagController < Api::BaseController
|
class Api::V1::Timelines::TagController < Api::BaseController
|
||||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
|
|
||||||
before_action :require_user!, if: :require_auth?
|
|
||||||
before_action :load_tag
|
before_action :load_tag
|
||||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@statuses = load_statuses
|
@statuses = load_statuses
|
||||||
account_ids = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
|
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||||
|
|
||||||
render json: @statuses, each_serializer: REST::StatusSerializer,
|
|
||||||
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
|
|
||||||
account_relationships: AccountRelationshipsPresenter.new(account_ids, current_user&.account_id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def require_auth?
|
|
||||||
!Setting.timeline_preview
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_tag
|
def load_tag
|
||||||
@tag = Tag.find_normalized(params[:id])
|
@tag = Tag.find_normalized(params[:id])
|
||||||
end
|
end
|
||||||
|
@ -18,14 +18,6 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def next_path
|
|
||||||
api_v2_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue?
|
|
||||||
end
|
|
||||||
|
|
||||||
def prev_path
|
|
||||||
api_v2_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty?
|
|
||||||
end
|
|
||||||
|
|
||||||
def filtered_accounts
|
def filtered_accounts
|
||||||
AccountFilter.new(translated_filter_params).results
|
AccountFilter.new(translated_filter_params).results
|
||||||
end
|
end
|
||||||
|
@ -5,7 +5,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||||||
|
|
||||||
def self.provides_callback_for(provider)
|
def self.provides_callback_for(provider)
|
||||||
define_method provider do
|
define_method provider do
|
||||||
@user = User.find_for_omniauth(request.env['omniauth.auth'], current_user)
|
@user = User.find_for_oauth(request.env['omniauth.auth'], current_user)
|
||||||
|
|
||||||
if @user.persisted?
|
if @user.persisted?
|
||||||
LoginActivity.create(
|
LoginActivity.create(
|
||||||
@ -24,9 +24,6 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||||||
session["devise.#{provider}_data"] = request.env['omniauth.auth']
|
session["devise.#{provider}_data"] = request.env['omniauth.auth']
|
||||||
redirect_to new_user_registration_url
|
redirect_to new_user_registration_url
|
||||||
end
|
end
|
||||||
rescue ActiveRecord::RecordInvalid
|
|
||||||
flash[:alert] = I18n.t('devise.failure.omniauth_user_creation_failure') if is_navigational_format?
|
|
||||||
redirect_to new_user_session_url
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||||||
super(hash)
|
super(hash)
|
||||||
|
|
||||||
resource.locale = I18n.locale
|
resource.locale = I18n.locale
|
||||||
resource.invite_code = @invite&.code if resource.invite_code.blank?
|
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
|
||||||
resource.registration_form_time = session[:registration_form_time]
|
resource.registration_form_time = session[:registration_form_time]
|
||||||
resource.sign_up_ip = request.remote_ip
|
resource.sign_up_ip = request.remote_ip
|
||||||
|
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Auth::SessionsController < Devise::SessionsController
|
class Auth::SessionsController < Devise::SessionsController
|
||||||
include Redisable
|
|
||||||
|
|
||||||
MAX_2FA_ATTEMPTS_PER_HOUR = 10
|
|
||||||
|
|
||||||
layout 'auth'
|
layout 'auth'
|
||||||
|
|
||||||
skip_before_action :require_no_authentication, only: [:create]
|
skip_before_action :require_no_authentication, only: [:create]
|
||||||
@ -140,23 +136,9 @@ class Auth::SessionsController < Devise::SessionsController
|
|||||||
session.delete(:attempt_user_updated_at)
|
session.delete(:attempt_user_updated_at)
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_2fa_attempt_from_user(user)
|
|
||||||
redis.del(second_factor_attempts_key(user))
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_second_factor_rate_limits(user)
|
|
||||||
attempts, = redis.multi do |multi|
|
|
||||||
multi.incr(second_factor_attempts_key(user))
|
|
||||||
multi.expire(second_factor_attempts_key(user), 1.hour)
|
|
||||||
end
|
|
||||||
|
|
||||||
attempts >= MAX_2FA_ATTEMPTS_PER_HOUR
|
|
||||||
end
|
|
||||||
|
|
||||||
def on_authentication_success(user, security_measure)
|
def on_authentication_success(user, security_measure)
|
||||||
@on_authentication_success_called = true
|
@on_authentication_success_called = true
|
||||||
|
|
||||||
clear_2fa_attempt_from_user(user)
|
|
||||||
clear_attempt_from_session
|
clear_attempt_from_session
|
||||||
|
|
||||||
user.update_sign_in!(new_sign_in: true)
|
user.update_sign_in!(new_sign_in: true)
|
||||||
@ -188,8 +170,4 @@ class Auth::SessionsController < Devise::SessionsController
|
|||||||
user_agent: request.user_agent
|
user_agent: request.user_agent
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def second_factor_attempts_key(user)
|
|
||||||
"2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -13,11 +13,7 @@ class BackupsController < ApplicationController
|
|||||||
when :s3
|
when :s3
|
||||||
redirect_to @backup.dump.expiring_url(10)
|
redirect_to @backup.dump.expiring_url(10)
|
||||||
when :fog
|
when :fog
|
||||||
if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present?
|
redirect_to @backup.dump.expiring_url(Time.now.utc + 10)
|
||||||
redirect_to @backup.dump.expiring_url(Time.now.utc + 10)
|
|
||||||
else
|
|
||||||
redirect_to full_asset_url(@backup.dump.url)
|
|
||||||
end
|
|
||||||
when :filesystem
|
when :filesystem
|
||||||
redirect_to full_asset_url(@backup.dump.url)
|
redirect_to full_asset_url(@backup.dump.url)
|
||||||
end
|
end
|
||||||
|
@ -28,19 +28,29 @@ module CacheConcern
|
|||||||
response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature'
|
response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature'
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Rename this method, as it does not perform any caching anymore.
|
|
||||||
def cache_collection(raw, klass)
|
def cache_collection(raw, klass)
|
||||||
return raw unless klass.respond_to?(:preload_cacheable_associations)
|
return raw unless klass.respond_to?(:with_includes)
|
||||||
|
|
||||||
records = raw.to_a
|
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
|
||||||
|
return [] if raw.empty?
|
||||||
|
|
||||||
klass.preload_cacheable_associations(records)
|
cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id)
|
||||||
|
uncached_ids = raw.map(&:id) - cached_keys_with_value.keys
|
||||||
|
|
||||||
records
|
klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)
|
||||||
|
|
||||||
|
unless uncached_ids.empty?
|
||||||
|
uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id)
|
||||||
|
|
||||||
|
uncached.each_value do |item|
|
||||||
|
Rails.cache.write(item, item)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
raw.filter_map { |item| cached_keys_with_value[item.id] || uncached[item.id] }
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Rename this method, as it does not perform any caching anymore.
|
|
||||||
def cache_collection_paginated_by_id(raw, klass, limit, options)
|
def cache_collection_paginated_by_id(raw, klass, limit, options)
|
||||||
cache_collection raw.to_a_paginated_by_id(limit, options), klass
|
cache_collection raw.cache_ids.to_a_paginated_by_id(limit, options), klass
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -91,23 +91,14 @@ module SignatureVerification
|
|||||||
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
|
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
|
||||||
|
|
||||||
signature = Base64.decode64(signature_params['signature'])
|
signature = Base64.decode64(signature_params['signature'])
|
||||||
compare_signed_string = build_signed_string(include_query_string: true)
|
compare_signed_string = build_signed_string
|
||||||
|
|
||||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
||||||
|
|
||||||
# Compatibility quirk with older Mastodon versions
|
|
||||||
compare_signed_string = build_signed_string(include_query_string: false)
|
|
||||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
|
||||||
|
|
||||||
actor = stoplight_wrap_request { actor_refresh_key!(actor) }
|
actor = stoplight_wrap_request { actor_refresh_key!(actor) }
|
||||||
|
|
||||||
raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil?
|
raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil?
|
||||||
|
|
||||||
compare_signed_string = build_signed_string(include_query_string: true)
|
|
||||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
|
||||||
|
|
||||||
# Compatibility quirk with older Mastodon versions
|
|
||||||
compare_signed_string = build_signed_string(include_query_string: false)
|
|
||||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
||||||
|
|
||||||
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)", signed_string: compare_signed_string, signature: signature_params['signature']
|
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)", signed_string: compare_signed_string, signature: signature_params['signature']
|
||||||
@ -186,24 +177,16 @@ module SignatureVerification
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_signed_string(include_query_string: true)
|
def build_signed_string
|
||||||
signed_headers.map do |signed_header|
|
signed_headers.map do |signed_header|
|
||||||
case signed_header
|
if signed_header == Request::REQUEST_TARGET
|
||||||
when Request::REQUEST_TARGET
|
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
||||||
if include_query_string
|
elsif signed_header == '(created)'
|
||||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}"
|
|
||||||
else
|
|
||||||
# Current versions of Mastodon incorrectly omit the query string from the (request-target) pseudo-header.
|
|
||||||
# Therefore, temporarily support such incorrect signatures for compatibility.
|
|
||||||
# TODO: remove eventually some time after release of the fixed version
|
|
||||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
|
||||||
end
|
|
||||||
when '(created)'
|
|
||||||
raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
|
raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
|
||||||
raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank?
|
raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank?
|
||||||
|
|
||||||
"(created): #{signature_params['created']}"
|
"(created): #{signature_params['created']}"
|
||||||
when '(expires)'
|
elsif signed_header == '(expires)'
|
||||||
raise SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019'
|
raise SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019'
|
||||||
raise SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank?
|
raise SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank?
|
||||||
|
|
||||||
@ -263,7 +246,7 @@ module SignatureVerification
|
|||||||
stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''), suppress_errors: false) }
|
stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''), suppress_errors: false) }
|
||||||
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
|
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
|
||||||
account = ActivityPub::TagManager.instance.uri_to_actor(key_id)
|
account = ActivityPub::TagManager.instance.uri_to_actor(key_id)
|
||||||
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) }
|
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) }
|
||||||
account
|
account
|
||||||
end
|
end
|
||||||
rescue Mastodon::PrivateNetworkAddressError => e
|
rescue Mastodon::PrivateNetworkAddressError => e
|
||||||
|
@ -65,11 +65,6 @@ module TwoFactorAuthenticationConcern
|
|||||||
end
|
end
|
||||||
|
|
||||||
def authenticate_with_two_factor_via_otp(user)
|
def authenticate_with_two_factor_via_otp(user)
|
||||||
if check_second_factor_rate_limits(user)
|
|
||||||
flash.now[:alert] = I18n.t('users.rate_limited')
|
|
||||||
return prompt_for_two_factor(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
if valid_otp_attempt?(user)
|
if valid_otp_attempt?(user)
|
||||||
on_authentication_success(user, :otp)
|
on_authentication_success(user, :otp)
|
||||||
else
|
else
|
||||||
|
@ -46,6 +46,6 @@ class MediaController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def allow_iframing
|
def allow_iframing
|
||||||
response.headers.delete('X-Frame-Options')
|
response.headers['X-Frame-Options'] = 'ALLOWALL'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -8,15 +8,12 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
|||||||
before_action :require_not_suspended!, only: :destroy
|
before_action :require_not_suspended!, only: :destroy
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
|
||||||
before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json }
|
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
include Localized
|
include Localized
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
Web::PushSubscription.unsubscribe_for(params[:id], current_resource_owner)
|
Web::PushSubscription.unsubscribe_for(params[:id], current_resource_owner)
|
||||||
Doorkeeper::Application.find_by(id: params[:id])&.close_streaming_sessions(current_resource_owner)
|
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -33,14 +30,4 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
|||||||
def require_not_suspended!
|
def require_not_suspended!
|
||||||
forbidden if current_account.suspended?
|
forbidden if current_account.suspended?
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_last_used_at_by_app
|
|
||||||
@last_used_at_by_app = Doorkeeper::AccessToken
|
|
||||||
.select('DISTINCT ON (application_id) application_id, last_used_at')
|
|
||||||
.where(resource_owner_id: current_resource_owner.id)
|
|
||||||
.where.not(last_used_at: nil)
|
|
||||||
.order(application_id: :desc, last_used_at: :desc)
|
|
||||||
.pluck(:application_id, :last_used_at)
|
|
||||||
.to_h
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -55,7 +55,6 @@ class Settings::PreferencesController < Settings::BaseController
|
|||||||
:setting_trends,
|
:setting_trends,
|
||||||
:setting_crop_images,
|
:setting_crop_images,
|
||||||
:setting_always_send_emails,
|
:setting_always_send_emails,
|
||||||
:setting_place_tab_bar_at_bottom,
|
|
||||||
notification_emails: %i(follow follow_request reblog favourite mention report pending_account trending_tag appeal),
|
notification_emails: %i(follow follow_request reblog favourite mention report pending_account trending_tag appeal),
|
||||||
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
||||||
)
|
)
|
||||||
|
@ -43,7 +43,7 @@ class StatusesController < ApplicationController
|
|||||||
return not_found if @status.hidden? || @status.reblog?
|
return not_found if @status.hidden? || @status.reblog?
|
||||||
|
|
||||||
expires_in 180, public: true
|
expires_in 180, public: true
|
||||||
response.headers.delete('X-Frame-Options')
|
response.headers['X-Frame-Options'] = 'ALLOWALL'
|
||||||
|
|
||||||
render layout: 'embedded'
|
render layout: 'embedded'
|
||||||
end
|
end
|
||||||
|
@ -18,14 +18,7 @@ module WellKnown
|
|||||||
private
|
private
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
username = username_from_resource
|
@account = Account.find_local!(username_from_resource)
|
||||||
@account = begin
|
|
||||||
if username == Rails.configuration.x.local_domain || username == Rails.configuration.x.web_domain
|
|
||||||
Account.representative
|
|
||||||
else
|
|
||||||
Account.find_local!(username)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def username_from_resource
|
def username_from_resource
|
||||||
|
@ -23,7 +23,6 @@ module ContextHelper
|
|||||||
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
|
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
|
||||||
olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' },
|
olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' },
|
||||||
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
||||||
quoteUrl: { 'quoteUrl' => 'as:quoteUrl' },
|
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
def full_context
|
def full_context
|
||||||
|
@ -15,12 +15,7 @@ module FormattingHelper
|
|||||||
module_function :extract_status_plain_text
|
module_function :extract_status_plain_text
|
||||||
|
|
||||||
def status_content_format(status)
|
def status_content_format(status)
|
||||||
html_aware_format(
|
html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []))
|
||||||
status.text,
|
|
||||||
status.local?,
|
|
||||||
preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []),
|
|
||||||
quote: status.respond_to?(:quote) && status.quote,
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def rss_status_content_format(status)
|
def rss_status_content_format(status)
|
||||||
@ -63,10 +58,6 @@ module FormattingHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def account_field_value_format(field, with_rel_me: true)
|
def account_field_value_format(field, with_rel_me: true)
|
||||||
if field.verified? && !field.account.local?
|
html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false)
|
||||||
TextFormatter.shortened_link(field.value_for_verification)
|
|
||||||
else
|
|
||||||
html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -157,8 +157,8 @@ module JsonLdHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_resource(uri, id_is_known, on_behalf_of = nil, request_options: {})
|
def fetch_resource(uri, id, on_behalf_of = nil)
|
||||||
unless id_is_known
|
unless id
|
||||||
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
||||||
|
|
||||||
return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id'])
|
return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id'])
|
||||||
@ -166,29 +166,17 @@ module JsonLdHelper
|
|||||||
uri = json['id']
|
uri = json['id']
|
||||||
end
|
end
|
||||||
|
|
||||||
json = fetch_resource_without_id_validation(uri, on_behalf_of, request_options: request_options)
|
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
||||||
json.present? && json['id'] == uri ? json : nil
|
json.present? && json['id'] == uri ? json : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {})
|
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false)
|
||||||
on_behalf_of ||= Account.representative
|
on_behalf_of ||= Account.representative
|
||||||
|
|
||||||
build_request(uri, on_behalf_of, options: request_options).perform do |response|
|
build_request(uri, on_behalf_of).perform do |response|
|
||||||
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
|
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
|
||||||
|
|
||||||
body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response)
|
body_to_json(response.body_with_limit) if response.code == 200
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def valid_activitypub_content_type?(response)
|
|
||||||
return true if response.mime_type == 'application/activity+json'
|
|
||||||
|
|
||||||
# When the mime type is `application/ld+json`, we need to check the profile,
|
|
||||||
# but `http.rb` does not parse it for us.
|
|
||||||
return false unless response.mime_type == 'application/ld+json'
|
|
||||||
|
|
||||||
response.headers[HTTP::Headers::CONTENT_TYPE]&.split(';')&.map(&:strip)&.any? do |str|
|
|
||||||
str.start_with?('profile="') && str[9...-1].split.include?('https://www.w3.org/ns/activitystreams')
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -218,8 +206,8 @@ module JsonLdHelper
|
|||||||
response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code))
|
response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code))
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_request(uri, on_behalf_of = nil, options: {})
|
def build_request(uri, on_behalf_of = nil)
|
||||||
Request.new(:get, uri, **options).tap do |request|
|
Request.new(:get, uri).tap do |request|
|
||||||
request.on_behalf_of(on_behalf_of) if on_behalf_of
|
request.on_behalf_of(on_behalf_of) if on_behalf_of
|
||||||
request.add_headers('Accept' => 'application/activity+json, application/ld+json')
|
request.add_headers('Accept' => 'application/activity+json, application/ld+json')
|
||||||
end
|
end
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 444 B After Width: | Height: | Size: 588 B |
Binary file not shown.
Before Width: | Height: | Size: 735 B After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.6 KiB |
@ -1,89 +0,0 @@
|
|||||||
import { changeCompose } from '../actions/compose';
|
|
||||||
|
|
||||||
export const UTILBTNS_GOJI = 'UTILBTNS_GOJI';
|
|
||||||
export const UTILBTNS_HARUKIN = 'UTILBTNS_HARUKIN';
|
|
||||||
export const UTILBTNS_RISA = 'UTILBTNS_RISA';
|
|
||||||
|
|
||||||
export function submitGoji (textarea) {
|
|
||||||
return function (dispatch, getState) {
|
|
||||||
if (!textarea.value) {
|
|
||||||
let text = [
|
|
||||||
"#ゴジモリィィィィイイ",
|
|
||||||
":goji:"
|
|
||||||
].join("\r\n");
|
|
||||||
|
|
||||||
dispatch(submitGojiRequest());
|
|
||||||
dispatch(changeCompose(text));
|
|
||||||
|
|
||||||
textarea.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function submitGojiRequest () {
|
|
||||||
return {
|
|
||||||
type: UTILBTNS_GOJI
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function submitHarukin (textarea) {
|
|
||||||
return function (dispatch, getState) {
|
|
||||||
const HARUKINS = [":harukin: ", ":harukin_old: ", ":harukin_ika: ", ":harukin_tako: "];
|
|
||||||
const MAX = 6;
|
|
||||||
|
|
||||||
if (!textarea.value) {
|
|
||||||
let text = "";
|
|
||||||
|
|
||||||
let quantity = Math.round(Math.random() * MAX + 1);
|
|
||||||
let type = Math.round(Math.random() * (HARUKINS.length - 1));
|
|
||||||
|
|
||||||
let harukin = HARUKINS[type];
|
|
||||||
|
|
||||||
switch (quantity) {
|
|
||||||
default:
|
|
||||||
text = [
|
|
||||||
harukin.repeat(quantity),
|
|
||||||
"🔥 ".repeat(quantity)
|
|
||||||
].join("\r\n");
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MAX + 1:
|
|
||||||
text = `${harukin}💕\r\n`.repeat(6);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(submitHarukinRequest());
|
|
||||||
dispatch(changeCompose(text));
|
|
||||||
|
|
||||||
textarea.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function submitHarukinRequest () {
|
|
||||||
return {
|
|
||||||
type: UTILBTNS_HARUKIN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function submitRisa (textarea) {
|
|
||||||
return function (dispatch, getState) {
|
|
||||||
if (!textarea.value) {
|
|
||||||
let text = [
|
|
||||||
"@risa2 "
|
|
||||||
].join("\r\n");
|
|
||||||
|
|
||||||
dispatch(submitRisaRequest());
|
|
||||||
dispatch(changeCompose(text));
|
|
||||||
|
|
||||||
textarea.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function submitRisaRequest () {
|
|
||||||
return {
|
|
||||||
type: UTILBTNS_RISA
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,5 @@
|
|||||||
import { fetchRelationships } from './accounts';
|
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
import { importFetchedStatuses } from './importer';
|
import { importFetchedStatuses } from './importer';
|
||||||
import { uniq } from '../utils/uniq';
|
|
||||||
|
|
||||||
export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST';
|
export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST';
|
||||||
export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS';
|
export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS';
|
||||||
@ -22,7 +20,6 @@ export function fetchBookmarkedStatuses() {
|
|||||||
api(getState).get('/api/v1/bookmarks').then(response => {
|
api(getState).get('/api/v1/bookmarks').then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
dispatch(importFetchedStatuses(response.data));
|
dispatch(importFetchedStatuses(response.data));
|
||||||
dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
|
|
||||||
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchBookmarkedStatusesFail(error));
|
dispatch(fetchBookmarkedStatusesFail(error));
|
||||||
@ -64,7 +61,6 @@ export function expandBookmarkedStatuses() {
|
|||||||
api(getState).get(url).then(response => {
|
api(getState).get(url).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
dispatch(importFetchedStatuses(response.data));
|
dispatch(importFetchedStatuses(response.data));
|
||||||
dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
|
|
||||||
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(expandBookmarkedStatusesFail(error));
|
dispatch(expandBookmarkedStatusesFail(error));
|
||||||
|
@ -22,8 +22,6 @@ export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
|
|||||||
export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
|
export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
|
||||||
export const COMPOSE_REPLY = 'COMPOSE_REPLY';
|
export const COMPOSE_REPLY = 'COMPOSE_REPLY';
|
||||||
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
|
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
|
||||||
export const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
|
|
||||||
export const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL';
|
|
||||||
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
|
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
|
||||||
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
|
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
|
||||||
export const COMPOSE_RESET = 'COMPOSE_RESET';
|
export const COMPOSE_RESET = 'COMPOSE_RESET';
|
||||||
@ -122,23 +120,6 @@ export function cancelReplyCompose() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function quoteCompose(status, routerHistory) {
|
|
||||||
return (dispatch, getState) => {
|
|
||||||
dispatch({
|
|
||||||
type: COMPOSE_QUOTE,
|
|
||||||
status: status,
|
|
||||||
});
|
|
||||||
|
|
||||||
ensureComposeIsVisible(getState, routerHistory);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function cancelQuoteCompose() {
|
|
||||||
return {
|
|
||||||
type: COMPOSE_QUOTE_CANCEL,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function resetCompose() {
|
export function resetCompose() {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_RESET,
|
type: COMPOSE_RESET,
|
||||||
@ -212,7 +193,6 @@ export function submitCompose(routerHistory) {
|
|||||||
visibility: getState().getIn(['compose', 'privacy']),
|
visibility: getState().getIn(['compose', 'privacy']),
|
||||||
poll: getState().getIn(['compose', 'poll'], null),
|
poll: getState().getIn(['compose', 'poll'], null),
|
||||||
language: getState().getIn(['compose', 'language']),
|
language: getState().getIn(['compose', 'language']),
|
||||||
quote_id: getState().getIn(['compose', 'quote_from'], null),
|
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { fetchRelationships } from './accounts';
|
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
import { importFetchedStatuses } from './importer';
|
import { importFetchedStatuses } from './importer';
|
||||||
import { uniq } from '../utils/uniq';
|
|
||||||
|
|
||||||
export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST';
|
export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST';
|
||||||
export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS';
|
export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS';
|
||||||
@ -22,7 +20,6 @@ export function fetchFavouritedStatuses() {
|
|||||||
api(getState).get('/api/v1/favourites').then(response => {
|
api(getState).get('/api/v1/favourites').then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
dispatch(importFetchedStatuses(response.data));
|
dispatch(importFetchedStatuses(response.data));
|
||||||
dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
|
|
||||||
dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
|
dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchFavouritedStatusesFail(error));
|
dispatch(fetchFavouritedStatusesFail(error));
|
||||||
@ -67,7 +64,6 @@ export function expandFavouritedStatuses() {
|
|||||||
api(getState).get(url).then(response => {
|
api(getState).get(url).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
dispatch(importFetchedStatuses(response.data));
|
dispatch(importFetchedStatuses(response.data));
|
||||||
dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
|
|
||||||
dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));
|
dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(expandFavouritedStatusesFail(error));
|
dispatch(expandFavouritedStatusesFail(error));
|
||||||
|
@ -80,10 +80,6 @@ export function importFetchedStatuses(statuses) {
|
|||||||
processStatus(status.reblog);
|
processStatus(status.reblog);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.quote && status.quote.id) {
|
|
||||||
processStatus(status.quote);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.poll && status.poll.id) {
|
if (status.poll && status.poll.id) {
|
||||||
pushUnique(polls, normalizePoll(status.poll));
|
pushUnique(polls, normalizePoll(status.poll));
|
||||||
}
|
}
|
||||||
|
@ -75,8 +75,6 @@ export function normalizeStatus(status, normalOldStatus) {
|
|||||||
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
|
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
|
||||||
normalStatus.spoiler_text = normalOldStatus.get('spoiler_text');
|
normalStatus.spoiler_text = normalOldStatus.get('spoiler_text');
|
||||||
normalStatus.hidden = normalOldStatus.get('hidden');
|
normalStatus.hidden = normalOldStatus.get('hidden');
|
||||||
normalStatus.quote = normalOldStatus.get('quote');
|
|
||||||
normalStatus.quote_hidden = normalOldStatus.get('quote_hidden');
|
|
||||||
} else {
|
} else {
|
||||||
// If the status has a CW but no contents, treat the CW as if it were the
|
// If the status has a CW but no contents, treat the CW as if it were the
|
||||||
// status' contents, to avoid having a CW toggle with seemingly no effect.
|
// status' contents, to avoid having a CW toggle with seemingly no effect.
|
||||||
@ -93,11 +91,6 @@ export function normalizeStatus(status, normalOldStatus) {
|
|||||||
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
|
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
|
||||||
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
|
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
|
||||||
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
|
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
|
||||||
|
|
||||||
if (status.quote) {
|
|
||||||
normalStatus.quote = normalizeStatus(status.quote, null);
|
|
||||||
normalStatus.quote_hidden = normalStatus.quote.hidden;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalStatus;
|
return normalStatus;
|
||||||
|
@ -28,9 +28,6 @@ export const STATUS_REVEAL = 'STATUS_REVEAL';
|
|||||||
export const STATUS_HIDE = 'STATUS_HIDE';
|
export const STATUS_HIDE = 'STATUS_HIDE';
|
||||||
export const STATUS_COLLAPSE = 'STATUS_COLLAPSE';
|
export const STATUS_COLLAPSE = 'STATUS_COLLAPSE';
|
||||||
|
|
||||||
export const QUOTE_REVEAL = 'QUOTE_REVEAL';
|
|
||||||
export const QUOTE_HIDE = 'QUOTE_HIDE';
|
|
||||||
|
|
||||||
export const REDRAFT = 'REDRAFT';
|
export const REDRAFT = 'REDRAFT';
|
||||||
|
|
||||||
export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
|
export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
|
||||||
@ -350,25 +347,3 @@ export const undoStatusTranslation = id => ({
|
|||||||
type: STATUS_TRANSLATE_UNDO,
|
type: STATUS_TRANSLATE_UNDO,
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function hideQuote(ids) {
|
|
||||||
if (!Array.isArray(ids)) {
|
|
||||||
ids = [ids];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: QUOTE_HIDE,
|
|
||||||
ids,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function revealQuote(ids) {
|
|
||||||
if (!Array.isArray(ids)) {
|
|
||||||
ids = [ids];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: QUOTE_REVEAL,
|
|
||||||
ids,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import { fetchRelationships } from './accounts';
|
|
||||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||||
import { submitMarkers } from './markers';
|
import { submitMarkers } from './markers';
|
||||||
import api, { getLinks } from 'mastodon/api';
|
import api, { getLinks } from 'mastodon/api';
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
import compareId from 'mastodon/compare_id';
|
import compareId from 'mastodon/compare_id';
|
||||||
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
|
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
|
||||||
import { uniq } from '../utils/uniq';
|
|
||||||
|
|
||||||
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
|
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
|
||||||
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
|
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
|
||||||
@ -41,7 +39,6 @@ export function updateTimeline(timeline, status, accept) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispatch(importFetchedStatus(status));
|
dispatch(importFetchedStatus(status));
|
||||||
dispatch(fetchRelationships([status.reblog ? status.reblog.account.id : status.account.id, status.quote ? status.quote.account.id : null].filter(x => x)));
|
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: TIMELINE_UPDATE,
|
type: TIMELINE_UPDATE,
|
||||||
@ -114,7 +111,6 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
|
|||||||
api(getState).get(path, { params }).then(response => {
|
api(getState).get(path, { params }).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
dispatch(importFetchedStatuses(response.data));
|
dispatch(importFetchedStatuses(response.data));
|
||||||
dispatch(fetchRelationships(uniq(response.data.map(item => [item.reblog ? item.reblog.account.id : item.account.id, item.quote ? item.quote.account.id : null]).flat().filter(x => x))));
|
|
||||||
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
|
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
|
||||||
|
|
||||||
if (timelineId === 'home') {
|
if (timelineId === 'home') {
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const Logo = () => (
|
const Logo = () => (
|
||||||
<img src="/y-zu-logo.svg" height="32px" />
|
<svg viewBox='0 0 261 66' className='logo' role='img'>
|
||||||
|
<title>Mastodon</title>
|
||||||
|
<use xlinkHref='#logo-symbol-wordmark' />
|
||||||
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default Logo;
|
export default Logo;
|
||||||
|
@ -236,12 +236,10 @@ class MediaGallery extends React.PureComponent {
|
|||||||
visible: PropTypes.bool,
|
visible: PropTypes.bool,
|
||||||
autoplay: PropTypes.bool,
|
autoplay: PropTypes.bool,
|
||||||
onToggleVisibility: PropTypes.func,
|
onToggleVisibility: PropTypes.func,
|
||||||
quote: PropTypes.bool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
standalone: false,
|
standalone: false,
|
||||||
quote: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -312,7 +310,7 @@ class MediaGallery extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { media, intl, sensitive, height, defaultWidth, standalone, autoplay, quote } = this.props;
|
const { media, intl, sensitive, height, defaultWidth, standalone, autoplay } = this.props;
|
||||||
const { visible } = this.state;
|
const { visible } = this.state;
|
||||||
|
|
||||||
const width = this.state.width || defaultWidth;
|
const width = this.state.width || defaultWidth;
|
||||||
@ -331,10 +329,6 @@ class MediaGallery extends React.PureComponent {
|
|||||||
style.height = height;
|
style.height = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (quote && style.height) {
|
|
||||||
style.height /= 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
const size = media.take(4).size;
|
const size = media.take(4).size;
|
||||||
const uncached = media.every(attachment => attachment.get('type') === 'unknown');
|
const uncached = media.every(attachment => attachment.get('type') === 'unknown');
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Avatar from './avatar';
|
import Avatar from './avatar';
|
||||||
@ -23,29 +22,6 @@ import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_
|
|||||||
// to use the progress bar to show download progress
|
// to use the progress bar to show download progress
|
||||||
import Bundle from '../features/ui/components/bundle';
|
import Bundle from '../features/ui/components/bundle';
|
||||||
|
|
||||||
export const mapStateToProps = (state, props) => {
|
|
||||||
let status = props.status;
|
|
||||||
|
|
||||||
if (status === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
|
|
||||||
status = status.get('reblog');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.get('quote', null) === null) {
|
|
||||||
return {
|
|
||||||
quoteMuted: !!status.get('quote_id', null),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = status.getIn(['quote', 'account', 'id'], null);
|
|
||||||
return {
|
|
||||||
quoteMuted: id !== null && (state.getIn(['relationships', id, 'muting']) || state.getIn(['relationships', id, 'blocking']) || state.getIn(['relationships', id, 'blocked_by']) || state.getIn(['relationships', id, 'domain_blocking']) || status.getIn(['quote', 'muted'])),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
|
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
|
||||||
const displayName = status.getIn(['account', 'display_name']);
|
const displayName = status.getIn(['account', 'display_name']);
|
||||||
|
|
||||||
@ -83,61 +59,7 @@ const messages = defineMessages({
|
|||||||
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
|
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const quote = (status, muted, quoteMuted, handleQuoteClick, handleExpandedQuoteToggle, identity, media, router, contextType = 'home') => {
|
export default @injectIntl
|
||||||
const quoteStatus = status.get('quote', null);
|
|
||||||
|
|
||||||
if (!quoteStatus) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const quoteInner = (() => {
|
|
||||||
const hideUnlisted = quoteStatus.get('visibility') === 'unlisted'
|
|
||||||
&& ['public', 'community', 'hashtag'].includes(contextType);
|
|
||||||
if (quoteMuted || hideUnlisted) {
|
|
||||||
const content = (() => {
|
|
||||||
if (quoteMuted) {
|
|
||||||
return (
|
|
||||||
<FormattedMessage id='status.muted_quote' defaultMessage='Muted quote' />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button onClick={handleQuoteClick}>
|
|
||||||
<FormattedMessage id='status.unlisted_quote' defaultMessage='Unlisted quote' />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames('status__content', { 'muted-quote': quoteMuted, 'unlisted-quote': hideUnlisted, 'status__content--with-action': router })}>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className='status__info'>
|
|
||||||
{identity(quoteStatus, null, null, true)}
|
|
||||||
</div>
|
|
||||||
<StatusContent status={quoteStatus} onClick={handleQuoteClick} expanded={!status.get('quote_hidden')} onExpandedToggle={handleExpandedQuoteToggle} quote />
|
|
||||||
{media(quoteStatus, true)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames('quote-status', `status-${quoteStatus.get('visibility')}`, { muted: muted })}
|
|
||||||
data-id={quoteStatus.get('id')}
|
|
||||||
dataurl={quoteStatus.get('url')}
|
|
||||||
>
|
|
||||||
{quoteInner}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default @connect(mapStateToProps) @injectIntl
|
|
||||||
class Status extends ImmutablePureComponent {
|
class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
@ -165,9 +87,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
onToggleCollapsed: PropTypes.func,
|
onToggleCollapsed: PropTypes.func,
|
||||||
onTranslate: PropTypes.func,
|
onTranslate: PropTypes.func,
|
||||||
onInteractionModal: PropTypes.func,
|
onInteractionModal: PropTypes.func,
|
||||||
onQuoteToggleHidden: PropTypes.func,
|
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
quoteMuted: PropTypes.bool,
|
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
unread: PropTypes.bool,
|
unread: PropTypes.bool,
|
||||||
onMoveUp: PropTypes.func,
|
onMoveUp: PropTypes.func,
|
||||||
@ -183,7 +103,6 @@ class Status extends ImmutablePureComponent {
|
|||||||
inUse: PropTypes.bool,
|
inUse: PropTypes.bool,
|
||||||
available: PropTypes.bool,
|
available: PropTypes.bool,
|
||||||
}),
|
}),
|
||||||
contextType: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Avoid checking props that are functions (and whose equality will always
|
// Avoid checking props that are functions (and whose equality will always
|
||||||
@ -195,12 +114,10 @@ class Status extends ImmutablePureComponent {
|
|||||||
'hidden',
|
'hidden',
|
||||||
'unread',
|
'unread',
|
||||||
'pictureInPicture',
|
'pictureInPicture',
|
||||||
'quoteMuted',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
showMedia: defaultMediaVisibility(this.props.status),
|
showMedia: defaultMediaVisibility(this.props.status),
|
||||||
showQuoteMedia: defaultMediaVisibility(this.props.status ? this.props.status.get('quote', null) : null),
|
|
||||||
statusId: undefined,
|
statusId: undefined,
|
||||||
forceFilter: undefined,
|
forceFilter: undefined,
|
||||||
};
|
};
|
||||||
@ -209,7 +126,6 @@ class Status extends ImmutablePureComponent {
|
|||||||
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
|
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
|
||||||
return {
|
return {
|
||||||
showMedia: defaultMediaVisibility(nextProps.status),
|
showMedia: defaultMediaVisibility(nextProps.status),
|
||||||
showQuoteMedia: defaultMediaVisibility(nextProps.status ? nextProps.status.get('quote', null) : null),
|
|
||||||
statusId: nextProps.status.get('id'),
|
statusId: nextProps.status.get('id'),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@ -221,10 +137,6 @@ class Status extends ImmutablePureComponent {
|
|||||||
this.setState({ showMedia: !this.state.showMedia });
|
this.setState({ showMedia: !this.state.showMedia });
|
||||||
};
|
};
|
||||||
|
|
||||||
handleToggleQuoteMediaVisibility = () => {
|
|
||||||
this.setState({ showQuoteMedia: !this.state.showQuoteMedia });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick = e => {
|
handleClick = e => {
|
||||||
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
|
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
|
||||||
return;
|
return;
|
||||||
@ -253,15 +165,6 @@ class Status extends ImmutablePureComponent {
|
|||||||
this._openProfile(proper);
|
this._openProfile(proper);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleQuoteClick = () => {
|
|
||||||
if (!this.context.router) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { status } = this.props;
|
|
||||||
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'quote', 'id'], status.getIn(['quote', 'id']))}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleExpandedToggle = () => {
|
handleExpandedToggle = () => {
|
||||||
this.props.onToggleHidden(this._properStatus());
|
this.props.onToggleHidden(this._properStatus());
|
||||||
};
|
};
|
||||||
@ -274,10 +177,6 @@ class Status extends ImmutablePureComponent {
|
|||||||
this.props.onTranslate(this._properStatus());
|
this.props.onTranslate(this._properStatus());
|
||||||
};
|
};
|
||||||
|
|
||||||
handleExpandedQuoteToggle = () => {
|
|
||||||
this.props.onQuoteToggleHidden(this._properStatus());
|
|
||||||
}
|
|
||||||
|
|
||||||
renderLoadingMediaGallery () {
|
renderLoadingMediaGallery () {
|
||||||
return <div className='media-gallery' style={{ height: '110px' }} />;
|
return <div className='media-gallery' style={{ height: '110px' }} />;
|
||||||
}
|
}
|
||||||
@ -410,9 +309,10 @@ class Status extends ImmutablePureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let prepend, rebloggedByText;
|
let media = null;
|
||||||
|
let statusAvatar, prepend, rebloggedByText;
|
||||||
|
|
||||||
const { intl, hidden, featured, unread, showThread, scrollKey, pictureInPicture, quoteMuted, contextType } = this.props;
|
const { intl, hidden, featured, unread, showThread, scrollKey, pictureInPicture } = this.props;
|
||||||
|
|
||||||
let { status, account, ...other } = this.props;
|
let { status, account, ...other } = this.props;
|
||||||
|
|
||||||
@ -497,123 +397,103 @@ class Status extends ImmutablePureComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const media = (status, quote = false) => {
|
if (pictureInPicture.get('inUse')) {
|
||||||
if (pictureInPicture.get('inUse')) {
|
media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />;
|
||||||
return <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />;
|
} else if (status.get('media_attachments').size > 0) {
|
||||||
} else if (status.get('media_attachments').size > 0) {
|
if (this.props.muted) {
|
||||||
if (this.props.muted) {
|
media = (
|
||||||
return (
|
<AttachmentList
|
||||||
<AttachmentList
|
|
||||||
compact
|
|
||||||
media={status.get('media_attachments')}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
|
||||||
const attachment = status.getIn(['media_attachments', 0]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer}>
|
|
||||||
{Component => (
|
|
||||||
<Component
|
|
||||||
src={attachment.get('url')}
|
|
||||||
alt={attachment.get('description')}
|
|
||||||
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
|
||||||
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
|
||||||
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
|
|
||||||
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
|
|
||||||
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
|
||||||
width={this.props.cachedMediaWidth}
|
|
||||||
height={110}
|
|
||||||
cacheWidth={this.props.cacheMediaWidth}
|
|
||||||
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
|
|
||||||
sensitive={status.get('sensitive')}
|
|
||||||
blurhash={attachment.get('blurhash')}
|
|
||||||
visible={this.state.showMedia}
|
|
||||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
|
||||||
quote={quote}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Bundle>
|
|
||||||
);
|
|
||||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
|
||||||
const attachment = status.getIn(['media_attachments', 0]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer}>
|
|
||||||
{Component => (
|
|
||||||
<Component
|
|
||||||
preview={attachment.get('preview_url')}
|
|
||||||
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
|
||||||
blurhash={attachment.get('blurhash')}
|
|
||||||
src={attachment.get('url')}
|
|
||||||
alt={attachment.get('description')}
|
|
||||||
width={this.props.cachedMediaWidth}
|
|
||||||
height={110}
|
|
||||||
inline
|
|
||||||
sensitive={status.get('sensitive')}
|
|
||||||
onOpenVideo={this.handleOpenVideo}
|
|
||||||
cacheWidth={this.props.cacheMediaWidth}
|
|
||||||
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
|
|
||||||
visible={quote ? this.state.showQuoteMedia : this.state.showMedia}
|
|
||||||
onToggleVisibility={quote ? this.handleToggleQuoteMediaVisibility : this.handleToggleMediaVisibility}
|
|
||||||
quote={quote}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Bundle>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
|
||||||
{Component => (
|
|
||||||
<Component
|
|
||||||
media={status.get('media_attachments')}
|
|
||||||
sensitive={status.get('sensitive')}
|
|
||||||
height={110}
|
|
||||||
onOpenMedia={this.handleOpenMedia}
|
|
||||||
cacheWidth={this.props.cacheMediaWidth}
|
|
||||||
defaultWidth={this.props.cachedMediaWidth}
|
|
||||||
visible={quote ? this.state.showQuoteMedia : this.state.showMedia}
|
|
||||||
onToggleVisibility={quote ? this.handleToggleQuoteMediaVisibility : this.handleToggleMediaVisibility}
|
|
||||||
quote={quote}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Bundle>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (status.get('spoiler_text').length === 0 && status.get('card') && !this.props.muted) {
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
onOpenMedia={this.handleOpenMedia}
|
|
||||||
card={status.get('card')}
|
|
||||||
compact
|
compact
|
||||||
cacheWidth={this.props.cacheMediaWidth}
|
media={status.get('media_attachments')}
|
||||||
defaultWidth={this.props.cachedMediaWidth}
|
|
||||||
sensitive={status.get('sensitive')}
|
|
||||||
quote={quote}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||||
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
|
|
||||||
return null;
|
media = (
|
||||||
};
|
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
|
||||||
|
{Component => (
|
||||||
|
<Component
|
||||||
|
src={attachment.get('url')}
|
||||||
|
alt={attachment.get('description')}
|
||||||
|
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
||||||
|
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
||||||
|
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
|
||||||
|
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
|
||||||
|
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
||||||
|
width={this.props.cachedMediaWidth}
|
||||||
|
height={110}
|
||||||
|
cacheWidth={this.props.cacheMediaWidth}
|
||||||
|
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
blurhash={attachment.get('blurhash')}
|
||||||
|
visible={this.state.showMedia}
|
||||||
|
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Bundle>
|
||||||
|
);
|
||||||
|
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||||
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
|
|
||||||
const statusAvatar = (status, account) => {
|
media = (
|
||||||
if (account === undefined || account === null) {
|
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
||||||
return <Avatar account={status.get('account')} size={46} />;
|
{Component => (
|
||||||
|
<Component
|
||||||
|
preview={attachment.get('preview_url')}
|
||||||
|
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
||||||
|
blurhash={attachment.get('blurhash')}
|
||||||
|
src={attachment.get('url')}
|
||||||
|
alt={attachment.get('description')}
|
||||||
|
width={this.props.cachedMediaWidth}
|
||||||
|
height={110}
|
||||||
|
inline
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
onOpenVideo={this.handleOpenVideo}
|
||||||
|
cacheWidth={this.props.cacheMediaWidth}
|
||||||
|
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
|
||||||
|
visible={this.state.showMedia}
|
||||||
|
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Bundle>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return <AvatarOverlay account={status.get('account')} friend={account} />;
|
media = (
|
||||||
|
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
||||||
|
{Component => (
|
||||||
|
<Component
|
||||||
|
media={status.get('media_attachments')}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
height={110}
|
||||||
|
onOpenMedia={this.handleOpenMedia}
|
||||||
|
cacheWidth={this.props.cacheMediaWidth}
|
||||||
|
defaultWidth={this.props.cachedMediaWidth}
|
||||||
|
visible={this.state.showMedia}
|
||||||
|
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Bundle>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
} else if (status.get('spoiler_text').length === 0 && status.get('card') && !this.props.muted) {
|
||||||
|
media = (
|
||||||
|
<Card
|
||||||
|
onOpenMedia={this.handleOpenMedia}
|
||||||
|
card={status.get('card')}
|
||||||
|
compact
|
||||||
|
cacheWidth={this.props.cacheMediaWidth}
|
||||||
|
defaultWidth={this.props.cachedMediaWidth}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const identity = (status, account) => (
|
if (account === undefined || account === null) {
|
||||||
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
|
statusAvatar = <Avatar account={status.get('account')} size={46} />;
|
||||||
<div className='status__avatar'>
|
} else {
|
||||||
{statusAvatar(status, account)}
|
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
|
||||||
</div>
|
}
|
||||||
|
|
||||||
<DisplayName account={status.get('account')} />
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
|
|
||||||
const visibilityIconInfo = {
|
const visibilityIconInfo = {
|
||||||
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
|
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
|
||||||
@ -636,7 +516,13 @@ class Status extends ImmutablePureComponent {
|
|||||||
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
|
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{identity(status, account, false)}
|
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
|
||||||
|
<div className='status__avatar'>
|
||||||
|
{statusAvatar}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DisplayName account={status.get('account')} />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<StatusContent
|
<StatusContent
|
||||||
@ -649,9 +535,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
onCollapsedToggle={this.handleCollapsedToggle}
|
onCollapsedToggle={this.handleCollapsedToggle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{media(status)}
|
{media}
|
||||||
|
|
||||||
{quote(status, this.props.muted, quoteMuted, this.handleQuoteClick, this.handleExpandedQuoteToggle, identity, media, this.context.router, contextType)}
|
|
||||||
|
|
||||||
<StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters ? this.handleFilterClick : null} {...other} />
|
<StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters ? this.handleFilterClick : null} {...other} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,8 +26,6 @@ const messages = defineMessages({
|
|||||||
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
|
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
|
||||||
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||||
quote: { id: 'status.quote', defaultMessage: 'Quote' },
|
|
||||||
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
|
|
||||||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||||
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
||||||
removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' },
|
removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' },
|
||||||
@ -70,7 +68,6 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
onReply: PropTypes.func,
|
onReply: PropTypes.func,
|
||||||
onFavourite: PropTypes.func,
|
onFavourite: PropTypes.func,
|
||||||
onReblog: PropTypes.func,
|
onReblog: PropTypes.func,
|
||||||
onQuote: PropTypes.func,
|
|
||||||
onDelete: PropTypes.func,
|
onDelete: PropTypes.func,
|
||||||
onDirect: PropTypes.func,
|
onDirect: PropTypes.func,
|
||||||
onMention: PropTypes.func,
|
onMention: PropTypes.func,
|
||||||
@ -141,10 +138,6 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleQuoteClick = () => {
|
|
||||||
this.props.onQuote(this.props.status, this.context.router.history);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleBookmarkClick = () => {
|
handleBookmarkClick = () => {
|
||||||
this.props.onBookmark(this.props.status);
|
this.props.onBookmark(this.props.status);
|
||||||
};
|
};
|
||||||
@ -238,14 +231,6 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
this.props.onFilter();
|
this.props.onFilter();
|
||||||
};
|
};
|
||||||
|
|
||||||
static quoteTitle = (intl, messages, publicStatus) => {
|
|
||||||
if (publicStatus) {
|
|
||||||
return intl.formatMessage(messages.quote);
|
|
||||||
} else {
|
|
||||||
return intl.formatMessage(messages.cannot_quote);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
|
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
|
||||||
const { signedIn, permissions } = this.context.identity;
|
const { signedIn, permissions } = this.context.identity;
|
||||||
@ -376,8 +361,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
<div className='status__action-bar'>
|
<div className='status__action-bar'>
|
||||||
<IconButton className='status__action-bar__button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
<IconButton className='status__action-bar__button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
||||||
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
|
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
|
||||||
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
|
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
|
||||||
<IconButton className='status__action-bar__button' disabled={!publicStatus} title={StatusActionBar.quoteTitle(intl, messages, publicStatus)} icon='quote-right' onClick={this.handleQuoteClick} />
|
|
||||||
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
||||||
|
|
||||||
{shareButton}
|
{shareButton}
|
||||||
|
@ -9,7 +9,6 @@ import Icon from 'mastodon/components/icon';
|
|||||||
import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'mastodon/initial_state';
|
import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'mastodon/initial_state';
|
||||||
|
|
||||||
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
|
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
|
||||||
const QUOTE_MAX_HEIGHT = 112; // 22px * 5 (+ 2px padding at the top)
|
|
||||||
|
|
||||||
class TranslateButton extends React.PureComponent {
|
class TranslateButton extends React.PureComponent {
|
||||||
|
|
||||||
@ -65,7 +64,6 @@ class StatusContent extends React.PureComponent {
|
|||||||
collapsable: PropTypes.bool,
|
collapsable: PropTypes.bool,
|
||||||
onCollapsedToggle: PropTypes.func,
|
onCollapsedToggle: PropTypes.func,
|
||||||
intl: PropTypes.object,
|
intl: PropTypes.object,
|
||||||
quote: PropTypes.bool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -109,12 +107,12 @@ class StatusContent extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('collapsed', null) === null && onCollapsedToggle) {
|
if (status.get('collapsed', null) === null && onCollapsedToggle) {
|
||||||
const { collapsable, onClick, quote } = this.props;
|
const { collapsable, onClick } = this.props;
|
||||||
|
|
||||||
const collapsed =
|
const collapsed =
|
||||||
collapsable
|
collapsable
|
||||||
&& onClick
|
&& onClick
|
||||||
&& node.clientHeight > (quote ? QUOTE_MAX_HEIGHT : MAX_HEIGHT)
|
&& node.clientHeight > MAX_HEIGHT
|
||||||
&& status.get('spoiler_text').length === 0;
|
&& status.get('spoiler_text').length === 0;
|
||||||
|
|
||||||
onCollapsedToggle(collapsed);
|
onCollapsedToggle(collapsed);
|
||||||
@ -218,7 +216,7 @@ class StatusContent extends React.PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, intl, quote } = this.props;
|
const { status, intl } = this.props;
|
||||||
|
|
||||||
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
|
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
|
||||||
const renderReadMore = this.props.onClick && status.get('collapsed');
|
const renderReadMore = this.props.onClick && status.get('collapsed');
|
||||||
@ -247,12 +245,6 @@ class StatusContent extends React.PureComponent {
|
|||||||
<PollContainer pollId={status.get('poll')} />
|
<PollContainer pollId={status.get('poll')} />
|
||||||
);
|
);
|
||||||
|
|
||||||
if (quote) {
|
|
||||||
const doc = new DOMParser().parseFromString(content.__html, 'text/html').documentElement;
|
|
||||||
Array.from(doc.querySelectorAll('br')).forEach(nl => nl.replaceWith(' '));
|
|
||||||
content.__html = doc.outerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.get('spoiler_text').length > 0) {
|
if (status.get('spoiler_text').length > 0) {
|
||||||
let mentionsPlaceholder = '';
|
let mentionsPlaceholder = '';
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ import Status from '../components/status';
|
|||||||
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
|
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
|
||||||
import {
|
import {
|
||||||
replyCompose,
|
replyCompose,
|
||||||
quoteCompose,
|
|
||||||
mentionCompose,
|
mentionCompose,
|
||||||
directCompose,
|
directCompose,
|
||||||
} from '../actions/compose';
|
} from '../actions/compose';
|
||||||
@ -28,8 +27,6 @@ import {
|
|||||||
editStatus,
|
editStatus,
|
||||||
translateStatus,
|
translateStatus,
|
||||||
undoStatusTranslation,
|
undoStatusTranslation,
|
||||||
hideQuote,
|
|
||||||
revealQuote,
|
|
||||||
} from '../actions/statuses';
|
} from '../actions/statuses';
|
||||||
import {
|
import {
|
||||||
unmuteAccount,
|
unmuteAccount,
|
||||||
@ -61,8 +58,6 @@ const messages = defineMessages({
|
|||||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||||
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
|
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
|
||||||
editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||||
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
|
|
||||||
quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
|
||||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -112,22 +107,6 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onQuote (status, router) {
|
|
||||||
dispatch((_, getState) => {
|
|
||||||
let state = getState();
|
|
||||||
|
|
||||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
|
||||||
dispatch(openModal('CONFIRM', {
|
|
||||||
message: intl.formatMessage(messages.quoteMessage),
|
|
||||||
confirm: intl.formatMessage(messages.quoteConfirm),
|
|
||||||
onConfirm: () => dispatch(quoteCompose(status, router)),
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
dispatch(quoteCompose(status, router));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onFavourite (status) {
|
onFavourite (status) {
|
||||||
if (status.get('favourited')) {
|
if (status.get('favourited')) {
|
||||||
dispatch(unfavourite(status));
|
dispatch(unfavourite(status));
|
||||||
@ -255,14 +234,6 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
|||||||
dispatch(toggleStatusCollapse(status.get('id'), isCollapsed));
|
dispatch(toggleStatusCollapse(status.get('id'), isCollapsed));
|
||||||
},
|
},
|
||||||
|
|
||||||
onQuoteToggleHidden (status) {
|
|
||||||
if (status.get('quote_hidden')) {
|
|
||||||
dispatch(revealQuote(status.get('id')));
|
|
||||||
} else {
|
|
||||||
dispatch(hideQuote(status.get('id')));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onBlockDomain (domain) {
|
onBlockDomain (domain) {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
|
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
|
||||||
|
@ -50,7 +50,6 @@ class Audio extends React.PureComponent {
|
|||||||
volume: PropTypes.number,
|
volume: PropTypes.number,
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
deployPictureInPicture: PropTypes.func,
|
deployPictureInPicture: PropTypes.func,
|
||||||
quote: PropTypes.bool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -95,11 +94,7 @@ class Audio extends React.PureComponent {
|
|||||||
|
|
||||||
_setDimensions () {
|
_setDimensions () {
|
||||||
const width = this.player.offsetWidth;
|
const width = this.player.offsetWidth;
|
||||||
let height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9));
|
const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9));
|
||||||
|
|
||||||
if (this.props.quote) {
|
|
||||||
height /= 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.cacheWidth) {
|
if (this.props.cacheWidth) {
|
||||||
this.props.cacheWidth(width);
|
this.props.cacheWidth(width);
|
||||||
|
@ -29,9 +29,6 @@ const messages = defineMessages({
|
|||||||
publish: { id: 'compose_form.publish', defaultMessage: 'Publish' },
|
publish: { id: 'compose_form.publish', defaultMessage: 'Publish' },
|
||||||
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
||||||
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
|
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
|
||||||
utilBtns_goji: { id: 'compose_form.utilBtns_goji', defaultMessage: 'Typo!!!' },
|
|
||||||
utilBtns_harukin: { id: 'compose_form.utilBtns_harukin', defaultMessage: 'Burn Harukin' },
|
|
||||||
utilBtns_risa: { id: 'compose_form.utilBtns_risa', defaultMessage: 'Risa' }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
@ -68,9 +65,6 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
isInReply: PropTypes.bool,
|
isInReply: PropTypes.bool,
|
||||||
singleColumn: PropTypes.bool,
|
singleColumn: PropTypes.bool,
|
||||||
lang: PropTypes.string,
|
lang: PropTypes.string,
|
||||||
onGojiSubmit: PropTypes.func.isRequired,
|
|
||||||
onHarukinSubmit: PropTypes.func.isRequired,
|
|
||||||
onRisaSubmit: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -96,7 +90,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
const fulltext = this.getFulltextForCharacterCounting();
|
const fulltext = this.getFulltextForCharacterCounting();
|
||||||
const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0;
|
const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0;
|
||||||
|
|
||||||
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 2048 || (isOnlyWhitespace && !anyMedia));
|
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 500 || (isOnlyWhitespace && !anyMedia));
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSubmit = (e) => {
|
handleSubmit = (e) => {
|
||||||
@ -212,10 +206,6 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
this.props.onPickEmoji(position, data, needsSpace);
|
this.props.onPickEmoji(position, data, needsSpace);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleOnGojiSubmit = () => this.props.onGojiSubmit(this.autosuggestTextarea.textarea);
|
|
||||||
handleOnHarukinSubmit = () => this.props.onHarukinSubmit(this.autosuggestTextarea.textarea);
|
|
||||||
handleOnRisaSubmit = () => this.props.onRisaSubmit(this.autosuggestTextarea.textarea);
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, onPaste, autoFocus } = this.props;
|
const { intl, onPaste, autoFocus } = this.props;
|
||||||
const disabled = this.props.isSubmitting;
|
const disabled = this.props.isSubmitting;
|
||||||
@ -235,7 +225,6 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
<WarningContainer />
|
<WarningContainer />
|
||||||
|
|
||||||
<ReplyIndicatorContainer />
|
<ReplyIndicatorContainer />
|
||||||
<ReplyIndicatorContainer quote />
|
|
||||||
|
|
||||||
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}>
|
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}>
|
||||||
<AutosuggestInput
|
<AutosuggestInput
|
||||||
@ -291,13 +280,12 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='character-counter__wrapper'>
|
<div className='character-counter__wrapper'>
|
||||||
<CharacterCounter max={2048} text={this.getFulltextForCharacterCounting()} />
|
<CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='compose-form__publish'>
|
<div className='compose-form__publish'>
|
||||||
<div className='compose-form__publish-button-wrapper'>
|
<div className='compose-form__publish-button-wrapper'>
|
||||||
<Button className="compose-form__utilBtns-risa" text={intl.formatMessage(messages.utilBtns_risa)} onClick={this.handleOnRisaSubmit} block />
|
|
||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
text={publishText}
|
text={publishText}
|
||||||
@ -306,10 +294,6 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="compose-form__utilBtns">
|
|
||||||
<Button className="compose-form__utilBtns-goji" text={intl.formatMessage(messages.utilBtns_goji)} onClick={this.handleOnGojiSubmit} block />
|
|
||||||
<Button className="compose-form__utilBtns-harukin" text={intl.formatMessage(messages.utilBtns_harukin)} onClick={this.handleOnHarukinSubmit} block />
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import IconButton from '../../../components/icon_button';
|
|||||||
import DisplayName from '../../../components/display_name';
|
import DisplayName from '../../../components/display_name';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import classNames from 'classnames';
|
|
||||||
import AttachmentList from 'mastodon/components/attachment_list';
|
import AttachmentList from 'mastodon/components/attachment_list';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
@ -24,7 +23,6 @@ class ReplyIndicator extends ImmutablePureComponent {
|
|||||||
status: ImmutablePropTypes.map,
|
status: ImmutablePropTypes.map,
|
||||||
onCancel: PropTypes.func.isRequired,
|
onCancel: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
quote: PropTypes.bool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleClick = () => {
|
handleClick = () => {
|
||||||
@ -39,7 +37,7 @@ class ReplyIndicator extends ImmutablePureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, intl, quote } = this.props;
|
const { status, intl } = this.props;
|
||||||
|
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return null;
|
return null;
|
||||||
@ -48,7 +46,7 @@ class ReplyIndicator extends ImmutablePureComponent {
|
|||||||
const content = { __html: status.get('contentHtml') };
|
const content = { __html: status.get('contentHtml') };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('reply-indicator', { 'quote-indicator': quote })}>
|
<div className='reply-indicator'>
|
||||||
<div className='reply-indicator__header'>
|
<div className='reply-indicator__header'>
|
||||||
<div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} inverted /></div>
|
<div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} inverted /></div>
|
||||||
|
|
||||||
|
@ -10,11 +10,6 @@ import {
|
|||||||
insertEmojiCompose,
|
insertEmojiCompose,
|
||||||
uploadCompose,
|
uploadCompose,
|
||||||
} from '../../../actions/compose';
|
} from '../../../actions/compose';
|
||||||
import {
|
|
||||||
submitGoji,
|
|
||||||
submitHarukin,
|
|
||||||
submitRisa
|
|
||||||
} from '../../../actions/UtilBtns';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
text: state.getIn(['compose', 'text']),
|
text: state.getIn(['compose', 'text']),
|
||||||
@ -68,18 +63,6 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
dispatch(insertEmojiCompose(position, data, needsSpace));
|
dispatch(insertEmojiCompose(position, data, needsSpace));
|
||||||
},
|
},
|
||||||
|
|
||||||
onRisaSubmit (textarea) {
|
|
||||||
dispatch(submitRisa(textarea));
|
|
||||||
},
|
|
||||||
|
|
||||||
onGojiSubmit (textarea) {
|
|
||||||
dispatch(submitGoji(textarea));
|
|
||||||
},
|
|
||||||
|
|
||||||
onHarukinSubmit (textarea) {
|
|
||||||
dispatch(submitHarukin(textarea));
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ComposeForm);
|
export default connect(mapStateToProps, mapDispatchToProps)(ComposeForm);
|
||||||
|
@ -1,23 +1,22 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { cancelReplyCompose, cancelQuoteCompose } from '../../../actions/compose';
|
import { cancelReplyCompose } from '../../../actions/compose';
|
||||||
import { makeGetStatus } from '../../../selectors';
|
import { makeGetStatus } from '../../../selectors';
|
||||||
import ReplyIndicator from '../components/reply_indicator';
|
import ReplyIndicator from '../components/reply_indicator';
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
const getStatus = makeGetStatus();
|
const getStatus = makeGetStatus();
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
const mapStateToProps = state => {
|
||||||
let statusId = state.getIn(['compose', 'id'], null);
|
let statusId = state.getIn(['compose', 'id'], null);
|
||||||
let editing = true;
|
let editing = true;
|
||||||
|
|
||||||
if (statusId === null) {
|
if (statusId === null) {
|
||||||
statusId = state.getIn(['compose', props.quote ? 'quote_from' : 'in_reply_to']);
|
statusId = state.getIn(['compose', 'in_reply_to']);
|
||||||
editing = false;
|
editing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: getStatus(state, { id: statusId }),
|
status: getStatus(state, { id: statusId }),
|
||||||
quote: props.quote,
|
|
||||||
editing,
|
editing,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -27,8 +26,8 @@ const makeMapStateToProps = () => {
|
|||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
||||||
onCancel (quote) {
|
onCancel () {
|
||||||
dispatch(quote ? cancelQuoteCompose() : cancelReplyCompose());
|
dispatch(cancelReplyCompose());
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -4,11 +4,10 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import IconButton from 'mastodon/components/icon_button';
|
import IconButton from 'mastodon/components/icon_button';
|
||||||
import StatusActionBar from 'mastodon/components/status_action_bar';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { me, boostModal } from 'mastodon/initial_state';
|
import { me, boostModal } from 'mastodon/initial_state';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import { replyCompose, quoteCompose } from 'mastodon/actions/compose';
|
import { replyCompose } from 'mastodon/actions/compose';
|
||||||
import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions';
|
import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions';
|
||||||
import { makeGetStatus } from 'mastodon/selectors';
|
import { makeGetStatus } from 'mastodon/selectors';
|
||||||
import { initBoostModal } from 'mastodon/actions/boosts';
|
import { initBoostModal } from 'mastodon/actions/boosts';
|
||||||
@ -21,13 +20,9 @@ const messages = defineMessages({
|
|||||||
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
|
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
|
||||||
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||||
quote: { id: 'status.quote', defaultMessage: 'Quote' },
|
|
||||||
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
|
|
||||||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||||
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
|
|
||||||
quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
|
||||||
open: { id: 'status.open', defaultMessage: 'Expand this status' },
|
open: { id: 'status.open', defaultMessage: 'Expand this status' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -140,31 +135,6 @@ class Footer extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_performQuote = () => {
|
|
||||||
const { dispatch, status, onClose } = this.props;
|
|
||||||
const { router } = this.context;
|
|
||||||
|
|
||||||
if (onClose) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(quoteCompose(status, router.history));
|
|
||||||
}
|
|
||||||
|
|
||||||
handleQuoteClick = () => {
|
|
||||||
const { dispatch, askReplyConfirmation, intl } = this.props;
|
|
||||||
|
|
||||||
if (askReplyConfirmation) {
|
|
||||||
dispatch(openModal('CONFIRM', {
|
|
||||||
message: intl.formatMessage(messages.quoteMessage),
|
|
||||||
confirm: intl.formatMessage(messages.quoteConfirm),
|
|
||||||
onConfirm: this._performQuote,
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
this._performQuote();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleOpenClick = e => {
|
handleOpenClick = e => {
|
||||||
const { router } = this.context;
|
const { router } = this.context;
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import IconButton from '../../../components/icon_button';
|
import IconButton from '../../../components/icon_button';
|
||||||
import StatusActionBar from '../../../components/status_action_bar';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
@ -21,8 +20,6 @@ const messages = defineMessages({
|
|||||||
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
|
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
|
||||||
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||||
quote: { id: 'status.quote', defaultMessage: 'Quote' },
|
|
||||||
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
|
|
||||||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||||
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
||||||
more: { id: 'status.more', defaultMessage: 'More' },
|
more: { id: 'status.more', defaultMessage: 'More' },
|
||||||
@ -64,7 +61,6 @@ class ActionBar extends React.PureComponent {
|
|||||||
relationship: ImmutablePropTypes.map,
|
relationship: ImmutablePropTypes.map,
|
||||||
onReply: PropTypes.func.isRequired,
|
onReply: PropTypes.func.isRequired,
|
||||||
onReblog: PropTypes.func.isRequired,
|
onReblog: PropTypes.func.isRequired,
|
||||||
onQuote: PropTypes.func,
|
|
||||||
onFavourite: PropTypes.func.isRequired,
|
onFavourite: PropTypes.func.isRequired,
|
||||||
onBookmark: PropTypes.func.isRequired,
|
onBookmark: PropTypes.func.isRequired,
|
||||||
onDelete: PropTypes.func.isRequired,
|
onDelete: PropTypes.func.isRequired,
|
||||||
@ -92,10 +88,6 @@ class ActionBar extends React.PureComponent {
|
|||||||
this.props.onReblog(this.props.status, e);
|
this.props.onReblog(this.props.status, e);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleQuoteClick = () => {
|
|
||||||
this.props.onQuote(this.props.status, this.context.router.history);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFavouriteClick = () => {
|
handleFavouriteClick = () => {
|
||||||
this.props.onFavourite(this.props.status);
|
this.props.onFavourite(this.props.status);
|
||||||
};
|
};
|
||||||
@ -294,7 +286,6 @@ class ActionBar extends React.PureComponent {
|
|||||||
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
|
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
|
||||||
<div className='detailed-status__button' ><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
|
<div className='detailed-status__button' ><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
|
||||||
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
|
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
|
||||||
<div className='detailed-status__button'><IconButton disabled={!publicStatus} title={StatusActionBar.quoteTitle(intl, messages, publicStatus)} icon='quote-right' onClick={this.handleQuoteClick} /></div>
|
|
||||||
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
|
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
|
||||||
|
|
||||||
{shareButton}
|
{shareButton}
|
||||||
|
@ -68,7 +68,6 @@ export default class Card extends React.PureComponent {
|
|||||||
defaultWidth: PropTypes.number,
|
defaultWidth: PropTypes.number,
|
||||||
cacheWidth: PropTypes.func,
|
cacheWidth: PropTypes.func,
|
||||||
sensitive: PropTypes.bool,
|
sensitive: PropTypes.bool,
|
||||||
quote: PropTypes.bool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -185,7 +184,7 @@ export default class Card extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { card, maxDescription, compact, quote } = this.props;
|
const { card, maxDescription, compact } = this.props;
|
||||||
const { width, embedded, revealed } = this.state;
|
const { width, embedded, revealed } = this.state;
|
||||||
|
|
||||||
if (card === null) {
|
if (card === null) {
|
||||||
@ -198,11 +197,7 @@ export default class Card extends React.PureComponent {
|
|||||||
const className = classnames('status-card', { horizontal, compact, interactive });
|
const className = classnames('status-card', { horizontal, compact, interactive });
|
||||||
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
|
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
|
||||||
const ratio = card.get('width') / card.get('height');
|
const ratio = card.get('width') / card.get('height');
|
||||||
let height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
|
const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
|
||||||
|
|
||||||
if (quote && height) {
|
|
||||||
height /= 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
const description = (
|
const description = (
|
||||||
<div className='status-card__content'>
|
<div className='status-card__content'>
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Avatar from '../../../components/avatar';
|
import Avatar from '../../../components/avatar';
|
||||||
import DisplayName from '../../../components/display_name';
|
import DisplayName from '../../../components/display_name';
|
||||||
import { mapStateToProps, quote } from '../../../components/status';
|
|
||||||
import StatusContent from '../../../components/status_content';
|
import StatusContent from '../../../components/status_content';
|
||||||
import MediaGallery from '../../../components/media_gallery';
|
import MediaGallery from '../../../components/media_gallery';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
@ -27,7 +25,7 @@ const messages = defineMessages({
|
|||||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps) @injectIntl
|
export default @injectIntl
|
||||||
class DetailedStatus extends ImmutablePureComponent {
|
class DetailedStatus extends ImmutablePureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
@ -39,21 +37,17 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||||||
onOpenMedia: PropTypes.func.isRequired,
|
onOpenMedia: PropTypes.func.isRequired,
|
||||||
onOpenVideo: PropTypes.func.isRequired,
|
onOpenVideo: PropTypes.func.isRequired,
|
||||||
onToggleHidden: PropTypes.func.isRequired,
|
onToggleHidden: PropTypes.func.isRequired,
|
||||||
onQuoteToggleHidden: PropTypes.func.isRequired,
|
|
||||||
onTranslate: PropTypes.func.isRequired,
|
onTranslate: PropTypes.func.isRequired,
|
||||||
measureHeight: PropTypes.bool,
|
measureHeight: PropTypes.bool,
|
||||||
onHeightChange: PropTypes.func,
|
onHeightChange: PropTypes.func,
|
||||||
domain: PropTypes.string.isRequired,
|
domain: PropTypes.string.isRequired,
|
||||||
compact: PropTypes.bool,
|
compact: PropTypes.bool,
|
||||||
quoteMuted: PropTypes.bool,
|
|
||||||
showMedia: PropTypes.bool,
|
showMedia: PropTypes.bool,
|
||||||
showQuoteMedia: PropTypes.bool,
|
|
||||||
pictureInPicture: ImmutablePropTypes.contains({
|
pictureInPicture: ImmutablePropTypes.contains({
|
||||||
inUse: PropTypes.bool,
|
inUse: PropTypes.bool,
|
||||||
available: PropTypes.bool,
|
available: PropTypes.bool,
|
||||||
}),
|
}),
|
||||||
onToggleMediaVisibility: PropTypes.func,
|
onToggleMediaVisibility: PropTypes.func,
|
||||||
onQuoteToggleMediaVisibility: PropTypes.func,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -62,9 +56,8 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||||||
|
|
||||||
handleAccountClick = (e) => {
|
handleAccountClick = (e) => {
|
||||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) {
|
if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) {
|
||||||
const acct = e.currentTarget.getAttribute('data-acct');
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.context.router.history.push(`/@${acct}`);
|
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -78,19 +71,6 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||||||
this.props.onToggleHidden(this.props.status);
|
this.props.onToggleHidden(this.props.status);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleExpandedQuoteToggle = () => {
|
|
||||||
this.props.onQuoteToggleHidden(this.props.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleQuoteClick = () => {
|
|
||||||
if (!this.context.router) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { status } = this.props;
|
|
||||||
this.context.router.history.push(`/statuses/${status.getIn(['quote', 'id'])}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
_measureHeight (heightJustChanged) {
|
_measureHeight (heightJustChanged) {
|
||||||
if (this.props.measureHeight && this.node) {
|
if (this.props.measureHeight && this.node) {
|
||||||
scheduleIdleTask(() => this.node && this.setState({ height: Math.ceil(this.node.scrollHeight) + 1 }));
|
scheduleIdleTask(() => this.node && this.setState({ height: Math.ceil(this.node.scrollHeight) + 1 }));
|
||||||
@ -132,12 +112,13 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||||||
render () {
|
render () {
|
||||||
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
||||||
const outerStyle = { boxSizing: 'border-box' };
|
const outerStyle = { boxSizing: 'border-box' };
|
||||||
const { intl, compact, pictureInPicture, quoteMuted } = this.props;
|
const { intl, compact, pictureInPicture } = this.props;
|
||||||
|
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let media = '';
|
||||||
let applicationLink = '';
|
let applicationLink = '';
|
||||||
let reblogLink = '';
|
let reblogLink = '';
|
||||||
let reblogIcon = 'retweet';
|
let reblogIcon = 'retweet';
|
||||||
@ -148,84 +129,63 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||||||
outerStyle.height = `${this.state.height}px`;
|
outerStyle.height = `${this.state.height}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const identity = (status, _0, _1, quote = false) => (
|
if (pictureInPicture.get('inUse')) {
|
||||||
<a href={`/@${status.getIn(['account', 'acct'])}`} onClick={this.handleAccountClick} data-acct={status.getIn(['account', 'acct'])} className='detailed-status__display-name'>
|
media = <PictureInPicturePlaceholder />;
|
||||||
<div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={quote ? 18 : 46} /></div>
|
} else if (status.get('media_attachments').size > 0) {
|
||||||
<DisplayName account={status.get('account')} localDomain={this.props.domain} />
|
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||||
</a>
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
);
|
|
||||||
|
|
||||||
const media = (status, quote = false) => {
|
media = (
|
||||||
if (pictureInPicture.get('inUse')) {
|
<Audio
|
||||||
return <PictureInPicturePlaceholder />;
|
src={attachment.get('url')}
|
||||||
} else if (status.get('media_attachments').size > 0) {
|
alt={attachment.get('description')}
|
||||||
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
||||||
const attachment = status.getIn(['media_attachments', 0]);
|
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
||||||
|
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
||||||
return (
|
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
|
||||||
<Audio
|
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
|
||||||
src={attachment.get('url')}
|
|
||||||
alt={attachment.get('description')}
|
|
||||||
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
|
||||||
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
|
||||||
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
|
||||||
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
|
|
||||||
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
|
|
||||||
sensitive={status.get('sensitive')}
|
|
||||||
visible={this.props.showMedia}
|
|
||||||
blurhash={attachment.get('blurhash')}
|
|
||||||
height={150}
|
|
||||||
onToggleVisibility={this.props.onToggleMediaVisibility}
|
|
||||||
quote={quote}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
|
||||||
const attachment = status.getIn(['media_attachments', 0]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Video
|
|
||||||
preview={attachment.get('preview_url')}
|
|
||||||
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
|
||||||
blurhash={attachment.get('blurhash')}
|
|
||||||
src={attachment.get('url')}
|
|
||||||
alt={attachment.get('description')}
|
|
||||||
width={300}
|
|
||||||
height={150}
|
|
||||||
inline
|
|
||||||
onOpenVideo={this.handleOpenVideo}
|
|
||||||
sensitive={status.get('sensitive')}
|
|
||||||
visible={this.props.showMedia}
|
|
||||||
onToggleVisibility={this.props.onToggleMediaVisibility}
|
|
||||||
quote={quote}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<MediaGallery
|
|
||||||
standalone
|
|
||||||
sensitive={status.get('sensitive')}
|
|
||||||
media={status.get('media_attachments')}
|
|
||||||
height={300}
|
|
||||||
onOpenMedia={this.props.onOpenMedia}
|
|
||||||
visible={this.props.showMedia}
|
|
||||||
onToggleVisibility={this.props.onToggleMediaVisibility}
|
|
||||||
quote={quote}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (status.get('spoiler_text').length === 0) {
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
sensitive={status.get('sensitive')}
|
sensitive={status.get('sensitive')}
|
||||||
|
visible={this.props.showMedia}
|
||||||
|
blurhash={attachment.get('blurhash')}
|
||||||
|
height={150}
|
||||||
|
onToggleVisibility={this.props.onToggleMediaVisibility}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||||
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
|
|
||||||
|
media = (
|
||||||
|
<Video
|
||||||
|
preview={attachment.get('preview_url')}
|
||||||
|
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
||||||
|
blurhash={attachment.get('blurhash')}
|
||||||
|
src={attachment.get('url')}
|
||||||
|
alt={attachment.get('description')}
|
||||||
|
width={300}
|
||||||
|
height={150}
|
||||||
|
inline
|
||||||
|
onOpenVideo={this.handleOpenVideo}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
visible={this.props.showMedia}
|
||||||
|
onToggleVisibility={this.props.onToggleMediaVisibility}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
media = (
|
||||||
|
<MediaGallery
|
||||||
|
standalone
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
media={status.get('media_attachments')}
|
||||||
|
height={300}
|
||||||
onOpenMedia={this.props.onOpenMedia}
|
onOpenMedia={this.props.onOpenMedia}
|
||||||
card={status.get('card', null)}
|
visible={this.props.showMedia}
|
||||||
quote={quote}
|
onToggleVisibility={this.props.onToggleMediaVisibility}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else if (status.get('spoiler_text').length === 0) {
|
||||||
return null;
|
media = <Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} />;
|
||||||
};
|
}
|
||||||
|
|
||||||
if (status.get('application')) {
|
if (status.get('application')) {
|
||||||
applicationLink = <React.Fragment> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></React.Fragment>;
|
applicationLink = <React.Fragment> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></React.Fragment>;
|
||||||
@ -301,7 +261,10 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||||||
return (
|
return (
|
||||||
<div style={outerStyle}>
|
<div style={outerStyle}>
|
||||||
<div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })}>
|
<div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })}>
|
||||||
{identity(status, null, null, false)}
|
<a href={`/@${status.getIn(['account', 'acct'])}`} onClick={this.handleAccountClick} className='detailed-status__display-name'>
|
||||||
|
<div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={46} /></div>
|
||||||
|
<DisplayName account={status.get('account')} localDomain={this.props.domain} />
|
||||||
|
</a>
|
||||||
|
|
||||||
<StatusContent
|
<StatusContent
|
||||||
status={status}
|
status={status}
|
||||||
@ -310,9 +273,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||||||
onTranslate={this.handleTranslate}
|
onTranslate={this.handleTranslate}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{media(status, false)}
|
{media}
|
||||||
|
|
||||||
{quote(status, false, quoteMuted, this.handleQuoteClick, this.handleExpandedQuoteToggle, identity, media, this.context.router)}
|
|
||||||
|
|
||||||
<div className='detailed-status__meta'>
|
<div className='detailed-status__meta'>
|
||||||
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}\/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
|
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}\/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
|
||||||
|
@ -20,8 +20,6 @@ import {
|
|||||||
deleteStatus,
|
deleteStatus,
|
||||||
hideStatus,
|
hideStatus,
|
||||||
revealStatus,
|
revealStatus,
|
||||||
hideQuote,
|
|
||||||
revealQuote,
|
|
||||||
} from '../../../actions/statuses';
|
} from '../../../actions/statuses';
|
||||||
import { initMuteModal } from '../../../actions/mutes';
|
import { initMuteModal } from '../../../actions/mutes';
|
||||||
import { initBlockModal } from '../../../actions/blocks';
|
import { initBlockModal } from '../../../actions/blocks';
|
||||||
@ -167,14 +165,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onQuoteToggleHidden (status) {
|
|
||||||
if (status.get('quote_hidden')) {
|
|
||||||
dispatch(revealQuote(status.get('id')));
|
|
||||||
} else {
|
|
||||||
dispatch(hideQuote(status.get('id')));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(DetailedStatus));
|
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(DetailedStatus));
|
||||||
|
@ -23,7 +23,6 @@ import {
|
|||||||
} from '../../actions/interactions';
|
} from '../../actions/interactions';
|
||||||
import {
|
import {
|
||||||
replyCompose,
|
replyCompose,
|
||||||
quoteCompose,
|
|
||||||
mentionCompose,
|
mentionCompose,
|
||||||
directCompose,
|
directCompose,
|
||||||
} from '../../actions/compose';
|
} from '../../actions/compose';
|
||||||
@ -36,8 +35,6 @@ import {
|
|||||||
revealStatus,
|
revealStatus,
|
||||||
translateStatus,
|
translateStatus,
|
||||||
undoStatusTranslation,
|
undoStatusTranslation,
|
||||||
hideQuote,
|
|
||||||
revealQuote,
|
|
||||||
} from '../../actions/statuses';
|
} from '../../actions/statuses';
|
||||||
import {
|
import {
|
||||||
unblockAccount,
|
unblockAccount,
|
||||||
@ -76,8 +73,6 @@ const messages = defineMessages({
|
|||||||
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
|
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
|
||||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||||
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
|
|
||||||
quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
|
||||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -210,7 +205,6 @@ class Status extends ImmutablePureComponent {
|
|||||||
state = {
|
state = {
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
showMedia: defaultMediaVisibility(this.props.status),
|
showMedia: defaultMediaVisibility(this.props.status),
|
||||||
showQuoteMedia: defaultMediaVisibility(this.props.status ? this.props.status.get('quote', null) : null),
|
|
||||||
loadedStatusId: undefined,
|
loadedStatusId: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -233,11 +227,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) {
|
if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) {
|
||||||
this.setState({
|
this.setState({ showMedia: defaultMediaVisibility(nextProps.status), loadedStatusId: nextProps.status.get('id') });
|
||||||
showMedia: defaultMediaVisibility(nextProps.status),
|
|
||||||
showQuoteMedia: defaultMediaVisibility(nextProps.status.get('quote', null)),
|
|
||||||
loadedStatusId: nextProps.status.get('id'),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,10 +235,6 @@ class Status extends ImmutablePureComponent {
|
|||||||
this.setState({ showMedia: !this.state.showMedia });
|
this.setState({ showMedia: !this.state.showMedia });
|
||||||
};
|
};
|
||||||
|
|
||||||
handleToggleQuoteMediaVisibility = () => {
|
|
||||||
this.setState({ showQuoteMedia: !this.state.showQuoteMedia });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFavouriteClick = (status) => {
|
handleFavouriteClick = (status) => {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
const { signedIn } = this.context.identity;
|
const { signedIn } = this.context.identity;
|
||||||
@ -299,19 +285,6 @@ class Status extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleQuoteClick = (status) => {
|
|
||||||
let { askReplyConfirmation, dispatch, intl } = this.props;
|
|
||||||
if (askReplyConfirmation) {
|
|
||||||
dispatch(openModal('CONFIRM', {
|
|
||||||
message: intl.formatMessage(messages.quoteMessage),
|
|
||||||
confirm: intl.formatMessage(messages.quoteConfirm),
|
|
||||||
onConfirm: () => dispatch(quoteCompose(status, this.context.router.history)),
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
dispatch(quoteCompose(status, this.context.router.history));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleModalReblog = (status, privacy) => {
|
handleModalReblog = (status, privacy) => {
|
||||||
this.props.dispatch(reblog(status, privacy));
|
this.props.dispatch(reblog(status, privacy));
|
||||||
};
|
};
|
||||||
@ -415,14 +388,6 @@ class Status extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleQuoteToggleHidden = (status) => {
|
|
||||||
if (status.get('quote_hidden')) {
|
|
||||||
this.props.dispatch(revealQuote(status.get('id')));
|
|
||||||
} else {
|
|
||||||
this.props.dispatch(hideQuote(status.get('id')));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleToggleAll = () => {
|
handleToggleAll = () => {
|
||||||
const { status, ancestorsIds, descendantsIds } = this.props;
|
const { status, ancestorsIds, descendantsIds } = this.props;
|
||||||
const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS());
|
const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS());
|
||||||
@ -675,12 +640,9 @@ class Status extends ImmutablePureComponent {
|
|||||||
onOpenMedia={this.handleOpenMedia}
|
onOpenMedia={this.handleOpenMedia}
|
||||||
onToggleHidden={this.handleToggleHidden}
|
onToggleHidden={this.handleToggleHidden}
|
||||||
onTranslate={this.handleTranslate}
|
onTranslate={this.handleTranslate}
|
||||||
onQuoteToggleHidden={this.handleQuoteToggleHidden}
|
|
||||||
domain={domain}
|
domain={domain}
|
||||||
showMedia={this.state.showMedia}
|
showMedia={this.state.showMedia}
|
||||||
showQuoteMedia={this.state.showQuoteMedia}
|
|
||||||
onToggleMediaVisibility={this.handleToggleMediaVisibility}
|
onToggleMediaVisibility={this.handleToggleMediaVisibility}
|
||||||
onToggleQuoteMediaVisibility={this.handleToggleQuoteMediaVisibility}
|
|
||||||
pictureInPicture={pictureInPicture}
|
pictureInPicture={pictureInPicture}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -690,7 +652,6 @@ class Status extends ImmutablePureComponent {
|
|||||||
onReply={this.handleReplyClick}
|
onReply={this.handleReplyClick}
|
||||||
onFavourite={this.handleFavouriteClick}
|
onFavourite={this.handleFavouriteClick}
|
||||||
onReblog={this.handleReblogClick}
|
onReblog={this.handleReblogClick}
|
||||||
onQuote={this.handleQuoteClick}
|
|
||||||
onBookmark={this.handleBookmarkClick}
|
onBookmark={this.handleBookmarkClick}
|
||||||
onDelete={this.handleDeleteClick}
|
onDelete={this.handleDeleteClick}
|
||||||
onEdit={this.handleEditClick}
|
onEdit={this.handleEditClick}
|
||||||
|
@ -21,15 +21,8 @@ import {
|
|||||||
} from '../../ui/util/async-components';
|
} from '../../ui/util/async-components';
|
||||||
import ComposePanel from './compose_panel';
|
import ComposePanel from './compose_panel';
|
||||||
import NavigationPanel from './navigation_panel';
|
import NavigationPanel from './navigation_panel';
|
||||||
import TabsBar from './tabs_bar';
|
|
||||||
import { place_tab_bar_at_bottom } from 'mastodon/initial_state';
|
|
||||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
import { scrollRight } from '../../../scroll';
|
import { scrollRight } from '../../../scroll';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import Icon from 'mastodon/components/icon';
|
|
||||||
|
|
||||||
const shouldHideFAB = path => path.match(/^\/statuses\/|^\/search|^\/getting-started/);
|
|
||||||
|
|
||||||
const componentMap = {
|
const componentMap = {
|
||||||
'COMPOSE': Compose,
|
'COMPOSE': Compose,
|
||||||
@ -145,55 +138,26 @@ export default class ColumnsArea extends ImmutablePureComponent {
|
|||||||
const { renderComposePanel } = this.state;
|
const { renderComposePanel } = this.state;
|
||||||
|
|
||||||
if (singleColumn) {
|
if (singleColumn) {
|
||||||
if (place_tab_bar_at_bottom) {
|
return (
|
||||||
return (
|
<div className='columns-area__panels'>
|
||||||
<div className='columns-area__panels tab-ber-bottom'>
|
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
|
||||||
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
|
<div className='columns-area__panels__pane__inner'>
|
||||||
<div className='columns-area__panels__pane__inner'>
|
{renderComposePanel && <ComposePanel />}
|
||||||
{renderComposePanel && <ComposePanel />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='columns-area__panels__main timeline'>
|
|
||||||
<div className='tabs-bar__wrapper'><div id='tabs-bar__portal' /></div>
|
|
||||||
<div className='columns-area columns-area--mobile'>{children}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='columns-area__panels__main navber'>
|
|
||||||
{location.pathname !== '/publish' && <Link to='/publish' className='button bottom_right'><Icon id='pencil' fixedWidth /></Link>}
|
|
||||||
<TabsBar key='tabs' />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational columns-area__panels__pane-tab-ber'>
|
|
||||||
<div className='columns-area__panels__pane__inner'>
|
|
||||||
<NavigationPanel />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div className='columns-area__panels'>
|
|
||||||
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
|
|
||||||
<div className='columns-area__panels__pane__inner'>
|
|
||||||
{renderComposePanel && <ComposePanel />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='columns-area__panels__main'>
|
|
||||||
<div className='tabs-bar__wrapper'><div id='tabs-bar__portal' /></div>
|
|
||||||
<div className='columns-area columns-area--mobile'>{children}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
|
|
||||||
<div className='columns-area__panels__pane__inner'>
|
|
||||||
<NavigationPanel />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
}
|
<div className='columns-area__panels__main'>
|
||||||
|
<div className='tabs-bar__wrapper'><div id='tabs-bar__portal' /></div>
|
||||||
|
<div className='columns-area columns-area--mobile'>{children}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
|
||||||
|
<div className='columns-area__panels__pane__inner'>
|
||||||
|
<NavigationPanel />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,96 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { NavLink, withRouter } from 'react-router-dom';
|
|
||||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
|
||||||
import NotificationsCounterIcon from './notifications_counter_icon';
|
|
||||||
import { place_tab_bar_at_bottom, show_tab_bar_label } from 'mastodon/initial_state';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { debounce } from 'lodash';
|
|
||||||
import { isUserTouching } from '../../../is_mobile';
|
|
||||||
import Icon from 'mastodon/components/icon';
|
|
||||||
|
|
||||||
export const links = [
|
|
||||||
<NavLink className='tabs-bar__link' to='/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /></NavLink>,
|
|
||||||
<NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /></NavLink>,
|
|
||||||
<NavLink className='tabs-bar__link' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /></NavLink>,
|
|
||||||
<NavLink className='tabs-bar__link' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /></NavLink>,
|
|
||||||
<NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /></NavLink>,
|
|
||||||
<NavLink className='tabs-bar__link hamburger' to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>,
|
|
||||||
];
|
|
||||||
|
|
||||||
export function getIndex (path) {
|
|
||||||
return links.findIndex(link => link.props.to === path);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLink (index) {
|
|
||||||
return links[index].props.to;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default @injectIntl
|
|
||||||
@withRouter
|
|
||||||
class TabsBar extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
history: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
setRef = ref => {
|
|
||||||
this.node = ref;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick = (e) => {
|
|
||||||
// Only apply optimization for touch devices, which we assume are slower
|
|
||||||
// We thus avoid the 250ms delay for non-touch devices and the lag for touch devices
|
|
||||||
if (isUserTouching()) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.persist();
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const tabs = Array(...this.node.querySelectorAll('.tabs-bar__link'));
|
|
||||||
const currentTab = tabs.find(tab => tab.classList.contains('active'));
|
|
||||||
const nextTab = tabs.find(tab => tab.contains(e.target));
|
|
||||||
const { props: { to } } = links[Array(...this.node.childNodes).indexOf(nextTab)];
|
|
||||||
|
|
||||||
|
|
||||||
if (currentTab !== nextTab) {
|
|
||||||
if (currentTab) {
|
|
||||||
currentTab.classList.remove('active');
|
|
||||||
}
|
|
||||||
|
|
||||||
const listener = debounce(() => {
|
|
||||||
nextTab.removeEventListener('transitionend', listener);
|
|
||||||
this.props.history.push(to);
|
|
||||||
}, 50);
|
|
||||||
|
|
||||||
nextTab.addEventListener('transitionend', listener);
|
|
||||||
nextTab.classList.add('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static contextTypes = {
|
|
||||||
router: PropTypes.object.isRequired,
|
|
||||||
identity: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { intl: { formatMessage } } = this.props;
|
|
||||||
return (
|
|
||||||
<div className='tabs-bar__wrapper'>
|
|
||||||
<nav className={classNames('tabs-bar', { 'bottom-bar': place_tab_bar_at_bottom })} ref={this.setRef}>
|
|
||||||
{links.map(link => React.cloneElement(link, { key: link.props.to, className: classNames(link.props.className, { 'short-label': show_tab_bar_label }), onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div id='tabs-bar__portal' />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -121,7 +121,6 @@ class Video extends React.PureComponent {
|
|||||||
autoPlay: PropTypes.bool,
|
autoPlay: PropTypes.bool,
|
||||||
volume: PropTypes.number,
|
volume: PropTypes.number,
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
quote: PropTypes.bool,
|
|
||||||
componentIndex: PropTypes.number,
|
componentIndex: PropTypes.number,
|
||||||
autoFocus: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
};
|
};
|
||||||
@ -525,7 +524,7 @@ class Video extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, editable, blurhash, autoFocus, quote } = this.props;
|
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
|
||||||
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
|
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
|
||||||
const progress = Math.min((currentTime / duration) * 100, 100);
|
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||||
const playerStyle = {};
|
const playerStyle = {};
|
||||||
@ -539,11 +538,6 @@ class Video extends React.PureComponent {
|
|||||||
playerStyle.height = height;
|
playerStyle.height = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (quote && height) {
|
|
||||||
height /= 2;
|
|
||||||
playerStyle.height = height;
|
|
||||||
}
|
|
||||||
|
|
||||||
let preload;
|
let preload;
|
||||||
|
|
||||||
if (this.props.currentTime || fullscreen || dragging) {
|
if (this.props.currentTime || fullscreen || dragging) {
|
||||||
|
@ -135,7 +135,5 @@ export const version = getMeta('version');
|
|||||||
export const translationEnabled = getMeta('translation_enabled');
|
export const translationEnabled = getMeta('translation_enabled');
|
||||||
export const languages = initialState?.languages;
|
export const languages = initialState?.languages;
|
||||||
export const statusPageUrl = getMeta('status_page_url');
|
export const statusPageUrl = getMeta('status_page_url');
|
||||||
export const place_tab_bar_at_bottom = getMeta('place_tab_bar_at_bottom');
|
|
||||||
export const show_tab_bar_label = getMeta('show_tab_bar_label');
|
|
||||||
|
|
||||||
export default initialState;
|
export default initialState;
|
||||||
|
@ -607,14 +607,6 @@
|
|||||||
"defaultMessage": "This post cannot be boosted",
|
"defaultMessage": "This post cannot be boosted",
|
||||||
"id": "status.cannot_reblog"
|
"id": "status.cannot_reblog"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"defaultMessage": "Quote",
|
|
||||||
"id": "status.quote"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"defaultMessage": "This post cannot be quoted",
|
|
||||||
"id": "status.cannot_quote"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"defaultMessage": "Favourite",
|
"defaultMessage": "Favourite",
|
||||||
"id": "status.favourite"
|
"id": "status.favourite"
|
||||||
@ -772,14 +764,6 @@
|
|||||||
{
|
{
|
||||||
"defaultMessage": "Replied to {name}",
|
"defaultMessage": "Replied to {name}",
|
||||||
"id": "status.replied_to"
|
"id": "status.replied_to"
|
||||||
},
|
|
||||||
{
|
|
||||||
"defaultMessage": "Muted quote",
|
|
||||||
"id": "status.muted_quote"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"defaultMessage": "Unlisted quote",
|
|
||||||
"id": "status.unlisted_quote"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/components/status.json"
|
"path": "app/javascript/mastodon/components/status.json"
|
||||||
@ -3732,14 +3716,6 @@
|
|||||||
"defaultMessage": "Detailed conversation view",
|
"defaultMessage": "Detailed conversation view",
|
||||||
"id": "status.detailed_status"
|
"id": "status.detailed_status"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"defaultMessage": "Quote",
|
|
||||||
"id": "confirmations.quote.confirm"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"defaultMessage": "Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?",
|
|
||||||
"id": "confirmations.quote.message"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"defaultMessage": "Reply",
|
"defaultMessage": "Reply",
|
||||||
"id": "confirmations.reply.confirm"
|
"id": "confirmations.reply.confirm"
|
||||||
@ -4359,4 +4335,4 @@
|
|||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/video/index.json"
|
"path": "app/javascript/mastodon/features/video/index.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
@ -169,8 +169,6 @@
|
|||||||
"confirmations.mute.confirm": "Mute",
|
"confirmations.mute.confirm": "Mute",
|
||||||
"confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.",
|
"confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.",
|
||||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||||
"confirmations.quote.confirm": "Quote",
|
|
||||||
"confirmations.quote.message": "Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?",
|
|
||||||
"confirmations.redraft.confirm": "Delete & redraft",
|
"confirmations.redraft.confirm": "Delete & redraft",
|
||||||
"confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.",
|
"confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.",
|
||||||
"confirmations.reply.confirm": "Reply",
|
"confirmations.reply.confirm": "Reply",
|
||||||
@ -555,7 +553,6 @@
|
|||||||
"status.block": "Block @{name}",
|
"status.block": "Block @{name}",
|
||||||
"status.bookmark": "Bookmark",
|
"status.bookmark": "Bookmark",
|
||||||
"status.cancel_reblog_private": "Unboost",
|
"status.cancel_reblog_private": "Unboost",
|
||||||
"status.cannot_quote": "This post cannot be quoted",
|
|
||||||
"status.cannot_reblog": "This post cannot be boosted",
|
"status.cannot_reblog": "This post cannot be boosted",
|
||||||
"status.copy": "Copy link to post",
|
"status.copy": "Copy link to post",
|
||||||
"status.delete": "Delete",
|
"status.delete": "Delete",
|
||||||
@ -577,11 +574,9 @@
|
|||||||
"status.more": "More",
|
"status.more": "More",
|
||||||
"status.mute": "Mute @{name}",
|
"status.mute": "Mute @{name}",
|
||||||
"status.mute_conversation": "Mute conversation",
|
"status.mute_conversation": "Mute conversation",
|
||||||
"status.muted_quote": "Muted quote",
|
|
||||||
"status.open": "Expand this post",
|
"status.open": "Expand this post",
|
||||||
"status.pin": "Pin on profile",
|
"status.pin": "Pin on profile",
|
||||||
"status.pinned": "Pinned post",
|
"status.pinned": "Pinned post",
|
||||||
"status.quote": "Quote",
|
|
||||||
"status.read_more": "Read more",
|
"status.read_more": "Read more",
|
||||||
"status.reblog": "Boost",
|
"status.reblog": "Boost",
|
||||||
"status.reblog_private": "Boost with original visibility",
|
"status.reblog_private": "Boost with original visibility",
|
||||||
@ -604,7 +599,6 @@
|
|||||||
"status.translate": "Translate",
|
"status.translate": "Translate",
|
||||||
"status.translated_from_with": "Translated from {lang} using {provider}",
|
"status.translated_from_with": "Translated from {lang} using {provider}",
|
||||||
"status.uncached_media_warning": "Not available",
|
"status.uncached_media_warning": "Not available",
|
||||||
"status.unlisted_quote": "Unlisted quote",
|
|
||||||
"status.unmute_conversation": "Unmute conversation",
|
"status.unmute_conversation": "Unmute conversation",
|
||||||
"status.unpin": "Unpin from profile",
|
"status.unpin": "Unpin from profile",
|
||||||
"subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.",
|
"subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.",
|
||||||
|
@ -21,13 +21,13 @@
|
|||||||
"account.browse_more_on_origin_server": "リモートで表示",
|
"account.browse_more_on_origin_server": "リモートで表示",
|
||||||
"account.cancel_follow_request": "フォローリクエストの取り消し",
|
"account.cancel_follow_request": "フォローリクエストの取り消し",
|
||||||
"account.direct": "@{name}さんにダイレクトメッセージ",
|
"account.direct": "@{name}さんにダイレクトメッセージ",
|
||||||
"account.disable_notifications": "@{name}さんのトゥート時の通知を停止",
|
"account.disable_notifications": "@{name}さんの投稿時の通知を停止",
|
||||||
"account.domain_blocked": "ドメインブロック中",
|
"account.domain_blocked": "ドメインブロック中",
|
||||||
"account.edit_profile": "プロフィール編集",
|
"account.edit_profile": "プロフィール編集",
|
||||||
"account.enable_notifications": "@{name}さんのトゥート時に通知",
|
"account.enable_notifications": "@{name}さんの投稿時に通知",
|
||||||
"account.endorse": "プロフィールで紹介する",
|
"account.endorse": "プロフィールで紹介する",
|
||||||
"account.featured_tags.last_status_at": "最終トゥート {date}",
|
"account.featured_tags.last_status_at": "最終投稿 {date}",
|
||||||
"account.featured_tags.last_status_never": "トゥートがありません",
|
"account.featured_tags.last_status_never": "投稿がありません",
|
||||||
"account.featured_tags.title": "{name}の注目ハッシュタグ",
|
"account.featured_tags.title": "{name}の注目ハッシュタグ",
|
||||||
"account.follow": "フォロー",
|
"account.follow": "フォロー",
|
||||||
"account.followers": "フォロワー",
|
"account.followers": "フォロワー",
|
||||||
@ -50,14 +50,14 @@
|
|||||||
"account.mute_notifications": "@{name}さんからの通知を受け取らない",
|
"account.mute_notifications": "@{name}さんからの通知を受け取らない",
|
||||||
"account.muted": "ミュート済み",
|
"account.muted": "ミュート済み",
|
||||||
"account.open_original_page": "元のページを開く",
|
"account.open_original_page": "元のページを開く",
|
||||||
"account.posts": "トゥート",
|
"account.posts": "投稿",
|
||||||
"account.posts_with_replies": "トゥートと返信",
|
"account.posts_with_replies": "投稿と返信",
|
||||||
"account.report": "@{name}さんを通報",
|
"account.report": "@{name}さんを通報",
|
||||||
"account.requested": "フォロー承認待ちです。クリックしてキャンセル",
|
"account.requested": "フォロー承認待ちです。クリックしてキャンセル",
|
||||||
"account.requested_follow": "{name}さんがあなたにフォローリクエストしました",
|
"account.requested_follow": "{name}さんがあなたにフォローリクエストしました",
|
||||||
"account.share": "@{name}さんのプロフィールを共有する",
|
"account.share": "@{name}さんのプロフィールを共有する",
|
||||||
"account.show_reblogs": "@{name}さんからのブーストを表示",
|
"account.show_reblogs": "@{name}さんからのブーストを表示",
|
||||||
"account.statuses_counter": "{counter} トゥート",
|
"account.statuses_counter": "{counter} 投稿",
|
||||||
"account.unblock": "@{name}さんのブロックを解除",
|
"account.unblock": "@{name}さんのブロックを解除",
|
||||||
"account.unblock_domain": "{domain}のブロックを解除",
|
"account.unblock_domain": "{domain}のブロックを解除",
|
||||||
"account.unblock_short": "ブロック解除",
|
"account.unblock_short": "ブロック解除",
|
||||||
@ -111,7 +111,7 @@
|
|||||||
"column.lists": "リスト",
|
"column.lists": "リスト",
|
||||||
"column.mutes": "ミュートしたユーザー",
|
"column.mutes": "ミュートしたユーザー",
|
||||||
"column.notifications": "通知",
|
"column.notifications": "通知",
|
||||||
"column.pins": "固定されたトゥート",
|
"column.pins": "固定された投稿",
|
||||||
"column.public": "連合タイムライン",
|
"column.public": "連合タイムライン",
|
||||||
"column_back_button.label": "戻る",
|
"column_back_button.label": "戻る",
|
||||||
"column_header.hide_settings": "設定を隠す",
|
"column_header.hide_settings": "設定を隠す",
|
||||||
@ -127,9 +127,9 @@
|
|||||||
"compose.language.change": "言語を変更",
|
"compose.language.change": "言語を変更",
|
||||||
"compose.language.search": "言語を検索...",
|
"compose.language.search": "言語を検索...",
|
||||||
"compose_form.direct_message_warning_learn_more": "もっと詳しく",
|
"compose_form.direct_message_warning_learn_more": "もっと詳しく",
|
||||||
"compose_form.encryption_warning": "Mastodonのトゥートはエンドツーエンド暗号化に対応していません。安全に送受信されるべき情報をMastodonで共有しないでください。",
|
"compose_form.encryption_warning": "Mastodonの投稿はエンドツーエンド暗号化に対応していません。安全に送受信されるべき情報をMastodonで共有しないでください。",
|
||||||
"compose_form.hashtag_warning": "このトゥートは公開設定ではないのでハッシュタグの一覧に表示されません。公開トゥートだけがハッシュタグで検索できます。",
|
"compose_form.hashtag_warning": "この投稿は公開設定ではないのでハッシュタグの一覧に表示されません。公開投稿だけがハッシュタグで検索できます。",
|
||||||
"compose_form.lock_disclaimer": "あなたのアカウントは{locked}になっていません。誰でもあなたをフォローすることができ、フォロワー限定のトゥートを見ることができます。",
|
"compose_form.lock_disclaimer": "あなたのアカウントは{locked}になっていません。誰でもあなたをフォローすることができ、フォロワー限定の投稿を見ることができます。",
|
||||||
"compose_form.lock_disclaimer.lock": "承認制",
|
"compose_form.lock_disclaimer.lock": "承認制",
|
||||||
"compose_form.placeholder": "今なにしてる?",
|
"compose_form.placeholder": "今なにしてる?",
|
||||||
"compose_form.poll.add_option": "追加",
|
"compose_form.poll.add_option": "追加",
|
||||||
@ -138,8 +138,8 @@
|
|||||||
"compose_form.poll.remove_option": "この項目を削除",
|
"compose_form.poll.remove_option": "この項目を削除",
|
||||||
"compose_form.poll.switch_to_multiple": "複数選択に変更",
|
"compose_form.poll.switch_to_multiple": "複数選択に変更",
|
||||||
"compose_form.poll.switch_to_single": "単一選択に変更",
|
"compose_form.poll.switch_to_single": "単一選択に変更",
|
||||||
"compose_form.publish": "トゥート",
|
"compose_form.publish": "投稿",
|
||||||
"compose_form.publish_form": "トゥート",
|
"compose_form.publish_form": "投稿",
|
||||||
"compose_form.publish_loud": "{publish}!",
|
"compose_form.publish_loud": "{publish}!",
|
||||||
"compose_form.save_changes": "変更を保存",
|
"compose_form.save_changes": "変更を保存",
|
||||||
"compose_form.sensitive.hide": "メディアを閲覧注意にする",
|
"compose_form.sensitive.hide": "メディアを閲覧注意にする",
|
||||||
@ -148,9 +148,6 @@
|
|||||||
"compose_form.spoiler.marked": "本文は警告の後ろに隠されます",
|
"compose_form.spoiler.marked": "本文は警告の後ろに隠されます",
|
||||||
"compose_form.spoiler.unmarked": "本文は隠されていません",
|
"compose_form.spoiler.unmarked": "本文は隠されていません",
|
||||||
"compose_form.spoiler_placeholder": "ここに警告を書いてください",
|
"compose_form.spoiler_placeholder": "ここに警告を書いてください",
|
||||||
"compose_form.utilBtns_goji": "誤字盛!",
|
|
||||||
"compose_form.utilBtns_harukin": "はるきん焼却",
|
|
||||||
"compose_form.utilBtns_risa": "りさ姉",
|
|
||||||
"confirmation_modal.cancel": "キャンセル",
|
"confirmation_modal.cancel": "キャンセル",
|
||||||
"confirmations.block.block_and_report": "ブロックし通報",
|
"confirmations.block.block_and_report": "ブロックし通報",
|
||||||
"confirmations.block.confirm": "ブロック",
|
"confirmations.block.confirm": "ブロック",
|
||||||
@ -168,12 +165,10 @@
|
|||||||
"confirmations.logout.confirm": "ログアウト",
|
"confirmations.logout.confirm": "ログアウト",
|
||||||
"confirmations.logout.message": "本当にログアウトしますか?",
|
"confirmations.logout.message": "本当にログアウトしますか?",
|
||||||
"confirmations.mute.confirm": "ミュート",
|
"confirmations.mute.confirm": "ミュート",
|
||||||
"confirmations.mute.explanation": "これにより相手のトゥートと返信は見えなくなりますが、相手はあなたをフォローし続けトゥートを見ることができます。",
|
"confirmations.mute.explanation": "これにより相手の投稿と返信は見えなくなりますが、相手はあなたをフォローし続け投稿を見ることができます。",
|
||||||
"confirmations.mute.message": "本当に{name}さんをミュートしますか?",
|
"confirmations.mute.message": "本当に{name}さんをミュートしますか?",
|
||||||
"confirmations.quote.confirm": "引用",
|
|
||||||
"confirmations.quote.message": "今引用すると現在作成中のメッセージが上書きされます。本当に実行しますか?",
|
|
||||||
"confirmations.redraft.confirm": "削除して下書きに戻す",
|
"confirmations.redraft.confirm": "削除して下書きに戻す",
|
||||||
"confirmations.redraft.message": "本当にこのトゥートを削除して下書きに戻しますか? このトゥートへのお気に入り登録やブーストは失われ、返信は孤立することになります。",
|
"confirmations.redraft.message": "本当にこの投稿を削除して下書きに戻しますか? この投稿へのお気に入り登録やブーストは失われ、返信は孤立することになります。",
|
||||||
"confirmations.reply.confirm": "返信",
|
"confirmations.reply.confirm": "返信",
|
||||||
"confirmations.reply.message": "今返信すると現在作成中のメッセージが上書きされます。本当に実行しますか?",
|
"confirmations.reply.message": "今返信すると現在作成中のメッセージが上書きされます。本当に実行しますか?",
|
||||||
"confirmations.unfollow.confirm": "フォロー解除",
|
"confirmations.unfollow.confirm": "フォロー解除",
|
||||||
@ -190,12 +185,12 @@
|
|||||||
"directory.recently_active": "最近の活動順",
|
"directory.recently_active": "最近の活動順",
|
||||||
"disabled_account_banner.account_settings": "アカウント設定",
|
"disabled_account_banner.account_settings": "アカウント設定",
|
||||||
"disabled_account_banner.text": "あなたのアカウント『{disabledAccount}』は現在無効になっています。",
|
"disabled_account_banner.text": "あなたのアカウント『{disabledAccount}』は現在無効になっています。",
|
||||||
"dismissable_banner.community_timeline": "これらは{domain}がホストしている人たちの最新の公開トゥートです。",
|
"dismissable_banner.community_timeline": "これらは{domain}がホストしている人たちの最新の公開投稿です。",
|
||||||
"dismissable_banner.dismiss": "閉じる",
|
"dismissable_banner.dismiss": "閉じる",
|
||||||
"dismissable_banner.explore_links": "これらのニュース記事は現在分散型ネットワークの他のサーバーの人たちに話されています。",
|
"dismissable_banner.explore_links": "これらのニュース記事は現在分散型ネットワークの他のサーバーの人たちに話されています。",
|
||||||
"dismissable_banner.explore_statuses": "分散型ネットワーク内の他のサーバーのこれらのトゥートは現在このサーバー上で注目されています。",
|
"dismissable_banner.explore_statuses": "分散型ネットワーク内の他のサーバーのこれらの投稿は現在このサーバー上で注目されています。",
|
||||||
"dismissable_banner.explore_tags": "これらのハッシュタグは現在分散型ネットワークの他のサーバーの人たちに話されています。",
|
"dismissable_banner.explore_tags": "これらのハッシュタグは現在分散型ネットワークの他のサーバーの人たちに話されています。",
|
||||||
"dismissable_banner.public_timeline": "これらのトゥートはこのサーバーが知っている分散型ネットワークの他のサーバーの人たちの最新の公開トゥートです。",
|
"dismissable_banner.public_timeline": "これらの投稿はこのサーバーが知っている分散型ネットワークの他のサーバーの人たちの最新の公開投稿です。",
|
||||||
"embed.instructions": "下記のコードをコピーしてウェブサイトに埋め込みます。",
|
"embed.instructions": "下記のコードをコピーしてウェブサイトに埋め込みます。",
|
||||||
"embed.preview": "表示例:",
|
"embed.preview": "表示例:",
|
||||||
"emoji_button.activity": "活動",
|
"emoji_button.activity": "活動",
|
||||||
@ -214,7 +209,7 @@
|
|||||||
"emoji_button.symbols": "記号",
|
"emoji_button.symbols": "記号",
|
||||||
"emoji_button.travel": "旅行と場所",
|
"emoji_button.travel": "旅行と場所",
|
||||||
"empty_column.account_suspended": "アカウントは停止されています",
|
"empty_column.account_suspended": "アカウントは停止されています",
|
||||||
"empty_column.account_timeline": "トゥートがありません!",
|
"empty_column.account_timeline": "投稿がありません!",
|
||||||
"empty_column.account_unavailable": "プロフィールは利用できません",
|
"empty_column.account_unavailable": "プロフィールは利用できません",
|
||||||
"empty_column.blocks": "まだ誰もブロックしていません。",
|
"empty_column.blocks": "まだ誰もブロックしていません。",
|
||||||
"empty_column.bookmarked_statuses": "まだ何もブックマーク登録していません。ブックマーク登録するとここに表示されます。",
|
"empty_column.bookmarked_statuses": "まだ何もブックマーク登録していません。ブックマーク登録するとここに表示されます。",
|
||||||
@ -230,11 +225,11 @@
|
|||||||
"empty_column.hashtag": "このハッシュタグはまだ使われていません。",
|
"empty_column.hashtag": "このハッシュタグはまだ使われていません。",
|
||||||
"empty_column.home": "ホームタイムラインはまだ空っぽです。誰かフォローして埋めてみましょう。 {suggestions}",
|
"empty_column.home": "ホームタイムラインはまだ空っぽです。誰かフォローして埋めてみましょう。 {suggestions}",
|
||||||
"empty_column.home.suggestions": "おすすめを見る",
|
"empty_column.home.suggestions": "おすすめを見る",
|
||||||
"empty_column.list": "このリストにはまだなにもありません。このリストのメンバーが新しいトゥートをするとここに表示されます。",
|
"empty_column.list": "このリストにはまだなにもありません。このリストのメンバーが新しい投稿をするとここに表示されます。",
|
||||||
"empty_column.lists": "まだリストがありません。リストを作るとここに表示されます。",
|
"empty_column.lists": "まだリストがありません。リストを作るとここに表示されます。",
|
||||||
"empty_column.mutes": "まだ誰もミュートしていません。",
|
"empty_column.mutes": "まだ誰もミュートしていません。",
|
||||||
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
|
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
|
||||||
"empty_column.public": "ここにはまだ何もありません! 公開で何かをトゥートしたり、他のサーバーのユーザーをフォローしたりしていっぱいにしましょう",
|
"empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のサーバーのユーザーをフォローしたりしていっぱいにしましょう",
|
||||||
"error.unexpected_crash.explanation": "不具合かブラウザの互換性問題のため、このページを正しく表示できませんでした。",
|
"error.unexpected_crash.explanation": "不具合かブラウザの互換性問題のため、このページを正しく表示できませんでした。",
|
||||||
"error.unexpected_crash.explanation_addons": "このページは正しく表示できませんでした。このエラーはブラウザのアドオンや自動翻訳ツールによって引き起こされることがあります。",
|
"error.unexpected_crash.explanation_addons": "このページは正しく表示できませんでした。このエラーはブラウザのアドオンや自動翻訳ツールによって引き起こされることがあります。",
|
||||||
"error.unexpected_crash.next_steps": "ページの再読み込みをお試しください。それでも解決しない場合、別のブラウザかアプリを使えば使用できることがあります。",
|
"error.unexpected_crash.next_steps": "ページの再読み込みをお試しください。それでも解決しない場合、別のブラウザかアプリを使えば使用できることがあります。",
|
||||||
@ -245,27 +240,27 @@
|
|||||||
"explore.suggested_follows": "おすすめ",
|
"explore.suggested_follows": "おすすめ",
|
||||||
"explore.title": "エクスプローラー",
|
"explore.title": "エクスプローラー",
|
||||||
"explore.trending_links": "ニュース",
|
"explore.trending_links": "ニュース",
|
||||||
"explore.trending_statuses": "トゥート",
|
"explore.trending_statuses": "投稿",
|
||||||
"explore.trending_tags": "ハッシュタグ",
|
"explore.trending_tags": "ハッシュタグ",
|
||||||
"filter_modal.added.context_mismatch_explanation": "このフィルターカテゴリーはあなたがアクセスしたトゥートのコンテキストには適用されません。このトゥートのコンテキストでもフィルターを適用するにはフィルターを編集する必要があります。",
|
"filter_modal.added.context_mismatch_explanation": "このフィルターカテゴリーはあなたがアクセスした投稿のコンテキストには適用されません。この投稿のコンテキストでもフィルターを適用するにはフィルターを編集する必要があります。",
|
||||||
"filter_modal.added.context_mismatch_title": "コンテキストが一致しません!",
|
"filter_modal.added.context_mismatch_title": "コンテキストが一致しません!",
|
||||||
"filter_modal.added.expired_explanation": "このフィルターカテゴリは有効期限が切れています。適用するには有効期限を更新してください。",
|
"filter_modal.added.expired_explanation": "このフィルターカテゴリは有効期限が切れています。適用するには有効期限を更新してください。",
|
||||||
"filter_modal.added.expired_title": "フィルターの有効期限が切れています!",
|
"filter_modal.added.expired_title": "フィルターの有効期限が切れています!",
|
||||||
"filter_modal.added.review_and_configure": "このフィルターカテゴリーを確認して設定するには、{settings_link}に移動します。",
|
"filter_modal.added.review_and_configure": "このフィルターカテゴリーを確認して設定するには、{settings_link}に移動します。",
|
||||||
"filter_modal.added.review_and_configure_title": "フィルター設定",
|
"filter_modal.added.review_and_configure_title": "フィルター設定",
|
||||||
"filter_modal.added.settings_link": "設定",
|
"filter_modal.added.settings_link": "設定",
|
||||||
"filter_modal.added.short_explanation": "このトゥートはフィルターカテゴリー『{title}』に追加されました。",
|
"filter_modal.added.short_explanation": "この投稿はフィルターカテゴリー『{title}』に追加されました。",
|
||||||
"filter_modal.added.title": "フィルターを追加しました!",
|
"filter_modal.added.title": "フィルターを追加しました!",
|
||||||
"filter_modal.select_filter.context_mismatch": "このコンテキストには当てはまりません",
|
"filter_modal.select_filter.context_mismatch": "このコンテキストには当てはまりません",
|
||||||
"filter_modal.select_filter.expired": "期限切れ",
|
"filter_modal.select_filter.expired": "期限切れ",
|
||||||
"filter_modal.select_filter.prompt_new": "新しいカテゴリー: {name}",
|
"filter_modal.select_filter.prompt_new": "新しいカテゴリー: {name}",
|
||||||
"filter_modal.select_filter.search": "検索または新規作成",
|
"filter_modal.select_filter.search": "検索または新規作成",
|
||||||
"filter_modal.select_filter.subtitle": "既存のカテゴリーを使用するか新規作成します",
|
"filter_modal.select_filter.subtitle": "既存のカテゴリーを使用するか新規作成します",
|
||||||
"filter_modal.select_filter.title": "このトゥートをフィルターする",
|
"filter_modal.select_filter.title": "この投稿をフィルターする",
|
||||||
"filter_modal.title.status": "トゥートをフィルターする",
|
"filter_modal.title.status": "投稿をフィルターする",
|
||||||
"follow_recommendations.done": "完了",
|
"follow_recommendations.done": "完了",
|
||||||
"follow_recommendations.heading": "トゥートを見たい人をフォローしてください!ここにおすすめがあります。",
|
"follow_recommendations.heading": "投稿を見たい人をフォローしてください!ここにおすすめがあります。",
|
||||||
"follow_recommendations.lead": "あなたがフォローしている人のトゥートは、ホームフィードに時系列で表示されます。いつでも簡単に解除できるので、気軽にフォローしてみてください!",
|
"follow_recommendations.lead": "あなたがフォローしている人の投稿は、ホームフィードに時系列で表示されます。いつでも簡単に解除できるので、気軽にフォローしてみてください!",
|
||||||
"follow_request.authorize": "許可",
|
"follow_request.authorize": "許可",
|
||||||
"follow_request.reject": "拒否",
|
"follow_request.reject": "拒否",
|
||||||
"follow_requests.unlocked_explanation": "あなたのアカウントは承認制ではありませんが、{domain}のスタッフはこれらのアカウントからのフォローリクエストの確認が必要であると判断しました。",
|
"follow_requests.unlocked_explanation": "あなたのアカウントは承認制ではありませんが、{domain}のスタッフはこれらのアカウントからのフォローリクエストの確認が必要であると判断しました。",
|
||||||
@ -296,18 +291,18 @@
|
|||||||
"home.column_settings.show_replies": "返信表示",
|
"home.column_settings.show_replies": "返信表示",
|
||||||
"home.hide_announcements": "お知らせを隠す",
|
"home.hide_announcements": "お知らせを隠す",
|
||||||
"home.show_announcements": "お知らせを表示",
|
"home.show_announcements": "お知らせを表示",
|
||||||
"interaction_modal.description.favourite": "Mastodonのアカウントでこのトゥートをお気に入りに入れてトゥート者に感謝を知らせたり保存することができます。",
|
"interaction_modal.description.favourite": "Mastodonのアカウントでこの投稿をお気に入りに入れて投稿者に感謝を知らせたり保存することができます。",
|
||||||
"interaction_modal.description.follow": "Mastodonのアカウントで{name}さんをフォローしてホームフィードでトゥートを受け取れます。",
|
"interaction_modal.description.follow": "Mastodonのアカウントで{name}さんをフォローしてホームフィードで投稿を受け取れます。",
|
||||||
"interaction_modal.description.reblog": "Mastodonのアカウントでこのトゥートをブーストして自分のフォロワーに共有できます。",
|
"interaction_modal.description.reblog": "Mastodonのアカウントでこの投稿をブーストして自分のフォロワーに共有できます。",
|
||||||
"interaction_modal.description.reply": "Mastodonのアカウントでこのトゥートに反応できます。",
|
"interaction_modal.description.reply": "Mastodonのアカウントでこの投稿に反応できます。",
|
||||||
"interaction_modal.on_another_server": "別のサーバー",
|
"interaction_modal.on_another_server": "別のサーバー",
|
||||||
"interaction_modal.on_this_server": "このサーバー",
|
"interaction_modal.on_this_server": "このサーバー",
|
||||||
"interaction_modal.other_server_instructions": "このURLをお気に入りのMastodonアプリやMastodonサーバーのWebインターフェースの検索フィールドにコピーして貼り付けます。",
|
"interaction_modal.other_server_instructions": "このURLをお気に入りのMastodonアプリやMastodonサーバーのWebインターフェースの検索フィールドにコピーして貼り付けます。",
|
||||||
"interaction_modal.preamble": "Mastodonは分散化されているためアカウントを持っていなくても別のMastodonサーバーまたは互換性のあるプラットフォームでホストされているアカウントを使用できます。",
|
"interaction_modal.preamble": "Mastodonは分散化されているためアカウントを持っていなくても別のMastodonサーバーまたは互換性のあるプラットフォームでホストされているアカウントを使用できます。",
|
||||||
"interaction_modal.title.favourite": "{name}さんのトゥートをお気に入り",
|
"interaction_modal.title.favourite": "{name}さんの投稿をお気に入り",
|
||||||
"interaction_modal.title.follow": "{name}さんをフォロー",
|
"interaction_modal.title.follow": "{name}さんをフォロー",
|
||||||
"interaction_modal.title.reblog": "{name}さんのトゥートをブースト",
|
"interaction_modal.title.reblog": "{name}さんの投稿をブースト",
|
||||||
"interaction_modal.title.reply": "{name}さんのトゥートにリプライ",
|
"interaction_modal.title.reply": "{name}さんの投稿にリプライ",
|
||||||
"intervals.full.days": "{number}日",
|
"intervals.full.days": "{number}日",
|
||||||
"intervals.full.hours": "{number}時間",
|
"intervals.full.hours": "{number}時間",
|
||||||
"intervals.full.minutes": "{number}分",
|
"intervals.full.minutes": "{number}分",
|
||||||
@ -315,11 +310,11 @@
|
|||||||
"keyboard_shortcuts.blocked": "ブロックしたユーザーのリストを開く",
|
"keyboard_shortcuts.blocked": "ブロックしたユーザーのリストを開く",
|
||||||
"keyboard_shortcuts.boost": "ブースト",
|
"keyboard_shortcuts.boost": "ブースト",
|
||||||
"keyboard_shortcuts.column": "左からn番目のカラムの最新に移動",
|
"keyboard_shortcuts.column": "左からn番目のカラムの最新に移動",
|
||||||
"keyboard_shortcuts.compose": "トゥートの入力欄に移動",
|
"keyboard_shortcuts.compose": "投稿の入力欄に移動",
|
||||||
"keyboard_shortcuts.description": "説明",
|
"keyboard_shortcuts.description": "説明",
|
||||||
"keyboard_shortcuts.direct": "ダイレクトメッセージのカラムを開く",
|
"keyboard_shortcuts.direct": "ダイレクトメッセージのカラムを開く",
|
||||||
"keyboard_shortcuts.down": "カラム内一つ下に移動",
|
"keyboard_shortcuts.down": "カラム内一つ下に移動",
|
||||||
"keyboard_shortcuts.enter": "トゥートの詳細を表示",
|
"keyboard_shortcuts.enter": "投稿の詳細を表示",
|
||||||
"keyboard_shortcuts.favourite": "お気に入り",
|
"keyboard_shortcuts.favourite": "お気に入り",
|
||||||
"keyboard_shortcuts.favourites": "お気に入り登録のリストを開く",
|
"keyboard_shortcuts.favourites": "お気に入り登録のリストを開く",
|
||||||
"keyboard_shortcuts.federated": "連合タイムラインを開く",
|
"keyboard_shortcuts.federated": "連合タイムラインを開く",
|
||||||
@ -333,7 +328,7 @@
|
|||||||
"keyboard_shortcuts.my_profile": "自分のプロフィールを開く",
|
"keyboard_shortcuts.my_profile": "自分のプロフィールを開く",
|
||||||
"keyboard_shortcuts.notifications": "通知カラムを開く",
|
"keyboard_shortcuts.notifications": "通知カラムを開く",
|
||||||
"keyboard_shortcuts.open_media": "メディアを開く",
|
"keyboard_shortcuts.open_media": "メディアを開く",
|
||||||
"keyboard_shortcuts.pinned": "固定したトゥートのリストを開く",
|
"keyboard_shortcuts.pinned": "固定した投稿のリストを開く",
|
||||||
"keyboard_shortcuts.profile": "プロフィールを開く",
|
"keyboard_shortcuts.profile": "プロフィールを開く",
|
||||||
"keyboard_shortcuts.reply": "返信",
|
"keyboard_shortcuts.reply": "返信",
|
||||||
"keyboard_shortcuts.requests": "フォローリクエストのリストを開く",
|
"keyboard_shortcuts.requests": "フォローリクエストのリストを開く",
|
||||||
@ -342,8 +337,8 @@
|
|||||||
"keyboard_shortcuts.start": "\"スタート\" カラムを開く",
|
"keyboard_shortcuts.start": "\"スタート\" カラムを開く",
|
||||||
"keyboard_shortcuts.toggle_hidden": "CWで隠れた文を見る/隠す",
|
"keyboard_shortcuts.toggle_hidden": "CWで隠れた文を見る/隠す",
|
||||||
"keyboard_shortcuts.toggle_sensitivity": "非表示のメディアを見る/隠す",
|
"keyboard_shortcuts.toggle_sensitivity": "非表示のメディアを見る/隠す",
|
||||||
"keyboard_shortcuts.toot": "新規トゥート",
|
"keyboard_shortcuts.toot": "新規投稿",
|
||||||
"keyboard_shortcuts.unfocus": "トゥートの入力欄・検索欄から離れる",
|
"keyboard_shortcuts.unfocus": "投稿の入力欄・検索欄から離れる",
|
||||||
"keyboard_shortcuts.up": "カラム内一つ上に移動",
|
"keyboard_shortcuts.up": "カラム内一つ上に移動",
|
||||||
"lightbox.close": "閉じる",
|
"lightbox.close": "閉じる",
|
||||||
"lightbox.compress": "画像ビューボックスを閉じる",
|
"lightbox.compress": "画像ビューボックスを閉じる",
|
||||||
@ -378,7 +373,7 @@
|
|||||||
"navigation_bar.blocks": "ブロックしたユーザー",
|
"navigation_bar.blocks": "ブロックしたユーザー",
|
||||||
"navigation_bar.bookmarks": "ブックマーク",
|
"navigation_bar.bookmarks": "ブックマーク",
|
||||||
"navigation_bar.community_timeline": "ローカルタイムライン",
|
"navigation_bar.community_timeline": "ローカルタイムライン",
|
||||||
"navigation_bar.compose": "トゥートの新規作成",
|
"navigation_bar.compose": "投稿の新規作成",
|
||||||
"navigation_bar.direct": "ダイレクトメッセージ",
|
"navigation_bar.direct": "ダイレクトメッセージ",
|
||||||
"navigation_bar.discover": "見つける",
|
"navigation_bar.discover": "見つける",
|
||||||
"navigation_bar.domain_blocks": "ブロックしたドメイン",
|
"navigation_bar.domain_blocks": "ブロックしたドメイン",
|
||||||
@ -393,7 +388,7 @@
|
|||||||
"navigation_bar.logout": "ログアウト",
|
"navigation_bar.logout": "ログアウト",
|
||||||
"navigation_bar.mutes": "ミュートしたユーザー",
|
"navigation_bar.mutes": "ミュートしたユーザー",
|
||||||
"navigation_bar.personal": "個人用",
|
"navigation_bar.personal": "個人用",
|
||||||
"navigation_bar.pins": "固定したトゥート",
|
"navigation_bar.pins": "固定した投稿",
|
||||||
"navigation_bar.preferences": "ユーザー設定",
|
"navigation_bar.preferences": "ユーザー設定",
|
||||||
"navigation_bar.public_timeline": "連合タイムライン",
|
"navigation_bar.public_timeline": "連合タイムライン",
|
||||||
"navigation_bar.search": "検索",
|
"navigation_bar.search": "検索",
|
||||||
@ -401,15 +396,15 @@
|
|||||||
"not_signed_in_indicator.not_signed_in": "この機能を使うにはログインする必要があります。",
|
"not_signed_in_indicator.not_signed_in": "この機能を使うにはログインする必要があります。",
|
||||||
"notification.admin.report": "{name}さんが{target}さんを通報しました",
|
"notification.admin.report": "{name}さんが{target}さんを通報しました",
|
||||||
"notification.admin.sign_up": "{name}さんがサインアップしました",
|
"notification.admin.sign_up": "{name}さんがサインアップしました",
|
||||||
"notification.favourite": "{name}さんがあなたのトゥートに╰( ^o^)╮-=ニ=一=三★しました",
|
"notification.favourite": "{name}さんがあなたの投稿をお気に入りに登録しました",
|
||||||
"notification.follow": "{name}さんにフォローされました",
|
"notification.follow": "{name}さんにフォローされました",
|
||||||
"notification.follow_request": "{name}さんがあなたにフォローリクエストしました",
|
"notification.follow_request": "{name}さんがあなたにフォローリクエストしました",
|
||||||
"notification.mention": "{name}さんがあなたに返信しました",
|
"notification.mention": "{name}さんがあなたに返信しました",
|
||||||
"notification.own_poll": "アンケートが終了しました",
|
"notification.own_poll": "アンケートが終了しました",
|
||||||
"notification.poll": "アンケートが終了しました",
|
"notification.poll": "アンケートが終了しました",
|
||||||
"notification.reblog": "{name}さんがあなたのトゥートをブーストしました",
|
"notification.reblog": "{name}さんがあなたの投稿をブーストしました",
|
||||||
"notification.status": "{name}さんがトゥートしました",
|
"notification.status": "{name}さんが投稿しました",
|
||||||
"notification.update": "{name}さんがトゥートを編集しました",
|
"notification.update": "{name}さんが投稿を編集しました",
|
||||||
"notifications.clear": "通知を消去",
|
"notifications.clear": "通知を消去",
|
||||||
"notifications.clear_confirmation": "本当に通知を消去しますか?",
|
"notifications.clear_confirmation": "本当に通知を消去しますか?",
|
||||||
"notifications.column_settings.admin.report": "新しい通報:",
|
"notifications.column_settings.admin.report": "新しい通報:",
|
||||||
@ -427,7 +422,7 @@
|
|||||||
"notifications.column_settings.reblog": "ブースト:",
|
"notifications.column_settings.reblog": "ブースト:",
|
||||||
"notifications.column_settings.show": "カラムに表示",
|
"notifications.column_settings.show": "カラムに表示",
|
||||||
"notifications.column_settings.sound": "通知音を再生",
|
"notifications.column_settings.sound": "通知音を再生",
|
||||||
"notifications.column_settings.status": "新しいトゥート:",
|
"notifications.column_settings.status": "新しい投稿:",
|
||||||
"notifications.column_settings.unread_notifications.category": "未読の通知:",
|
"notifications.column_settings.unread_notifications.category": "未読の通知:",
|
||||||
"notifications.column_settings.unread_notifications.highlight": "未読の通知を強調表示",
|
"notifications.column_settings.unread_notifications.highlight": "未読の通知を強調表示",
|
||||||
"notifications.column_settings.update": "編集:",
|
"notifications.column_settings.update": "編集:",
|
||||||
@ -484,20 +479,20 @@
|
|||||||
"relative_time.today": "今日",
|
"relative_time.today": "今日",
|
||||||
"reply_indicator.cancel": "キャンセル",
|
"reply_indicator.cancel": "キャンセル",
|
||||||
"report.block": "ブロック",
|
"report.block": "ブロック",
|
||||||
"report.block_explanation": "相手のトゥートが表示されなくなります。相手はあなたのトゥートを見ることやフォローすることができません。相手はブロックされていることがわかります。",
|
"report.block_explanation": "相手の投稿が表示されなくなります。相手はあなたの投稿を見ることやフォローすることができません。相手はブロックされていることがわかります。",
|
||||||
"report.categories.other": "その他",
|
"report.categories.other": "その他",
|
||||||
"report.categories.spam": "スパム",
|
"report.categories.spam": "スパム",
|
||||||
"report.categories.violation": "サーバーのルールに違反",
|
"report.categories.violation": "サーバーのルールに違反",
|
||||||
"report.category.subtitle": "近いものを選択してください",
|
"report.category.subtitle": "近いものを選択してください",
|
||||||
"report.category.title": "この{type}について教えてください",
|
"report.category.title": "この{type}について教えてください",
|
||||||
"report.category.title_account": "プロフィール",
|
"report.category.title_account": "プロフィール",
|
||||||
"report.category.title_status": "トゥート",
|
"report.category.title_status": "投稿",
|
||||||
"report.close": "完了",
|
"report.close": "完了",
|
||||||
"report.comment.title": "その他に私たちに伝えておくべき事はありますか?",
|
"report.comment.title": "その他に私たちに伝えておくべき事はありますか?",
|
||||||
"report.forward": "{target}に転送する",
|
"report.forward": "{target}に転送する",
|
||||||
"report.forward_hint": "このアカウントは別のサーバーに所属しています。通報内容を匿名で転送しますか?",
|
"report.forward_hint": "このアカウントは別のサーバーに所属しています。通報内容を匿名で転送しますか?",
|
||||||
"report.mute": "ミュート",
|
"report.mute": "ミュート",
|
||||||
"report.mute_explanation": "相手のトゥートは表示されなくなります。相手は引き続きあなたをフォローして、あなたのトゥートを表示することができますが、ミュートされていることはわかりません。",
|
"report.mute_explanation": "相手の投稿は表示されなくなります。相手は引き続きあなたをフォローして、あなたの投稿を表示することができますが、ミュートされていることはわかりません。",
|
||||||
"report.next": "次へ",
|
"report.next": "次へ",
|
||||||
"report.placeholder": "追加コメント",
|
"report.placeholder": "追加コメント",
|
||||||
"report.reasons.dislike": "興味がありません",
|
"report.reasons.dislike": "興味がありません",
|
||||||
@ -511,7 +506,7 @@
|
|||||||
"report.rules.subtitle": "当てはまるものをすべて選んでください:",
|
"report.rules.subtitle": "当てはまるものをすべて選んでください:",
|
||||||
"report.rules.title": "どのルールに違反していますか?",
|
"report.rules.title": "どのルールに違反していますか?",
|
||||||
"report.statuses.subtitle": "当てはまるものをすべて選んでください:",
|
"report.statuses.subtitle": "当てはまるものをすべて選んでください:",
|
||||||
"report.statuses.title": "この通報を裏付けるようなトゥートはありますか?",
|
"report.statuses.title": "この通報を裏付けるような投稿はありますか?",
|
||||||
"report.submit": "通報する",
|
"report.submit": "通報する",
|
||||||
"report.target": "{target}さんを通報する",
|
"report.target": "{target}さんを通報する",
|
||||||
"report.thanks.take_action": "次のような方法はいかがでしょうか?",
|
"report.thanks.take_action": "次のような方法はいかがでしょうか?",
|
||||||
@ -519,8 +514,8 @@
|
|||||||
"report.thanks.title": "見えないようにしたいですか?",
|
"report.thanks.title": "見えないようにしたいですか?",
|
||||||
"report.thanks.title_actionable": "ご報告ありがとうございます、追って確認します。",
|
"report.thanks.title_actionable": "ご報告ありがとうございます、追って確認します。",
|
||||||
"report.unfollow": "@{name}さんのフォローを解除",
|
"report.unfollow": "@{name}さんのフォローを解除",
|
||||||
"report.unfollow_explanation": "このアカウントをフォローしています。ホームフィードに彼らのトゥートを表示しないようにするには、彼らのフォローを外してください。",
|
"report.unfollow_explanation": "このアカウントをフォローしています。ホームフィードに彼らの投稿を表示しないようにするには、彼らのフォローを外してください。",
|
||||||
"report_notification.attached_statuses": "{count, plural, one {{count}件のトゥート} other {{count}件のトゥート}}が添付されました。",
|
"report_notification.attached_statuses": "{count, plural, one {{count}件の投稿} other {{count}件の投稿}}が添付されました。",
|
||||||
"report_notification.categories.other": "その他",
|
"report_notification.categories.other": "その他",
|
||||||
"report_notification.categories.spam": "スパム",
|
"report_notification.categories.spam": "スパム",
|
||||||
"report_notification.categories.violation": "ルール違反",
|
"report_notification.categories.violation": "ルール違反",
|
||||||
@ -528,17 +523,17 @@
|
|||||||
"search.placeholder": "検索",
|
"search.placeholder": "検索",
|
||||||
"search.search_or_paste": "検索またはURLを入力",
|
"search.search_or_paste": "検索またはURLを入力",
|
||||||
"search_popout.search_format": "高度な検索フォーマット",
|
"search_popout.search_format": "高度な検索フォーマット",
|
||||||
"search_popout.tips.full_text": "表示名やユーザー名、ハッシュタグのほか、あなたのトゥートやお気に入り、ブーストしたトゥート、返信に一致する単純なテキスト。",
|
"search_popout.tips.full_text": "表示名やユーザー名、ハッシュタグのほか、あなたの投稿やお気に入り、ブーストした投稿、返信に一致する単純なテキスト。",
|
||||||
"search_popout.tips.hashtag": "ハッシュタグ",
|
"search_popout.tips.hashtag": "ハッシュタグ",
|
||||||
"search_popout.tips.status": "トゥート",
|
"search_popout.tips.status": "投稿",
|
||||||
"search_popout.tips.text": "表示名やユーザー名、ハッシュタグに一致する単純なテキスト",
|
"search_popout.tips.text": "表示名やユーザー名、ハッシュタグに一致する単純なテキスト",
|
||||||
"search_popout.tips.user": "ユーザー",
|
"search_popout.tips.user": "ユーザー",
|
||||||
"search_results.accounts": "人々",
|
"search_results.accounts": "人々",
|
||||||
"search_results.all": "すべて",
|
"search_results.all": "すべて",
|
||||||
"search_results.hashtags": "ハッシュタグ",
|
"search_results.hashtags": "ハッシュタグ",
|
||||||
"search_results.nothing_found": "この検索条件では何も見つかりませんでした",
|
"search_results.nothing_found": "この検索条件では何も見つかりませんでした",
|
||||||
"search_results.statuses": "トゥート",
|
"search_results.statuses": "投稿",
|
||||||
"search_results.statuses_fts_disabled": "このサーバーではトゥート本文の検索は利用できません。",
|
"search_results.statuses_fts_disabled": "このサーバーでは投稿本文の検索は利用できません。",
|
||||||
"search_results.title": "『{q}』の検索結果",
|
"search_results.title": "『{q}』の検索結果",
|
||||||
"search_results.total": "{count, number}件の結果",
|
"search_results.total": "{count, number}件の結果",
|
||||||
"server_banner.about_active_users": "過去30日間にこのサーバーを使用している人 (月間アクティブユーザー)",
|
"server_banner.about_active_users": "過去30日間にこのサーバーを使用している人 (月間アクティブユーザー)",
|
||||||
@ -549,16 +544,15 @@
|
|||||||
"server_banner.server_stats": "サーバーの情報",
|
"server_banner.server_stats": "サーバーの情報",
|
||||||
"sign_in_banner.create_account": "アカウント作成",
|
"sign_in_banner.create_account": "アカウント作成",
|
||||||
"sign_in_banner.sign_in": "ログイン",
|
"sign_in_banner.sign_in": "ログイン",
|
||||||
"sign_in_banner.text": "ログインしてプロファイルやハッシュタグ、お気に入りをフォローしたり、トゥートを共有したり、返信したり、別のサーバーのアカウントと交流したりできます。",
|
"sign_in_banner.text": "ログインしてプロファイルやハッシュタグ、お気に入りをフォローしたり、投稿を共有したり、返信したり、別のサーバーのアカウントと交流したりできます。",
|
||||||
"status.admin_account": "@{name}さんのモデレーション画面を開く",
|
"status.admin_account": "@{name}さんのモデレーション画面を開く",
|
||||||
"status.admin_domain": "{domain}のモデレーション画面を開く",
|
"status.admin_domain": "{domain}のモデレーション画面を開く",
|
||||||
"status.admin_status": "このトゥートをモデレーション画面で開く",
|
"status.admin_status": "この投稿をモデレーション画面で開く",
|
||||||
"status.block": "@{name}さんをブロック",
|
"status.block": "@{name}さんをブロック",
|
||||||
"status.bookmark": "ブックマーク",
|
"status.bookmark": "ブックマーク",
|
||||||
"status.cancel_reblog_private": "ブースト解除",
|
"status.cancel_reblog_private": "ブースト解除",
|
||||||
"status.cannot_reblog": "このトゥートはブーストできません",
|
"status.cannot_reblog": "この投稿はブーストできません",
|
||||||
"status.copy": "トゥートへのリンクをコピー",
|
"status.copy": "投稿へのリンクをコピー",
|
||||||
"status.cannot_quote": "このトゥートは引用できません",
|
|
||||||
"status.delete": "削除",
|
"status.delete": "削除",
|
||||||
"status.detailed_status": "詳細な会話ビュー",
|
"status.detailed_status": "詳細な会話ビュー",
|
||||||
"status.direct": "@{name}さんにダイレクトメッセージ",
|
"status.direct": "@{name}さんにダイレクトメッセージ",
|
||||||
@ -567,22 +561,20 @@
|
|||||||
"status.edited_x_times": "{count}回編集",
|
"status.edited_x_times": "{count}回編集",
|
||||||
"status.embed": "埋め込み",
|
"status.embed": "埋め込み",
|
||||||
"status.favourite": "お気に入り",
|
"status.favourite": "お気に入り",
|
||||||
"status.filter": "このトゥートをフィルターする",
|
"status.filter": "この投稿をフィルターする",
|
||||||
"status.filtered": "フィルターされました",
|
"status.filtered": "フィルターされました",
|
||||||
"status.hide": "トゥートを非表示",
|
"status.hide": "投稿を非表示",
|
||||||
"status.history.created": "{name}さんが{date}に作成",
|
"status.history.created": "{name}さんが{date}に作成",
|
||||||
"status.history.edited": "{name}さんが{date}に編集",
|
"status.history.edited": "{name}さんが{date}に編集",
|
||||||
"status.load_more": "もっと見る",
|
"status.load_more": "もっと見る",
|
||||||
"status.media_hidden": "非表示のメディア",
|
"status.media_hidden": "非表示のメディア",
|
||||||
"status.mention": "@{name}さんにトゥート",
|
"status.mention": "@{name}さんに投稿",
|
||||||
"status.more": "もっと見る",
|
"status.more": "もっと見る",
|
||||||
"status.mute": "@{name}さんをミュート",
|
"status.mute": "@{name}さんをミュート",
|
||||||
"status.mute_conversation": "会話をミュート",
|
"status.mute_conversation": "会話をミュート",
|
||||||
"status.muted_quote": "ミュートされた引用",
|
|
||||||
"status.open": "詳細を表示",
|
"status.open": "詳細を表示",
|
||||||
"status.pin": "プロフィールに固定表示",
|
"status.pin": "プロフィールに固定表示",
|
||||||
"status.pinned": "固定されたトゥート",
|
"status.pinned": "固定された投稿",
|
||||||
"status.quote": "引用",
|
|
||||||
"status.read_more": "もっと見る",
|
"status.read_more": "もっと見る",
|
||||||
"status.reblog": "ブースト",
|
"status.reblog": "ブースト",
|
||||||
"status.reblog_private": "ブースト",
|
"status.reblog_private": "ブースト",
|
||||||
@ -605,10 +597,9 @@
|
|||||||
"status.translate": "翻訳",
|
"status.translate": "翻訳",
|
||||||
"status.translated_from_with": "{provider}を使って{lang}から翻訳",
|
"status.translated_from_with": "{provider}を使って{lang}から翻訳",
|
||||||
"status.uncached_media_warning": "利用できません",
|
"status.uncached_media_warning": "利用できません",
|
||||||
"status.unlisted_quote": "未収載の引用",
|
|
||||||
"status.unmute_conversation": "会話のミュートを解除",
|
"status.unmute_conversation": "会話のミュートを解除",
|
||||||
"status.unpin": "プロフィールへの固定を解除",
|
"status.unpin": "プロフィールへの固定を解除",
|
||||||
"subscribed_languages.lead": "選択した言語のトゥートだけがホームとリストのタイムラインに表示されます。全ての言語のトゥートを受け取る場合は全てのチェックを外して下さい。",
|
"subscribed_languages.lead": "選択した言語の投稿だけがホームとリストのタイムラインに表示されます。全ての言語の投稿を受け取る場合は全てのチェックを外して下さい。",
|
||||||
"subscribed_languages.save": "変更を保存",
|
"subscribed_languages.save": "変更を保存",
|
||||||
"subscribed_languages.target": "{target}さんの購読言語を変更します",
|
"subscribed_languages.target": "{target}さんの購読言語を変更します",
|
||||||
"suggestions.dismiss": "隠す",
|
"suggestions.dismiss": "隠す",
|
||||||
@ -625,10 +616,10 @@
|
|||||||
"timeline_hint.remote_resource_not_displayed": "他のサーバーの{resource}は表示されません。",
|
"timeline_hint.remote_resource_not_displayed": "他のサーバーの{resource}は表示されません。",
|
||||||
"timeline_hint.resources.followers": "フォロワー",
|
"timeline_hint.resources.followers": "フォロワー",
|
||||||
"timeline_hint.resources.follows": "フォロー",
|
"timeline_hint.resources.follows": "フォロー",
|
||||||
"timeline_hint.resources.statuses": "以前のトゥート",
|
"timeline_hint.resources.statuses": "以前の投稿",
|
||||||
"trends.counter_by_accounts": "過去{days, plural, one {{days}日} other {{days}日}}に{count, plural, one {{counter}人} other {{counter} 人}}",
|
"trends.counter_by_accounts": "過去{days, plural, one {{days}日} other {{days}日}}に{count, plural, one {{counter}人} other {{counter} 人}}",
|
||||||
"trends.trending_now": "トレンドタグ",
|
"trends.trending_now": "トレンドタグ",
|
||||||
"ui.beforeunload": "Mastodonから離れると送信前のトゥートは失われます。",
|
"ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。",
|
||||||
"units.short.billion": "{count}B",
|
"units.short.billion": "{count}B",
|
||||||
"units.short.million": "{count}M",
|
"units.short.million": "{count}M",
|
||||||
"units.short.thousand": "{count}K",
|
"units.short.thousand": "{count}K",
|
||||||
|
@ -4,8 +4,6 @@ import {
|
|||||||
COMPOSE_CHANGE,
|
COMPOSE_CHANGE,
|
||||||
COMPOSE_REPLY,
|
COMPOSE_REPLY,
|
||||||
COMPOSE_REPLY_CANCEL,
|
COMPOSE_REPLY_CANCEL,
|
||||||
COMPOSE_QUOTE,
|
|
||||||
COMPOSE_QUOTE_CANCEL,
|
|
||||||
COMPOSE_DIRECT,
|
COMPOSE_DIRECT,
|
||||||
COMPOSE_MENTION,
|
COMPOSE_MENTION,
|
||||||
COMPOSE_SUBMIT_REQUEST,
|
COMPOSE_SUBMIT_REQUEST,
|
||||||
@ -69,8 +67,6 @@ const initialState = ImmutableMap({
|
|||||||
caretPosition: null,
|
caretPosition: null,
|
||||||
preselectDate: null,
|
preselectDate: null,
|
||||||
in_reply_to: null,
|
in_reply_to: null,
|
||||||
quote_from: null,
|
|
||||||
quote_from_url: null,
|
|
||||||
is_composing: false,
|
is_composing: false,
|
||||||
is_submitting: false,
|
is_submitting: false,
|
||||||
is_changing_upload: false,
|
is_changing_upload: false,
|
||||||
@ -123,8 +119,6 @@ function clearAll(state) {
|
|||||||
map.set('is_submitting', false);
|
map.set('is_submitting', false);
|
||||||
map.set('is_changing_upload', false);
|
map.set('is_changing_upload', false);
|
||||||
map.set('in_reply_to', null);
|
map.set('in_reply_to', null);
|
||||||
map.set('quote_from', null);
|
|
||||||
map.set('quote_from_url', null);
|
|
||||||
map.set('privacy', state.get('default_privacy'));
|
map.set('privacy', state.get('default_privacy'));
|
||||||
map.set('sensitive', state.get('default_sensitive'));
|
map.set('sensitive', state.get('default_sensitive'));
|
||||||
map.set('language', state.get('default_language'));
|
map.set('language', state.get('default_language'));
|
||||||
@ -256,17 +250,6 @@ const expiresInFromExpiresAt = expires_at => {
|
|||||||
return [300, 1800, 3600, 21600, 86400, 259200, 604800].find(expires_in => expires_in >= delta) || 24 * 3600;
|
return [300, 1800, 3600, 21600, 86400, 259200, 604800].find(expires_in => expires_in >= delta) || 24 * 3600;
|
||||||
};
|
};
|
||||||
|
|
||||||
const rejectQuoteAltText = html => {
|
|
||||||
const fragment = domParser.parseFromString(html, 'text/html').documentElement;
|
|
||||||
|
|
||||||
const quote_inline = fragment.querySelector('span.quote-inline');
|
|
||||||
if (quote_inline) {
|
|
||||||
quote_inline.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
return fragment.innerHTML;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
|
const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
|
||||||
prefix = prefix.toLowerCase();
|
prefix = prefix.toLowerCase();
|
||||||
if (suggestions.length < 4) {
|
if (suggestions.length < 4) {
|
||||||
@ -340,20 +323,10 @@ export default function compose(state = initialState, action) {
|
|||||||
case COMPOSE_COMPOSING_CHANGE:
|
case COMPOSE_COMPOSING_CHANGE:
|
||||||
return state.set('is_composing', action.value);
|
return state.set('is_composing', action.value);
|
||||||
case COMPOSE_REPLY:
|
case COMPOSE_REPLY:
|
||||||
case COMPOSE_QUOTE:
|
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.set('id', null);
|
map.set('id', null);
|
||||||
if (action.type === COMPOSE_REPLY) {
|
map.set('in_reply_to', action.status.get('id'));
|
||||||
map.set('in_reply_to', action.status.get('id'));
|
map.set('text', statusToTextMentions(state, action.status));
|
||||||
map.set('quote_from', null);
|
|
||||||
map.set('quote_from_url', null);
|
|
||||||
map.set('text', statusToTextMentions(state, action.status));
|
|
||||||
} else {
|
|
||||||
map.set('in_reply_to', null);
|
|
||||||
map.set('quote_from', action.status.get('id'));
|
|
||||||
map.set('quote_from_url', action.status.get('url'));
|
|
||||||
map.set('text', '');
|
|
||||||
}
|
|
||||||
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
||||||
map.set('focusDate', new Date());
|
map.set('focusDate', new Date());
|
||||||
map.set('caretPosition', null);
|
map.set('caretPosition', null);
|
||||||
@ -385,7 +358,6 @@ export default function compose(state = initialState, action) {
|
|||||||
case COMPOSE_UPLOAD_CHANGE_REQUEST:
|
case COMPOSE_UPLOAD_CHANGE_REQUEST:
|
||||||
return state.set('is_changing_upload', true);
|
return state.set('is_changing_upload', true);
|
||||||
case COMPOSE_REPLY_CANCEL:
|
case COMPOSE_REPLY_CANCEL:
|
||||||
case COMPOSE_QUOTE_CANCEL:
|
|
||||||
case COMPOSE_RESET:
|
case COMPOSE_RESET:
|
||||||
case COMPOSE_SUBMIT_SUCCESS:
|
case COMPOSE_SUBMIT_SUCCESS:
|
||||||
return clearAll(state);
|
return clearAll(state);
|
||||||
@ -484,10 +456,8 @@ export default function compose(state = initialState, action) {
|
|||||||
}));
|
}));
|
||||||
case REDRAFT:
|
case REDRAFT:
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.set('text', action.raw_text || unescapeHTML(rejectQuoteAltText(expandMentions(action.status))));
|
map.set('text', action.raw_text || unescapeHTML(expandMentions(action.status)));
|
||||||
map.set('in_reply_to', action.status.get('in_reply_to_id'));
|
map.set('in_reply_to', action.status.get('in_reply_to_id'));
|
||||||
map.set('quote_from', action.status.getIn(['quote', 'id']));
|
|
||||||
map.set('quote_from_url', action.status.getIn(['quote', 'url']));
|
|
||||||
map.set('privacy', action.status.get('visibility'));
|
map.set('privacy', action.status.get('visibility'));
|
||||||
map.set('media_attachments', action.status.get('media_attachments').map((media) => media.set('unattached', true)));
|
map.set('media_attachments', action.status.get('media_attachments').map((media) => media.set('unattached', true)));
|
||||||
map.set('focusDate', new Date());
|
map.set('focusDate', new Date());
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
COMPOSE_MENTION,
|
COMPOSE_MENTION,
|
||||||
COMPOSE_REPLY,
|
COMPOSE_REPLY,
|
||||||
COMPOSE_QUOTE,
|
|
||||||
COMPOSE_DIRECT,
|
COMPOSE_DIRECT,
|
||||||
} from '../actions/compose';
|
} from '../actions/compose';
|
||||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||||
@ -38,7 +37,6 @@ export default function search(state = initialState, action) {
|
|||||||
case SEARCH_SHOW:
|
case SEARCH_SHOW:
|
||||||
return state.set('hidden', false);
|
return state.set('hidden', false);
|
||||||
case COMPOSE_REPLY:
|
case COMPOSE_REPLY:
|
||||||
case COMPOSE_QUOTE:
|
|
||||||
case COMPOSE_MENTION:
|
case COMPOSE_MENTION:
|
||||||
case COMPOSE_DIRECT:
|
case COMPOSE_DIRECT:
|
||||||
return state.set('hidden', true);
|
return state.set('hidden', true);
|
||||||
|
@ -17,8 +17,6 @@ import {
|
|||||||
STATUS_TRANSLATE_UNDO,
|
STATUS_TRANSLATE_UNDO,
|
||||||
STATUS_FETCH_REQUEST,
|
STATUS_FETCH_REQUEST,
|
||||||
STATUS_FETCH_FAIL,
|
STATUS_FETCH_FAIL,
|
||||||
QUOTE_REVEAL,
|
|
||||||
QUOTE_HIDE,
|
|
||||||
} from '../actions/statuses';
|
} from '../actions/statuses';
|
||||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||||
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
|
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
|
||||||
@ -85,14 +83,6 @@ export default function statuses(state = initialState, action) {
|
|||||||
});
|
});
|
||||||
case STATUS_COLLAPSE:
|
case STATUS_COLLAPSE:
|
||||||
return state.setIn([action.id, 'collapsed'], action.isCollapsed);
|
return state.setIn([action.id, 'collapsed'], action.isCollapsed);
|
||||||
case QUOTE_REVEAL:
|
|
||||||
return state.withMutations(map => {
|
|
||||||
action.ids.forEach(id => map.setIn([id, 'quote_hidden'], false));
|
|
||||||
});
|
|
||||||
case QUOTE_HIDE:
|
|
||||||
return state.withMutations(map => {
|
|
||||||
action.ids.forEach(id => map.setIn([id, 'quote_hidden'], true));
|
|
||||||
});
|
|
||||||
case TIMELINE_DELETE:
|
case TIMELINE_DELETE:
|
||||||
return deleteStatus(state, action.id, action.references);
|
return deleteStatus(state, action.id, action.references);
|
||||||
case STATUS_TRANSLATE_SUCCESS:
|
case STATUS_TRANSLATE_SUCCESS:
|
||||||
|
@ -2,7 +2,6 @@ import { createSelector } from 'reselect';
|
|||||||
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||||
import { toServerSideType } from 'mastodon/utils/filters';
|
import { toServerSideType } from 'mastodon/utils/filters';
|
||||||
import { me } from '../initial_state';
|
import { me } from '../initial_state';
|
||||||
import {reblogRequest} from '../actions/interactions';
|
|
||||||
|
|
||||||
const getAccountBase = (state, id) => state.getIn(['accounts', id], null);
|
const getAccountBase = (state, id) => state.getIn(['accounts', id], null);
|
||||||
const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null);
|
const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null);
|
||||||
@ -36,54 +35,22 @@ export const makeGetStatus = () => {
|
|||||||
[
|
[
|
||||||
(state, { id }) => state.getIn(['statuses', id]),
|
(state, { id }) => state.getIn(['statuses', id]),
|
||||||
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]),
|
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]),
|
||||||
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'quote_id'])]),
|
|
||||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
|
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
|
||||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
|
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
|
||||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'quote_id']), 'account'])]),
|
|
||||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'quote', 'account'])]),
|
|
||||||
(state, { id }) => state.getIn(['relationships', state.getIn(['statuses', id, 'account'])]),
|
|
||||||
(state, { id }) => state.getIn(['relationships', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
|
|
||||||
(state, { id }) => state.getIn(['relationships', state.getIn(['statuses', state.getIn(['statuses', id, 'quote_id']), 'account'])]),
|
|
||||||
(state, { id }) => state.getIn(['accounts', state.getIn(['accounts', state.getIn(['statuses', id, 'account']), 'moved'])]),
|
|
||||||
(state, { id }) => state.getIn(['accounts', state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account']), 'moved'])]),
|
|
||||||
(state, { id }) => state.getIn(['accounts', state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'quote_id']), 'account']), 'moved'])]),
|
|
||||||
getFilters,
|
getFilters,
|
||||||
],
|
],
|
||||||
|
|
||||||
(statusBase, statusReblog, statusQuote, accountBase, accountReblog, accountQuote, accountReblogQuote, relationshipBase, relationshipReblog, relationshipQuote, movedBase, movedReblog, movedQuote, filters) => {
|
(statusBase, statusReblog, accountBase, accountReblog, filters) => {
|
||||||
if (!statusBase || statusBase.get('isLoading')) {
|
if (!statusBase || statusBase.get('isLoading')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
accountBase = accountBase.withMutations(map => {
|
|
||||||
map.set('relationship', relationshipBase);
|
|
||||||
map.set('moved', movedBase);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (statusReblog) {
|
if (statusReblog) {
|
||||||
accountReblog = accountReblog.withMutations(map => {
|
|
||||||
map.set('relationship', relationshipReblog);
|
|
||||||
map.set('moved', movedReblog);
|
|
||||||
});
|
|
||||||
statusReblog = statusReblog.set('account', accountReblog);
|
statusReblog = statusReblog.set('account', accountReblog);
|
||||||
} else {
|
} else {
|
||||||
statusReblog = null;
|
statusReblog = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statusQuote) {
|
|
||||||
accountQuote = accountQuote.withMutations(map => {
|
|
||||||
map.set('relationship', relationshipQuote);
|
|
||||||
map.set('moved', movedQuote);
|
|
||||||
});
|
|
||||||
statusQuote = statusQuote.set('account', accountQuote);
|
|
||||||
} else {
|
|
||||||
statusQuote = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusReblog && accountReblogQuote) {
|
|
||||||
statusReblog = statusReblog.setIn(['quote', 'account'], accountReblogQuote);
|
|
||||||
}
|
|
||||||
|
|
||||||
let filtered = false;
|
let filtered = false;
|
||||||
if ((accountReblog || accountBase).get('id') !== me && filters) {
|
if ((accountReblog || accountBase).get('id') !== me && filters) {
|
||||||
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
|
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
|
||||||
@ -98,7 +65,6 @@ export const makeGetStatus = () => {
|
|||||||
|
|
||||||
return statusBase.withMutations(map => {
|
return statusBase.withMutations(map => {
|
||||||
map.set('reblog', statusReblog);
|
map.set('reblog', statusReblog);
|
||||||
map.set('quote', statusQuote);
|
|
||||||
map.set('account', accountBase);
|
map.set('account', accountBase);
|
||||||
map.set('matched_filters', filtered);
|
map.set('matched_filters', filtered);
|
||||||
});
|
});
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export const uniq = (array) => array.filter((x, i, self) => self.indexOf(x) === i);
|
|
@ -322,29 +322,6 @@ function main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
delegate(document, '.quote-status', 'click', ({ target }) => {
|
|
||||||
if (target.closest('.status__content__spoiler-link') ||
|
|
||||||
target.closest('.media-gallery') ||
|
|
||||||
target.closest('.video-player') ||
|
|
||||||
target.closest('.audio-player')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = target.closest('.quote-status').getAttribute('dataurl');
|
|
||||||
if (target.closest('.status__display-name')) {
|
|
||||||
url = target.closest('.status__display-name').getAttribute('href');
|
|
||||||
} else if (target.closest('.status-card')) {
|
|
||||||
url = target.closest('.status-card').getAttribute('href');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.location.hostname === url.split('/')[2].split(':')[0]) {
|
|
||||||
window.location.href = url;
|
|
||||||
} else {
|
|
||||||
window.open(url, 'blank');
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPolyfills()
|
loadPolyfills()
|
||||||
|
@ -23,5 +23,3 @@
|
|||||||
@import 'mastodon/dashboard';
|
@import 'mastodon/dashboard';
|
||||||
@import 'mastodon/rtl';
|
@import 'mastodon/rtl';
|
||||||
@import 'mastodon/accessibility';
|
@import 'mastodon/accessibility';
|
||||||
|
|
||||||
@import 'plugin';
|
|
@ -1,3 +0,0 @@
|
|||||||
@import 'light-pink/variables';
|
|
||||||
@import 'application';
|
|
||||||
@import 'mastodon-light/diff';
|
|
@ -1,44 +0,0 @@
|
|||||||
// Dependent colors
|
|
||||||
$black: #000000;
|
|
||||||
$white: #ffffff;
|
|
||||||
|
|
||||||
$classic-base-color: #6e202f;
|
|
||||||
$classic-primary-color: #ffa7ae;
|
|
||||||
$classic-secondary-color: #faeef1;
|
|
||||||
$classic-highlight-color: #ff375b;
|
|
||||||
|
|
||||||
// Differences
|
|
||||||
$success-green: lighten(#3c754d, 8%);
|
|
||||||
|
|
||||||
$base-overlay-background: $white !default;
|
|
||||||
$valid-value-color: $success-green !default;
|
|
||||||
|
|
||||||
$ui-base-color: $classic-secondary-color !default;
|
|
||||||
$ui-base-lighter-color: #ffe1e9;
|
|
||||||
$ui-primary-color: #f1adbf;
|
|
||||||
$ui-secondary-color: $classic-base-color !default;
|
|
||||||
$ui-highlight-color: $classic-highlight-color !default;
|
|
||||||
|
|
||||||
$primary-text-color: $black !default;
|
|
||||||
$darker-text-color: $classic-base-color !default;
|
|
||||||
$highlight-text-color: darken($ui-highlight-color, 8%) !default;
|
|
||||||
$dark-text-color: #6e202f;
|
|
||||||
$action-button-color: #ffa7ae;
|
|
||||||
|
|
||||||
$inverted-text-color: $black !default;
|
|
||||||
$lighter-text-color: $classic-base-color !default;
|
|
||||||
$light-text-color: #ffa7ae;
|
|
||||||
|
|
||||||
// Newly added colors
|
|
||||||
$account-background-color: $white !default;
|
|
||||||
|
|
||||||
// Invert darkened and lightened colors
|
|
||||||
@function darken($color, $amount) {
|
|
||||||
@return hsl(hue($color), saturation($color), lightness($color) + $amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
@function lighten($color, $amount) {
|
|
||||||
@return hsl(hue($color), saturation($color), lightness($color) - $amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
$emojis-requiring-inversion: 'chains';
|
|
@ -736,29 +736,10 @@ body > [data-popper-placement] {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
padding-top: 10px;
|
|
||||||
|
|
||||||
.compose-form__publish-button-wrapper {
|
.compose-form__publish-button-wrapper {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
button {
|
|
||||||
display: inline-block;
|
|
||||||
width: auto;
|
|
||||||
|
|
||||||
margin-right: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:last-child {
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.compose-form__utilBtns {
|
|
||||||
padding-top: 10px;
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -827,10 +808,6 @@ body > [data-popper-placement] {
|
|||||||
min-height: 23px;
|
min-height: 23px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex: 0 2 auto;
|
flex: 0 2 auto;
|
||||||
|
|
||||||
&.quote-indicator {
|
|
||||||
background: $success-green;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply-indicator__header {
|
.reply-indicator__header {
|
||||||
@ -1012,10 +989,6 @@ body > [data-popper-placement] {
|
|||||||
|
|
||||||
.status__content.status__content--collapsed {
|
.status__content.status__content--collapsed {
|
||||||
max-height: 22px * 15; // 15 lines is roughly above 500 characters
|
max-height: 22px * 15; // 15 lines is roughly above 500 characters
|
||||||
|
|
||||||
.quote-status & {
|
|
||||||
max-height: 22px * 5;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__content__read-more-button {
|
.status__content__read-more-button {
|
||||||
@ -1088,69 +1061,6 @@ body > [data-popper-placement] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.quote-status {
|
|
||||||
border: solid 1px $ui-base-lighter-color;
|
|
||||||
border-radius: 4px !important;
|
|
||||||
padding: 5px !important;
|
|
||||||
margin-top: 8px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.muted-quote,
|
|
||||||
.unlisted-quote button {
|
|
||||||
color: $dark-text-color;
|
|
||||||
font-size: 15px;
|
|
||||||
width: 100%;
|
|
||||||
border: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.muted-quote {
|
|
||||||
text-align: center;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unlisted-quote button {
|
|
||||||
background-color: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status__avatar,
|
|
||||||
.detailed-status__display-avatar {
|
|
||||||
position: absolute;
|
|
||||||
top: 5px !important;
|
|
||||||
left: 5px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.display-name {
|
|
||||||
padding-left: 56px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detailed-status__display-name {
|
|
||||||
margin-bottom: 0;
|
|
||||||
line-height: unset;
|
|
||||||
|
|
||||||
strong,
|
|
||||||
span {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status__content__text {
|
|
||||||
p {
|
|
||||||
display: inline;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: ' ';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-inline {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.focusable {
|
.focusable {
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
@ -1166,12 +1076,9 @@ body > [data-popper-placement] {
|
|||||||
.status {
|
.status {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
min-height: 54px;
|
min-height: 54px;
|
||||||
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
|
|
||||||
&:not(.quote-status) {
|
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fade {
|
@keyframes fade {
|
||||||
0% { opacity: 0; }
|
0% { opacity: 0; }
|
||||||
100% { opacity: 1; }
|
100% { opacity: 1; }
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
.detailed-status,
|
.detailed-status,
|
||||||
.status:not(.quote-status),
|
.status,
|
||||||
.load-more {
|
.load-more {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
border-radius: 0 0 4px 4px;
|
border-radius: 0 0 4px 4px;
|
||||||
@ -63,18 +63,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.detailed-status .quote-status {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-status {
|
|
||||||
margin-top: 15px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 740px) {
|
@media screen and (max-width: 740px) {
|
||||||
.detailed-status,
|
.detailed-status,
|
||||||
.status:not(.quote-status),
|
.status,
|
||||||
.load-more {
|
.load-more {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
}
|
}
|
||||||
@ -86,10 +77,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.standalone-timeline .quote-status {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.logo-button {
|
.button.logo-button {
|
||||||
flex: 0 auto;
|
flex: 0 auto;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
// ここから下タブバーの実装
|
|
||||||
|
|
||||||
//投稿ボタン
|
|
||||||
.columns-area__panels__main .button.bottom_right {
|
|
||||||
position: fixed;
|
|
||||||
right: 18px;
|
|
||||||
bottom: 65px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
font-size: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-ber-bottom .navber {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 630px) {
|
|
||||||
.tab-ber-bottom .timeline{
|
|
||||||
width:100%;
|
|
||||||
height: calc(100% - 55px);
|
|
||||||
margin: 0 0 50px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-ber-bottom .navber{
|
|
||||||
display: flex;
|
|
||||||
width:100%;
|
|
||||||
bottom: 0;
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-ber-bottom .navber .tabs-bar__wrapper{
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.columns-area__panels__pane-tab-ber{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//ここまで下タブバーの実装
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user