Merge remote-tracking branch 'tootsuite/main' into custom/quote
# Conflicts: # app/controllers/api/v1/statuses_controller.rb # app/javascript/mastodon/actions/compose.js # app/javascript/mastodon/actions/notifications.js # app/javascript/mastodon/components/status_action_bar.js # app/javascript/mastodon/containers/status_container.js # app/javascript/mastodon/features/compose/containers/reply_indicator_container.js # app/javascript/mastodon/reducers/compose.js
This commit is contained in:
commit
c15001381b
@ -32,7 +32,7 @@ commands:
|
|||||||
name: Install system dependencies
|
name: Install system dependencies
|
||||||
command: |
|
command: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
|
sudo apt-get install -y libicu-dev libidn11-dev
|
||||||
install-ruby-dependencies:
|
install-ruby-dependencies:
|
||||||
parameters:
|
parameters:
|
||||||
ruby-version:
|
ruby-version:
|
||||||
|
10
.github/workflows/build-image.yml
vendored
10
.github/workflows/build-image.yml
vendored
@ -6,16 +6,22 @@ on:
|
|||||||
- "main"
|
- "main"
|
||||||
tags:
|
tags:
|
||||||
- "*"
|
- "*"
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- .github/workflows/build-image.yml
|
||||||
|
- Dockerfile
|
||||||
jobs:
|
jobs:
|
||||||
build-image:
|
build-image:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
- uses: docker/setup-qemu-action@v1
|
||||||
- uses: docker/setup-buildx-action@v1
|
- uses: docker/setup-buildx-action@v1
|
||||||
- uses: docker/login-action@v1
|
- uses: docker/login-action@v1
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
- uses: docker/metadata-action@v3
|
- uses: docker/metadata-action@v3
|
||||||
id: meta
|
id: meta
|
||||||
with:
|
with:
|
||||||
@ -25,10 +31,12 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
type=edge,branch=main
|
type=edge,branch=main
|
||||||
type=semver,pattern={{ raw }}
|
type=semver,pattern={{ raw }}
|
||||||
|
type=ref,event=pr
|
||||||
- uses: docker/build-push-action@v2
|
- uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
cache-from: type=registry,ref=tootsuite/mastodon:latest
|
cache-from: type=registry,ref=tootsuite/mastodon:latest
|
||||||
cache-to: type=inline
|
cache-to: type=inline
|
||||||
|
2
.github/workflows/check-i18n.yml
vendored
2
.github/workflows/check-i18n.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
|||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
|
sudo apt-get install -y libicu-dev libidn11-dev
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
|
@ -32,10 +32,11 @@ Layout/EmptyLineAfterGuardClause:
|
|||||||
Layout/EmptyLinesAroundAttributeAccessor:
|
Layout/EmptyLinesAroundAttributeAccessor:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
|
||||||
|
Layout/FirstHashElementIndentation:
|
||||||
|
EnforcedStyle: consistent
|
||||||
|
|
||||||
Layout/HashAlignment:
|
Layout/HashAlignment:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
# EnforcedHashRocketStyle: table
|
|
||||||
# EnforcedColonStyle: table
|
|
||||||
|
|
||||||
Layout/SpaceAroundMethodCallOperator:
|
Layout/SpaceAroundMethodCallOperator:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
2
Aptfile
2
Aptfile
@ -4,10 +4,8 @@ libicu-dev
|
|||||||
libidn11
|
libidn11
|
||||||
libidn11-dev
|
libidn11-dev
|
||||||
libpq-dev
|
libpq-dev
|
||||||
libprotobuf-dev
|
|
||||||
libxdamage1
|
libxdamage1
|
||||||
libxfixes3
|
libxfixes3
|
||||||
protobuf-compiler
|
|
||||||
zlib1g-dev
|
zlib1g-dev
|
||||||
libcairo2
|
libcairo2
|
||||||
libcroco3
|
libcroco3
|
||||||
|
@ -51,7 +51,7 @@ RUN npm install -g npm@latest && \
|
|||||||
gem install bundler && \
|
gem install bundler && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y --no-install-recommends git libicu-dev libidn11-dev \
|
apt-get install -y --no-install-recommends git libicu-dev libidn11-dev \
|
||||||
libpq-dev libprotobuf-dev protobuf-compiler shared-mime-info
|
libpq-dev shared-mime-info
|
||||||
|
|
||||||
COPY Gemfile* package.json yarn.lock /opt/mastodon/
|
COPY Gemfile* package.json yarn.lock /opt/mastodon/
|
||||||
|
|
||||||
@ -88,7 +88,7 @@ RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selectio
|
|||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get -y --no-install-recommends install \
|
apt-get -y --no-install-recommends install \
|
||||||
libssl1.1 libpq5 imagemagick ffmpeg libjemalloc2 \
|
libssl1.1 libpq5 imagemagick ffmpeg libjemalloc2 \
|
||||||
libicu66 libprotobuf17 libidn11 libyaml-0-2 \
|
libicu66 libidn11 libyaml-0-2 \
|
||||||
file ca-certificates tzdata libreadline8 gcc tini apt-utils && \
|
file ca-certificates tzdata libreadline8 gcc tini apt-utils && \
|
||||||
ln -s /opt/mastodon /mastodon && \
|
ln -s /opt/mastodon /mastodon && \
|
||||||
gem install bundler && \
|
gem install bundler && \
|
||||||
|
20
Gemfile
20
Gemfile
@ -6,7 +6,7 @@ ruby '>= 2.5.0', '< 3.1.0'
|
|||||||
gem 'pkg-config', '~> 1.4'
|
gem 'pkg-config', '~> 1.4'
|
||||||
gem 'rexml', '~> 3.2'
|
gem 'rexml', '~> 3.2'
|
||||||
|
|
||||||
gem 'puma', '~> 5.5'
|
gem 'puma', '~> 5.6'
|
||||||
gem 'rails', '~> 6.1.4'
|
gem 'rails', '~> 6.1.4'
|
||||||
gem 'sprockets', '~> 3.7.2'
|
gem 'sprockets', '~> 3.7.2'
|
||||||
gem 'thor', '~> 1.2'
|
gem 'thor', '~> 1.2'
|
||||||
@ -18,20 +18,18 @@ gem 'makara', '~> 0.5'
|
|||||||
gem 'pghero', '~> 2.8'
|
gem 'pghero', '~> 2.8'
|
||||||
gem 'dotenv-rails', '~> 2.7'
|
gem 'dotenv-rails', '~> 2.7'
|
||||||
|
|
||||||
gem 'aws-sdk-s3', '~> 1.111', require: false
|
gem 'aws-sdk-s3', '~> 1.113', require: false
|
||||||
gem 'fog-core', '<= 2.1.0'
|
gem 'fog-core', '<= 2.1.0'
|
||||||
gem 'fog-openstack', '~> 0.3', require: false
|
gem 'fog-openstack', '~> 0.3', require: false
|
||||||
gem 'kt-paperclip', '~> 7.0'
|
gem 'kt-paperclip', '~> 7.1'
|
||||||
gem 'blurhash', '~> 0.1'
|
gem 'blurhash', '~> 0.1'
|
||||||
|
|
||||||
gem 'active_model_serializers', '~> 0.10'
|
gem 'active_model_serializers', '~> 0.10'
|
||||||
gem 'addressable', '~> 2.8'
|
gem 'addressable', '~> 2.8'
|
||||||
gem 'bootsnap', '~> 1.10.2', require: false
|
gem 'bootsnap', '~> 1.10.3', require: false
|
||||||
gem 'browser'
|
gem 'browser'
|
||||||
gem 'charlock_holmes', '~> 0.7.7'
|
gem 'charlock_holmes', '~> 0.7.7'
|
||||||
gem 'iso-639'
|
|
||||||
gem 'chewy', '~> 7.2'
|
gem 'chewy', '~> 7.2'
|
||||||
gem 'cld3', '~> 3.4.4'
|
|
||||||
gem 'devise', '~> 4.8'
|
gem 'devise', '~> 4.8'
|
||||||
gem 'devise-two-factor', '~> 4.0'
|
gem 'devise-two-factor', '~> 4.0'
|
||||||
|
|
||||||
@ -67,7 +65,7 @@ gem 'oj', '~> 3.13'
|
|||||||
gem 'ox', '~> 2.14'
|
gem 'ox', '~> 2.14'
|
||||||
gem 'parslet'
|
gem 'parslet'
|
||||||
gem 'posix-spawn'
|
gem 'posix-spawn'
|
||||||
gem 'pundit', '~> 2.1'
|
gem 'pundit', '~> 2.2'
|
||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
gem 'rack-attack', '~> 6.5'
|
gem 'rack-attack', '~> 6.5'
|
||||||
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
|
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
|
||||||
@ -78,7 +76,7 @@ gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
|||||||
gem 'rqrcode', '~> 2.1'
|
gem 'rqrcode', '~> 2.1'
|
||||||
gem 'ruby-progressbar', '~> 1.11'
|
gem 'ruby-progressbar', '~> 1.11'
|
||||||
gem 'sanitize', '~> 6.0'
|
gem 'sanitize', '~> 6.0'
|
||||||
gem 'scenic', '~> 1.5'
|
gem 'scenic', '~> 1.6'
|
||||||
gem 'sidekiq', '~> 6.4'
|
gem 'sidekiq', '~> 6.4'
|
||||||
gem 'sidekiq-scheduler', '~> 3.1'
|
gem 'sidekiq-scheduler', '~> 3.1'
|
||||||
gem 'sidekiq-unique-jobs', '~> 7.1'
|
gem 'sidekiq-unique-jobs', '~> 7.1'
|
||||||
@ -100,12 +98,12 @@ gem 'json-ld-preloaded', '~> 3.2'
|
|||||||
gem 'rdf-normalize', '~> 0.5'
|
gem 'rdf-normalize', '~> 0.5'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'fabrication', '~> 2.24'
|
gem 'fabrication', '~> 2.27'
|
||||||
gem 'fuubar', '~> 2.5'
|
gem 'fuubar', '~> 2.5'
|
||||||
gem 'i18n-tasks', '~> 0.9', require: false
|
gem 'i18n-tasks', '~> 0.9', require: false
|
||||||
gem 'pry-byebug', '~> 3.9'
|
gem 'pry-byebug', '~> 3.9'
|
||||||
gem 'pry-rails', '~> 0.3'
|
gem 'pry-rails', '~> 0.3'
|
||||||
gem 'rspec-rails', '~> 5.0'
|
gem 'rspec-rails', '~> 5.1'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :production, :test do
|
group :production, :test do
|
||||||
@ -126,7 +124,7 @@ end
|
|||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
gem 'active_record_query_trace', '~> 1.8'
|
gem 'active_record_query_trace', '~> 1.8'
|
||||||
gem 'annotate', '~> 3.1'
|
gem 'annotate', '~> 3.2'
|
||||||
gem 'better_errors', '~> 2.9'
|
gem 'better_errors', '~> 2.9'
|
||||||
gem 'binding_of_caller', '~> 1.0'
|
gem 'binding_of_caller', '~> 1.0'
|
||||||
gem 'bullet', '~> 7.0'
|
gem 'bullet', '~> 7.0'
|
||||||
|
229
Gemfile.lock
229
Gemfile.lock
@ -1,40 +1,40 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.1.4.4)
|
actioncable (6.1.4.6)
|
||||||
actionpack (= 6.1.4.4)
|
actionpack (= 6.1.4.6)
|
||||||
activesupport (= 6.1.4.4)
|
activesupport (= 6.1.4.6)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.1.4.4)
|
actionmailbox (6.1.4.6)
|
||||||
actionpack (= 6.1.4.4)
|
actionpack (= 6.1.4.6)
|
||||||
activejob (= 6.1.4.4)
|
activejob (= 6.1.4.6)
|
||||||
activerecord (= 6.1.4.4)
|
activerecord (= 6.1.4.6)
|
||||||
activestorage (= 6.1.4.4)
|
activestorage (= 6.1.4.6)
|
||||||
activesupport (= 6.1.4.4)
|
activesupport (= 6.1.4.6)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.1.4.4)
|
actionmailer (6.1.4.6)
|
||||||
actionpack (= 6.1.4.4)
|
actionpack (= 6.1.4.6)
|
||||||
actionview (= 6.1.4.4)
|
actionview (= 6.1.4.6)
|
||||||
activejob (= 6.1.4.4)
|
activejob (= 6.1.4.6)
|
||||||
activesupport (= 6.1.4.4)
|
activesupport (= 6.1.4.6)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (6.1.4.4)
|
actionpack (6.1.4.6)
|
||||||
actionview (= 6.1.4.4)
|
actionview (= 6.1.4.6)
|
||||||
activesupport (= 6.1.4.4)
|
activesupport (= 6.1.4.6)
|
||||||
rack (~> 2.0, >= 2.0.9)
|
rack (~> 2.0, >= 2.0.9)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (6.1.4.4)
|
actiontext (6.1.4.6)
|
||||||
actionpack (= 6.1.4.4)
|
actionpack (= 6.1.4.6)
|
||||||
activerecord (= 6.1.4.4)
|
activerecord (= 6.1.4.6)
|
||||||
activestorage (= 6.1.4.4)
|
activestorage (= 6.1.4.6)
|
||||||
activesupport (= 6.1.4.4)
|
activesupport (= 6.1.4.6)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.1.4.4)
|
actionview (6.1.4.6)
|
||||||
activesupport (= 6.1.4.4)
|
activesupport (= 6.1.4.6)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
@ -45,22 +45,22 @@ GEM
|
|||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||||
active_record_query_trace (1.8)
|
active_record_query_trace (1.8)
|
||||||
activejob (6.1.4.4)
|
activejob (6.1.4.6)
|
||||||
activesupport (= 6.1.4.4)
|
activesupport (= 6.1.4.6)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.1.4.4)
|
activemodel (6.1.4.6)
|
||||||
activesupport (= 6.1.4.4)
|
activesupport (= 6.1.4.6)
|
||||||
activerecord (6.1.4.4)
|
activerecord (6.1.4.6)
|
||||||
activemodel (= 6.1.4.4)
|
activemodel (= 6.1.4.6)
|
||||||
activesupport (= 6.1.4.4)
|
activesupport (= 6.1.4.6)
|
||||||
activestorage (6.1.4.4)
|
activestorage (6.1.4.6)
|
||||||
actionpack (= 6.1.4.4)
|
actionpack (= 6.1.4.6)
|
||||||
activejob (= 6.1.4.4)
|
activejob (= 6.1.4.6)
|
||||||
activerecord (= 6.1.4.4)
|
activerecord (= 6.1.4.6)
|
||||||
activesupport (= 6.1.4.4)
|
activesupport (= 6.1.4.6)
|
||||||
marcel (~> 1.0.0)
|
marcel (~> 1.0.0)
|
||||||
mini_mime (>= 1.1.0)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (6.1.4.4)
|
activesupport (6.1.4.6)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
@ -71,25 +71,25 @@ GEM
|
|||||||
airbrussh (1.4.0)
|
airbrussh (1.4.0)
|
||||||
sshkit (>= 1.6.1, != 1.7.0)
|
sshkit (>= 1.6.1, != 1.7.0)
|
||||||
android_key_attestation (0.3.0)
|
android_key_attestation (0.3.0)
|
||||||
annotate (3.1.1)
|
annotate (3.2.0)
|
||||||
activerecord (>= 3.2, < 7.0)
|
activerecord (>= 3.2, < 8.0)
|
||||||
rake (>= 10.4, < 14.0)
|
rake (>= 10.4, < 14.0)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
attr_encrypted (3.1.0)
|
attr_encrypted (3.1.0)
|
||||||
encryptor (~> 3.0.0)
|
encryptor (~> 3.0.0)
|
||||||
awrence (1.1.1)
|
awrence (1.1.1)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.2.0)
|
||||||
aws-partitions (1.549.0)
|
aws-partitions (1.558.0)
|
||||||
aws-sdk-core (3.125.5)
|
aws-sdk-core (3.127.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.525.0)
|
aws-partitions (~> 1, >= 1.525.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-kms (1.53.0)
|
aws-sdk-kms (1.55.0)
|
||||||
aws-sdk-core (~> 3, >= 3.125.0)
|
aws-sdk-core (~> 3, >= 3.127.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.111.3)
|
aws-sdk-s3 (1.113.0)
|
||||||
aws-sdk-core (~> 3, >= 3.125.0)
|
aws-sdk-core (~> 3, >= 3.127.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.4)
|
aws-sigv4 (~> 1.4)
|
||||||
aws-sigv4 (1.4.0)
|
aws-sigv4 (1.4.0)
|
||||||
@ -104,9 +104,9 @@ GEM
|
|||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
blurhash (0.1.5)
|
blurhash (0.1.5)
|
||||||
ffi (~> 1.14)
|
ffi (~> 1.14)
|
||||||
bootsnap (1.10.2)
|
bootsnap (1.10.3)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
brakeman (5.2.0)
|
brakeman (5.2.1)
|
||||||
browser (4.2.0)
|
browser (4.2.0)
|
||||||
brpoplpush-redis_script (0.1.2)
|
brpoplpush-redis_script (0.1.2)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
@ -147,13 +147,11 @@ GEM
|
|||||||
activesupport
|
activesupport
|
||||||
cbor (0.5.9.6)
|
cbor (0.5.9.6)
|
||||||
charlock_holmes (0.7.7)
|
charlock_holmes (0.7.7)
|
||||||
chewy (7.2.3)
|
chewy (7.2.4)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
elasticsearch (>= 7.12.0, < 7.14.0)
|
elasticsearch (>= 7.12.0, < 7.14.0)
|
||||||
elasticsearch-dsl
|
elasticsearch-dsl
|
||||||
chunky_png (1.4.0)
|
chunky_png (1.4.0)
|
||||||
cld3 (3.4.4)
|
|
||||||
ffi (>= 1.1.0, < 1.16.0)
|
|
||||||
climate_control (0.2.0)
|
climate_control (0.2.0)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
color_diff (0.1)
|
color_diff (0.1)
|
||||||
@ -183,7 +181,7 @@ GEM
|
|||||||
devise_pam_authenticatable2 (9.2.0)
|
devise_pam_authenticatable2 (9.2.0)
|
||||||
devise (>= 4.0.0)
|
devise (>= 4.0.0)
|
||||||
rpam2 (~> 4.0)
|
rpam2 (~> 4.0)
|
||||||
diff-lcs (1.4.4)
|
diff-lcs (1.5.0)
|
||||||
discard (1.2.1)
|
discard (1.2.1)
|
||||||
activerecord (>= 4.2, < 8)
|
activerecord (>= 4.2, < 8)
|
||||||
docile (1.3.4)
|
docile (1.3.4)
|
||||||
@ -208,31 +206,35 @@ GEM
|
|||||||
multi_json
|
multi_json
|
||||||
encryptor (3.0.0)
|
encryptor (3.0.0)
|
||||||
erubi (1.10.0)
|
erubi (1.10.0)
|
||||||
et-orbi (1.2.4)
|
et-orbi (1.2.6)
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (0.76.0)
|
excon (0.76.0)
|
||||||
fabrication (2.24.0)
|
fabrication (2.27.0)
|
||||||
faker (2.19.0)
|
faker (2.19.0)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
faraday (1.8.0)
|
faraday (1.9.3)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
faraday-em_synchrony (~> 1.0)
|
faraday-em_synchrony (~> 1.0)
|
||||||
faraday-excon (~> 1.1)
|
faraday-excon (~> 1.1)
|
||||||
faraday-httpclient (~> 1.0.1)
|
faraday-httpclient (~> 1.0)
|
||||||
|
faraday-multipart (~> 1.0)
|
||||||
faraday-net_http (~> 1.0)
|
faraday-net_http (~> 1.0)
|
||||||
faraday-net_http_persistent (~> 1.1)
|
faraday-net_http_persistent (~> 1.0)
|
||||||
faraday-patron (~> 1.0)
|
faraday-patron (~> 1.0)
|
||||||
faraday-rack (~> 1.0)
|
faraday-rack (~> 1.0)
|
||||||
multipart-post (>= 1.2, < 3)
|
faraday-retry (~> 1.0)
|
||||||
ruby2_keywords (>= 0.0.4)
|
ruby2_keywords (>= 0.0.4)
|
||||||
faraday-em_http (1.0.0)
|
faraday-em_http (1.0.0)
|
||||||
faraday-em_synchrony (1.0.0)
|
faraday-em_synchrony (1.0.0)
|
||||||
faraday-excon (1.1.0)
|
faraday-excon (1.1.0)
|
||||||
faraday-httpclient (1.0.1)
|
faraday-httpclient (1.0.1)
|
||||||
|
faraday-multipart (1.0.3)
|
||||||
|
multipart-post (>= 1.2, < 3)
|
||||||
faraday-net_http (1.0.1)
|
faraday-net_http (1.0.1)
|
||||||
faraday-net_http_persistent (1.2.0)
|
faraday-net_http_persistent (1.2.0)
|
||||||
faraday-patron (1.0.0)
|
faraday-patron (1.0.0)
|
||||||
faraday-rack (1.0.0)
|
faraday-rack (1.0.0)
|
||||||
|
faraday-retry (1.0.3)
|
||||||
fast_blank (1.0.1)
|
fast_blank (1.0.1)
|
||||||
fastimage (2.2.6)
|
fastimage (2.2.6)
|
||||||
ffi (1.15.5)
|
ffi (1.15.5)
|
||||||
@ -252,7 +254,7 @@ GEM
|
|||||||
fog-json (>= 1.0)
|
fog-json (>= 1.0)
|
||||||
ipaddress (>= 0.8)
|
ipaddress (>= 0.8)
|
||||||
formatador (0.2.5)
|
formatador (0.2.5)
|
||||||
fugit (1.4.5)
|
fugit (1.5.2)
|
||||||
et-orbi (~> 1.1, >= 1.1.8)
|
et-orbi (~> 1.1, >= 1.1.8)
|
||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
fuubar (2.5.1)
|
fuubar (2.5.1)
|
||||||
@ -287,7 +289,7 @@ GEM
|
|||||||
httplog (1.5.0)
|
httplog (1.5.0)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
rainbow (>= 2.0.0)
|
rainbow (>= 2.0.0)
|
||||||
i18n (1.8.11)
|
i18n (1.9.1)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
i18n-tasks (0.9.37)
|
i18n-tasks (0.9.37)
|
||||||
activesupport (>= 4.0.2)
|
activesupport (>= 4.0.2)
|
||||||
@ -301,8 +303,7 @@ GEM
|
|||||||
terminal-table (>= 1.5.1)
|
terminal-table (>= 1.5.1)
|
||||||
idn-ruby (0.1.4)
|
idn-ruby (0.1.4)
|
||||||
ipaddress (0.8.3)
|
ipaddress (0.8.3)
|
||||||
iso-639 (0.3.5)
|
jmespath (1.6.0)
|
||||||
jmespath (1.5.0)
|
|
||||||
json (2.5.1)
|
json (2.5.1)
|
||||||
json-canonicalization (0.3.0)
|
json-canonicalization (0.3.0)
|
||||||
json-ld (3.2.0)
|
json-ld (3.2.0)
|
||||||
@ -329,7 +330,7 @@ GEM
|
|||||||
activerecord
|
activerecord
|
||||||
kaminari-core (= 1.2.2)
|
kaminari-core (= 1.2.2)
|
||||||
kaminari-core (1.2.2)
|
kaminari-core (1.2.2)
|
||||||
kt-paperclip (7.0.1)
|
kt-paperclip (7.1.1)
|
||||||
activemodel (>= 4.2.0)
|
activemodel (>= 4.2.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
marcel (~> 1.0.1)
|
marcel (~> 1.0.1)
|
||||||
@ -353,14 +354,14 @@ GEM
|
|||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.13.0)
|
loofah (2.14.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
makara (0.5.1)
|
makara (0.5.1)
|
||||||
activerecord (>= 5.2.0)
|
activerecord (>= 5.2.0)
|
||||||
marcel (1.0.1)
|
marcel (1.0.2)
|
||||||
mario-redis-lock (1.2.1)
|
mario-redis-lock (1.2.1)
|
||||||
redis (>= 3.0.5)
|
redis (>= 3.0.5)
|
||||||
matrix (0.4.2)
|
matrix (0.4.2)
|
||||||
@ -371,9 +372,9 @@ GEM
|
|||||||
nokogiri (~> 1.10)
|
nokogiri (~> 1.10)
|
||||||
mime-types (3.4.1)
|
mime-types (3.4.1)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2021.1115)
|
mime-types-data (3.2022.0105)
|
||||||
mini_mime (1.1.2)
|
mini_mime (1.1.2)
|
||||||
mini_portile2 (2.7.1)
|
mini_portile2 (2.8.0)
|
||||||
minitest (5.15.0)
|
minitest (5.15.0)
|
||||||
msgpack (1.4.4)
|
msgpack (1.4.4)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
@ -383,8 +384,8 @@ GEM
|
|||||||
net-ssh (>= 2.6.5, < 7.0.0)
|
net-ssh (>= 2.6.5, < 7.0.0)
|
||||||
net-ssh (6.1.0)
|
net-ssh (6.1.0)
|
||||||
nio4r (2.5.8)
|
nio4r (2.5.8)
|
||||||
nokogiri (1.13.1)
|
nokogiri (1.13.3)
|
||||||
mini_portile2 (~> 2.7.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nsa (0.2.8)
|
nsa (0.2.8)
|
||||||
activesupport (>= 4.2, < 7)
|
activesupport (>= 4.2, < 7)
|
||||||
@ -408,14 +409,14 @@ GEM
|
|||||||
openssl (2.2.0)
|
openssl (2.2.0)
|
||||||
openssl-signature_algorithm (0.4.0)
|
openssl-signature_algorithm (0.4.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ox (2.14.6)
|
ox (2.14.9)
|
||||||
parallel (1.21.0)
|
parallel (1.21.0)
|
||||||
parser (3.1.0.0)
|
parser (3.1.0.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.3.0)
|
pg (1.3.3)
|
||||||
pghero (2.8.2)
|
pghero (2.8.2)
|
||||||
activerecord (>= 5)
|
activerecord (>= 5)
|
||||||
pkg-config (1.4.7)
|
pkg-config (1.4.7)
|
||||||
@ -437,9 +438,9 @@ GEM
|
|||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (4.0.6)
|
public_suffix (4.0.6)
|
||||||
puma (5.5.2)
|
puma (5.6.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.1.1)
|
pundit (2.2.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.6.0)
|
racc (1.6.0)
|
||||||
@ -452,20 +453,20 @@ GEM
|
|||||||
rack
|
rack
|
||||||
rack-test (1.1.0)
|
rack-test (1.1.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rails (6.1.4.4)
|
rails (6.1.4.6)
|
||||||
actioncable (= 6.1.4.4)
|
actioncable (= 6.1.4.6)
|
||||||
actionmailbox (= 6.1.4.4)
|
actionmailbox (= 6.1.4.6)
|
||||||
actionmailer (= 6.1.4.4)
|
actionmailer (= 6.1.4.6)
|
||||||
actionpack (= 6.1.4.4)
|
actionpack (= 6.1.4.6)
|
||||||
actiontext (= 6.1.4.4)
|
actiontext (= 6.1.4.6)
|
||||||
actionview (= 6.1.4.4)
|
actionview (= 6.1.4.6)
|
||||||
activejob (= 6.1.4.4)
|
activejob (= 6.1.4.6)
|
||||||
activemodel (= 6.1.4.4)
|
activemodel (= 6.1.4.6)
|
||||||
activerecord (= 6.1.4.4)
|
activerecord (= 6.1.4.6)
|
||||||
activestorage (= 6.1.4.4)
|
activestorage (= 6.1.4.6)
|
||||||
activesupport (= 6.1.4.4)
|
activesupport (= 6.1.4.6)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.1.4.4)
|
railties (= 6.1.4.6)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-controller-testing (1.0.5)
|
rails-controller-testing (1.0.5)
|
||||||
actionpack (>= 5.0.1.rc1)
|
actionpack (>= 5.0.1.rc1)
|
||||||
@ -481,9 +482,9 @@ GEM
|
|||||||
railties (>= 6.0.0, < 7)
|
railties (>= 6.0.0, < 7)
|
||||||
rails-settings-cached (0.6.6)
|
rails-settings-cached (0.6.6)
|
||||||
rails (>= 4.2.0)
|
rails (>= 4.2.0)
|
||||||
railties (6.1.4.4)
|
railties (6.1.4.6)
|
||||||
actionpack (= 6.1.4.4)
|
actionpack (= 6.1.4.6)
|
||||||
activesupport (= 6.1.4.4)
|
activesupport (= 6.1.4.6)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 0.13)
|
rake (>= 0.13)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
@ -505,19 +506,19 @@ GEM
|
|||||||
rexml (3.2.5)
|
rexml (3.2.5)
|
||||||
rotp (6.2.0)
|
rotp (6.2.0)
|
||||||
rpam2 (4.0.2)
|
rpam2 (4.0.2)
|
||||||
rqrcode (2.1.0)
|
rqrcode (2.1.1)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
rqrcode_core (~> 1.0)
|
rqrcode_core (~> 1.0)
|
||||||
rqrcode_core (1.2.0)
|
rqrcode_core (1.2.0)
|
||||||
rspec-core (3.10.1)
|
rspec-core (3.11.0)
|
||||||
rspec-support (~> 3.10.0)
|
rspec-support (~> 3.11.0)
|
||||||
rspec-expectations (3.10.1)
|
rspec-expectations (3.11.0)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.10.0)
|
rspec-support (~> 3.11.0)
|
||||||
rspec-mocks (3.10.2)
|
rspec-mocks (3.11.0)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.10.0)
|
rspec-support (~> 3.11.0)
|
||||||
rspec-rails (5.0.2)
|
rspec-rails (5.1.0)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
@ -528,10 +529,10 @@ GEM
|
|||||||
rspec-sidekiq (3.1.0)
|
rspec-sidekiq (3.1.0)
|
||||||
rspec-core (~> 3.0, >= 3.0.0)
|
rspec-core (~> 3.0, >= 3.0.0)
|
||||||
sidekiq (>= 2.4.0)
|
sidekiq (>= 2.4.0)
|
||||||
rspec-support (3.10.3)
|
rspec-support (3.11.0)
|
||||||
rspec_junit_formatter (0.5.1)
|
rspec_junit_formatter (0.5.1)
|
||||||
rspec-core (>= 2, < 4, != 2.12.0)
|
rspec-core (>= 2, < 4, != 2.12.0)
|
||||||
rubocop (1.25.0)
|
rubocop (1.25.1)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.1.0.0)
|
parser (>= 3.1.0.0)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
@ -551,32 +552,32 @@ GEM
|
|||||||
nokogiri (>= 1.10.5)
|
nokogiri (>= 1.10.5)
|
||||||
rexml
|
rexml
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rufus-scheduler (3.7.0)
|
rufus-scheduler (3.8.1)
|
||||||
fugit (~> 1.1, >= 1.1.6)
|
fugit (~> 1.1, >= 1.1.6)
|
||||||
safety_net_attestation (0.4.0)
|
safety_net_attestation (0.4.0)
|
||||||
jwt (~> 2.0)
|
jwt (~> 2.0)
|
||||||
sanitize (6.0.0)
|
sanitize (6.0.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
scenic (1.5.5)
|
scenic (1.6.0)
|
||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
railties (>= 4.0.0)
|
railties (>= 4.0.0)
|
||||||
securecompare (1.0.0)
|
securecompare (1.0.0)
|
||||||
semantic_range (3.0.0)
|
semantic_range (3.0.0)
|
||||||
sidekiq (6.4.0)
|
sidekiq (6.4.1)
|
||||||
connection_pool (>= 2.2.2)
|
connection_pool (>= 2.2.2)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
redis (>= 4.2.0)
|
redis (>= 4.2.0)
|
||||||
sidekiq-bulk (0.2.0)
|
sidekiq-bulk (0.2.0)
|
||||||
sidekiq
|
sidekiq
|
||||||
sidekiq-scheduler (3.1.0)
|
sidekiq-scheduler (3.1.1)
|
||||||
e2mmap
|
e2mmap
|
||||||
redis (>= 3, < 5)
|
redis (>= 3, < 5)
|
||||||
rufus-scheduler (~> 3.2)
|
rufus-scheduler (~> 3.2)
|
||||||
sidekiq (>= 3)
|
sidekiq (>= 3)
|
||||||
thwait
|
thwait
|
||||||
tilt (>= 1.4.0)
|
tilt (>= 1.4.0)
|
||||||
sidekiq-unique-jobs (7.1.12)
|
sidekiq-unique-jobs (7.1.15)
|
||||||
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
sidekiq (>= 5.0, < 8.0)
|
sidekiq (>= 5.0, < 8.0)
|
||||||
@ -672,7 +673,7 @@ GEM
|
|||||||
xorcist (1.1.2)
|
xorcist (1.1.2)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.5.3)
|
zeitwerk (2.5.4)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
@ -681,12 +682,12 @@ DEPENDENCIES
|
|||||||
active_model_serializers (~> 0.10)
|
active_model_serializers (~> 0.10)
|
||||||
active_record_query_trace (~> 1.8)
|
active_record_query_trace (~> 1.8)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
annotate (~> 3.1)
|
annotate (~> 3.2)
|
||||||
aws-sdk-s3 (~> 1.111)
|
aws-sdk-s3 (~> 1.113)
|
||||||
better_errors (~> 2.9)
|
better_errors (~> 2.9)
|
||||||
binding_of_caller (~> 1.0)
|
binding_of_caller (~> 1.0)
|
||||||
blurhash (~> 0.1)
|
blurhash (~> 0.1)
|
||||||
bootsnap (~> 1.10.2)
|
bootsnap (~> 1.10.3)
|
||||||
brakeman (~> 5.2)
|
brakeman (~> 5.2)
|
||||||
browser
|
browser
|
||||||
bullet (~> 7.0)
|
bullet (~> 7.0)
|
||||||
@ -698,7 +699,6 @@ DEPENDENCIES
|
|||||||
capybara (~> 3.36)
|
capybara (~> 3.36)
|
||||||
charlock_holmes (~> 0.7.7)
|
charlock_holmes (~> 0.7.7)
|
||||||
chewy (~> 7.2)
|
chewy (~> 7.2)
|
||||||
cld3 (~> 3.4.4)
|
|
||||||
climate_control (~> 0.2)
|
climate_control (~> 0.2)
|
||||||
color_diff (~> 0.1)
|
color_diff (~> 0.1)
|
||||||
concurrent-ruby
|
concurrent-ruby
|
||||||
@ -710,7 +710,7 @@ DEPENDENCIES
|
|||||||
doorkeeper (~> 5.5)
|
doorkeeper (~> 5.5)
|
||||||
dotenv-rails (~> 2.7)
|
dotenv-rails (~> 2.7)
|
||||||
ed25519 (~> 1.3)
|
ed25519 (~> 1.3)
|
||||||
fabrication (~> 2.24)
|
fabrication (~> 2.27)
|
||||||
faker (~> 2.19)
|
faker (~> 2.19)
|
||||||
fast_blank (~> 1.0)
|
fast_blank (~> 1.0)
|
||||||
fastimage
|
fastimage
|
||||||
@ -725,11 +725,10 @@ DEPENDENCIES
|
|||||||
httplog (~> 1.5.0)
|
httplog (~> 1.5.0)
|
||||||
i18n-tasks (~> 0.9)
|
i18n-tasks (~> 0.9)
|
||||||
idn-ruby
|
idn-ruby
|
||||||
iso-639
|
|
||||||
json-ld
|
json-ld
|
||||||
json-ld-preloaded (~> 3.2)
|
json-ld-preloaded (~> 3.2)
|
||||||
kaminari (~> 1.2)
|
kaminari (~> 1.2)
|
||||||
kt-paperclip (~> 7.0)
|
kt-paperclip (~> 7.1)
|
||||||
letter_opener (~> 1.7)
|
letter_opener (~> 1.7)
|
||||||
letter_opener_web (~> 2.0)
|
letter_opener_web (~> 2.0)
|
||||||
link_header (~> 0.0)
|
link_header (~> 0.0)
|
||||||
@ -757,8 +756,8 @@ DEPENDENCIES
|
|||||||
private_address_check (~> 0.5)
|
private_address_check (~> 0.5)
|
||||||
pry-byebug (~> 3.9)
|
pry-byebug (~> 3.9)
|
||||||
pry-rails (~> 0.3)
|
pry-rails (~> 0.3)
|
||||||
puma (~> 5.5)
|
puma (~> 5.6)
|
||||||
pundit (~> 2.1)
|
pundit (~> 2.2)
|
||||||
rack (~> 2.2.3)
|
rack (~> 2.2.3)
|
||||||
rack-attack (~> 6.5)
|
rack-attack (~> 6.5)
|
||||||
rack-cors (~> 1.1)
|
rack-cors (~> 1.1)
|
||||||
@ -771,14 +770,14 @@ DEPENDENCIES
|
|||||||
redis-namespace (~> 1.8)
|
redis-namespace (~> 1.8)
|
||||||
rexml (~> 3.2)
|
rexml (~> 3.2)
|
||||||
rqrcode (~> 2.1)
|
rqrcode (~> 2.1)
|
||||||
rspec-rails (~> 5.0)
|
rspec-rails (~> 5.1)
|
||||||
rspec-sidekiq (~> 3.1)
|
rspec-sidekiq (~> 3.1)
|
||||||
rspec_junit_formatter (~> 0.5)
|
rspec_junit_formatter (~> 0.5)
|
||||||
rubocop (~> 1.25)
|
rubocop (~> 1.25)
|
||||||
rubocop-rails (~> 2.13)
|
rubocop-rails (~> 2.13)
|
||||||
ruby-progressbar (~> 1.11)
|
ruby-progressbar (~> 1.11)
|
||||||
sanitize (~> 6.0)
|
sanitize (~> 6.0)
|
||||||
scenic (~> 1.5)
|
scenic (~> 1.6)
|
||||||
sidekiq (~> 6.4)
|
sidekiq (~> 6.4)
|
||||||
sidekiq-bulk (~> 0.2.0)
|
sidekiq-bulk (~> 0.2.0)
|
||||||
sidekiq-scheduler (~> 3.1)
|
sidekiq-scheduler (~> 3.1)
|
||||||
|
2
Vagrantfile
vendored
2
Vagrantfile
vendored
@ -33,11 +33,9 @@ sudo apt-get install \
|
|||||||
redis-tools \
|
redis-tools \
|
||||||
postgresql \
|
postgresql \
|
||||||
postgresql-contrib \
|
postgresql-contrib \
|
||||||
protobuf-compiler \
|
|
||||||
yarn \
|
yarn \
|
||||||
libicu-dev \
|
libicu-dev \
|
||||||
libidn11-dev \
|
libidn11-dev \
|
||||||
libprotobuf-dev \
|
|
||||||
libreadline-dev \
|
libreadline-dev \
|
||||||
libpam0g-dev \
|
libpam0g-dev \
|
||||||
-y
|
-y
|
||||||
|
@ -63,15 +63,29 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def next_page
|
def next_page
|
||||||
only_other_accounts = !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT)
|
if only_other_accounts?
|
||||||
|
# Only consider remote accounts
|
||||||
|
return nil if @replies.size < DESCENDANTS_LIMIT
|
||||||
|
|
||||||
account_status_replies_url(
|
account_status_replies_url(
|
||||||
@account,
|
@account,
|
||||||
@status,
|
@status,
|
||||||
page: true,
|
page: true,
|
||||||
min_id: only_other_accounts && !only_other_accounts? ? nil : @replies&.last&.id,
|
min_id: @replies&.last&.id,
|
||||||
only_other_accounts: only_other_accounts
|
only_other_accounts: true
|
||||||
)
|
)
|
||||||
|
else
|
||||||
|
# For now, we're serving only self-replies, but next page might be other accounts
|
||||||
|
next_only_other_accounts = @replies&.last&.account_id != @account.id || @replies.size < DESCENDANTS_LIMIT
|
||||||
|
|
||||||
|
account_status_replies_url(
|
||||||
|
@account,
|
||||||
|
@status,
|
||||||
|
page: true,
|
||||||
|
min_id: next_only_other_accounts ? nil : @replies&.last&.id,
|
||||||
|
only_other_accounts: next_only_other_accounts
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def page_params
|
def page_params
|
||||||
|
@ -28,7 +28,7 @@ module Admin
|
|||||||
@deletion_request = @account.deletion_request
|
@deletion_request = @account.deletion_request
|
||||||
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
|
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
|
||||||
@moderation_notes = @account.targeted_moderation_notes.latest
|
@moderation_notes = @account.targeted_moderation_notes.latest
|
||||||
@warnings = @account.strikes.custom.latest
|
@warnings = @account.strikes.includes(:target_account, :account, :appeal).latest
|
||||||
@domain_block = DomainBlock.rule_for(@account.domain)
|
@domain_block = DomainBlock.rule_for(@account.domain)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def filter_params
|
def filter_params
|
||||||
params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS)
|
params.slice(:page, *AccountFilter::KEYS).permit(:page, *AccountFilter::KEYS)
|
||||||
end
|
end
|
||||||
|
|
||||||
def form_account_batch_params
|
def form_account_batch_params
|
||||||
|
@ -8,6 +8,7 @@ module Admin
|
|||||||
@pending_users_count = User.pending.count
|
@pending_users_count = User.pending.count
|
||||||
@pending_reports_count = Report.unresolved.count
|
@pending_reports_count = Report.unresolved.count
|
||||||
@pending_tags_count = Tag.pending_review.count
|
@pending_tags_count = Tag.pending_review.count
|
||||||
|
@pending_appeals_count = Appeal.pending.count
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
40
app/controllers/admin/disputes/appeals_controller.rb
Normal file
40
app/controllers/admin/disputes/appeals_controller.rb
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Disputes::AppealsController < Admin::BaseController
|
||||||
|
before_action :set_appeal, except: :index
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize :appeal, :index?
|
||||||
|
|
||||||
|
@appeals = filtered_appeals.page(params[:page])
|
||||||
|
end
|
||||||
|
|
||||||
|
def approve
|
||||||
|
authorize @appeal, :approve?
|
||||||
|
log_action :approve, @appeal
|
||||||
|
ApproveAppealService.new.call(@appeal, current_account)
|
||||||
|
redirect_to disputes_strike_path(@appeal.strike)
|
||||||
|
end
|
||||||
|
|
||||||
|
def reject
|
||||||
|
authorize @appeal, :approve?
|
||||||
|
log_action :reject, @appeal
|
||||||
|
@appeal.reject!(current_account)
|
||||||
|
UserMailer.appeal_rejected(@appeal.account.user, @appeal)
|
||||||
|
redirect_to disputes_strike_path(@appeal.strike)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def filtered_appeals
|
||||||
|
Admin::AppealFilter.new(filter_params.with_defaults(status: 'pending')).results.includes(strike: :account)
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_params
|
||||||
|
params.slice(:page, *Admin::AppealFilter::KEYS).permit(:page, *Admin::AppealFilter::KEYS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_appeal
|
||||||
|
@appeal = Appeal.find(params[:id])
|
||||||
|
end
|
||||||
|
end
|
@ -6,7 +6,20 @@ module Admin
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
authorize :email_domain_block, :index?
|
authorize :email_domain_block, :index?
|
||||||
|
|
||||||
@email_domain_blocks = EmailDomainBlock.where(parent_id: nil).includes(:children).order(id: :desc).page(params[:page])
|
@email_domain_blocks = EmailDomainBlock.where(parent_id: nil).includes(:children).order(id: :desc).page(params[:page])
|
||||||
|
@form = Form::EmailDomainBlockBatch.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def batch
|
||||||
|
@form = Form::EmailDomainBlockBatch.new(form_email_domain_block_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
|
@form.save
|
||||||
|
rescue ActionController::ParameterMissing
|
||||||
|
flash[:alert] = I18n.t('admin.email_domain_blocks.no_email_domain_block_selected')
|
||||||
|
rescue Mastodon::NotPermittedError
|
||||||
|
flash[:alert] = I18n.t('admin.custom_emojis.not_permitted')
|
||||||
|
ensure
|
||||||
|
redirect_to admin_email_domain_blocks_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@ -19,41 +32,27 @@ module Admin
|
|||||||
|
|
||||||
@email_domain_block = EmailDomainBlock.new(resource_params)
|
@email_domain_block = EmailDomainBlock.new(resource_params)
|
||||||
|
|
||||||
if @email_domain_block.save
|
if action_from_button == 'save'
|
||||||
|
EmailDomainBlock.transaction do
|
||||||
|
@email_domain_block.save!
|
||||||
log_action :create, @email_domain_block
|
log_action :create, @email_domain_block
|
||||||
|
|
||||||
if @email_domain_block.with_dns_records?
|
(@email_domain_block.other_domains || []).uniq.each do |domain|
|
||||||
hostnames = []
|
next if EmailDomainBlock.where(domain: domain).exists?
|
||||||
ips = []
|
|
||||||
|
|
||||||
Resolv::DNS.open do |dns|
|
other_email_domain_block = EmailDomainBlock.create!(domain: domain, parent: @email_domain_block)
|
||||||
dns.timeouts = 5
|
log_action :create, other_email_domain_block
|
||||||
|
|
||||||
hostnames = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s }
|
|
||||||
|
|
||||||
([@email_domain_block.domain] + hostnames).uniq.each do |hostname|
|
|
||||||
ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::A).to_a.map { |e| e.address.to_s })
|
|
||||||
ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::AAAA).to_a.map { |e| e.address.to_s })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
(hostnames + ips).each do |hostname|
|
|
||||||
another_email_domain_block = EmailDomainBlock.new(domain: hostname, parent: @email_domain_block)
|
|
||||||
log_action :create, another_email_domain_block if another_email_domain_block.save
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg')
|
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg')
|
||||||
else
|
else
|
||||||
|
set_resolved_records
|
||||||
render :new
|
render :new
|
||||||
end
|
end
|
||||||
end
|
rescue ActiveRecord::RecordInvalid
|
||||||
|
set_resolved_records
|
||||||
def destroy
|
render :new
|
||||||
authorize @email_domain_block, :destroy?
|
|
||||||
@email_domain_block.destroy!
|
|
||||||
log_action :destroy, @email_domain_block
|
|
||||||
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@ -62,8 +61,27 @@ module Admin
|
|||||||
@email_domain_block = EmailDomainBlock.find(params[:id])
|
@email_domain_block = EmailDomainBlock.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_resolved_records
|
||||||
|
Resolv::DNS.open do |dns|
|
||||||
|
dns.timeouts = 5
|
||||||
|
@resolved_records = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params.require(:email_domain_block).permit(:domain, :with_dns_records)
|
params.require(:email_domain_block).permit(:domain, other_domains: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def form_email_domain_block_batch_params
|
||||||
|
params.require(:form_email_domain_block_batch).permit(email_domain_block_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_from_button
|
||||||
|
if params[:delete]
|
||||||
|
'delete'
|
||||||
|
elsif params[:save]
|
||||||
|
'save'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -9,7 +9,8 @@ module Admin
|
|||||||
def index
|
def index
|
||||||
authorize :account, :index?
|
authorize :account, :index?
|
||||||
|
|
||||||
@accounts = RelationshipFilter.new(@account, filter_params).results.page(params[:page]).per(PER_PAGE)
|
@accounts = RelationshipFilter.new(@account, filter_params).results.includes(:account_stat, user: [:ips, :invite_request]).page(params[:page]).per(PER_PAGE)
|
||||||
|
@form = Form::AccountBatch.new
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
50
app/controllers/admin/reports/actions_controller.rb
Normal file
50
app/controllers/admin/reports/actions_controller.rb
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Reports::ActionsController < Admin::BaseController
|
||||||
|
before_action :set_report
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize @report, :show?
|
||||||
|
|
||||||
|
case action_from_button
|
||||||
|
when 'delete'
|
||||||
|
status_batch_action = Admin::StatusBatchAction.new(
|
||||||
|
type: action_from_button,
|
||||||
|
status_ids: @report.status_ids,
|
||||||
|
current_account: current_account,
|
||||||
|
report_id: @report.id,
|
||||||
|
send_email_notification: !@report.spam?
|
||||||
|
)
|
||||||
|
|
||||||
|
status_batch_action.save!
|
||||||
|
when 'silence', 'suspend'
|
||||||
|
account_action = Admin::AccountAction.new(
|
||||||
|
type: action_from_button,
|
||||||
|
report_id: @report.id,
|
||||||
|
target_account: @report.target_account,
|
||||||
|
current_account: current_account,
|
||||||
|
send_email_notification: !@report.spam?
|
||||||
|
)
|
||||||
|
|
||||||
|
account_action.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to admin_reports_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_report
|
||||||
|
@report = Report.find(params[:report_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_from_button
|
||||||
|
if params[:delete]
|
||||||
|
'delete'
|
||||||
|
elsif params[:silence]
|
||||||
|
'silence'
|
||||||
|
elsif params[:suspend]
|
||||||
|
'suspend'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -29,8 +29,9 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def after_create_redirect_path
|
def after_create_redirect_path
|
||||||
if @status_batch_action.report_id.present?
|
report_id = @status_batch_action&.report_id || params[:report_id]
|
||||||
admin_report_path(@status_batch_action.report_id)
|
if report_id.present?
|
||||||
|
admin_report_path(report_id)
|
||||||
else
|
else
|
||||||
admin_account_statuses_path(params[:account_id], current_params)
|
admin_account_statuses_path(params[:account_id], current_params)
|
||||||
end
|
end
|
||||||
@ -48,6 +49,10 @@ module Admin
|
|||||||
params.slice(*Admin::StatusFilter::KEYS).permit(*Admin::StatusFilter::KEYS)
|
params.slice(*Admin::StatusFilter::KEYS).permit(*Admin::StatusFilter::KEYS)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def current_params
|
||||||
|
params.slice(:media, :page).permit(:media, :page)
|
||||||
|
end
|
||||||
|
|
||||||
def action_from_button
|
def action_from_button
|
||||||
if params[:report]
|
if params[:report]
|
||||||
'report'
|
'report'
|
||||||
|
@ -5,11 +5,11 @@ class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseControll
|
|||||||
authorize :preview_card_provider, :index?
|
authorize :preview_card_provider, :index?
|
||||||
|
|
||||||
@preview_card_providers = filtered_preview_card_providers.page(params[:page])
|
@preview_card_providers = filtered_preview_card_providers.page(params[:page])
|
||||||
@form = Form::PreviewCardProviderBatch.new
|
@form = Trends::PreviewCardProviderBatch.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def batch
|
def batch
|
||||||
@form = Form::PreviewCardProviderBatch.new(form_preview_card_provider_batch_params.merge(current_account: current_account, action: action_from_button))
|
@form = Trends::PreviewCardProviderBatch.new(trends_preview_card_provider_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
@form.save
|
@form.save
|
||||||
rescue ActionController::ParameterMissing
|
rescue ActionController::ParameterMissing
|
||||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||||
@ -20,15 +20,15 @@ class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseControll
|
|||||||
private
|
private
|
||||||
|
|
||||||
def filtered_preview_card_providers
|
def filtered_preview_card_providers
|
||||||
PreviewCardProviderFilter.new(filter_params).results
|
Trends::PreviewCardProviderFilter.new(filter_params).results
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_params
|
def filter_params
|
||||||
params.slice(:page, *PreviewCardProviderFilter::KEYS).permit(:page, *PreviewCardProviderFilter::KEYS)
|
params.slice(:page, *Trends::PreviewCardProviderFilter::KEYS).permit(:page, *Trends::PreviewCardProviderFilter::KEYS)
|
||||||
end
|
end
|
||||||
|
|
||||||
def form_preview_card_provider_batch_params
|
def trends_preview_card_provider_batch_params
|
||||||
params.require(:form_preview_card_provider_batch).permit(:action, preview_card_provider_ids: [])
|
params.require(:trends_preview_card_provider_batch).permit(:action, preview_card_provider_ids: [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def action_from_button
|
def action_from_button
|
||||||
|
@ -5,11 +5,11 @@ class Admin::Trends::LinksController < Admin::BaseController
|
|||||||
authorize :preview_card, :index?
|
authorize :preview_card, :index?
|
||||||
|
|
||||||
@preview_cards = filtered_preview_cards.page(params[:page])
|
@preview_cards = filtered_preview_cards.page(params[:page])
|
||||||
@form = Form::PreviewCardBatch.new
|
@form = Trends::PreviewCardBatch.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def batch
|
def batch
|
||||||
@form = Form::PreviewCardBatch.new(form_preview_card_batch_params.merge(current_account: current_account, action: action_from_button))
|
@form = Trends::PreviewCardBatch.new(trends_preview_card_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
@form.save
|
@form.save
|
||||||
rescue ActionController::ParameterMissing
|
rescue ActionController::ParameterMissing
|
||||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||||
@ -20,26 +20,26 @@ class Admin::Trends::LinksController < Admin::BaseController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def filtered_preview_cards
|
def filtered_preview_cards
|
||||||
PreviewCardFilter.new(filter_params.with_defaults(trending: 'all')).results
|
Trends::PreviewCardFilter.new(filter_params.with_defaults(trending: 'all')).results
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_params
|
def filter_params
|
||||||
params.slice(:page, *PreviewCardFilter::KEYS).permit(:page, *PreviewCardFilter::KEYS)
|
params.slice(:page, *Trends::PreviewCardFilter::KEYS).permit(:page, *Trends::PreviewCardFilter::KEYS)
|
||||||
end
|
end
|
||||||
|
|
||||||
def form_preview_card_batch_params
|
def trends_preview_card_batch_params
|
||||||
params.require(:form_preview_card_batch).permit(:action, preview_card_ids: [])
|
params.require(:trends_preview_card_batch).permit(:action, preview_card_ids: [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def action_from_button
|
def action_from_button
|
||||||
if params[:approve]
|
if params[:approve]
|
||||||
'approve'
|
'approve'
|
||||||
elsif params[:approve_all]
|
elsif params[:approve_providers]
|
||||||
'approve_all'
|
'approve_providers'
|
||||||
elsif params[:reject]
|
elsif params[:reject]
|
||||||
'reject'
|
'reject'
|
||||||
elsif params[:reject_all]
|
elsif params[:reject_providers]
|
||||||
'reject_all'
|
'reject_providers'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
45
app/controllers/admin/trends/statuses_controller.rb
Normal file
45
app/controllers/admin/trends/statuses_controller.rb
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Trends::StatusesController < Admin::BaseController
|
||||||
|
def index
|
||||||
|
authorize :status, :index?
|
||||||
|
|
||||||
|
@statuses = filtered_statuses.page(params[:page])
|
||||||
|
@form = Trends::StatusBatch.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def batch
|
||||||
|
@form = Trends::StatusBatch.new(trends_status_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
|
@form.save
|
||||||
|
rescue ActionController::ParameterMissing
|
||||||
|
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||||
|
ensure
|
||||||
|
redirect_to admin_trends_statuses_path(filter_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def filtered_statuses
|
||||||
|
Trends::StatusFilter.new(filter_params.with_defaults(trending: 'all')).results.includes(:account, :media_attachments, :active_mentions)
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_params
|
||||||
|
params.slice(:page, *Trends::StatusFilter::KEYS).permit(:page, *Trends::StatusFilter::KEYS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def trends_status_batch_params
|
||||||
|
params.require(:trends_status_batch).permit(:action, status_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_from_button
|
||||||
|
if params[:approve]
|
||||||
|
'approve'
|
||||||
|
elsif params[:approve_accounts]
|
||||||
|
'approve_accounts'
|
||||||
|
elsif params[:reject]
|
||||||
|
'reject'
|
||||||
|
elsif params[:reject_accounts]
|
||||||
|
'reject_accounts'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -5,11 +5,11 @@ class Admin::Trends::TagsController < Admin::BaseController
|
|||||||
authorize :tag, :index?
|
authorize :tag, :index?
|
||||||
|
|
||||||
@tags = filtered_tags.page(params[:page])
|
@tags = filtered_tags.page(params[:page])
|
||||||
@form = Form::TagBatch.new
|
@form = Trends::TagBatch.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def batch
|
def batch
|
||||||
@form = Form::TagBatch.new(form_tag_batch_params.merge(current_account: current_account, action: action_from_button))
|
@form = Trends::TagBatch.new(trends_tag_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
@form.save
|
@form.save
|
||||||
rescue ActionController::ParameterMissing
|
rescue ActionController::ParameterMissing
|
||||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||||
@ -20,15 +20,15 @@ class Admin::Trends::TagsController < Admin::BaseController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def filtered_tags
|
def filtered_tags
|
||||||
TagFilter.new(filter_params).results
|
Trends::TagFilter.new(filter_params).results
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_params
|
def filter_params
|
||||||
params.slice(:page, *TagFilter::KEYS).permit(:page, *TagFilter::KEYS)
|
params.slice(:page, *Trends::TagFilter::KEYS).permit(:page, *Trends::TagFilter::KEYS)
|
||||||
end
|
end
|
||||||
|
|
||||||
def form_tag_batch_params
|
def trends_tag_batch_params
|
||||||
params.require(:form_tag_batch).permit(:action, tag_ids: [])
|
params.require(:trends_tag_batch).permit(:action, tag_ids: [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def action_from_button
|
def action_from_button
|
||||||
|
19
app/controllers/api/v1/admin/trends/links_controller.rb
Normal file
19
app/controllers/api/v1/admin/trends/links_controller.rb
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Admin::Trends::LinksController < Api::BaseController
|
||||||
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
|
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||||
|
before_action :require_staff!
|
||||||
|
before_action :set_links
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @links, each_serializer: REST::Trends::LinkSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_links
|
||||||
|
@links = Trends.links.query.limit(limit_param(10))
|
||||||
|
end
|
||||||
|
end
|
19
app/controllers/api/v1/admin/trends/statuses_controller.rb
Normal file
19
app/controllers/api/v1/admin/trends/statuses_controller.rb
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Admin::Trends::StatusesController < Api::BaseController
|
||||||
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
|
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||||
|
before_action :require_staff!
|
||||||
|
before_action :set_statuses
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @statuses, each_serializer: REST::StatusSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_statuses
|
||||||
|
@statuses = cache_collection(Trends.statuses.query.limit(limit_param(DEFAULT_STATUSES_LIMIT)), Status)
|
||||||
|
end
|
||||||
|
end
|
@ -14,6 +14,6 @@ class Api::V1::Admin::Trends::TagsController < Api::BaseController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def set_tags
|
def set_tags
|
||||||
@tags = Trends.tags.get(false, limit_param(10))
|
@tags = Trends.tags.query.limit(limit_param(10))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -20,7 +20,7 @@ class Api::V1::MediaController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@media_attachment.update!(media_attachment_params)
|
@media_attachment.update!(updateable_media_attachment_params)
|
||||||
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
|
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -42,6 +42,10 @@ class Api::V1::MediaController < Api::BaseController
|
|||||||
params.permit(:file, :thumbnail, :description, :focus)
|
params.permit(:file, :thumbnail, :description, :focus)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def updateable_media_attachment_params
|
||||||
|
params.permit(:thumbnail, :description, :focus)
|
||||||
|
end
|
||||||
|
|
||||||
def file_type_error
|
def file_type_error
|
||||||
{ error: 'File type of uploaded media could not be verified' }
|
{ error: 'File type of uploaded media could not be verified' }
|
||||||
end
|
end
|
||||||
|
@ -33,6 +33,6 @@ class Api::V1::ReportsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def report_params
|
def report_params
|
||||||
params.permit(:account_id, :comment, :forward, status_ids: [])
|
params.permit(:account_id, :comment, :category, :forward, status_ids: [], rule_ids: [])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -7,7 +7,7 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController
|
|||||||
before_action :set_status
|
before_action :set_status
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: @status.edits, each_serializer: REST::StatusEditSerializer
|
render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
class Api::V1::StatusesController < Api::BaseController
|
class Api::V1::StatusesController < Api::BaseController
|
||||||
include Authorization
|
include Authorization
|
||||||
|
|
||||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy]
|
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy]
|
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
|
||||||
before_action :require_user!, except: [:show, :context]
|
before_action :require_user!, except: [:show, :context]
|
||||||
before_action :set_status, only: [:show, :context]
|
before_action :set_status, only: [:show, :context]
|
||||||
before_action :set_thread, only: [:create]
|
before_action :set_thread, only: [:create]
|
||||||
@ -38,25 +38,45 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@status = PostStatusService.new.call(current_user.account,
|
@status = PostStatusService.new.call(
|
||||||
|
current_user.account,
|
||||||
text: status_params[:status],
|
text: status_params[:status],
|
||||||
thread: @thread,
|
thread: @thread,
|
||||||
media_ids: status_params[:media_ids],
|
media_ids: status_params[:media_ids],
|
||||||
sensitive: status_params[:sensitive],
|
sensitive: status_params[:sensitive],
|
||||||
spoiler_text: status_params[:spoiler_text],
|
spoiler_text: status_params[:spoiler_text],
|
||||||
visibility: status_params[:visibility],
|
visibility: status_params[:visibility],
|
||||||
|
language: status_params[:language],
|
||||||
scheduled_at: status_params[:scheduled_at],
|
scheduled_at: status_params[:scheduled_at],
|
||||||
application: doorkeeper_token.application,
|
application: doorkeeper_token.application,
|
||||||
poll: status_params[:poll],
|
poll: status_params[:poll],
|
||||||
idempotency: request.headers['Idempotency-Key'],
|
idempotency: request.headers['Idempotency-Key'],
|
||||||
with_rate_limit: true,
|
with_rate_limit: true,
|
||||||
quote_id: status_params[:quote_id].presence)
|
quote_id: status_params[:quote_id].presence,
|
||||||
|
)
|
||||||
|
|
||||||
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
|
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@status = Status.where(account: current_account).find(params[:id])
|
||||||
|
authorize @status, :update?
|
||||||
|
|
||||||
|
UpdateStatusService.new.call(
|
||||||
|
@status,
|
||||||
|
current_account.id,
|
||||||
|
text: status_params[:status],
|
||||||
|
media_ids: status_params[:media_ids],
|
||||||
|
sensitive: status_params[:sensitive],
|
||||||
|
spoiler_text: status_params[:spoiler_text],
|
||||||
|
poll: status_params[:poll]
|
||||||
|
)
|
||||||
|
|
||||||
|
render json: @status, serializer: REST::StatusSerializer
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@status = Status.where(account_id: current_user.account).find(params[:id])
|
@status = Status.where(account: current_account).find(params[:id])
|
||||||
authorize @status, :destroy?
|
authorize @status, :destroy?
|
||||||
|
|
||||||
@status.discard
|
@status.discard
|
||||||
@ -88,6 +108,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
:sensitive,
|
:sensitive,
|
||||||
:spoiler_text,
|
:spoiler_text,
|
||||||
:visibility,
|
:visibility,
|
||||||
|
:language,
|
||||||
:scheduled_at,
|
:scheduled_at,
|
||||||
:quote_id,
|
:quote_id,
|
||||||
media_ids: [],
|
media_ids: [],
|
||||||
|
@ -12,10 +12,14 @@ class Api::V1::Trends::LinksController < Api::BaseController
|
|||||||
def set_links
|
def set_links
|
||||||
@links = begin
|
@links = begin
|
||||||
if Setting.trends
|
if Setting.trends
|
||||||
Trends.links.get(true, limit_param(10))
|
links_from_trends
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def links_from_trends
|
||||||
|
Trends.links.query.allowed.in_locale(content_locale).limit(limit_param(10))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
27
app/controllers/api/v1/trends/statuses_controller.rb
Normal file
27
app/controllers/api/v1/trends/statuses_controller.rb
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Trends::StatusesController < Api::BaseController
|
||||||
|
before_action :set_statuses
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @statuses, each_serializer: REST::StatusSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_statuses
|
||||||
|
@statuses = begin
|
||||||
|
if Setting.trends
|
||||||
|
cache_collection(statuses_from_trends, Status)
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def statuses_from_trends
|
||||||
|
scope = Trends.statuses.query.allowed.in_locale(content_locale)
|
||||||
|
scope = scope.filtered_for(current_account) if user_signed_in?
|
||||||
|
scope.limit(limit_param(DEFAULT_STATUSES_LIMIT))
|
||||||
|
end
|
||||||
|
end
|
@ -12,7 +12,7 @@ class Api::V1::Trends::TagsController < Api::BaseController
|
|||||||
def set_tags
|
def set_tags
|
||||||
@tags = begin
|
@tags = begin
|
||||||
if Setting.trends
|
if Setting.trends
|
||||||
Trends.tags.get(true, limit_param(10))
|
Trends.tags.query.allowed.limit(limit_param(10))
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
@ -17,16 +17,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||||||
|
|
||||||
data = {
|
data = {
|
||||||
policy: 'all',
|
policy: 'all',
|
||||||
|
alerts: Notification::TYPES.index_with { alerts_enabled },
|
||||||
alerts: {
|
|
||||||
follow: alerts_enabled,
|
|
||||||
follow_request: alerts_enabled,
|
|
||||||
favourite: alerts_enabled,
|
|
||||||
reblog: alerts_enabled,
|
|
||||||
mention: alerts_enabled,
|
|
||||||
poll: alerts_enabled,
|
|
||||||
status: alerts_enabled,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data.deep_merge!(data_params) if params[:data]
|
data.deep_merge!(data_params) if params[:data]
|
||||||
@ -61,6 +52,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def data_params
|
def data_params
|
||||||
@data_params ||= params.require(:data).permit(:policy, alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
|
@data_params ||= params.require(:data).permit(:policy, alerts: Notification::TYPES)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -9,6 +9,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||||||
before_action :check_enabled_registrations, only: [:new, :create]
|
before_action :check_enabled_registrations, only: [:new, :create]
|
||||||
before_action :configure_sign_up_params, only: [:create]
|
before_action :configure_sign_up_params, only: [:create]
|
||||||
before_action :set_sessions, only: [:edit, :update]
|
before_action :set_sessions, only: [:edit, :update]
|
||||||
|
before_action :set_strikes, only: [:edit, :update]
|
||||||
before_action :set_instance_presenter, only: [:new, :create, :update]
|
before_action :set_instance_presenter, only: [:new, :create, :update]
|
||||||
before_action :set_body_classes, only: [:new, :create, :edit, :update]
|
before_action :set_body_classes, only: [:new, :create, :edit, :update]
|
||||||
before_action :require_not_suspended!, only: [:update]
|
before_action :require_not_suspended!, only: [:update]
|
||||||
@ -111,8 +112,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_invite
|
def set_invite
|
||||||
invite = invite_code.present? ? Invite.find_by(code: invite_code) : nil
|
@invite = begin
|
||||||
@invite = invite&.valid_for_use? ? invite : nil
|
invite = Invite.find_by(code: invite_code) if invite_code.present?
|
||||||
|
invite if invite&.valid_for_use?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def determine_layout
|
def determine_layout
|
||||||
@ -123,6 +126,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||||||
@sessions = current_user.session_activations
|
@sessions = current_user.session_activations
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_strikes
|
||||||
|
@strikes = current_account.strikes.active.latest
|
||||||
|
end
|
||||||
|
|
||||||
def require_not_suspended!
|
def require_not_suspended!
|
||||||
forbidden if current_account.suspended?
|
forbidden if current_account.suspended?
|
||||||
end
|
end
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
module Authorization
|
module Authorization
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
include Pundit
|
include Pundit::Authorization
|
||||||
|
|
||||||
def pundit_user
|
def pundit_user
|
||||||
current_account
|
current_account
|
||||||
|
@ -7,27 +7,28 @@ module Localized
|
|||||||
around_action :set_locale
|
around_action :set_locale
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_locale
|
def set_locale(&block)
|
||||||
locale = current_user.locale if respond_to?(:user_signed_in?) && user_signed_in?
|
I18n.with_locale(requested_locale || I18n.default_locale, &block)
|
||||||
locale ||= session[:locale] ||= default_locale
|
|
||||||
locale = default_locale unless I18n.available_locales.include?(locale.to_sym)
|
|
||||||
|
|
||||||
I18n.with_locale(locale) do
|
|
||||||
yield
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def default_locale
|
def requested_locale
|
||||||
if ENV['DEFAULT_LOCALE'].present?
|
requested_locale_name = available_locale_or_nil(params[:lang])
|
||||||
I18n.default_locale
|
requested_locale_name ||= available_locale_or_nil(current_user.locale) if respond_to?(:user_signed_in?) && user_signed_in?
|
||||||
else
|
requested_locale_name ||= http_accept_language if ENV['DEFAULT_LOCALE'].blank?
|
||||||
request_locale || I18n.default_locale
|
requested_locale_name
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def request_locale
|
def http_accept_language
|
||||||
http_accept_language.language_region_compatible_from(I18n.available_locales)
|
HttpAcceptLanguage::Parser.new(request.headers.fetch('Accept-Language')).language_region_compatible_from(I18n.available_locales) if request.headers.key?('Accept-Language')
|
||||||
|
end
|
||||||
|
|
||||||
|
def available_locale_or_nil(locale_name)
|
||||||
|
locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym)
|
||||||
|
end
|
||||||
|
|
||||||
|
def content_locale
|
||||||
|
@content_locale ||= I18n.locale.to_s.split(/[_-]/).first
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
26
app/controllers/disputes/appeals_controller.rb
Normal file
26
app/controllers/disputes/appeals_controller.rb
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Disputes::AppealsController < Disputes::BaseController
|
||||||
|
before_action :set_strike
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize @strike, :appeal?
|
||||||
|
|
||||||
|
@appeal = AppealService.new.call(@strike, appeal_params[:text])
|
||||||
|
|
||||||
|
redirect_to disputes_strike_path(@strike), notice: I18n.t('disputes.strikes.appealed_msg')
|
||||||
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
@appeal = e.record
|
||||||
|
render template: 'disputes/strikes/show'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_strike
|
||||||
|
@strike = current_account.strikes.find(params[:strike_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def appeal_params
|
||||||
|
params.require(:appeal).permit(:text)
|
||||||
|
end
|
||||||
|
end
|
18
app/controllers/disputes/base_controller.rb
Normal file
18
app/controllers/disputes/base_controller.rb
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Disputes::BaseController < ApplicationController
|
||||||
|
include Authorization
|
||||||
|
|
||||||
|
layout 'admin'
|
||||||
|
|
||||||
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
|
before_action :set_body_classes
|
||||||
|
before_action :authenticate_user!
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_body_classes
|
||||||
|
@body_classes = 'admin'
|
||||||
|
end
|
||||||
|
end
|
17
app/controllers/disputes/strikes_controller.rb
Normal file
17
app/controllers/disputes/strikes_controller.rb
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Disputes::StrikesController < Disputes::BaseController
|
||||||
|
before_action :set_strike
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize @strike, :show?
|
||||||
|
|
||||||
|
@appeal = @strike.appeal || @strike.build_appeal
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_strike
|
||||||
|
@strike = AccountWarning.find(params[:id])
|
||||||
|
end
|
||||||
|
end
|
@ -3,6 +3,7 @@
|
|||||||
class InstanceActorsController < ApplicationController
|
class InstanceActorsController < ApplicationController
|
||||||
include AccountControllerConcern
|
include AccountControllerConcern
|
||||||
|
|
||||||
|
skip_before_action :check_account_confirmation
|
||||||
skip_around_action :set_locale
|
skip_around_action :set_locale
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Admin::AccountModerationNotesHelper
|
module Admin::AccountModerationNotesHelper
|
||||||
def admin_account_link_to(account)
|
def admin_account_link_to(account, path: nil)
|
||||||
return if account.nil?
|
return if account.nil?
|
||||||
|
|
||||||
link_to admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do
|
link_to path || admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do
|
||||||
safe_join([
|
safe_join([
|
||||||
image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'),
|
image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'),
|
||||||
content_tag(:span, account.acct, class: 'username'),
|
content_tag(:span, account.acct, class: 'username'),
|
||||||
|
@ -33,6 +33,8 @@ module Admin::ActionLogsHelper
|
|||||||
"#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})"
|
"#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})"
|
||||||
when 'Instance'
|
when 'Instance'
|
||||||
record.domain
|
record.domain
|
||||||
|
when 'Appeal'
|
||||||
|
link_to record.account.acct, disputes_strike_path(record.strike)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -5,9 +5,10 @@ module Admin::FilterHelper
|
|||||||
AccountFilter::KEYS,
|
AccountFilter::KEYS,
|
||||||
CustomEmojiFilter::KEYS,
|
CustomEmojiFilter::KEYS,
|
||||||
ReportFilter::KEYS,
|
ReportFilter::KEYS,
|
||||||
TagFilter::KEYS,
|
Trends::TagFilter::KEYS,
|
||||||
PreviewCardProviderFilter::KEYS,
|
Trends::PreviewCardProviderFilter::KEYS,
|
||||||
PreviewCardFilter::KEYS,
|
Trends::PreviewCardFilter::KEYS,
|
||||||
|
Trends::StatusFilter::KEYS,
|
||||||
InstanceFilter::KEYS,
|
InstanceFilter::KEYS,
|
||||||
InviteFilter::KEYS,
|
InviteFilter::KEYS,
|
||||||
RelationshipFilter::KEYS,
|
RelationshipFilter::KEYS,
|
||||||
|
20
app/helpers/admin/trends/statuses_helper.rb
Normal file
20
app/helpers/admin/trends/statuses_helper.rb
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin::Trends::StatusesHelper
|
||||||
|
def one_line_preview(status)
|
||||||
|
text = begin
|
||||||
|
if status.local?
|
||||||
|
status.text.split("\n").first
|
||||||
|
else
|
||||||
|
Nokogiri::HTML(status.text).css('html > body > *').first&.text
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return '' if text.blank?
|
||||||
|
|
||||||
|
html = Formatter.instance.send(:encode, text)
|
||||||
|
html = Formatter.instance.send(:encode_custom_emojis, html, status.emojis, prefers_autoplay?)
|
||||||
|
|
||||||
|
html.html_safe # rubocop:disable Rails/OutputSafety
|
||||||
|
end
|
||||||
|
end
|
@ -1,94 +1,247 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module LanguagesHelper
|
module LanguagesHelper
|
||||||
HUMAN_LOCALES = {
|
ISO_639_1 = {
|
||||||
af: 'Afrikaans',
|
aa: ['Afar', 'Afaraf'].freeze,
|
||||||
ar: 'العربية',
|
ab: ['Abkhaz', 'аҧсуа бызшәа'].freeze,
|
||||||
ast: 'Asturianu',
|
ae: ['Avestan', 'avesta'].freeze,
|
||||||
bg: 'Български',
|
af: ['Afrikaans', 'Afrikaans'].freeze,
|
||||||
bn: 'বাংলা',
|
ak: ['Akan', 'Akan'].freeze,
|
||||||
br: 'Breton',
|
am: ['Amharic', 'አማርኛ'].freeze,
|
||||||
ca: 'Català',
|
an: ['Aragonese', 'aragonés'].freeze,
|
||||||
co: 'Corsu',
|
ar: ['Arabic', 'اللغة العربية'].freeze,
|
||||||
cs: 'Čeština',
|
as: ['Assamese', 'অসমীয়া'].freeze,
|
||||||
cy: 'Cymraeg',
|
av: ['Avaric', 'авар мацӀ'].freeze,
|
||||||
da: 'Dansk',
|
ay: ['Aymara', 'aymar aru'].freeze,
|
||||||
de: 'Deutsch',
|
az: ['Azerbaijani', 'azərbaycan dili'].freeze,
|
||||||
el: 'Ελληνικά',
|
ba: ['Bashkir', 'башҡорт теле'].freeze,
|
||||||
en: 'English',
|
be: ['Belarusian', 'беларуская мова'].freeze,
|
||||||
eo: 'Esperanto',
|
bg: ['Bulgarian', 'български език'].freeze,
|
||||||
|
bh: ['Bihari', 'भोजपुरी'].freeze,
|
||||||
|
bi: ['Bislama', 'Bislama'].freeze,
|
||||||
|
bm: ['Bambara', 'bamanankan'].freeze,
|
||||||
|
bn: ['Bengali', 'বাংলা'].freeze,
|
||||||
|
bo: ['Tibetan', 'བོད་ཡིག'].freeze,
|
||||||
|
br: ['Breton', 'brezhoneg'].freeze,
|
||||||
|
bs: ['Bosnian', 'bosanski jezik'].freeze,
|
||||||
|
ca: ['Catalan', 'Català'].freeze,
|
||||||
|
ce: ['Chechen', 'нохчийн мотт'].freeze,
|
||||||
|
ch: ['Chamorro', 'Chamoru'].freeze,
|
||||||
|
co: ['Corsican', 'corsu'].freeze,
|
||||||
|
cr: ['Cree', 'ᓀᐦᐃᔭᐍᐏᐣ'].freeze,
|
||||||
|
cs: ['Czech', 'čeština'].freeze,
|
||||||
|
cu: ['Old Church Slavonic', 'ѩзыкъ словѣньскъ'].freeze,
|
||||||
|
cv: ['Chuvash', 'чӑваш чӗлхи'].freeze,
|
||||||
|
cy: ['Welsh', 'Cymraeg'].freeze,
|
||||||
|
da: ['Danish', 'dansk'].freeze,
|
||||||
|
de: ['German', 'Deutsch'].freeze,
|
||||||
|
dv: ['Divehi', 'Dhivehi'].freeze,
|
||||||
|
dz: ['Dzongkha', 'རྫོང་ཁ'].freeze,
|
||||||
|
ee: ['Ewe', 'Eʋegbe'].freeze,
|
||||||
|
el: ['Greek', 'Ελληνικά'].freeze,
|
||||||
|
en: ['English', 'English'].freeze,
|
||||||
|
eo: ['Esperanto', 'Esperanto'].freeze,
|
||||||
|
es: ['Spanish', 'Español'].freeze,
|
||||||
|
et: ['Estonian', 'eesti'].freeze,
|
||||||
|
eu: ['Basque', 'euskara'].freeze,
|
||||||
|
fa: ['Persian', 'فارسی'].freeze,
|
||||||
|
ff: ['Fula', 'Fulfulde'].freeze,
|
||||||
|
fi: ['Finnish', 'suomi'].freeze,
|
||||||
|
fj: ['Fijian', 'Vakaviti'].freeze,
|
||||||
|
fo: ['Faroese', 'føroyskt'].freeze,
|
||||||
|
fr: ['French', 'Français'].freeze,
|
||||||
|
fy: ['Western Frisian', 'Frysk'].freeze,
|
||||||
|
ga: ['Irish', 'Gaeilge'].freeze,
|
||||||
|
gd: ['Scottish Gaelic', 'Gàidhlig'].freeze,
|
||||||
|
gl: ['Galician', 'galego'].freeze,
|
||||||
|
gu: ['Gujarati', 'ગુજરાતી'].freeze,
|
||||||
|
gv: ['Manx', 'Gaelg'].freeze,
|
||||||
|
ha: ['Hausa', 'هَوُسَ'].freeze,
|
||||||
|
he: ['Hebrew', 'עברית'].freeze,
|
||||||
|
hi: ['Hindi', 'हिन्दी'].freeze,
|
||||||
|
ho: ['Hiri Motu', 'Hiri Motu'].freeze,
|
||||||
|
hr: ['Croatian', 'Hrvatski'].freeze,
|
||||||
|
ht: ['Haitian', 'Kreyòl ayisyen'].freeze,
|
||||||
|
hu: ['Hungarian', 'magyar'].freeze,
|
||||||
|
hy: ['Armenian', 'Հայերեն'].freeze,
|
||||||
|
hz: ['Herero', 'Otjiherero'].freeze,
|
||||||
|
ia: ['Interlingua', 'Interlingua'].freeze,
|
||||||
|
id: ['Indonesian', 'Bahasa Indonesia'].freeze,
|
||||||
|
ie: ['Interlingue', 'Interlingue'].freeze,
|
||||||
|
ig: ['Igbo', 'Asụsụ Igbo'].freeze,
|
||||||
|
ii: ['Nuosu', 'ꆈꌠ꒿ Nuosuhxop'].freeze,
|
||||||
|
ik: ['Inupiaq', 'Iñupiaq'].freeze,
|
||||||
|
io: ['Ido', 'Ido'].freeze,
|
||||||
|
is: ['Icelandic', 'Íslenska'].freeze,
|
||||||
|
it: ['Italian', 'Italiano'].freeze,
|
||||||
|
iu: ['Inuktitut', 'ᐃᓄᒃᑎᑐᑦ'].freeze,
|
||||||
|
ja: ['Japanese', '日本語'].freeze,
|
||||||
|
jv: ['Javanese', 'basa Jawa'].freeze,
|
||||||
|
ka: ['Georgian', 'ქართული'].freeze,
|
||||||
|
kg: ['Kongo', 'Kikongo'].freeze,
|
||||||
|
ki: ['Kikuyu', 'Gĩkũyũ'].freeze,
|
||||||
|
kj: ['Kwanyama', 'Kuanyama'].freeze,
|
||||||
|
kk: ['Kazakh', 'қазақ тілі'].freeze,
|
||||||
|
kl: ['Kalaallisut', 'kalaallisut'].freeze,
|
||||||
|
km: ['Khmer', 'ខេមរភាសា'].freeze,
|
||||||
|
kn: ['Kannada', 'ಕನ್ನಡ'].freeze,
|
||||||
|
ko: ['Korean', '한국어'].freeze,
|
||||||
|
kr: ['Kanuri', 'Kanuri'].freeze,
|
||||||
|
ks: ['Kashmiri', 'कश्मीरी'].freeze,
|
||||||
|
ku: ['Kurdish', 'Kurdî'].freeze,
|
||||||
|
kv: ['Komi', 'коми кыв'].freeze,
|
||||||
|
kw: ['Cornish', 'Kernewek'].freeze,
|
||||||
|
ky: ['Kyrgyz', 'Кыргызча'].freeze,
|
||||||
|
la: ['Latin', 'latine'].freeze,
|
||||||
|
lb: ['Luxembourgish', 'Lëtzebuergesch'].freeze,
|
||||||
|
lg: ['Ganda', 'Luganda'].freeze,
|
||||||
|
li: ['Limburgish', 'Limburgs'].freeze,
|
||||||
|
ln: ['Lingala', 'Lingála'].freeze,
|
||||||
|
lo: ['Lao', 'ພາສາ'].freeze,
|
||||||
|
lt: ['Lithuanian', 'lietuvių kalba'].freeze,
|
||||||
|
lu: ['Luba-Katanga', 'Tshiluba'].freeze,
|
||||||
|
lv: ['Latvian', 'latviešu valoda'].freeze,
|
||||||
|
mg: ['Malagasy', 'fiteny malagasy'].freeze,
|
||||||
|
mh: ['Marshallese', 'Kajin M̧ajeļ'].freeze,
|
||||||
|
mi: ['Māori', 'te reo Māori'].freeze,
|
||||||
|
mk: ['Macedonian', 'македонски јазик'].freeze,
|
||||||
|
ml: ['Malayalam', 'മലയാളം'].freeze,
|
||||||
|
mn: ['Mongolian', 'Монгол хэл'].freeze,
|
||||||
|
mr: ['Marathi', 'मराठी'].freeze,
|
||||||
|
ms: ['Malay', 'Bahasa Malaysia'].freeze,
|
||||||
|
mt: ['Maltese', 'Malti'].freeze,
|
||||||
|
my: ['Burmese', 'ဗမာစာ'].freeze,
|
||||||
|
na: ['Nauru', 'Ekakairũ Naoero'].freeze,
|
||||||
|
nb: ['Norwegian Bokmål', 'Norsk bokmål'].freeze,
|
||||||
|
nd: ['Northern Ndebele', 'isiNdebele'].freeze,
|
||||||
|
ne: ['Nepali', 'नेपाली'].freeze,
|
||||||
|
ng: ['Ndonga', 'Owambo'].freeze,
|
||||||
|
nl: ['Dutch', 'Nederlands'].freeze,
|
||||||
|
nn: ['Norwegian Nynorsk', 'Norsk nynorsk'].freeze,
|
||||||
|
no: ['Norwegian', 'Norsk'].freeze,
|
||||||
|
nr: ['Southern Ndebele', 'isiNdebele'].freeze,
|
||||||
|
nv: ['Navajo', 'Diné bizaad'].freeze,
|
||||||
|
ny: ['Chichewa', 'chiCheŵa'].freeze,
|
||||||
|
oc: ['Occitan', 'occitan'].freeze,
|
||||||
|
oj: ['Ojibwe', 'ᐊᓂᔑᓈᐯᒧᐎᓐ'].freeze,
|
||||||
|
om: ['Oromo', 'Afaan Oromoo'].freeze,
|
||||||
|
or: ['Oriya', 'ଓଡ଼ିଆ'].freeze,
|
||||||
|
os: ['Ossetian', 'ирон æвзаг'].freeze,
|
||||||
|
pa: ['Panjabi', 'ਪੰਜਾਬੀ'].freeze,
|
||||||
|
pi: ['Pāli', 'पाऴि'].freeze,
|
||||||
|
pl: ['Polish', 'Polski'].freeze,
|
||||||
|
ps: ['Pashto', 'پښتو'].freeze,
|
||||||
|
pt: ['Portuguese', 'Português'].freeze,
|
||||||
|
qu: ['Quechua', 'Runa Simi'].freeze,
|
||||||
|
rm: ['Romansh', 'rumantsch grischun'].freeze,
|
||||||
|
rn: ['Kirundi', 'Ikirundi'].freeze,
|
||||||
|
ro: ['Romanian', 'Română'].freeze,
|
||||||
|
ru: ['Russian', 'Русский'].freeze,
|
||||||
|
rw: ['Kinyarwanda', 'Ikinyarwanda'].freeze,
|
||||||
|
sa: ['Sanskrit', 'संस्कृतम्'].freeze,
|
||||||
|
sc: ['Sardinian', 'sardu'].freeze,
|
||||||
|
sd: ['Sindhi', 'सिन्धी'].freeze,
|
||||||
|
se: ['Northern Sami', 'Davvisámegiella'].freeze,
|
||||||
|
sg: ['Sango', 'yângâ tî sängö'].freeze,
|
||||||
|
si: ['Sinhala', 'සිංහල'].freeze,
|
||||||
|
sk: ['Slovak', 'slovenčina'].freeze,
|
||||||
|
sl: ['Slovenian', 'slovenščina'].freeze,
|
||||||
|
sn: ['Shona', 'chiShona'].freeze,
|
||||||
|
so: ['Somali', 'Soomaaliga'].freeze,
|
||||||
|
sq: ['Albanian', 'Shqip'].freeze,
|
||||||
|
sr: ['Serbian', 'српски језик'].freeze,
|
||||||
|
ss: ['Swati', 'SiSwati'].freeze,
|
||||||
|
st: ['Southern Sotho', 'Sesotho'].freeze,
|
||||||
|
su: ['Sundanese', 'Basa Sunda'].freeze,
|
||||||
|
sv: ['Swedish', 'Svenska'].freeze,
|
||||||
|
sw: ['Swahili', 'Kiswahili'].freeze,
|
||||||
|
ta: ['Tamil', 'தமிழ்'].freeze,
|
||||||
|
te: ['Telugu', 'తెలుగు'].freeze,
|
||||||
|
tg: ['Tajik', 'тоҷикӣ'].freeze,
|
||||||
|
th: ['Thai', 'ไทย'].freeze,
|
||||||
|
ti: ['Tigrinya', 'ትግርኛ'].freeze,
|
||||||
|
tk: ['Turkmen', 'Türkmen'].freeze,
|
||||||
|
tl: ['Tagalog', 'Wikang Tagalog'].freeze,
|
||||||
|
tn: ['Tswana', 'Setswana'].freeze,
|
||||||
|
to: ['Tonga', 'faka Tonga'].freeze,
|
||||||
|
tr: ['Turkish', 'Türkçe'].freeze,
|
||||||
|
ts: ['Tsonga', 'Xitsonga'].freeze,
|
||||||
|
tt: ['Tatar', 'татар теле'].freeze,
|
||||||
|
tw: ['Twi', 'Twi'].freeze,
|
||||||
|
ty: ['Tahitian', 'Reo Tahiti'].freeze,
|
||||||
|
ug: ['Uyghur', 'ئۇيغۇرچە'].freeze,
|
||||||
|
uk: ['Ukrainian', 'Українська'].freeze,
|
||||||
|
ur: ['Urdu', 'اردو'].freeze,
|
||||||
|
uz: ['Uzbek', 'Ўзбек'].freeze,
|
||||||
|
ve: ['Venda', 'Tshivenḓa'].freeze,
|
||||||
|
vi: ['Vietnamese', 'Tiếng Việt'].freeze,
|
||||||
|
vo: ['Volapük', 'Volapük'].freeze,
|
||||||
|
wa: ['Walloon', 'walon'].freeze,
|
||||||
|
wo: ['Wolof', 'Wollof'].freeze,
|
||||||
|
xh: ['Xhosa', 'isiXhosa'].freeze,
|
||||||
|
yi: ['Yiddish', 'ייִדיש'].freeze,
|
||||||
|
yo: ['Yoruba', 'Yorùbá'].freeze,
|
||||||
|
za: ['Zhuang', 'Saɯ cueŋƅ'].freeze,
|
||||||
|
zh: ['Chinese', '中文'].freeze,
|
||||||
|
zu: ['Zulu', 'isiZulu'].freeze,
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
ISO_639_3 = {
|
||||||
|
ast: ['Asturian', 'Asturianu'].freeze,
|
||||||
|
kab: ['Kabyle', 'Taqbaylit'].freeze,
|
||||||
|
kmr: ['Northern Kurdish', 'Kurmancî'].freeze,
|
||||||
|
zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze,
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
SUPPORTED_LOCALES = {}.merge(ISO_639_1).merge(ISO_639_3).freeze
|
||||||
|
|
||||||
|
# For ISO-639-1 and ISO-639-3 language codes, we have their official
|
||||||
|
# names, but for some translations, we need the names of the
|
||||||
|
# regional variants specifically
|
||||||
|
REGIONAL_LOCALE_NAMES = {
|
||||||
'es-AR': 'Español (Argentina)',
|
'es-AR': 'Español (Argentina)',
|
||||||
'es-MX': 'Español (México)',
|
'es-MX': 'Español (México)',
|
||||||
es: 'Español',
|
|
||||||
et: 'Eesti',
|
|
||||||
eu: 'Euskara',
|
|
||||||
fa: 'فارسی',
|
|
||||||
fi: 'Suomi',
|
|
||||||
fr: 'Français',
|
|
||||||
ga: 'Gaeilge',
|
|
||||||
gd: 'Gàidhlig',
|
|
||||||
gl: 'Galego',
|
|
||||||
he: 'עברית',
|
|
||||||
hi: 'हिन्दी',
|
|
||||||
hr: 'Hrvatski',
|
|
||||||
hu: 'Magyar',
|
|
||||||
hy: 'Հայերեն',
|
|
||||||
id: 'Bahasa Indonesia',
|
|
||||||
io: 'Ido',
|
|
||||||
is: 'Íslenska',
|
|
||||||
it: 'Italiano',
|
|
||||||
ja: '日本語',
|
|
||||||
ka: 'ქართული',
|
|
||||||
kab: 'Taqbaylit',
|
|
||||||
kk: 'Қазақша',
|
|
||||||
kmr: 'Kurmancî',
|
|
||||||
kn: 'ಕನ್ನಡ',
|
|
||||||
ko: '한국어',
|
|
||||||
ku: 'سۆرانی',
|
|
||||||
lt: 'Lietuvių',
|
|
||||||
lv: 'Latviešu',
|
|
||||||
mk: 'Македонски',
|
|
||||||
ml: 'മലയാളം',
|
|
||||||
mr: 'मराठी',
|
|
||||||
ms: 'Bahasa Melayu',
|
|
||||||
nl: 'Nederlands',
|
|
||||||
nn: 'Nynorsk',
|
|
||||||
no: 'Norsk',
|
|
||||||
oc: 'Occitan',
|
|
||||||
pl: 'Polski',
|
|
||||||
'pt-BR': 'Português (Brasil)',
|
'pt-BR': 'Português (Brasil)',
|
||||||
'pt-PT': 'Português (Portugal)',
|
'pt-PT': 'Português (Portugal)',
|
||||||
pt: 'Português',
|
|
||||||
ro: 'Română',
|
|
||||||
ru: 'Русский',
|
|
||||||
sa: 'संस्कृतम्',
|
|
||||||
sc: 'Sardu',
|
|
||||||
si: 'සිංහල',
|
|
||||||
sk: 'Slovenčina',
|
|
||||||
sl: 'Slovenščina',
|
|
||||||
sq: 'Shqip',
|
|
||||||
'sr-Latn': 'Srpski (latinica)',
|
'sr-Latn': 'Srpski (latinica)',
|
||||||
sr: 'Српски',
|
|
||||||
sv: 'Svenska',
|
|
||||||
ta: 'தமிழ்',
|
|
||||||
te: 'తెలుగు',
|
|
||||||
th: 'ไทย',
|
|
||||||
tr: 'Türkçe',
|
|
||||||
uk: 'Українська',
|
|
||||||
ur: 'اُردُو',
|
|
||||||
vi: 'Tiếng Việt',
|
|
||||||
zgh: 'ⵜⴰⵎⴰⵣⵉⵖⵜ',
|
|
||||||
'zh-CN': '简体中文',
|
'zh-CN': '简体中文',
|
||||||
'zh-HK': '繁體中文(香港)',
|
'zh-HK': '繁體中文(香港)',
|
||||||
'zh-TW': '繁體中文(臺灣)',
|
'zh-TW': '繁體中文(臺灣)',
|
||||||
zh: '中文',
|
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
def human_locale(locale)
|
def native_locale_name(locale)
|
||||||
if locale == 'und'
|
if locale.blank? || locale == 'und'
|
||||||
I18n.t('generic.none')
|
I18n.t('generic.none')
|
||||||
|
elsif (supported_locale = SUPPORTED_LOCALES[locale.to_sym])
|
||||||
|
supported_locale[1]
|
||||||
|
elsif (regional_locale = REGIONAL_LOCALE_NAMES[locale.to_sym])
|
||||||
|
regional_locale
|
||||||
else
|
else
|
||||||
HUMAN_LOCALES[locale.to_sym] || locale
|
locale
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def standard_locale_name(locale)
|
||||||
|
if locale.blank?
|
||||||
|
I18n.t('generic.none')
|
||||||
|
elsif (supported_locale = SUPPORTED_LOCALES[locale.to_sym])
|
||||||
|
supported_locale[0]
|
||||||
|
else
|
||||||
|
locale
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_locale_or_nil(str)
|
||||||
|
return if str.blank?
|
||||||
|
|
||||||
|
code, = str.to_s.split(/[_-]/) # Strip out the region from e.g. en_US or ja-JP
|
||||||
|
|
||||||
|
return unless valid_locale?(code)
|
||||||
|
|
||||||
|
code
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_locale?(locale)
|
||||||
|
locale.present? && SUPPORTED_LOCALES.key?(locale.to_sym)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module SettingsHelper
|
module SettingsHelper
|
||||||
def filterable_languages
|
def filterable_languages
|
||||||
LanguageDetector.instance.language_names.select(&LanguagesHelper::HUMAN_LOCALES.method(:key?))
|
LanguagesHelper::SUPPORTED_LOCALES.keys
|
||||||
end
|
end
|
||||||
|
|
||||||
def hash_to_object(hash)
|
def hash_to_object(hash)
|
||||||
|
@ -72,6 +72,8 @@ export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL';
|
|||||||
export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION';
|
export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION';
|
||||||
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
|
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
|
||||||
|
|
||||||
|
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||||
@ -85,6 +87,15 @@ export const ensureComposeIsVisible = (getState, routerHistory) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function setComposeToStatus(status, text, spoiler_text) {
|
||||||
|
return{
|
||||||
|
type: COMPOSE_SET_STATUS,
|
||||||
|
status,
|
||||||
|
text,
|
||||||
|
spoiler_text,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function changeCompose(text) {
|
export function changeCompose(text) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_CHANGE,
|
type: COMPOSE_CHANGE,
|
||||||
@ -158,6 +169,7 @@ export function submitCompose(routerHistory) {
|
|||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
const status = getState().getIn(['compose', 'text'], '');
|
const status = getState().getIn(['compose', 'text'], '');
|
||||||
const media = getState().getIn(['compose', 'media_attachments']);
|
const media = getState().getIn(['compose', 'media_attachments']);
|
||||||
|
const statusId = getState().getIn(['compose', 'id'], null);
|
||||||
|
|
||||||
if ((!status || !status.length) && media.size === 0) {
|
if ((!status || !status.length) && media.size === 0) {
|
||||||
return;
|
return;
|
||||||
@ -165,7 +177,10 @@ export function submitCompose(routerHistory) {
|
|||||||
|
|
||||||
dispatch(submitComposeRequest());
|
dispatch(submitComposeRequest());
|
||||||
|
|
||||||
api(getState).post('/api/v1/statuses', {
|
api(getState).request({
|
||||||
|
url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
|
||||||
|
method: statusId === null ? 'post' : 'put',
|
||||||
|
data: {
|
||||||
status,
|
status,
|
||||||
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
||||||
media_ids: media.map(item => item.get('id')),
|
media_ids: media.map(item => item.get('id')),
|
||||||
@ -174,7 +189,7 @@ export function submitCompose(routerHistory) {
|
|||||||
visibility: getState().getIn(['compose', 'privacy']),
|
visibility: getState().getIn(['compose', 'privacy']),
|
||||||
poll: getState().getIn(['compose', 'poll'], null),
|
poll: getState().getIn(['compose', 'poll'], null),
|
||||||
quote_id: getState().getIn(['compose', 'quote_from'], null),
|
quote_id: getState().getIn(['compose', 'quote_from'], null),
|
||||||
}, {
|
},
|
||||||
headers: {
|
headers: {
|
||||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||||
},
|
},
|
||||||
@ -196,11 +211,11 @@ export function submitCompose(routerHistory) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (response.data.visibility !== 'direct') {
|
if (statusId === null && response.data.visibility !== 'direct') {
|
||||||
insertIfOnline('home');
|
insertIfOnline('home');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
|
if (statusId === null && response.data.in_reply_to_id === null && response.data.visibility === 'public') {
|
||||||
insertIfOnline('community');
|
insertIfOnline('community');
|
||||||
insertIfOnline('public');
|
insertIfOnline('public');
|
||||||
insertIfOnline(`account:${response.data.account.id}`);
|
insertIfOnline(`account:${response.data.account.id}`);
|
||||||
|
37
app/javascript/mastodon/actions/history.js
Normal file
37
app/javascript/mastodon/actions/history.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import api from '../api';
|
||||||
|
import { importFetchedAccounts } from './importer';
|
||||||
|
|
||||||
|
export const HISTORY_FETCH_REQUEST = 'HISTORY_FETCH_REQUEST';
|
||||||
|
export const HISTORY_FETCH_SUCCESS = 'HISTORY_FETCH_SUCCESS';
|
||||||
|
export const HISTORY_FETCH_FAIL = 'HISTORY_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const fetchHistory = statusId => (dispatch, getState) => {
|
||||||
|
const loading = getState().getIn(['history', statusId, 'loading']);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(fetchHistoryRequest(statusId));
|
||||||
|
|
||||||
|
api(getState).get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => {
|
||||||
|
dispatch(importFetchedAccounts(data.map(x => x.account)));
|
||||||
|
dispatch(fetchHistorySuccess(statusId, data));
|
||||||
|
}).catch(error => dispatch(fetchHistoryFail(error)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchHistoryRequest = statusId => ({
|
||||||
|
type: HISTORY_FETCH_REQUEST,
|
||||||
|
statusId,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchHistorySuccess = (statusId, history) => ({
|
||||||
|
type: HISTORY_FETCH_SUCCESS,
|
||||||
|
statusId,
|
||||||
|
history,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchHistoryFail = error => ({
|
||||||
|
type: HISTORY_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
@ -9,9 +9,10 @@ export function openModal(type, props) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function closeModal(type) {
|
export function closeModal(type, options = { ignoreFocus: false }) {
|
||||||
return {
|
return {
|
||||||
type: MODAL_CLOSE,
|
type: MODAL_CLOSE,
|
||||||
modalType: type,
|
modalType: type,
|
||||||
|
ignoreFocus: options.ignoreFocus,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -34,7 +34,6 @@ export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
|
|||||||
export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT';
|
export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT';
|
||||||
export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
|
export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
|
||||||
|
|
||||||
|
|
||||||
export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
|
export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
|
||||||
|
|
||||||
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
|
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
|
||||||
@ -46,7 +45,7 @@ defineMessages({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const fetchRelatedRelationships = (dispatch, notifications) => {
|
const fetchRelatedRelationships = (dispatch, notifications) => {
|
||||||
const accountIds = notifications.map(item => item.account.id);
|
const accountIds = notifications.filter(item => ['follow', 'follow_request', 'admin.sign_up'].indexOf(item.type) !== -1).map(item => item.account.id);
|
||||||
|
|
||||||
if (accountIds.length > 0) {
|
if (accountIds.length > 0) {
|
||||||
dispatch(fetchRelationships(accountIds));
|
dispatch(fetchRelationships(accountIds));
|
||||||
@ -124,7 +123,18 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
|||||||
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
||||||
|
|
||||||
const excludeTypesFromFilter = filter => {
|
const excludeTypesFromFilter = filter => {
|
||||||
const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll']);
|
const allTypes = ImmutableList([
|
||||||
|
'follow',
|
||||||
|
'follow_request',
|
||||||
|
'favourite',
|
||||||
|
'reblog',
|
||||||
|
'mention',
|
||||||
|
'poll',
|
||||||
|
'status',
|
||||||
|
'update',
|
||||||
|
'admin.sign_up',
|
||||||
|
]);
|
||||||
|
|
||||||
return allTypes.filterNot(item => item === filter).toJS();
|
return allTypes.filterNot(item => item === filter).toJS();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,89 +1,38 @@
|
|||||||
import api from '../api';
|
import api from '../api';
|
||||||
import { openModal, closeModal } from './modal';
|
import { openModal } from './modal';
|
||||||
|
|
||||||
export const REPORT_INIT = 'REPORT_INIT';
|
|
||||||
export const REPORT_CANCEL = 'REPORT_CANCEL';
|
|
||||||
|
|
||||||
export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST';
|
export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST';
|
||||||
export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS';
|
export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS';
|
||||||
export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL';
|
export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL';
|
||||||
|
|
||||||
export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE';
|
export const initReport = (account, status) => dispatch =>
|
||||||
export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
|
dispatch(openModal('REPORT', {
|
||||||
export const REPORT_FORWARD_CHANGE = 'REPORT_FORWARD_CHANGE';
|
accountId: account.get('id'),
|
||||||
|
statusId: status?.get('id'),
|
||||||
|
}));
|
||||||
|
|
||||||
export function initReport(account, status) {
|
export const submitReport = (params, onSuccess, onFail) => (dispatch, getState) => {
|
||||||
return dispatch => {
|
|
||||||
dispatch({
|
|
||||||
type: REPORT_INIT,
|
|
||||||
account,
|
|
||||||
status,
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch(openModal('REPORT'));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function cancelReport() {
|
|
||||||
return {
|
|
||||||
type: REPORT_CANCEL,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function toggleStatusReport(statusId, checked) {
|
|
||||||
return {
|
|
||||||
type: REPORT_STATUS_TOGGLE,
|
|
||||||
statusId,
|
|
||||||
checked,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function submitReport() {
|
|
||||||
return (dispatch, getState) => {
|
|
||||||
dispatch(submitReportRequest());
|
dispatch(submitReportRequest());
|
||||||
|
|
||||||
api(getState).post('/api/v1/reports', {
|
api(getState).post('/api/v1/reports', params).then(response => {
|
||||||
account_id: getState().getIn(['reports', 'new', 'account_id']),
|
|
||||||
status_ids: getState().getIn(['reports', 'new', 'status_ids']),
|
|
||||||
comment: getState().getIn(['reports', 'new', 'comment']),
|
|
||||||
forward: getState().getIn(['reports', 'new', 'forward']),
|
|
||||||
}).then(response => {
|
|
||||||
dispatch(closeModal());
|
|
||||||
dispatch(submitReportSuccess(response.data));
|
dispatch(submitReportSuccess(response.data));
|
||||||
}).catch(error => dispatch(submitReportFail(error)));
|
if (onSuccess) onSuccess();
|
||||||
};
|
}).catch(error => {
|
||||||
|
dispatch(submitReportFail(error));
|
||||||
|
if (onFail) onFail();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export function submitReportRequest() {
|
export const submitReportRequest = () => ({
|
||||||
return {
|
|
||||||
type: REPORT_SUBMIT_REQUEST,
|
type: REPORT_SUBMIT_REQUEST,
|
||||||
};
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export function submitReportSuccess(report) {
|
export const submitReportSuccess = report => ({
|
||||||
return {
|
|
||||||
type: REPORT_SUBMIT_SUCCESS,
|
type: REPORT_SUBMIT_SUCCESS,
|
||||||
report,
|
report,
|
||||||
};
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export function submitReportFail(error) {
|
export const submitReportFail = error => ({
|
||||||
return {
|
|
||||||
type: REPORT_SUBMIT_FAIL,
|
type: REPORT_SUBMIT_FAIL,
|
||||||
error,
|
error,
|
||||||
};
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export function changeReportComment(comment) {
|
|
||||||
return {
|
|
||||||
type: REPORT_COMMENT_CHANGE,
|
|
||||||
comment,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function changeReportForward(forward) {
|
|
||||||
return {
|
|
||||||
type: REPORT_FORWARD_CHANGE,
|
|
||||||
forward,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
27
app/javascript/mastodon/actions/rules.js
Normal file
27
app/javascript/mastodon/actions/rules.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import api from '../api';
|
||||||
|
|
||||||
|
export const RULES_FETCH_REQUEST = 'RULES_FETCH_REQUEST';
|
||||||
|
export const RULES_FETCH_SUCCESS = 'RULES_FETCH_SUCCESS';
|
||||||
|
export const RULES_FETCH_FAIL = 'RULES_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const fetchRules = () => (dispatch, getState) => {
|
||||||
|
dispatch(fetchRulesRequest());
|
||||||
|
|
||||||
|
api(getState)
|
||||||
|
.get('/api/v1/instance').then(({ data }) => dispatch(fetchRulesSuccess(data.rules)))
|
||||||
|
.catch(err => dispatch(fetchRulesFail(err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchRulesRequest = () => ({
|
||||||
|
type: RULES_FETCH_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchRulesSuccess = rules => ({
|
||||||
|
type: RULES_FETCH_SUCCESS,
|
||||||
|
rules,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchRulesFail = error => ({
|
||||||
|
type: RULES_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
@ -2,7 +2,7 @@ import api from '../api';
|
|||||||
|
|
||||||
import { deleteFromTimelines } from './timelines';
|
import { deleteFromTimelines } from './timelines';
|
||||||
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
|
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
|
||||||
import { ensureComposeIsVisible } from './compose';
|
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
|
||||||
|
|
||||||
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
||||||
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
|
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
|
||||||
@ -33,6 +33,10 @@ export const QUOTE_HIDE = 'QUOTE_HIDE';
|
|||||||
|
|
||||||
export const REDRAFT = 'REDRAFT';
|
export const REDRAFT = 'REDRAFT';
|
||||||
|
|
||||||
|
export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
|
||||||
|
export const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS';
|
||||||
|
export const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL';
|
||||||
|
|
||||||
export function fetchStatusRequest(id, skipLoading) {
|
export function fetchStatusRequest(id, skipLoading) {
|
||||||
return {
|
return {
|
||||||
type: STATUS_FETCH_REQUEST,
|
type: STATUS_FETCH_REQUEST,
|
||||||
@ -87,6 +91,37 @@ export function redraft(status, raw_text) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const editStatus = (id, routerHistory) => (dispatch, getState) => {
|
||||||
|
let status = getState().getIn(['statuses', id]);
|
||||||
|
|
||||||
|
if (status.get('poll')) {
|
||||||
|
status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(fetchStatusSourceRequest());
|
||||||
|
|
||||||
|
api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
|
||||||
|
dispatch(fetchStatusSourceSuccess());
|
||||||
|
ensureComposeIsVisible(getState, routerHistory);
|
||||||
|
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(fetchStatusSourceFail(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchStatusSourceRequest = () => ({
|
||||||
|
type: STATUS_FETCH_SOURCE_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchStatusSourceSuccess = () => ({
|
||||||
|
type: STATUS_FETCH_SOURCE_SUCCESS,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchStatusSourceFail = error => ({
|
||||||
|
type: STATUS_FETCH_SOURCE_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
export function deleteStatus(id, routerHistory, withRedraft = false) {
|
export function deleteStatus(id, routerHistory, withRedraft = false) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
let status = getState().getIn(['statuses', id]);
|
let status = getState().getIn(['statuses', id]);
|
||||||
|
@ -1,31 +1,94 @@
|
|||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
import { importFetchedStatuses } from './importer';
|
||||||
|
|
||||||
export const TRENDS_FETCH_REQUEST = 'TRENDS_FETCH_REQUEST';
|
export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST';
|
||||||
export const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS';
|
export const TRENDS_TAGS_FETCH_SUCCESS = 'TRENDS_TAGS_FETCH_SUCCESS';
|
||||||
export const TRENDS_FETCH_FAIL = 'TRENDS_FETCH_FAIL';
|
export const TRENDS_TAGS_FETCH_FAIL = 'TRENDS_TAGS_FETCH_FAIL';
|
||||||
|
|
||||||
export const fetchTrends = () => (dispatch, getState) => {
|
export const TRENDS_LINKS_FETCH_REQUEST = 'TRENDS_LINKS_FETCH_REQUEST';
|
||||||
dispatch(fetchTrendsRequest());
|
export const TRENDS_LINKS_FETCH_SUCCESS = 'TRENDS_LINKS_FETCH_SUCCESS';
|
||||||
|
export const TRENDS_LINKS_FETCH_FAIL = 'TRENDS_LINKS_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const TRENDS_STATUSES_FETCH_REQUEST = 'TRENDS_STATUSES_FETCH_REQUEST';
|
||||||
|
export const TRENDS_STATUSES_FETCH_SUCCESS = 'TRENDS_STATUSES_FETCH_SUCCESS';
|
||||||
|
export const TRENDS_STATUSES_FETCH_FAIL = 'TRENDS_STATUSES_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const fetchTrendingHashtags = () => (dispatch, getState) => {
|
||||||
|
dispatch(fetchTrendingHashtagsRequest());
|
||||||
|
|
||||||
api(getState)
|
api(getState)
|
||||||
.get('/api/v1/trends')
|
.get('/api/v1/trends/tags')
|
||||||
.then(({ data }) => dispatch(fetchTrendsSuccess(data)))
|
.then(({ data }) => dispatch(fetchTrendingHashtagsSuccess(data)))
|
||||||
.catch(err => dispatch(fetchTrendsFail(err)));
|
.catch(err => dispatch(fetchTrendingHashtagsFail(err)));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchTrendsRequest = () => ({
|
export const fetchTrendingHashtagsRequest = () => ({
|
||||||
type: TRENDS_FETCH_REQUEST,
|
type: TRENDS_TAGS_FETCH_REQUEST,
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchTrendsSuccess = trends => ({
|
export const fetchTrendingHashtagsSuccess = trends => ({
|
||||||
type: TRENDS_FETCH_SUCCESS,
|
type: TRENDS_TAGS_FETCH_SUCCESS,
|
||||||
trends,
|
trends,
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchTrendsFail = error => ({
|
export const fetchTrendingHashtagsFail = error => ({
|
||||||
type: TRENDS_FETCH_FAIL,
|
type: TRENDS_TAGS_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
skipLoading: true,
|
||||||
|
skipAlert: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchTrendingLinks = () => (dispatch, getState) => {
|
||||||
|
dispatch(fetchTrendingLinksRequest());
|
||||||
|
|
||||||
|
api(getState)
|
||||||
|
.get('/api/v1/trends/links')
|
||||||
|
.then(({ data }) => dispatch(fetchTrendingLinksSuccess(data)))
|
||||||
|
.catch(err => dispatch(fetchTrendingLinksFail(err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchTrendingLinksRequest = () => ({
|
||||||
|
type: TRENDS_LINKS_FETCH_REQUEST,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchTrendingLinksSuccess = trends => ({
|
||||||
|
type: TRENDS_LINKS_FETCH_SUCCESS,
|
||||||
|
trends,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchTrendingLinksFail = error => ({
|
||||||
|
type: TRENDS_LINKS_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
skipLoading: true,
|
||||||
|
skipAlert: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchTrendingStatuses = () => (dispatch, getState) => {
|
||||||
|
dispatch(fetchTrendingStatusesRequest());
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/trends/statuses').then(({ data }) => {
|
||||||
|
dispatch(importFetchedStatuses(data));
|
||||||
|
dispatch(fetchTrendingStatusesSuccess(data));
|
||||||
|
}).catch(err => dispatch(fetchTrendingStatusesFail(err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchTrendingStatusesRequest = () => ({
|
||||||
|
type: TRENDS_STATUSES_FETCH_REQUEST,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchTrendingStatusesSuccess = statuses => ({
|
||||||
|
type: TRENDS_STATUSES_FETCH_SUCCESS,
|
||||||
|
statuses,
|
||||||
|
skipLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchTrendingStatusesFail = error => ({
|
||||||
|
type: TRENDS_STATUSES_FETCH_FAIL,
|
||||||
error,
|
error,
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
skipAlert: true,
|
skipAlert: true,
|
||||||
|
9
app/javascript/mastodon/components/check.js
Normal file
9
app/javascript/mastodon/components/check.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Check = () => (
|
||||||
|
<svg width='14' height='11' viewBox='0 0 14 11'>
|
||||||
|
<path d='M11.264 0L5.26 6.004 2.103 2.847 0 4.95l5.26 5.26 8.108-8.107L11.264 0' fill='currentColor' fillRule='evenodd' />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Check;
|
@ -6,6 +6,8 @@ import Overlay from 'react-overlays/lib/Overlay';
|
|||||||
import Motion from '../features/ui/util/optional_motion';
|
import Motion from '../features/ui/util/optional_motion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { CircularProgress } from 'mastodon/components/loading_indicator';
|
||||||
|
|
||||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||||
let id = 0;
|
let id = 0;
|
||||||
@ -17,13 +19,18 @@ class DropdownMenu extends React.PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
items: PropTypes.array.isRequired,
|
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
|
||||||
|
loading: PropTypes.bool,
|
||||||
|
scrollable: PropTypes.bool,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
placement: PropTypes.string,
|
placement: PropTypes.string,
|
||||||
arrowOffsetLeft: PropTypes.string,
|
arrowOffsetLeft: PropTypes.string,
|
||||||
arrowOffsetTop: PropTypes.string,
|
arrowOffsetTop: PropTypes.string,
|
||||||
openedViaKeyboard: PropTypes.bool,
|
openedViaKeyboard: PropTypes.bool,
|
||||||
|
renderItem: PropTypes.func,
|
||||||
|
renderHeader: PropTypes.func,
|
||||||
|
onItemClick: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -45,9 +52,11 @@ class DropdownMenu extends React.PureComponent {
|
|||||||
document.addEventListener('click', this.handleDocumentClick, false);
|
document.addEventListener('click', this.handleDocumentClick, false);
|
||||||
document.addEventListener('keydown', this.handleKeyDown, false);
|
document.addEventListener('keydown', this.handleKeyDown, false);
|
||||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
|
|
||||||
if (this.focusedItem && this.props.openedViaKeyboard) {
|
if (this.focusedItem && this.props.openedViaKeyboard) {
|
||||||
this.focusedItem.focus({ preventScroll: true });
|
this.focusedItem.focus({ preventScroll: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ mounted: true });
|
this.setState({ mounted: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +75,7 @@ class DropdownMenu extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown = e => {
|
handleKeyDown = e => {
|
||||||
const items = Array.from(this.node.getElementsByTagName('a'));
|
const items = Array.from(this.node.querySelectorAll('a, button'));
|
||||||
const index = items.indexOf(document.activeElement);
|
const index = items.indexOf(document.activeElement);
|
||||||
let element = null;
|
let element = null;
|
||||||
|
|
||||||
@ -109,21 +118,11 @@ class DropdownMenu extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleClick = e => {
|
handleClick = e => {
|
||||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
const { onItemClick } = this.props;
|
||||||
const { action, to } = this.props.items[i];
|
onItemClick(e);
|
||||||
|
|
||||||
this.props.onClose();
|
|
||||||
|
|
||||||
if (typeof action === 'function') {
|
|
||||||
e.preventDefault();
|
|
||||||
action(e);
|
|
||||||
} else if (to) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.context.router.history.push(to);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderItem (option, i) {
|
renderItem = (option, i) => {
|
||||||
if (option === null) {
|
if (option === null) {
|
||||||
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
|
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
|
||||||
}
|
}
|
||||||
@ -140,9 +139,11 @@ class DropdownMenu extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
|
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop, scrollable, renderHeader, loading } = this.props;
|
||||||
const { mounted } = this.state;
|
const { mounted } = this.state;
|
||||||
|
|
||||||
|
let renderItem = this.props.renderItem || this.renderItem;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||||
{({ opacity, scaleX, scaleY }) => (
|
{({ opacity, scaleX, scaleY }) => (
|
||||||
@ -152,9 +153,23 @@ class DropdownMenu extends React.PureComponent {
|
|||||||
<div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
|
<div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
|
||||||
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
|
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
|
||||||
|
|
||||||
<ul>
|
<div className={classNames('dropdown-menu__container', { 'dropdown-menu__container--loading': loading })}>
|
||||||
{items.map((option, i) => this.renderItem(option, i))}
|
{loading && (
|
||||||
|
<CircularProgress size={30} strokeWidth={3.5} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && renderHeader && (
|
||||||
|
<div className='dropdown-menu__container__header'>
|
||||||
|
{renderHeader(items)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && (
|
||||||
|
<ul className={classNames('dropdown-menu__container__list', { 'dropdown-menu__container__list--scrollable': scrollable })}>
|
||||||
|
{items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))}
|
||||||
</ul>
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Motion>
|
</Motion>
|
||||||
@ -170,11 +185,14 @@ export default class Dropdown extends React.PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
icon: PropTypes.string.isRequired,
|
children: PropTypes.node,
|
||||||
items: PropTypes.array.isRequired,
|
icon: PropTypes.string,
|
||||||
size: PropTypes.number.isRequired,
|
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
|
||||||
|
loading: PropTypes.bool,
|
||||||
|
size: PropTypes.number,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
|
scrollable: PropTypes.bool,
|
||||||
status: ImmutablePropTypes.map,
|
status: ImmutablePropTypes.map,
|
||||||
isUserTouching: PropTypes.func,
|
isUserTouching: PropTypes.func,
|
||||||
onOpen: PropTypes.func.isRequired,
|
onOpen: PropTypes.func.isRequired,
|
||||||
@ -182,6 +200,9 @@ export default class Dropdown extends React.PureComponent {
|
|||||||
dropdownPlacement: PropTypes.string,
|
dropdownPlacement: PropTypes.string,
|
||||||
openDropdownId: PropTypes.number,
|
openDropdownId: PropTypes.number,
|
||||||
openedViaKeyboard: PropTypes.bool,
|
openedViaKeyboard: PropTypes.bool,
|
||||||
|
renderItem: PropTypes.func,
|
||||||
|
renderHeader: PropTypes.func,
|
||||||
|
onItemClick: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -237,17 +258,21 @@ export default class Dropdown extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleItemClick = e => {
|
handleItemClick = e => {
|
||||||
|
const { onItemClick } = this.props;
|
||||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||||
const { action, to } = this.props.items[i];
|
const item = this.props.items[i];
|
||||||
|
|
||||||
this.handleClose();
|
this.handleClose();
|
||||||
|
|
||||||
if (typeof action === 'function') {
|
if (typeof onItemClick === 'function') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
action();
|
onItemClick(item, i);
|
||||||
} else if (to) {
|
} else if (item && typeof item.action === 'function') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.context.router.history.push(to);
|
item.action();
|
||||||
|
} else if (item && item.to) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.context.router.history.push(item.to);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,12 +290,36 @@ export default class Dropdown extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
close = () => {
|
||||||
|
this.handleClose();
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props;
|
const {
|
||||||
|
icon,
|
||||||
|
items,
|
||||||
|
size,
|
||||||
|
title,
|
||||||
|
disabled,
|
||||||
|
loading,
|
||||||
|
scrollable,
|
||||||
|
dropdownPlacement,
|
||||||
|
openDropdownId,
|
||||||
|
openedViaKeyboard,
|
||||||
|
children,
|
||||||
|
renderItem,
|
||||||
|
renderHeader,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
const open = this.state.id === openDropdownId;
|
const open = this.state.id === openDropdownId;
|
||||||
|
|
||||||
return (
|
const button = children ? React.cloneElement(React.Children.only(children), {
|
||||||
<div>
|
ref: this.setTargetRef,
|
||||||
|
onClick: this.handleClick,
|
||||||
|
onMouseDown: this.handleMouseDown,
|
||||||
|
onKeyDown: this.handleButtonKeyDown,
|
||||||
|
onKeyPress: this.handleKeyPress,
|
||||||
|
}) : (
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={icon}
|
icon={icon}
|
||||||
title={title}
|
title={title}
|
||||||
@ -283,11 +332,25 @@ export default class Dropdown extends React.PureComponent {
|
|||||||
onKeyDown={this.handleButtonKeyDown}
|
onKeyDown={this.handleButtonKeyDown}
|
||||||
onKeyPress={this.handleKeyPress}
|
onKeyPress={this.handleKeyPress}
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{button}
|
||||||
|
|
||||||
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
|
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
|
||||||
<DropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} />
|
<DropdownMenu
|
||||||
|
items={items}
|
||||||
|
loading={loading}
|
||||||
|
scrollable={scrollable}
|
||||||
|
onClose={this.handleClose}
|
||||||
|
openedViaKeyboard={openedViaKeyboard}
|
||||||
|
renderItem={renderItem}
|
||||||
|
renderHeader={renderHeader}
|
||||||
|
onItemClick={this.handleItemClick}
|
||||||
|
/>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
</div>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { openDropdownMenu, closeDropdownMenu } from 'mastodon/actions/dropdown_menu';
|
||||||
|
import { fetchHistory } from 'mastodon/actions/history';
|
||||||
|
import DropdownMenu from 'mastodon/components/dropdown_menu';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { statusId }) => ({
|
||||||
|
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
|
||||||
|
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
|
||||||
|
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
||||||
|
items: state.getIn(['history', statusId, 'items']),
|
||||||
|
loading: state.getIn(['history', statusId, 'loading']),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, { statusId }) => ({
|
||||||
|
|
||||||
|
onOpen (id, onItemClick, dropdownPlacement, keyboard) {
|
||||||
|
dispatch(fetchHistory(statusId));
|
||||||
|
dispatch(openDropdownMenu(id, dropdownPlacement, keyboard));
|
||||||
|
},
|
||||||
|
|
||||||
|
onClose (id) {
|
||||||
|
dispatch(closeDropdownMenu(id));
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu);
|
70
app/javascript/mastodon/components/edited_timestamp/index.js
Normal file
70
app/javascript/mastodon/components/edited_timestamp/index.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||||
|
import Icon from 'mastodon/components/icon';
|
||||||
|
import DropdownMenu from './containers/dropdown_menu_container';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
|
||||||
|
import InlineAccount from 'mastodon/components/inline_account';
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, { statusId }) => ({
|
||||||
|
|
||||||
|
onItemClick (index) {
|
||||||
|
dispatch(openModal('COMPARE_HISTORY', { index, statusId }));
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(null, mapDispatchToProps)
|
||||||
|
@injectIntl
|
||||||
|
class EditedTimestamp extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
statusId: PropTypes.string.isRequired,
|
||||||
|
timestamp: PropTypes.string.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
onItemClick: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleItemClick = (item, i) => {
|
||||||
|
const { onItemClick } = this.props;
|
||||||
|
onItemClick(i);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderHeader = items => {
|
||||||
|
return (
|
||||||
|
<FormattedMessage id='status.edited_x_times' defaultMessage='Edited {count, plural, one {{count} time} other {{count} times}}' values={{ count: items.size - 1 }} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderItem = (item, index, { onClick, onKeyPress }) => {
|
||||||
|
const formattedDate = <RelativeTimestamp timestamp={item.get('created_at')} short={false} />;
|
||||||
|
const formattedName = <InlineAccount accountId={item.get('account')} />;
|
||||||
|
|
||||||
|
const label = item.get('original') ? (
|
||||||
|
<FormattedMessage id='status.history.created' defaultMessage='{name} created {date}' values={{ name: formattedName, date: formattedDate }} />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage id='status.history.edited' defaultMessage='{name} edited {date}' values={{ name: formattedName, date: formattedDate }} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className='dropdown-menu__item edited-timestamp__history__item' key={item.get('created_at')}>
|
||||||
|
<button data-index={index} onClick={onClick} onKeyPress={onKeyPress}>{label}</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { timestamp, intl, statusId } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
|
||||||
|
<button className='dropdown-menu__text-button'>
|
||||||
|
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} /> <Icon id='caret-down' />
|
||||||
|
</button>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -38,7 +38,7 @@ class SilentErrorBoundary extends React.Component {
|
|||||||
*
|
*
|
||||||
* @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
|
* @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
|
||||||
*/
|
*/
|
||||||
const accountsCountRenderer = (displayNumber, pluralReady) => (
|
export const accountsCountRenderer = (displayNumber, pluralReady) => (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='trends.counter_by_accounts'
|
id='trends.counter_by_accounts'
|
||||||
defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} talking'
|
defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} talking'
|
||||||
|
@ -27,6 +27,7 @@ export default class IconButton extends React.PureComponent {
|
|||||||
tabIndex: PropTypes.string,
|
tabIndex: PropTypes.string,
|
||||||
counter: PropTypes.number,
|
counter: PropTypes.number,
|
||||||
obfuscateCount: PropTypes.bool,
|
obfuscateCount: PropTypes.bool,
|
||||||
|
href: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -102,6 +103,7 @@ export default class IconButton extends React.PureComponent {
|
|||||||
title,
|
title,
|
||||||
counter,
|
counter,
|
||||||
obfuscateCount,
|
obfuscateCount,
|
||||||
|
href,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -123,6 +125,20 @@ export default class IconButton extends React.PureComponent {
|
|||||||
style.width = 'auto';
|
style.width = 'auto';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let contents = (
|
||||||
|
<React.Fragment>
|
||||||
|
<Icon id={icon} fixedWidth aria-hidden='true' /> {typeof counter !== 'undefined' && <span className='icon-button__counter'><AnimatedNumber value={counter} obfuscate={obfuscateCount} /></span>}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (href) {
|
||||||
|
contents = (
|
||||||
|
<a href={href} target='_blank' rel='noopener noreferrer'>
|
||||||
|
{contents}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
@ -138,7 +154,7 @@ export default class IconButton extends React.PureComponent {
|
|||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<Icon id={icon} fixedWidth aria-hidden='true' /> {typeof counter !== 'undefined' && <span className='icon-button__counter'><AnimatedNumber value={counter} obfuscate={obfuscateCount} /></span>}
|
{contents}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
34
app/javascript/mastodon/components/inline_account.js
Normal file
34
app/javascript/mastodon/components/inline_account.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeGetAccount } from 'mastodon/selectors';
|
||||||
|
import Avatar from 'mastodon/components/avatar';
|
||||||
|
|
||||||
|
const makeMapStateToProps = () => {
|
||||||
|
const getAccount = makeGetAccount();
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { accountId }) => ({
|
||||||
|
account: getAccount(state, accountId),
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default @connect(makeMapStateToProps)
|
||||||
|
class InlineAccount extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { account } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className='inline-account'>
|
||||||
|
<Avatar size={13} account={account} /> <strong>{account.get('username')}</strong>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,10 +1,31 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export const CircularProgress = ({ size, strokeWidth }) => {
|
||||||
|
const viewBox = `0 0 ${size} ${size}`;
|
||||||
|
const radius = (size - strokeWidth) / 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg width={size} height={size} viewBox={viewBox} className='circular-progress' role='progressbar'>
|
||||||
|
<circle
|
||||||
|
fill='none'
|
||||||
|
cx={size / 2}
|
||||||
|
cy={size / 2}
|
||||||
|
r={radius}
|
||||||
|
strokeWidth={`${strokeWidth}px`}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CircularProgress.propTypes = {
|
||||||
|
size: PropTypes.number.isRequired,
|
||||||
|
strokeWidth: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
const LoadingIndicator = () => (
|
const LoadingIndicator = () => (
|
||||||
<div className='loading-indicator'>
|
<div className='loading-indicator'>
|
||||||
<div className='loading-indicator__figure' />
|
<CircularProgress size={50} strokeWidth={6} />
|
||||||
<FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ export default class ModalRoot extends React.PureComponent {
|
|||||||
g: PropTypes.number,
|
g: PropTypes.number,
|
||||||
b: PropTypes.number,
|
b: PropTypes.number,
|
||||||
}),
|
}),
|
||||||
|
ignoreFocus: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
activeElement = this.props.children ? document.activeElement : null;
|
activeElement = this.props.children ? document.activeElement : null;
|
||||||
@ -72,7 +73,9 @@ export default class ModalRoot extends React.PureComponent {
|
|||||||
// immediately selectable, we have to wait for observers to run, as
|
// immediately selectable, we have to wait for observers to run, as
|
||||||
// described in https://github.com/WICG/inert#performance-and-gotchas
|
// described in https://github.com/WICG/inert#performance-and-gotchas
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
|
if (!this.props.ignoreFocus) {
|
||||||
this.activeElement.focus({ preventScroll: true });
|
this.activeElement.focus({ preventScroll: true });
|
||||||
|
}
|
||||||
this.activeElement = null;
|
this.activeElement = null;
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
|
|
||||||
|
@ -5,10 +5,15 @@ import PropTypes from 'prop-types';
|
|||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
today: { id: 'relative_time.today', defaultMessage: 'today' },
|
today: { id: 'relative_time.today', defaultMessage: 'today' },
|
||||||
just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
|
just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
|
||||||
|
just_now_full: { id: 'relative_time.full.just_now', defaultMessage: 'just now' },
|
||||||
seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' },
|
seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' },
|
||||||
|
seconds_full: { id: 'relative_time.full.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} ago' },
|
||||||
minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
|
minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
|
||||||
|
minutes_full: { id: 'relative_time.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago' },
|
||||||
hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
|
hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
|
||||||
|
hours_full: { id: 'relative_time.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} ago' },
|
||||||
days: { id: 'relative_time.days', defaultMessage: '{number}d' },
|
days: { id: 'relative_time.days', defaultMessage: '{number}d' },
|
||||||
|
days_full: { id: 'relative_time.full.days', defaultMessage: '{number, plural, one {# day} other {# days}} ago' },
|
||||||
moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
|
moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
|
||||||
seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
|
seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
|
||||||
minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
|
minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
|
||||||
@ -66,7 +71,7 @@ const getUnitDelay = units => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const timeAgoString = (intl, date, now, year, timeGiven = true) => {
|
export const timeAgoString = (intl, date, now, year, timeGiven, short) => {
|
||||||
const delta = now - date.getTime();
|
const delta = now - date.getTime();
|
||||||
|
|
||||||
let relativeTime;
|
let relativeTime;
|
||||||
@ -74,16 +79,16 @@ export const timeAgoString = (intl, date, now, year, timeGiven = true) => {
|
|||||||
if (delta < DAY && !timeGiven) {
|
if (delta < DAY && !timeGiven) {
|
||||||
relativeTime = intl.formatMessage(messages.today);
|
relativeTime = intl.formatMessage(messages.today);
|
||||||
} else if (delta < 10 * SECOND) {
|
} else if (delta < 10 * SECOND) {
|
||||||
relativeTime = intl.formatMessage(messages.just_now);
|
relativeTime = intl.formatMessage(short ? messages.just_now : messages.just_now_full);
|
||||||
} else if (delta < 7 * DAY) {
|
} else if (delta < 7 * DAY) {
|
||||||
if (delta < MINUTE) {
|
if (delta < MINUTE) {
|
||||||
relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) });
|
relativeTime = intl.formatMessage(short ? messages.seconds : messages.seconds_full, { number: Math.floor(delta / SECOND) });
|
||||||
} else if (delta < HOUR) {
|
} else if (delta < HOUR) {
|
||||||
relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) });
|
relativeTime = intl.formatMessage(short ? messages.minutes : messages.minutes_full, { number: Math.floor(delta / MINUTE) });
|
||||||
} else if (delta < DAY) {
|
} else if (delta < DAY) {
|
||||||
relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) });
|
relativeTime = intl.formatMessage(short ? messages.hours : messages.hours_full, { number: Math.floor(delta / HOUR) });
|
||||||
} else {
|
} else {
|
||||||
relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) });
|
relativeTime = intl.formatMessage(short ? messages.days : messages.days_full, { number: Math.floor(delta / DAY) });
|
||||||
}
|
}
|
||||||
} else if (date.getFullYear() === year) {
|
} else if (date.getFullYear() === year) {
|
||||||
relativeTime = intl.formatDate(date, shortDateFormatOptions);
|
relativeTime = intl.formatDate(date, shortDateFormatOptions);
|
||||||
@ -124,6 +129,7 @@ class RelativeTimestamp extends React.Component {
|
|||||||
timestamp: PropTypes.string.isRequired,
|
timestamp: PropTypes.string.isRequired,
|
||||||
year: PropTypes.number.isRequired,
|
year: PropTypes.number.isRequired,
|
||||||
futureDate: PropTypes.bool,
|
futureDate: PropTypes.bool,
|
||||||
|
short: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -132,6 +138,7 @@ class RelativeTimestamp extends React.Component {
|
|||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
year: (new Date()).getFullYear(),
|
year: (new Date()).getFullYear(),
|
||||||
|
short: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
shouldComponentUpdate (nextProps, nextState) {
|
shouldComponentUpdate (nextProps, nextState) {
|
||||||
@ -176,11 +183,11 @@ class RelativeTimestamp extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { timestamp, intl, year, futureDate } = this.props;
|
const { timestamp, intl, year, futureDate, short } = this.props;
|
||||||
|
|
||||||
const timeGiven = timestamp.includes('T');
|
const timeGiven = timestamp.includes('T');
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven);
|
const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven, short);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>
|
<time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>
|
||||||
|
@ -12,6 +12,7 @@ import classNames from 'classnames';
|
|||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
||||||
|
edit: { id: 'status.edit', defaultMessage: 'Edit' },
|
||||||
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
||||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||||
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||||
@ -79,6 +80,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
onPin: PropTypes.func,
|
onPin: PropTypes.func,
|
||||||
onBookmark: PropTypes.func,
|
onBookmark: PropTypes.func,
|
||||||
withDismiss: PropTypes.bool,
|
withDismiss: PropTypes.bool,
|
||||||
|
withCounters: PropTypes.bool,
|
||||||
scrollKey: PropTypes.string,
|
scrollKey: PropTypes.string,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
@ -144,6 +146,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
this.props.onDelete(this.props.status, this.context.router.history, true);
|
this.props.onDelete(this.props.status, this.context.router.history, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleEditClick = () => {
|
||||||
|
this.props.onEdit(this.props.status, this.context.router.history);
|
||||||
|
}
|
||||||
|
|
||||||
handlePinClick = () => {
|
handlePinClick = () => {
|
||||||
this.props.onPin(this.props.status);
|
this.props.onPin(this.props.status);
|
||||||
}
|
}
|
||||||
@ -236,7 +242,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, relationship, intl, withDismiss, scrollKey } = this.props;
|
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
|
||||||
|
|
||||||
const anonymousAccess = !me;
|
const anonymousAccess = !me;
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
@ -270,6 +276,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (writtenByMe) {
|
if (writtenByMe) {
|
||||||
|
// menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
||||||
} else {
|
} else {
|
||||||
@ -340,8 +347,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
return (
|
return (
|
||||||
<div className='status__action-bar'>
|
<div className='status__action-bar'>
|
||||||
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
||||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
|
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
|
||||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
|
||||||
<IconButton className='status__action-bar-button' disabled={!publicStatus} title={StatusActionBar.quoteTitle(intl, messages, publicStatus)} icon='quote-right' onClick={this.handleQuoteClick} />
|
<IconButton className='status__action-bar-button' disabled={!publicStatus} title={StatusActionBar.quoteTitle(intl, messages, publicStatus)} icon='quote-right' onClick={this.handleQuoteClick} />
|
||||||
|
|
||||||
{shareButton}
|
{shareButton}
|
||||||
|
@ -24,6 +24,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||||||
prepend: PropTypes.node,
|
prepend: PropTypes.node,
|
||||||
emptyMessage: PropTypes.node,
|
emptyMessage: PropTypes.node,
|
||||||
alwaysPrepend: PropTypes.bool,
|
alwaysPrepend: PropTypes.bool,
|
||||||
|
withCounters: PropTypes.bool,
|
||||||
timelineId: PropTypes.string,
|
timelineId: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -100,6 +101,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||||||
contextType={timelineId}
|
contextType={timelineId}
|
||||||
scrollKey={this.props.scrollKey}
|
scrollKey={this.props.scrollKey}
|
||||||
showThread
|
showThread
|
||||||
|
withCounters={this.props.withCounters}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : null;
|
) : null;
|
||||||
@ -114,6 +116,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||||||
onMoveDown={this.handleMoveDown}
|
onMoveDown={this.handleMoveDown}
|
||||||
contextType={timelineId}
|
contextType={timelineId}
|
||||||
showThread
|
showThread
|
||||||
|
withCounters={this.props.withCounters}
|
||||||
/>
|
/>
|
||||||
)).concat(scrollableContent);
|
)).concat(scrollableContent);
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import {
|
|||||||
hideStatus,
|
hideStatus,
|
||||||
revealStatus,
|
revealStatus,
|
||||||
toggleStatusCollapse,
|
toggleStatusCollapse,
|
||||||
|
editStatus,
|
||||||
hideQuote,
|
hideQuote,
|
||||||
revealQuote,
|
revealQuote,
|
||||||
} from '../actions/statuses';
|
} from '../actions/statuses';
|
||||||
@ -163,6 +164,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onEdit (status, history) {
|
||||||
|
dispatch(editStatus(status.get('id'), history));
|
||||||
|
},
|
||||||
|
|
||||||
onDirect (account, router) {
|
onDirect (account, router) {
|
||||||
dispatch(directCompose(account, router));
|
dispatch(directCompose(account, router));
|
||||||
},
|
},
|
||||||
|
@ -28,6 +28,7 @@ const messages = defineMessages({
|
|||||||
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
|
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
|
||||||
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
|
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
|
||||||
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
||||||
|
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
@ -49,6 +50,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
preselectDate: PropTypes.instanceOf(Date),
|
preselectDate: PropTypes.instanceOf(Date),
|
||||||
isSubmitting: PropTypes.bool,
|
isSubmitting: PropTypes.bool,
|
||||||
isChangingUpload: PropTypes.bool,
|
isChangingUpload: PropTypes.bool,
|
||||||
|
isEditing: PropTypes.bool,
|
||||||
isUploading: PropTypes.bool,
|
isUploading: PropTypes.bool,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
onSubmit: PropTypes.func.isRequired,
|
onSubmit: PropTypes.func.isRequired,
|
||||||
@ -161,8 +163,13 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
selectionStart = selectionEnd;
|
selectionStart = selectionEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Because of the wicg-inert polyfill, the activeElement may not be
|
||||||
|
// immediately selectable, we have to wait for observers to run, as
|
||||||
|
// described in https://github.com/WICG/inert#performance-and-gotchas
|
||||||
|
Promise.resolve().then(() => {
|
||||||
this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
|
this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
|
||||||
this.autosuggestTextarea.textarea.focus();
|
this.autosuggestTextarea.textarea.focus();
|
||||||
|
}).catch(console.error);
|
||||||
} else if(prevProps.isSubmitting && !this.props.isSubmitting) {
|
} else if(prevProps.isSubmitting && !this.props.isSubmitting) {
|
||||||
this.autosuggestTextarea.textarea.focus();
|
this.autosuggestTextarea.textarea.focus();
|
||||||
} else if (this.props.spoiler !== prevProps.spoiler) {
|
} else if (this.props.spoiler !== prevProps.spoiler) {
|
||||||
@ -199,7 +206,9 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
const disabled = this.props.isSubmitting;
|
const disabled = this.props.isSubmitting;
|
||||||
let publishText = '';
|
let publishText = '';
|
||||||
|
|
||||||
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
|
if (this.props.isEditing) {
|
||||||
|
publishText = intl.formatMessage(messages.saveChanges);
|
||||||
|
} else if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
|
||||||
publishText = <span className='compose-form__publish-private'><Icon id='lock' /> {intl.formatMessage(messages.publish)}</span>;
|
publishText = <span className='compose-form__publish-private'><Icon id='lock' /> {intl.formatMessage(messages.publish)}</span>;
|
||||||
} else {
|
} else {
|
||||||
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
|
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
|
||||||
@ -256,7 +265,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
<div className='compose-form__buttons'>
|
<div className='compose-form__buttons'>
|
||||||
<UploadButtonContainer />
|
<UploadButtonContainer />
|
||||||
<PollButtonContainer />
|
<PollButtonContainer />
|
||||||
<PrivacyDropdownContainer />
|
<PrivacyDropdownContainer disabled={this.props.isEditing} />
|
||||||
<SpoilerButtonContainer />
|
<SpoilerButtonContainer />
|
||||||
</div>
|
</div>
|
||||||
<div className='character-counter__wrapper'><CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} /></div>
|
<div className='character-counter__wrapper'><CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} /></div>
|
||||||
|
@ -170,7 +170,7 @@ class EmojiPickerMenu extends React.PureComponent {
|
|||||||
|
|
||||||
state = {
|
state = {
|
||||||
modifierOpen: false,
|
modifierOpen: false,
|
||||||
placement: null,
|
readyToFocus: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDocumentClick = e => {
|
handleDocumentClick = e => {
|
||||||
@ -182,6 +182,16 @@ class EmojiPickerMenu extends React.PureComponent {
|
|||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
document.addEventListener('click', this.handleDocumentClick, false);
|
document.addEventListener('click', this.handleDocumentClick, false);
|
||||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
|
|
||||||
|
// Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need
|
||||||
|
// to wait for a frame before focusing
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.setState({ readyToFocus: true });
|
||||||
|
if (this.node) {
|
||||||
|
const element = this.node.querySelector('input[type="search"]');
|
||||||
|
if (element) element.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
@ -281,7 +291,7 @@ class EmojiPickerMenu extends React.PureComponent {
|
|||||||
showSkinTones={false}
|
showSkinTones={false}
|
||||||
backgroundImageFn={backgroundImageFn}
|
backgroundImageFn={backgroundImageFn}
|
||||||
notFound={notFoundFn}
|
notFound={notFoundFn}
|
||||||
autoFocus
|
autoFocus={this.state.readyToFocus}
|
||||||
emojiTooltip
|
emojiTooltip
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -314,6 +324,7 @@ class EmojiPickerDropdown extends React.PureComponent {
|
|||||||
state = {
|
state = {
|
||||||
active: false,
|
active: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
placement: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
setRef = (c) => {
|
setRef = (c) => {
|
||||||
|
@ -159,6 +159,7 @@ class PrivacyDropdown extends React.PureComponent {
|
|||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
noDirect: PropTypes.bool,
|
noDirect: PropTypes.bool,
|
||||||
container: PropTypes.func,
|
container: PropTypes.func,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -247,7 +248,7 @@ class PrivacyDropdown extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { value, container, intl } = this.props;
|
const { value, container, disabled, intl } = this.props;
|
||||||
const { open, placement } = this.state;
|
const { open, placement } = this.state;
|
||||||
|
|
||||||
const valueOption = this.options.find(item => item.value === value);
|
const valueOption = this.options.find(item => item.value === value);
|
||||||
@ -267,6 +268,7 @@ class PrivacyDropdown extends React.PureComponent {
|
|||||||
onMouseDown={this.handleMouseDown}
|
onMouseDown={this.handleMouseDown}
|
||||||
onKeyDown={this.handleButtonKeyDown}
|
onKeyDown={this.handleButtonKeyDown}
|
||||||
style={{ height: null, lineHeight: '27px' }}
|
style={{ height: null, lineHeight: '27px' }}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ export default class Upload extends ImmutablePureComponent {
|
|||||||
media: ImmutablePropTypes.map.isRequired,
|
media: ImmutablePropTypes.map.isRequired,
|
||||||
onUndo: PropTypes.func.isRequired,
|
onUndo: PropTypes.func.isRequired,
|
||||||
onOpenFocalPoint: PropTypes.func.isRequired,
|
onOpenFocalPoint: PropTypes.func.isRequired,
|
||||||
|
isEditingStatus: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleUndoClick = e => {
|
handleUndoClick = e => {
|
||||||
@ -31,7 +32,7 @@ export default class Upload extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { media } = this.props;
|
const { media, isEditingStatus } = this.props;
|
||||||
const focusX = media.getIn(['meta', 'focus', 'x']);
|
const focusX = media.getIn(['meta', 'focus', 'x']);
|
||||||
const focusY = media.getIn(['meta', 'focus', 'y']);
|
const focusY = media.getIn(['meta', 'focus', 'y']);
|
||||||
const x = ((focusX / 2) + .5) * 100;
|
const x = ((focusX / 2) + .5) * 100;
|
||||||
@ -44,7 +45,7 @@ export default class Upload extends ImmutablePureComponent {
|
|||||||
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
|
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
|
||||||
<div className={classNames('compose-form__upload__actions', { active: true })}>
|
<div className={classNames('compose-form__upload__actions', { active: true })}>
|
||||||
<button className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
|
<button className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
|
||||||
<button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
|
{!isEditingStatus && (<button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -21,6 +21,7 @@ const mapStateToProps = state => ({
|
|||||||
caretPosition: state.getIn(['compose', 'caretPosition']),
|
caretPosition: state.getIn(['compose', 'caretPosition']),
|
||||||
preselectDate: state.getIn(['compose', 'preselectDate']),
|
preselectDate: state.getIn(['compose', 'preselectDate']),
|
||||||
isSubmitting: state.getIn(['compose', 'is_submitting']),
|
isSubmitting: state.getIn(['compose', 'is_submitting']),
|
||||||
|
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']),
|
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
||||||
|
@ -6,10 +6,21 @@ import ReplyIndicator from '../components/reply_indicator';
|
|||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
const getStatus = makeGetStatus();
|
const getStatus = makeGetStatus();
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => {
|
||||||
status: getStatus(state, { id: state.getIn(['compose', props.quote ? 'quote_from' : 'in_reply_to']) }),
|
let statusId = state.getIn(['compose', 'id'], null);
|
||||||
|
let editing = true;
|
||||||
|
|
||||||
|
if (statusId === null) {
|
||||||
|
statusId = state.getIn(['compose', props.quote ? 'quote_from' : 'in_reply_to']);
|
||||||
|
editing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: getStatus(state, { id: statusId }),
|
||||||
quote: props.quote,
|
quote: props.quote,
|
||||||
});
|
editing,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
};
|
};
|
||||||
|
@ -5,6 +5,7 @@ import { submitCompose } from '../../../actions/compose';
|
|||||||
|
|
||||||
const mapStateToProps = (state, { id }) => ({
|
const mapStateToProps = (state, { id }) => ({
|
||||||
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
||||||
|
isEditingStatus: state.getIn(['compose', 'id']) !== null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
@ -90,7 +90,7 @@ Object.keys(emojiIndex.emojis).forEach(key => {
|
|||||||
let { short_names, search, unified } = emojiMartData.emojis[key];
|
let { short_names, search, unified } = emojiMartData.emojis[key];
|
||||||
|
|
||||||
if (short_names[0] !== key) {
|
if (short_names[0] !== key) {
|
||||||
throw new Error('The compresser expects the first short_code to be the ' +
|
throw new Error('The compressor expects the first short_code to be the ' +
|
||||||
'key. It may need to be rewritten if the emoji change such that this ' +
|
'key. It may need to be rewritten if the emoji change such that this ' +
|
||||||
'is no longer the case.');
|
'is no longer the case.');
|
||||||
}
|
}
|
||||||
|
51
app/javascript/mastodon/features/explore/components/story.js
Normal file
51
app/javascript/mastodon/features/explore/components/story.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Blurhash from 'mastodon/components/blurhash';
|
||||||
|
import { accountsCountRenderer } from 'mastodon/components/hashtag';
|
||||||
|
import ShortNumber from 'mastodon/components/short_number';
|
||||||
|
import Skeleton from 'mastodon/components/skeleton';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
export default class Story extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
url: PropTypes.string,
|
||||||
|
title: PropTypes.string,
|
||||||
|
publisher: PropTypes.string,
|
||||||
|
sharedTimes: PropTypes.number,
|
||||||
|
thumbnail: PropTypes.string,
|
||||||
|
blurhash: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
thumbnailLoaded: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleImageLoad = () => this.setState({ thumbnailLoaded: true });
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { url, title, publisher, sharedTimes, thumbnail, blurhash } = this.props;
|
||||||
|
|
||||||
|
const { thumbnailLoaded } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a className='story' href={url} target='blank' rel='noopener'>
|
||||||
|
<div className='story__details'>
|
||||||
|
<div className='story__details__publisher'>{publisher ? publisher : <Skeleton width={50} />}</div>
|
||||||
|
<div className='story__details__title'>{title ? title : <Skeleton />}</div>
|
||||||
|
<div className='story__details__shared'>{typeof sharedTimes === 'number' ? <ShortNumber value={sharedTimes} renderer={accountsCountRenderer} /> : <Skeleton width={100} />}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='story__thumbnail'>
|
||||||
|
{thumbnail ? (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className={classNames('story__thumbnail__preview', { 'story__thumbnail__preview--hidden': thumbnailLoaded })}><Blurhash hash={blurhash} /></div>
|
||||||
|
<img src={thumbnail} onLoad={this.handleImageLoad} alt='' role='presentation' />
|
||||||
|
</React.Fragment>
|
||||||
|
) : <Skeleton />}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
91
app/javascript/mastodon/features/explore/index.js
Normal file
91
app/javascript/mastodon/features/explore/index.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Column from 'mastodon/components/column';
|
||||||
|
import ColumnHeader from 'mastodon/components/column_header';
|
||||||
|
import { NavLink, Switch, Route } from 'react-router-dom';
|
||||||
|
import Links from './links';
|
||||||
|
import Tags from './tags';
|
||||||
|
import Statuses from './statuses';
|
||||||
|
import Suggestions from './suggestions';
|
||||||
|
import Search from 'mastodon/features/compose/containers/search_container';
|
||||||
|
import SearchResults from './results';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'explore.title', defaultMessage: 'Explore' },
|
||||||
|
searchResults: { id: 'explore.search_results', defaultMessage: 'Search results' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
layout: state.getIn(['meta', 'layout']),
|
||||||
|
isSearching: state.getIn(['search', 'submitted']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
|
class Explore extends React.PureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
isSearching: PropTypes.bool,
|
||||||
|
layout: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleHeaderClick = () => {
|
||||||
|
this.column.scrollTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.column = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl, multiColumn, isSearching, layout } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||||
|
{layout === 'mobile' ? (
|
||||||
|
<div className='explore__search-header'>
|
||||||
|
<Search />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ColumnHeader
|
||||||
|
icon={isSearching ? 'search' : 'globe'}
|
||||||
|
title={intl.formatMessage(isSearching ? messages.searchResults : messages.title)}
|
||||||
|
onClick={this.handleHeaderClick}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='scrollable scrollable--flex'>
|
||||||
|
{isSearching ? (
|
||||||
|
<SearchResults />
|
||||||
|
) : (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className='account__section-headline'>
|
||||||
|
<NavLink exact to='/explore'><FormattedMessage id='explore.trending_statuses' defaultMessage='Posts' /></NavLink>
|
||||||
|
<NavLink exact to='/explore/tags'><FormattedMessage id='explore.trending_tags' defaultMessage='Hashtags' /></NavLink>
|
||||||
|
<NavLink exact to='/explore/links'><FormattedMessage id='explore.trending_links' defaultMessage='News' /></NavLink>
|
||||||
|
<NavLink exact to='/explore/suggestions'><FormattedMessage id='explore.suggested_follows' defaultMessage='For you' /></NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch>
|
||||||
|
<Route path='/explore/tags' component={Tags} />
|
||||||
|
<Route path='/explore/links' component={Links} />
|
||||||
|
<Route path='/explore/suggestions' component={Suggestions} />
|
||||||
|
<Route exact path={['/explore', '/explore/posts', '/search']} component={Statuses} componentParams={{ multiColumn }} />
|
||||||
|
</Switch>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
48
app/javascript/mastodon/features/explore/links.js
Normal file
48
app/javascript/mastodon/features/explore/links.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import Story from './components/story';
|
||||||
|
import LoadingIndicator from 'mastodon/components/loading_indicator';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { fetchTrendingLinks } from 'mastodon/actions/trends';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
links: state.getIn(['trends', 'links', 'items']),
|
||||||
|
isLoading: state.getIn(['trends', 'links', 'isLoading']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class Links extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
links: ImmutablePropTypes.list,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(fetchTrendingLinks());
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { isLoading, links } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='explore__links'>
|
||||||
|
{isLoading ? (<LoadingIndicator />) : links.map(link => (
|
||||||
|
<Story
|
||||||
|
key={link.get('id')}
|
||||||
|
url={link.get('url')}
|
||||||
|
title={link.get('title')}
|
||||||
|
publisher={link.get('provider_name')}
|
||||||
|
sharedTimes={link.getIn(['history', 0, 'accounts']) * 1 + link.getIn(['history', 1, 'accounts']) * 1}
|
||||||
|
thumbnail={link.get('image')}
|
||||||
|
blurhash={link.get('blurhash')}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
113
app/javascript/mastodon/features/explore/results.js
Normal file
113
app/javascript/mastodon/features/explore/results.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { expandSearch } from 'mastodon/actions/search';
|
||||||
|
import Account from 'mastodon/containers/account_container';
|
||||||
|
import Status from 'mastodon/containers/status_container';
|
||||||
|
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
import LoadMore from 'mastodon/components/load_more';
|
||||||
|
import LoadingIndicator from 'mastodon/components/loading_indicator';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
isLoading: state.getIn(['search', 'isLoading']),
|
||||||
|
results: state.getIn(['search', 'results']),
|
||||||
|
});
|
||||||
|
|
||||||
|
const appendLoadMore = (id, list, onLoadMore) => {
|
||||||
|
if (list.size >= 5) {
|
||||||
|
return list.push(<LoadMore key={`${id}-load-more`} visible onClick={onLoadMore} />);
|
||||||
|
} else {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderAccounts = (results, onLoadMore) => appendLoadMore('accounts', results.get('accounts').map(item => (
|
||||||
|
<Account key={`account-${item}`} id={item} />
|
||||||
|
)), onLoadMore);
|
||||||
|
|
||||||
|
const renderHashtags = (results, onLoadMore) => appendLoadMore('hashtags', results.get('hashtags').map(item => (
|
||||||
|
<Hashtag key={`tag-${item.get('name')}`} hashtag={item} />
|
||||||
|
)), onLoadMore);
|
||||||
|
|
||||||
|
const renderStatuses = (results, onLoadMore) => appendLoadMore('statuses', results.get('statuses').map(item => (
|
||||||
|
<Status key={`status-${item}`} id={item} />
|
||||||
|
)), onLoadMore);
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class Results extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
results: ImmutablePropTypes.map,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
type: 'all',
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSelectAll = () => this.setState({ type: 'all' });
|
||||||
|
handleSelectAccounts = () => this.setState({ type: 'accounts' });
|
||||||
|
handleSelectHashtags = () => this.setState({ type: 'hashtags' });
|
||||||
|
handleSelectStatuses = () => this.setState({ type: 'statuses' });
|
||||||
|
handleLoadMoreAccounts = () => this.loadMore('accounts');
|
||||||
|
handleLoadMoreStatuses = () => this.loadMore('statuses');
|
||||||
|
handleLoadMoreHashtags = () => this.loadMore('hashtags');
|
||||||
|
|
||||||
|
loadMore (type) {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(expandSearch(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { isLoading, results } = this.props;
|
||||||
|
const { type } = this.state;
|
||||||
|
|
||||||
|
let filteredResults = ImmutableList();
|
||||||
|
|
||||||
|
if (!isLoading) {
|
||||||
|
switch(type) {
|
||||||
|
case 'all':
|
||||||
|
filteredResults = filteredResults.concat(renderAccounts(results, this.handleLoadMoreAccounts), renderHashtags(results, this.handleLoadMoreHashtags), renderStatuses(results, this.handleLoadMoreStatuses));
|
||||||
|
break;
|
||||||
|
case 'accounts':
|
||||||
|
filteredResults = filteredResults.concat(renderAccounts(results, this.handleLoadMoreAccounts));
|
||||||
|
break;
|
||||||
|
case 'hashtags':
|
||||||
|
filteredResults = filteredResults.concat(renderHashtags(results, this.handleLoadMoreHashtags));
|
||||||
|
break;
|
||||||
|
case 'statuses':
|
||||||
|
filteredResults = filteredResults.concat(renderStatuses(results, this.handleLoadMoreStatuses));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredResults.size === 0) {
|
||||||
|
filteredResults = (
|
||||||
|
<div className='empty-column-indicator'>
|
||||||
|
<FormattedMessage id='search_results.nothing_found' defaultMessage='Could not find anything for these search terms' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className='account__section-headline'>
|
||||||
|
<button onClick={this.handleSelectAll} className={type === 'all' && 'active'}><FormattedMessage id='search_results.all' defaultMessage='All' /></button>
|
||||||
|
<button onClick={this.handleSelectAccounts} className={type === 'accounts' && 'active'}><FormattedMessage id='search_results.accounts' defaultMessage='People' /></button>
|
||||||
|
<button onClick={this.handleSelectHashtags} className={type === 'hashtags' && 'active'}><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></button>
|
||||||
|
<button onClick={this.handleSelectStatuses} className={type === 'statuses' && 'active'}><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='explore__search-results'>
|
||||||
|
{isLoading ? (<LoadingIndicator />) : filteredResults}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
48
app/javascript/mastodon/features/explore/statuses.js
Normal file
48
app/javascript/mastodon/features/explore/statuses.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import StatusList from 'mastodon/components/status_list';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { fetchTrendingStatuses } from 'mastodon/actions/trends';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
statusIds: state.getIn(['status_lists', 'trending', 'items']),
|
||||||
|
isLoading: state.getIn(['status_lists', 'trending', 'isLoading'], true),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class Statuses extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
statusIds: ImmutablePropTypes.list,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(fetchTrendingStatuses());
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { isLoading, statusIds, multiColumn } = this.props;
|
||||||
|
|
||||||
|
const emptyMessage = <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusList
|
||||||
|
trackScroll
|
||||||
|
statusIds={statusIds}
|
||||||
|
scrollKey='explore-statuses'
|
||||||
|
hasMore={false}
|
||||||
|
isLoading={isLoading}
|
||||||
|
emptyMessage={emptyMessage}
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
withCounters
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
40
app/javascript/mastodon/features/explore/suggestions.js
Normal file
40
app/javascript/mastodon/features/explore/suggestions.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import Account from 'mastodon/containers/account_container';
|
||||||
|
import LoadingIndicator from 'mastodon/components/loading_indicator';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { fetchSuggestions } from 'mastodon/actions/suggestions';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
suggestions: state.getIn(['suggestions', 'items']),
|
||||||
|
isLoading: state.getIn(['suggestions', 'isLoading']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class Suggestions extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
suggestions: ImmutablePropTypes.list,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(fetchSuggestions(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { isLoading, suggestions } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='explore__links'>
|
||||||
|
{isLoading ? (<LoadingIndicator />) : suggestions.map(suggestion => (
|
||||||
|
<Account key={suggestion.get('account')} id={suggestion.get('account')} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
40
app/javascript/mastodon/features/explore/tags.js
Normal file
40
app/javascript/mastodon/features/explore/tags.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
|
||||||
|
import LoadingIndicator from 'mastodon/components/loading_indicator';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { fetchTrendingHashtags } from 'mastodon/actions/trends';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
hashtags: state.getIn(['trends', 'tags', 'items']),
|
||||||
|
isLoadingHashtags: state.getIn(['trends', 'tags', 'isLoading']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class Tags extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
hashtags: ImmutablePropTypes.list,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(fetchTrendingHashtags());
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { isLoading, hashtags } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='explore__links'>
|
||||||
|
{isLoading ? (<LoadingIndicator />) : hashtags.map(hashtag => (
|
||||||
|
<Hashtag key={hashtag.get('name')} hashtag={hashtag} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,13 +1,13 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { fetchTrends } from 'mastodon/actions/trends';
|
import { fetchTrendingHashtags } from 'mastodon/actions/trends';
|
||||||
import Trends from '../components/trends';
|
import Trends from '../components/trends';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
trends: state.getIn(['trends', 'items']),
|
trends: state.getIn(['trends', 'tags', 'items']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
fetchTrends: () => dispatch(fetchTrends()),
|
fetchTrends: () => dispatch(fetchTrendingHashtags()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Trends);
|
export default connect(mapStateToProps, mapDispatchToProps)(Trends);
|
||||||
|
@ -5,6 +5,7 @@ import { FormattedMessage } from 'react-intl';
|
|||||||
import ClearColumnButton from './clear_column_button';
|
import ClearColumnButton from './clear_column_button';
|
||||||
import GrantPermissionButton from './grant_permission_button';
|
import GrantPermissionButton from './grant_permission_button';
|
||||||
import SettingToggle from './setting_toggle';
|
import SettingToggle from './setting_toggle';
|
||||||
|
import { isStaff } from 'mastodon/initial_state';
|
||||||
|
|
||||||
export default class ColumnSettings extends React.PureComponent {
|
export default class ColumnSettings extends React.PureComponent {
|
||||||
|
|
||||||
@ -153,6 +154,30 @@ export default class ColumnSettings extends React.PureComponent {
|
|||||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'status']} onChange={onChange} label={soundStr} />
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'status']} onChange={onChange} label={soundStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div role='group' aria-labelledby='notifications-update'>
|
||||||
|
<span id='notifications-update' className='column-settings__section'><FormattedMessage id='notifications.column_settings.update' defaultMessage='Edits:' /></span>
|
||||||
|
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'update']} onChange={onChange} label={alertStr} />
|
||||||
|
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'update']} onChange={this.onPushChange} label={pushStr} />}
|
||||||
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'update']} onChange={onChange} label={showStr} />
|
||||||
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'update']} onChange={onChange} label={soundStr} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isStaff && (
|
||||||
|
<div role='group' aria-labelledby='notifications-admin-sign-up'>
|
||||||
|
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.sign_up' defaultMessage='New sign-ups:' /></span>
|
||||||
|
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.sign_up']} onChange={onChange} label={alertStr} />
|
||||||
|
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'admin.sign_up']} onChange={this.onPushChange} label={pushStr} />}
|
||||||
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'admin.sign_up']} onChange={onChange} label={showStr} />
|
||||||
|
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'admin.sign_up']} onChange={onChange} label={soundStr} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,8 @@ const messages = defineMessages({
|
|||||||
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
|
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
|
||||||
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
|
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
|
||||||
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
|
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
|
||||||
|
update: { id: 'notification.update', defaultMessage: '{name} edited a post' },
|
||||||
|
adminSignUp: { id: 'notification.admin.sign_up', defaultMessage: '{name} signed up' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const notificationForScreenReader = (intl, message, timestamp) => {
|
const notificationForScreenReader = (intl, message, timestamp) => {
|
||||||
@ -273,6 +275,38 @@ class Notification extends ImmutablePureComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderUpdate (notification, link) {
|
||||||
|
const { intl, unread } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HotKeys handlers={this.getHandlers()}>
|
||||||
|
<div className={classNames('notification notification-update focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.update, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||||
|
<div className='notification__message'>
|
||||||
|
<div className='notification__favourite-icon-wrapper'>
|
||||||
|
<Icon id='pencil' fixedWidth />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span title={notification.get('created_at')}>
|
||||||
|
<FormattedMessage id='notification.update' defaultMessage='{name} edited a post' values={{ name: link }} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StatusContainer
|
||||||
|
id={notification.get('status')}
|
||||||
|
account={notification.get('account')}
|
||||||
|
muted
|
||||||
|
withDismiss
|
||||||
|
hidden={this.props.hidden}
|
||||||
|
getScrollPosition={this.props.getScrollPosition}
|
||||||
|
updateScrollBottom={this.props.updateScrollBottom}
|
||||||
|
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||||
|
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</HotKeys>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderPoll (notification, account) {
|
renderPoll (notification, account) {
|
||||||
const { intl, unread } = this.props;
|
const { intl, unread } = this.props;
|
||||||
const ownPoll = me === account.get('id');
|
const ownPoll = me === account.get('id');
|
||||||
@ -311,6 +345,28 @@ class Notification extends ImmutablePureComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderAdminSignUp (notification, account, link) {
|
||||||
|
const { intl, unread } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HotKeys handlers={this.getHandlers()}>
|
||||||
|
<div className={classNames('notification notification-admin-sign-up focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.adminSignUp, { name: account.get('acct') }), notification.get('created_at'))}>
|
||||||
|
<div className='notification__message'>
|
||||||
|
<div className='notification__favourite-icon-wrapper'>
|
||||||
|
<Icon id='user-plus' fixedWidth />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span title={notification.get('created_at')}>
|
||||||
|
<FormattedMessage id='notification.admin.sign_up' defaultMessage='{name} signed up' values={{ name: link }} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AccountContainer id={account.get('id')} hidden={this.props.hidden} />
|
||||||
|
</div>
|
||||||
|
</HotKeys>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { notification } = this.props;
|
const { notification } = this.props;
|
||||||
const account = notification.get('account');
|
const account = notification.get('account');
|
||||||
@ -330,8 +386,12 @@ class Notification extends ImmutablePureComponent {
|
|||||||
return this.renderReblog(notification, link);
|
return this.renderReblog(notification, link);
|
||||||
case 'status':
|
case 'status':
|
||||||
return this.renderStatus(notification, link);
|
return this.renderStatus(notification, link);
|
||||||
|
case 'update':
|
||||||
|
return this.renderUpdate(notification, link);
|
||||||
case 'poll':
|
case 'poll':
|
||||||
return this.renderPoll(notification, account);
|
return this.renderPoll(notification, account);
|
||||||
|
case 'admin.sign_up':
|
||||||
|
return this.renderAdminSignUp(notification, account, link);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -65,7 +65,7 @@ class Footer extends ImmutablePureComponent {
|
|||||||
const { router } = this.context;
|
const { router } = this.context;
|
||||||
|
|
||||||
if (onClose) {
|
if (onClose) {
|
||||||
onClose();
|
onClose(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(replyCompose(status, router.history));
|
dispatch(replyCompose(status, router.history));
|
||||||
@ -186,7 +186,7 @@ class Footer extends ImmutablePureComponent {
|
|||||||
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
||||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
|
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
|
||||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
|
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
|
||||||
{withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' onClick={this.handleOpenClick} />}
|
{withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' onClick={this.handleOpenClick} href={status.get('url')} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
93
app/javascript/mastodon/features/report/category.js
Normal file
93
app/javascript/mastodon/features/report/category.js
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import Button from 'mastodon/components/button';
|
||||||
|
import Option from './components/option';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
dislike: { id: 'report.reasons.dislike', defaultMessage: 'I don\'t like it' },
|
||||||
|
dislike_description: { id: 'report.reasons.dislike_description', defaultMessage: 'It is not something you want to see' },
|
||||||
|
spam: { id: 'report.reasons.spam', defaultMessage: 'It\'s spam' },
|
||||||
|
spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetetive replies' },
|
||||||
|
violation: { id: 'report.reasons.violation', defaultMessage: 'It violates server rules' },
|
||||||
|
violation_description: { id: 'report.reasons.violation_description', defaultMessage: 'You are aware that it breaks specific rules' },
|
||||||
|
other: { id: 'report.reasons.other', defaultMessage: 'It\'s something else' },
|
||||||
|
other_description: { id: 'report.reasons.other_description', defaultMessage: 'The issue does not fit into other categories' },
|
||||||
|
status: { id: 'report.category.title_status', defaultMessage: 'post' },
|
||||||
|
account: { id: 'report.category.title_account', defaultMessage: 'profile' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class Category extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onNextStep: PropTypes.func.isRequired,
|
||||||
|
category: PropTypes.string,
|
||||||
|
onChangeCategory: PropTypes.func.isRequired,
|
||||||
|
startedFrom: PropTypes.oneOf(['status', 'account']),
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleNextClick = () => {
|
||||||
|
const { onNextStep, category } = this.props;
|
||||||
|
|
||||||
|
switch(category) {
|
||||||
|
case 'dislike':
|
||||||
|
onNextStep('thanks');
|
||||||
|
break;
|
||||||
|
case 'violation':
|
||||||
|
onNextStep('rules');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
onNextStep('statuses');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCategoryToggle = (value, checked) => {
|
||||||
|
const { onChangeCategory } = this.props;
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
onChangeCategory(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { category, startedFrom, intl } = this.props;
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
'dislike',
|
||||||
|
'spam',
|
||||||
|
'violation',
|
||||||
|
'other',
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<h3 className='report-dialog-modal__title'><FormattedMessage id='report.category.title' defaultMessage="Tell us what's going on with this {type}" values={{ type: intl.formatMessage(messages[startedFrom]) }} /></h3>
|
||||||
|
<p className='report-dialog-modal__lead'><FormattedMessage id='report.category.subtitle' defaultMessage='Choose the best match' /></p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{options.map(item => (
|
||||||
|
<Option
|
||||||
|
key={item}
|
||||||
|
name='category'
|
||||||
|
value={item}
|
||||||
|
checked={category === item}
|
||||||
|
onToggle={this.handleCategoryToggle}
|
||||||
|
label={intl.formatMessage(messages[item])}
|
||||||
|
description={intl.formatMessage(messages[`${item}_description`])}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex-spacer' />
|
||||||
|
|
||||||
|
<div className='report-dialog-modal__actions'>
|
||||||
|
<Button onClick={this.handleNextClick} disabled={category === null}><FormattedMessage id='report.next' defaultMessage='Next' /></Button>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
83
app/javascript/mastodon/features/report/comment.js
Normal file
83
app/javascript/mastodon/features/report/comment.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||||
|
import Button from 'mastodon/components/button';
|
||||||
|
import Toggle from 'react-toggle';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
placeholder: { id: 'report.placeholder', defaultMessage: 'Type or paste additional comments' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class Comment extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onSubmit: PropTypes.func.isRequired,
|
||||||
|
comment: PropTypes.string.isRequired,
|
||||||
|
onChangeComment: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
isSubmitting: PropTypes.bool,
|
||||||
|
forward: PropTypes.bool,
|
||||||
|
isRemote: PropTypes.bool,
|
||||||
|
domain: PropTypes.string,
|
||||||
|
onChangeForward: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
const { onSubmit } = this.props;
|
||||||
|
onSubmit();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleChange = e => {
|
||||||
|
const { onChangeComment } = this.props;
|
||||||
|
onChangeComment(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleKeyDown = e => {
|
||||||
|
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||||
|
this.handleClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleForwardChange = e => {
|
||||||
|
const { onChangeForward } = this.props;
|
||||||
|
onChangeForward(e.target.checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { comment, isRemote, forward, domain, isSubmitting, intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<h3 className='report-dialog-modal__title'><FormattedMessage id='report.comment.title' defaultMessage='Is there anything else you think we should know?' /></h3>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
className='report-dialog-modal__textarea'
|
||||||
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
|
value={comment}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
onKeyDown={this.handleKeyDown}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isRemote && (
|
||||||
|
<React.Fragment>
|
||||||
|
<p className='report-dialog-modal__lead'><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' /></p>
|
||||||
|
|
||||||
|
<label className='report-dialog-modal__toggle'>
|
||||||
|
<Toggle checked={forward} disabled={isSubmitting} onChange={this.handleForwardChange} />
|
||||||
|
<FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} />
|
||||||
|
</label>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='flex-spacer' />
|
||||||
|
|
||||||
|
<div className='report-dialog-modal__actions'>
|
||||||
|
<Button onClick={this.handleClick}><FormattedMessage id='report.submit' defaultMessage='Submit report' /></Button>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
60
app/javascript/mastodon/features/report/components/option.js
Normal file
60
app/javascript/mastodon/features/report/components/option.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import Check from 'mastodon/components/check';
|
||||||
|
|
||||||
|
export default class Option extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
checked: PropTypes.bool,
|
||||||
|
label: PropTypes.node,
|
||||||
|
description: PropTypes.node,
|
||||||
|
onToggle: PropTypes.func,
|
||||||
|
multiple: PropTypes.bool,
|
||||||
|
labelComponent: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleKeyPress = e => {
|
||||||
|
const { value, checked, onToggle } = this.props;
|
||||||
|
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
onToggle(value, !checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange = e => {
|
||||||
|
const { value, onToggle } = this.props;
|
||||||
|
onToggle(value, e.target.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { name, value, checked, label, labelComponent, description, multiple } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label className='dialog-option poll__option selectable'>
|
||||||
|
<input type={multiple ? 'checkbox' : 'radio'} name={name} value={value} checked={checked} onChange={this.handleChange} />
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={classNames('poll__input', { active: checked, checkbox: multiple })}
|
||||||
|
tabIndex='0'
|
||||||
|
role='radio'
|
||||||
|
onKeyPress={this.handleKeyPress}
|
||||||
|
aria-checked={checked}
|
||||||
|
aria-label={label}
|
||||||
|
>{checked && <Check />}</span>
|
||||||
|
|
||||||
|
{labelComponent ? labelComponent : (
|
||||||
|
<span className='poll__option__text'>
|
||||||
|
<strong>{label}</strong>
|
||||||
|
{description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,23 +1,32 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Toggle from 'react-toggle';
|
|
||||||
import noop from 'lodash/noop';
|
import noop from 'lodash/noop';
|
||||||
import StatusContent from '../../../components/status_content';
|
import StatusContent from 'mastodon/components/status_content';
|
||||||
import { MediaGallery, Video } from '../../ui/util/async-components';
|
import { MediaGallery, Video } from 'mastodon/features/ui/util/async-components';
|
||||||
import Bundle from '../../ui/components/bundle';
|
import Bundle from 'mastodon/features/ui/components/bundle';
|
||||||
|
import Avatar from 'mastodon/components/avatar';
|
||||||
|
import DisplayName from 'mastodon/components/display_name';
|
||||||
|
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
|
||||||
|
import Option from './option';
|
||||||
|
|
||||||
export default class StatusCheckBox extends React.PureComponent {
|
export default class StatusCheckBox extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
status: ImmutablePropTypes.map.isRequired,
|
status: ImmutablePropTypes.map.isRequired,
|
||||||
checked: PropTypes.bool,
|
checked: PropTypes.bool,
|
||||||
onToggle: PropTypes.func.isRequired,
|
onToggle: PropTypes.func.isRequired,
|
||||||
disabled: PropTypes.bool,
|
};
|
||||||
|
|
||||||
|
handleStatusesToggle = (value, checked) => {
|
||||||
|
const { onToggle } = this.props;
|
||||||
|
onToggle(value, checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, checked, onToggle, disabled } = this.props;
|
const { status, checked } = this.props;
|
||||||
|
|
||||||
let media = null;
|
let media = null;
|
||||||
|
|
||||||
if (status.get('reblog')) {
|
if (status.get('reblog')) {
|
||||||
@ -50,24 +59,46 @@ export default class StatusCheckBox extends React.PureComponent {
|
|||||||
} else {
|
} else {
|
||||||
media = (
|
media = (
|
||||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
|
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
|
||||||
{Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={noop} />}
|
{Component => (
|
||||||
|
<Component
|
||||||
|
media={status.get('media_attachments')}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
height={110}
|
||||||
|
onOpenMedia={noop}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Bundle>
|
</Bundle>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const labelComponent = (
|
||||||
<div className='status-check-box'>
|
<div className='status-check-box__status poll__option__text'>
|
||||||
<div className='status-check-box__status'>
|
<div className='detailed-status__display-name'>
|
||||||
<StatusContent status={status} />
|
<div className='detailed-status__display-avatar'>
|
||||||
{media}
|
<Avatar account={status.get('account')} size={46} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='status-check-box-toggle'>
|
<div><DisplayName account={status.get('account')} /> · <RelativeTimestamp timestamp={status.get('created_at')} /></div>
|
||||||
<Toggle checked={checked} onChange={onToggle} disabled={disabled} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<StatusContent status={status} />
|
||||||
|
|
||||||
|
{media}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Option
|
||||||
|
name='status_ids'
|
||||||
|
value={status.get('id')}
|
||||||
|
checked={checked}
|
||||||
|
onToggle={this.handleStatusesToggle}
|
||||||
|
label={status.get('search_index')}
|
||||||
|
labelComponent={labelComponent}
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,15 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import StatusCheckBox from '../components/status_check_box';
|
import StatusCheckBox from '../components/status_check_box';
|
||||||
import { toggleStatusReport } from '../../../actions/reports';
|
import { makeGetStatus } from 'mastodon/selectors';
|
||||||
import { Set as ImmutableSet } from 'immutable';
|
|
||||||
|
const makeMapStateToProps = () => {
|
||||||
|
const getStatus = makeGetStatus();
|
||||||
|
|
||||||
const mapStateToProps = (state, { id }) => ({
|
const mapStateToProps = (state, { id }) => ({
|
||||||
status: state.getIn(['statuses', id]),
|
status: getStatus(state, { id }),
|
||||||
checked: state.getIn(['reports', 'new', 'status_ids'], ImmutableSet()).includes(id),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { id }) => ({
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
onToggle (e) {
|
export default connect(makeMapStateToProps)(StatusCheckBox);
|
||||||
dispatch(toggleStatusReport(id, e.target.checked));
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(StatusCheckBox);
|
|
||||||
|
64
app/javascript/mastodon/features/report/rules.js
Normal file
64
app/javascript/mastodon/features/report/rules.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import Button from 'mastodon/components/button';
|
||||||
|
import Option from './components/option';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
rules: state.get('rules'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class Rules extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onNextStep: PropTypes.func.isRequired,
|
||||||
|
rules: ImmutablePropTypes.list,
|
||||||
|
selectedRuleIds: ImmutablePropTypes.set.isRequired,
|
||||||
|
onToggle: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleNextClick = () => {
|
||||||
|
const { onNextStep } = this.props;
|
||||||
|
onNextStep('statuses');
|
||||||
|
};
|
||||||
|
|
||||||
|
handleRulesToggle = (value, checked) => {
|
||||||
|
const { onToggle } = this.props;
|
||||||
|
onToggle(value, checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { rules, selectedRuleIds } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<h3 className='report-dialog-modal__title'><FormattedMessage id='report.rules.title' defaultMessage='Which rules are being violated?' /></h3>
|
||||||
|
<p className='report-dialog-modal__lead'><FormattedMessage id='report.rules.subtitle' defaultMessage='Select all that apply' /></p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{rules.map(item => (
|
||||||
|
<Option
|
||||||
|
key={item.get('id')}
|
||||||
|
name='rule_ids'
|
||||||
|
value={item.get('id')}
|
||||||
|
checked={selectedRuleIds.includes(item.get('id'))}
|
||||||
|
onToggle={this.handleRulesToggle}
|
||||||
|
label={item.get('text')}
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex-spacer' />
|
||||||
|
|
||||||
|
<div className='report-dialog-modal__actions'>
|
||||||
|
<Button onClick={this.handleNextClick} disabled={selectedRuleIds.size < 1}><FormattedMessage id='report.next' defaultMessage='Next' /></Button>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
58
app/javascript/mastodon/features/report/statuses.js
Normal file
58
app/javascript/mastodon/features/report/statuses.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import StatusCheckBox from 'mastodon/features/report/containers/status_check_box_container';
|
||||||
|
import { OrderedSet } from 'immutable';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import Button from 'mastodon/components/button';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { accountId }) => ({
|
||||||
|
availableStatusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class Statuses extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onNextStep: PropTypes.func.isRequired,
|
||||||
|
accountId: PropTypes.string.isRequired,
|
||||||
|
availableStatusIds: ImmutablePropTypes.set.isRequired,
|
||||||
|
selectedStatusIds: ImmutablePropTypes.set.isRequired,
|
||||||
|
onToggle: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleNextClick = () => {
|
||||||
|
const { onNextStep } = this.props;
|
||||||
|
onNextStep('comment');
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { availableStatusIds, selectedStatusIds, onToggle } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<h3 className='report-dialog-modal__title'><FormattedMessage id='report.statuses.title' defaultMessage='Are there any posts that back up this report?' /></h3>
|
||||||
|
<p className='report-dialog-modal__lead'><FormattedMessage id='report.statuses.subtitle' defaultMessage='Select all that apply' /></p>
|
||||||
|
|
||||||
|
<div className='report-dialog-modal__statuses'>
|
||||||
|
{availableStatusIds.union(selectedStatusIds).map(statusId => (
|
||||||
|
<StatusCheckBox
|
||||||
|
id={statusId}
|
||||||
|
key={statusId}
|
||||||
|
checked={selectedStatusIds.includes(statusId)}
|
||||||
|
onToggle={onToggle}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex-spacer' />
|
||||||
|
|
||||||
|
<div className='report-dialog-modal__actions'>
|
||||||
|
<Button onClick={this.handleNextClick}><FormattedMessage id='report.next' defaultMessage='Next' /></Button>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
84
app/javascript/mastodon/features/report/thanks.js
Normal file
84
app/javascript/mastodon/features/report/thanks.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import Button from 'mastodon/components/button';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
unfollowAccount,
|
||||||
|
muteAccount,
|
||||||
|
blockAccount,
|
||||||
|
} from 'mastodon/actions/accounts';
|
||||||
|
|
||||||
|
const mapStateToProps = () => ({});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class Thanks extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
submitted: PropTypes.bool,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCloseClick = () => {
|
||||||
|
const { onClose } = this.props;
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleUnfollowClick = () => {
|
||||||
|
const { dispatch, account, onClose } = this.props;
|
||||||
|
dispatch(unfollowAccount(account.get('id')));
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMuteClick = () => {
|
||||||
|
const { dispatch, account, onClose } = this.props;
|
||||||
|
dispatch(muteAccount(account.get('id')));
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleBlockClick = () => {
|
||||||
|
const { dispatch, account, onClose } = this.props;
|
||||||
|
dispatch(blockAccount(account.get('id')));
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { account, submitted } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<h3 className='report-dialog-modal__title'>{submitted ? <FormattedMessage id='report.thanks.title_actionable' defaultMessage="Thanks for reporting, we'll look into this." /> : <FormattedMessage id='report.thanks.title' defaultMessage="Don't want to see this?" />}</h3>
|
||||||
|
<p className='report-dialog-modal__lead'>{submitted ? <FormattedMessage id='report.thanks.take_action_actionable' defaultMessage='While we review this, you can take action against @{name}:' values={{ name: account.get('username') }} /> : <FormattedMessage id='report.thanks.take_action' defaultMessage='Here are your options for controlling what you see on Mastodon:' />}</p>
|
||||||
|
|
||||||
|
{account.getIn(['relationship', 'following']) && (
|
||||||
|
<React.Fragment>
|
||||||
|
<h4 className='report-dialog-modal__subtitle'><FormattedMessage id='report.unfollow' defaultMessage='Unfollow @{name}' values={{ name: account.get('username') }} /></h4>
|
||||||
|
<p className='report-dialog-modal__lead'><FormattedMessage id='report.unfollow_explanation' defaultMessage='You are following this account. To not see their posts in your home feed anymore, unfollow them.' /></p>
|
||||||
|
<Button secondary onClick={this.handleUnfollowClick}><FormattedMessage id='account.unfollow' defaultMessage='Unfollow' /></Button>
|
||||||
|
<hr />
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h4 className='report-dialog-modal__subtitle'><FormattedMessage id='account.mute' defaultMessage='Mute @{name}' values={{ name: account.get('username') }} /></h4>
|
||||||
|
<p className='report-dialog-modal__lead'><FormattedMessage id='report.mute_explanation' defaultMessage='You will not see their posts. They can still follow you and see your posts and will not know that they are muted.' /></p>
|
||||||
|
<Button secondary onClick={this.handleMuteClick}>{!account.getIn(['relationship', 'muting']) ? <FormattedMessage id='report.mute' defaultMessage='Mute' /> : <FormattedMessage id='account.muted' defaultMessage='Muted' />}</Button>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h4 className='report-dialog-modal__subtitle'><FormattedMessage id='account.block' defaultMessage='Block @{name}' values={{ name: account.get('username') }} /></h4>
|
||||||
|
<p className='report-dialog-modal__lead'><FormattedMessage id='report.block_explanation' defaultMessage='You will not see their posts. They will not be able to see your posts or follow you. They will be able to tell that they are blocked.' /></p>
|
||||||
|
<Button secondary onClick={this.handleBlockClick}>{!account.getIn(['relationship', 'blocking']) ? <FormattedMessage id='report.block' defaultMessage='Block' /> : <FormattedMessage id='account.blocked' defaultMessage='Blocked' />}</Button>
|
||||||
|
|
||||||
|
<div className='flex-spacer' />
|
||||||
|
|
||||||
|
<div className='report-dialog-modal__actions'>
|
||||||
|
<Button onClick={this.handleCloseClick}><FormattedMessage id='report.close' defaultMessage='Done' /></Button>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,17 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import SearchContainer from 'mastodon/features/compose/containers/search_container';
|
|
||||||
import SearchResultsContainer from 'mastodon/features/compose/containers/search_results_container';
|
|
||||||
|
|
||||||
const Search = () => (
|
|
||||||
<div className='column search-page'>
|
|
||||||
<SearchContainer />
|
|
||||||
|
|
||||||
<div className='drawer__pager'>
|
|
||||||
<div className='drawer__inner darker'>
|
|
||||||
<SearchResultsContainer />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Search;
|
|
@ -12,6 +12,7 @@ import classNames from 'classnames';
|
|||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
||||||
|
edit: { id: 'status.edit', defaultMessage: 'Edit' },
|
||||||
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
||||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||||
@ -63,6 +64,7 @@ class ActionBar extends React.PureComponent {
|
|||||||
onFavourite: PropTypes.func.isRequired,
|
onFavourite: PropTypes.func.isRequired,
|
||||||
onBookmark: PropTypes.func.isRequired,
|
onBookmark: PropTypes.func.isRequired,
|
||||||
onDelete: PropTypes.func.isRequired,
|
onDelete: PropTypes.func.isRequired,
|
||||||
|
onEdit: PropTypes.func.isRequired,
|
||||||
onDirect: PropTypes.func.isRequired,
|
onDirect: PropTypes.func.isRequired,
|
||||||
onMention: PropTypes.func.isRequired,
|
onMention: PropTypes.func.isRequired,
|
||||||
onMute: PropTypes.func,
|
onMute: PropTypes.func,
|
||||||
@ -106,6 +108,10 @@ class ActionBar extends React.PureComponent {
|
|||||||
this.props.onDelete(this.props.status, this.context.router.history, true);
|
this.props.onDelete(this.props.status, this.context.router.history, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleEditClick = () => {
|
||||||
|
this.props.onEdit(this.props.status, this.context.router.history);
|
||||||
|
}
|
||||||
|
|
||||||
handleDirectClick = () => {
|
handleDirectClick = () => {
|
||||||
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
|
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
|
||||||
}
|
}
|
||||||
@ -217,6 +223,7 @@ class ActionBar extends React.PureComponent {
|
|||||||
|
|
||||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
// menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
||||||
} else {
|
} else {
|
||||||
|
@ -8,7 +8,7 @@ import { mapStateToProps, quote } from '../../../components/status';
|
|||||||
import StatusContent from '../../../components/status_content';
|
import StatusContent from '../../../components/status_content';
|
||||||
import MediaGallery from '../../../components/media_gallery';
|
import MediaGallery from '../../../components/media_gallery';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { injectIntl, defineMessages, FormattedDate, FormattedMessage } from 'react-intl';
|
import { injectIntl, defineMessages, FormattedDate } from 'react-intl';
|
||||||
import Card from './card';
|
import Card from './card';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import Video from '../../video';
|
import Video from '../../video';
|
||||||
@ -18,6 +18,7 @@ import classNames from 'classnames';
|
|||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
import AnimatedNumber from 'mastodon/components/animated_number';
|
import AnimatedNumber from 'mastodon/components/animated_number';
|
||||||
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
|
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
|
||||||
|
import EditedTimestamp from 'mastodon/components/edited_timestamp';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||||
@ -282,7 +283,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||||||
edited = (
|
edited = (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<React.Fragment> · </React.Fragment>
|
<React.Fragment> · </React.Fragment>
|
||||||
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(status.get('edited_at'), { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} />
|
<EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ import {
|
|||||||
muteStatus,
|
muteStatus,
|
||||||
unmuteStatus,
|
unmuteStatus,
|
||||||
deleteStatus,
|
deleteStatus,
|
||||||
|
editStatus,
|
||||||
hideStatus,
|
hideStatus,
|
||||||
revealStatus,
|
revealStatus,
|
||||||
hideQuote,
|
hideQuote,
|
||||||
@ -300,6 +301,10 @@ class Status extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleEditClick = (status, history) => {
|
||||||
|
this.props.dispatch(editStatus(status.get('id'), history));
|
||||||
|
}
|
||||||
|
|
||||||
handleDirectClick = (account, router) => {
|
handleDirectClick = (account, router) => {
|
||||||
this.props.dispatch(directCompose(account, router));
|
this.props.dispatch(directCompose(account, router));
|
||||||
}
|
}
|
||||||
@ -606,6 +611,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
onQuote={this.handleQuoteClick}
|
onQuote={this.handleQuoteClick}
|
||||||
onBookmark={this.handleBookmarkClick}
|
onBookmark={this.handleBookmarkClick}
|
||||||
onDelete={this.handleDeleteClick}
|
onDelete={this.handleDeleteClick}
|
||||||
|
onEdit={this.handleEditClick}
|
||||||
onDirect={this.handleDirectClick}
|
onDirect={this.handleDirectClick}
|
||||||
onMention={this.handleMentionClick}
|
onMention={this.handleMentionClick}
|
||||||
onMute={this.handleMuteClick}
|
onMute={this.handleMuteClick}
|
||||||
|
@ -53,7 +53,7 @@ const messages = defineMessages({
|
|||||||
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
|
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const shouldHideFAB = path => path.match(/^\/statuses\/|^\/@[^/]+\/\d+|^\/publish|^\/search|^\/getting-started|^\/start/);
|
const shouldHideFAB = path => path.match(/^\/statuses\/|^\/@[^/]+\/\d+|^\/publish|^\/explore|^\/getting-started|^\/start/);
|
||||||
|
|
||||||
export default @(component => injectIntl(component, { withRef: true }))
|
export default @(component => injectIntl(component, { withRef: true }))
|
||||||
class ColumnsArea extends ImmutablePureComponent {
|
class ColumnsArea extends ImmutablePureComponent {
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { closeModal } from 'mastodon/actions/modal';
|
||||||
|
import emojify from 'mastodon/features/emoji/emoji';
|
||||||
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
|
import InlineAccount from 'mastodon/components/inline_account';
|
||||||
|
import IconButton from 'mastodon/components/icon_button';
|
||||||
|
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { statusId }) => ({
|
||||||
|
versions: state.getIn(['history', statusId, 'items']),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
dispatch(closeModal());
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||||
|
class CompareHistoryModal extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
|
statusId: PropTypes.string.isRequired,
|
||||||
|
versions: ImmutablePropTypes.list.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { index, versions, onClose } = this.props;
|
||||||
|
const currentVersion = versions.get(index);
|
||||||
|
|
||||||
|
const emojiMap = currentVersion.get('emojis').reduce((obj, emoji) => {
|
||||||
|
obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const content = { __html: emojify(currentVersion.get('content'), emojiMap) };
|
||||||
|
const spoilerContent = { __html: emojify(escapeTextContentForBrowser(currentVersion.get('spoiler_text')), emojiMap) };
|
||||||
|
|
||||||
|
const formattedDate = <RelativeTimestamp timestamp={currentVersion.get('created_at')} short={false} />;
|
||||||
|
const formattedName = <InlineAccount accountId={currentVersion.get('account')} />;
|
||||||
|
|
||||||
|
const label = currentVersion.get('original') ? (
|
||||||
|
<FormattedMessage id='status.history.created' defaultMessage='{name} created {date}' values={{ name: formattedName, date: formattedDate }} />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage id='status.history.edited' defaultMessage='{name} edited {date}' values={{ name: formattedName, date: formattedDate }} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal compare-history-modal'>
|
||||||
|
<div className='report-modal__target'>
|
||||||
|
<IconButton className='report-modal__close' icon='times' onClick={onClose} size={20} />
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='compare-history-modal__container'>
|
||||||
|
<div className='status__content'>
|
||||||
|
{currentVersion.get('spoiler_text').length > 0 && (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className='translate' dangerouslySetInnerHTML={spoilerContent} />
|
||||||
|
<hr />
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='status__content__text status__content__text--visible translate' dangerouslySetInnerHTML={content} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -19,7 +19,8 @@ import {
|
|||||||
EmbedModal,
|
EmbedModal,
|
||||||
ListEditor,
|
ListEditor,
|
||||||
ListAdder,
|
ListAdder,
|
||||||
} from '../../../features/ui/util/async-components';
|
CompareHistoryModal,
|
||||||
|
} from 'mastodon/features/ui/util/async-components';
|
||||||
|
|
||||||
const MODAL_COMPONENTS = {
|
const MODAL_COMPONENTS = {
|
||||||
'MEDIA': () => Promise.resolve({ default: MediaModal }),
|
'MEDIA': () => Promise.resolve({ default: MediaModal }),
|
||||||
@ -35,6 +36,7 @@ const MODAL_COMPONENTS = {
|
|||||||
'LIST_EDITOR': ListEditor,
|
'LIST_EDITOR': ListEditor,
|
||||||
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
|
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
|
||||||
'LIST_ADDER': ListAdder,
|
'LIST_ADDER': ListAdder,
|
||||||
|
'COMPARE_HISTORY': CompareHistoryModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class ModalRoot extends React.PureComponent {
|
export default class ModalRoot extends React.PureComponent {
|
||||||
@ -43,6 +45,7 @@ export default class ModalRoot extends React.PureComponent {
|
|||||||
type: PropTypes.string,
|
type: PropTypes.string,
|
||||||
props: PropTypes.object,
|
props: PropTypes.object,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
|
ignoreFocus: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -77,7 +80,7 @@ export default class ModalRoot extends React.PureComponent {
|
|||||||
return <BundleModalError {...props} onClose={onClose} />;
|
return <BundleModalError {...props} onClose={onClose} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClose = () => {
|
handleClose = (ignoreFocus = false) => {
|
||||||
const { onClose } = this.props;
|
const { onClose } = this.props;
|
||||||
let message = null;
|
let message = null;
|
||||||
try {
|
try {
|
||||||
@ -87,7 +90,7 @@ export default class ModalRoot extends React.PureComponent {
|
|||||||
// isn't set.
|
// isn't set.
|
||||||
// This would be much smoother with react-intl 3+ and `forwardRef`.
|
// This would be much smoother with react-intl 3+ and `forwardRef`.
|
||||||
}
|
}
|
||||||
onClose(message);
|
onClose(message, ignoreFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
setModalRef = (c) => {
|
setModalRef = (c) => {
|
||||||
@ -95,12 +98,12 @@ export default class ModalRoot extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { type, props } = this.props;
|
const { type, props, ignoreFocus } = this.props;
|
||||||
const { backgroundColor } = this.state;
|
const { backgroundColor } = this.state;
|
||||||
const visible = !!type;
|
const visible = !!type;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Base backgroundColor={backgroundColor} onClose={this.handleClose}>
|
<Base backgroundColor={backgroundColor} onClose={this.handleClose} ignoreFocus={ignoreFocus}>
|
||||||
{visible && (
|
{visible && (
|
||||||
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
|
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
|
||||||
{(SpecificComponent) => <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={this.setModalRef} />}
|
{(SpecificComponent) => <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={this.setModalRef} />}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user