Merge branch 'main' into glitch-soc/merge-upstream
Conflicts: - `README.md`: Discarded upstream changes: we have our own README - `app/controllers/follower_accounts_controller.rb`: Port upstream's minor refactoring
This commit is contained in:
commit
f3a4d57be1
|
@ -1,8 +1,8 @@
|
||||||
version: 2.1
|
version: 2.1
|
||||||
|
|
||||||
orbs:
|
orbs:
|
||||||
ruby: circleci/ruby@1.4.1
|
ruby: circleci/ruby@2.0.0
|
||||||
node: circleci/node@5.0.1
|
node: circleci/node@5.0.3
|
||||||
|
|
||||||
executors:
|
executors:
|
||||||
default:
|
default:
|
||||||
|
@ -19,11 +19,11 @@ executors:
|
||||||
DB_USER: root
|
DB_USER: root
|
||||||
DISABLE_SIMPLECOV: true
|
DISABLE_SIMPLECOV: true
|
||||||
RAILS_ENV: test
|
RAILS_ENV: test
|
||||||
- image: cimg/postgres:14.0
|
- image: cimg/postgres:14.5
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: root
|
POSTGRES_USER: root
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
- image: cimg/redis:6.2
|
- image: cimg/redis:7.0
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
install-system-dependencies:
|
install-system-dependencies:
|
||||||
|
@ -45,7 +45,7 @@ commands:
|
||||||
bundle config without 'development production'
|
bundle config without 'development production'
|
||||||
name: Set bundler settings
|
name: Set bundler settings
|
||||||
- ruby/install-deps:
|
- ruby/install-deps:
|
||||||
bundler-version: '2.3.8'
|
bundler-version: '2.3.26'
|
||||||
key: ruby<< parameters.ruby-version >>-gems-v1
|
key: ruby<< parameters.ruby-version >>-gems-v1
|
||||||
wait-db:
|
wait-db:
|
||||||
steps:
|
steps:
|
||||||
|
@ -221,5 +221,5 @@ workflows:
|
||||||
pkg-manager: yarn
|
pkg-manager: yarn
|
||||||
requires:
|
requires:
|
||||||
- build
|
- build
|
||||||
version: lts
|
version: '16.18'
|
||||||
yarn-run: test:jest
|
yarn-run: test:jest
|
||||||
|
|
|
@ -9,7 +9,7 @@ FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT}
|
||||||
# The value is a comma-separated list of allowed domains
|
# The value is a comma-separated list of allowed domains
|
||||||
ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev"
|
ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev"
|
||||||
|
|
||||||
# [Choice] Node.js version: lts/*, 16, 14, 12, 10
|
# [Choice] Node.js version: lts/*, 18, 16, 14
|
||||||
ARG NODE_VERSION="lts/*"
|
ARG NODE_VERSION="lts/*"
|
||||||
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"
|
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "Mastodon",
|
"name": "Mastodon",
|
||||||
"dockerComposeFile": "docker-compose.yml",
|
"dockerComposeFile": "docker-compose.yml",
|
||||||
"service": "app",
|
"service": "app",
|
||||||
"workspaceFolder": "/workspaces/mastodon",
|
"workspaceFolder": "/mastodon",
|
||||||
|
|
||||||
// Set *default* container specific settings.json values on container create.
|
// Set *default* container specific settings.json values on container create.
|
||||||
"settings": {},
|
"settings": {},
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
"forwardPorts": [3000, 4000],
|
"forwardPorts": [3000, 4000],
|
||||||
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
"postCreateCommand": "bundle install --path vendor/bundle && yarn install && git checkout -- Gemfile.lock && ./bin/rails db:setup",
|
"postCreateCommand": ".devcontainer/post-create.sh",
|
||||||
|
|
||||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||||
"remoteUser": "vscode"
|
"remoteUser": "vscode"
|
||||||
|
|
|
@ -11,9 +11,9 @@ services:
|
||||||
# Use -bullseye variants on local arm64/Apple Silicon.
|
# Use -bullseye variants on local arm64/Apple Silicon.
|
||||||
VARIANT: '3.0-bullseye'
|
VARIANT: '3.0-bullseye'
|
||||||
# Optional Node.js version to install
|
# Optional Node.js version to install
|
||||||
NODE_VERSION: '14'
|
NODE_VERSION: '16'
|
||||||
volumes:
|
volumes:
|
||||||
- ..:/workspaces/mastodon:cached
|
- ..:/mastodon:cached
|
||||||
environment:
|
environment:
|
||||||
RAILS_ENV: development
|
RAILS_ENV: development
|
||||||
NODE_ENV: development
|
NODE_ENV: development
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e # Fail the whole script on first error
|
||||||
|
|
||||||
|
# Fetch Ruby gem dependencies
|
||||||
|
bundle install --path vendor/bundle --with='development test'
|
||||||
|
|
||||||
|
# Fetch Javascript dependencies
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# Make Gemfile.lock pristine again
|
||||||
|
git checkout -- Gemfile.lock
|
||||||
|
|
||||||
|
# [re]create, migrate, and seed the test database
|
||||||
|
RAILS_ENV=test ./bin/rails db:setup
|
||||||
|
|
||||||
|
# Precompile assets for development
|
||||||
|
RAILS_ENV=development ./bin/rails assets:precompile
|
||||||
|
|
||||||
|
# Precompile assets for test
|
||||||
|
RAILS_ENV=test NODE_ENV=tests ./bin/rails assets:precompile
|
|
@ -103,7 +103,7 @@ VAPID_PUBLIC_KEY=
|
||||||
|
|
||||||
# Sending mail
|
# Sending mail
|
||||||
# ------------
|
# ------------
|
||||||
SMTP_SERVER=smtp.mailgun.org
|
SMTP_SERVER=
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_LOGIN=
|
SMTP_LOGIN=
|
||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
|
|
|
@ -17,6 +17,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
- uses: hadolint/hadolint-action@v3.0.0
|
||||||
- uses: docker/setup-qemu-action@v2
|
- uses: docker/setup-qemu-action@v2
|
||||||
- uses: docker/setup-buildx-action@v2
|
- uses: docker/setup-buildx-action@v2
|
||||||
- uses: docker/login-action@v2
|
- uses: docker/login-action@v2
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ "main" ]
|
||||||
|
schedule:
|
||||||
|
- cron: '22 6 * * 1'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'javascript', 'ruby' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||||
|
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
|
||||||
|
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||||
|
# queries: security-extended,security-and-quality
|
||||||
|
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
|
|
||||||
|
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||||
|
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||||
|
|
||||||
|
# - run: |
|
||||||
|
# echo "Run, Build Application using script"
|
||||||
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
||||||
|
with:
|
||||||
|
category: "/language:${{matrix.language}}"
|
200
.rubocop.yml
200
.rubocop.yml
|
@ -1,12 +1,18 @@
|
||||||
require:
|
require:
|
||||||
- rubocop-rails
|
- rubocop-rails
|
||||||
|
- rubocop-rspec
|
||||||
|
- rubocop-performance
|
||||||
|
|
||||||
AllCops:
|
AllCops:
|
||||||
TargetRubyVersion: 2.7
|
TargetRubyVersion: 2.7
|
||||||
NewCops: disable
|
DisplayCopNames: true
|
||||||
|
DisplayStyleGuide: true
|
||||||
|
ExtraDetails: true
|
||||||
|
UseCache: true
|
||||||
|
CacheRootDirectory: tmp
|
||||||
|
NewCops: enable
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'spec/**/*'
|
- db/schema.rb
|
||||||
- 'db/**/*'
|
|
||||||
- 'app/views/**/*'
|
- 'app/views/**/*'
|
||||||
- 'config/**/*'
|
- 'config/**/*'
|
||||||
- 'bin/*'
|
- 'bin/*'
|
||||||
|
@ -67,15 +73,57 @@ Lint/UselessAccessModifier:
|
||||||
- class_methods
|
- class_methods
|
||||||
|
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Max: 115
|
Max: 34 # RuboCop default 17
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/mastodon/*_cli.rb'
|
- 'lib/**/*cli*.rb'
|
||||||
|
- db/*migrate/**/*
|
||||||
|
- lib/paperclip/color_extractor.rb
|
||||||
|
- app/workers/scheduler/follow_recommendations_scheduler.rb
|
||||||
|
- app/services/activitypub/fetch*_service.rb
|
||||||
|
- lib/paperclip/**/*
|
||||||
|
CountRepeatedAttributes: false
|
||||||
|
AllowedMethods:
|
||||||
|
- update_media_attachments!
|
||||||
|
- account_link_to
|
||||||
|
- attempt_oembed
|
||||||
|
- build_crutches
|
||||||
|
- calculate_scores
|
||||||
|
- cc
|
||||||
|
- dump_actor!
|
||||||
|
- filter_from_home?
|
||||||
|
- hydrate
|
||||||
|
- import_bookmarks!
|
||||||
|
- import_relationships!
|
||||||
|
- initialize
|
||||||
|
- link_to_mention
|
||||||
|
- log_target
|
||||||
|
- matches_time_window?
|
||||||
|
- parse_metadata
|
||||||
|
- perform_statuses_search!
|
||||||
|
- privatize_media_attachments!
|
||||||
|
- process_update
|
||||||
|
- publish_media_attachments!
|
||||||
|
- remotable_attachment
|
||||||
|
- render_initial_state
|
||||||
|
- render_with_cache
|
||||||
|
- searchable_by
|
||||||
|
- self.cached_filters_for
|
||||||
|
- set_fetchable_attributes!
|
||||||
|
- signed_request_actor
|
||||||
|
- statuses_to_delete
|
||||||
|
- update_poll!
|
||||||
|
|
||||||
Metrics/BlockLength:
|
Metrics/BlockLength:
|
||||||
Max: 55
|
Max: 55
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/tasks/**/*'
|
|
||||||
- 'lib/mastodon/*_cli.rb'
|
- 'lib/mastodon/*_cli.rb'
|
||||||
|
CountComments: false
|
||||||
|
CountAsOne: [array, heredoc]
|
||||||
|
AllowedMethods:
|
||||||
|
- task
|
||||||
|
- namespace
|
||||||
|
- class_methods
|
||||||
|
- included
|
||||||
|
|
||||||
Metrics/BlockNesting:
|
Metrics/BlockNesting:
|
||||||
Max: 3
|
Max: 3
|
||||||
|
@ -85,34 +133,144 @@ Metrics/BlockNesting:
|
||||||
Metrics/ClassLength:
|
Metrics/ClassLength:
|
||||||
CountComments: false
|
CountComments: false
|
||||||
Max: 500
|
Max: 500
|
||||||
|
CountAsOne: [array, heredoc]
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/mastodon/*_cli.rb'
|
- 'lib/mastodon/*_cli.rb'
|
||||||
|
|
||||||
Metrics/CyclomaticComplexity:
|
Metrics/CyclomaticComplexity:
|
||||||
Max: 25
|
Max: 12
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/mastodon/*_cli.rb'
|
- lib/mastodon/*cli*.rb
|
||||||
|
- db/*migrate/**/*
|
||||||
|
AllowedMethods:
|
||||||
|
- attempt_oembed
|
||||||
|
- blocked?
|
||||||
|
- build_crutches
|
||||||
|
- calculate_scores
|
||||||
|
- cc
|
||||||
|
- discover_endpoint!
|
||||||
|
- filter_from_home?
|
||||||
|
- hydrate
|
||||||
|
- klass
|
||||||
|
- link_to_mention
|
||||||
|
- log_target
|
||||||
|
- matches_time_window?
|
||||||
|
- patch_for_forwarding!
|
||||||
|
- preprocess_attributes!
|
||||||
|
- process_update
|
||||||
|
- remotable_attachment
|
||||||
|
- scan_text!
|
||||||
|
- self.cached_filters_for
|
||||||
|
- set_fetchable_attributes!
|
||||||
|
- setup_redis_env_url
|
||||||
|
- update_media_attachments!
|
||||||
|
|
||||||
Layout/LineLength:
|
Layout/LineLength:
|
||||||
|
Max: 140 # RuboCop default 120
|
||||||
|
AllowHeredoc: true
|
||||||
AllowURI: true
|
AllowURI: true
|
||||||
Enabled: false
|
IgnoreCopDirectives: true
|
||||||
|
AllowedPatterns:
|
||||||
|
# Allow comments to be long lines
|
||||||
|
- !ruby/regexp / \# .*$/
|
||||||
|
- !ruby/regexp /^\# .*$/
|
||||||
|
Exclude:
|
||||||
|
- lib/**/*cli*.rb
|
||||||
|
- db/*migrate/**/*
|
||||||
|
- db/seeds/**/*
|
||||||
|
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
CountComments: false
|
CountComments: false
|
||||||
Max: 65
|
CountAsOne: [array, heredoc]
|
||||||
|
Max: 25 # RuboCop default 10
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/mastodon/*_cli.rb'
|
- 'lib/mastodon/*_cli.rb'
|
||||||
|
AllowedMethods:
|
||||||
|
- account_link_to
|
||||||
|
- attempt_oembed
|
||||||
|
- body_with_limit
|
||||||
|
- build_crutches
|
||||||
|
- cached_filters_for
|
||||||
|
- calculate_scores
|
||||||
|
- check_webfinger!
|
||||||
|
- clean_feeds!
|
||||||
|
- collection_items
|
||||||
|
- collection_presenter
|
||||||
|
- copy_account_notes!
|
||||||
|
- deduplicate_accounts!
|
||||||
|
- deduplicate_conversations!
|
||||||
|
- deduplicate_local_accounts!
|
||||||
|
- deduplicate_statuses!
|
||||||
|
- deduplicate_tags!
|
||||||
|
- deduplicate_users!
|
||||||
|
- discover_endpoint!
|
||||||
|
- extract_extra_uris_with_indices
|
||||||
|
- extract_hashtags_with_indices
|
||||||
|
- extract_mentions_or_lists_with_indices
|
||||||
|
- filter_from_home?
|
||||||
|
- from_elasticsearch
|
||||||
|
- handle_explicit_update!
|
||||||
|
- handle_mark_as_sensitive!
|
||||||
|
- hsl_to_rgb
|
||||||
|
- import_bookmarks!
|
||||||
|
- import_domain_blocks!
|
||||||
|
- import_relationships!
|
||||||
|
- ldap_options
|
||||||
|
- matches_time_window?
|
||||||
|
- outbox_presenter
|
||||||
|
- pam_get_user
|
||||||
|
- parallelize_with_progress
|
||||||
|
- parse_and_transform
|
||||||
|
- patch_for_forwarding!
|
||||||
|
- populate_home
|
||||||
|
- post_process_style
|
||||||
|
- preload_cache_collection_target_statuses
|
||||||
|
- privatize_media_attachments!
|
||||||
|
- provides_callback_for
|
||||||
|
- publish_media_attachments!
|
||||||
|
- relevant_account_timestamp
|
||||||
|
- remotable_attachment
|
||||||
|
- rgb_to_hsl
|
||||||
|
- rss_status_content_format
|
||||||
|
- set_fetchable_attributes!
|
||||||
|
- setup_redis_env_url
|
||||||
|
- signed_request_actor
|
||||||
|
- to_preview_card_attributes
|
||||||
|
- upgrade_storage_filesystem
|
||||||
|
- upgrade_storage_s3
|
||||||
|
- user_settings_params
|
||||||
|
- hydrate
|
||||||
|
- cc
|
||||||
|
- self_destruct
|
||||||
|
|
||||||
Metrics/ModuleLength:
|
Metrics/ModuleLength:
|
||||||
CountComments: false
|
CountComments: false
|
||||||
Max: 200
|
Max: 200
|
||||||
|
CountAsOne: [array, heredoc]
|
||||||
|
|
||||||
Metrics/ParameterLists:
|
Metrics/ParameterLists:
|
||||||
Max: 5
|
Max: 5 # RuboCop default 5
|
||||||
CountKeywordArgs: true
|
CountKeywordArgs: true # RuboCop default true
|
||||||
|
MaxOptionalParameters: 3 # RuboCop default 3
|
||||||
|
Exclude:
|
||||||
|
- app/models/concerns/account_interactions.rb
|
||||||
|
- app/services/activitypub/fetch_remote_account_service.rb
|
||||||
|
- app/services/activitypub/fetch_remote_actor_service.rb
|
||||||
|
|
||||||
Metrics/PerceivedComplexity:
|
Metrics/PerceivedComplexity:
|
||||||
Max: 25
|
Max: 16 # RuboCop default 8
|
||||||
|
AllowedMethods:
|
||||||
|
- attempt_oembed
|
||||||
|
- build_crutches
|
||||||
|
- calculate_scores
|
||||||
|
- deduplicate_users!
|
||||||
|
- discover_endpoint!
|
||||||
|
- filter_from_home?
|
||||||
|
- hydrate
|
||||||
|
- patch_for_forwarding!
|
||||||
|
- process_update
|
||||||
|
- remove_orphans
|
||||||
|
- update_media_attachments!
|
||||||
|
|
||||||
Naming/MemoizedInstanceVariableName:
|
Naming/MemoizedInstanceVariableName:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
@ -267,9 +425,6 @@ Style/PercentLiteralDelimiters:
|
||||||
Style/PerlBackrefs:
|
Style/PerlBackrefs:
|
||||||
AutoCorrect: false
|
AutoCorrect: false
|
||||||
|
|
||||||
Style/RedundantAssignment:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/RedundantFetchBlock:
|
Style/RedundantFetchBlock:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
|
||||||
|
@ -292,7 +447,7 @@ Style/RegexpLiteral:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Style/RescueStandardError:
|
Style/RescueStandardError:
|
||||||
Enabled: false
|
Enabled: true
|
||||||
|
|
||||||
Style/SignalException:
|
Style/SignalException:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
@ -311,3 +466,14 @@ Style/TrailingCommaInHashLiteral:
|
||||||
|
|
||||||
Style/UnpackFirst:
|
Style/UnpackFirst:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
RSpec/ScatteredSetup:
|
||||||
|
Enabled: false
|
||||||
|
RSpec/ImplicitExpect:
|
||||||
|
Enabled: false
|
||||||
|
RSpec/NamedSubject:
|
||||||
|
Enabled: false
|
||||||
|
RSpec/DescribeClass:
|
||||||
|
Enabled: false
|
||||||
|
RSpec/LetSetup:
|
||||||
|
Enabled: false
|
||||||
|
|
22
Aptfile
22
Aptfile
|
@ -1,26 +1,4 @@
|
||||||
ffmpeg
|
ffmpeg
|
||||||
libicu[0-9][0-9]
|
|
||||||
libicu-dev
|
|
||||||
libidn12
|
|
||||||
libidn-dev
|
|
||||||
libpq-dev
|
libpq-dev
|
||||||
libxdamage1
|
libxdamage1
|
||||||
libxfixes3
|
libxfixes3
|
||||||
zlib1g-dev
|
|
||||||
libcairo2
|
|
||||||
libcroco3
|
|
||||||
libdatrie1
|
|
||||||
libgdk-pixbuf2.0-0
|
|
||||||
libgraphite2-3
|
|
||||||
libharfbuzz0b
|
|
||||||
libpango-1.0-0
|
|
||||||
libpangocairo-1.0-0
|
|
||||||
libpangoft2-1.0-0
|
|
||||||
libpixman-1-0
|
|
||||||
librsvg2-2
|
|
||||||
libthai-data
|
|
||||||
libthai0
|
|
||||||
libvpx[5-9]
|
|
||||||
libxcb-render0
|
|
||||||
libxcb-shm0
|
|
||||||
libxrender1
|
|
||||||
|
|
|
@ -15,7 +15,8 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
WORKDIR /opt/mastodon
|
WORKDIR /opt/mastodon
|
||||||
COPY Gemfile* package.json yarn.lock /opt/mastodon/
|
COPY Gemfile* package.json yarn.lock /opt/mastodon/
|
||||||
|
|
||||||
RUN apt update && \
|
# hadolint ignore=DL3008
|
||||||
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends build-essential \
|
apt-get install -y --no-install-recommends build-essential \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
git \
|
git \
|
||||||
|
@ -50,10 +51,12 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
ENV DEBIAN_FRONTEND="noninteractive" \
|
ENV DEBIAN_FRONTEND="noninteractive" \
|
||||||
PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin"
|
PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin"
|
||||||
|
|
||||||
|
# Ignoreing these here since we don't want to pin any versions and the Debian image removes apt-get content after use
|
||||||
|
# hadolint ignore=DL3008,DL3009
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
echo "Etc/UTC" > /etc/localtime && \
|
echo "Etc/UTC" > /etc/localtime && \
|
||||||
groupadd -g "${GID}" mastodon && \
|
groupadd -g "${GID}" mastodon && \
|
||||||
useradd -u "$UID" -g "${GID}" -m -d /opt/mastodon mastodon && \
|
useradd -l -u "$UID" -g "${GID}" -m -d /opt/mastodon mastodon && \
|
||||||
apt-get -y --no-install-recommends install whois \
|
apt-get -y --no-install-recommends install whois \
|
||||||
wget \
|
wget \
|
||||||
procps \
|
procps \
|
||||||
|
|
11
Gemfile
11
Gemfile
|
@ -107,6 +107,10 @@ group :development, :test do
|
||||||
gem 'pry-byebug', '~> 3.10'
|
gem 'pry-byebug', '~> 3.10'
|
||||||
gem 'pry-rails', '~> 0.3'
|
gem 'pry-rails', '~> 0.3'
|
||||||
gem 'rspec-rails', '~> 5.1'
|
gem 'rspec-rails', '~> 5.1'
|
||||||
|
gem 'rubocop-performance', require: false
|
||||||
|
gem 'rubocop-rails', require: false
|
||||||
|
gem 'rubocop-rspec', require: false
|
||||||
|
gem 'rubocop', require: false
|
||||||
end
|
end
|
||||||
|
|
||||||
group :production, :test do
|
group :production, :test do
|
||||||
|
@ -117,13 +121,14 @@ group :test do
|
||||||
gem 'capybara', '~> 3.38'
|
gem 'capybara', '~> 3.38'
|
||||||
gem 'climate_control', '~> 0.2'
|
gem 'climate_control', '~> 0.2'
|
||||||
gem 'faker', '~> 3.0'
|
gem 'faker', '~> 3.0'
|
||||||
|
gem 'json-schema', '~> 3.0'
|
||||||
gem 'microformats', '~> 4.4'
|
gem 'microformats', '~> 4.4'
|
||||||
|
gem 'rack-test', '~> 2.0'
|
||||||
gem 'rails-controller-testing', '~> 1.0'
|
gem 'rails-controller-testing', '~> 1.0'
|
||||||
|
gem 'rspec_junit_formatter', '~> 0.6'
|
||||||
gem 'rspec-sidekiq', '~> 3.1'
|
gem 'rspec-sidekiq', '~> 3.1'
|
||||||
gem 'simplecov', '~> 0.21', require: false
|
gem 'simplecov', '~> 0.21', require: false
|
||||||
gem 'webmock', '~> 3.18'
|
gem 'webmock', '~> 3.18'
|
||||||
gem 'rspec_junit_formatter', '~> 0.6'
|
|
||||||
gem 'rack-test', '~> 2.0'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
|
@ -135,8 +140,6 @@ group :development do
|
||||||
gem 'letter_opener', '~> 1.8'
|
gem 'letter_opener', '~> 1.8'
|
||||||
gem 'letter_opener_web', '~> 2.0'
|
gem 'letter_opener_web', '~> 2.0'
|
||||||
gem 'memory_profiler'
|
gem 'memory_profiler'
|
||||||
gem 'rubocop', '~> 1.30', require: false
|
|
||||||
gem 'rubocop-rails', '~> 2.15', require: false
|
|
||||||
gem 'brakeman', '~> 5.4', require: false
|
gem 'brakeman', '~> 5.4', require: false
|
||||||
gem 'bundler-audit', '~> 0.9', require: false
|
gem 'bundler-audit', '~> 0.9', require: false
|
||||||
|
|
||||||
|
|
33
Gemfile.lock
33
Gemfile.lock
|
@ -346,6 +346,8 @@ GEM
|
||||||
json-ld-preloaded (3.2.2)
|
json-ld-preloaded (3.2.2)
|
||||||
json-ld (~> 3.2)
|
json-ld (~> 3.2)
|
||||||
rdf (~> 3.2)
|
rdf (~> 3.2)
|
||||||
|
json-schema (3.0.0)
|
||||||
|
addressable (>= 2.8)
|
||||||
jsonapi-renderer (0.2.2)
|
jsonapi-renderer (0.2.2)
|
||||||
jwt (2.4.1)
|
jwt (2.4.1)
|
||||||
kaminari (1.2.2)
|
kaminari (1.2.2)
|
||||||
|
@ -587,21 +589,27 @@ GEM
|
||||||
rspec-support (3.11.1)
|
rspec-support (3.11.1)
|
||||||
rspec_junit_formatter (0.6.0)
|
rspec_junit_formatter (0.6.0)
|
||||||
rspec-core (>= 2, < 4, != 2.12.0)
|
rspec-core (>= 2, < 4, != 2.12.0)
|
||||||
rubocop (1.30.1)
|
rubocop (1.39.0)
|
||||||
|
json (~> 2.3)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.1.0.0)
|
parser (>= 3.1.2.1)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
rexml (>= 3.2.5, < 4.0)
|
rexml (>= 3.2.5, < 4.0)
|
||||||
rubocop-ast (>= 1.18.0, < 2.0)
|
rubocop-ast (>= 1.23.0, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 3.0)
|
unicode-display_width (>= 1.4.0, < 3.0)
|
||||||
rubocop-ast (1.18.0)
|
rubocop-ast (1.23.0)
|
||||||
parser (>= 3.1.1.0)
|
parser (>= 3.1.1.0)
|
||||||
rubocop-rails (2.15.0)
|
rubocop-performance (1.15.1)
|
||||||
|
rubocop (>= 1.7.0, < 2.0)
|
||||||
|
rubocop-ast (>= 0.4.0)
|
||||||
|
rubocop-rails (2.17.2)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.7.0, < 2.0)
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
|
rubocop-rspec (2.15.0)
|
||||||
|
rubocop (~> 1.33)
|
||||||
ruby-progressbar (1.11.0)
|
ruby-progressbar (1.11.0)
|
||||||
ruby-saml (1.13.0)
|
ruby-saml (1.13.0)
|
||||||
nokogiri (>= 1.10.5)
|
nokogiri (>= 1.10.5)
|
||||||
|
@ -794,6 +802,7 @@ DEPENDENCIES
|
||||||
idn-ruby
|
idn-ruby
|
||||||
json-ld
|
json-ld
|
||||||
json-ld-preloaded (~> 3.2)
|
json-ld-preloaded (~> 3.2)
|
||||||
|
json-schema (~> 3.0)
|
||||||
kaminari (~> 1.2)
|
kaminari (~> 1.2)
|
||||||
kt-paperclip (~> 7.1)
|
kt-paperclip (~> 7.1)
|
||||||
letter_opener (~> 1.8)
|
letter_opener (~> 1.8)
|
||||||
|
@ -843,8 +852,10 @@ DEPENDENCIES
|
||||||
rspec-rails (~> 5.1)
|
rspec-rails (~> 5.1)
|
||||||
rspec-sidekiq (~> 3.1)
|
rspec-sidekiq (~> 3.1)
|
||||||
rspec_junit_formatter (~> 0.6)
|
rspec_junit_formatter (~> 0.6)
|
||||||
rubocop (~> 1.30)
|
rubocop
|
||||||
rubocop-rails (~> 2.15)
|
rubocop-performance
|
||||||
|
rubocop-rails
|
||||||
|
rubocop-rspec
|
||||||
ruby-progressbar (~> 1.11)
|
ruby-progressbar (~> 1.11)
|
||||||
sanitize (~> 6.0)
|
sanitize (~> 6.0)
|
||||||
scenic (~> 1.6)
|
scenic (~> 1.6)
|
||||||
|
@ -869,3 +880,9 @@ DEPENDENCIES
|
||||||
webpacker (~> 5.4)
|
webpacker (~> 5.4)
|
||||||
webpush!
|
webpush!
|
||||||
xorcist (~> 1.1)
|
xorcist (~> 1.1)
|
||||||
|
|
||||||
|
RUBY VERSION
|
||||||
|
ruby 3.0.4p208
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
2.2.33
|
||||||
|
|
|
@ -55,12 +55,8 @@ module Admin
|
||||||
def update
|
def update
|
||||||
authorize :domain_block, :update?
|
authorize :domain_block, :update?
|
||||||
|
|
||||||
@domain_block.update(update_params)
|
if @domain_block.update(update_params)
|
||||||
|
DomainBlockWorker.perform_async(@domain_block.id, @domain_block.severity_previously_changed?)
|
||||||
severity_changed = @domain_block.severity_changed?
|
|
||||||
|
|
||||||
if @domain_block.save
|
|
||||||
DomainBlockWorker.perform_async(@domain_block.id, severity_changed)
|
|
||||||
log_action :update, @domain_block
|
log_action :update, @domain_block
|
||||||
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
|
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
|
||||||
else
|
else
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
module Admin
|
module Admin
|
||||||
class RelaysController < BaseController
|
class RelaysController < BaseController
|
||||||
before_action :set_relay, except: [:index, :new, :create]
|
before_action :set_relay, except: [:index, :new, :create]
|
||||||
before_action :require_signatures_enabled!, only: [:new, :create, :enable]
|
before_action :warn_signatures_not_enabled!, only: [:new, :create, :enable]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
authorize :relay, :update?
|
authorize :relay, :update?
|
||||||
|
@ -56,8 +56,8 @@ module Admin
|
||||||
params.require(:relay).permit(:inbox_url)
|
params.require(:relay).permit(:inbox_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_signatures_enabled!
|
def warn_signatures_not_enabled!
|
||||||
redirect_to admin_relays_path, alert: I18n.t('admin.relays.signatures_not_enabled') if authorized_fetch_mode?
|
flash.now[:error] = I18n.t('admin.relays.signatures_not_enabled') if authorized_fetch_mode?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,6 +16,26 @@ class Api::BaseController < ApplicationController
|
||||||
|
|
||||||
protect_from_forgery with: :null_session
|
protect_from_forgery with: :null_session
|
||||||
|
|
||||||
|
content_security_policy do |p|
|
||||||
|
# Set every directive that does not have a fallback
|
||||||
|
p.default_src :none
|
||||||
|
p.frame_ancestors :none
|
||||||
|
p.form_action :none
|
||||||
|
|
||||||
|
# Disable every directive with a fallback to cut on response size
|
||||||
|
p.base_uri false
|
||||||
|
p.font_src false
|
||||||
|
p.img_src false
|
||||||
|
p.style_src false
|
||||||
|
p.media_src false
|
||||||
|
p.frame_src false
|
||||||
|
p.manifest_src false
|
||||||
|
p.connect_src false
|
||||||
|
p.script_src false
|
||||||
|
p.child_src false
|
||||||
|
p.worker_src false
|
||||||
|
end
|
||||||
|
|
||||||
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
|
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
|
||||||
render json: { error: e.to_s }, status: 422
|
render json: { error: e.to_s }, status: 422
|
||||||
end
|
end
|
||||||
|
|
|
@ -40,10 +40,8 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
|
||||||
|
|
||||||
def update
|
def update
|
||||||
authorize @domain_block, :update?
|
authorize @domain_block, :update?
|
||||||
@domain_block.update(domain_block_params)
|
@domain_block.update!(domain_block_params)
|
||||||
severity_changed = @domain_block.severity_changed?
|
DomainBlockWorker.perform_async(@domain_block.id, @domain_block.severity_previously_changed?)
|
||||||
@domain_block.save!
|
|
||||||
DomainBlockWorker.perform_async(@domain_block.id, severity_changed)
|
|
||||||
log_action :update, @domain_block
|
log_action :update, @domain_block
|
||||||
render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer
|
render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer
|
||||||
end
|
end
|
||||||
|
|
|
@ -40,7 +40,7 @@ class Api::V1::NotificationsController < Api::BaseController
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_notifications
|
def load_notifications
|
||||||
notifications = browserable_account_notifications.includes(from_account: :account_stat).to_a_paginated_by_id(
|
notifications = browserable_account_notifications.includes(from_account: [:account_stat, :user]).to_a_paginated_by_id(
|
||||||
limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
|
limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
|
||||||
params_slice(:max_id, :since_id, :min_id)
|
params_slice(:max_id, :since_id, :min_id)
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,6 +11,8 @@ class Auth::PasswordsController < Devise::PasswordsController
|
||||||
super do |resource|
|
super do |resource|
|
||||||
if resource.errors.empty?
|
if resource.errors.empty?
|
||||||
resource.session_activations.destroy_all
|
resource.session_activations.destroy_all
|
||||||
|
|
||||||
|
resource.revoke_access!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -57,8 +57,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
end
|
end
|
||||||
|
|
||||||
def configure_sign_up_params
|
def configure_sign_up_params
|
||||||
devise_parameter_sanitizer.permit(:sign_up) do |u|
|
devise_parameter_sanitizer.permit(:sign_up) do |user_params|
|
||||||
u.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password)
|
user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,7 @@ module RateLimitHeaders
|
||||||
end
|
end
|
||||||
|
|
||||||
def api_throttle_data
|
def api_throttle_data
|
||||||
most_limited_type, = request.env['rack.attack.throttle_data'].min_by { |_, v| v[:limit] - v[:count] }
|
most_limited_type, = request.env['rack.attack.throttle_data'].min_by { |_key, value| value[:limit] - value[:count] }
|
||||||
request.env['rack.attack.throttle_data'][most_limited_type]
|
request.env['rack.attack.throttle_data'][most_limited_type]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,8 @@ module SignatureVerification
|
||||||
end
|
end
|
||||||
|
|
||||||
class SignatureParamsTransformer < Parslet::Transform
|
class SignatureParamsTransformer < Parslet::Transform
|
||||||
rule(params: subtree(:p)) do
|
rule(params: subtree(:param)) do
|
||||||
(p.is_a?(Array) ? p : [p]).each_with_object({}) { |(key, val), h| h[key] = val }
|
(param.is_a?(Array) ? param : [param]).each_with_object({}) { |(key, value), hash| hash[key] = value }
|
||||||
end
|
end
|
||||||
|
|
||||||
rule(param: { key: simple(:key), value: simple(:val) }) do
|
rule(param: { key: simple(:key), value: simple(:val) }) do
|
||||||
|
|
|
@ -63,7 +63,7 @@ class FollowerAccountsController < ApplicationController
|
||||||
if page_requested?
|
if page_requested?
|
||||||
ActivityPub::CollectionPresenter.new(
|
ActivityPub::CollectionPresenter.new(
|
||||||
id: account_followers_url(@account, page: params.fetch(:page, 1)),
|
id: account_followers_url(@account, page: params.fetch(:page, 1)),
|
||||||
items: follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) },
|
items: follows.map { |follow| ActivityPub::TagManager.instance.uri_for(follow.account) },
|
||||||
part_of: account_followers_url(@account),
|
part_of: account_followers_url(@account),
|
||||||
next: next_page_url,
|
next: next_page_url,
|
||||||
prev: prev_page_url,
|
prev: prev_page_url,
|
||||||
|
|
|
@ -66,7 +66,7 @@ class FollowingAccountsController < ApplicationController
|
||||||
id: account_following_index_url(@account, page: params.fetch(:page, 1)),
|
id: account_following_index_url(@account, page: params.fetch(:page, 1)),
|
||||||
type: :ordered,
|
type: :ordered,
|
||||||
size: @account.following_count,
|
size: @account.following_count,
|
||||||
items: follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.target_account) },
|
items: follows.map { |follow| ActivityPub::TagManager.instance.uri_for(follow.target_account) },
|
||||||
part_of: account_following_index_url(@account),
|
part_of: account_following_index_url(@account),
|
||||||
next: next_page_url,
|
next: next_page_url,
|
||||||
prev: prev_page_url
|
prev: prev_page_url
|
||||||
|
|
|
@ -13,8 +13,8 @@ class MediaController < ApplicationController
|
||||||
before_action :allow_iframing, only: :player
|
before_action :allow_iframing, only: :player
|
||||||
before_action :set_pack, only: :player
|
before_action :set_pack, only: :player
|
||||||
|
|
||||||
content_security_policy only: :player do |p|
|
content_security_policy only: :player do |policy|
|
||||||
p.frame_ancestors(false)
|
policy.frame_ancestors(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
|
|
@ -17,8 +17,8 @@ class StatusesController < ApplicationController
|
||||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||||
skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode?
|
skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode?
|
||||||
|
|
||||||
content_security_policy only: :embed do |p|
|
content_security_policy only: :embed do |policy|
|
||||||
p.frame_ancestors(false)
|
policy.frame_ancestors(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
|
|
@ -65,7 +65,7 @@ class TagsController < ApplicationController
|
||||||
id: tag_url(@tag),
|
id: tag_url(@tag),
|
||||||
type: :ordered,
|
type: :ordered,
|
||||||
size: @tag.statuses.count,
|
size: @tag.statuses.count,
|
||||||
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
|
items: @statuses.map { |status| ActivityPub::TagManager.instance.uri_for(status) }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,19 +23,28 @@ module FormattingHelper
|
||||||
|
|
||||||
before_html = begin
|
before_html = begin
|
||||||
if status.spoiler_text?
|
if status.spoiler_text?
|
||||||
"<p><strong>#{I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale)}</strong> #{h(status.spoiler_text)}</p><hr />"
|
tag.p do
|
||||||
else
|
tag.strong do
|
||||||
''
|
I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale)
|
||||||
|
end
|
||||||
|
|
||||||
|
status.spoiler_text
|
||||||
|
end + tag.hr
|
||||||
end
|
end
|
||||||
end.html_safe # rubocop:disable Rails/OutputSafety
|
end
|
||||||
|
|
||||||
after_html = begin
|
after_html = begin
|
||||||
if status.preloadable_poll
|
if status.preloadable_poll
|
||||||
"<p>#{status.preloadable_poll.options.map { |o| "<input type=#{status.preloadable_poll.multiple? ? 'checkbox' : 'radio'} disabled /> #{h(o)}" }.join('<br />')}</p>"
|
tag.p do
|
||||||
else
|
safe_join(
|
||||||
''
|
status.preloadable_poll.options.map do |o|
|
||||||
|
tag.send(status.preloadable_poll.multiple? ? 'checkbox' : 'radio', o, disabled: true)
|
||||||
|
end,
|
||||||
|
tag.br
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end.html_safe # rubocop:disable Rails/OutputSafety
|
end
|
||||||
|
|
||||||
prerender_custom_emojis(
|
prerender_custom_emojis(
|
||||||
safe_join([before_html, html, after_html]),
|
safe_join([before_html, html, after_html]),
|
||||||
|
|
|
@ -190,12 +190,15 @@ module LanguagesHelper
|
||||||
ISO_639_3 = {
|
ISO_639_3 = {
|
||||||
ast: ['Asturian', 'Asturianu'].freeze,
|
ast: ['Asturian', 'Asturianu'].freeze,
|
||||||
ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze,
|
ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze,
|
||||||
|
cnr: ['Montenegrin', 'crnogorski'].freeze,
|
||||||
jbo: ['Lojban', 'la .lojban.'].freeze,
|
jbo: ['Lojban', 'la .lojban.'].freeze,
|
||||||
kab: ['Kabyle', 'Taqbaylit'].freeze,
|
kab: ['Kabyle', 'Taqbaylit'].freeze,
|
||||||
kmr: ['Kurmanji (Kurdish)', 'Kurmancî'].freeze,
|
kmr: ['Kurmanji (Kurdish)', 'Kurmancî'].freeze,
|
||||||
ldn: ['Láadan', 'Láadan'].freeze,
|
ldn: ['Láadan', 'Láadan'].freeze,
|
||||||
lfn: ['Lingua Franca Nova', 'lingua franca nova'].freeze,
|
lfn: ['Lingua Franca Nova', 'lingua franca nova'].freeze,
|
||||||
sco: ['Scots', 'Scots'].freeze,
|
sco: ['Scots', 'Scots'].freeze,
|
||||||
|
sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze,
|
||||||
|
smj: ['Lule Sami', 'Julevsámegiella'].freeze,
|
||||||
tok: ['Toki Pona', 'toki pona'].freeze,
|
tok: ['Toki Pona', 'toki pona'].freeze,
|
||||||
zba: ['Balaibalan', 'باليبلن'].freeze,
|
zba: ['Balaibalan', 'باليبلن'].freeze,
|
||||||
zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze,
|
zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze,
|
||||||
|
|
|
@ -21,7 +21,7 @@ module StatusesHelper
|
||||||
def media_summary(status)
|
def media_summary(status)
|
||||||
attachments = { image: 0, video: 0, audio: 0 }
|
attachments = { image: 0, video: 0, audio: 0 }
|
||||||
|
|
||||||
status.media_attachments.each do |media|
|
status.ordered_media_attachments.each do |media|
|
||||||
if media.video?
|
if media.video?
|
||||||
attachments[:video] += 1
|
attachments[:video] += 1
|
||||||
elsif media.audio?
|
elsif media.audio?
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="79" height="79" viewBox="0 0 79 75"><symbol id="logo-symbol-icon"><path d="M74.7135 16.6043C73.6199 8.54587 66.5351 2.19527 58.1366 0.964691C56.7196 0.756754 51.351 0 38.9148 0H38.822C26.3824 0 23.7135 0.756754 22.2966 0.964691C14.1319 2.16118 6.67571 7.86752 4.86669 16.0214C3.99657 20.0369 3.90371 24.4888 4.06535 28.5726C4.29578 34.4289 4.34049 40.275 4.877 46.1075C5.24791 49.9817 5.89495 53.8251 6.81328 57.6088C8.53288 64.5968 15.4938 70.4122 22.3138 72.7848C29.6155 75.259 37.468 75.6697 44.9919 73.971C45.8196 73.7801 46.6381 73.5586 47.4475 73.3063C49.2737 72.7302 51.4164 72.086 52.9915 70.9542C53.0131 70.9384 53.0308 70.9178 53.0433 70.8942C53.0558 70.8706 53.0628 70.8445 53.0637 70.8179V65.1661C53.0634 65.1412 53.0574 65.1167 53.0462 65.0944C53.035 65.0721 53.0189 65.0525 52.9992 65.0371C52.9794 65.0218 52.9564 65.011 52.9318 65.0056C52.9073 65.0002 52.8819 65.0003 52.8574 65.0059C48.0369 66.1472 43.0971 66.7193 38.141 66.7103C29.6118 66.7103 27.3178 62.6981 26.6609 61.0278C26.1329 59.5842 25.7976 58.0784 25.6636 56.5486C25.6622 56.5229 25.667 56.4973 25.6775 56.4738C25.688 56.4502 25.7039 56.4295 25.724 56.4132C25.7441 56.397 25.7678 56.3856 25.7931 56.3801C25.8185 56.3746 25.8448 56.3751 25.8699 56.3816C30.6101 57.5151 35.4693 58.0873 40.3455 58.086C41.5183 58.086 42.6876 58.086 43.8604 58.0553C48.7647 57.919 53.9339 57.6701 58.7591 56.7361C58.8794 56.7123 58.9998 56.6918 59.103 56.6611C66.7139 55.2124 73.9569 50.665 74.6929 39.1501C74.7204 38.6967 74.7892 34.4016 74.7892 33.9312C74.7926 32.3325 75.3085 22.5901 74.7135 16.6043ZM62.9996 45.3371H54.9966V25.9069C54.9966 21.8163 53.277 19.7302 49.7793 19.7302C45.9343 19.7302 44.0083 22.1981 44.0083 27.0727V37.7082H36.0534V27.0727C36.0534 22.1981 34.124 19.7302 30.279 19.7302C26.8019 19.7302 25.0651 21.8163 25.0617 25.9069V45.3371H17.0656V25.3172C17.0656 21.2266 18.1191 17.9769 20.2262 15.568C22.3998 13.1648 25.2509 11.9308 28.7898 11.9308C32.8859 11.9308 35.9812 13.492 38.0447 16.6111L40.036 19.9245L42.0308 16.6111C44.0943 13.492 47.1896 11.9308 51.2788 11.9308C54.8143 11.9308 57.6654 13.1648 59.8459 15.568C61.9529 17.9746 63.0065 21.2243 63.0065 25.3172L62.9996 45.3371Z" fill="currentColor"/></symbol><use xlink:href="#logo-symbol-icon" style="color:#fff" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="79" height="79" viewBox="0 0 79 75"><symbol id="logo-symbol-icon"><path d="M74.7135 16.6043C73.6199 8.54587 66.5351 2.19527 58.1366 0.964691C56.7196 0.756754 51.351 0 38.9148 0H38.822C26.3824 0 23.7135 0.756754 22.2966 0.964691C14.1319 2.16118 6.67571 7.86752 4.86669 16.0214C3.99657 20.0369 3.90371 24.4888 4.06535 28.5726C4.29578 34.4289 4.34049 40.275 4.877 46.1075C5.24791 49.9817 5.89495 53.8251 6.81328 57.6088C8.53288 64.5968 15.4938 70.4122 22.3138 72.7848C29.6155 75.259 37.468 75.6697 44.9919 73.971C45.8196 73.7801 46.6381 73.5586 47.4475 73.3063C49.2737 72.7302 51.4164 72.086 52.9915 70.9542C53.0131 70.9384 53.0308 70.9178 53.0433 70.8942C53.0558 70.8706 53.0628 70.8445 53.0637 70.8179V65.1661C53.0634 65.1412 53.0574 65.1167 53.0462 65.0944C53.035 65.0721 53.0189 65.0525 52.9992 65.0371C52.9794 65.0218 52.9564 65.011 52.9318 65.0056C52.9073 65.0002 52.8819 65.0003 52.8574 65.0059C48.0369 66.1472 43.0971 66.7193 38.141 66.7103C29.6118 66.7103 27.3178 62.6981 26.6609 61.0278C26.1329 59.5842 25.7976 58.0784 25.6636 56.5486C25.6622 56.5229 25.667 56.4973 25.6775 56.4738C25.688 56.4502 25.7039 56.4295 25.724 56.4132C25.7441 56.397 25.7678 56.3856 25.7931 56.3801C25.8185 56.3746 25.8448 56.3751 25.8699 56.3816C30.6101 57.5151 35.4693 58.0873 40.3455 58.086C41.5183 58.086 42.6876 58.086 43.8604 58.0553C48.7647 57.919 53.9339 57.6701 58.7591 56.7361C58.8794 56.7123 58.9998 56.6918 59.103 56.6611C66.7139 55.2124 73.9569 50.665 74.6929 39.1501C74.7204 38.6967 74.7892 34.4016 74.7892 33.9312C74.7926 32.3325 75.3085 22.5901 74.7135 16.6043ZM62.9996 45.3371H54.9966V25.9069C54.9966 21.8163 53.277 19.7302 49.7793 19.7302C45.9343 19.7302 44.0083 22.1981 44.0083 27.0727V37.7082H36.0534V27.0727C36.0534 22.1981 34.124 19.7302 30.279 19.7302C26.8019 19.7302 25.0651 21.8163 25.0617 25.9069V45.3371H17.0656V25.3172C17.0656 21.2266 18.1191 17.9769 20.2262 15.568C22.3998 13.1648 25.2509 11.9308 28.7898 11.9308C32.8859 11.9308 35.9812 13.492 38.0447 16.6111L40.036 19.9245L42.0308 16.6111C44.0943 13.492 47.1896 11.9308 51.2788 11.9308C54.8143 11.9308 57.6654 13.1648 59.8459 15.568C61.9529 17.9746 63.0065 21.2243 63.0065 25.3172L62.9996 45.3371Z" fill="currentColor"/></symbol><use xlink:href="#logo-symbol-icon"/></svg>
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
@ -7,5 +7,5 @@
|
||||||
<stop stop-color="#6364FF"/>
|
<stop stop-color="#6364FF"/>
|
||||||
<stop offset="1" stop-color="#563ACC"/>
|
<stop offset="1" stop-color="#563ACC"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs></symbol><use xlink:href="#logo-symbol-wordmark" style="color:#fff"/>
|
</defs></symbol><use xlink:href="#logo-symbol-wordmark"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
@ -102,7 +102,7 @@ export const addReaction = (announcementId, name) => (dispatch, getState) => {
|
||||||
dispatch(addReactionRequest(announcementId, name, alreadyAdded));
|
dispatch(addReactionRequest(announcementId, name, alreadyAdded));
|
||||||
}
|
}
|
||||||
|
|
||||||
api(getState).put(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => {
|
api(getState).put(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => {
|
||||||
dispatch(addReactionSuccess(announcementId, name, alreadyAdded));
|
dispatch(addReactionSuccess(announcementId, name, alreadyAdded));
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
if (!alreadyAdded) {
|
if (!alreadyAdded) {
|
||||||
|
@ -136,7 +136,7 @@ export const addReactionFail = (announcementId, name, error) => ({
|
||||||
export const removeReaction = (announcementId, name) => (dispatch, getState) => {
|
export const removeReaction = (announcementId, name) => (dispatch, getState) => {
|
||||||
dispatch(removeReactionRequest(announcementId, name));
|
dispatch(removeReactionRequest(announcementId, name));
|
||||||
|
|
||||||
api(getState).delete(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => {
|
api(getState).delete(`/api/v1/announcements/${announcementId}/reactions/${encodeURIComponent(name)}`).then(() => {
|
||||||
dispatch(removeReactionSuccess(announcementId, name));
|
dispatch(removeReactionSuccess(announcementId, name));
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
dispatch(removeReactionFail(announcementId, name, err));
|
dispatch(removeReactionFail(announcementId, name, err));
|
||||||
|
|
|
@ -246,12 +246,13 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
|
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
|
||||||
|
|
||||||
if (publicStatus) {
|
if (publicStatus && isRemote) {
|
||||||
if (isRemote) {
|
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
|
||||||
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
|
}
|
||||||
}
|
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
||||||
|
|
||||||
|
if (publicStatus) {
|
||||||
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,9 @@ export const store = configureStore();
|
||||||
const hydrateAction = hydrateStore(initialState);
|
const hydrateAction = hydrateStore(initialState);
|
||||||
|
|
||||||
store.dispatch(hydrateAction);
|
store.dispatch(hydrateAction);
|
||||||
store.dispatch(fetchCustomEmojis());
|
if (initialState.meta.me) {
|
||||||
|
store.dispatch(fetchCustomEmojis());
|
||||||
|
}
|
||||||
|
|
||||||
const createIdentityContext = state => ({
|
const createIdentityContext = state => ({
|
||||||
signedIn: !!state.meta.me,
|
signedIn: !!state.meta.me,
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import Icon from 'mastodon/components/icon';
|
||||||
|
|
||||||
|
export default class FollowRequestNote extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { account, onAuthorize, onReject } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='follow-request-banner'>
|
||||||
|
<div className='follow-request-banner__message'>
|
||||||
|
<FormattedMessage id='account.requested_follow' defaultMessage='{name} has requested to follow you' values={{ name: <bdi><strong dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi> }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='follow-request-banner__action'>
|
||||||
|
<button type='button' className='button button-tertiary button--confirmation' onClick={onAuthorize}>
|
||||||
|
<Icon id='check' fixedWidth />
|
||||||
|
<FormattedMessage id='follow_request.authorize' defaultMessage='Authorize' />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type='button' className='button button-tertiary button--destructive' onClick={onReject}>
|
||||||
|
<Icon id='times' fixedWidth />
|
||||||
|
<FormattedMessage id='follow_request.reject' defaultMessage='Reject' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import ShortNumber from 'mastodon/components/short_number';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
||||||
import AccountNoteContainer from '../containers/account_note_container';
|
import AccountNoteContainer from '../containers/account_note_container';
|
||||||
|
import FollowRequestNoteContainer from '../containers/follow_request_note_container';
|
||||||
import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions';
|
import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
|
@ -311,6 +312,8 @@ class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('account__header', { inactive: !!account.get('moved') })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
<div className={classNames('account__header', { inactive: !!account.get('moved') })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||||
|
{!(suspended || hidden || account.get('moved')) && account.getIn(['relationship', 'requested_by']) && <FollowRequestNoteContainer account={account} />}
|
||||||
|
|
||||||
<div className='account__header__image'>
|
<div className='account__header__image'>
|
||||||
<div className='account__header__info'>
|
<div className='account__header__info'>
|
||||||
{!suspended && info}
|
{!suspended && info}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import FollowRequestNote from '../components/follow_request_note';
|
||||||
|
import { authorizeFollowRequest, rejectFollowRequest } from 'mastodon/actions/accounts';
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, { account }) => ({
|
||||||
|
onAuthorize () {
|
||||||
|
dispatch(authorizeFollowRequest(account.get('id')));
|
||||||
|
},
|
||||||
|
|
||||||
|
onReject () {
|
||||||
|
dispatch(rejectFollowRequest(account.get('id')));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(FollowRequestNote);
|
|
@ -104,6 +104,7 @@ export default class MediaItem extends ImmutablePureComponent {
|
||||||
<video
|
<video
|
||||||
className='media-gallery__item-gifv-thumbnail'
|
className='media-gallery__item-gifv-thumbnail'
|
||||||
aria-label={attachment.get('description')}
|
aria-label={attachment.get('description')}
|
||||||
|
title={attachment.get('description')}
|
||||||
role='application'
|
role='application'
|
||||||
src={attachment.get('url')}
|
src={attachment.get('url')}
|
||||||
onMouseEnter={this.handleMouseEnter}
|
onMouseEnter={this.handleMouseEnter}
|
||||||
|
|
|
@ -16,7 +16,6 @@ import PollFormContainer from '../containers/poll_form_container';
|
||||||
import UploadFormContainer from '../containers/upload_form_container';
|
import UploadFormContainer from '../containers/upload_form_container';
|
||||||
import WarningContainer from '../containers/warning_container';
|
import WarningContainer from '../containers/warning_container';
|
||||||
import LanguageDropdown from '../containers/language_dropdown_container';
|
import LanguageDropdown from '../containers/language_dropdown_container';
|
||||||
import { isMobile } from '../../../is_mobile';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { length } from 'stringz';
|
import { length } from 'stringz';
|
||||||
import { countableText } from '../util/counter';
|
import { countableText } from '../util/counter';
|
||||||
|
@ -62,14 +61,14 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
onChangeSpoilerText: PropTypes.func.isRequired,
|
onChangeSpoilerText: PropTypes.func.isRequired,
|
||||||
onPaste: PropTypes.func.isRequired,
|
onPaste: PropTypes.func.isRequired,
|
||||||
onPickEmoji: PropTypes.func.isRequired,
|
onPickEmoji: PropTypes.func.isRequired,
|
||||||
showSearch: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
anyMedia: PropTypes.bool,
|
anyMedia: PropTypes.bool,
|
||||||
isInReply: PropTypes.bool,
|
isInReply: PropTypes.bool,
|
||||||
singleColumn: PropTypes.bool,
|
singleColumn: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
showSearch: false,
|
autoFocus: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleChange = (e) => {
|
handleChange = (e) => {
|
||||||
|
@ -155,7 +154,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
// - Replying to zero or one users, places the cursor at the end of the textbox.
|
// - Replying to zero or one users, places the cursor at the end of the textbox.
|
||||||
// - Replying to more than one user, selects any usernames past the first;
|
// - Replying to more than one user, selects any usernames past the first;
|
||||||
// this provides a convenient shortcut to drop everyone else from the conversation.
|
// this provides a convenient shortcut to drop everyone else from the conversation.
|
||||||
if (this.props.focusDate !== prevProps.focusDate) {
|
if (this.props.focusDate && this.props.focusDate !== prevProps.focusDate) {
|
||||||
let selectionEnd, selectionStart;
|
let selectionEnd, selectionStart;
|
||||||
|
|
||||||
if (this.props.preselectDate !== prevProps.preselectDate && this.props.isInReply) {
|
if (this.props.preselectDate !== prevProps.preselectDate && this.props.isInReply) {
|
||||||
|
@ -181,7 +180,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
} else if (this.props.spoiler !== prevProps.spoiler) {
|
} else if (this.props.spoiler !== prevProps.spoiler) {
|
||||||
if (this.props.spoiler) {
|
if (this.props.spoiler) {
|
||||||
this.spoilerText.input.focus();
|
this.spoilerText.input.focus();
|
||||||
} else {
|
} else if (prevProps.spoiler) {
|
||||||
this.autosuggestTextarea.textarea.focus();
|
this.autosuggestTextarea.textarea.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -208,7 +207,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, onPaste, showSearch } = this.props;
|
const { intl, onPaste, autoFocus } = this.props;
|
||||||
const disabled = this.props.isSubmitting;
|
const disabled = this.props.isSubmitting;
|
||||||
|
|
||||||
let publishText = '';
|
let publishText = '';
|
||||||
|
@ -258,7 +257,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||||
onSuggestionSelected={this.onSuggestionSelected}
|
onSuggestionSelected={this.onSuggestionSelected}
|
||||||
onPaste={onPaste}
|
onPaste={onPaste}
|
||||||
autoFocus={!showSearch && !isMobile(window.innerWidth)}
|
autoFocus={autoFocus}
|
||||||
>
|
>
|
||||||
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
|
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
|
||||||
|
|
||||||
|
|
|
@ -165,6 +165,7 @@ class PollForm extends ImmutablePureComponent {
|
||||||
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
|
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
|
||||||
<option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
|
<option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
|
||||||
<option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option>
|
<option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option>
|
||||||
|
<option value={43200}>{intl.formatMessage(messages.hours, { number: 12 })}</option>
|
||||||
<option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
|
<option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
|
||||||
<option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
|
<option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
|
||||||
<option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
|
<option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
|
||||||
|
|
|
@ -123,27 +123,24 @@ class Search extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='search'>
|
<div className='search'>
|
||||||
<label>
|
<input
|
||||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.placeholder)}</span>
|
ref={this.setRef}
|
||||||
<input
|
className='search__input'
|
||||||
ref={this.setRef}
|
type='text'
|
||||||
className='search__input'
|
placeholder={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)}
|
||||||
type='text'
|
aria-label={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)}
|
||||||
placeholder={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)}
|
value={value}
|
||||||
value={value}
|
onChange={this.handleChange}
|
||||||
onChange={this.handleChange}
|
onKeyUp={this.handleKeyUp}
|
||||||
onKeyUp={this.handleKeyUp}
|
onFocus={this.handleFocus}
|
||||||
onFocus={this.handleFocus}
|
onBlur={this.handleBlur}
|
||||||
onBlur={this.handleBlur}
|
/>
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
|
<div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
|
||||||
<Icon id='search' className={hasValue ? '' : 'active'} />
|
<Icon id='search' className={hasValue ? '' : 'active'} />
|
||||||
<Icon id='times-circle' className={hasValue ? 'active' : ''} aria-label={intl.formatMessage(messages.placeholder)} />
|
<Icon id='times-circle' className={hasValue ? 'active' : ''} aria-label={intl.formatMessage(messages.placeholder)} />
|
||||||
</div>
|
</div>
|
||||||
|
<Overlay show={expanded && !hasValue} placement='bottom' target={this} container={this}>
|
||||||
<Overlay show={expanded && !hasValue} placement='bottom' target={this}>
|
|
||||||
<SearchPopout />
|
<SearchPopout />
|
||||||
</Overlay>
|
</Overlay>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,7 +24,6 @@ const mapStateToProps = state => ({
|
||||||
isEditing: state.getIn(['compose', 'id']) !== null,
|
isEditing: state.getIn(['compose', 'id']) !== null,
|
||||||
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
|
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
|
||||||
isUploading: state.getIn(['compose', 'is_uploading']),
|
isUploading: state.getIn(['compose', 'is_uploading']),
|
||||||
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
|
||||||
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||||
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
|
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,6 +18,7 @@ import Icon from 'mastodon/components/icon';
|
||||||
import { logOut } from 'mastodon/utils/log_out';
|
import { logOut } from 'mastodon/utils/log_out';
|
||||||
import Column from 'mastodon/components/column';
|
import Column from 'mastodon/components/column';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { isMobile } from '../../is_mobile';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||||
|
@ -115,7 +116,7 @@ class Compose extends React.PureComponent {
|
||||||
<div className='drawer__inner' onFocus={this.onFocus}>
|
<div className='drawer__inner' onFocus={this.onFocus}>
|
||||||
<NavigationContainer onClose={this.onBlur} />
|
<NavigationContainer onClose={this.onBlur} />
|
||||||
|
|
||||||
<ComposeFormContainer />
|
<ComposeFormContainer autoFocus={!isMobile(window.innerWidth)} />
|
||||||
|
|
||||||
<div className='drawer__inner__mastodon'>
|
<div className='drawer__inner__mastodon'>
|
||||||
<img alt='' draggable='false' src={mascot || elephantUIPlane} />
|
<img alt='' draggable='false' src={mascot || elephantUIPlane} />
|
||||||
|
|
|
@ -24,16 +24,6 @@ const mapStateToProps = state => ({
|
||||||
isSearching: state.getIn(['search', 'submitted']) || !showTrends,
|
isSearching: state.getIn(['search', 'submitted']) || !showTrends,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fix strange bug on Safari where <span> (rendered by FormattedMessage) disappears
|
|
||||||
// after clicking around Explore top bar (issue #20885).
|
|
||||||
// Removing width=100% from <a> also fixes it, as well as replacing <span> with <div>
|
|
||||||
// We're choosing to wrap span with div to keep the changes local only to this tool bar.
|
|
||||||
const WrapFormattedMessage = ({ children, ...props }) => <div><FormattedMessage {...props}>{children}</FormattedMessage></div>;
|
|
||||||
WrapFormattedMessage.propTypes = {
|
|
||||||
children: PropTypes.any,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
@injectIntl
|
@injectIntl
|
||||||
class Explore extends React.PureComponent {
|
class Explore extends React.PureComponent {
|
||||||
|
@ -78,12 +68,22 @@ class Explore extends React.PureComponent {
|
||||||
{isSearching ? (
|
{isSearching ? (
|
||||||
<SearchResults />
|
<SearchResults />
|
||||||
) : (
|
) : (
|
||||||
<React.Fragment>
|
<>
|
||||||
<div className='account__section-headline'>
|
<div className='account__section-headline'>
|
||||||
<NavLink exact to='/explore'><WrapFormattedMessage id='explore.trending_statuses' defaultMessage='Posts' /></NavLink>
|
<NavLink exact to='/explore'>
|
||||||
<NavLink exact to='/explore/tags'><WrapFormattedMessage id='explore.trending_tags' defaultMessage='Hashtags' /></NavLink>
|
<FormattedMessage tagName='div' id='explore.trending_statuses' defaultMessage='Posts' />
|
||||||
<NavLink exact to='/explore/links'><WrapFormattedMessage id='explore.trending_links' defaultMessage='News' /></NavLink>
|
</NavLink>
|
||||||
{signedIn && <NavLink exact to='/explore/suggestions'><WrapFormattedMessage id='explore.suggested_follows' defaultMessage='For you' /></NavLink>}
|
<NavLink exact to='/explore/tags'>
|
||||||
|
<FormattedMessage tagName='div' id='explore.trending_tags' defaultMessage='Hashtags' />
|
||||||
|
</NavLink>
|
||||||
|
<NavLink exact to='/explore/links'>
|
||||||
|
<FormattedMessage tagName='div' id='explore.trending_links' defaultMessage='News' />
|
||||||
|
</NavLink>
|
||||||
|
{signedIn && (
|
||||||
|
<NavLink exact to='/explore/suggestions'>
|
||||||
|
<FormattedMessage tagName='div' id='explore.suggested_follows' defaultMessage='For you' />
|
||||||
|
</NavLink>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Switch>
|
<Switch>
|
||||||
|
@ -97,7 +97,7 @@ class Explore extends React.PureComponent {
|
||||||
<title>{intl.formatMessage(messages.title)}</title>
|
<title>{intl.formatMessage(messages.title)}</title>
|
||||||
<meta name='robots' content={isSearching ? 'noindex' : 'all'} />
|
<meta name='robots' content={isSearching ? 'noindex' : 'all'} />
|
||||||
</Helmet>
|
</Helmet>
|
||||||
</React.Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -194,7 +194,7 @@ class HashtagTimeline extends React.PureComponent {
|
||||||
const following = tag.get('following');
|
const following = tag.get('following');
|
||||||
|
|
||||||
followButton = (
|
followButton = (
|
||||||
<button className={classNames('column-header__button')} onClick={this.handleFollow} disabled={!signedIn} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)}>
|
<button className={classNames('column-header__button')} onClick={this.handleFollow} disabled={!signedIn} active={following} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)}>
|
||||||
<Icon id={following ? 'user-times' : 'user-plus'} fixedWidth className='column-header__icon' />
|
<Icon id={following ? 'user-times' : 'user-plus'} fixedWidth className='column-header__icon' />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -97,7 +97,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
|
||||||
if (this.mediaQuery.removeEventListener) {
|
if (this.mediaQuery.removeEventListener) {
|
||||||
this.mediaQuery.removeEventListener('change', this.handleLayoutChange);
|
this.mediaQuery.removeEventListener('change', this.handleLayoutChange);
|
||||||
} else {
|
} else {
|
||||||
this.mediaQuery.removeListener(this.handleLayouteChange);
|
this.mediaQuery.removeListener(this.handleLayoutChange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -291,11 +291,11 @@ class FocalPointModal extends ImmutablePureComponent {
|
||||||
let descriptionLabel = null;
|
let descriptionLabel = null;
|
||||||
|
|
||||||
if (media.get('type') === 'audio') {
|
if (media.get('type') === 'audio') {
|
||||||
descriptionLabel = <FormattedMessage id='upload_form.audio_description' defaultMessage='Describe for people with hearing loss' />;
|
descriptionLabel = <FormattedMessage id='upload_form.audio_description' defaultMessage='Describe for people who are hard of hearing' />;
|
||||||
} else if (media.get('type') === 'video') {
|
} else if (media.get('type') === 'video') {
|
||||||
descriptionLabel = <FormattedMessage id='upload_form.video_description' defaultMessage='Describe for people with hearing loss or visual impairment' />;
|
descriptionLabel = <FormattedMessage id='upload_form.video_description' defaultMessage='Describe for people who are deaf, hard of hearing, blind or have low vision' />;
|
||||||
} else {
|
} else {
|
||||||
descriptionLabel = <FormattedMessage id='upload_form.description' defaultMessage='Describe for the visually impaired' />;
|
descriptionLabel = <FormattedMessage id='upload_form.description' defaultMessage='Describe for people who are blind or have low vision' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ocrMessage = '';
|
let ocrMessage = '';
|
||||||
|
|
|
@ -2014,6 +2014,22 @@
|
||||||
{
|
{
|
||||||
"defaultMessage": "Search results",
|
"defaultMessage": "Search results",
|
||||||
"id": "explore.search_results"
|
"id": "explore.search_results"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Posts",
|
||||||
|
"id": "explore.trending_statuses"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "Hashtags",
|
||||||
|
"id": "explore.trending_tags"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "News",
|
||||||
|
"id": "explore.trending_links"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defaultMessage": "For you",
|
||||||
|
"id": "explore.suggested_follows"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"path": "app/javascript/mastodon/features/explore/index.json"
|
"path": "app/javascript/mastodon/features/explore/index.json"
|
||||||
|
@ -3918,15 +3934,15 @@
|
||||||
"id": "confirmations.discard_edit_media.confirm"
|
"id": "confirmations.discard_edit_media.confirm"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Describe for people with hearing loss",
|
"defaultMessage": "Describe for people who are deaf or hard of hearing",
|
||||||
"id": "upload_form.audio_description"
|
"id": "upload_form.audio_description"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Describe for people with hearing loss or visual impairment",
|
"defaultMessage": "Describe for people who are deaf, hard of hearing, blind or have low vision",
|
||||||
"id": "upload_form.video_description"
|
"id": "upload_form.video_description"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"defaultMessage": "Describe for the visually impaired",
|
"defaultMessage": "Describe for people who are blind or have low vision",
|
||||||
"id": "upload_form.description"
|
"id": "upload_form.description"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -239,7 +239,11 @@
|
||||||
"errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard",
|
"errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard",
|
||||||
"errors.unexpected_crash.report_issue": "Report issue",
|
"errors.unexpected_crash.report_issue": "Report issue",
|
||||||
"explore.search_results": "Search results",
|
"explore.search_results": "Search results",
|
||||||
|
"explore.suggested_follows": "For you",
|
||||||
"explore.title": "Explore",
|
"explore.title": "Explore",
|
||||||
|
"explore.trending_links": "News",
|
||||||
|
"explore.trending_statuses": "Posts",
|
||||||
|
"explore.trending_tags": "Hashtags",
|
||||||
"filter_modal.added.context_mismatch_explanation": "This filter category does not apply to the context in which you have accessed this post. If you want the post to be filtered in this context too, you will have to edit the filter.",
|
"filter_modal.added.context_mismatch_explanation": "This filter category does not apply to the context in which you have accessed this post. If you want the post to be filtered in this context too, you will have to edit the filter.",
|
||||||
"filter_modal.added.context_mismatch_title": "Context mismatch!",
|
"filter_modal.added.context_mismatch_title": "Context mismatch!",
|
||||||
"filter_modal.added.expired_explanation": "This filter category has expired, you will need to change the expiration date for it to apply.",
|
"filter_modal.added.expired_explanation": "This filter category has expired, you will need to change the expiration date for it to apply.",
|
||||||
|
@ -462,6 +466,7 @@
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"regeneration_indicator.label": "Loading…",
|
"regeneration_indicator.label": "Loading…",
|
||||||
"regeneration_indicator.sublabel": "Your home feed is being prepared!",
|
"regeneration_indicator.sublabel": "Your home feed is being prepared!",
|
||||||
|
"relative_format.today": "Today at {time}",
|
||||||
"relative_time.days": "{number}d",
|
"relative_time.days": "{number}d",
|
||||||
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
|
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
|
||||||
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
|
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
|
||||||
|
@ -622,13 +627,13 @@
|
||||||
"upload_button.label": "Add images, a video or an audio file",
|
"upload_button.label": "Add images, a video or an audio file",
|
||||||
"upload_error.limit": "File upload limit exceeded.",
|
"upload_error.limit": "File upload limit exceeded.",
|
||||||
"upload_error.poll": "File upload not allowed with polls.",
|
"upload_error.poll": "File upload not allowed with polls.",
|
||||||
"upload_form.audio_description": "Describe for people with hearing loss",
|
"upload_form.audio_description": "Describe for people who are deaf or hard of hearing",
|
||||||
"upload_form.description": "Describe for the visually impaired",
|
"upload_form.description": "Describe for people who are blind or have low vision",
|
||||||
"upload_form.description_missing": "No description added",
|
"upload_form.description_missing": "No description added",
|
||||||
"upload_form.edit": "Edit",
|
"upload_form.edit": "Edit",
|
||||||
"upload_form.thumbnail": "Change thumbnail",
|
"upload_form.thumbnail": "Change thumbnail",
|
||||||
"upload_form.undo": "Delete",
|
"upload_form.undo": "Delete",
|
||||||
"upload_form.video_description": "Describe for people with hearing loss or visual impairment",
|
"upload_form.video_description": "Describe for people who are deaf, hard of hearing, blind or have low vision",
|
||||||
"upload_modal.analyzing_picture": "Analyzing picture…",
|
"upload_modal.analyzing_picture": "Analyzing picture…",
|
||||||
"upload_modal.apply": "Apply",
|
"upload_modal.apply": "Apply",
|
||||||
"upload_modal.applying": "Applying…",
|
"upload_modal.applying": "Applying…",
|
||||||
|
|
|
@ -431,6 +431,8 @@ export default function compose(state = initialState, action) {
|
||||||
case TIMELINE_DELETE:
|
case TIMELINE_DELETE:
|
||||||
if (action.id === state.get('in_reply_to')) {
|
if (action.id === state.get('in_reply_to')) {
|
||||||
return state.set('in_reply_to', null);
|
return state.set('in_reply_to', null);
|
||||||
|
} else if (action.id === state.get('id')) {
|
||||||
|
return state.set('id', null);
|
||||||
} else {
|
} else {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import {
|
||||||
|
NOTIFICATIONS_UPDATE,
|
||||||
|
} from '../actions/notifications';
|
||||||
import {
|
import {
|
||||||
ACCOUNT_FOLLOW_SUCCESS,
|
ACCOUNT_FOLLOW_SUCCESS,
|
||||||
ACCOUNT_FOLLOW_REQUEST,
|
ACCOUNT_FOLLOW_REQUEST,
|
||||||
|
@ -12,6 +15,8 @@ import {
|
||||||
ACCOUNT_PIN_SUCCESS,
|
ACCOUNT_PIN_SUCCESS,
|
||||||
ACCOUNT_UNPIN_SUCCESS,
|
ACCOUNT_UNPIN_SUCCESS,
|
||||||
RELATIONSHIPS_FETCH_SUCCESS,
|
RELATIONSHIPS_FETCH_SUCCESS,
|
||||||
|
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
|
||||||
|
FOLLOW_REQUEST_REJECT_SUCCESS,
|
||||||
} from '../actions/accounts';
|
} from '../actions/accounts';
|
||||||
import {
|
import {
|
||||||
DOMAIN_BLOCK_SUCCESS,
|
DOMAIN_BLOCK_SUCCESS,
|
||||||
|
@ -44,6 +49,12 @@ const initialState = ImmutableMap();
|
||||||
|
|
||||||
export default function relationships(state = initialState, action) {
|
export default function relationships(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
|
case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
|
||||||
|
return state.setIn([action.id, 'followed_by'], true).setIn([action.id, 'requested_by'], false);
|
||||||
|
case FOLLOW_REQUEST_REJECT_SUCCESS:
|
||||||
|
return state.setIn([action.id, 'followed_by'], false).setIn([action.id, 'requested_by'], false);
|
||||||
|
case NOTIFICATIONS_UPDATE:
|
||||||
|
return action.notification.type === 'follow_request' ? state.setIn([action.notification.account.id, 'requested_by'], true) : state;
|
||||||
case ACCOUNT_FOLLOW_REQUEST:
|
case ACCOUNT_FOLLOW_REQUEST:
|
||||||
return state.getIn([action.id, 'following']) ? state : state.setIn([action.id, action.locked ? 'requested' : 'following'], true);
|
return state.getIn([action.id, 'following']) ? state : state.setIn([action.id, action.locked ? 'requested' : 'following'], true);
|
||||||
case ACCOUNT_FOLLOW_FAIL:
|
case ACCOUNT_FOLLOW_FAIL:
|
||||||
|
|
|
@ -46,6 +46,18 @@ function main() {
|
||||||
minute: 'numeric',
|
minute: 'numeric',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dateFormat = new Intl.DateTimeFormat(locale, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
timeFormat: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeFormat = new Intl.DateTimeFormat(locale, {
|
||||||
|
timeStyle: 'short',
|
||||||
|
hour12: false,
|
||||||
|
});
|
||||||
|
|
||||||
[].forEach.call(document.querySelectorAll('.emojify'), (content) => {
|
[].forEach.call(document.querySelectorAll('.emojify'), (content) => {
|
||||||
content.innerHTML = emojify(content.innerHTML);
|
content.innerHTML = emojify(content.innerHTML);
|
||||||
});
|
});
|
||||||
|
@ -58,6 +70,32 @@ function main() {
|
||||||
content.textContent = formattedDate;
|
content.textContent = formattedDate;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isToday = date => {
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
return date.getDate() === today.getDate() &&
|
||||||
|
date.getMonth() === today.getMonth() &&
|
||||||
|
date.getFullYear() === today.getFullYear();
|
||||||
|
};
|
||||||
|
const todayFormat = new IntlMessageFormat(messages['relative_format.today'] || 'Today at {time}', locale);
|
||||||
|
|
||||||
|
[].forEach.call(document.querySelectorAll('time.relative-formatted'), (content) => {
|
||||||
|
const datetime = new Date(content.getAttribute('datetime'));
|
||||||
|
|
||||||
|
let formattedContent;
|
||||||
|
|
||||||
|
if (isToday(datetime)) {
|
||||||
|
const formattedTime = timeFormat.format(datetime);
|
||||||
|
|
||||||
|
formattedContent = todayFormat.format({ time: formattedTime });
|
||||||
|
} else {
|
||||||
|
formattedContent = dateFormat.format(datetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
content.title = formattedContent;
|
||||||
|
content.textContent = formattedContent;
|
||||||
|
});
|
||||||
|
|
||||||
[].forEach.call(document.querySelectorAll('time.time-ago'), (content) => {
|
[].forEach.call(document.querySelectorAll('time.time-ago'), (content) => {
|
||||||
const datetime = new Date(content.getAttribute('datetime'));
|
const datetime = new Date(content.getAttribute('datetime'));
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
|
@ -1682,7 +1682,7 @@ a.sparkline {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text: &highlight-text-color;
|
color: $highlight-text-color;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
|
@ -166,6 +166,30 @@
|
||||||
&:disabled {
|
&:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.button--confirmation {
|
||||||
|
color: $valid-value-color;
|
||||||
|
border-color: $valid-value-color;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
background: $valid-value-color;
|
||||||
|
color: $primary-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.button--destructive {
|
||||||
|
color: $error-value-color;
|
||||||
|
border-color: $error-value-color;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
background: $error-value-color;
|
||||||
|
color: $primary-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.button--block {
|
&.button--block {
|
||||||
|
@ -2474,8 +2498,7 @@ $ui-header-height: 55px;
|
||||||
height: calc(100% - 10px) !important;
|
height: calc(100% - 10px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.getting-started__wrapper,
|
.getting-started__wrapper {
|
||||||
.search {
|
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2528,7 +2551,7 @@ $ui-header-height: 55px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui__header {
|
.layout-single-column .ui__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: $ui-base-color;
|
background: $ui-base-color;
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
@ -4671,6 +4694,7 @@ a.status-card.compact:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
|
margin-bottom: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6722,7 +6746,8 @@ noscript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.moved-account-banner {
|
.moved-account-banner,
|
||||||
|
.follow-request-banner {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: lighten($ui-base-color, 4%);
|
background: lighten($ui-base-color, 4%);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -6745,6 +6770,7 @@ noscript {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detailed-status__display-name {
|
.detailed-status__display-name {
|
||||||
|
@ -6752,6 +6778,10 @@ noscript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.follow-request-banner .button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.column-inline-form {
|
.column-inline-form {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -7021,7 +7051,6 @@ noscript {
|
||||||
display: block;
|
display: block;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 94px;
|
width: 94px;
|
||||||
margin-left: -2px;
|
|
||||||
|
|
||||||
.account__avatar {
|
.account__avatar {
|
||||||
background: darken($ui-base-color, 8%);
|
background: darken($ui-base-color, 8%);
|
||||||
|
@ -7038,6 +7067,7 @@ noscript {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
margin-left: -2px; // aligns the pfp with content below
|
||||||
|
|
||||||
&__buttons {
|
&__buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -7666,7 +7696,7 @@ noscript {
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-left: 2px solid $highlight-text-color;
|
border-left: 4px solid $highlight-text-color;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
.modal-layout {
|
.modal-layout {
|
||||||
background: $ui-base-color url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-lighter-color)}"/></svg>') repeat-x bottom fixed;
|
background: $ui-base-color url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-lighter-color)}33"/></svg>') repeat-x bottom fixed;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|
|
@ -39,6 +39,8 @@
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
margin: -3px 0 0;
|
margin: -3px 0 0;
|
||||||
|
margin-left: 0.075em;
|
||||||
|
margin-right: 0.075em;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
|
|
@ -34,6 +34,7 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
|
||||||
end
|
end
|
||||||
|
|
||||||
def compatible_version?
|
def compatible_version?
|
||||||
|
return false if running_version.nil?
|
||||||
Gem::Version.new(running_version) >= Gem::Version.new(required_version)
|
Gem::Version.new(running_version) >= Gem::Version.new(required_version)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -30,7 +30,8 @@ class Request
|
||||||
@verb = verb
|
@verb = verb
|
||||||
@url = Addressable::URI.parse(url).normalize
|
@url = Addressable::URI.parse(url).normalize
|
||||||
@http_client = options.delete(:http_client)
|
@http_client = options.delete(:http_client)
|
||||||
@options = options.merge(socket_class: use_proxy? ? ProxySocket : Socket)
|
@allow_local = options.delete(:allow_local)
|
||||||
|
@options = options.merge(socket_class: use_proxy? || @allow_local ? ProxySocket : Socket)
|
||||||
@options = @options.merge(proxy_url) if use_proxy?
|
@options = @options.merge(proxy_url) if use_proxy?
|
||||||
@headers = {}
|
@headers = {}
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ class StatusReachFinder
|
||||||
|
|
||||||
def followers_inboxes
|
def followers_inboxes
|
||||||
if @status.in_reply_to_local_account? && distributable?
|
if @status.in_reply_to_local_account? && distributable?
|
||||||
@status.account.followers.or(@status.thread.account.followers).inboxes
|
@status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).inboxes
|
||||||
elsif @status.direct_visibility? || @status.limited_visibility?
|
elsif @status.direct_visibility? || @status.limited_visibility?
|
||||||
[]
|
[]
|
||||||
else
|
else
|
||||||
|
|
|
@ -27,7 +27,7 @@ class TranslationService::LibreTranslate < TranslationService
|
||||||
|
|
||||||
def request(text, source_language, target_language)
|
def request(text, source_language, target_language)
|
||||||
body = Oj.dump(q: text, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key)
|
body = Oj.dump(q: text, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key)
|
||||||
req = Request.new(:post, "#{@base_url}/translate", body: body)
|
req = Request.new(:post, "#{@base_url}/translate", body: body, allow_local: true)
|
||||||
req.add_headers('Content-Type': 'application/json')
|
req.add_headers('Content-Type': 'application/json')
|
||||||
req
|
req
|
||||||
end
|
end
|
||||||
|
|
|
@ -339,9 +339,15 @@ class Account < ApplicationRecord
|
||||||
|
|
||||||
def save_with_optional_media!
|
def save_with_optional_media!
|
||||||
save!
|
save!
|
||||||
rescue ActiveRecord::RecordInvalid
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
self.avatar = nil
|
errors = e.record.errors.errors
|
||||||
self.header = nil
|
errors.each do |err|
|
||||||
|
if err.attribute == :avatar
|
||||||
|
self.avatar = nil
|
||||||
|
elsif err.attribute == :header
|
||||||
|
self.header = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
save!
|
save!
|
||||||
end
|
end
|
||||||
|
|
|
@ -81,7 +81,7 @@ class AccountFilter
|
||||||
when 'suspended'
|
when 'suspended'
|
||||||
Account.suspended
|
Account.suspended
|
||||||
when 'disabled'
|
when 'disabled'
|
||||||
accounts_with_users.merge(User.disabled)
|
accounts_with_users.merge(User.disabled).without_suspended
|
||||||
when 'silenced'
|
when 'silenced'
|
||||||
Account.silenced
|
Account.silenced
|
||||||
when 'sensitized'
|
when 'sensitized'
|
||||||
|
|
|
@ -44,6 +44,10 @@ module AccountInteractions
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def requested_by_map(target_account_ids, account_id)
|
||||||
|
follow_mapping(FollowRequest.where(account_id: target_account_ids, target_account_id: account_id), :account_id)
|
||||||
|
end
|
||||||
|
|
||||||
def endorsed_map(target_account_ids, account_id)
|
def endorsed_map(target_account_ids, account_id)
|
||||||
follow_mapping(AccountPin.where(account_id: account_id, target_account_id: target_account_ids), :target_account_id)
|
follow_mapping(AccountPin.where(account_id: account_id, target_account_id: target_account_ids), :target_account_id)
|
||||||
end
|
end
|
||||||
|
|
|
@ -210,6 +210,8 @@ class MediaAttachment < ApplicationRecord
|
||||||
|
|
||||||
default_scope { order(id: :asc) }
|
default_scope { order(id: :asc) }
|
||||||
|
|
||||||
|
attr_accessor :skip_download
|
||||||
|
|
||||||
def local?
|
def local?
|
||||||
remote_url.blank?
|
remote_url.blank?
|
||||||
end
|
end
|
||||||
|
|
|
@ -386,6 +386,15 @@ class User < ApplicationRecord
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def revoke_access!
|
||||||
|
Doorkeeper::AccessGrant.by_resource_owner(self).update_all(revoked_at: Time.now.utc)
|
||||||
|
|
||||||
|
Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch|
|
||||||
|
batch.update_all(revoked_at: Time.now.utc)
|
||||||
|
Web::PushSubscription.where(access_token_id: batch).delete_all
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def reset_password!
|
def reset_password!
|
||||||
# First, change password to something random and deactivate all sessions
|
# First, change password to something random and deactivate all sessions
|
||||||
transaction do
|
transaction do
|
||||||
|
@ -394,12 +403,7 @@ class User < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
# Then, remove all authorized applications and connected push subscriptions
|
# Then, remove all authorized applications and connected push subscriptions
|
||||||
Doorkeeper::AccessGrant.by_resource_owner(self).in_batches.update_all(revoked_at: Time.now.utc)
|
revoke_access!
|
||||||
|
|
||||||
Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch|
|
|
||||||
batch.update_all(revoked_at: Time.now.utc)
|
|
||||||
Web::PushSubscription.where(access_token_id: batch).delete_all
|
|
||||||
end
|
|
||||||
|
|
||||||
# Finally, send a reset password prompt to the user
|
# Finally, send a reset password prompt to the user
|
||||||
send_reset_password_instructions
|
send_reset_password_instructions
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
class AccountRelationshipsPresenter
|
class AccountRelationshipsPresenter
|
||||||
attr_reader :following, :followed_by, :blocking, :blocked_by,
|
attr_reader :following, :followed_by, :blocking, :blocked_by,
|
||||||
:muting, :requested, :domain_blocking,
|
:muting, :requested, :requested_by, :domain_blocking,
|
||||||
:endorsed, :account_note
|
:endorsed, :account_note
|
||||||
|
|
||||||
def initialize(account_ids, current_account_id, **options)
|
def initialize(account_ids, current_account_id, **options)
|
||||||
|
@ -15,6 +15,7 @@ class AccountRelationshipsPresenter
|
||||||
@blocked_by = cached[:blocked_by].merge(Account.blocked_by_map(@uncached_account_ids, @current_account_id))
|
@blocked_by = cached[:blocked_by].merge(Account.blocked_by_map(@uncached_account_ids, @current_account_id))
|
||||||
@muting = cached[:muting].merge(Account.muting_map(@uncached_account_ids, @current_account_id))
|
@muting = cached[:muting].merge(Account.muting_map(@uncached_account_ids, @current_account_id))
|
||||||
@requested = cached[:requested].merge(Account.requested_map(@uncached_account_ids, @current_account_id))
|
@requested = cached[:requested].merge(Account.requested_map(@uncached_account_ids, @current_account_id))
|
||||||
|
@requested_by = cached[:requested_by].merge(Account.requested_by_map(@uncached_account_ids, @current_account_id))
|
||||||
@domain_blocking = cached[:domain_blocking].merge(Account.domain_blocking_map(@uncached_account_ids, @current_account_id))
|
@domain_blocking = cached[:domain_blocking].merge(Account.domain_blocking_map(@uncached_account_ids, @current_account_id))
|
||||||
@endorsed = cached[:endorsed].merge(Account.endorsed_map(@uncached_account_ids, @current_account_id))
|
@endorsed = cached[:endorsed].merge(Account.endorsed_map(@uncached_account_ids, @current_account_id))
|
||||||
@account_note = cached[:account_note].merge(Account.account_note_map(@uncached_account_ids, @current_account_id))
|
@account_note = cached[:account_note].merge(Account.account_note_map(@uncached_account_ids, @current_account_id))
|
||||||
|
@ -27,6 +28,7 @@ class AccountRelationshipsPresenter
|
||||||
@blocked_by.merge!(options[:blocked_by_map] || {})
|
@blocked_by.merge!(options[:blocked_by_map] || {})
|
||||||
@muting.merge!(options[:muting_map] || {})
|
@muting.merge!(options[:muting_map] || {})
|
||||||
@requested.merge!(options[:requested_map] || {})
|
@requested.merge!(options[:requested_map] || {})
|
||||||
|
@requested_by.merge!(options[:requested_by_map] || {})
|
||||||
@domain_blocking.merge!(options[:domain_blocking_map] || {})
|
@domain_blocking.merge!(options[:domain_blocking_map] || {})
|
||||||
@endorsed.merge!(options[:endorsed_map] || {})
|
@endorsed.merge!(options[:endorsed_map] || {})
|
||||||
@account_note.merge!(options[:account_note_map] || {})
|
@account_note.merge!(options[:account_note_map] || {})
|
||||||
|
@ -44,6 +46,7 @@ class AccountRelationshipsPresenter
|
||||||
blocked_by: {},
|
blocked_by: {},
|
||||||
muting: {},
|
muting: {},
|
||||||
requested: {},
|
requested: {},
|
||||||
|
requested_by: {},
|
||||||
domain_blocking: {},
|
domain_blocking: {},
|
||||||
endorsed: {},
|
endorsed: {},
|
||||||
account_note: {},
|
account_note: {},
|
||||||
|
@ -73,6 +76,7 @@ class AccountRelationshipsPresenter
|
||||||
blocked_by: { account_id => blocked_by[account_id] },
|
blocked_by: { account_id => blocked_by[account_id] },
|
||||||
muting: { account_id => muting[account_id] },
|
muting: { account_id => muting[account_id] },
|
||||||
requested: { account_id => requested[account_id] },
|
requested: { account_id => requested[account_id] },
|
||||||
|
requested_by: { account_id => requested_by[account_id] },
|
||||||
domain_blocking: { account_id => domain_blocking[account_id] },
|
domain_blocking: { account_id => domain_blocking[account_id] },
|
||||||
endorsed: { account_id => endorsed[account_id] },
|
endorsed: { account_id => endorsed[account_id] },
|
||||||
account_note: { account_id => account_note[account_id] },
|
account_note: { account_id => account_note[account_id] },
|
||||||
|
|
|
@ -30,7 +30,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
streaming_api_base_url: Rails.configuration.x.streaming_api_base_url,
|
streaming_api_base_url: Rails.configuration.x.streaming_api_base_url,
|
||||||
access_token: object.token,
|
access_token: object.token,
|
||||||
locale: I18n.locale,
|
locale: I18n.locale,
|
||||||
domain: instance_presenter.domain,
|
domain: Addressable::IDNA.to_unicode(instance_presenter.domain),
|
||||||
title: instance_presenter.title,
|
title: instance_presenter.title,
|
||||||
admin: object.admin&.id&.to_s,
|
admin: object.admin&.id&.to_s,
|
||||||
search_enabled: Chewy.enabled?,
|
search_enabled: Chewy.enabled?,
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
class REST::RelationshipSerializer < ActiveModel::Serializer
|
class REST::RelationshipSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :following, :showing_reblogs, :notifying, :languages, :followed_by,
|
attributes :id, :following, :showing_reblogs, :notifying, :languages, :followed_by,
|
||||||
:blocking, :blocked_by, :muting, :muting_notifications, :requested,
|
:blocking, :blocked_by, :muting, :muting_notifications,
|
||||||
:domain_blocking, :endorsed, :note
|
:requested, :requested_by, :domain_blocking, :endorsed, :note
|
||||||
|
|
||||||
def id
|
def id
|
||||||
object.id.to_s
|
object.id.to_s
|
||||||
|
@ -54,6 +54,10 @@ class REST::RelationshipSerializer < ActiveModel::Serializer
|
||||||
instance_options[:relationships].requested[object.id] ? true : false
|
instance_options[:relationships].requested[object.id] ? true : false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def requested_by
|
||||||
|
instance_options[:relationships].requested_by[object.id] ? true : false
|
||||||
|
end
|
||||||
|
|
||||||
def domain_blocking
|
def domain_blocking
|
||||||
instance_options[:relationships].domain_blocking[object.id] || false
|
instance_options[:relationships].domain_blocking[object.id] || false
|
||||||
end
|
end
|
||||||
|
|
|
@ -45,6 +45,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||||
create_edits!
|
create_edits!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
download_media_files!
|
||||||
queue_poll_notifications!
|
queue_poll_notifications!
|
||||||
|
|
||||||
next unless significant_changes?
|
next unless significant_changes?
|
||||||
|
@ -66,12 +67,12 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||||
def update_media_attachments!
|
def update_media_attachments!
|
||||||
previous_media_attachments = @status.media_attachments.to_a
|
previous_media_attachments = @status.media_attachments.to_a
|
||||||
previous_media_attachments_ids = @status.ordered_media_attachment_ids || previous_media_attachments.map(&:id)
|
previous_media_attachments_ids = @status.ordered_media_attachment_ids || previous_media_attachments.map(&:id)
|
||||||
next_media_attachments = []
|
@next_media_attachments = []
|
||||||
|
|
||||||
as_array(@json['attachment']).each do |attachment|
|
as_array(@json['attachment']).each do |attachment|
|
||||||
media_attachment_parser = ActivityPub::Parser::MediaAttachmentParser.new(attachment)
|
media_attachment_parser = ActivityPub::Parser::MediaAttachmentParser.new(attachment)
|
||||||
|
|
||||||
next if media_attachment_parser.remote_url.blank? || next_media_attachments.size > 4
|
next if media_attachment_parser.remote_url.blank? || @next_media_attachments.size > 4
|
||||||
|
|
||||||
begin
|
begin
|
||||||
media_attachment = previous_media_attachments.find { |previous_media_attachment| previous_media_attachment.remote_url == media_attachment_parser.remote_url }
|
media_attachment = previous_media_attachments.find { |previous_media_attachment| previous_media_attachment.remote_url == media_attachment_parser.remote_url }
|
||||||
|
@ -87,34 +88,39 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||||
media_attachment.focus = media_attachment_parser.focus
|
media_attachment.focus = media_attachment_parser.focus
|
||||||
media_attachment.thumbnail_remote_url = media_attachment_parser.thumbnail_remote_url
|
media_attachment.thumbnail_remote_url = media_attachment_parser.thumbnail_remote_url
|
||||||
media_attachment.blurhash = media_attachment_parser.blurhash
|
media_attachment.blurhash = media_attachment_parser.blurhash
|
||||||
|
media_attachment.status_id = @status.id
|
||||||
|
media_attachment.skip_download = unsupported_media_type?(media_attachment_parser.file_content_type) || skip_download?
|
||||||
media_attachment.save!
|
media_attachment.save!
|
||||||
|
|
||||||
next_media_attachments << media_attachment
|
@next_media_attachments << media_attachment
|
||||||
|
|
||||||
next if unsupported_media_type?(media_attachment_parser.file_content_type) || skip_download?
|
|
||||||
|
|
||||||
begin
|
|
||||||
media_attachment.download_file! if media_attachment.remote_url_previously_changed?
|
|
||||||
media_attachment.download_thumbnail! if media_attachment.thumbnail_remote_url_previously_changed?
|
|
||||||
media_attachment.save
|
|
||||||
rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError
|
|
||||||
RedownloadMediaWorker.perform_in(rand(30..600).seconds, media_attachment.id)
|
|
||||||
end
|
|
||||||
rescue Addressable::URI::InvalidURIError => e
|
rescue Addressable::URI::InvalidURIError => e
|
||||||
Rails.logger.debug "Invalid URL in attachment: #{e}"
|
Rails.logger.debug "Invalid URL in attachment: #{e}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
added_media_attachments = next_media_attachments - previous_media_attachments
|
added_media_attachments = @next_media_attachments - previous_media_attachments
|
||||||
|
|
||||||
MediaAttachment.where(id: added_media_attachments.map(&:id)).update_all(status_id: @status.id)
|
@status.ordered_media_attachment_ids = @next_media_attachments.map(&:id)
|
||||||
|
|
||||||
@status.ordered_media_attachment_ids = next_media_attachments.map(&:id)
|
|
||||||
@status.media_attachments.reload
|
|
||||||
|
|
||||||
@media_attachments_changed = true if @status.ordered_media_attachment_ids != previous_media_attachments_ids
|
@media_attachments_changed = true if @status.ordered_media_attachment_ids != previous_media_attachments_ids
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def download_media_files!
|
||||||
|
@next_media_attachments.each do |media_attachment|
|
||||||
|
next if media_attachment.skip_download
|
||||||
|
|
||||||
|
media_attachment.download_file! if media_attachment.remote_url_previously_changed?
|
||||||
|
media_attachment.download_thumbnail! if media_attachment.thumbnail_remote_url_previously_changed?
|
||||||
|
media_attachment.save
|
||||||
|
rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError
|
||||||
|
RedownloadMediaWorker.perform_in(rand(30..600).seconds, media_attachment.id)
|
||||||
|
rescue Seahorse::Client::NetworkingError => e
|
||||||
|
Rails.logger.warn "Error storing media attachment: #{e}"
|
||||||
|
end
|
||||||
|
|
||||||
|
@status.media_attachments.reload
|
||||||
|
end
|
||||||
|
|
||||||
def update_poll!(allow_significant_changes: true)
|
def update_poll!(allow_significant_changes: true)
|
||||||
previous_poll = @status.preloadable_poll
|
previous_poll = @status.preloadable_poll
|
||||||
@previous_expires_at = previous_poll&.expires_at
|
@previous_expires_at = previous_poll&.expires_at
|
||||||
|
|
|
@ -37,12 +37,15 @@ class PostStatusService < BaseService
|
||||||
schedule_status!
|
schedule_status!
|
||||||
else
|
else
|
||||||
process_status!
|
process_status!
|
||||||
postprocess_status!
|
|
||||||
bump_potential_friendship!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
redis.setex(idempotency_key, 3_600, @status.id) if idempotency_given?
|
redis.setex(idempotency_key, 3_600, @status.id) if idempotency_given?
|
||||||
|
|
||||||
|
unless scheduled?
|
||||||
|
postprocess_status!
|
||||||
|
bump_potential_friendship!
|
||||||
|
end
|
||||||
|
|
||||||
@status
|
@status
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -75,9 +78,6 @@ class PostStatusService < BaseService
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
@status = @account.statuses.create!(status_attributes)
|
@status = @account.statuses.create!(status_attributes)
|
||||||
end
|
end
|
||||||
|
|
||||||
process_hashtags_service.call(@status)
|
|
||||||
process_mentions_service.call(@status)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def schedule_status!
|
def schedule_status!
|
||||||
|
@ -101,6 +101,8 @@ class PostStatusService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def postprocess_status!
|
def postprocess_status!
|
||||||
|
process_hashtags_service.call(@status)
|
||||||
|
process_mentions_service.call(@status)
|
||||||
Trends.tags.register(@status)
|
Trends.tags.register(@status)
|
||||||
LinkCrawlWorker.perform_async(@status.id)
|
LinkCrawlWorker.perform_async(@status.id)
|
||||||
DistributionWorker.perform_async(@status.id)
|
DistributionWorker.perform_async(@status.id)
|
||||||
|
|
|
@ -76,11 +76,27 @@ class TagSearchService < BaseService
|
||||||
definition = TagsIndex.query(query)
|
definition = TagsIndex.query(query)
|
||||||
definition = definition.filter(filter) if @options[:exclude_unreviewed]
|
definition = definition.filter(filter) if @options[:exclude_unreviewed]
|
||||||
|
|
||||||
definition.limit(@limit).offset(@offset).objects.compact
|
ensure_exact_match(definition.limit(@limit).offset(@offset).objects.compact)
|
||||||
rescue Faraday::ConnectionFailed, Parslet::ParseFailed
|
rescue Faraday::ConnectionFailed, Parslet::ParseFailed
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Since the ElasticSearch Query doesn't guarantee the exact match will be the
|
||||||
|
# first result or that it will even be returned, patch the results accordingly
|
||||||
|
def ensure_exact_match(results)
|
||||||
|
return results unless @offset.nil? || @offset.zero?
|
||||||
|
|
||||||
|
normalized_query = Tag.normalize(@query)
|
||||||
|
exact_match = results.find { |tag| tag.name.downcase == normalized_query }
|
||||||
|
exact_match ||= Tag.find_normalized(normalized_query)
|
||||||
|
unless exact_match.nil?
|
||||||
|
results.delete(exact_match)
|
||||||
|
results = [exact_match] + results
|
||||||
|
end
|
||||||
|
|
||||||
|
results
|
||||||
|
end
|
||||||
|
|
||||||
def from_database
|
def from_database
|
||||||
Tag.search_for(@query, @limit, @offset, @options)
|
Tag.search_for(@query, @limit, @offset, @options)
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
.filter-subset.filter-subset--with-select
|
.filter-subset.filter-subset--with-select
|
||||||
%strong= t('admin.accounts.moderation.title')
|
%strong= t('admin.accounts.moderation.title')
|
||||||
.input.select.optional
|
.input.select.optional
|
||||||
= select_tag :status, options_for_select([[t('admin.accounts.moderation.active'), 'active'], [t('admin.accounts.moderation.silenced'), 'silenced'], [t('admin.accounts.moderation.suspended'), 'suspended'], [safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), 'pending']], params[:status]), prompt: I18n.t('generic.all')
|
= select_tag :status, options_for_select([[t('admin.accounts.moderation.active'), 'active'], [t('admin.accounts.moderation.silenced'), 'silenced'], [t('admin.accounts.moderation.disabled'), 'disabled'], [t('admin.accounts.moderation.suspended'), 'suspended'], [safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), 'pending']], params[:status]), prompt: I18n.t('generic.all')
|
||||||
.filter-subset.filter-subset--with-select
|
.filter-subset.filter-subset--with-select
|
||||||
%strong= t('admin.accounts.role')
|
%strong= t('admin.accounts.role')
|
||||||
.input.select.optional
|
.input.select.optional
|
||||||
|
|
|
@ -195,9 +195,13 @@
|
||||||
- if @account.suspended?
|
- if @account.suspended?
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
%p.muted-hint= @deletion_request.present? ? t('admin.accounts.suspension_reversible_hint_html', date: content_tag(:strong, l(@deletion_request.due_at.to_date))) : t('admin.accounts.suspension_irreversible')
|
- if @account.suspension_origin_remote?
|
||||||
|
%p.muted-hint= @deletion_request.present? ? t('admin.accounts.remote_suspension_reversible_hint_html', date: content_tag(:strong, l(@deletion_request.due_at.to_date))) : t('admin.accounts.remote_suspension_irreversible')
|
||||||
|
- else
|
||||||
|
%p.muted-hint= @deletion_request.present? ? t('admin.accounts.suspension_reversible_hint_html', date: content_tag(:strong, l(@deletion_request.due_at.to_date))) : t('admin.accounts.suspension_irreversible')
|
||||||
|
|
||||||
= link_to t('admin.accounts.undo_suspension'), unsuspend_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsuspend, @account)
|
= link_to t('admin.accounts.undo_suspension'), unsuspend_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsuspend, @account)
|
||||||
|
= link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account) && @account.suspension_origin_remote?
|
||||||
|
|
||||||
- if @deletion_request.present?
|
- if @deletion_request.present?
|
||||||
= link_to t('admin.accounts.delete'), admin_account_path(@account.id), method: :delete, class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure') } if can?(:destroy, @account)
|
= link_to t('admin.accounts.delete'), admin_account_path(@account.id), method: :delete, class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure') } if can?(:destroy, @account)
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
- content_for :page_title do
|
- content_for :page_title do
|
||||||
= t('admin.export_domain_blocks.import.title')
|
= t('admin.export_domain_blocks.import.title')
|
||||||
|
|
||||||
|
- content_for :header_tags do
|
||||||
|
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
|
||||||
|
|
||||||
%p= t('admin.export_domain_blocks.import.description_html')
|
%p= t('admin.export_domain_blocks.import.description_html')
|
||||||
|
|
||||||
- if defined?(@global_private_comment) && @global_private_comment.present?
|
- if defined?(@global_private_comment) && @global_private_comment.present?
|
||||||
|
|
|
@ -4,11 +4,8 @@
|
||||||
.report-notes__item__header
|
.report-notes__item__header
|
||||||
%span.username
|
%span.username
|
||||||
= link_to report_note.account.username, admin_account_path(report_note.account_id)
|
= link_to report_note.account.username, admin_account_path(report_note.account_id)
|
||||||
%time{ datetime: report_note.created_at.iso8601, title: l(report_note.created_at) }
|
%time.relative-formatted{ datetime: report_note.created_at }
|
||||||
- if report_note.created_at.today?
|
= t('admin.report_notes.created_at')
|
||||||
= t('admin.report_notes.today_at', time: l(report_note.created_at, format: :time))
|
|
||||||
- else
|
|
||||||
= l report_note.created_at.to_date
|
|
||||||
|
|
||||||
.report-notes__item__content
|
.report-notes__item__content
|
||||||
= simple_format(h(report_note.content))
|
= simple_format(h(report_note.content))
|
||||||
|
|
|
@ -140,11 +140,8 @@
|
||||||
= link_to @report.account.username, admin_account_path(@report.account_id)
|
= link_to @report.account.username, admin_account_path(@report.account_id)
|
||||||
- else
|
- else
|
||||||
= link_to @report.account.domain, admin_instance_path(@report.account.domain)
|
= link_to @report.account.domain, admin_instance_path(@report.account.domain)
|
||||||
%time{ datetime: @report.created_at.iso8601, title: l(@report.created_at) }
|
%time.relative-formatted{ datetime: @report.created_at.iso8601 }
|
||||||
- if @report.created_at.today?
|
= t('admin.report_notes.created_at')
|
||||||
= t('admin.report_notes.today_at', time: l(@report.created_at, format: :time))
|
|
||||||
- else
|
|
||||||
= l @report.created_at.to_date
|
|
||||||
|
|
||||||
.report-notes__item__content
|
.report-notes__item__content
|
||||||
= simple_format(h(@report.comment))
|
= simple_format(h(@report.comment))
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
|
|
||||||
- unless omniauth_only?
|
- unless omniauth_only?
|
||||||
= simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
|
= simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
|
||||||
|
%h1.title= t('auth.sign_in.title', domain: site_hostname)
|
||||||
|
%p.lead= t('auth.sign_in.preamble_html', domain: site_hostname)
|
||||||
.fields-group
|
.fields-group
|
||||||
- if use_seamless_external_login?
|
- if use_seamless_external_login?
|
||||||
= f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.username_or_email'), input_html: { 'aria-label': t('simple_form.labels.defaults.username_or_email') }, hint: false
|
= f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.username_or_email'), input_html: { 'aria-label': t('simple_form.labels.defaults.username_or_email') }, hint: false
|
||||||
|
|
|
@ -110,11 +110,8 @@
|
||||||
.report-notes__item__header
|
.report-notes__item__header
|
||||||
%span.username
|
%span.username
|
||||||
= link_to @appeal.account.username, can?(:show, @appeal.account) ? admin_account_path(@appeal.account_id) : short_account_url(@appeal.account)
|
= link_to @appeal.account.username, can?(:show, @appeal.account) ? admin_account_path(@appeal.account_id) : short_account_url(@appeal.account)
|
||||||
%time{ datetime: @appeal.created_at.iso8601, title: l(@appeal.created_at) }
|
%time.relative-formatted{ datetime: @appeal.created_at.iso8601 }
|
||||||
- if @appeal.created_at.today?
|
= t('admin.report_notes.created_at')
|
||||||
= t('admin.report_notes.today_at', time: l(@appeal.created_at, format: :time))
|
|
||||||
- else
|
|
||||||
= l @appeal.created_at.to_date
|
|
||||||
|
|
||||||
.report-notes__item__content
|
.report-notes__item__content
|
||||||
= simple_format(h(@appeal.text))
|
= simple_format(h(@appeal.text))
|
||||||
|
|
|
@ -26,6 +26,6 @@
|
||||||
- if featured_tag.last_status_at.nil?
|
- if featured_tag.last_status_at.nil?
|
||||||
= t('accounts.nothing_here')
|
= t('accounts.nothing_here')
|
||||||
- else
|
- else
|
||||||
%time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
|
%time.formatted{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
|
||||||
= table_link_to 'trash', t('filters.index.delete'), settings_featured_tag_path(featured_tag), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
|
= table_link_to 'trash', t('filters.index.delete'), settings_featured_tag_path(featured_tag), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
|
||||||
.trends__item__current= friendly_number_to_human featured_tag.statuses_count
|
.trends__item__current= friendly_number_to_human featured_tag.statuses_count
|
||||||
|
|
|
@ -9,7 +9,7 @@ class Scheduler::SuspendedUserCleanupScheduler
|
||||||
MAX_PULL_SIZE = 50
|
MAX_PULL_SIZE = 50
|
||||||
|
|
||||||
# Since account deletion is very expensive, we want to avoid
|
# Since account deletion is very expensive, we want to avoid
|
||||||
# overloading the server by queing too much at once.
|
# overloading the server by queuing too much at once.
|
||||||
# This job runs approximately once per 2 minutes, so with a
|
# This job runs approximately once per 2 minutes, so with a
|
||||||
# value of `MAX_DELETIONS_PER_JOB` of 10, a server can
|
# value of `MAX_DELETIONS_PER_JOB` of 10, a server can
|
||||||
# handle the deletion of 7200 accounts per day, provided it
|
# handle the deletion of 7200 accounts per day, provided it
|
||||||
|
|
|
@ -93,6 +93,7 @@ module Mastodon
|
||||||
:fa,
|
:fa,
|
||||||
:fi,
|
:fi,
|
||||||
:fr,
|
:fr,
|
||||||
|
:fy,
|
||||||
:ga,
|
:ga,
|
||||||
:gd,
|
:gd,
|
||||||
:gl,
|
:gl,
|
||||||
|
|
|
@ -159,7 +159,7 @@ Devise.setup do |config|
|
||||||
# config.request_keys = []
|
# config.request_keys = []
|
||||||
|
|
||||||
# Configure which authentication keys should be case-insensitive.
|
# Configure which authentication keys should be case-insensitive.
|
||||||
# These keys will be downcased upon creating or modifying a user and when used
|
# These keys will be lowercased upon creating or modifying a user and when used
|
||||||
# to authenticate or find a user. Default is :email.
|
# to authenticate or find a user. Default is :email.
|
||||||
config.case_insensitive_keys = [:email]
|
config.case_insensitive_keys = [:email]
|
||||||
|
|
||||||
|
|
|
@ -149,9 +149,19 @@ en:
|
||||||
scopes:
|
scopes:
|
||||||
admin:read: read all data on the server
|
admin:read: read all data on the server
|
||||||
admin:read:accounts: read sensitive information of all accounts
|
admin:read:accounts: read sensitive information of all accounts
|
||||||
|
admin:read:canonical_email_blocks: read sensitive information of all canonical email blocks
|
||||||
|
admin:read:domain_allows: read sensitive information of all domain allows
|
||||||
|
admin:read:domain_blocks: read sensitive information of all domain blocks
|
||||||
|
admin:read:email_domain_blocks: read sensitive information of all email domain blocks
|
||||||
|
admin:read:ip_blocks: read sensitive information of all IP blocks
|
||||||
admin:read:reports: read sensitive information of all reports and reported accounts
|
admin:read:reports: read sensitive information of all reports and reported accounts
|
||||||
admin:write: modify all data on the server
|
admin:write: modify all data on the server
|
||||||
admin:write:accounts: perform moderation actions on accounts
|
admin:write:accounts: perform moderation actions on accounts
|
||||||
|
admin:write:canonical_email_blocks: perform moderation actions on canonical email blocks
|
||||||
|
admin:write:domain_allows: perform moderation actions on domain allows
|
||||||
|
admin:write:domain_blocks: perform moderation actions on domain blocks
|
||||||
|
admin:write:email_domain_blocks: perform moderation actions on email domain blocks
|
||||||
|
admin:write:ip_blocks: perform moderation actions on IP blocks
|
||||||
admin:write:reports: perform moderation actions on reports
|
admin:write:reports: perform moderation actions on reports
|
||||||
crypto: use end-to-end encryption
|
crypto: use end-to-end encryption
|
||||||
follow: modify account relationships
|
follow: modify account relationships
|
||||||
|
|
|
@ -116,6 +116,8 @@ en:
|
||||||
redownloaded_msg: Successfully refreshed %{username}'s profile from origin
|
redownloaded_msg: Successfully refreshed %{username}'s profile from origin
|
||||||
reject: Reject
|
reject: Reject
|
||||||
rejected_msg: Successfully rejected %{username}'s sign-up application
|
rejected_msg: Successfully rejected %{username}'s sign-up application
|
||||||
|
remote_suspension_irreversible: The data of this account has been irreversibly deleted.
|
||||||
|
remote_suspension_reversible_hint_html: The account has been suspended on their server, and the data will be fully removed on %{date}. Until then, the remote server can restore this account without any ill effects. If you wish to remove all of the account's data immediately, you can do so below.
|
||||||
remove_avatar: Remove avatar
|
remove_avatar: Remove avatar
|
||||||
remove_header: Remove header
|
remove_header: Remove header
|
||||||
removed_avatar_msg: Successfully removed %{username}'s avatar image
|
removed_avatar_msg: Successfully removed %{username}'s avatar image
|
||||||
|
@ -555,13 +557,12 @@ en:
|
||||||
pending: Waiting for relay's approval
|
pending: Waiting for relay's approval
|
||||||
save_and_enable: Save and enable
|
save_and_enable: Save and enable
|
||||||
setup: Setup a relay connection
|
setup: Setup a relay connection
|
||||||
signatures_not_enabled: Relays will not work correctly while secure mode or limited federation mode is enabled
|
signatures_not_enabled: Relays may not work correctly while secure mode or limited federation mode is enabled
|
||||||
status: Status
|
status: Status
|
||||||
title: Relays
|
title: Relays
|
||||||
report_notes:
|
report_notes:
|
||||||
created_msg: Report note successfully created!
|
created_msg: Report note successfully created!
|
||||||
destroyed_msg: Report note successfully deleted!
|
destroyed_msg: Report note successfully deleted!
|
||||||
today_at: Today at %{time}
|
|
||||||
reports:
|
reports:
|
||||||
account:
|
account:
|
||||||
notes:
|
notes:
|
||||||
|
@ -974,6 +975,9 @@ en:
|
||||||
email_below_hint_html: If the below e-mail address is incorrect, you can change it here and receive a new confirmation e-mail.
|
email_below_hint_html: If the below e-mail address is incorrect, you can change it here and receive a new confirmation e-mail.
|
||||||
email_settings_hint_html: The confirmation e-mail was sent to %{email}. If that e-mail address is not correct, you can change it in account settings.
|
email_settings_hint_html: The confirmation e-mail was sent to %{email}. If that e-mail address is not correct, you can change it in account settings.
|
||||||
title: Setup
|
title: Setup
|
||||||
|
sign_in:
|
||||||
|
preamble_html: Sign in with your <strong>%{domain}</strong> credentials. If your account is hosted on a different server, you will not be able to log in here.
|
||||||
|
title: Sign in to %{domain}
|
||||||
sign_up:
|
sign_up:
|
||||||
preamble: With an account on this Mastodon server, you'll be able to follow any other person on the network, regardless of where their account is hosted.
|
preamble: With an account on this Mastodon server, you'll be able to follow any other person on the network, regardless of where their account is hosted.
|
||||||
title: Let's get you set up on %{domain}.
|
title: Let's get you set up on %{domain}.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
:concurrency: 5
|
:concurrency: <%= ENV.fetch('SIDEKIQ_CONCURRENCY', 5) %>
|
||||||
:queues:
|
:queues:
|
||||||
- [default, 8]
|
- [default, 8]
|
||||||
- [push, 6]
|
- [push, 6]
|
||||||
|
|
|
@ -34,6 +34,12 @@ module.exports = merge(sharedConfig, {
|
||||||
cache: true,
|
cache: true,
|
||||||
test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/,
|
test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/,
|
||||||
}),
|
}),
|
||||||
|
new CompressionPlugin({
|
||||||
|
filename: '[path][base].br[query]',
|
||||||
|
algorithm: 'brotliCompress',
|
||||||
|
cache: true,
|
||||||
|
test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/,
|
||||||
|
}),
|
||||||
new BundleAnalyzerPlugin({ // generates report.html
|
new BundleAnalyzerPlugin({ // generates report.html
|
||||||
analyzerMode: 'static',
|
analyzerMode: 'static',
|
||||||
openAnalyzer: false,
|
openAnalyzer: false,
|
||||||
|
|
|
@ -200,21 +200,44 @@ module Mastodon
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'delete USERNAME', 'Delete a user'
|
option :email
|
||||||
|
option :dry_run, type: :boolean
|
||||||
|
desc 'delete [USERNAME]', 'Delete a user'
|
||||||
long_desc <<-LONG_DESC
|
long_desc <<-LONG_DESC
|
||||||
Remove a user account with a given USERNAME.
|
Remove a user account with a given USERNAME.
|
||||||
LONG_DESC
|
|
||||||
def delete(username)
|
|
||||||
account = Account.find_local(username)
|
|
||||||
|
|
||||||
if account.nil?
|
With the --email option, the user is selected based on email
|
||||||
say('No user with such username', :red)
|
rather than username.
|
||||||
|
LONG_DESC
|
||||||
|
def delete(username = nil)
|
||||||
|
if username.present? && options[:email].present?
|
||||||
|
say('Use username or --email, not both', :red)
|
||||||
|
exit(1)
|
||||||
|
elsif username.blank? && options[:email].blank?
|
||||||
|
say('No username provided', :red)
|
||||||
exit(1)
|
exit(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
say("Deleting user with #{account.statuses_count} statuses, this might take a while...")
|
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
|
||||||
DeleteAccountService.new.call(account, reserve_email: false)
|
account = nil
|
||||||
say('OK', :green)
|
|
||||||
|
if username.present?
|
||||||
|
account = Account.find_local(username)
|
||||||
|
if account.nil?
|
||||||
|
say('No user with such username', :red)
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
account = Account.left_joins(:user).find_by(user: { email: options[:email] })
|
||||||
|
if account.nil?
|
||||||
|
say('No user with such email', :red)
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
say("Deleting user with #{account.statuses_count} statuses, this might take a while...#{dry_run}")
|
||||||
|
DeleteAccountService.new.call(account, reserve_email: false) unless options[:dry_run]
|
||||||
|
say("OK#{dry_run}", :green)
|
||||||
end
|
end
|
||||||
|
|
||||||
option :force, type: :boolean, aliases: [:f], description: 'Override public key check'
|
option :force, type: :boolean, aliases: [:f], description: 'Override public key check'
|
||||||
|
|
|
@ -14,35 +14,78 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
option :days, type: :numeric, default: 7, aliases: [:d]
|
option :days, type: :numeric, default: 7, aliases: [:d]
|
||||||
|
option :prune_profiles, type: :boolean, default: false
|
||||||
|
option :remove_headers, type: :boolean, default: false
|
||||||
|
option :include_follows, type: :boolean, default: false
|
||||||
option :concurrency, type: :numeric, default: 5, aliases: [:c]
|
option :concurrency, type: :numeric, default: 5, aliases: [:c]
|
||||||
option :verbose, type: :boolean, default: false, aliases: [:v]
|
|
||||||
option :dry_run, type: :boolean, default: false
|
option :dry_run, type: :boolean, default: false
|
||||||
desc 'remove', 'Remove remote media files'
|
desc 'remove', 'Remove remote media files, headers or avatars'
|
||||||
long_desc <<-DESC
|
long_desc <<-DESC
|
||||||
Removes locally cached copies of media attachments from other servers.
|
Removes locally cached copies of media attachments (and optionally profile
|
||||||
|
headers and avatars) from other servers. By default, only media attachements
|
||||||
|
are removed.
|
||||||
The --days option specifies how old media attachments have to be before
|
The --days option specifies how old media attachments have to be before
|
||||||
they are removed. It defaults to 7 days.
|
they are removed. In case of avatars and headers, it specifies how old
|
||||||
|
the last webfinger request and update to the user has to be before they
|
||||||
|
are pruned. It defaults to 7 days.
|
||||||
|
If --prune-profiles is specified, only avatars and headers are removed.
|
||||||
|
If --remove-headers is specified, only headers are removed.
|
||||||
|
If --include-follows is specified along with --prune-profiles or
|
||||||
|
--remove-headers, all non-local profiles will be pruned irrespective of
|
||||||
|
follow status. By default, only accounts that are not followed by or
|
||||||
|
following anyone locally are pruned.
|
||||||
DESC
|
DESC
|
||||||
|
# rubocop:disable Metrics/PerceivedComplexity
|
||||||
def remove
|
def remove
|
||||||
time_ago = options[:days].days.ago
|
if options[:prune_profiles] && options[:remove_headers]
|
||||||
dry_run = options[:dry_run] ? '(DRY RUN)' : ''
|
say('--prune-profiles and --remove-headers should not be specified simultaneously', :red, true)
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
|
if options[:include_follows] && !(options[:prune_profiles] || options[:remove_headers])
|
||||||
|
say('--include-follows can only be used with --prune-profiles or --remove-headers', :red, true)
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
|
time_ago = options[:days].days.ago
|
||||||
|
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
|
||||||
|
|
||||||
processed, aggregate = parallelize_with_progress(MediaAttachment.cached.where.not(remote_url: '').where('created_at < ?', time_ago)) do |media_attachment|
|
if options[:prune_profiles] || options[:remove_headers]
|
||||||
next if media_attachment.file.blank?
|
processed, aggregate = parallelize_with_progress(Account.remote.where({ last_webfingered_at: ..time_ago, updated_at: ..time_ago })) do |account|
|
||||||
|
next if !options[:include_follows] && Follow.where(account: account).or(Follow.where(target_account: account)).exists?
|
||||||
|
next if account.avatar.blank? && account.header.blank?
|
||||||
|
next if options[:remove_headers] && account.header.blank?
|
||||||
|
|
||||||
size = (media_attachment.file_file_size || 0) + (media_attachment.thumbnail_file_size || 0)
|
size = (account.header_file_size || 0)
|
||||||
|
size += (account.avatar_file_size || 0) if options[:prune_profiles]
|
||||||
|
|
||||||
unless options[:dry_run]
|
unless options[:dry_run]
|
||||||
media_attachment.file.destroy
|
account.header.destroy
|
||||||
media_attachment.thumbnail.destroy
|
account.avatar.destroy if options[:prune_profiles]
|
||||||
media_attachment.save
|
account.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
size
|
||||||
end
|
end
|
||||||
|
|
||||||
size
|
say("Visited #{processed} accounts and removed profile media totaling #{number_to_human_size(aggregate)}#{dry_run}", :green, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
say("Removed #{processed} media attachments (approx. #{number_to_human_size(aggregate)}) #{dry_run}", :green, true)
|
unless options[:prune_profiles] || options[:remove_headers]
|
||||||
|
processed, aggregate = parallelize_with_progress(MediaAttachment.cached.where.not(remote_url: '').where(created_at: ..time_ago)) do |media_attachment|
|
||||||
|
next if media_attachment.file.blank?
|
||||||
|
|
||||||
|
size = (media_attachment.file_file_size || 0) + (media_attachment.thumbnail_file_size || 0)
|
||||||
|
|
||||||
|
unless options[:dry_run]
|
||||||
|
media_attachment.file.destroy
|
||||||
|
media_attachment.thumbnail.destroy
|
||||||
|
media_attachment.save
|
||||||
|
end
|
||||||
|
|
||||||
|
size
|
||||||
|
end
|
||||||
|
|
||||||
|
say("Removed #{processed} media attachments (approx. #{number_to_human_size(aggregate)})#{dry_run}", :green, true)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
option :start_after
|
option :start_after
|
||||||
|
@ -183,6 +226,7 @@ module Mastodon
|
||||||
|
|
||||||
say("Removed #{removed} orphans (approx. #{number_to_human_size(reclaimed_bytes)})#{dry_run}", :green, true)
|
say("Removed #{removed} orphans (approx. #{number_to_human_size(reclaimed_bytes)})#{dry_run}", :green, true)
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics/PerceivedComplexity
|
||||||
|
|
||||||
option :account, type: :string
|
option :account, type: :string
|
||||||
option :domain, type: :string
|
option :domain, type: :string
|
||||||
|
@ -269,7 +313,7 @@ module Mastodon
|
||||||
def lookup(url)
|
def lookup(url)
|
||||||
path = Addressable::URI.parse(url).path
|
path = Addressable::URI.parse(url).path
|
||||||
|
|
||||||
path_segments = path.split('/')[2..-1]
|
path_segments = path.split('/')[2..]
|
||||||
path_segments.delete('cache')
|
path_segments.delete('cache')
|
||||||
|
|
||||||
unless [7, 10].include?(path_segments.size)
|
unless [7, 10].include?(path_segments.size)
|
||||||
|
|
|
@ -194,7 +194,7 @@ namespace :mastodon do
|
||||||
|
|
||||||
env['S3_HOSTNAME'] = prompt.ask('S3 hostname:') do |q|
|
env['S3_HOSTNAME'] = prompt.ask('S3 hostname:') do |q|
|
||||||
q.required true
|
q.required true
|
||||||
q.default 's3-us-east-1.amazonaws.com'
|
q.default 's3.us-east-1.amazonaws.com'
|
||||||
q.modify :strip
|
q.modify :strip
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "@mastodon/mastodon",
|
"name": "@mastodon/mastodon",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=16"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postversion": "git push --tags",
|
"postversion": "git push --tags",
|
||||||
|
|
|
@ -1,24 +1,28 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
(function() {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {() => void} loaded
|
* @param {() => void} loaded
|
||||||
*/
|
*/
|
||||||
var ready = function(loaded) {
|
var ready = function (loaded) {
|
||||||
if (['interactive', 'complete'].indexOf(document.readyState) !== -1) {
|
if (document.readyState === 'complete') {
|
||||||
loaded();
|
loaded();
|
||||||
} else {
|
} else {
|
||||||
document.addEventListener('DOMContentLoaded', loaded);
|
document.addEventListener('readystatechange', function () {
|
||||||
|
if (document.readyState === 'complete') {
|
||||||
|
loaded();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ready(function() {
|
ready(function () {
|
||||||
/** @type {Map<number, HTMLIFrameElement>} */
|
/** @type {Map<number, HTMLIFrameElement>} */
|
||||||
var iframes = new Map();
|
var iframes = new Map();
|
||||||
|
|
||||||
window.addEventListener('message', function(e) {
|
window.addEventListener('message', function (e) {
|
||||||
var data = e.data || {};
|
var data = e.data || {};
|
||||||
|
|
||||||
if (typeof data !== 'object' || data.type !== 'setHeight' || !iframes.has(data.id)) {
|
if (typeof data !== 'object' || data.type !== 'setHeight' || !iframes.has(data.id)) {
|
||||||
|
@ -34,7 +38,7 @@
|
||||||
iframe.height = data.height;
|
iframe.height = data.height;
|
||||||
});
|
});
|
||||||
|
|
||||||
[].forEach.call(document.querySelectorAll('iframe.mastodon-embed'), function(iframe) {
|
[].forEach.call(document.querySelectorAll('iframe.mastodon-embed'), function (iframe) {
|
||||||
// select unique id for each iframe
|
// select unique id for each iframe
|
||||||
var id = 0, failCount = 0, idBuffer = new Uint32Array(1);
|
var id = 0, failCount = 0, idBuffer = new Uint32Array(1);
|
||||||
while (id === 0 || iframes.has(id)) {
|
while (id === 0 || iframes.has(id)) {
|
||||||
|
@ -49,10 +53,10 @@
|
||||||
|
|
||||||
iframes.set(id, iframe);
|
iframes.set(id, iframe);
|
||||||
|
|
||||||
iframe.scrolling = 'no';
|
iframe.scrolling = 'no';
|
||||||
iframe.style.overflow = 'hidden';
|
iframe.style.overflow = 'hidden';
|
||||||
|
|
||||||
iframe.onload = function() {
|
iframe.onload = function () {
|
||||||
iframe.contentWindow.postMessage({
|
iframe.contentWindow.postMessage({
|
||||||
type: 'setHeight',
|
type: 'setHeight',
|
||||||
id: id,
|
id: id,
|
||||||
|
|
|
@ -70,6 +70,53 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'PUT #update' do
|
||||||
|
let!(:remote_account) { Fabricate(:account, domain: 'example.com') }
|
||||||
|
let(:domain_block) { Fabricate(:domain_block, domain: 'example.com', severity: original_severity) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
BlockDomainService.new.call(domain_block)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:subject) do
|
||||||
|
post :update, params: { id: domain_block.id, domain_block: { domain: 'example.com', severity: new_severity } }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'downgrading a domain suspension to silence' do
|
||||||
|
let(:original_severity) { 'suspend' }
|
||||||
|
let(:new_severity) { 'silence' }
|
||||||
|
|
||||||
|
it 'changes the block severity' do
|
||||||
|
expect { subject }.to change { domain_block.reload.severity }.from('suspend').to('silence')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'undoes individual suspensions' do
|
||||||
|
expect { subject }.to change { remote_account.reload.suspended? }.from(true).to(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'performs individual silences' do
|
||||||
|
expect { subject }.to change { remote_account.reload.silenced? }.from(false).to(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'upgrading a domain silence to suspend' do
|
||||||
|
let(:original_severity) { 'silence' }
|
||||||
|
let(:new_severity) { 'suspend' }
|
||||||
|
|
||||||
|
it 'changes the block severity' do
|
||||||
|
expect { subject }.to change { domain_block.reload.severity }.from('silence').to('suspend')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'undoes individual silences' do
|
||||||
|
expect { subject }.to change { remote_account.reload.silenced? }.from(true).to(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'performs individual suspends' do
|
||||||
|
expect { subject }.to change { remote_account.reload.suspended? }.from(false).to(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'DELETE #destroy' do
|
describe 'DELETE #destroy' do
|
||||||
it 'unblocks the domain' do
|
it 'unblocks the domain' do
|
||||||
service = double(call: true)
|
service = double(call: true)
|
||||||
|
|
|
@ -71,6 +71,53 @@ RSpec.describe Api::V1::Admin::DomainBlocksController, type: :controller do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'PUT #update' do
|
||||||
|
let!(:remote_account) { Fabricate(:account, domain: 'example.com') }
|
||||||
|
let(:domain_block) { Fabricate(:domain_block, domain: 'example.com', severity: original_severity) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
BlockDomainService.new.call(domain_block)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:subject) do
|
||||||
|
post :update, params: { id: domain_block.id, domain: 'example.com', severity: new_severity }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'downgrading a domain suspension to silence' do
|
||||||
|
let(:original_severity) { 'suspend' }
|
||||||
|
let(:new_severity) { 'silence' }
|
||||||
|
|
||||||
|
it 'changes the block severity' do
|
||||||
|
expect { subject }.to change { domain_block.reload.severity }.from('suspend').to('silence')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'undoes individual suspensions' do
|
||||||
|
expect { subject }.to change { remote_account.reload.suspended? }.from(true).to(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'performs individual silences' do
|
||||||
|
expect { subject }.to change { remote_account.reload.silenced? }.from(false).to(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'upgrading a domain silence to suspend' do
|
||||||
|
let(:original_severity) { 'silence' }
|
||||||
|
let(:new_severity) { 'suspend' }
|
||||||
|
|
||||||
|
it 'changes the block severity' do
|
||||||
|
expect { subject }.to change { domain_block.reload.severity }.from('silence').to('suspend')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'undoes individual silences' do
|
||||||
|
expect { subject }.to change { remote_account.reload.silenced? }.from(true).to(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'performs individual suspends' do
|
||||||
|
expect { subject }.to change { remote_account.reload.suspended? }.from(false).to(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'DELETE #destroy' do
|
describe 'DELETE #destroy' do
|
||||||
let!(:block) { Fabricate(:domain_block) }
|
let!(:block) { Fabricate(:domain_block) }
|
||||||
|
|
||||||
|
|
|
@ -35,4 +35,65 @@ describe Auth::PasswordsController, type: :controller do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'POST #update' do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
@password = 'reset0password'
|
||||||
|
request.env['devise.mapping'] = Devise.mappings[:user]
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with valid reset_password_token' do
|
||||||
|
let!(:session_activation) { Fabricate(:session_activation, user: user) }
|
||||||
|
let!(:access_token) { Fabricate(:access_token, resource_owner_id: user.id) }
|
||||||
|
let!(:web_push_subscription) { Fabricate(:web_push_subscription, access_token: access_token) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
@token = user.send_reset_password_instructions
|
||||||
|
|
||||||
|
post :update, params: { user: { password: @password, password_confirmation: @password, reset_password_token: @token } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'redirect to sign in' do
|
||||||
|
expect(response).to redirect_to '/auth/sign_in'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'changes password' do
|
||||||
|
this_user = User.find(user.id)
|
||||||
|
|
||||||
|
expect(this_user).to_not be_nil
|
||||||
|
expect(this_user.valid_password?(@password)).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deactivates all sessions' do
|
||||||
|
expect(user.session_activations.count).to eq 0
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'revokes all access tokens' do
|
||||||
|
expect(Doorkeeper::AccessToken.active_for(user).count).to eq 0
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes push subscriptions' do
|
||||||
|
expect(Web::PushSubscription.where(user: user).or(Web::PushSubscription.where(access_token: access_token)).count).to eq 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid reset_password_token' do
|
||||||
|
before do
|
||||||
|
post :update, params: { user: { password: @password, password_confirmation: @password, reset_password_token: 'some_invalid_value' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders reset password' do
|
||||||
|
expect(response).to render_template(:new)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'retains password' do
|
||||||
|
this_user = User.find(user.id)
|
||||||
|
|
||||||
|
expect(this_user).to_not be_nil
|
||||||
|
expect(this_user.external_or_valid_password?(user.password)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,6 +27,8 @@ describe WellKnown::NodeInfoController, type: :controller do
|
||||||
|
|
||||||
json = body_as_json
|
json = body_as_json
|
||||||
|
|
||||||
|
expect({ "foo" => 0 }).not_to match_json_schema("nodeinfo_2.0")
|
||||||
|
expect(json).to match_json_schema("nodeinfo_2.0")
|
||||||
expect(json[:version]).to eq '2.0'
|
expect(json[:version]).to eq '2.0'
|
||||||
expect(json[:usage]).to be_a Hash
|
expect(json[:usage]).to be_a Hash
|
||||||
expect(json[:software]).to be_a Hash
|
expect(json[:software]).to be_a Hash
|
||||||
|
|
|
@ -113,7 +113,7 @@ describe ApplicationHelper do
|
||||||
Setting.site_title = site_title
|
Setting.site_title = site_title
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns site title on production enviroment' do
|
it 'returns site title on production environment' do
|
||||||
Setting.site_title = 'site title'
|
Setting.site_title = 'site title'
|
||||||
expect(Rails.env).to receive(:production?).and_return(true)
|
expect(Rails.env).to receive(:production?).and_return(true)
|
||||||
expect(helper.title).to eq 'site title'
|
expect(helper.title).to eq 'site title'
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe FormattingHelper, type: :helper do
|
||||||
|
include Devise::Test::ControllerHelpers
|
||||||
|
|
||||||
|
describe '#rss_status_content_format' do
|
||||||
|
let(:status) { Fabricate(:status, text: 'Hello world<>', spoiler_text: 'This is a spoiler<>', poll: Fabricate(:poll, options: %w(Yes<> No))) }
|
||||||
|
let(:html) { helper.rss_status_content_format(status) }
|
||||||
|
|
||||||
|
it 'renders the spoiler text' do
|
||||||
|
expect(html).to include('<p>This is a spoiler<></p><hr>')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the status text' do
|
||||||
|
expect(html).to include('<p>Hello world<></p>')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the poll' do
|
||||||
|
expect(html).to include('<radio disabled="disabled">Yes<></radio><br>')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -160,7 +160,7 @@ RSpec.describe Account, type: :model do
|
||||||
expect(account.avatar_remote_url).to eq 'https://remote.test/invalid_avatar'
|
expect(account.avatar_remote_url).to eq 'https://remote.test/invalid_avatar'
|
||||||
expect(account.header_remote_url).to eq expectation.header_remote_url
|
expect(account.header_remote_url).to eq expectation.header_remote_url
|
||||||
expect(account.avatar_file_name).to eq nil
|
expect(account.avatar_file_name).to eq nil
|
||||||
expect(account.header_file_name).to eq nil
|
expect(account.header_file_name).to eq expectation.header_file_name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -658,6 +658,12 @@ RSpec.describe Account, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.requested_by_map' do
|
||||||
|
it 'returns an hash' do
|
||||||
|
expect(Account.requested_by_map([], 1)).to be_a Hash
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'MENTION_RE' do
|
describe 'MENTION_RE' do
|
||||||
subject { Account::MENTION_RE }
|
subject { Account::MENTION_RE }
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue