Merge branch 'main' of https://github.com/glitch-soc/mastodon
This commit is contained in:
commit
e2971a743f
|
@ -32,7 +32,7 @@ commands:
|
|||
name: Install system dependencies
|
||||
command: |
|
||||
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:
|
||||
parameters:
|
||||
ruby-version:
|
||||
|
@ -45,7 +45,7 @@ commands:
|
|||
bundle config without 'development production'
|
||||
name: Set bundler settings
|
||||
- ruby/install-deps:
|
||||
bundler-version: '2.2.31'
|
||||
bundler-version: '2.3.8'
|
||||
key: ruby<< parameters.ruby-version >>-gems-v1
|
||||
wait-db:
|
||||
steps:
|
||||
|
|
8
.github/workflows/build-image.yml
vendored
8
.github/workflows/build-image.yml
vendored
|
@ -6,6 +6,10 @@ on:
|
|||
- "main"
|
||||
tags:
|
||||
- "*"
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/build-image.yml
|
||||
- Dockerfile
|
||||
jobs:
|
||||
build-image:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -18,6 +22,7 @@ jobs:
|
|||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
if: github.event_name != 'pull_request'
|
||||
- uses: docker/metadata-action@v3
|
||||
id: meta
|
||||
with:
|
||||
|
@ -27,11 +32,12 @@ jobs:
|
|||
tags: |
|
||||
type=edge,branch=main
|
||||
type=semver,pattern={{ raw }}
|
||||
type=ref,event=pr
|
||||
- uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/mastodon:latest
|
||||
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
|
||||
run: |
|
||||
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
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
|
|
|
@ -32,10 +32,11 @@ Layout/EmptyLineAfterGuardClause:
|
|||
Layout/EmptyLinesAroundAttributeAccessor:
|
||||
Enabled: true
|
||||
|
||||
Layout/FirstHashElementIndentation:
|
||||
EnforcedStyle: consistent
|
||||
|
||||
Layout/HashAlignment:
|
||||
Enabled: false
|
||||
# EnforcedHashRocketStyle: table
|
||||
# EnforcedColonStyle: table
|
||||
|
||||
Layout/SpaceAroundMethodCallOperator:
|
||||
Enabled: true
|
||||
|
|
2
Aptfile
2
Aptfile
|
@ -4,10 +4,8 @@ libicu-dev
|
|||
libidn11
|
||||
libidn11-dev
|
||||
libpq-dev
|
||||
libprotobuf-dev
|
||||
libxdamage1
|
||||
libxfixes3
|
||||
protobuf-compiler
|
||||
zlib1g-dev
|
||||
libcairo2
|
||||
libcroco3
|
||||
|
|
|
@ -51,7 +51,7 @@ RUN npm install -g npm@latest && \
|
|||
gem install bundler && \
|
||||
apt-get update && \
|
||||
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/
|
||||
|
||||
|
@ -88,7 +88,7 @@ RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selectio
|
|||
RUN apt-get update && \
|
||||
apt-get -y --no-install-recommends install \
|
||||
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 && \
|
||||
ln -s /opt/mastodon /mastodon && \
|
||||
gem install bundler && \
|
||||
|
|
20
Gemfile
20
Gemfile
|
@ -6,7 +6,7 @@ ruby '>= 2.5.0', '< 3.1.0'
|
|||
gem 'pkg-config', '~> 1.4'
|
||||
gem 'rexml', '~> 3.2'
|
||||
|
||||
gem 'puma', '~> 5.5'
|
||||
gem 'puma', '~> 5.6'
|
||||
gem 'rails', '~> 6.1.4'
|
||||
gem 'sprockets', '~> 3.7.2'
|
||||
gem 'thor', '~> 1.2'
|
||||
|
@ -18,20 +18,18 @@ gem 'makara', '~> 0.5'
|
|||
gem 'pghero', '~> 2.8'
|
||||
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-openstack', '~> 0.3', require: false
|
||||
gem 'kt-paperclip', '~> 7.0'
|
||||
gem 'kt-paperclip', '~> 7.1'
|
||||
gem 'blurhash', '~> 0.1'
|
||||
|
||||
gem 'active_model_serializers', '~> 0.10'
|
||||
gem 'addressable', '~> 2.8'
|
||||
gem 'bootsnap', '~> 1.10.2', require: false
|
||||
gem 'bootsnap', '~> 1.10.3', require: false
|
||||
gem 'browser'
|
||||
gem 'charlock_holmes', '~> 0.7.7'
|
||||
gem 'iso-639'
|
||||
gem 'chewy', '~> 7.2'
|
||||
gem 'cld3', '~> 3.4.4'
|
||||
gem 'devise', '~> 4.8'
|
||||
gem 'devise-two-factor', '~> 4.0'
|
||||
|
||||
|
@ -67,7 +65,7 @@ gem 'oj', '~> 3.13'
|
|||
gem 'ox', '~> 2.14'
|
||||
gem 'parslet'
|
||||
gem 'posix-spawn'
|
||||
gem 'pundit', '~> 2.1'
|
||||
gem 'pundit', '~> 2.2'
|
||||
gem 'premailer-rails'
|
||||
gem 'rack-attack', '~> 6.5'
|
||||
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 'ruby-progressbar', '~> 1.11'
|
||||
gem 'sanitize', '~> 6.0'
|
||||
gem 'scenic', '~> 1.5'
|
||||
gem 'scenic', '~> 1.6'
|
||||
gem 'sidekiq', '~> 6.4'
|
||||
gem 'sidekiq-scheduler', '~> 3.1'
|
||||
gem 'sidekiq-unique-jobs', '~> 7.1'
|
||||
|
@ -102,12 +100,12 @@ gem 'rdf-normalize', '~> 0.5'
|
|||
gem 'redcarpet', '~> 3.5'
|
||||
|
||||
group :development, :test do
|
||||
gem 'fabrication', '~> 2.24'
|
||||
gem 'fabrication', '~> 2.27'
|
||||
gem 'fuubar', '~> 2.5'
|
||||
gem 'i18n-tasks', '~> 0.9', require: false
|
||||
gem 'pry-byebug', '~> 3.9'
|
||||
gem 'pry-rails', '~> 0.3'
|
||||
gem 'rspec-rails', '~> 5.0'
|
||||
gem 'rspec-rails', '~> 5.1'
|
||||
end
|
||||
|
||||
group :production, :test do
|
||||
|
@ -128,7 +126,7 @@ end
|
|||
|
||||
group :development do
|
||||
gem 'active_record_query_trace', '~> 1.8'
|
||||
gem 'annotate', '~> 3.1'
|
||||
gem 'annotate', '~> 3.2'
|
||||
gem 'better_errors', '~> 2.9'
|
||||
gem 'binding_of_caller', '~> 1.0'
|
||||
gem 'bullet', '~> 7.0'
|
||||
|
|
221
Gemfile.lock
221
Gemfile.lock
|
@ -1,40 +1,40 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (6.1.4.4)
|
||||
actionpack (= 6.1.4.4)
|
||||
activesupport (= 6.1.4.4)
|
||||
actioncable (6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.1.4.4)
|
||||
actionpack (= 6.1.4.4)
|
||||
activejob (= 6.1.4.4)
|
||||
activerecord (= 6.1.4.4)
|
||||
activestorage (= 6.1.4.4)
|
||||
activesupport (= 6.1.4.4)
|
||||
actionmailbox (6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
activejob (= 6.1.4.6)
|
||||
activerecord (= 6.1.4.6)
|
||||
activestorage (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.1.4.4)
|
||||
actionpack (= 6.1.4.4)
|
||||
actionview (= 6.1.4.4)
|
||||
activejob (= 6.1.4.4)
|
||||
activesupport (= 6.1.4.4)
|
||||
actionmailer (6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
actionview (= 6.1.4.6)
|
||||
activejob (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.1.4.4)
|
||||
actionview (= 6.1.4.4)
|
||||
activesupport (= 6.1.4.4)
|
||||
actionpack (6.1.4.6)
|
||||
actionview (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
rack (~> 2.0, >= 2.0.9)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.1.4.4)
|
||||
actionpack (= 6.1.4.4)
|
||||
activerecord (= 6.1.4.4)
|
||||
activestorage (= 6.1.4.4)
|
||||
activesupport (= 6.1.4.4)
|
||||
actiontext (6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
activerecord (= 6.1.4.6)
|
||||
activestorage (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.1.4.4)
|
||||
activesupport (= 6.1.4.4)
|
||||
actionview (6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
|
@ -45,22 +45,22 @@ GEM
|
|||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
active_record_query_trace (1.8)
|
||||
activejob (6.1.4.4)
|
||||
activesupport (= 6.1.4.4)
|
||||
activejob (6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.1.4.4)
|
||||
activesupport (= 6.1.4.4)
|
||||
activerecord (6.1.4.4)
|
||||
activemodel (= 6.1.4.4)
|
||||
activesupport (= 6.1.4.4)
|
||||
activestorage (6.1.4.4)
|
||||
actionpack (= 6.1.4.4)
|
||||
activejob (= 6.1.4.4)
|
||||
activerecord (= 6.1.4.4)
|
||||
activesupport (= 6.1.4.4)
|
||||
activemodel (6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
activerecord (6.1.4.6)
|
||||
activemodel (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
activestorage (6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
activejob (= 6.1.4.6)
|
||||
activerecord (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
marcel (~> 1.0.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (6.1.4.4)
|
||||
activesupport (6.1.4.6)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
|
@ -71,25 +71,25 @@ GEM
|
|||
airbrussh (1.4.0)
|
||||
sshkit (>= 1.6.1, != 1.7.0)
|
||||
android_key_attestation (0.3.0)
|
||||
annotate (3.1.1)
|
||||
activerecord (>= 3.2, < 7.0)
|
||||
annotate (3.2.0)
|
||||
activerecord (>= 3.2, < 8.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.2)
|
||||
attr_encrypted (3.1.0)
|
||||
encryptor (~> 3.0.0)
|
||||
awrence (1.1.1)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.549.0)
|
||||
aws-sdk-core (3.125.5)
|
||||
aws-partitions (1.558.0)
|
||||
aws-sdk-core (3.127.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.53.0)
|
||||
aws-sdk-core (~> 3, >= 3.125.0)
|
||||
aws-sdk-kms (1.55.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.111.3)
|
||||
aws-sdk-core (~> 3, >= 3.125.0)
|
||||
aws-sdk-s3 (1.113.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.4.0)
|
||||
|
@ -102,9 +102,9 @@ GEM
|
|||
bindata (2.4.10)
|
||||
binding_of_caller (1.0.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
blurhash (0.1.5)
|
||||
blurhash (0.1.6)
|
||||
ffi (~> 1.14)
|
||||
bootsnap (1.10.2)
|
||||
bootsnap (1.10.3)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (5.2.1)
|
||||
browser (4.2.0)
|
||||
|
@ -147,13 +147,11 @@ GEM
|
|||
activesupport
|
||||
cbor (0.5.9.6)
|
||||
charlock_holmes (0.7.7)
|
||||
chewy (7.2.3)
|
||||
chewy (7.2.4)
|
||||
activesupport (>= 5.2)
|
||||
elasticsearch (>= 7.12.0, < 7.14.0)
|
||||
elasticsearch-dsl
|
||||
chunky_png (1.4.0)
|
||||
cld3 (3.4.4)
|
||||
ffi (>= 1.1.0, < 1.16.0)
|
||||
climate_control (0.2.0)
|
||||
coderay (1.1.3)
|
||||
color_diff (0.1)
|
||||
|
@ -183,7 +181,7 @@ GEM
|
|||
devise_pam_authenticatable2 (9.2.0)
|
||||
devise (>= 4.0.0)
|
||||
rpam2 (~> 4.0)
|
||||
diff-lcs (1.4.4)
|
||||
diff-lcs (1.5.0)
|
||||
discard (1.2.1)
|
||||
activerecord (>= 4.2, < 8)
|
||||
docile (1.3.4)
|
||||
|
@ -211,28 +209,32 @@ GEM
|
|||
et-orbi (1.2.6)
|
||||
tzinfo
|
||||
excon (0.76.0)
|
||||
fabrication (2.24.0)
|
||||
fabrication (2.27.0)
|
||||
faker (2.19.0)
|
||||
i18n (>= 1.6, < 2)
|
||||
faraday (1.8.0)
|
||||
faraday (1.9.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
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_persistent (~> 1.1)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.3)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday-net_http (1.0.1)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
fast_blank (1.0.1)
|
||||
fastimage (2.2.6)
|
||||
ffi (1.15.5)
|
||||
|
@ -289,7 +291,7 @@ GEM
|
|||
httplog (1.5.0)
|
||||
rack (>= 1.0)
|
||||
rainbow (>= 2.0.0)
|
||||
i18n (1.8.11)
|
||||
i18n (1.9.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-tasks (0.9.37)
|
||||
activesupport (>= 4.0.2)
|
||||
|
@ -303,8 +305,7 @@ GEM
|
|||
terminal-table (>= 1.5.1)
|
||||
idn-ruby (0.1.4)
|
||||
ipaddress (0.8.3)
|
||||
iso-639 (0.3.5)
|
||||
jmespath (1.5.0)
|
||||
jmespath (1.6.0)
|
||||
json (2.5.1)
|
||||
json-canonicalization (0.3.0)
|
||||
json-ld (3.2.0)
|
||||
|
@ -331,7 +332,7 @@ GEM
|
|||
activerecord
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-core (1.2.2)
|
||||
kt-paperclip (7.0.1)
|
||||
kt-paperclip (7.1.1)
|
||||
activemodel (>= 4.2.0)
|
||||
activesupport (>= 4.2.0)
|
||||
marcel (~> 1.0.1)
|
||||
|
@ -355,14 +356,14 @@ GEM
|
|||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.13.0)
|
||||
loofah (2.14.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
makara (0.5.1)
|
||||
activerecord (>= 5.2.0)
|
||||
marcel (1.0.1)
|
||||
marcel (1.0.2)
|
||||
mario-redis-lock (1.2.1)
|
||||
redis (>= 3.0.5)
|
||||
matrix (0.4.2)
|
||||
|
@ -373,9 +374,9 @@ GEM
|
|||
nokogiri (~> 1.10)
|
||||
mime-types (3.4.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2021.1115)
|
||||
mime-types-data (3.2022.0105)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.7.1)
|
||||
mini_portile2 (2.8.0)
|
||||
minitest (5.15.0)
|
||||
msgpack (1.4.4)
|
||||
multi_json (1.15.0)
|
||||
|
@ -385,8 +386,8 @@ GEM
|
|||
net-ssh (>= 2.6.5, < 7.0.0)
|
||||
net-ssh (6.1.0)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.13.1)
|
||||
mini_portile2 (~> 2.7.0)
|
||||
nokogiri (1.13.3)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
racc (~> 1.4)
|
||||
nsa (0.2.8)
|
||||
activesupport (>= 4.2, < 7)
|
||||
|
@ -410,14 +411,14 @@ GEM
|
|||
openssl (2.2.0)
|
||||
openssl-signature_algorithm (0.4.0)
|
||||
orm_adapter (0.5.0)
|
||||
ox (2.14.6)
|
||||
ox (2.14.9)
|
||||
parallel (1.21.0)
|
||||
parser (3.1.0.0)
|
||||
ast (~> 2.4.1)
|
||||
parslet (2.0.0)
|
||||
pastel (0.8.0)
|
||||
tty-color (~> 0.5)
|
||||
pg (1.3.0)
|
||||
pg (1.3.3)
|
||||
pghero (2.8.2)
|
||||
activerecord (>= 5)
|
||||
pkg-config (1.4.7)
|
||||
|
@ -439,9 +440,9 @@ GEM
|
|||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.6)
|
||||
puma (5.5.2)
|
||||
puma (5.6.2)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.1)
|
||||
pundit (2.2.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.6.0)
|
||||
|
@ -454,20 +455,20 @@ GEM
|
|||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rails (6.1.4.4)
|
||||
actioncable (= 6.1.4.4)
|
||||
actionmailbox (= 6.1.4.4)
|
||||
actionmailer (= 6.1.4.4)
|
||||
actionpack (= 6.1.4.4)
|
||||
actiontext (= 6.1.4.4)
|
||||
actionview (= 6.1.4.4)
|
||||
activejob (= 6.1.4.4)
|
||||
activemodel (= 6.1.4.4)
|
||||
activerecord (= 6.1.4.4)
|
||||
activestorage (= 6.1.4.4)
|
||||
activesupport (= 6.1.4.4)
|
||||
rails (6.1.4.6)
|
||||
actioncable (= 6.1.4.6)
|
||||
actionmailbox (= 6.1.4.6)
|
||||
actionmailer (= 6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
actiontext (= 6.1.4.6)
|
||||
actionview (= 6.1.4.6)
|
||||
activejob (= 6.1.4.6)
|
||||
activemodel (= 6.1.4.6)
|
||||
activerecord (= 6.1.4.6)
|
||||
activestorage (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 6.1.4.4)
|
||||
railties (= 6.1.4.6)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
|
@ -483,9 +484,9 @@ GEM
|
|||
railties (>= 6.0.0, < 7)
|
||||
rails-settings-cached (0.6.6)
|
||||
rails (>= 4.2.0)
|
||||
railties (6.1.4.4)
|
||||
actionpack (= 6.1.4.4)
|
||||
activesupport (= 6.1.4.4)
|
||||
railties (6.1.4.6)
|
||||
actionpack (= 6.1.4.6)
|
||||
activesupport (= 6.1.4.6)
|
||||
method_source
|
||||
rake (>= 0.13)
|
||||
thor (~> 1.0)
|
||||
|
@ -508,19 +509,19 @@ GEM
|
|||
rexml (3.2.5)
|
||||
rotp (6.2.0)
|
||||
rpam2 (4.0.2)
|
||||
rqrcode (2.1.0)
|
||||
rqrcode (2.1.1)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 1.0)
|
||||
rqrcode_core (1.2.0)
|
||||
rspec-core (3.10.1)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-expectations (3.10.1)
|
||||
rspec-core (3.11.0)
|
||||
rspec-support (~> 3.11.0)
|
||||
rspec-expectations (3.11.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-mocks (3.10.2)
|
||||
rspec-support (~> 3.11.0)
|
||||
rspec-mocks (3.11.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-rails (5.0.2)
|
||||
rspec-support (~> 3.11.0)
|
||||
rspec-rails (5.1.0)
|
||||
actionpack (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
|
@ -531,10 +532,10 @@ GEM
|
|||
rspec-sidekiq (3.1.0)
|
||||
rspec-core (~> 3.0, >= 3.0.0)
|
||||
sidekiq (>= 2.4.0)
|
||||
rspec-support (3.10.3)
|
||||
rspec-support (3.11.0)
|
||||
rspec_junit_formatter (0.5.1)
|
||||
rspec-core (>= 2, < 4, != 2.12.0)
|
||||
rubocop (1.25.0)
|
||||
rubocop (1.25.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.1.0.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
|
@ -561,12 +562,12 @@ GEM
|
|||
sanitize (6.0.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
scenic (1.5.5)
|
||||
scenic (1.6.0)
|
||||
activerecord (>= 4.0.0)
|
||||
railties (>= 4.0.0)
|
||||
securecompare (1.0.0)
|
||||
semantic_range (3.0.0)
|
||||
sidekiq (6.4.0)
|
||||
sidekiq (6.4.1)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.2.0)
|
||||
|
@ -579,7 +580,7 @@ GEM
|
|||
sidekiq (>= 3)
|
||||
thwait
|
||||
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)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||
sidekiq (>= 5.0, < 8.0)
|
||||
|
@ -675,7 +676,7 @@ GEM
|
|||
xorcist (1.1.2)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.5.3)
|
||||
zeitwerk (2.5.4)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
@ -684,12 +685,12 @@ DEPENDENCIES
|
|||
active_model_serializers (~> 0.10)
|
||||
active_record_query_trace (~> 1.8)
|
||||
addressable (~> 2.8)
|
||||
annotate (~> 3.1)
|
||||
aws-sdk-s3 (~> 1.111)
|
||||
annotate (~> 3.2)
|
||||
aws-sdk-s3 (~> 1.113)
|
||||
better_errors (~> 2.9)
|
||||
binding_of_caller (~> 1.0)
|
||||
blurhash (~> 0.1)
|
||||
bootsnap (~> 1.10.2)
|
||||
bootsnap (~> 1.10.3)
|
||||
brakeman (~> 5.2)
|
||||
browser
|
||||
bullet (~> 7.0)
|
||||
|
@ -701,7 +702,6 @@ DEPENDENCIES
|
|||
capybara (~> 3.36)
|
||||
charlock_holmes (~> 0.7.7)
|
||||
chewy (~> 7.2)
|
||||
cld3 (~> 3.4.4)
|
||||
climate_control (~> 0.2)
|
||||
color_diff (~> 0.1)
|
||||
concurrent-ruby
|
||||
|
@ -713,7 +713,7 @@ DEPENDENCIES
|
|||
doorkeeper (~> 5.5)
|
||||
dotenv-rails (~> 2.7)
|
||||
ed25519 (~> 1.3)
|
||||
fabrication (~> 2.24)
|
||||
fabrication (~> 2.27)
|
||||
faker (~> 2.19)
|
||||
fast_blank (~> 1.0)
|
||||
fastimage
|
||||
|
@ -729,11 +729,10 @@ DEPENDENCIES
|
|||
httplog (~> 1.5.0)
|
||||
i18n-tasks (~> 0.9)
|
||||
idn-ruby
|
||||
iso-639
|
||||
json-ld
|
||||
json-ld-preloaded (~> 3.2)
|
||||
kaminari (~> 1.2)
|
||||
kt-paperclip (~> 7.0)
|
||||
kt-paperclip (~> 7.1)
|
||||
letter_opener (~> 1.7)
|
||||
letter_opener_web (~> 2.0)
|
||||
link_header (~> 0.0)
|
||||
|
@ -761,8 +760,8 @@ DEPENDENCIES
|
|||
private_address_check (~> 0.5)
|
||||
pry-byebug (~> 3.9)
|
||||
pry-rails (~> 0.3)
|
||||
puma (~> 5.5)
|
||||
pundit (~> 2.1)
|
||||
puma (~> 5.6)
|
||||
pundit (~> 2.2)
|
||||
rack (~> 2.2.3)
|
||||
rack-attack (~> 6.5)
|
||||
rack-cors (~> 1.1)
|
||||
|
@ -776,14 +775,14 @@ DEPENDENCIES
|
|||
redis-namespace (~> 1.8)
|
||||
rexml (~> 3.2)
|
||||
rqrcode (~> 2.1)
|
||||
rspec-rails (~> 5.0)
|
||||
rspec-rails (~> 5.1)
|
||||
rspec-sidekiq (~> 3.1)
|
||||
rspec_junit_formatter (~> 0.5)
|
||||
rubocop (~> 1.25)
|
||||
rubocop-rails (~> 2.13)
|
||||
ruby-progressbar (~> 1.11)
|
||||
sanitize (~> 6.0)
|
||||
scenic (~> 1.5)
|
||||
scenic (~> 1.6)
|
||||
sidekiq (~> 6.4)
|
||||
sidekiq-bulk (~> 0.2.0)
|
||||
sidekiq-scheduler (~> 3.1)
|
||||
|
|
2
Vagrantfile
vendored
2
Vagrantfile
vendored
|
@ -33,11 +33,9 @@ sudo apt-get install \
|
|||
redis-tools \
|
||||
postgresql \
|
||||
postgresql-contrib \
|
||||
protobuf-compiler \
|
||||
yarn \
|
||||
libicu-dev \
|
||||
libidn11-dev \
|
||||
libprotobuf-dev \
|
||||
libreadline-dev \
|
||||
libpam0g-dev \
|
||||
-y
|
||||
|
|
|
@ -63,15 +63,29 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
|
|||
end
|
||||
|
||||
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,
|
||||
page: true,
|
||||
min_id: only_other_accounts && !only_other_accounts? ? nil : @replies&.last&.id,
|
||||
only_other_accounts: only_other_accounts
|
||||
)
|
||||
account_status_replies_url(
|
||||
@account,
|
||||
@status,
|
||||
page: true,
|
||||
min_id: @replies&.last&.id,
|
||||
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
|
||||
|
||||
def page_params
|
||||
|
|
|
@ -28,7 +28,7 @@ module Admin
|
|||
@deletion_request = @account.deletion_request
|
||||
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
|
||||
@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)
|
||||
end
|
||||
|
||||
|
@ -146,7 +146,7 @@ module Admin
|
|||
end
|
||||
|
||||
def filter_params
|
||||
params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS)
|
||||
params.slice(:page, *AccountFilter::KEYS).permit(:page, *AccountFilter::KEYS)
|
||||
end
|
||||
|
||||
def form_account_batch_params
|
||||
|
|
|
@ -8,6 +8,7 @@ module Admin
|
|||
@pending_users_count = User.pending.count
|
||||
@pending_reports_count = Report.unresolved.count
|
||||
@pending_tags_count = Tag.pending_review.count
|
||||
@pending_appeals_count = Appeal.pending.count
|
||||
end
|
||||
|
||||
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
|
||||
authorize :email_domain_block, :index?
|
||||
|
||||
@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
|
||||
|
||||
def new
|
||||
|
@ -19,41 +32,27 @@ module Admin
|
|||
|
||||
@email_domain_block = EmailDomainBlock.new(resource_params)
|
||||
|
||||
if @email_domain_block.save
|
||||
log_action :create, @email_domain_block
|
||||
if action_from_button == 'save'
|
||||
EmailDomainBlock.transaction do
|
||||
@email_domain_block.save!
|
||||
log_action :create, @email_domain_block
|
||||
|
||||
if @email_domain_block.with_dns_records?
|
||||
hostnames = []
|
||||
ips = []
|
||||
(@email_domain_block.other_domains || []).uniq.each do |domain|
|
||||
next if EmailDomainBlock.where(domain: domain).exists?
|
||||
|
||||
Resolv::DNS.open do |dns|
|
||||
dns.timeouts = 5
|
||||
|
||||
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
|
||||
other_email_domain_block = EmailDomainBlock.create!(domain: domain, parent: @email_domain_block)
|
||||
log_action :create, other_email_domain_block
|
||||
end
|
||||
end
|
||||
|
||||
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg')
|
||||
else
|
||||
set_resolved_records
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
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')
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
set_resolved_records
|
||||
render :new
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -62,8 +61,27 @@ module Admin
|
|||
@email_domain_block = EmailDomainBlock.find(params[:id])
|
||||
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
|
||||
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
|
||||
|
|
52
app/controllers/admin/reports/actions_controller.rb
Normal file
52
app/controllers/admin/reports/actions_controller.rb
Normal file
|
@ -0,0 +1,52 @@
|
|||
# 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', 'mark_as_sensitive'
|
||||
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[:mark_as_sensitive]
|
||||
'mark_as_sensitive'
|
||||
elsif params[:silence]
|
||||
'silence'
|
||||
elsif params[:suspend]
|
||||
'suspend'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -29,8 +29,9 @@ module Admin
|
|||
end
|
||||
|
||||
def after_create_redirect_path
|
||||
if @status_batch_action.report_id.present?
|
||||
admin_report_path(@status_batch_action.report_id)
|
||||
report_id = @status_batch_action&.report_id || params[:report_id]
|
||||
if report_id.present?
|
||||
admin_report_path(report_id)
|
||||
else
|
||||
admin_account_statuses_path(params[:account_id], current_params)
|
||||
end
|
||||
|
@ -48,6 +49,10 @@ module Admin
|
|||
params.slice(*Admin::StatusFilter::KEYS).permit(*Admin::StatusFilter::KEYS)
|
||||
end
|
||||
|
||||
def current_params
|
||||
params.slice(:media, :page).permit(:media, :page)
|
||||
end
|
||||
|
||||
def action_from_button
|
||||
if params[:report]
|
||||
'report'
|
||||
|
|
|
@ -5,11 +5,11 @@ class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseControll
|
|||
authorize :preview_card_provider, :index?
|
||||
|
||||
@preview_card_providers = filtered_preview_card_providers.page(params[:page])
|
||||
@form = Form::PreviewCardProviderBatch.new
|
||||
@form = Trends::PreviewCardProviderBatch.new
|
||||
end
|
||||
|
||||
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
|
||||
rescue ActionController::ParameterMissing
|
||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||
|
@ -20,15 +20,15 @@ class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseControll
|
|||
private
|
||||
|
||||
def filtered_preview_card_providers
|
||||
PreviewCardProviderFilter.new(filter_params).results
|
||||
Trends::PreviewCardProviderFilter.new(filter_params).results
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.slice(:page, *PreviewCardProviderFilter::KEYS).permit(:page, *PreviewCardProviderFilter::KEYS)
|
||||
params.slice(:page, *Trends::PreviewCardProviderFilter::KEYS).permit(:page, *Trends::PreviewCardProviderFilter::KEYS)
|
||||
end
|
||||
|
||||
def form_preview_card_provider_batch_params
|
||||
params.require(:form_preview_card_provider_batch).permit(:action, preview_card_provider_ids: [])
|
||||
def trends_preview_card_provider_batch_params
|
||||
params.require(:trends_preview_card_provider_batch).permit(:action, preview_card_provider_ids: [])
|
||||
end
|
||||
|
||||
def action_from_button
|
||||
|
|
|
@ -5,11 +5,11 @@ class Admin::Trends::LinksController < Admin::BaseController
|
|||
authorize :preview_card, :index?
|
||||
|
||||
@preview_cards = filtered_preview_cards.page(params[:page])
|
||||
@form = Form::PreviewCardBatch.new
|
||||
@form = Trends::PreviewCardBatch.new
|
||||
end
|
||||
|
||||
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
|
||||
rescue ActionController::ParameterMissing
|
||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||
|
@ -20,26 +20,26 @@ class Admin::Trends::LinksController < Admin::BaseController
|
|||
private
|
||||
|
||||
def filtered_preview_cards
|
||||
PreviewCardFilter.new(filter_params.with_defaults(trending: 'all')).results
|
||||
Trends::PreviewCardFilter.new(filter_params.with_defaults(trending: 'all')).results
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.slice(:page, *PreviewCardFilter::KEYS).permit(:page, *PreviewCardFilter::KEYS)
|
||||
params.slice(:page, *Trends::PreviewCardFilter::KEYS).permit(:page, *Trends::PreviewCardFilter::KEYS)
|
||||
end
|
||||
|
||||
def form_preview_card_batch_params
|
||||
params.require(:form_preview_card_batch).permit(:action, preview_card_ids: [])
|
||||
def trends_preview_card_batch_params
|
||||
params.require(:trends_preview_card_batch).permit(:action, preview_card_ids: [])
|
||||
end
|
||||
|
||||
def action_from_button
|
||||
if params[:approve]
|
||||
'approve'
|
||||
elsif params[:approve_all]
|
||||
'approve_all'
|
||||
elsif params[:approve_providers]
|
||||
'approve_providers'
|
||||
elsif params[:reject]
|
||||
'reject'
|
||||
elsif params[:reject_all]
|
||||
'reject_all'
|
||||
elsif params[:reject_providers]
|
||||
'reject_providers'
|
||||
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?
|
||||
|
||||
@tags = filtered_tags.page(params[:page])
|
||||
@form = Form::TagBatch.new
|
||||
@form = Trends::TagBatch.new
|
||||
end
|
||||
|
||||
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
|
||||
rescue ActionController::ParameterMissing
|
||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||
|
@ -20,15 +20,15 @@ class Admin::Trends::TagsController < Admin::BaseController
|
|||
private
|
||||
|
||||
def filtered_tags
|
||||
TagFilter.new(filter_params).results
|
||||
Trends::TagFilter.new(filter_params).results
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.slice(:page, *TagFilter::KEYS).permit(:page, *TagFilter::KEYS)
|
||||
params.slice(:page, *Trends::TagFilter::KEYS).permit(:page, *Trends::TagFilter::KEYS)
|
||||
end
|
||||
|
||||
def form_tag_batch_params
|
||||
params.require(:form_tag_batch).permit(:action, tag_ids: [])
|
||||
def trends_tag_batch_params
|
||||
params.require(:trends_tag_batch).permit(:action, tag_ids: [])
|
||||
end
|
||||
|
||||
def action_from_button
|
||||
|
|
|
@ -5,6 +5,7 @@ class Api::BaseController < ApplicationController
|
|||
DEFAULT_ACCOUNTS_LIMIT = 40
|
||||
|
||||
include RateLimitHeaders
|
||||
include AccessTokenTrackingConcern
|
||||
|
||||
skip_before_action :store_current_location
|
||||
skip_before_action :require_functional!, unless: :whitelist_mode?
|
||||
|
|
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
|
||||
|
||||
def set_tags
|
||||
@tags = Trends.tags.get(false, limit_param(10))
|
||||
@tags = Trends.tags.query.limit(limit_param(10))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,7 +20,7 @@ class Api::V1::MediaController < Api::BaseController
|
|||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
@ -42,6 +42,10 @@ class Api::V1::MediaController < Api::BaseController
|
|||
params.permit(:file, :thumbnail, :description, :focus)
|
||||
end
|
||||
|
||||
def updateable_media_attachment_params
|
||||
params.permit(:thumbnail, :description, :focus)
|
||||
end
|
||||
|
||||
def file_type_error
|
||||
{ error: 'File type of uploaded media could not be verified' }
|
||||
end
|
||||
|
|
|
@ -10,9 +10,7 @@ class Api::V1::ReportsController < Api::BaseController
|
|||
@report = ReportService.new.call(
|
||||
current_account,
|
||||
reported_account,
|
||||
status_ids: reported_status_ids,
|
||||
comment: report_params[:comment],
|
||||
forward: report_params[:forward]
|
||||
report_params
|
||||
)
|
||||
|
||||
render json: @report, serializer: REST::ReportSerializer
|
||||
|
@ -20,19 +18,11 @@ class Api::V1::ReportsController < Api::BaseController
|
|||
|
||||
private
|
||||
|
||||
def reported_status_ids
|
||||
reported_account.statuses.with_discarded.find(status_ids).pluck(:id)
|
||||
end
|
||||
|
||||
def status_ids
|
||||
Array(report_params[:status_ids])
|
||||
end
|
||||
|
||||
def reported_account
|
||||
Account.find(report_params[:account_id])
|
||||
end
|
||||
|
||||
def report_params
|
||||
params.permit(:account_id, :comment, :forward, status_ids: [])
|
||||
params.permit(:account_id, :comment, :category, :forward, status_ids: [], rule_ids: [])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,7 +7,7 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController
|
|||
before_action :set_status
|
||||
|
||||
def show
|
||||
render json: @status.edits, each_serializer: REST::StatusEditSerializer
|
||||
render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
class Api::V1::StatusesController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy]
|
||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
|
||||
before_action :require_user!, except: [:show, :context]
|
||||
before_action :set_status, only: [:show, :context]
|
||||
before_action :set_thread, only: [:create]
|
||||
|
@ -35,25 +35,46 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
end
|
||||
|
||||
def create
|
||||
@status = PostStatusService.new.call(current_user.account,
|
||||
text: status_params[:status],
|
||||
thread: @thread,
|
||||
media_ids: status_params[:media_ids],
|
||||
sensitive: status_params[:sensitive],
|
||||
spoiler_text: status_params[:spoiler_text],
|
||||
visibility: status_params[:visibility],
|
||||
scheduled_at: status_params[:scheduled_at],
|
||||
application: doorkeeper_token.application,
|
||||
poll: status_params[:poll],
|
||||
content_type: status_params[:content_type],
|
||||
idempotency: request.headers['Idempotency-Key'],
|
||||
with_rate_limit: true)
|
||||
@status = PostStatusService.new.call(
|
||||
current_user.account,
|
||||
text: status_params[:status],
|
||||
thread: @thread,
|
||||
media_ids: status_params[:media_ids],
|
||||
sensitive: status_params[:sensitive],
|
||||
spoiler_text: status_params[:spoiler_text],
|
||||
visibility: status_params[:visibility],
|
||||
language: status_params[:language],
|
||||
scheduled_at: status_params[:scheduled_at],
|
||||
application: doorkeeper_token.application,
|
||||
poll: status_params[:poll],
|
||||
content_type: status_params[:content_type],
|
||||
idempotency: request.headers['Idempotency-Key'],
|
||||
with_rate_limit: true
|
||||
)
|
||||
|
||||
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
|
||||
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],
|
||||
content_type: status_params[:content_type]
|
||||
)
|
||||
|
||||
render json: @status, serializer: REST::StatusSerializer
|
||||
end
|
||||
|
||||
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?
|
||||
|
||||
@status.discard
|
||||
|
@ -73,8 +94,9 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
end
|
||||
|
||||
def set_thread
|
||||
@thread = status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
@thread = Status.find(status_params[:in_reply_to_id]) if status_params[:in_reply_to_id].present?
|
||||
authorize(@thread, :show?) if @thread.present?
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404
|
||||
end
|
||||
|
||||
|
@ -85,6 +107,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
:sensitive,
|
||||
:spoiler_text,
|
||||
:visibility,
|
||||
:language,
|
||||
:scheduled_at,
|
||||
:content_type,
|
||||
media_ids: [],
|
||||
|
|
|
@ -12,10 +12,14 @@ class Api::V1::Trends::LinksController < Api::BaseController
|
|||
def set_links
|
||||
@links = begin
|
||||
if Setting.trends
|
||||
Trends.links.get(true, limit_param(10))
|
||||
links_from_trends
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def links_from_trends
|
||||
Trends.links.query.allowed.in_locale(content_locale).limit(limit_param(10))
|
||||
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
|
||||
@tags = begin
|
||||
if Setting.trends
|
||||
Trends.tags.get(true, limit_param(10))
|
||||
Trends.tags.query.allowed.limit(limit_param(10))
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
|
|
@ -17,16 +17,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||
|
||||
data = {
|
||||
policy: 'all',
|
||||
|
||||
alerts: {
|
||||
follow: alerts_enabled,
|
||||
follow_request: alerts_enabled,
|
||||
favourite: alerts_enabled,
|
||||
reblog: alerts_enabled,
|
||||
mention: alerts_enabled,
|
||||
poll: alerts_enabled,
|
||||
status: alerts_enabled,
|
||||
},
|
||||
alerts: Notification::TYPES.index_with { alerts_enabled },
|
||||
}
|
||||
|
||||
data.deep_merge!(data_params) if params[:data]
|
||||
|
@ -61,6 +52,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -10,6 +10,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
before_action :configure_sign_up_params, only: [:create]
|
||||
before_action :set_pack
|
||||
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_body_classes, only: [:new, :create, :edit, :update]
|
||||
before_action :require_not_suspended!, only: [:update]
|
||||
|
@ -116,8 +117,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
end
|
||||
|
||||
def set_invite
|
||||
invite = invite_code.present? ? Invite.find_by(code: invite_code) : nil
|
||||
@invite = invite&.valid_for_use? ? invite : nil
|
||||
@invite = begin
|
||||
invite = Invite.find_by(code: invite_code) if invite_code.present?
|
||||
invite if invite&.valid_for_use?
|
||||
end
|
||||
end
|
||||
|
||||
def determine_layout
|
||||
|
@ -128,6 +131,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
@sessions = current_user.session_activations
|
||||
end
|
||||
|
||||
def set_strikes
|
||||
@strikes = current_account.strikes.recent.latest
|
||||
end
|
||||
|
||||
def require_not_suspended!
|
||||
forbidden if current_account.suspended?
|
||||
end
|
||||
|
|
21
app/controllers/concerns/access_token_tracking_concern.rb
Normal file
21
app/controllers/concerns/access_token_tracking_concern.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AccessTokenTrackingConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
ACCESS_TOKEN_UPDATE_FREQUENCY = 24.hours.freeze
|
||||
|
||||
included do
|
||||
before_action :update_access_token_last_used
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_access_token_last_used
|
||||
doorkeeper_token.update_last_used(request) if access_token_needs_update?
|
||||
end
|
||||
|
||||
def access_token_needs_update?
|
||||
doorkeeper_token.present? && (doorkeeper_token.last_used_at.nil? || doorkeeper_token.last_used_at < ACCESS_TOKEN_UPDATE_FREQUENCY.ago)
|
||||
end
|
||||
end
|
|
@ -3,7 +3,7 @@
|
|||
module Authorization
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
include Pundit
|
||||
include Pundit::Authorization
|
||||
|
||||
def pundit_user
|
||||
current_account
|
||||
|
|
|
@ -7,27 +7,28 @@ module Localized
|
|||
around_action :set_locale
|
||||
end
|
||||
|
||||
def set_locale
|
||||
locale = current_user.locale if respond_to?(:user_signed_in?) && user_signed_in?
|
||||
locale ||= session[:locale] ||= default_locale
|
||||
locale = default_locale unless I18n.available_locales.include?(locale.to_sym)
|
||||
|
||||
I18n.with_locale(locale) do
|
||||
yield
|
||||
end
|
||||
def set_locale(&block)
|
||||
I18n.with_locale(requested_locale || I18n.default_locale, &block)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_locale
|
||||
if ENV['DEFAULT_LOCALE'].present?
|
||||
I18n.default_locale
|
||||
else
|
||||
request_locale || I18n.default_locale
|
||||
end
|
||||
def requested_locale
|
||||
requested_locale_name = available_locale_or_nil(params[:lang])
|
||||
requested_locale_name ||= available_locale_or_nil(current_user.locale) if respond_to?(:user_signed_in?) && user_signed_in?
|
||||
requested_locale_name ||= http_accept_language if ENV['DEFAULT_LOCALE'].blank?
|
||||
requested_locale_name
|
||||
end
|
||||
|
||||
def request_locale
|
||||
http_accept_language.language_region_compatible_from(I18n.available_locales)
|
||||
def http_accept_language
|
||||
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
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
module SessionTrackingConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
UPDATE_SIGN_IN_HOURS = 24
|
||||
SESSION_UPDATE_FREQUENCY = 24.hours.freeze
|
||||
|
||||
included do
|
||||
before_action :set_session_activity
|
||||
|
@ -17,6 +17,6 @@ module SessionTrackingConcern
|
|||
end
|
||||
|
||||
def session_needs_update?
|
||||
!current_session.nil? && current_session.updated_at < UPDATE_SIGN_IN_HOURS.hours.ago
|
||||
!current_session.nil? && current_session.updated_at < SESSION_UPDATE_FREQUENCY.ago
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,7 +20,7 @@ module ThemingConcern
|
|||
end
|
||||
|
||||
def valid_pack_data?(data, pack_name)
|
||||
data['pack'].is_a?(Hash) && [String, Hash].any? { |c| data['pack'][pack_name].is_a?(c) }
|
||||
data['pack'].is_a?(Hash) && data['pack'][pack_name].present?
|
||||
end
|
||||
|
||||
def nil_pack(data)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
module UserTrackingConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
UPDATE_SIGN_IN_FREQUENCY = 24.hours.freeze
|
||||
SIGN_IN_UPDATE_FREQUENCY = 24.hours.freeze
|
||||
|
||||
included do
|
||||
before_action :update_user_sign_in
|
||||
|
@ -16,6 +16,6 @@ module UserTrackingConcern
|
|||
end
|
||||
|
||||
def user_needs_sign_in_update?
|
||||
user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_FREQUENCY.ago)
|
||||
user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < SIGN_IN_UPDATE_FREQUENCY.ago)
|
||||
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
|
23
app/controllers/disputes/base_controller.rb
Normal file
23
app/controllers/disputes/base_controller.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
# 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!
|
||||
before_action :set_pack
|
||||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
use_pack 'admin'
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
end
|
21
app/controllers/disputes/strikes_controller.rb
Normal file
21
app/controllers/disputes/strikes_controller.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Disputes::StrikesController < Disputes::BaseController
|
||||
before_action :set_strike, only: [:show]
|
||||
|
||||
def index
|
||||
@strikes = current_account.strikes.latest
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @strike, :show?
|
||||
|
||||
@appeal = @strike.appeal || @strike.build_appeal
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_strike
|
||||
@strike = AccountWarning.find(params[:id])
|
||||
end
|
||||
end
|
|
@ -58,7 +58,7 @@ class Settings::PreferencesController < Settings::BaseController
|
|||
:setting_use_pending_items,
|
||||
:setting_trends,
|
||||
:setting_crop_images,
|
||||
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag),
|
||||
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag trending_link trending_status),
|
||||
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
||||
)
|
||||
end
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin::AccountModerationNotesHelper
|
||||
def admin_account_link_to(account)
|
||||
def admin_account_link_to(account, path: 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([
|
||||
image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'),
|
||||
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}")})"
|
||||
when 'Instance'
|
||||
record.domain
|
||||
when 'Appeal'
|
||||
link_to record.account.acct, disputes_strike_path(record.strike)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -5,9 +5,10 @@ module Admin::FilterHelper
|
|||
AccountFilter::KEYS,
|
||||
CustomEmojiFilter::KEYS,
|
||||
ReportFilter::KEYS,
|
||||
TagFilter::KEYS,
|
||||
PreviewCardProviderFilter::KEYS,
|
||||
PreviewCardFilter::KEYS,
|
||||
Trends::TagFilter::KEYS,
|
||||
Trends::PreviewCardProviderFilter::KEYS,
|
||||
Trends::PreviewCardFilter::KEYS,
|
||||
Trends::StatusFilter::KEYS,
|
||||
InstanceFilter::KEYS,
|
||||
InviteFilter::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
|
|
@ -225,4 +225,19 @@ module ApplicationHelper
|
|||
content_tag(:script, json_escape(json).html_safe, id: 'initial-state', type: 'application/json')
|
||||
# rubocop:enable Rails/OutputSafety
|
||||
end
|
||||
|
||||
def grouped_scopes(scopes)
|
||||
scope_parser = ScopeParser.new
|
||||
scope_transformer = ScopeTransformer.new
|
||||
|
||||
scopes.each_with_object({}) do |str, h|
|
||||
scope = scope_transformer.apply(scope_parser.parse(str))
|
||||
|
||||
if h[scope.key]
|
||||
h[scope.key].merge!(scope)
|
||||
else
|
||||
h[scope.key] = scope
|
||||
end
|
||||
end.values
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,94 +1,247 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module LanguagesHelper
|
||||
HUMAN_LOCALES = {
|
||||
af: 'Afrikaans',
|
||||
ar: 'العربية',
|
||||
ast: 'Asturianu',
|
||||
bg: 'Български',
|
||||
bn: 'বাংলা',
|
||||
br: 'Breton',
|
||||
ca: 'Català',
|
||||
co: 'Corsu',
|
||||
cs: 'Čeština',
|
||||
cy: 'Cymraeg',
|
||||
da: 'Dansk',
|
||||
de: 'Deutsch',
|
||||
el: 'Ελληνικά',
|
||||
en: 'English',
|
||||
eo: 'Esperanto',
|
||||
ISO_639_1 = {
|
||||
aa: ['Afar', 'Afaraf'].freeze,
|
||||
ab: ['Abkhaz', 'аҧсуа бызшәа'].freeze,
|
||||
ae: ['Avestan', 'avesta'].freeze,
|
||||
af: ['Afrikaans', 'Afrikaans'].freeze,
|
||||
ak: ['Akan', 'Akan'].freeze,
|
||||
am: ['Amharic', 'አማርኛ'].freeze,
|
||||
an: ['Aragonese', 'aragonés'].freeze,
|
||||
ar: ['Arabic', 'اللغة العربية'].freeze,
|
||||
as: ['Assamese', 'অসমীয়া'].freeze,
|
||||
av: ['Avaric', 'авар мацӀ'].freeze,
|
||||
ay: ['Aymara', 'aymar aru'].freeze,
|
||||
az: ['Azerbaijani', 'azərbaycan dili'].freeze,
|
||||
ba: ['Bashkir', 'башҡорт теле'].freeze,
|
||||
be: ['Belarusian', 'беларуская мова'].freeze,
|
||||
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-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-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: 'Српски',
|
||||
sv: 'Svenska',
|
||||
ta: 'தமிழ்',
|
||||
te: 'తెలుగు',
|
||||
th: 'ไทย',
|
||||
tr: 'Türkçe',
|
||||
uk: 'Українська',
|
||||
ur: 'اُردُو',
|
||||
vi: 'Tiếng Việt',
|
||||
zgh: 'ⵜⴰⵎⴰⵣⵉⵖⵜ',
|
||||
'zh-CN': '简体中文',
|
||||
'zh-HK': '繁體中文(香港)',
|
||||
'zh-TW': '繁體中文(臺灣)',
|
||||
zh: '中文',
|
||||
}.freeze
|
||||
|
||||
def human_locale(locale)
|
||||
if locale == 'und'
|
||||
def native_locale_name(locale)
|
||||
if locale.blank? || locale == 'und'
|
||||
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
|
||||
HUMAN_LOCALES[locale.to_sym] || locale
|
||||
locale
|
||||
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
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module SettingsHelper
|
||||
def filterable_languages
|
||||
LanguageDetector.instance.language_names.select(&LanguagesHelper::HUMAN_LOCALES.method(:key?))
|
||||
LanguagesHelper::SUPPORTED_LOCALES.keys
|
||||
end
|
||||
|
||||
def hash_to_object(hash)
|
||||
|
|
|
@ -75,6 +75,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_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
|
||||
|
||||
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
|
||||
|
||||
const messages = defineMessages({
|
||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||
|
@ -88,6 +90,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) {
|
||||
return {
|
||||
type: COMPOSE_CHANGE,
|
||||
|
@ -150,8 +161,9 @@ export function directCompose(account, routerHistory) {
|
|||
|
||||
export function submitCompose(routerHistory) {
|
||||
return function (dispatch, getState) {
|
||||
let status = getState().getIn(['compose', 'text'], '');
|
||||
let media = getState().getIn(['compose', 'media_attachments']);
|
||||
let status = getState().getIn(['compose', 'text'], '');
|
||||
const media = getState().getIn(['compose', 'media_attachments']);
|
||||
const statusId = getState().getIn(['compose', 'id'], null);
|
||||
const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']);
|
||||
let spoilerText = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : '';
|
||||
|
||||
|
@ -159,20 +171,25 @@ export function submitCompose(routerHistory) {
|
|||
return;
|
||||
}
|
||||
|
||||
dispatch(submitComposeRequest());
|
||||
if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) {
|
||||
status = status + ' 👁️';
|
||||
}
|
||||
api(getState).post('/api/v1/statuses', {
|
||||
status,
|
||||
content_type: getState().getIn(['compose', 'content_type']),
|
||||
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
||||
media_ids: media.map(item => item.get('id')),
|
||||
sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
|
||||
spoiler_text: spoilerText,
|
||||
visibility: getState().getIn(['compose', 'privacy']),
|
||||
poll: getState().getIn(['compose', 'poll'], null),
|
||||
}, {
|
||||
|
||||
dispatch(submitComposeRequest());
|
||||
|
||||
api(getState).request({
|
||||
url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
|
||||
method: statusId === null ? 'post' : 'put',
|
||||
data: {
|
||||
status,
|
||||
content_type: getState().getIn(['compose', 'content_type']),
|
||||
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
||||
media_ids: media.map(item => item.get('id')),
|
||||
sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
|
||||
spoiler_text: spoilerText,
|
||||
visibility: getState().getIn(['compose', 'privacy']),
|
||||
poll: getState().getIn(['compose', 'poll'], null),
|
||||
},
|
||||
headers: {
|
||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||
},
|
||||
|
@ -202,14 +219,16 @@ export function submitCompose(routerHistory) {
|
|||
}
|
||||
};
|
||||
|
||||
insertIfOnline('home');
|
||||
if (statusId === null) {
|
||||
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');
|
||||
if (!response.data.local_only) {
|
||||
insertIfOnline('public');
|
||||
}
|
||||
} else if (response.data.visibility === 'direct') {
|
||||
} else if (statusId === null && response.data.visibility === 'direct') {
|
||||
insertIfOnline('direct');
|
||||
}
|
||||
}).catch(function (error) {
|
||||
|
|
37
app/javascript/flavours/glitch/actions/history.js
Normal file
37
app/javascript/flavours/glitch/actions/history.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import api from 'flavours/glitch/util/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 {
|
||||
type: MODAL_CLOSE,
|
||||
modalType: type,
|
||||
ignoreFocus: options.ignoreFocus,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -47,7 +47,6 @@ export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
|
|||
|
||||
export const NOTIFICATIONS_SET_VISIBILITY = 'NOTIFICATIONS_SET_VISIBILITY';
|
||||
|
||||
|
||||
export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
|
||||
|
||||
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
|
||||
|
@ -58,7 +57,7 @@ defineMessages({
|
|||
});
|
||||
|
||||
const fetchRelatedRelationships = (dispatch, notifications) => {
|
||||
const accountIds = notifications.filter(item => item.type === 'follow').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 > 0) {
|
||||
dispatch(fetchRelationships(accountIds));
|
||||
|
@ -136,7 +135,18 @@ const excludeTypesFromSettings = state => state.getIn(['settings', 'notification
|
|||
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
|
|
|
@ -1,89 +1,38 @@
|
|||
import api from 'flavours/glitch/util/api';
|
||||
import { openModal, closeModal } from './modal';
|
||||
|
||||
export const REPORT_INIT = 'REPORT_INIT';
|
||||
export const REPORT_CANCEL = 'REPORT_CANCEL';
|
||||
import { openModal } from './modal';
|
||||
|
||||
export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST';
|
||||
export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS';
|
||||
export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL';
|
||||
|
||||
export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE';
|
||||
export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
|
||||
export const REPORT_FORWARD_CHANGE = 'REPORT_FORWARD_CHANGE';
|
||||
export const initReport = (account, status) => dispatch =>
|
||||
dispatch(openModal('REPORT', {
|
||||
accountId: account.get('id'),
|
||||
statusId: status?.get('id'),
|
||||
}));
|
||||
|
||||
export function initReport(account, status) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: REPORT_INIT,
|
||||
account,
|
||||
status,
|
||||
});
|
||||
export const submitReport = (params, onSuccess, onFail) => (dispatch, getState) => {
|
||||
dispatch(submitReportRequest());
|
||||
|
||||
dispatch(openModal('REPORT'));
|
||||
};
|
||||
api(getState).post('/api/v1/reports', params).then(response => {
|
||||
dispatch(submitReportSuccess(response.data));
|
||||
if (onSuccess) onSuccess();
|
||||
}).catch(error => {
|
||||
dispatch(submitReportFail(error));
|
||||
if (onFail) onFail();
|
||||
});
|
||||
};
|
||||
|
||||
export function cancelReport() {
|
||||
return {
|
||||
type: REPORT_CANCEL,
|
||||
};
|
||||
};
|
||||
export const submitReportRequest = () => ({
|
||||
type: REPORT_SUBMIT_REQUEST,
|
||||
});
|
||||
|
||||
export function toggleStatusReport(statusId, checked) {
|
||||
return {
|
||||
type: REPORT_STATUS_TOGGLE,
|
||||
statusId,
|
||||
checked,
|
||||
};
|
||||
};
|
||||
export const submitReportSuccess = report => ({
|
||||
type: REPORT_SUBMIT_SUCCESS,
|
||||
report,
|
||||
});
|
||||
|
||||
export function submitReport() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(submitReportRequest());
|
||||
|
||||
api(getState).post('/api/v1/reports', {
|
||||
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));
|
||||
}).catch(error => dispatch(submitReportFail(error)));
|
||||
};
|
||||
};
|
||||
|
||||
export function submitReportRequest() {
|
||||
return {
|
||||
type: REPORT_SUBMIT_REQUEST,
|
||||
};
|
||||
};
|
||||
|
||||
export function submitReportSuccess(report) {
|
||||
return {
|
||||
type: REPORT_SUBMIT_SUCCESS,
|
||||
report,
|
||||
};
|
||||
};
|
||||
|
||||
export function submitReportFail(error) {
|
||||
return {
|
||||
type: REPORT_SUBMIT_FAIL,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export function changeReportComment(comment) {
|
||||
return {
|
||||
type: REPORT_COMMENT_CHANGE,
|
||||
comment,
|
||||
};
|
||||
};
|
||||
|
||||
export function changeReportForward(forward) {
|
||||
return {
|
||||
type: REPORT_FORWARD_CHANGE,
|
||||
forward,
|
||||
};
|
||||
};
|
||||
export const submitReportFail = error => ({
|
||||
type: REPORT_SUBMIT_FAIL,
|
||||
error,
|
||||
});
|
||||
|
|
27
app/javascript/flavours/glitch/actions/rules.js
Normal file
27
app/javascript/flavours/glitch/actions/rules.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import api from 'flavours/glitch/util/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 'flavours/glitch/util/api';
|
|||
|
||||
import { deleteFromTimelines } from './timelines';
|
||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||
import { ensureComposeIsVisible } from './compose';
|
||||
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
|
||||
|
||||
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
||||
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
|
||||
|
@ -26,6 +26,10 @@ export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
|
|||
|
||||
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) {
|
||||
return {
|
||||
type: STATUS_FETCH_REQUEST,
|
||||
|
@ -81,6 +85,37 @@ export function redraft(status, raw_text, content_type) {
|
|||
};
|
||||
};
|
||||
|
||||
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) {
|
||||
return (dispatch, getState) => {
|
||||
let status = getState().getIn(['statuses', id]);
|
||||
|
|
9
app/javascript/flavours/glitch/components/check.js
Normal file
9
app/javascript/flavours/glitch/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 'flavours/glitch/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import classNames from 'classnames';
|
||||
import { CircularProgress } from 'mastodon/components/loading_indicator';
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
let id = 0;
|
||||
|
@ -17,13 +19,18 @@ class DropdownMenu extends React.PureComponent {
|
|||
};
|
||||
|
||||
static propTypes = {
|
||||
items: PropTypes.array.isRequired,
|
||||
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
|
||||
loading: PropTypes.bool,
|
||||
scrollable: PropTypes.bool,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
style: PropTypes.object,
|
||||
placement: PropTypes.string,
|
||||
arrowOffsetLeft: PropTypes.string,
|
||||
arrowOffsetTop: PropTypes.string,
|
||||
openedViaKeyboard: PropTypes.bool,
|
||||
renderItem: PropTypes.func,
|
||||
renderHeader: PropTypes.func,
|
||||
onItemClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -45,9 +52,11 @@ class DropdownMenu extends React.PureComponent {
|
|||
document.addEventListener('click', this.handleDocumentClick, false);
|
||||
document.addEventListener('keydown', this.handleKeyDown, false);
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
|
||||
if (this.focusedItem && this.props.openedViaKeyboard) {
|
||||
this.focusedItem.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
this.setState({ mounted: true });
|
||||
}
|
||||
|
||||
|
@ -66,7 +75,7 @@ class DropdownMenu extends React.PureComponent {
|
|||
}
|
||||
|
||||
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);
|
||||
let element = null;
|
||||
|
||||
|
@ -109,30 +118,20 @@ class DropdownMenu extends React.PureComponent {
|
|||
}
|
||||
|
||||
handleClick = e => {
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
const { action, to } = this.props.items[i];
|
||||
|
||||
this.props.onClose();
|
||||
|
||||
if (typeof action === 'function') {
|
||||
e.preventDefault();
|
||||
action();
|
||||
} else if (to) {
|
||||
e.preventDefault();
|
||||
this.context.router.history.push(to);
|
||||
}
|
||||
const { onItemClick } = this.props;
|
||||
onItemClick(e);
|
||||
}
|
||||
|
||||
renderItem (option, i) {
|
||||
renderItem = (option, i) => {
|
||||
if (option === null) {
|
||||
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
|
||||
}
|
||||
|
||||
const { text, href = '#' } = option;
|
||||
const { text, href = '#', target = '_blank', method } = option;
|
||||
|
||||
return (
|
||||
<li className='dropdown-menu__item' key={`${text}-${i}`}>
|
||||
<a href={href} target='_blank' rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
|
||||
<a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
|
||||
{text}
|
||||
</a>
|
||||
</li>
|
||||
|
@ -140,21 +139,37 @@ class DropdownMenu extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
|
||||
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop, scrollable, renderHeader, loading } = this.props;
|
||||
const { mounted } = this.state;
|
||||
|
||||
let renderItem = this.props.renderItem || this.renderItem;
|
||||
|
||||
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 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
// It should not be transformed when mounting because the resulting
|
||||
// size will be used to determine the coordinate of the menu by
|
||||
// react-overlays
|
||||
<div className='dropdown-menu' 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 }} />
|
||||
|
||||
<ul>
|
||||
{items.map((option, i) => this.renderItem(option, i))}
|
||||
</ul>
|
||||
<div className={classNames('dropdown-menu__container', { 'dropdown-menu__container--loading': loading })}>
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
|
@ -170,11 +185,14 @@ export default class Dropdown extends React.PureComponent {
|
|||
};
|
||||
|
||||
static propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
items: PropTypes.array.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
children: PropTypes.node,
|
||||
icon: PropTypes.string,
|
||||
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
|
||||
loading: PropTypes.bool,
|
||||
size: PropTypes.number,
|
||||
title: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
scrollable: PropTypes.bool,
|
||||
status: ImmutablePropTypes.map,
|
||||
isUserTouching: PropTypes.func,
|
||||
onOpen: PropTypes.func.isRequired,
|
||||
|
@ -182,6 +200,9 @@ export default class Dropdown extends React.PureComponent {
|
|||
dropdownPlacement: PropTypes.string,
|
||||
openDropdownId: PropTypes.number,
|
||||
openedViaKeyboard: PropTypes.bool,
|
||||
renderItem: PropTypes.func,
|
||||
renderHeader: PropTypes.func,
|
||||
onItemClick: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -236,17 +257,22 @@ export default class Dropdown extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleItemClick = (i, e) => {
|
||||
const { action, to } = this.props.items[i];
|
||||
handleItemClick = e => {
|
||||
const { onItemClick } = this.props;
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
const item = this.props.items[i];
|
||||
|
||||
this.handleClose();
|
||||
|
||||
if (typeof action === 'function') {
|
||||
if (typeof onItemClick === 'function') {
|
||||
e.preventDefault();
|
||||
action();
|
||||
} else if (to) {
|
||||
onItemClick(item, i);
|
||||
} else if (item && typeof item.action === 'function') {
|
||||
e.preventDefault();
|
||||
this.context.router.history.push(to);
|
||||
item.action();
|
||||
} else if (item && item.to) {
|
||||
e.preventDefault();
|
||||
this.context.router.history.push(item.to);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -264,29 +290,67 @@ export default class Dropdown extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
close = () => {
|
||||
this.handleClose();
|
||||
}
|
||||
|
||||
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 button = children ? React.cloneElement(React.Children.only(children), {
|
||||
ref: this.setTargetRef,
|
||||
onClick: this.handleClick,
|
||||
onMouseDown: this.handleMouseDown,
|
||||
onKeyDown: this.handleButtonKeyDown,
|
||||
onKeyPress: this.handleKeyPress,
|
||||
}) : (
|
||||
<IconButton
|
||||
icon={icon}
|
||||
title={title}
|
||||
active={open}
|
||||
disabled={disabled}
|
||||
size={size}
|
||||
ref={this.setTargetRef}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleButtonKeyDown}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IconButton
|
||||
icon={icon}
|
||||
title={title}
|
||||
active={open}
|
||||
disabled={disabled}
|
||||
size={size}
|
||||
ref={this.setTargetRef}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleButtonKeyDown}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
/>
|
||||
<React.Fragment>
|
||||
{button}
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { openDropdownMenu, closeDropdownMenu } from 'flavours/glitch/actions/dropdown_menu';
|
||||
import { fetchHistory } from 'flavours/glitch/actions/history';
|
||||
import DropdownMenu from 'flavours/glitch/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);
|
|
@ -0,0 +1,70 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
import DropdownMenu from './containers/dropdown_menu_container';
|
||||
import { connect } from 'react-redux';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
|
||||
import InlineAccount from 'flavours/glitch/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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -30,6 +30,7 @@ export default class IconButton extends React.PureComponent {
|
|||
label: PropTypes.string,
|
||||
counter: PropTypes.number,
|
||||
obfuscateCount: PropTypes.bool,
|
||||
href: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -109,6 +110,7 @@ export default class IconButton extends React.PureComponent {
|
|||
title,
|
||||
counter,
|
||||
obfuscateCount,
|
||||
href,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
|
@ -130,6 +132,21 @@ export default class IconButton extends React.PureComponent {
|
|||
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>}
|
||||
{this.props.label}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
if (href) {
|
||||
contents = (
|
||||
<a href={href} target='_blank' rel='noopener noreferrer'>
|
||||
{contents}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
aria-label={title}
|
||||
|
@ -145,8 +162,7 @@ export default class IconButton extends React.PureComponent {
|
|||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon id={icon} fixedWidth aria-hidden='true' /> {typeof counter !== 'undefined' && <span className='icon-button__counter'><AnimatedNumber value={counter} obfuscate={obfuscateCount} /></span>}
|
||||
{this.props.label}
|
||||
{contents}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
34
app/javascript/flavours/glitch/components/inline_account.js
Normal file
34
app/javascript/flavours/glitch/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 'flavours/glitch/selectors';
|
||||
import Avatar from 'flavours/glitch/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 { 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} heigh={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 = () => (
|
||||
<div className='loading-indicator'>
|
||||
<div className='loading-indicator__figure' />
|
||||
<FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />
|
||||
<CircularProgress size={50} strokeWidth={6} />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ export default class ModalRoot extends React.PureComponent {
|
|||
b: PropTypes.number,
|
||||
}),
|
||||
noEsc: PropTypes.bool,
|
||||
ignoreFocus: PropTypes.bool,
|
||||
};
|
||||
|
||||
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
|
||||
// described in https://github.com/WICG/inert#performance-and-gotchas
|
||||
Promise.resolve().then(() => {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
if (!this.props.ignoreFocus) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
this.activeElement = null;
|
||||
}).catch(console.error);
|
||||
|
||||
|
|
|
@ -5,10 +5,15 @@ import PropTypes from 'prop-types';
|
|||
const messages = defineMessages({
|
||||
today: { id: 'relative_time.today', defaultMessage: 'today' },
|
||||
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_full: { id: 'relative_time.full.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} ago' },
|
||||
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_full: { id: 'relative_time.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} ago' },
|
||||
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' },
|
||||
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' },
|
||||
|
@ -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();
|
||||
|
||||
let relativeTime;
|
||||
|
@ -74,16 +79,16 @@ export const timeAgoString = (intl, date, now, year, timeGiven = true) => {
|
|||
if (delta < DAY && !timeGiven) {
|
||||
relativeTime = intl.formatMessage(messages.today);
|
||||
} 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) {
|
||||
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) {
|
||||
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) {
|
||||
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 {
|
||||
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) {
|
||||
relativeTime = intl.formatDate(date, shortDateFormatOptions);
|
||||
|
@ -124,6 +129,7 @@ class RelativeTimestamp extends React.Component {
|
|||
timestamp: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
futureDate: PropTypes.bool,
|
||||
short: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -132,6 +138,7 @@ class RelativeTimestamp extends React.Component {
|
|||
|
||||
static defaultProps = {
|
||||
year: (new Date()).getFullYear(),
|
||||
short: true,
|
||||
};
|
||||
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
|
@ -176,11 +183,11 @@ class RelativeTimestamp extends React.Component {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { timestamp, intl, year, futureDate } = this.props;
|
||||
const { timestamp, intl, year, futureDate, short } = this.props;
|
||||
|
||||
const timeGiven = timestamp.includes('T');
|
||||
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 (
|
||||
<time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>
|
||||
|
|
|
@ -13,6 +13,7 @@ import classNames from 'classnames';
|
|||
const messages = defineMessages({
|
||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
||||
edit: { id: 'status.edit', defaultMessage: 'Edit' },
|
||||
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||
|
@ -126,6 +127,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
this.props.onDelete(this.props.status, this.context.router.history, true);
|
||||
}
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.onEdit(this.props.status, this.context.router.history);
|
||||
}
|
||||
|
||||
handlePinClick = () => {
|
||||
this.props.onPin(this.props.status);
|
||||
}
|
||||
|
@ -225,6 +230,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
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.redraft), action: this.handleRedraftClick });
|
||||
} else {
|
||||
|
|
|
@ -88,6 +88,14 @@ export default class StatusPrepend extends React.PureComponent {
|
|||
/>
|
||||
);
|
||||
}
|
||||
case 'update':
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='notification.update'
|
||||
defaultMessage='{name} edited a post'
|
||||
values={{ name: link }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -115,6 +123,9 @@ export default class StatusPrepend extends React.PureComponent {
|
|||
case 'status':
|
||||
iconId = 'bell';
|
||||
break;
|
||||
case 'update':
|
||||
iconId = 'pencil';
|
||||
break;
|
||||
};
|
||||
|
||||
return !type ? null : (
|
||||
|
|
|
@ -14,15 +14,11 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
|||
onOpen(id, onItemClick, dropdownPlacement, keyboard) {
|
||||
dispatch(isUserTouching() ? openModal('ACTIONS', {
|
||||
status,
|
||||
actions: items.map(
|
||||
(item, i) => item ? {
|
||||
...item,
|
||||
name: `${item.text}-${i}`,
|
||||
onClick: item.action ? ((e) => { return onItemClick(i, e) }) : null,
|
||||
} : null
|
||||
),
|
||||
actions: items,
|
||||
onClick: onItemClick,
|
||||
}) : openDropdownMenu(id, dropdownPlacement, keyboard, scrollKey));
|
||||
},
|
||||
|
||||
onClose(id) {
|
||||
dispatch(closeModal('ACTIONS'));
|
||||
dispatch(closeDropdownMenu(id));
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
pin,
|
||||
unpin,
|
||||
} from 'flavours/glitch/actions/interactions';
|
||||
import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses';
|
||||
import { muteStatus, unmuteStatus, deleteStatus, editStatus } from 'flavours/glitch/actions/statuses';
|
||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||
import { initReport } from 'flavours/glitch/actions/reports';
|
||||
|
@ -169,6 +169,10 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
|||
}
|
||||
},
|
||||
|
||||
onEdit (status, history) {
|
||||
dispatch(editStatus(status.get('id'), history));
|
||||
},
|
||||
|
||||
onDirect (account, router) {
|
||||
dispatch(directCompose(account, router));
|
||||
},
|
||||
|
|
|
@ -47,6 +47,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
preselectDate: PropTypes.instanceOf(Date),
|
||||
isSubmitting: PropTypes.bool,
|
||||
isChangingUpload: PropTypes.bool,
|
||||
isEditing: PropTypes.bool,
|
||||
isUploading: PropTypes.bool,
|
||||
onChange: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
|
@ -245,9 +246,14 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
selectionStart = selectionEnd = text.length;
|
||||
}
|
||||
if (textarea) {
|
||||
textarea.setSelectionRange(selectionStart, selectionEnd);
|
||||
textarea.focus();
|
||||
if (!singleColumn) textarea.scrollIntoView();
|
||||
// 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(() => {
|
||||
textarea.setSelectionRange(selectionStart, selectionEnd);
|
||||
textarea.focus();
|
||||
if (!singleColumn) textarea.scrollIntoView();
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
// Refocuses the textarea after submitting.
|
||||
|
@ -293,6 +299,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
spoilerText,
|
||||
suggestions,
|
||||
spoilersAlwaysOn,
|
||||
isEditing,
|
||||
} = this.props;
|
||||
|
||||
const countText = this.getFulltextForCharacterCounting();
|
||||
|
@ -353,6 +360,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
onToggleSpoiler={spoilersAlwaysOn ? null : onChangeSpoilerness}
|
||||
onUpload={onPaste}
|
||||
privacy={privacy}
|
||||
isEditing={isEditing}
|
||||
sensitive={sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0)}
|
||||
spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler}
|
||||
/>
|
||||
|
@ -364,6 +372,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
<Publisher
|
||||
countText={countText}
|
||||
disabled={!this.canSubmit()}
|
||||
isEditing={isEditing}
|
||||
onSecondarySubmit={handleSecondarySubmit}
|
||||
onSubmit={handleSubmit}
|
||||
privacy={privacy}
|
||||
|
|
|
@ -21,22 +21,25 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
|||
icon: PropTypes.string,
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
icon: PropTypes.string,
|
||||
meta: PropTypes.node,
|
||||
meta: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
on: PropTypes.bool,
|
||||
text: PropTypes.node,
|
||||
text: PropTypes.string,
|
||||
})).isRequired,
|
||||
onModalOpen: PropTypes.func,
|
||||
onModalClose: PropTypes.func,
|
||||
title: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
noModal: PropTypes.bool,
|
||||
container: PropTypes.func,
|
||||
renderItemContents: PropTypes.func,
|
||||
closeOnChange: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
closeOnChange: true,
|
||||
};
|
||||
|
||||
state = {
|
||||
needsModalUpdate: false,
|
||||
open: false,
|
||||
openedViaKeyboard: undefined,
|
||||
placement: 'bottom',
|
||||
|
@ -44,10 +47,10 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
|||
|
||||
// Toggles opening and closing the dropdown.
|
||||
handleToggle = ({ target, type }) => {
|
||||
const { onModalOpen, noModal } = this.props;
|
||||
const { onModalOpen } = this.props;
|
||||
const { open } = this.state;
|
||||
|
||||
if (!noModal && isUserTouching()) {
|
||||
if (isUserTouching()) {
|
||||
if (this.state.open) {
|
||||
this.props.onModalClose();
|
||||
} else {
|
||||
|
@ -107,9 +110,25 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
|||
this.setState({ open: false });
|
||||
}
|
||||
|
||||
handleItemClick = (e) => {
|
||||
const {
|
||||
items,
|
||||
onChange,
|
||||
onModalClose,
|
||||
closeOnChange,
|
||||
} = this.props;
|
||||
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
|
||||
const { name } = items[i];
|
||||
|
||||
e.preventDefault(); // Prevents focus from changing
|
||||
if (closeOnChange) onModalClose();
|
||||
onChange(name);
|
||||
};
|
||||
|
||||
// Creates an action modal object.
|
||||
handleMakeModal = () => {
|
||||
const component = this;
|
||||
const {
|
||||
items,
|
||||
onChange,
|
||||
|
@ -125,6 +144,8 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
|||
|
||||
// The object.
|
||||
return {
|
||||
renderItemContents: this.props.renderItemContents,
|
||||
onClick: this.handleItemClick,
|
||||
actions: items.map(
|
||||
({
|
||||
name,
|
||||
|
@ -133,48 +154,11 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
|||
...rest,
|
||||
active: value && name === value,
|
||||
name,
|
||||
onClick (e) {
|
||||
e.preventDefault(); // Prevents focus from changing
|
||||
onModalClose();
|
||||
onChange(name);
|
||||
},
|
||||
onPassiveClick (e) {
|
||||
e.preventDefault(); // Prevents focus from changing
|
||||
onChange(name);
|
||||
component.setState({ needsModalUpdate: true });
|
||||
},
|
||||
})
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// If our modal is open and our props update, we need to also update
|
||||
// the modal.
|
||||
handleUpdate = () => {
|
||||
const { onModalOpen } = this.props;
|
||||
const { needsModalUpdate } = this.state;
|
||||
|
||||
// Gets our modal object.
|
||||
const modal = this.handleMakeModal();
|
||||
|
||||
// Reopens the modal with the new object.
|
||||
if (needsModalUpdate && modal && onModalOpen) {
|
||||
onModalOpen(modal);
|
||||
}
|
||||
}
|
||||
|
||||
// Updates our modal as necessary.
|
||||
componentDidUpdate (prevProps) {
|
||||
const { items } = this.props;
|
||||
const { needsModalUpdate } = this.state;
|
||||
if (needsModalUpdate && items.find(
|
||||
(item, i) => item.on !== prevProps.items[i].on
|
||||
)) {
|
||||
this.handleUpdate();
|
||||
this.setState({ needsModalUpdate: false });
|
||||
}
|
||||
}
|
||||
|
||||
// Rendering.
|
||||
render () {
|
||||
const {
|
||||
|
@ -186,6 +170,8 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
|||
onChange,
|
||||
value,
|
||||
container,
|
||||
renderItemContents,
|
||||
closeOnChange,
|
||||
} = this.props;
|
||||
const { open, placement } = this.state;
|
||||
const computedClass = classNames('composer--options--dropdown', {
|
||||
|
@ -226,10 +212,12 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
|||
>
|
||||
<DropdownMenu
|
||||
items={items}
|
||||
renderItemContents={renderItemContents}
|
||||
onChange={onChange}
|
||||
onClose={this.handleClose}
|
||||
value={value}
|
||||
openedViaKeyboard={this.state.openedViaKeyboard}
|
||||
closeOnChange={closeOnChange}
|
||||
/>
|
||||
</Overlay>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import Toggle from 'react-toggle';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
@ -28,18 +27,20 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
|
|||
icon: PropTypes.string,
|
||||
meta: PropTypes.node,
|
||||
name: PropTypes.string.isRequired,
|
||||
on: PropTypes.bool,
|
||||
text: PropTypes.node,
|
||||
})),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
style: PropTypes.object,
|
||||
value: PropTypes.string,
|
||||
renderItemContents: PropTypes.func,
|
||||
openedViaKeyboard: PropTypes.bool,
|
||||
closeOnChange: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
style: {},
|
||||
closeOnChange: true,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -77,16 +78,19 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
|
|||
document.removeEventListener('touchend', this.handleDocumentClick, withPassive);
|
||||
}
|
||||
|
||||
handleClick = (name, e) => {
|
||||
handleClick = (e) => {
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
|
||||
const {
|
||||
onChange,
|
||||
onClose,
|
||||
closeOnChange,
|
||||
items,
|
||||
} = this.props;
|
||||
|
||||
const { on } = this.props.items.find(item => item.name === name);
|
||||
const { name } = this.props.items[i];
|
||||
e.preventDefault(); // Prevents change in focus on click
|
||||
if ((on === null || typeof on === 'undefined')) {
|
||||
if (closeOnChange) {
|
||||
onClose();
|
||||
}
|
||||
onChange(name);
|
||||
|
@ -101,11 +105,9 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
|
|||
}
|
||||
}
|
||||
|
||||
handleKeyDown = (name, e) => {
|
||||
handleKeyDown = (e) => {
|
||||
const index = Number(e.currentTarget.getAttribute('data-index'));
|
||||
const { items } = this.props;
|
||||
const index = items.findIndex(item => {
|
||||
return (item.name === name);
|
||||
});
|
||||
let element = null;
|
||||
|
||||
switch(e.key) {
|
||||
|
@ -139,7 +141,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
|
|||
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.handleChange(element.getAttribute('data-index'));
|
||||
this.handleChange(this.props.items[Number(element.getAttribute('data-index'))].name);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
@ -149,44 +151,40 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
|
|||
this.focusedItem = c;
|
||||
}
|
||||
|
||||
renderItem = (item) => {
|
||||
const { name, icon, meta, on, text } = item;
|
||||
renderItem = (item, i) => {
|
||||
const { name, icon, meta, text } = item;
|
||||
|
||||
const active = (name === (this.props.value || this.state.value));
|
||||
|
||||
const computedClass = classNames('composer--options--dropdown--content--item', {
|
||||
active,
|
||||
lengthy: meta,
|
||||
'toggled-off': !on && on !== null && typeof on !== 'undefined',
|
||||
'toggled-on': on,
|
||||
'with-icon': icon,
|
||||
});
|
||||
const computedClass = classNames('composer--options--dropdown--content--item', { active });
|
||||
|
||||
let prefix = null;
|
||||
let contents = this.props.renderItemContents && this.props.renderItemContents(item, i);
|
||||
|
||||
if (on !== null && typeof on !== 'undefined') {
|
||||
prefix = <Toggle checked={on} onChange={this.handleClick.bind(this, name)} />;
|
||||
} else if (icon) {
|
||||
prefix = <Icon className='icon' fixedWidth id={icon} />
|
||||
if (!contents) {
|
||||
contents = (
|
||||
<React.Fragment>
|
||||
{icon && <Icon className='icon' fixedWidth id={icon} />}
|
||||
|
||||
<div className='content'>
|
||||
<strong>{text}</strong>
|
||||
{meta}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={computedClass}
|
||||
onClick={this.handleClick.bind(this, name)}
|
||||
onKeyDown={this.handleKeyDown.bind(this, name)}
|
||||
onClick={this.handleClick}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
role='option'
|
||||
tabIndex='0'
|
||||
key={name}
|
||||
data-index={name}
|
||||
data-index={i}
|
||||
ref={active ? this.setFocusRef : null}
|
||||
>
|
||||
{prefix}
|
||||
|
||||
<div className='content'>
|
||||
<strong>{text}</strong>
|
||||
{meta}
|
||||
</div>
|
||||
{contents}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -229,7 +227,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
|
|||
transform: mounted ? `scale(${scaleX}, ${scaleY})` : null,
|
||||
}}
|
||||
>
|
||||
{!!items && items.map(item => this.renderItem(item))}
|
||||
{!!items && items.map((item, i) => this.renderItem(item, i))}
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
|
|
|
@ -119,7 +119,7 @@ class Header extends ImmutablePureComponent {
|
|||
<a
|
||||
aria-label={intl.formatMessage(messages.settings)}
|
||||
onClick={onSettingsClick}
|
||||
href='#'
|
||||
href='/settings/preferences'
|
||||
title={intl.formatMessage(messages.settings)}
|
||||
><Icon id='cogs' /></a>
|
||||
<a
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import Toggle from 'react-toggle';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
// Components.
|
||||
import IconButton from 'flavours/glitch/components/icon_button';
|
||||
|
@ -80,6 +82,36 @@ const messages = defineMessages({
|
|||
},
|
||||
});
|
||||
|
||||
@connect((state, { name }) => ({ checked: state.getIn(['compose', 'advanced_options', name]) }))
|
||||
class ToggleOption extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
checked: PropTypes.bool,
|
||||
onChangeAdvancedOption: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = () => {
|
||||
this.props.onChangeAdvancedOption(this.props.name);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { meta, text, checked } = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Toggle checked={checked} onChange={this.handleChange} />
|
||||
|
||||
<div className='content'>
|
||||
<strong>{text}</strong>
|
||||
{meta}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default @injectIntl
|
||||
class ComposerOptions extends ImmutablePureComponent {
|
||||
|
||||
|
@ -106,6 +138,7 @@ class ComposerOptions extends ImmutablePureComponent {
|
|||
resetFileKey: PropTypes.number,
|
||||
spoiler: PropTypes.bool,
|
||||
showContentTypeChoice: PropTypes.bool,
|
||||
isEditing: PropTypes.bool,
|
||||
};
|
||||
|
||||
// Handles file selection.
|
||||
|
@ -141,6 +174,13 @@ class ComposerOptions extends ImmutablePureComponent {
|
|||
this.fileElement = fileElement;
|
||||
}
|
||||
|
||||
renderToggleItemContents = (item) => {
|
||||
const { onChangeAdvancedOption } = this.props;
|
||||
const { name, meta, text } = item;
|
||||
|
||||
return <ToggleOption name={name} text={text} meta={meta} onChangeAdvancedOption={onChangeAdvancedOption} />;
|
||||
};
|
||||
|
||||
// Rendering.
|
||||
render () {
|
||||
const {
|
||||
|
@ -152,7 +192,6 @@ class ComposerOptions extends ImmutablePureComponent {
|
|||
hasMedia,
|
||||
allowPoll,
|
||||
hasPoll,
|
||||
intl,
|
||||
onChangeAdvancedOption,
|
||||
onChangeContentType,
|
||||
onChangeVisibility,
|
||||
|
@ -164,23 +203,25 @@ class ComposerOptions extends ImmutablePureComponent {
|
|||
resetFileKey,
|
||||
spoiler,
|
||||
showContentTypeChoice,
|
||||
isEditing,
|
||||
intl: { formatMessage },
|
||||
} = this.props;
|
||||
|
||||
const contentTypeItems = {
|
||||
plain: {
|
||||
icon: 'file-text',
|
||||
name: 'text/plain',
|
||||
text: <FormattedMessage {...messages.plain} />,
|
||||
text: formatMessage(messages.plain),
|
||||
},
|
||||
html: {
|
||||
icon: 'code',
|
||||
name: 'text/html',
|
||||
text: <FormattedMessage {...messages.html} />,
|
||||
text: formatMessage(messages.html),
|
||||
},
|
||||
markdown: {
|
||||
icon: 'arrow-circle-down',
|
||||
name: 'text/markdown',
|
||||
text: <FormattedMessage {...messages.markdown} />,
|
||||
text: formatMessage(messages.markdown),
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -204,18 +245,18 @@ class ComposerOptions extends ImmutablePureComponent {
|
|||
{
|
||||
icon: 'cloud-upload',
|
||||
name: 'upload',
|
||||
text: <FormattedMessage {...messages.upload} />,
|
||||
text: formatMessage(messages.upload),
|
||||
},
|
||||
{
|
||||
icon: 'paint-brush',
|
||||
name: 'doodle',
|
||||
text: <FormattedMessage {...messages.doodle} />,
|
||||
text: formatMessage(messages.doodle),
|
||||
},
|
||||
]}
|
||||
onChange={this.handleClickAttach}
|
||||
onModalClose={onModalClose}
|
||||
onModalOpen={onModalOpen}
|
||||
title={intl.formatMessage(messages.attach)}
|
||||
title={formatMessage(messages.attach)}
|
||||
/>
|
||||
{!!pollLimits && (
|
||||
<IconButton
|
||||
|
@ -229,12 +270,12 @@ class ComposerOptions extends ImmutablePureComponent {
|
|||
height: null,
|
||||
lineHeight: null,
|
||||
}}
|
||||
title={intl.formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)}
|
||||
title={formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)}
|
||||
/>
|
||||
)}
|
||||
<hr />
|
||||
<PrivacyDropdown
|
||||
disabled={disabled}
|
||||
disabled={disabled || isEditing}
|
||||
onChange={onChangeVisibility}
|
||||
onModalClose={onModalClose}
|
||||
onModalOpen={onModalOpen}
|
||||
|
@ -252,7 +293,7 @@ class ComposerOptions extends ImmutablePureComponent {
|
|||
onChange={onChangeContentType}
|
||||
onModalClose={onModalClose}
|
||||
onModalOpen={onModalOpen}
|
||||
title={intl.formatMessage(messages.content_type)}
|
||||
title={formatMessage(messages.content_type)}
|
||||
value={contentType}
|
||||
/>
|
||||
)}
|
||||
|
@ -262,31 +303,31 @@ class ComposerOptions extends ImmutablePureComponent {
|
|||
ariaControls='glitch.composer.spoiler.input'
|
||||
label='CW'
|
||||
onClick={onToggleSpoiler}
|
||||
title={intl.formatMessage(messages.spoiler)}
|
||||
title={formatMessage(messages.spoiler)}
|
||||
/>
|
||||
)}
|
||||
<Dropdown
|
||||
active={advancedOptions && advancedOptions.some(value => !!value)}
|
||||
disabled={disabled}
|
||||
disabled={disabled || isEditing}
|
||||
icon='ellipsis-h'
|
||||
items={advancedOptions ? [
|
||||
{
|
||||
meta: <FormattedMessage {...messages.local_only_long} />,
|
||||
meta: formatMessage(messages.local_only_long),
|
||||
name: 'do_not_federate',
|
||||
on: advancedOptions.get('do_not_federate'),
|
||||
text: <FormattedMessage {...messages.local_only_short} />,
|
||||
text: formatMessage(messages.local_only_short),
|
||||
},
|
||||
{
|
||||
meta: <FormattedMessage {...messages.threaded_mode_long} />,
|
||||
meta: formatMessage(messages.threaded_mode_long),
|
||||
name: 'threaded_mode',
|
||||
on: advancedOptions.get('threaded_mode'),
|
||||
text: <FormattedMessage {...messages.threaded_mode_short} />,
|
||||
text: formatMessage(messages.threaded_mode_short),
|
||||
},
|
||||
] : null}
|
||||
onChange={onChangeAdvancedOption}
|
||||
renderItemContents={this.renderToggleItemContents}
|
||||
onModalClose={onModalClose}
|
||||
onModalOpen={onModalOpen}
|
||||
title={intl.formatMessage(messages.advanced_options_icon_title)}
|
||||
title={formatMessage(messages.advanced_options_icon_title)}
|
||||
closeOnChange={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,46 +1,19 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Dropdown from './dropdown';
|
||||
|
||||
const messages = defineMessages({
|
||||
change_privacy: {
|
||||
defaultMessage: 'Adjust status privacy',
|
||||
id: 'privacy.change',
|
||||
},
|
||||
direct_long: {
|
||||
defaultMessage: 'Visible for mentioned users only',
|
||||
id: 'privacy.direct.long',
|
||||
},
|
||||
direct_short: {
|
||||
defaultMessage: 'Direct',
|
||||
id: 'privacy.direct.short',
|
||||
},
|
||||
private_long: {
|
||||
defaultMessage: 'Visible for followers only',
|
||||
id: 'privacy.private.long',
|
||||
},
|
||||
private_short: {
|
||||
defaultMessage: 'Followers-only',
|
||||
id: 'privacy.private.short',
|
||||
},
|
||||
public_long: {
|
||||
defaultMessage: 'Visible for all, shown in public timelines',
|
||||
id: 'privacy.public.long',
|
||||
},
|
||||
public_short: {
|
||||
defaultMessage: 'Public',
|
||||
id: 'privacy.public.short',
|
||||
},
|
||||
unlisted_long: {
|
||||
defaultMessage: 'Visible for all, but not in public timelines',
|
||||
id: 'privacy.unlisted.long',
|
||||
},
|
||||
unlisted_short: {
|
||||
defaultMessage: 'Unlisted',
|
||||
id: 'privacy.unlisted.short',
|
||||
},
|
||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||
public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all, shown in public timelines' },
|
||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but not in public timelines' },
|
||||
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
|
||||
private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' },
|
||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
||||
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' },
|
||||
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
|
@ -53,40 +26,40 @@ class PrivacyDropdown extends React.PureComponent {
|
|||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
noDirect: PropTypes.bool,
|
||||
noModal: PropTypes.bool,
|
||||
container: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, noModal, container, intl } = this.props;
|
||||
const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, container, intl: { formatMessage } } = this.props;
|
||||
|
||||
// We predefine our privacy items so that we can easily pick the
|
||||
// dropdown icon later.
|
||||
const privacyItems = {
|
||||
direct: {
|
||||
icon: 'envelope',
|
||||
meta: <FormattedMessage {...messages.direct_long} />,
|
||||
meta: formatMessage(messages.direct_long),
|
||||
name: 'direct',
|
||||
text: <FormattedMessage {...messages.direct_short} />,
|
||||
text: formatMessage(messages.direct_short),
|
||||
},
|
||||
private: {
|
||||
icon: 'lock',
|
||||
meta: <FormattedMessage {...messages.private_long} />,
|
||||
meta: formatMessage(messages.private_long),
|
||||
name: 'private',
|
||||
text: <FormattedMessage {...messages.private_short} />,
|
||||
text: formatMessage(messages.private_short),
|
||||
},
|
||||
public: {
|
||||
icon: 'globe',
|
||||
meta: <FormattedMessage {...messages.public_long} />,
|
||||
meta: formatMessage(messages.public_long),
|
||||
name: 'public',
|
||||
text: <FormattedMessage {...messages.public_short} />,
|
||||
text: formatMessage(messages.public_short),
|
||||
},
|
||||
unlisted: {
|
||||
icon: 'unlock',
|
||||
meta: <FormattedMessage {...messages.unlisted_long} />,
|
||||
meta: formatMessage(messages.unlisted_long),
|
||||
name: 'unlisted',
|
||||
text: <FormattedMessage {...messages.unlisted_short} />,
|
||||
text: formatMessage(messages.unlisted_short),
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -104,9 +77,8 @@ class PrivacyDropdown extends React.PureComponent {
|
|||
onChange={onChange}
|
||||
onModalClose={onModalClose}
|
||||
onModalOpen={onModalOpen}
|
||||
title={intl.formatMessage(messages.change_privacy)}
|
||||
title={formatMessage(messages.change_privacy)}
|
||||
container={container}
|
||||
noModal={noModal}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { length } from 'stringz';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
|
@ -23,6 +23,7 @@ const messages = defineMessages({
|
|||
defaultMessage: '{publish}!',
|
||||
id: 'compose_form.publish_loud',
|
||||
},
|
||||
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
|
@ -36,6 +37,7 @@ class Publisher extends ImmutablePureComponent {
|
|||
onSubmit: PropTypes.func,
|
||||
privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']),
|
||||
sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']),
|
||||
isEditing: PropTypes.bool,
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
|
@ -43,7 +45,7 @@ class Publisher extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm } = this.props;
|
||||
const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm, isEditing } = this.props;
|
||||
|
||||
const diff = maxChars - length(countText || '');
|
||||
const computedClass = classNames('composer--publisher', {
|
||||
|
@ -51,63 +53,37 @@ class Publisher extends ImmutablePureComponent {
|
|||
over: diff < 0,
|
||||
});
|
||||
|
||||
const privacyIcons = { direct: 'envelope', private: 'lock', public: 'globe', unlisted: 'unlock' };
|
||||
|
||||
let publishText;
|
||||
if (isEditing) {
|
||||
publishText = intl.formatMessage(messages.saveChanges);
|
||||
} else if (privacy === 'private' || privacy === 'direct') {
|
||||
const iconId = privacyIcons[privacy];
|
||||
publishText = (
|
||||
<span>
|
||||
<Icon id={iconId} /> {intl.formatMessage(messages.publish)}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
publishText = privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={computedClass}>
|
||||
{sideArm && sideArm !== 'none' ? (
|
||||
{sideArm && !isEditing && sideArm !== 'none' ? (
|
||||
<Button
|
||||
className='side_arm'
|
||||
disabled={disabled}
|
||||
onClick={onSecondarySubmit}
|
||||
style={{ padding: null }}
|
||||
text={
|
||||
<span>
|
||||
<Icon
|
||||
id={{
|
||||
public: 'globe',
|
||||
unlisted: 'unlock',
|
||||
private: 'lock',
|
||||
direct: 'envelope',
|
||||
}[sideArm]}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
text={<Icon id={privacyIcons[sideArm]} />}
|
||||
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${sideArm}.short` })}`}
|
||||
/>
|
||||
) : null}
|
||||
<Button
|
||||
className='primary'
|
||||
text={function () {
|
||||
switch (true) {
|
||||
case !!sideArm && sideArm !== 'none':
|
||||
case privacy === 'direct':
|
||||
case privacy === 'private':
|
||||
return (
|
||||
<span>
|
||||
<Icon
|
||||
id={{
|
||||
direct: 'envelope',
|
||||
private: 'lock',
|
||||
public: 'globe',
|
||||
unlisted: 'unlock',
|
||||
}[privacy]}
|
||||
/>
|
||||
{' '}
|
||||
<FormattedMessage {...messages.publish} />
|
||||
</span>
|
||||
);
|
||||
case privacy === 'public':
|
||||
return (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
{...messages.publishLoud}
|
||||
values={{ publish: <FormattedMessage {...messages.publish} /> }}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return <span><FormattedMessage {...messages.publish} /></span>;
|
||||
}
|
||||
}()}
|
||||
text={publishText}
|
||||
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${privacy}.short` })}`}
|
||||
onClick={this.handleSubmit}
|
||||
disabled={disabled}
|
||||
|
|
|
@ -19,6 +19,7 @@ export default class Upload extends ImmutablePureComponent {
|
|||
media: ImmutablePropTypes.map.isRequired,
|
||||
onUndo: PropTypes.func.isRequired,
|
||||
onOpenFocalPoint: PropTypes.func.isRequired,
|
||||
isEditingStatus: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleUndoClick = e => {
|
||||
|
@ -32,7 +33,7 @@ export default class Upload extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { intl, media } = this.props;
|
||||
const { intl, media, isEditingStatus } = this.props;
|
||||
const focusX = media.getIn(['meta', 'focus', 'x']);
|
||||
const focusY = media.getIn(['meta', 'focus', 'y']);
|
||||
const x = ((focusX / 2) + .5) * 100;
|
||||
|
@ -45,7 +46,7 @@ export default class Upload extends ImmutablePureComponent {
|
|||
<div style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
|
||||
<div className={classNames('composer--upload_form--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.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>
|
||||
)}
|
||||
|
|
|
@ -51,6 +51,7 @@ function mapStateToProps (state) {
|
|||
focusDate: state.getIn(['compose', 'focusDate']),
|
||||
caretPosition: state.getIn(['compose', 'caretPosition']),
|
||||
isSubmitting: state.getIn(['compose', 'is_submitting']),
|
||||
isEditing: state.getIn(['compose', 'id']) !== null,
|
||||
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
|
||||
isUploading: state.getIn(['compose', 'is_uploading']),
|
||||
layout: state.getIn(['local_settings', 'layout']),
|
||||
|
|
|
@ -1,14 +1,24 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { cancelReplyCompose } from 'flavours/glitch/actions/compose';
|
||||
import { makeGetStatus } from 'flavours/glitch/selectors';
|
||||
import ReplyIndicator from '../components/reply_indicator';
|
||||
|
||||
function makeMapStateToProps (state) {
|
||||
const inReplyTo = state.getIn(['compose', 'in_reply_to']);
|
||||
const makeMapStateToProps = () => {
|
||||
const mapStateToProps = state => {
|
||||
let statusId = state.getIn(['compose', 'id'], null);
|
||||
let editing = true;
|
||||
|
||||
return {
|
||||
status: inReplyTo ? state.getIn(['statuses', inReplyTo]) : null,
|
||||
if (statusId === null) {
|
||||
statusId = state.getIn(['compose', 'in_reply_to']);
|
||||
editing = false;
|
||||
}
|
||||
|
||||
return {
|
||||
status: state.getIn(['statuses', statusId]),
|
||||
editing,
|
||||
};
|
||||
};
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
|
|
@ -5,6 +5,7 @@ import { submitCompose } from 'flavours/glitch/actions/compose';
|
|||
|
||||
const mapStateToProps = (state, { id }) => ({
|
||||
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
||||
isEditingStatus: state.getIn(['compose', 'id']) !== null,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
|
|
@ -250,7 +250,7 @@ class EmojiPickerMenu extends React.PureComponent {
|
|||
|
||||
state = {
|
||||
modifierOpen: false,
|
||||
placement: null,
|
||||
readyToFocus: false,
|
||||
};
|
||||
|
||||
handleDocumentClick = e => {
|
||||
|
@ -262,6 +262,16 @@ class EmojiPickerMenu extends React.PureComponent {
|
|||
componentDidMount () {
|
||||
document.addEventListener('click', this.handleDocumentClick, false);
|
||||
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 () {
|
||||
|
@ -361,7 +371,7 @@ class EmojiPickerMenu extends React.PureComponent {
|
|||
showSkinTones={false}
|
||||
backgroundImageFn={backgroundImageFn}
|
||||
notFound={notFoundFn}
|
||||
autoFocus
|
||||
autoFocus={this.state.readyToFocus}
|
||||
emojiTooltip
|
||||
native={useSystemEmojiFont}
|
||||
/>
|
||||
|
@ -396,6 +406,7 @@ class EmojiPickerDropdown extends React.PureComponent {
|
|||
state = {
|
||||
active: false,
|
||||
loading: false,
|
||||
placement: null,
|
||||
};
|
||||
|
||||
setRef = (c) => {
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
// Package imports.
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import classNames from 'classnames';
|
||||
|
||||
// Our imports.
|
||||
import Permalink from 'flavours/glitch/components/permalink';
|
||||
import AccountContainer from 'flavours/glitch/containers/account_container';
|
||||
import NotificationOverlayContainer from '../containers/overlay_container';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
|
||||
export default class NotificationFollow extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
hidden: PropTypes.bool,
|
||||
id: PropTypes.string.isRequired,
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
notification: ImmutablePropTypes.map.isRequired,
|
||||
unread: PropTypes.bool,
|
||||
};
|
||||
|
||||
handleMoveUp = () => {
|
||||
const { notification, onMoveUp } = this.props;
|
||||
onMoveUp(notification.get('id'));
|
||||
}
|
||||
|
||||
handleMoveDown = () => {
|
||||
const { notification, onMoveDown } = this.props;
|
||||
onMoveDown(notification.get('id'));
|
||||
}
|
||||
|
||||
handleOpen = () => {
|
||||
this.handleOpenProfile();
|
||||
}
|
||||
|
||||
handleOpenProfile = () => {
|
||||
const { notification } = this.props;
|
||||
this.context.router.history.push(`/@${notification.getIn(['account', 'acct'])}`);
|
||||
}
|
||||
|
||||
handleMention = e => {
|
||||
e.preventDefault();
|
||||
|
||||
const { notification, onMention } = this.props;
|
||||
onMention(notification.get('account'), this.context.router.history);
|
||||
}
|
||||
|
||||
getHandlers () {
|
||||
return {
|
||||
moveUp: this.handleMoveUp,
|
||||
moveDown: this.handleMoveDown,
|
||||
open: this.handleOpen,
|
||||
openProfile: this.handleOpenProfile,
|
||||
mention: this.handleMention,
|
||||
reply: this.handleMention,
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
const { account, notification, hidden, unread } = this.props;
|
||||
|
||||
// Links to the display name.
|
||||
const displayName = account.get('display_name_html') || account.get('username');
|
||||
const link = (
|
||||
<bdi><Permalink
|
||||
className='notification__display-name'
|
||||
href={account.get('url')}
|
||||
title={account.get('acct')}
|
||||
to={`/@${account.get('acct')}`}
|
||||
dangerouslySetInnerHTML={{ __html: displayName }}
|
||||
/></bdi>
|
||||
);
|
||||
|
||||
// Renders.
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<div className={classNames('notification notification-admin-sign-up focusable', { unread })} tabIndex='0'>
|
||||
<div className='notification__message'>
|
||||
<div className='notification__favourite-icon-wrapper'>
|
||||
<Icon fixedWidth id='user-plus' />
|
||||
</div>
|
||||
|
||||
<FormattedMessage
|
||||
id='notification.admin.sign_up'
|
||||
defaultMessage='{name} signed up'
|
||||
values={{ name: link }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AccountContainer hidden={hidden} id={account.get('id')} withNote={false} />
|
||||
<NotificationOverlayContainer notification={notification} />
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -6,6 +6,7 @@ import ClearColumnButton from './clear_column_button';
|
|||
import GrantPermissionButton from './grant_permission_button';
|
||||
import SettingToggle from './setting_toggle';
|
||||
import PillBarButton from './pill_bar_button';
|
||||
import { isStaff } from 'flavours/glitch/util/initial_state';
|
||||
|
||||
export default class ColumnSettings extends React.PureComponent {
|
||||
|
||||
|
@ -154,6 +155,30 @@ export default class ColumnSettings extends React.PureComponent {
|
|||
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'status']} onChange={onChange} label={soundStr} />
|
||||
</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__pillbar'>
|
||||
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'update']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <PillBarButton prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'update']} onChange={this.onPushChange} label={pushStr} />}
|
||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'update']} onChange={onChange} label={showStr} />
|
||||
<PillBarButton 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__pillbar'>
|
||||
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.sign_up']} onChange={onChange} label={alertStr} />
|
||||
{showPushSettings && <PillBarButton prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'admin.sign_up']} onChange={this.onPushChange} label={pushStr} />}
|
||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'admin.sign_up']} onChange={onChange} label={showStr} />
|
||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'admin.sign_up']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
import StatusContainer from 'flavours/glitch/containers/status_container';
|
||||
import NotificationFollow from './follow';
|
||||
import NotificationFollowRequestContainer from '../containers/follow_request_container';
|
||||
import NotificationAdminSignup from './admin_signup';
|
||||
|
||||
export default class Notification extends ImmutablePureComponent {
|
||||
|
||||
|
@ -63,6 +64,19 @@ export default class Notification extends ImmutablePureComponent {
|
|||
unread={this.props.unread}
|
||||
/>
|
||||
);
|
||||
case 'admin.sign_up':
|
||||
return (
|
||||
<NotificationAdminSignup
|
||||
hidden={hidden}
|
||||
id={notification.get('id')}
|
||||
account={notification.get('account')}
|
||||
notification={notification}
|
||||
onMoveDown={onMoveDown}
|
||||
onMoveUp={onMoveUp}
|
||||
onMention={onMention}
|
||||
unread={this.props.unread}
|
||||
/>
|
||||
);
|
||||
case 'mention':
|
||||
return (
|
||||
<StatusContainer
|
||||
|
@ -171,6 +185,28 @@ export default class Notification extends ImmutablePureComponent {
|
|||
unread={this.props.unread}
|
||||
/>
|
||||
);
|
||||
case 'update':
|
||||
return (
|
||||
<StatusContainer
|
||||
containerId={notification.get('id')}
|
||||
hidden={hidden}
|
||||
id={notification.get('status')}
|
||||
account={notification.get('account')}
|
||||
prepend='update'
|
||||
muted
|
||||
notification={notification}
|
||||
onMoveDown={onMoveDown}
|
||||
onMoveUp={onMoveUp}
|
||||
onMention={onMention}
|
||||
getScrollPosition={getScrollPosition}
|
||||
updateScrollBottom={updateScrollBottom}
|
||||
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||
onUnmount={this.props.onUnmount}
|
||||
withDismiss
|
||||
unread={this.props.unread}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ class Footer extends ImmutablePureComponent {
|
|||
const { router } = this.context;
|
||||
|
||||
if (onClose) {
|
||||
onClose();
|
||||
onClose(true);
|
||||
}
|
||||
|
||||
dispatch(replyCompose(status, router.history));
|
||||
|
@ -181,7 +181,7 @@ class Footer extends ImmutablePureComponent {
|
|||
{replyButton}
|
||||
<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')} />
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
|
93
app/javascript/flavours/glitch/features/report/category.js
Normal file
93
app/javascript/flavours/glitch/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 'flavours/glitch/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/flavours/glitch/features/report/comment.js
Normal file
83
app/javascript/flavours/glitch/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 'flavours/glitch/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} disabled={isSubmitting}><FormattedMessage id='report.submit' defaultMessage='Submit report' /></Button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Check from 'flavours/glitch/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 PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Toggle from 'react-toggle';
|
||||
import noop from 'lodash/noop';
|
||||
import StatusContent from 'flavours/glitch/components/status_content';
|
||||
import { MediaGallery, Video } from 'flavours/glitch/util/async-components';
|
||||
import Bundle from 'flavours/glitch/features/ui/components/bundle';
|
||||
import Avatar from 'flavours/glitch/components/avatar';
|
||||
import DisplayName from 'flavours/glitch/components/display_name';
|
||||
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
|
||||
import Option from './option';
|
||||
|
||||
export default class StatusCheckBox extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
checked: PropTypes.bool,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
handleStatusesToggle = (value, checked) => {
|
||||
const { onToggle } = this.props;
|
||||
onToggle(value, checked);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { status, checked, onToggle, disabled } = this.props;
|
||||
const { status, checked } = this.props;
|
||||
|
||||
let media = null;
|
||||
|
||||
if (status.get('reblog')) {
|
||||
|
@ -51,26 +60,45 @@ export default class StatusCheckBox extends React.PureComponent {
|
|||
} else {
|
||||
media = (
|
||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
|
||||
{Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} revealed={false} height={110} onOpenMedia={noop} />}
|
||||
{Component => (
|
||||
<Component
|
||||
media={status.get('media_attachments')}
|
||||
sensitive={status.get('sensitive')}
|
||||
revealed={false}
|
||||
height={110}
|
||||
onOpenMedia={noop}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='status-check-box'>
|
||||
<div className='status-check-box__status'>
|
||||
<StatusContent
|
||||
status={status}
|
||||
media={media}
|
||||
/>
|
||||
const labelComponent = (
|
||||
<div className='status-check-box__status poll__option__text'>
|
||||
<div className='detailed-status__display-name'>
|
||||
<div className='detailed-status__display-avatar'>
|
||||
<Avatar account={status.get('account')} size={46} />
|
||||
</div>
|
||||
|
||||
<div><DisplayName account={status.get('account')} /> · <RelativeTimestamp timestamp={status.get('created_at')} /></div>
|
||||
</div>
|
||||
|
||||
<div className='status-check-box-toggle'>
|
||||
<Toggle checked={checked} onChange={onToggle} disabled={disabled} />
|
||||
</div>
|
||||
<StatusContent status={status} media={media} />
|
||||
</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 StatusCheckBox from '../components/status_check_box';
|
||||
import { toggleStatusReport } from 'flavours/glitch/actions/reports';
|
||||
import { Set as ImmutableSet } from 'immutable';
|
||||
import { makeGetStatus } from 'flavours/glitch/selectors';
|
||||
|
||||
const mapStateToProps = (state, { id }) => ({
|
||||
status: state.getIn(['statuses', id]),
|
||||
checked: state.getIn(['reports', 'new', 'status_ids'], ImmutableSet()).includes(id),
|
||||
});
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
const mapDispatchToProps = (dispatch, { id }) => ({
|
||||
const mapStateToProps = (state, { id }) => ({
|
||||
status: getStatus(state, { id }),
|
||||
});
|
||||
|
||||
onToggle (e) {
|
||||
dispatch(toggleStatusReport(id, e.target.checked));
|
||||
},
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StatusCheckBox);
|
||||
export default connect(makeMapStateToProps)(StatusCheckBox);
|
||||
|
|
64
app/javascript/flavours/glitch/features/report/rules.js
Normal file
64
app/javascript/flavours/glitch/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 'flavours/glitch/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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
61
app/javascript/flavours/glitch/features/report/statuses.js
Normal file
61
app/javascript/flavours/glitch/features/report/statuses.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
import StatusCheckBox from 'flavours/glitch/features/report/containers/status_check_box_container';
|
||||
import { OrderedSet } from 'immutable';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Button from 'flavours/glitch/components/button';
|
||||
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
availableStatusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])),
|
||||
isLoading: state.getIn(['timelines', `account:${accountId}:with_replies`, 'isLoading']),
|
||||
});
|
||||
|
||||
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,
|
||||
isLoading: PropTypes.bool,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleNextClick = () => {
|
||||
const { onNextStep } = this.props;
|
||||
onNextStep('comment');
|
||||
};
|
||||
|
||||
render () {
|
||||
const { availableStatusIds, selectedStatusIds, onToggle, isLoading } = 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'>
|
||||
{isLoading ? <LoadingIndicator /> : 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/flavours/glitch/features/report/thanks.js
Normal file
84
app/javascript/flavours/glitch/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 'flavours/glitch/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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -11,6 +11,7 @@ import classNames from 'classnames';
|
|||
const messages = defineMessages({
|
||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
||||
edit: { id: 'status.edit', defaultMessage: 'Edit' },
|
||||
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
|
@ -52,6 +53,7 @@ class ActionBar extends React.PureComponent {
|
|||
onMuteConversation: PropTypes.func,
|
||||
onBlock: PropTypes.func,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onEdit: PropTypes.func.isRequired,
|
||||
onDirect: PropTypes.func.isRequired,
|
||||
onMention: PropTypes.func.isRequired,
|
||||
onReport: PropTypes.func,
|
||||
|
@ -84,6 +86,10 @@ class ActionBar extends React.PureComponent {
|
|||
this.props.onDelete(this.props.status, this.context.router.history, true);
|
||||
}
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.onEdit(this.props.status, this.context.router.history);
|
||||
}
|
||||
|
||||
handleDirectClick = () => {
|
||||
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
|
||||
}
|
||||
|
@ -166,6 +172,7 @@ class ActionBar extends React.PureComponent {
|
|||
|
||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||
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.redraft), action: this.handleRedraftClick });
|
||||
} else {
|
||||
|
|
|
@ -7,7 +7,7 @@ import StatusContent from 'flavours/glitch/components/status_content';
|
|||
import MediaGallery from 'flavours/glitch/components/media_gallery';
|
||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, FormattedDate, FormattedMessage } from 'react-intl';
|
||||
import { injectIntl, FormattedDate } from 'react-intl';
|
||||
import Card from './card';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Video from 'flavours/glitch/features/video';
|
||||
|
@ -19,6 +19,7 @@ import PollContainer from 'flavours/glitch/containers/poll_container';
|
|||
import Icon from 'flavours/glitch/components/icon';
|
||||
import AnimatedNumber from 'flavours/glitch/components/animated_number';
|
||||
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
|
||||
import EditedTimestamp from 'flavours/glitch/components/edited_timestamp';
|
||||
|
||||
export default @injectIntl
|
||||
class DetailedStatus extends ImmutablePureComponent {
|
||||
|
@ -265,7 +266,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
edited = (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
directCompose,
|
||||
} from 'flavours/glitch/actions/compose';
|
||||
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
|
||||
import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses';
|
||||
import { muteStatus, unmuteStatus, deleteStatus, editStatus } from 'flavours/glitch/actions/statuses';
|
||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||
import { initReport } from 'flavours/glitch/actions/reports';
|
||||
|
@ -307,6 +307,10 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleEditClick = (status, history) => {
|
||||
this.props.dispatch(editStatus(status.get('id'), history));
|
||||
}
|
||||
|
||||
handleDirectClick = (account, router) => {
|
||||
this.props.dispatch(directCompose(account, router));
|
||||
}
|
||||
|
@ -585,6 +589,7 @@ class Status extends ImmutablePureComponent {
|
|||
onReblog={this.handleReblogClick}
|
||||
onBookmark={this.handleBookmarkClick}
|
||||
onDelete={this.handleDeleteClick}
|
||||
onEdit={this.handleEditClick}
|
||||
onDirect={this.handleDirectClick}
|
||||
onMention={this.handleMentionClick}
|
||||
onMute={this.handleMuteClick}
|
||||
|
|
|
@ -7,24 +7,22 @@ import Avatar from 'flavours/glitch/components/avatar';
|
|||
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
|
||||
import DisplayName from 'flavours/glitch/components/display_name';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
import Link from 'flavours/glitch/components/link';
|
||||
import Toggle from 'react-toggle';
|
||||
import IconButton from 'flavours/glitch/components/icon_button';
|
||||
|
||||
export default class ActionsModal extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map,
|
||||
onClick: PropTypes.func,
|
||||
actions: PropTypes.arrayOf(PropTypes.shape({
|
||||
active: PropTypes.bool,
|
||||
href: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
meta: PropTypes.node,
|
||||
meta: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
on: PropTypes.bool,
|
||||
onPassiveClick: PropTypes.func,
|
||||
text: PropTypes.node,
|
||||
text: PropTypes.string,
|
||||
})),
|
||||
renderItemContents: PropTypes.func,
|
||||
};
|
||||
|
||||
renderAction = (action, i) => {
|
||||
|
@ -32,57 +30,26 @@ export default class ActionsModal extends ImmutablePureComponent {
|
|||
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
|
||||
}
|
||||
|
||||
const {
|
||||
active,
|
||||
href,
|
||||
icon,
|
||||
meta,
|
||||
name,
|
||||
on,
|
||||
onClick,
|
||||
onPassiveClick,
|
||||
text,
|
||||
} = action;
|
||||
const { icon = null, text, meta = null, active = false, href = '#' } = action;
|
||||
let contents = this.props.renderItemContents && this.props.renderItemContents(action, i);
|
||||
|
||||
if (!contents) {
|
||||
contents = (
|
||||
<React.Fragment>
|
||||
{icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' inverted />}
|
||||
<div>
|
||||
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
|
||||
<div>{meta}</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={name || i}>
|
||||
<Link
|
||||
className={classNames('link', { active })}
|
||||
href={href}
|
||||
onClick={on !== null && typeof on !== 'undefined' && onPassiveClick || onClick}
|
||||
role={onClick ? 'button' : null}
|
||||
>
|
||||
{function () {
|
||||
|
||||
// We render a `<Toggle>` if we were provided an `on`
|
||||
// property, and otherwise show an `<Icon>` if available.
|
||||
switch (true) {
|
||||
case on !== null && typeof on !== 'undefined':
|
||||
return (
|
||||
<Toggle
|
||||
checked={on}
|
||||
onChange={onPassiveClick || onClick}
|
||||
/>
|
||||
);
|
||||
case !!icon:
|
||||
return (
|
||||
<Icon
|
||||
className='icon'
|
||||
fixedWidth
|
||||
id={icon}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}()}
|
||||
{meta ? (
|
||||
<div>
|
||||
<strong>{text}</strong>
|
||||
{meta}
|
||||
</div>
|
||||
) : <div>{text}</div>}
|
||||
</Link>
|
||||
<li key={`${text}-${i}`}>
|
||||
<a href={href} target='_blank' rel='noopener noreferrer' onClick={this.props.onClick} data-index={i} className={classNames('link', { active })}>
|
||||
{contents}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 'flavours/glitch/actions/modal';
|
||||
import emojify from 'flavours/glitch/util/emoji';
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import InlineAccount from 'flavours/glitch/components/inline_account';
|
||||
import IconButton from 'flavours/glitch/components/icon_button';
|
||||
import RelativeTimestamp from 'flavours/glitch/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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -24,6 +24,7 @@ import {
|
|||
ListEditor,
|
||||
ListAdder,
|
||||
PinnedAccountsEditor,
|
||||
CompareHistoryModal,
|
||||
} from 'flavours/glitch/util/async-components';
|
||||
|
||||
const MODAL_COMPONENTS = {
|
||||
|
@ -42,9 +43,10 @@ const MODAL_COMPONENTS = {
|
|||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||
'EMBED': EmbedModal,
|
||||
'LIST_EDITOR': ListEditor,
|
||||
'LIST_ADDER':ListAdder,
|
||||
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
|
||||
'LIST_ADDER': ListAdder,
|
||||
'PINNED_ACCOUNTS_EDITOR': PinnedAccountsEditor,
|
||||
'COMPARE_HISTORY': CompareHistoryModal,
|
||||
};
|
||||
|
||||
export default class ModalRoot extends React.PureComponent {
|
||||
|
@ -53,6 +55,7 @@ export default class ModalRoot extends React.PureComponent {
|
|||
type: PropTypes.string,
|
||||
props: PropTypes.object,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
ignoreFocus: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -83,7 +86,7 @@ export default class ModalRoot extends React.PureComponent {
|
|||
return <BundleModalError {...props} onClose={onClose} />;
|
||||
}
|
||||
|
||||
handleClose = () => {
|
||||
handleClose = (ignoreFocus = false) => {
|
||||
const { onClose } = this.props;
|
||||
let message = null;
|
||||
try {
|
||||
|
@ -93,7 +96,7 @@ export default class ModalRoot extends React.PureComponent {
|
|||
// isn't set.
|
||||
// This would be much smoother with react-intl 3+ and `forwardRef`.
|
||||
}
|
||||
onClose(message);
|
||||
onClose(message, ignoreFocus);
|
||||
}
|
||||
|
||||
setModalRef = (c) => {
|
||||
|
@ -101,12 +104,12 @@ export default class ModalRoot extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { type, props } = this.props;
|
||||
const { type, props, ignoreFocus } = this.props;
|
||||
const { backgroundColor } = this.state;
|
||||
const visible = !!type;
|
||||
|
||||
return (
|
||||
<Base backgroundColor={backgroundColor} onClose={this.handleClose} noEsc={props ? props.noEsc : false}>
|
||||
<Base backgroundColor={backgroundColor} onClose={this.handleClose} noEsc={props ? props.noEsc : false} ignoreFocus={ignoreFocus}>
|
||||
{visible && (
|
||||
<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} />}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue