Merge branch 'master' of https://github.com/glitch-soc/mastodon
commit
721e05a3af
|
@ -5,12 +5,13 @@ aliases:
|
|||
docker:
|
||||
- image: circleci/ruby:2.7-buster-node
|
||||
environment: &ruby_environment
|
||||
BUNDLE_JOBS: 3
|
||||
BUNDLE_RETRY: 3
|
||||
BUNDLE_APP_CONFIG: ./.bundle/
|
||||
BUNDLE_PATH: ./vendor/bundle/
|
||||
DB_HOST: localhost
|
||||
DB_USER: root
|
||||
RAILS_ENV: test
|
||||
PARALLEL_TEST_PROCESSORS: 4
|
||||
ALLOW_NOPAM: true
|
||||
CONTINUOUS_INTEGRATION: true
|
||||
DISABLE_SIMPLECOV: true
|
||||
|
@ -32,9 +33,9 @@ aliases:
|
|||
- &restore_ruby_dependencies
|
||||
restore_cache:
|
||||
keys:
|
||||
- v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
|
||||
- v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-
|
||||
- v2-ruby-dependencies-
|
||||
- v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
|
||||
- v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-
|
||||
- v3-ruby-dependencies-
|
||||
|
||||
- &install_steps
|
||||
steps:
|
||||
|
@ -42,11 +43,13 @@ aliases:
|
|||
- *attach_workspace
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-node-dependencies-{{ checksum "yarn.lock" }}
|
||||
- v1-node-dependencies-
|
||||
- run: yarn install --frozen-lockfile
|
||||
- v2-node-dependencies-{{ checksum "yarn.lock" }}
|
||||
- v2-node-dependencies-
|
||||
- run:
|
||||
name: Install yarn dependencies
|
||||
command: yarn install --frozen-lockfile
|
||||
- save_cache:
|
||||
key: v1-node-dependencies-{{ checksum "yarn.lock" }}
|
||||
key: v2-node-dependencies-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ./node_modules/
|
||||
- *persist_to_workspace
|
||||
|
@ -57,27 +60,28 @@ aliases:
|
|||
command: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
|
||||
|
||||
## TODO: FIX THESE BUSTER DEPENDANCES
|
||||
sudo wget http://ftp.au.debian.org/debian/pool/main/i/icu/libicu57_57.1-6+deb9u3_amd64.deb
|
||||
sudo dpkg -i libicu57_57.1-6+deb9u3_amd64.deb
|
||||
sudo wget http://ftp.au.debian.org/debian/pool/main/p/protobuf/libprotobuf10_3.0.0-9_amd64.deb
|
||||
sudo dpkg -i libprotobuf10_3.0.0-9_amd64.deb
|
||||
|
||||
- &install_ruby_dependencies
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- *install_system_dependencies
|
||||
- run: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
|
||||
- run:
|
||||
name: Set Ruby version
|
||||
command: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
|
||||
- *restore_ruby_dependencies
|
||||
- run: bundle config set clean 'true'
|
||||
- run: bundle config set deployment 'true'
|
||||
- run: bundle config set with 'pam_authentication'
|
||||
- run: bundle config set without 'development production'
|
||||
- run: bundle config set frozen 'true'
|
||||
- run: bundle install --jobs 16 --retry 3 && bundle clean
|
||||
- run:
|
||||
name: Set bundler settings
|
||||
command: |
|
||||
bundle config clean 'true'
|
||||
bundle config deployment 'true'
|
||||
bundle config with 'pam_authentication'
|
||||
bundle config without 'development production'
|
||||
bundle config frozen 'true'
|
||||
- run:
|
||||
name: Install bundler dependencies
|
||||
command: bundle check || (bundle install && bundle clean)
|
||||
- save_cache:
|
||||
key: v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
|
||||
key: v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
|
||||
paths:
|
||||
- ./.bundle/
|
||||
- ./vendor/bundle/
|
||||
|
@ -88,17 +92,26 @@ aliases:
|
|||
- ./mastodon/vendor/bundle/
|
||||
|
||||
- &test_steps
|
||||
parallelism: 4
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- *install_system_dependencies
|
||||
- run: sudo apt-get install -y ffmpeg
|
||||
- run:
|
||||
name: Prepare Tests
|
||||
command: ./bin/rails parallel:create parallel:load_schema parallel:prepare
|
||||
name: Install FFMPEG
|
||||
command: sudo apt-get install -y ffmpeg
|
||||
- run:
|
||||
name: Run Tests
|
||||
command: ./bin/retry bundle exec parallel_test ./spec/ --group-by filesize --type rspec
|
||||
|
||||
name: Load database schema
|
||||
command: ./bin/rails db:create db:schema:load db:seed
|
||||
- run:
|
||||
name: Run rspec in parallel
|
||||
command: |
|
||||
bundle exec rspec --profile 10 \
|
||||
--format RspecJunitFormatter \
|
||||
--out test_results/rspec.xml \
|
||||
--format progress \
|
||||
$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
|
||||
- store_test_results:
|
||||
path: test_results
|
||||
jobs:
|
||||
install:
|
||||
<<: *defaults
|
||||
|
@ -115,19 +128,14 @@ jobs:
|
|||
environment: *ruby_environment
|
||||
<<: *install_ruby_dependencies
|
||||
|
||||
install-ruby2.5:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.5-buster-node
|
||||
environment: *ruby_environment
|
||||
<<: *install_ruby_dependencies
|
||||
|
||||
build:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- *install_system_dependencies
|
||||
- run: ./bin/rails assets:precompile
|
||||
- run:
|
||||
name: Precompile assets
|
||||
command: ./bin/rails assets:precompile
|
||||
- persist_to_workspace:
|
||||
root: ~/projects/
|
||||
paths:
|
||||
|
@ -149,10 +157,10 @@ jobs:
|
|||
- *install_system_dependencies
|
||||
- run:
|
||||
name: Create database
|
||||
command: ./bin/rails parallel:create
|
||||
command: ./bin/rails db:create
|
||||
- run:
|
||||
name: Run migrations
|
||||
command: ./bin/rails parallel:migrate
|
||||
command: ./bin/rails db:migrate
|
||||
|
||||
test-ruby2.7:
|
||||
<<: *defaults
|
||||
|
@ -178,35 +186,33 @@ jobs:
|
|||
- image: circleci/redis:5-alpine
|
||||
<<: *test_steps
|
||||
|
||||
test-ruby2.5:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.5-buster-node
|
||||
environment: *ruby_environment
|
||||
- image: circleci/postgres:12.2
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
- image: circleci/redis:5-alpine
|
||||
<<: *test_steps
|
||||
|
||||
test-webui:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/node:12-buster
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- run: ./bin/retry yarn test:jest
|
||||
- run:
|
||||
name: Run jest
|
||||
command: yarn test:jest
|
||||
|
||||
check-i18n:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- *install_system_dependencies
|
||||
- run: bundle exec i18n-tasks check-normalized
|
||||
- run: bundle exec i18n-tasks unused -l en
|
||||
- run: bundle exec i18n-tasks check-consistent-interpolations
|
||||
- run: bundle exec rake repo:check_locales_files
|
||||
- run:
|
||||
name: Check locale file normalization
|
||||
command: bundle exec i18n-tasks check-normalized
|
||||
- run:
|
||||
name: Check for unused strings
|
||||
command: bundle exec i18n-tasks unused -l en
|
||||
- run:
|
||||
name: Check for wrong string interpolations
|
||||
command: bundle exec i18n-tasks check-consistent-interpolations
|
||||
- run:
|
||||
name: Check that all required locale files exist
|
||||
command: bundle exec rake repo:check_locales_files
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
@ -220,10 +226,6 @@ workflows:
|
|||
requires:
|
||||
- install
|
||||
- install-ruby2.7
|
||||
- install-ruby2.5:
|
||||
requires:
|
||||
- install
|
||||
- install-ruby2.7
|
||||
- build:
|
||||
requires:
|
||||
- install-ruby2.7
|
||||
|
@ -238,10 +240,6 @@ workflows:
|
|||
requires:
|
||||
- install-ruby2.6
|
||||
- build
|
||||
- test-ruby2.5:
|
||||
requires:
|
||||
- install-ruby2.5
|
||||
- build
|
||||
- test-webui:
|
||||
requires:
|
||||
- install
|
||||
|
|
|
@ -30,7 +30,7 @@ plugins:
|
|||
channel: eslint-6
|
||||
rubocop:
|
||||
enabled: true
|
||||
channel: rubocop-0-76
|
||||
channel: rubocop-0-82
|
||||
sass-lint:
|
||||
enabled: true
|
||||
exclude_patterns:
|
||||
|
|
|
@ -4,7 +4,25 @@ update_configs:
|
|||
- package_manager: "ruby:bundler"
|
||||
directory: "/"
|
||||
update_schedule: "weekly"
|
||||
# Supported update schedule: live daily weekly monthly
|
||||
version_requirement_updates: "auto"
|
||||
# Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
|
||||
allowed_updates:
|
||||
- match:
|
||||
dependency_type: "all"
|
||||
# Supported dependency types: all indirect direct production development
|
||||
update_type: "all"
|
||||
# Supported update types: all security
|
||||
|
||||
- package_manager: "javascript"
|
||||
directory: "/"
|
||||
update_schedule: "weekly"
|
||||
# Supported update schedule: live daily weekly monthly
|
||||
version_requirement_updates: "auto"
|
||||
# Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
|
||||
allowed_updates:
|
||||
- match:
|
||||
dependency_type: "all"
|
||||
# Supported dependency types: all indirect direct production development
|
||||
update_type: "all"
|
||||
# Supported update types: all security
|
||||
|
|
|
@ -33,7 +33,7 @@ LOCAL_DOMAIN=example.com
|
|||
# ALTERNATE_DOMAINS=example1.com,example2.com
|
||||
|
||||
# Application secrets
|
||||
# Generate each with the `RAILS_ENV=production bundle exec rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
|
||||
# Generate each with the `RAILS_ENV=production bundle exec rake secret` task (`docker-compose run --rm web bundle exec rake secret` if you use docker compose)
|
||||
SECRET_KEY_BASE=
|
||||
OTP_SECRET=
|
||||
|
||||
|
@ -42,7 +42,7 @@ OTP_SECRET=
|
|||
# You should only generate this once per instance. If you later decide to change it, all push subscription will
|
||||
# be invalidated, requiring the users to access the website again to resubscribe.
|
||||
#
|
||||
# Generate with `RAILS_ENV=production bundle exec rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose)
|
||||
# Generate with `RAILS_ENV=production bundle exec rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web bundle exec rake mastodon:webpush:generate_vapid_key` if you use docker compose)
|
||||
#
|
||||
# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html
|
||||
VAPID_PRIVATE_KEY=
|
||||
|
|
|
@ -58,7 +58,7 @@ yarn-error.log
|
|||
yarn-debug.log
|
||||
|
||||
# Ignore vagrant log files
|
||||
ubuntu-xenial-16.04-cloudimg-console.log
|
||||
*-cloudimg-console.log
|
||||
|
||||
# Ignore Docker option files
|
||||
docker-compose.override.yml
|
||||
|
|
|
@ -2,7 +2,7 @@ require:
|
|||
- rubocop-rails
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.3
|
||||
TargetRubyVersion: 2.4
|
||||
Exclude:
|
||||
- 'spec/**/*'
|
||||
- 'db/**/*'
|
||||
|
@ -46,7 +46,7 @@ Metrics/ClassLength:
|
|||
Metrics/CyclomaticComplexity:
|
||||
Max: 25
|
||||
|
||||
Metrics/LineLength:
|
||||
Layout/LineLength:
|
||||
AllowURI: true
|
||||
Enabled: false
|
||||
|
||||
|
|
67
CHANGELOG.md
67
CHANGELOG.md
|
@ -3,6 +3,73 @@ Changelog
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [v3.1.4] - 2020-05-14
|
||||
### Added
|
||||
|
||||
- Add `vi` to available locales ([taicv](https://github.com/tootsuite/mastodon/pull/13542))
|
||||
- Add ability to remove identity proofs from account ([Gargron](https://github.com/tootsuite/mastodon/pull/13682))
|
||||
- Add ability to exclude local content from federated timeline ([noellabo](https://github.com/tootsuite/mastodon/pull/13504), [noellabo](https://github.com/tootsuite/mastodon/pull/13745))
|
||||
- Add `remote` param to `GET /api/v1/timelines/public` REST API
|
||||
- Add `public/remote` / `public:remote` variants to streaming API
|
||||
- "Remote only" option in federated timeline column settings in web UI
|
||||
- Add ability to exclude remote content from hashtag timelines in web UI ([noellabo](https://github.com/tootsuite/mastodon/pull/13502))
|
||||
- No changes to REST API
|
||||
- "Local only" option in hashtag column settings in web UI
|
||||
- Add Capistrano tasks that reload the services after deploying ([berkes](https://github.com/tootsuite/mastodon/pull/12642))
|
||||
- Add `invites_enabled` attribute to `GET /api/v1/instance` in REST API ([ThibG](https://github.com/tootsuite/mastodon/pull/13501))
|
||||
- Add `tootctl emoji export` command ([lfuelling](https://github.com/tootsuite/mastodon/pull/13534))
|
||||
- Add separate cache directory for non-local uploads ([Gargron](https://github.com/tootsuite/mastodon/pull/12821), [Hanage999](https://github.com/tootsuite/mastodon/pull/13593), [mayaeh](https://github.com/tootsuite/mastodon/pull/13551))
|
||||
- Add `tootctl upgrade storage-schema` command to move old non-local uploads to the cache directory
|
||||
- Add buttons to delete header and avatar from profile settings ([sternenseemann](https://github.com/tootsuite/mastodon/pull/13234))
|
||||
- Add emoji graphics and shortcodes from Twemoji 12.1.5 ([DeeUnderscore](https://github.com/tootsuite/mastodon/pull/13021))
|
||||
|
||||
### Changed
|
||||
|
||||
- Change error message when trying to migrate to an account that does not have current account set as an alias to be more clear ([TheEvilSkeleton](https://github.com/tootsuite/mastodon/pull/13746))
|
||||
- Change delivery failure tracking to work with hostnames instead of URLs ([Gargron](https://github.com/tootsuite/mastodon/pull/13437), [noellabo](https://github.com/tootsuite/mastodon/pull/13481), [noellabo](https://github.com/tootsuite/mastodon/pull/13482), [noellabo](https://github.com/tootsuite/mastodon/pull/13535))
|
||||
- Change Content-Security-Policy to not need unsafe-inline style-src ([ThibG](https://github.com/tootsuite/mastodon/pull/13679), [ThibG](https://github.com/tootsuite/mastodon/pull/13692), [ThibG](https://github.com/tootsuite/mastodon/pull/13576), [ThibG](https://github.com/tootsuite/mastodon/pull/13575), [ThibG](https://github.com/tootsuite/mastodon/pull/13438))
|
||||
- Change how RSS items are titled and formatted ([ThibG](https://github.com/tootsuite/mastodon/pull/13592), [ykzts](https://github.com/tootsuite/mastodon/pull/13591))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix dropdown of muted and followed accounts offering option to hide boosts in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13748))
|
||||
- Fix "You are already signed in" alert being shown at wrong times ([ThibG](https://github.com/tootsuite/mastodon/pull/13547))
|
||||
- Fix retrying of failed-to-download media files not actually working ([noellabo](https://github.com/tootsuite/mastodon/pull/13741))
|
||||
- Fix first poll option not being focused when adding a poll in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13740))
|
||||
- Fix `sr` locale being selected over `sr-Latn` ([ThibG](https://github.com/tootsuite/mastodon/pull/13693))
|
||||
- Fix error within error when limiting backtrace to 3 lines ([Gargron](https://github.com/tootsuite/mastodon/pull/13120))
|
||||
- Fix `tootctl media remove-orphans` crashing on "Import" files ([ThibG](https://github.com/tootsuite/mastodon/pull/13685))
|
||||
- Fix regression in `tootctl media remove-orphans` ([Gargron](https://github.com/tootsuite/mastodon/pull/13405))
|
||||
- Fix old unique jobs digests not having been cleaned up ([Gargron](https://github.com/tootsuite/mastodon/pull/13683))
|
||||
- Fix own following/followers not showing muted users ([ThibG](https://github.com/tootsuite/mastodon/pull/13614))
|
||||
- Fix list of followed people ignoring sorting on Follows & Followers page ([taras2358](https://github.com/tootsuite/mastodon/pull/13676))
|
||||
- Fix wrong pgHero Content-Security-Policy when `CDN_HOST` is set ([ThibG](https://github.com/tootsuite/mastodon/pull/13595))
|
||||
- Fix needlessly deduplicating usernames on collisions with remote accounts when signing-up through SAML/CAS ([kaiyou](https://github.com/tootsuite/mastodon/pull/13581))
|
||||
- Fix page incorrectly scrolling when bringing up dropdown menus in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13574))
|
||||
- Fix messed up z-index when NoScript blocks media/previews in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13449))
|
||||
- Fix "See what's happening" page showing public instead of local timeline for logged-in users ([ThibG](https://github.com/tootsuite/mastodon/pull/13499))
|
||||
- Fix not being able to resolve public resources in development environment ([Gargron](https://github.com/tootsuite/mastodon/pull/13505))
|
||||
- Fix uninformative error message when uploading unsupported image files ([ThibG](https://github.com/tootsuite/mastodon/pull/13540))
|
||||
- Fix expanded video player issues in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13541), [eai04191](https://github.com/tootsuite/mastodon/pull/13533))
|
||||
- Fix and refactor keyboard navigation in dropdown menus in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13528))
|
||||
- Fix uploaded image orientation being messed up in some browsers in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13493))
|
||||
- Fix actions log crash when displaying updates of deleted announcements in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13489))
|
||||
- Fix search not working due to proxy settings when using hidden services ([Gargron](https://github.com/tootsuite/mastodon/pull/13488))
|
||||
- Fix poll refresh button not being debounced in web UI ([rasjonell](https://github.com/tootsuite/mastodon/pull/13485), [ThibG](https://github.com/tootsuite/mastodon/pull/13490))
|
||||
- Fix confusing error when failing to add an alias to an unknown account ([ThibG](https://github.com/tootsuite/mastodon/pull/13480))
|
||||
- Fix "Email changed" notification sometimes having wrong e-mail ([ThibG](https://github.com/tootsuite/mastodon/pull/13475))
|
||||
- Fix varioues issues on the account aliases page ([ThibG](https://github.com/tootsuite/mastodon/pull/13452))
|
||||
- Fix API footer link in web UI ([bubblineyuri](https://github.com/tootsuite/mastodon/pull/13441))
|
||||
- Fix pagination of following, followers, follow requests, blocks and mutes lists in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13445))
|
||||
- Fix styling of polls in JS-less fallback on public pages ([ThibG](https://github.com/tootsuite/mastodon/pull/13436))
|
||||
- Fix trying to delete already deleted file when post-processing ([Gargron](https://github.com/tootsuite/mastodon/pull/13406))
|
||||
|
||||
### Security
|
||||
|
||||
- Fix Doorkeeper vulnerability that exposed app secret to users who authorized the app and reset secret of the web UI that could have been exposed ([dependabot-preview[bot]](https://github.com/tootsuite/mastodon/pull/13613), [Gargron](https://github.com/tootsuite/mastodon/pull/13688))
|
||||
- For apps that self-register on behalf of every individual user (such as most mobile apps), this is a non-issue
|
||||
- The issue only affects developers of apps who are shared between multiple users, such as server-side apps like cross-posters
|
||||
|
||||
## [v3.1.3] - 2020-04-05
|
||||
### Added
|
||||
|
||||
|
|
29
Gemfile
29
Gemfile
|
@ -20,7 +20,7 @@ gem 'makara', '~> 0.4'
|
|||
gem 'pghero', '~> 2.4'
|
||||
gem 'dotenv-rails', '~> 2.7'
|
||||
|
||||
gem 'aws-sdk-s3', '~> 1.61', require: false
|
||||
gem 'aws-sdk-s3', '~> 1.64', require: false
|
||||
gem 'fog-core', '<= 2.1.0'
|
||||
gem 'fog-openstack', '~> 0.3', require: false
|
||||
gem 'paperclip', '~> 6.0'
|
||||
|
@ -49,7 +49,7 @@ gem 'omniauth-saml', '~> 1.10'
|
|||
gem 'omniauth', '~> 1.9'
|
||||
|
||||
gem 'discard', '~> 1.2'
|
||||
gem 'doorkeeper', '~> 5.3'
|
||||
gem 'doorkeeper', '~> 5.4'
|
||||
gem 'fast_blank', '~> 1.0'
|
||||
gem 'fastimage'
|
||||
gem 'goldfinger', '~> 2.1'
|
||||
|
@ -57,12 +57,12 @@ gem 'hiredis', '~> 0.6'
|
|||
gem 'redis-namespace', '~> 1.7'
|
||||
gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b'
|
||||
gem 'htmlentities', '~> 4.3'
|
||||
gem 'http', '~> 4.3'
|
||||
gem 'http', '~> 4.4'
|
||||
gem 'http_accept_language', '~> 2.1'
|
||||
gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2', submodules: true
|
||||
gem 'httplog', '~> 1.4.2'
|
||||
gem 'idn-ruby', require: 'idn'
|
||||
gem 'kaminari', '~> 1.1'
|
||||
gem 'kaminari', '~> 1.2'
|
||||
gem 'link_header', '~> 0.0'
|
||||
gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar'
|
||||
gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532'
|
||||
|
@ -75,7 +75,7 @@ gem 'parallel', '~> 1.19'
|
|||
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
|
||||
gem 'pundit', '~> 2.1'
|
||||
gem 'premailer-rails'
|
||||
gem 'rack-attack', '~> 6.2'
|
||||
gem 'rack-attack', '~> 6.3'
|
||||
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
|
||||
gem 'rails-i18n', '~> 5.1'
|
||||
gem 'rails-settings-cached', '~> 0.6'
|
||||
|
@ -96,8 +96,8 @@ gem 'strong_migrations', '~> 0.6'
|
|||
gem 'tty-command', '~> 0.9', require: false
|
||||
gem 'tty-prompt', '~> 0.21', require: false
|
||||
gem 'twitter-text', '~> 1.14'
|
||||
gem 'tzinfo-data', '~> 1.2019'
|
||||
gem 'webpacker', '~> 4.2'
|
||||
gem 'tzinfo-data', '~> 1.2020'
|
||||
gem 'webpacker', '~> 5.1'
|
||||
gem 'webpush'
|
||||
|
||||
gem 'json-ld'
|
||||
|
@ -110,7 +110,7 @@ group :development, :test do
|
|||
gem 'fabrication', '~> 2.21'
|
||||
gem 'fuubar', '~> 2.5'
|
||||
gem 'i18n-tasks', '~> 0.9', require: false
|
||||
gem 'pry-byebug', '~> 3.8'
|
||||
gem 'pry-byebug', '~> 3.9'
|
||||
gem 'pry-rails', '~> 0.3'
|
||||
gem 'rspec-rails', '~> 4.0'
|
||||
end
|
||||
|
@ -120,7 +120,7 @@ group :production, :test do
|
|||
end
|
||||
|
||||
group :test do
|
||||
gem 'capybara', '~> 3.31'
|
||||
gem 'capybara', '~> 3.32'
|
||||
gem 'climate_control', '~> 0.2'
|
||||
gem 'faker', '~> 2.11'
|
||||
gem 'microformats', '~> 4.2'
|
||||
|
@ -129,23 +129,24 @@ group :test do
|
|||
gem 'simplecov', '~> 0.18', require: false
|
||||
gem 'webmock', '~> 3.8'
|
||||
gem 'parallel_tests', '~> 2.32'
|
||||
gem 'rspec_junit_formatter', '~> 0.4'
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem 'active_record_query_trace', '~> 1.7'
|
||||
gem 'annotate', '~> 3.0'
|
||||
gem 'better_errors', '~> 2.6'
|
||||
gem 'annotate', '~> 3.1'
|
||||
gem 'better_errors', '~> 2.7'
|
||||
gem 'binding_of_caller', '~> 0.7'
|
||||
gem 'bullet', '~> 6.1'
|
||||
gem 'letter_opener', '~> 1.7'
|
||||
gem 'letter_opener_web', '~> 1.4'
|
||||
gem 'memory_profiler'
|
||||
gem 'rubocop', '~> 0.79', require: false
|
||||
gem 'rubocop-rails', '~> 2.4', require: false
|
||||
gem 'rubocop', '~> 0.82', require: false
|
||||
gem 'rubocop-rails', '~> 2.5', require: false
|
||||
gem 'brakeman', '~> 4.8', require: false
|
||||
gem 'bundler-audit', '~> 0.6', require: false
|
||||
|
||||
gem 'capistrano', '~> 3.12'
|
||||
gem 'capistrano', '~> 3.14'
|
||||
gem 'capistrano-rails', '~> 1.4'
|
||||
gem 'capistrano-rbenv', '~> 2.1'
|
||||
gem 'capistrano-yarn', '~> 2.0'
|
||||
|
|
227
Gemfile.lock
227
Gemfile.lock
|
@ -82,7 +82,7 @@ GEM
|
|||
public_suffix (>= 2.0.2, < 5.0)
|
||||
airbrussh (1.4.0)
|
||||
sshkit (>= 1.6.1, != 1.7.0)
|
||||
annotate (3.0.3)
|
||||
annotate (3.1.1)
|
||||
activerecord (>= 3.2, < 7.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
arel (9.0.0)
|
||||
|
@ -91,24 +91,24 @@ GEM
|
|||
encryptor (~> 3.0.0)
|
||||
av (0.9.0)
|
||||
cocaine (~> 0.5.3)
|
||||
aws-eventstream (1.0.3)
|
||||
aws-partitions (1.286.0)
|
||||
aws-sdk-core (3.92.0)
|
||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||
aws-eventstream (1.1.0)
|
||||
aws-partitions (1.312.0)
|
||||
aws-sdk-core (3.95.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.30.0)
|
||||
aws-sdk-kms (1.31.0)
|
||||
aws-sdk-core (~> 3, >= 3.71.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.61.1)
|
||||
aws-sdk-s3 (1.64.0)
|
||||
aws-sdk-core (~> 3, >= 3.83.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.1.1)
|
||||
aws-sigv4 (1.1.3)
|
||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||
bcrypt (3.1.12)
|
||||
better_errors (2.6.0)
|
||||
bcrypt (3.1.13)
|
||||
better_errors (2.7.0)
|
||||
coderay (>= 1.0.0)
|
||||
erubi (>= 1.0.0)
|
||||
rack (>= 0.9.0)
|
||||
|
@ -118,8 +118,8 @@ GEM
|
|||
ffi (~> 1.10.0)
|
||||
bootsnap (1.4.6)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (4.8.0)
|
||||
browser (4.0.0)
|
||||
brakeman (4.8.1)
|
||||
browser (4.1.0)
|
||||
builder (3.2.4)
|
||||
bullet (6.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
|
@ -127,15 +127,14 @@ GEM
|
|||
bundler-audit (0.6.1)
|
||||
bundler (>= 1.2.0, < 3)
|
||||
thor (~> 0.18)
|
||||
byebug (11.1.1)
|
||||
capistrano (3.12.1)
|
||||
byebug (11.1.3)
|
||||
capistrano (3.14.0)
|
||||
airbrussh (>= 1.0.0)
|
||||
i18n
|
||||
rake (>= 10.0.0)
|
||||
sshkit (>= 1.9.0)
|
||||
capistrano-bundler (1.3.0)
|
||||
capistrano-bundler (1.6.0)
|
||||
capistrano (~> 3.1)
|
||||
sshkit (~> 1.2)
|
||||
capistrano-rails (1.4.0)
|
||||
capistrano (~> 3.1)
|
||||
capistrano-bundler (~> 1.1)
|
||||
|
@ -144,7 +143,7 @@ GEM
|
|||
sshkit (~> 1.3)
|
||||
capistrano-yarn (2.0.2)
|
||||
capistrano (~> 3.0)
|
||||
capybara (3.31.0)
|
||||
capybara (3.32.1)
|
||||
addressable
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (~> 1.8)
|
||||
|
@ -195,32 +194,32 @@ GEM
|
|||
docile (1.3.2)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (5.3.1)
|
||||
doorkeeper (5.4.0)
|
||||
railties (>= 5)
|
||||
dotenv (2.7.5)
|
||||
dotenv-rails (2.7.5)
|
||||
dotenv (= 2.7.5)
|
||||
railties (>= 3.2, < 6.1)
|
||||
e2mmap (0.1.0)
|
||||
elasticsearch (7.5.0)
|
||||
elasticsearch-api (= 7.5.0)
|
||||
elasticsearch-transport (= 7.5.0)
|
||||
elasticsearch-api (7.5.0)
|
||||
elasticsearch (7.6.0)
|
||||
elasticsearch-api (= 7.6.0)
|
||||
elasticsearch-transport (= 7.6.0)
|
||||
elasticsearch-api (7.6.0)
|
||||
multi_json
|
||||
elasticsearch-dsl (0.1.8)
|
||||
elasticsearch-transport (7.5.0)
|
||||
faraday (>= 0.14, < 1)
|
||||
elasticsearch-dsl (0.1.9)
|
||||
elasticsearch-transport (7.6.0)
|
||||
faraday (~> 1)
|
||||
multi_json
|
||||
encryptor (3.0.0)
|
||||
equatable (0.6.1)
|
||||
erubi (1.9.0)
|
||||
et-orbi (1.2.3)
|
||||
et-orbi (1.2.4)
|
||||
tzinfo
|
||||
excon (0.71.0)
|
||||
fabrication (2.21.0)
|
||||
excon (0.73.0)
|
||||
fabrication (2.21.1)
|
||||
faker (2.11.0)
|
||||
i18n (>= 1.6, < 2)
|
||||
faraday (0.17.3)
|
||||
faraday (1.0.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
fast_blank (1.0.0)
|
||||
fastimage (2.1.7)
|
||||
|
@ -241,7 +240,7 @@ GEM
|
|||
fog-json (>= 1.0)
|
||||
ipaddress (>= 0.8)
|
||||
formatador (0.2.5)
|
||||
fugit (1.3.3)
|
||||
fugit (1.3.5)
|
||||
et-orbi (~> 1.1, >= 1.1.8)
|
||||
raabro (~> 1.1)
|
||||
fuubar (2.5.0)
|
||||
|
@ -271,14 +270,14 @@ GEM
|
|||
hiredis (0.6.3)
|
||||
hkdf (0.3.0)
|
||||
htmlentities (4.3.4)
|
||||
http (4.3.0)
|
||||
http (4.4.1)
|
||||
addressable (~> 2.3)
|
||||
http-cookie (~> 1.0)
|
||||
http-form_data (~> 2.2)
|
||||
http-parser (~> 1.2.0)
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
http-form_data (2.2.0)
|
||||
http-form_data (2.3.0)
|
||||
http-parser (1.2.1)
|
||||
ffi-compiler (>= 1.0, < 2.0)
|
||||
http_accept_language (2.1.1)
|
||||
|
@ -304,7 +303,7 @@ GEM
|
|||
jmespath (1.4.0)
|
||||
json (2.3.0)
|
||||
json-canonicalization (0.2.0)
|
||||
json-ld (3.1.2)
|
||||
json-ld (3.1.4)
|
||||
htmlentities (~> 4.3)
|
||||
json-canonicalization (~> 0.2)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
|
@ -315,21 +314,21 @@ GEM
|
|||
json-ld (~> 3.1)
|
||||
rdf (~> 3.1)
|
||||
jsonapi-renderer (0.2.2)
|
||||
jwt (2.1.0)
|
||||
kaminari (1.1.1)
|
||||
jwt (2.2.1)
|
||||
kaminari (1.2.0)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.1.1)
|
||||
kaminari-activerecord (= 1.1.1)
|
||||
kaminari-core (= 1.1.1)
|
||||
kaminari-actionview (1.1.1)
|
||||
kaminari-actionview (= 1.2.0)
|
||||
kaminari-activerecord (= 1.2.0)
|
||||
kaminari-core (= 1.2.0)
|
||||
kaminari-actionview (1.2.0)
|
||||
actionview
|
||||
kaminari-core (= 1.1.1)
|
||||
kaminari-activerecord (1.1.1)
|
||||
kaminari-core (= 1.2.0)
|
||||
kaminari-activerecord (1.2.0)
|
||||
activerecord
|
||||
kaminari-core (= 1.1.1)
|
||||
kaminari-core (1.1.1)
|
||||
launchy (2.4.3)
|
||||
addressable (~> 2.3)
|
||||
kaminari-core (= 1.2.0)
|
||||
kaminari-core (1.2.0)
|
||||
launchy (2.5.0)
|
||||
addressable (~> 2.7)
|
||||
letter_opener (1.7.0)
|
||||
launchy (~> 2.2)
|
||||
letter_opener_web (1.4.0)
|
||||
|
@ -342,7 +341,7 @@ GEM
|
|||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.4.0)
|
||||
loofah (2.5.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
|
@ -354,14 +353,14 @@ GEM
|
|||
mario-redis-lock (1.2.1)
|
||||
redis (>= 3.0.5)
|
||||
memory_profiler (0.9.14)
|
||||
method_source (0.9.2)
|
||||
method_source (1.0.0)
|
||||
microformats (4.2.0)
|
||||
json (~> 2.2)
|
||||
nokogiri (~> 1.10)
|
||||
mime-types (3.3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2019.1009)
|
||||
mimemagic (0.3.4)
|
||||
mime-types-data (3.2020.0425)
|
||||
mimemagic (0.3.5)
|
||||
mini_mime (1.0.2)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.14.0)
|
||||
|
@ -370,20 +369,20 @@ GEM
|
|||
multipart-post (2.1.1)
|
||||
necromancer (0.5.1)
|
||||
net-ldap (0.16.2)
|
||||
net-scp (2.0.0)
|
||||
net-ssh (>= 2.6.5, < 6.0.0)
|
||||
net-ssh (5.2.0)
|
||||
net-scp (3.0.0)
|
||||
net-ssh (>= 2.6.5, < 7.0.0)
|
||||
net-ssh (6.0.2)
|
||||
nio4r (2.5.2)
|
||||
nokogiri (1.10.9)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
nokogumbo (2.0.1)
|
||||
nokogumbo (2.0.2)
|
||||
nokogiri (~> 1.8, >= 1.8.4)
|
||||
nsa (0.2.7)
|
||||
activesupport (>= 4.2, < 6)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sidekiq (>= 3.5)
|
||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||
oj (3.10.5)
|
||||
oj (3.10.6)
|
||||
omniauth (1.9.1)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 1.6.2, < 3)
|
||||
|
@ -408,40 +407,40 @@ GEM
|
|||
parallel (1.19.1)
|
||||
parallel_tests (2.32.0)
|
||||
parallel
|
||||
parser (2.7.0.5)
|
||||
parser (2.7.1.2)
|
||||
ast (~> 2.4.0)
|
||||
parslet (1.8.2)
|
||||
pastel (0.7.3)
|
||||
parslet (2.0.0)
|
||||
pastel (0.7.4)
|
||||
equatable (~> 0.6)
|
||||
tty-color (~> 0.5)
|
||||
pg (1.2.3)
|
||||
pghero (2.4.1)
|
||||
pghero (2.4.2)
|
||||
activerecord (>= 5)
|
||||
pkg-config (1.4.1)
|
||||
premailer (1.11.1)
|
||||
addressable
|
||||
css_parser (>= 1.6.0)
|
||||
htmlentities (>= 4.0.0)
|
||||
premailer-rails (1.10.3)
|
||||
premailer-rails (1.11.1)
|
||||
actionmailer (>= 3)
|
||||
premailer (~> 1.7, >= 1.7.9)
|
||||
private_address_check (0.5.0)
|
||||
pry (0.12.2)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.9.0)
|
||||
pry-byebug (3.8.0)
|
||||
pry (0.13.1)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
pry-byebug (3.9.0)
|
||||
byebug (~> 11.0)
|
||||
pry (~> 0.10)
|
||||
pry (~> 0.13.0)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.3)
|
||||
public_suffix (4.0.5)
|
||||
puma (4.3.3)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.1.6)
|
||||
raabro (1.3.1)
|
||||
rack (2.2.2)
|
||||
rack-attack (6.2.2)
|
||||
rack-attack (6.3.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-cors (1.1.1)
|
||||
rack (>= 2.0.0)
|
||||
|
@ -492,40 +491,41 @@ GEM
|
|||
rdf-normalize (0.4.0)
|
||||
rdf (~> 3.1)
|
||||
redcarpet (3.5.0)
|
||||
redis (4.1.3)
|
||||
redis-actionpack (5.0.2)
|
||||
actionpack (>= 4.0, < 6)
|
||||
redis-rack (>= 1, < 3)
|
||||
redis (4.1.4)
|
||||
redis-actionpack (5.2.0)
|
||||
actionpack (>= 5, < 7)
|
||||
redis-rack (>= 2.1.0, < 3)
|
||||
redis-store (>= 1.1.0, < 2)
|
||||
redis-activesupport (5.0.4)
|
||||
activesupport (>= 3, < 6)
|
||||
redis-activesupport (5.2.0)
|
||||
activesupport (>= 3, < 7)
|
||||
redis-store (>= 1.3, < 2)
|
||||
redis-namespace (1.7.0)
|
||||
redis (>= 3.0.4)
|
||||
redis-rack (2.0.4)
|
||||
rack (>= 1.5, < 3)
|
||||
redis-rack (2.1.2)
|
||||
rack (>= 2.0.8, < 3)
|
||||
redis-store (>= 1.2, < 2)
|
||||
redis-rails (5.0.2)
|
||||
redis-actionpack (>= 5.0, < 6)
|
||||
redis-activesupport (>= 5.0, < 6)
|
||||
redis-store (>= 1.2, < 2)
|
||||
redis-store (1.5.0)
|
||||
redis (>= 2.2, < 5)
|
||||
regexp_parser (1.6.0)
|
||||
redis-store (1.8.2)
|
||||
redis (>= 4, < 5)
|
||||
regexp_parser (1.7.0)
|
||||
request_store (1.5.0)
|
||||
rack (>= 1.4)
|
||||
responders (3.0.0)
|
||||
actionpack (>= 5.0)
|
||||
railties (>= 5.0)
|
||||
rexml (3.2.4)
|
||||
rotp (2.1.2)
|
||||
rpam2 (4.0.2)
|
||||
rqrcode (1.1.2)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 0.1)
|
||||
rqrcode_core (0.1.1)
|
||||
rspec-core (3.9.1)
|
||||
rspec-support (~> 3.9.1)
|
||||
rspec-expectations (3.9.1)
|
||||
rqrcode_core (0.1.2)
|
||||
rspec-core (3.9.2)
|
||||
rspec-support (~> 3.9.3)
|
||||
rspec-expectations (3.9.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-mocks (3.9.1)
|
||||
|
@ -542,19 +542,23 @@ GEM
|
|||
rspec-sidekiq (3.0.3)
|
||||
rspec-core (~> 3.0, >= 3.0.0)
|
||||
sidekiq (>= 2.4.0)
|
||||
rspec-support (3.9.2)
|
||||
rubocop (0.79.0)
|
||||
rspec-support (3.9.3)
|
||||
rspec_junit_formatter (0.4.1)
|
||||
rspec-core (>= 2, < 4, != 2.12.0)
|
||||
rubocop (0.82.0)
|
||||
jaro_winkler (~> 1.5.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.7.0.1)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
rexml
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 1.7)
|
||||
rubocop-rails (2.4.2)
|
||||
unicode-display_width (>= 1.4.0, < 2.0)
|
||||
rubocop-rails (2.5.2)
|
||||
activesupport
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 0.72.0)
|
||||
ruby-progressbar (1.10.1)
|
||||
ruby-saml (1.9.0)
|
||||
ruby-saml (1.11.0)
|
||||
nokogiri (>= 1.5.10)
|
||||
rufus-scheduler (3.6.0)
|
||||
fugit (~> 1.1, >= 1.1.6)
|
||||
|
@ -563,9 +567,10 @@ GEM
|
|||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.8.0)
|
||||
nokogumbo (~> 2.0)
|
||||
sidekiq (6.0.4)
|
||||
semantic_range (2.3.0)
|
||||
sidekiq (6.0.7)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (>= 2.0.0)
|
||||
rack (~> 2.0)
|
||||
rack-protection (>= 2.0.0)
|
||||
redis (>= 4.1.0)
|
||||
sidekiq-bulk (0.2.0)
|
||||
|
@ -605,7 +610,7 @@ GEM
|
|||
stoplight (2.2.0)
|
||||
streamio-ffmpeg (3.0.2)
|
||||
multi_json (~> 1.8)
|
||||
strong_migrations (0.6.2)
|
||||
strong_migrations (0.6.6)
|
||||
activerecord (>= 5)
|
||||
temple (0.8.2)
|
||||
terminal-table (1.8.0)
|
||||
|
@ -633,12 +638,12 @@ GEM
|
|||
unf (~> 0.1.0)
|
||||
tzinfo (1.2.7)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo-data (1.2019.3)
|
||||
tzinfo-data (1.2020.1)
|
||||
tzinfo (>= 1.0.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.6)
|
||||
unicode-display_width (1.6.1)
|
||||
unf_ext (0.0.7.7)
|
||||
unicode-display_width (1.7.0)
|
||||
uniform_notifier (1.13.0)
|
||||
warden (1.2.8)
|
||||
rack (>= 2.0.6)
|
||||
|
@ -646,10 +651,11 @@ GEM
|
|||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webpacker (4.2.2)
|
||||
activesupport (>= 4.2)
|
||||
webpacker (5.1.1)
|
||||
activesupport (>= 5.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 4.2)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
webpush (0.3.8)
|
||||
hkdf (~> 0.2)
|
||||
jwt (~> 2.0)
|
||||
|
@ -667,9 +673,9 @@ DEPENDENCIES
|
|||
active_model_serializers (~> 0.10)
|
||||
active_record_query_trace (~> 1.7)
|
||||
addressable (~> 2.7)
|
||||
annotate (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.61)
|
||||
better_errors (~> 2.6)
|
||||
annotate (~> 3.1)
|
||||
aws-sdk-s3 (~> 1.64)
|
||||
better_errors (~> 2.7)
|
||||
binding_of_caller (~> 0.7)
|
||||
blurhash (~> 0.1)
|
||||
bootsnap (~> 1.4)
|
||||
|
@ -677,11 +683,11 @@ DEPENDENCIES
|
|||
browser
|
||||
bullet (~> 6.1)
|
||||
bundler-audit (~> 0.6)
|
||||
capistrano (~> 3.12)
|
||||
capistrano (~> 3.14)
|
||||
capistrano-rails (~> 1.4)
|
||||
capistrano-rbenv (~> 2.1)
|
||||
capistrano-yarn (~> 2.0)
|
||||
capybara (~> 3.31)
|
||||
capybara (~> 3.32)
|
||||
charlock_holmes (~> 0.7.7)
|
||||
chewy (~> 5.1)
|
||||
cld3 (~> 3.3.0)
|
||||
|
@ -692,7 +698,7 @@ DEPENDENCIES
|
|||
devise-two-factor (~> 3.1)
|
||||
devise_pam_authenticatable2 (~> 9.2)
|
||||
discard (~> 1.2)
|
||||
doorkeeper (~> 5.3)
|
||||
doorkeeper (~> 5.4)
|
||||
dotenv-rails (~> 2.7)
|
||||
e2mmap (~> 0.1.0)
|
||||
fabrication (~> 2.21)
|
||||
|
@ -707,7 +713,7 @@ DEPENDENCIES
|
|||
health_check!
|
||||
hiredis (~> 0.6)
|
||||
htmlentities (~> 4.3)
|
||||
http (~> 4.3)
|
||||
http (~> 4.4)
|
||||
http_accept_language (~> 2.1)
|
||||
http_parser.rb (~> 0.6)!
|
||||
httplog (~> 1.4.2)
|
||||
|
@ -716,7 +722,7 @@ DEPENDENCIES
|
|||
iso-639
|
||||
json-ld
|
||||
json-ld-preloaded (~> 3.1)
|
||||
kaminari (~> 1.1)
|
||||
kaminari (~> 1.2)
|
||||
letter_opener (~> 1.7)
|
||||
letter_opener_web (~> 1.4)
|
||||
link_header (~> 0.0)
|
||||
|
@ -746,12 +752,12 @@ DEPENDENCIES
|
|||
posix-spawn!
|
||||
premailer-rails
|
||||
private_address_check (~> 0.5)
|
||||
pry-byebug (~> 3.8)
|
||||
pry-byebug (~> 3.9)
|
||||
pry-rails (~> 0.3)
|
||||
puma (~> 4.3)
|
||||
pundit (~> 2.1)
|
||||
rack (~> 2.2.2)
|
||||
rack-attack (~> 6.2)
|
||||
rack-attack (~> 6.3)
|
||||
rack-cors (~> 1.1)
|
||||
rails (~> 5.2.4.2)
|
||||
rails-controller-testing (~> 1.0)
|
||||
|
@ -765,8 +771,9 @@ DEPENDENCIES
|
|||
rqrcode (~> 1.1)
|
||||
rspec-rails (~> 4.0)
|
||||
rspec-sidekiq (~> 3.0)
|
||||
rubocop (~> 0.79)
|
||||
rubocop-rails (~> 2.4)
|
||||
rspec_junit_formatter (~> 0.4)
|
||||
rubocop (~> 0.82)
|
||||
rubocop-rails (~> 2.5)
|
||||
ruby-progressbar (~> 1.10)
|
||||
sanitize (~> 5.1)
|
||||
sidekiq (~> 6.0)
|
||||
|
@ -787,7 +794,7 @@ DEPENDENCIES
|
|||
tty-command (~> 0.9)
|
||||
tty-prompt (~> 0.21)
|
||||
twitter-text (~> 1.14)
|
||||
tzinfo-data (~> 1.2019)
|
||||
tzinfo-data (~> 1.2020)
|
||||
webmock (~> 3.8)
|
||||
webpacker (~> 4.2)
|
||||
webpacker (~> 5.1)
|
||||
webpush
|
||||
|
|
|
@ -28,7 +28,7 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
|
||||
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
|
||||
@statuses = filtered_status_page(params)
|
||||
@statuses = filtered_status_page
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
@rss_url = rss_url
|
||||
|
||||
|
@ -41,7 +41,7 @@ class AccountsController < ApplicationController
|
|||
format.rss do
|
||||
expires_in 1.minute, public: true
|
||||
|
||||
@statuses = filtered_statuses.without_reblogs.without_replies.limit(PAGE_SIZE)
|
||||
@statuses = filtered_statuses.without_reblogs.limit(PAGE_SIZE)
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag])
|
||||
end
|
||||
|
@ -130,23 +130,23 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
|
||||
def media_requested?
|
||||
request.path.ends_with?('/media') && !tag_requested?
|
||||
request.path.split('.').first.ends_with?('/media') && !tag_requested?
|
||||
end
|
||||
|
||||
def replies_requested?
|
||||
request.path.ends_with?('/with_replies') && !tag_requested?
|
||||
request.path.split('.').first.ends_with?('/with_replies') && !tag_requested?
|
||||
end
|
||||
|
||||
def tag_requested?
|
||||
request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
|
||||
end
|
||||
|
||||
def filtered_status_page(params)
|
||||
if params[:min_id].present?
|
||||
filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse
|
||||
else
|
||||
filtered_statuses.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]).to_a
|
||||
end
|
||||
def filtered_status_page
|
||||
filtered_statuses.paginate_by_id(PAGE_SIZE, params_slice(:max_id, :min_id, :since_id))
|
||||
end
|
||||
|
||||
def params_slice(*keys)
|
||||
params.slice(*keys).permit(*keys)
|
||||
end
|
||||
|
||||
def restrict_fields_to
|
||||
|
|
|
@ -24,20 +24,23 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
|||
def set_size
|
||||
case params[:id]
|
||||
when 'featured'
|
||||
@account.pinned_statuses.count
|
||||
@size = @account.pinned_statuses.count
|
||||
else
|
||||
raise ActiveRecord::RecordNotFound
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
def scope_for_collection
|
||||
case params[:id]
|
||||
when 'featured'
|
||||
return Status.none if @account.blocking?(signed_request_account)
|
||||
|
||||
@account.pinned_statuses
|
||||
else
|
||||
raise ActiveRecord::RecordNotFound
|
||||
# Because in public fetch mode we cache the response, there would be no
|
||||
# benefit from performing the check below, since a blocked account or domain
|
||||
# would likely be served the cache from the reverse proxy anyway
|
||||
if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
|
||||
Status.none
|
||||
else
|
||||
@account.pinned_statuses
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
|||
ResolveAccountWorker.perform_async(signed_request_account.acct)
|
||||
end
|
||||
|
||||
DeliveryFailureTracker.track_inverse_success!(signed_request_account)
|
||||
DeliveryFailureTracker.reset!(signed_request_account.inbox_url)
|
||||
end
|
||||
|
||||
def process_payload
|
||||
|
|
|
@ -11,7 +11,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
|||
before_action :set_cache_headers
|
||||
|
||||
def show
|
||||
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
|
||||
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode? && !(signed_request_account.present? && page_requested?))
|
||||
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||
end
|
||||
|
||||
|
@ -50,12 +50,12 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
|||
return unless page_requested?
|
||||
|
||||
@statuses = @account.statuses.permitted_for(@account, signed_request_account)
|
||||
@statuses = params[:min_id].present? ? @statuses.paginate_by_min_id(LIMIT, params[:min_id]).reverse : @statuses.paginate_by_max_id(LIMIT, params[:max_id])
|
||||
@statuses = @statuses.paginate_by_id(LIMIT, params_slice(:max_id, :min_id, :since_id))
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
end
|
||||
|
||||
def page_requested?
|
||||
params[:page] == 'true'
|
||||
truthy_param?(:page)
|
||||
end
|
||||
|
||||
def page_params
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::RepliesController < ActivityPub::BaseController
|
||||
include SignatureAuthentication
|
||||
include SignatureVerification
|
||||
include Authorization
|
||||
include AccountOwnedConcern
|
||||
|
||||
|
@ -19,15 +19,19 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
|
|||
|
||||
private
|
||||
|
||||
def pundit_user
|
||||
signed_request_account
|
||||
end
|
||||
|
||||
def set_status
|
||||
@status = @account.statuses.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
raise ActiveRecord::RecordNotFound
|
||||
not_found
|
||||
end
|
||||
|
||||
def set_replies
|
||||
@replies = page_params[:only_other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses
|
||||
@replies = only_other_accounts? ? Status.where.not(account_id: @account.id) : @account.statuses
|
||||
@replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
|
||||
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
|
||||
end
|
||||
|
@ -38,7 +42,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
|
|||
type: :unordered,
|
||||
part_of: account_status_replies_url(@account, @status),
|
||||
next: next_page,
|
||||
items: @replies.map { |status| status.local ? status : status.uri }
|
||||
items: @replies.map { |status| status.local? ? status : status.uri }
|
||||
)
|
||||
|
||||
return page if page_requested?
|
||||
|
@ -51,16 +55,21 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
|
|||
end
|
||||
|
||||
def page_requested?
|
||||
params[:page] == 'true'
|
||||
truthy_param?(:page)
|
||||
end
|
||||
|
||||
def only_other_accounts?
|
||||
truthy_param?(:only_other_accounts)
|
||||
end
|
||||
|
||||
def next_page
|
||||
only_other_accounts = !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT)
|
||||
|
||||
account_status_replies_url(
|
||||
@account,
|
||||
@status,
|
||||
page: true,
|
||||
min_id: only_other_accounts && !page_params[:only_other_accounts] ? nil : @replies&.last&.id,
|
||||
min_id: only_other_accounts && !only_other_accounts? ? nil : @replies&.last&.id,
|
||||
only_other_accounts: only_other_accounts
|
||||
)
|
||||
end
|
||||
|
|
|
@ -19,7 +19,7 @@ module Admin
|
|||
@followers_count = Follow.where(target_account: Account.where(domain: params[:id])).count
|
||||
@reports_count = Report.where(target_account: Account.where(domain: params[:id])).count
|
||||
@blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count
|
||||
@available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url)
|
||||
@available = DeliveryFailureTracker.available?(params[:id])
|
||||
@media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size)
|
||||
@private_comment = @domain_block&.private_comment
|
||||
@public_comment = @domain_block&.public_comment
|
||||
|
|
|
@ -20,7 +20,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
|
|||
return [] if hide_results?
|
||||
|
||||
scope = default_accounts
|
||||
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
|
||||
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id
|
||||
scope.merge(paginated_follows).to_a
|
||||
end
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
|
|||
return [] if hide_results?
|
||||
|
||||
scope = default_accounts
|
||||
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
|
||||
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id
|
||||
scope.merge(paginated_follows).to_a
|
||||
end
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ class Api::V1::Polls::VotesController < Api::BaseController
|
|||
@poll = Poll.attached.find(params[:poll_id])
|
||||
authorize @poll.status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
raise ActiveRecord::RecordNotFound
|
||||
not_found
|
||||
end
|
||||
|
||||
def vote_params
|
||||
|
|
|
@ -17,7 +17,7 @@ class Api::V1::PollsController < Api::BaseController
|
|||
@poll = Poll.attached.find(params[:id])
|
||||
authorize @poll.status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
raise ActiveRecord::RecordNotFound
|
||||
not_found
|
||||
end
|
||||
|
||||
def refresh_poll
|
||||
|
|
|
@ -4,6 +4,7 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
|||
before_action -> { doorkeeper_authorize! :push }
|
||||
before_action :require_user!
|
||||
before_action :set_web_push_subscription
|
||||
before_action :check_web_push_subscription, only: [:show, :update]
|
||||
|
||||
def create
|
||||
@web_subscription&.destroy!
|
||||
|
@ -21,16 +22,11 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
|||
end
|
||||
|
||||
def show
|
||||
raise ActiveRecord::RecordNotFound if @web_subscription.nil?
|
||||
|
||||
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
end
|
||||
|
||||
def update
|
||||
raise ActiveRecord::RecordNotFound if @web_subscription.nil?
|
||||
|
||||
@web_subscription.update!(data: data_params)
|
||||
|
||||
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
end
|
||||
|
||||
|
@ -45,12 +41,17 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
|||
@web_subscription = ::Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id)
|
||||
end
|
||||
|
||||
def check_web_push_subscription
|
||||
not_found if @web_subscription.nil?
|
||||
end
|
||||
|
||||
def subscription_params
|
||||
params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
|
||||
end
|
||||
|
||||
def data_params
|
||||
return {} if params[:data].blank?
|
||||
|
||||
params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -28,8 +28,7 @@ class Api::V1::Statuses::MutesController < Api::BaseController
|
|||
@status = Status.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
# Reraise in order to get a 404 instead of a 403 error code
|
||||
raise ActiveRecord::RecordNotFound
|
||||
not_found
|
||||
end
|
||||
|
||||
def set_conversation
|
||||
|
|
|
@ -68,7 +68,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
@status = Status.find(params[:id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
raise ActiveRecord::RecordNotFound
|
||||
not_found
|
||||
end
|
||||
|
||||
def set_thread
|
||||
|
|
|
@ -39,7 +39,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
|
|||
end
|
||||
|
||||
def public_timeline_statuses
|
||||
Status.as_public_timeline(current_account, truthy_param?(:local))
|
||||
Status.as_public_timeline(current_account, truthy_param?(:remote) ? :remote : truthy_param?(:local))
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
|
@ -47,7 +47,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
|
|||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.slice(:local, :limit, :only_media).permit(:local, :limit, :only_media).merge(core_params)
|
||||
params.slice(:local, :remote, :limit, :only_media).permit(:local, :remote, :limit, :only_media).merge(core_params)
|
||||
end
|
||||
|
||||
def next_path
|
||||
|
|
|
@ -113,6 +113,13 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
render :two_factor
|
||||
end
|
||||
|
||||
def require_no_authentication
|
||||
super
|
||||
# Delete flash message that isn't entirely useful and may be confusing in
|
||||
# most cases because /web doesn't display/clear flash messages.
|
||||
flash.delete(:alert) if flash[:alert] == I18n.t('devise.failure.already_authenticated')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
|
|
|
@ -28,18 +28,6 @@ module Localized
|
|||
end
|
||||
|
||||
def request_locale
|
||||
preferred_locale || compatible_locale
|
||||
end
|
||||
|
||||
def preferred_locale
|
||||
http_accept_language.preferred_language_from(available_locales)
|
||||
end
|
||||
|
||||
def compatible_locale
|
||||
http_accept_language.compatible_language_from(available_locales)
|
||||
end
|
||||
|
||||
def available_locales
|
||||
I18n.available_locales.reverse
|
||||
http_accept_language.language_region_compatible_from(I18n.available_locales)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -33,7 +33,7 @@ class MediaController < ApplicationController
|
|||
def verify_permitted_status!
|
||||
authorize @media_attachment.status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
raise ActiveRecord::RecordNotFound
|
||||
not_found
|
||||
end
|
||||
|
||||
def check_playable
|
||||
|
|
|
@ -42,7 +42,7 @@ class RemoteInteractionController < ApplicationController
|
|||
@status = Status.find(params[:id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
raise ActiveRecord::RecordNotFound
|
||||
not_found
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
|
|
|
@ -22,8 +22,7 @@ class Settings::IdentityProofsController < Settings::BaseController
|
|||
if current_account.username.casecmp(params[:username]).zero?
|
||||
render layout: 'auth'
|
||||
else
|
||||
flash[:alert] = I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username)
|
||||
redirect_to settings_identity_proofs_path
|
||||
redirect_to settings_identity_proofs_path, alert: I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -35,11 +34,16 @@ class Settings::IdentityProofsController < Settings::BaseController
|
|||
PostStatusService.new.call(current_user.account, text: post_params[:status_text]) if publish_proof?
|
||||
redirect_to @proof.on_success_path(params[:user_agent])
|
||||
else
|
||||
flash[:alert] = I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize)
|
||||
redirect_to settings_identity_proofs_path
|
||||
redirect_to settings_identity_proofs_path, alert: I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@proof = current_account.identity_proofs.find(params[:id])
|
||||
@proof.destroy!
|
||||
redirect_to settings_identity_proofs_path, success: I18n.t('identity_proofs.removed')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_enabled
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Settings
|
||||
class PicturesController < BaseController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_account
|
||||
before_action :set_picture
|
||||
|
||||
def destroy
|
||||
if valid_picture
|
||||
account_params = {
|
||||
@picture => nil,
|
||||
(@picture + '_remote_url') => nil,
|
||||
}
|
||||
|
||||
msg = UpdateAccountService.new.call(@account, account_params) ? I18n.t('generic.changes_saved_msg') : nil
|
||||
redirect_to settings_profile_path, notice: msg, status: 303
|
||||
else
|
||||
bad_request
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = current_account
|
||||
end
|
||||
|
||||
def set_picture
|
||||
@picture = params[:id]
|
||||
end
|
||||
|
||||
def valid_picture
|
||||
@picture == 'avatar' || @picture == 'header'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -49,7 +49,7 @@ class StatusesController < ApplicationController
|
|||
|
||||
def embed
|
||||
use_pack 'embed'
|
||||
return not_found if @status.hidden?
|
||||
return not_found if @status.hidden? || @status.reblog?
|
||||
|
||||
expires_in 180, public: true
|
||||
response.headers['X-Frame-Options'] = 'ALLOWALL'
|
||||
|
|
|
@ -10,6 +10,7 @@ class TagsController < ApplicationController
|
|||
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :authenticate_user!, if: :whitelist_mode?
|
||||
before_action :set_tag
|
||||
before_action :set_local
|
||||
before_action :set_body_classes
|
||||
before_action :set_instance_presenter
|
||||
|
||||
|
@ -25,7 +26,7 @@ class TagsController < ApplicationController
|
|||
format.rss do
|
||||
expires_in 0, public: true
|
||||
|
||||
@statuses = HashtagQueryService.new.call(@tag, filter_params).limit(PAGE_SIZE)
|
||||
@statuses = HashtagQueryService.new.call(@tag, filter_params, nil, @local).limit(PAGE_SIZE)
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
|
||||
render xml: RSS::TagSerializer.render(@tag, @statuses)
|
||||
|
@ -34,7 +35,7 @@ class TagsController < ApplicationController
|
|||
format.json do
|
||||
expires_in 3.minutes, public: public_fetch_mode?
|
||||
|
||||
@statuses = HashtagQueryService.new.call(@tag, filter_params, current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id])
|
||||
@statuses = HashtagQueryService.new.call(@tag, filter_params, current_account, @local).paginate_by_max_id(PAGE_SIZE, params[:max_id])
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
|
||||
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||
|
@ -48,6 +49,10 @@ class TagsController < ApplicationController
|
|||
@tag = Tag.usable.find_normalized!(params[:id])
|
||||
end
|
||||
|
||||
def set_local
|
||||
@local = truthy_param?(:local)
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'with-modals'
|
||||
end
|
||||
|
|
|
@ -47,7 +47,7 @@ module Admin::ActionLogsHelper
|
|||
I18n.t('admin.action_logs.deleted_status')
|
||||
end
|
||||
when 'Announcement'
|
||||
truncate(attributes['text'])
|
||||
truncate(attributes['text'].is_a?(Array) ? attributes['text'].last : attributes['text'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,13 +7,13 @@ module HomeHelper
|
|||
}
|
||||
end
|
||||
|
||||
def account_link_to(account, button = '', size: 36, path: nil)
|
||||
def account_link_to(account, button = '', path: nil)
|
||||
content_tag(:div, class: 'account') do
|
||||
content_tag(:div, class: 'account__wrapper') do
|
||||
section = if account.nil?
|
||||
content_tag(:div, class: 'account__display-name') do
|
||||
content_tag(:div, class: 'account__avatar-wrapper') do
|
||||
content_tag(:div, '', class: 'account__avatar', style: "width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px; background-image: url(#{full_asset_url('avatars/original/missing.png', skip_pipeline: true)})")
|
||||
image_tag(full_asset_url('avatars/original/missing.png', skip_pipeline: true), class: 'account__avatar')
|
||||
end +
|
||||
content_tag(:span, class: 'display-name') do
|
||||
content_tag(:strong, t('about.contact_missing')) +
|
||||
|
@ -23,7 +23,7 @@ module HomeHelper
|
|||
else
|
||||
link_to(path || ActivityPub::TagManager.instance.url_for(account), class: 'account__display-name') do
|
||||
content_tag(:div, class: 'account__avatar-wrapper') do
|
||||
content_tag(:div, '', class: 'account__avatar', style: "width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px; background-image: url(#{full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url)})")
|
||||
image_tag(full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url), class: 'account__avatar')
|
||||
end +
|
||||
content_tag(:span, class: 'display-name') do
|
||||
content_tag(:bdi) do
|
||||
|
|
|
@ -68,6 +68,7 @@ module SettingsHelper
|
|||
tr: 'Türkçe',
|
||||
uk: 'Українська',
|
||||
ur: 'اُردُو',
|
||||
vi: 'Tiếng Việt',
|
||||
'zh-CN': '简体中文',
|
||||
'zh-HK': '繁體中文(香港)',
|
||||
'zh-TW': '繁體中文(臺灣)',
|
||||
|
@ -105,4 +106,13 @@ module SettingsHelper
|
|||
safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ')
|
||||
end
|
||||
end
|
||||
|
||||
def picture_hint(hint, picture)
|
||||
if picture.original_filename.nil?
|
||||
hint
|
||||
else
|
||||
link = link_to t('generic.delete'), settings_profile_picture_path(picture.name.to_s), data: { method: :delete }
|
||||
safe_join([hint, link], '<br/>'.html_safe)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module WebfingerHelper
|
||||
def webfinger!(uri)
|
||||
hidden_service_uri = /\.(onion|i2p)(:\d+)?$/.match(uri)
|
||||
|
||||
raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if !Rails.configuration.x.access_to_hidden_service && hidden_service_uri
|
||||
|
||||
opts = {
|
||||
ssl: !hidden_service_uri,
|
||||
|
||||
headers: {
|
||||
'User-Agent': Mastodon::Version.user_agent,
|
||||
},
|
||||
}
|
||||
|
||||
Goldfinger::Client.new(uri, opts.merge(Rails.configuration.x.http_client_proxy)).finger
|
||||
end
|
||||
end
|
|
@ -1,6 +1,5 @@
|
|||
// This file will be loaded on public pages, regardless of theme.
|
||||
|
||||
import createHistory from 'history/createBrowserHistory';
|
||||
import ready from '../mastodon/ready';
|
||||
|
||||
const { delegate } = require('@rails/ujs');
|
||||
|
|
|
@ -10,7 +10,7 @@ delegate(document, '#account_display_name', 'input', ({ target }) => {
|
|||
if (target.value) {
|
||||
name.innerHTML = emojify(escapeTextContentForBrowser(target.value));
|
||||
} else {
|
||||
name.textContent = document.querySelector('#default_account_display_name').textContent;
|
||||
name.textContent = name.textContent = target.dataset.default;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -73,7 +73,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
|
|||
|
||||
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
|
||||
export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
|
||||
export const connectPublicStream = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`);
|
||||
export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) => connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`);
|
||||
export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
|
||||
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
|
||||
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
|
||||
|
|
|
@ -55,7 +55,7 @@ export function updateTimeline(timeline, status, accept) {
|
|||
export function deleteFromTimelines(id) {
|
||||
return (dispatch, getState) => {
|
||||
const accountId = getState().getIn(['statuses', id, 'account']);
|
||||
const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => [status.get('id'), status.get('account')]);
|
||||
const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id'));
|
||||
const reblogOf = getState().getIn(['statuses', id, 'reblog'], null);
|
||||
|
||||
dispatch({
|
||||
|
@ -121,15 +121,14 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
|
|||
};
|
||||
|
||||
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
|
||||
export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
|
||||
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
|
||||
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
|
||||
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
|
||||
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
||||
|
||||
export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => {
|
||||
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
|
||||
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||
max_id: maxId,
|
||||
any: parseTags(tags, 'any'),
|
||||
|
|
|
@ -46,7 +46,7 @@ class DropdownMenu extends React.PureComponent {
|
|||
document.addEventListener('keydown', this.handleKeyDown, false);
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
if (this.focusedItem && this.props.openedViaKeyboard) {
|
||||
this.focusedItem.focus();
|
||||
this.focusedItem.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ mounted: true });
|
||||
}
|
||||
|
@ -68,20 +68,14 @@ class DropdownMenu extends React.PureComponent {
|
|||
handleKeyDown = e => {
|
||||
const items = Array.from(this.node.getElementsByTagName('a'));
|
||||
const index = items.indexOf(document.activeElement);
|
||||
let element;
|
||||
let element = null;
|
||||
|
||||
switch(e.key) {
|
||||
case 'ArrowDown':
|
||||
element = items[index+1];
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
element = items[index+1] || items[0];
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
element = items[index-1];
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
element = items[index-1] || items[items.length-1];
|
||||
break;
|
||||
case 'Tab':
|
||||
if (e.shiftKey) {
|
||||
|
@ -89,28 +83,23 @@ class DropdownMenu extends React.PureComponent {
|
|||
} else {
|
||||
element = items[index+1] || items[0];
|
||||
}
|
||||
if (element) {
|
||||
element.focus();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
element = items[0];
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
break;
|
||||
case 'End':
|
||||
element = items[items.length-1];
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
this.props.onClose();
|
||||
break;
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
handleItemKeyPress = e => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import 'wicg-inert';
|
||||
import createHistory from 'history/createBrowserHistory';
|
||||
import { createBrowserHistory } from 'history';
|
||||
|
||||
export default class ModalRoot extends React.PureComponent {
|
||||
static contextTypes = {
|
||||
|
@ -51,7 +51,7 @@ export default class ModalRoot extends React.PureComponent {
|
|||
componentDidMount () {
|
||||
window.addEventListener('keyup', this.handleKeyUp, false);
|
||||
window.addEventListener('keydown', this.handleKeyDown, false);
|
||||
this.history = this.context.router ? this.context.router.history : createHistory();
|
||||
this.history = this.context.router ? this.context.router.history : createBrowserHistory();
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
|
|
|
@ -4,7 +4,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { vote, fetchPoll } from 'flavours/glitch/actions/polls';
|
||||
import Motion from 'flavours/glitch/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
|
@ -28,8 +27,9 @@ class Poll extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
poll: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
refresh: PropTypes.func,
|
||||
onVote: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -100,7 +100,7 @@ class Poll extends ImmutablePureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected)));
|
||||
this.props.onVote(Object.keys(this.state.selected));
|
||||
};
|
||||
|
||||
handleRefresh = () => {
|
||||
|
@ -108,7 +108,7 @@ class Poll extends ImmutablePureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
this.props.dispatch(fetchPoll(this.props.poll.get('id')));
|
||||
this.props.refresh();
|
||||
};
|
||||
|
||||
renderOption (option, optionIndex, showResults) {
|
||||
|
|
|
@ -372,8 +372,8 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
handleOpenVideo = (media, startTime) => {
|
||||
this.props.onOpenVideo(media, startTime);
|
||||
handleOpenVideo = (media, options) => {
|
||||
this.props.onOpenVideo(media, options);
|
||||
}
|
||||
|
||||
handleHotkeyOpenMedia = e => {
|
||||
|
@ -385,7 +385,7 @@ class Status extends ImmutablePureComponent {
|
|||
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||
// TODO: toggle play/paused?
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
onOpenVideo(status.getIn(['media_attachments', 0]), 0);
|
||||
onOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
|
||||
} else {
|
||||
onOpenMedia(status.get('media_attachments'), 0);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,25 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import Poll from 'flavours/glitch/components/poll';
|
||||
import { fetchPoll, vote } from 'flavours/glitch/actions/polls';
|
||||
|
||||
const mapDispatchToProps = (dispatch, { pollId }) => ({
|
||||
refresh: debounce(
|
||||
() => {
|
||||
dispatch(fetchPoll(pollId));
|
||||
},
|
||||
1000,
|
||||
{ leading: true },
|
||||
),
|
||||
|
||||
onVote (choices) {
|
||||
dispatch(vote(pollId, choices));
|
||||
},
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { pollId }) => ({
|
||||
poll: state.getIn(['polls', pollId]),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(Poll);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Poll);
|
||||
|
|
|
@ -178,8 +178,8 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
|||
dispatch(openModal('MEDIA', { media, index }));
|
||||
},
|
||||
|
||||
onOpenVideo (media, time) {
|
||||
dispatch(openModal('VIDEO', { media, time }));
|
||||
onOpenVideo (media, options) {
|
||||
dispatch(openModal('VIDEO', { media, options }));
|
||||
},
|
||||
|
||||
onBlock (status) {
|
||||
|
|
|
@ -38,7 +38,7 @@ export default class TimelineContainer extends React.PureComponent {
|
|||
let timeline;
|
||||
|
||||
if (hashtag) {
|
||||
timeline = <HashtagTimeline hashtag={hashtag} />;
|
||||
timeline = <HashtagTimeline hashtag={hashtag} local={local} />;
|
||||
} else {
|
||||
timeline = <PublicTimeline local={local} />;
|
||||
}
|
||||
|
|
|
@ -186,10 +186,12 @@ class Header extends ImmutablePureComponent {
|
|||
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
|
||||
} else {
|
||||
if (account.getIn(['relationship', 'following'])) {
|
||||
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
|
||||
if (!account.getIn(['relationship', 'muting'])) {
|
||||
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
|
||||
}
|
||||
}
|
||||
|
||||
menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
|
||||
|
|
|
@ -19,6 +19,7 @@ const messages = defineMessages({
|
|||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'blocks', 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'blocks', 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'blocks', 'isLoading'], true),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
|
@ -30,6 +31,7 @@ class Blocks extends ImmutablePureComponent {
|
|||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
@ -43,7 +45,7 @@ class Blocks extends ImmutablePureComponent {
|
|||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, accountIds, hasMore, multiColumn } = this.props;
|
||||
const { intl, accountIds, hasMore, multiColumn, isLoading } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
|
@ -62,6 +64,7 @@ class Blocks extends ImmutablePureComponent {
|
|||
scrollKey='blocks'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
|
|
|
@ -64,9 +64,9 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
|
|||
document.addEventListener('click', this.handleDocumentClick, false);
|
||||
document.addEventListener('touchend', this.handleDocumentClick, withPassive);
|
||||
if (this.focusedItem) {
|
||||
this.focusedItem.focus();
|
||||
this.focusedItem.focus({ preventScroll: true });
|
||||
} else {
|
||||
this.node.firstChild.focus();
|
||||
this.node.firstChild.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ mounted: true });
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
|
|||
const index = items.findIndex(item => {
|
||||
return (item.name === name);
|
||||
});
|
||||
let element;
|
||||
let element = null;
|
||||
|
||||
switch(e.key) {
|
||||
case 'Escape':
|
||||
|
@ -117,18 +117,10 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
|
|||
this.handleClick(e);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
element = this.node.childNodes[index + 1];
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.handleChange(element.getAttribute('data-index'));
|
||||
}
|
||||
element = this.node.childNodes[index + 1] || this.node.firstChild;
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
element = this.node.childNodes[index - 1];
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.handleChange(element.getAttribute('data-index'));
|
||||
}
|
||||
element = this.node.childNodes[index - 1] || this.node.lastChild;
|
||||
break;
|
||||
case 'Tab':
|
||||
if (e.shiftKey) {
|
||||
|
@ -136,28 +128,21 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
|
|||
} else {
|
||||
element = this.node.childNodes[index + 1] || this.node.firstChild;
|
||||
}
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.handleChange(element.getAttribute('data-index'));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
element = this.node.firstChild;
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.handleChange(element.getAttribute('data-index'));
|
||||
}
|
||||
break;
|
||||
case 'End':
|
||||
element = this.node.lastChild;
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.handleChange(element.getAttribute('data-index'));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.handleChange(element.getAttribute('data-index'));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
setFocusRef = c => {
|
||||
|
|
|
@ -28,6 +28,7 @@ class Option extends React.PureComponent {
|
|||
title: PropTypes.string.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
isPollMultiple: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
|
@ -58,7 +59,7 @@ class Option extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { isPollMultiple, title, index, intl } = this.props;
|
||||
const { isPollMultiple, title, index, autoFocus, intl } = this.props;
|
||||
|
||||
return (
|
||||
<li>
|
||||
|
@ -75,6 +76,7 @@ class Option extends React.PureComponent {
|
|||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
</label>
|
||||
|
||||
|
@ -125,10 +127,12 @@ class PollForm extends ImmutablePureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
const autoFocusIndex = options.indexOf('');
|
||||
|
||||
return (
|
||||
<div className='compose-form__poll-wrapper'>
|
||||
<ul>
|
||||
{options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} {...other} />)}
|
||||
{options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} autoFocus={i === autoFocusIndex} {...other} />)}
|
||||
{options.size < pollLimits.max_options && (
|
||||
<label className='poll__text editable'>
|
||||
<span className={classNames('poll__input')} style={{ opacity: 0 }} />
|
||||
|
|
|
@ -19,6 +19,7 @@ const messages = defineMessages({
|
|||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'follow_requests', 'items']),
|
||||
isLoading: state.getIn(['user_lists', 'follow_requests', 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']),
|
||||
locked: !!state.getIn(['accounts', me, 'locked']),
|
||||
domain: state.getIn(['meta', 'domain']),
|
||||
|
@ -32,6 +33,7 @@ class FollowRequests extends ImmutablePureComponent {
|
|||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
locked: PropTypes.bool,
|
||||
domain: PropTypes.string,
|
||||
|
@ -48,7 +50,7 @@ class FollowRequests extends ImmutablePureComponent {
|
|||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, accountIds, hasMore, multiColumn, locked, domain } = this.props;
|
||||
const { intl, accountIds, hasMore, multiColumn, locked, domain, isLoading } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
|
@ -77,6 +79,7 @@ class FollowRequests extends ImmutablePureComponent {
|
|||
scrollKey='follow_requests'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
prepend={unlockedPrependMessage}
|
||||
|
|
|
@ -22,6 +22,7 @@ const mapStateToProps = (state, props) => ({
|
|||
isAccount: !!state.getIn(['accounts', props.params.accountId]),
|
||||
accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'followers', props.params.accountId, 'isLoading'], true),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
|
@ -32,6 +33,7 @@ class Followers extends ImmutablePureComponent {
|
|||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
@ -54,14 +56,6 @@ class Followers extends ImmutablePureComponent {
|
|||
this.column.scrollTop();
|
||||
}
|
||||
|
||||
handleScroll = (e) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
|
||||
if (scrollTop === scrollHeight - clientHeight && this.props.hasMore) {
|
||||
this.props.dispatch(expandFollowers(this.props.params.accountId));
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandFollowers(this.props.params.accountId));
|
||||
}, 300, { leading: true });
|
||||
|
@ -71,7 +65,7 @@ class Followers extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { accountIds, hasMore, isAccount, multiColumn } = this.props;
|
||||
const { accountIds, hasMore, isAccount, multiColumn, isLoading } = this.props;
|
||||
|
||||
if (!isAccount) {
|
||||
return (
|
||||
|
@ -98,6 +92,7 @@ class Followers extends ImmutablePureComponent {
|
|||
<ScrollableList
|
||||
scrollKey='followers'
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
|
||||
alwaysPrepend
|
||||
|
|
|
@ -22,6 +22,7 @@ const mapStateToProps = (state, props) => ({
|
|||
isAccount: !!state.getIn(['accounts', props.params.accountId]),
|
||||
accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'following', props.params.accountId, 'isLoading'], true),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
|
@ -32,6 +33,7 @@ class Following extends ImmutablePureComponent {
|
|||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
@ -54,14 +56,6 @@ class Following extends ImmutablePureComponent {
|
|||
this.column.scrollTop();
|
||||
}
|
||||
|
||||
handleScroll = (e) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
|
||||
if (scrollTop === scrollHeight - clientHeight && this.props.hasMore) {
|
||||
this.props.dispatch(expandFollowing(this.props.params.accountId));
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandFollowing(this.props.params.accountId));
|
||||
}, 300, { leading: true });
|
||||
|
@ -71,7 +65,7 @@ class Following extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { accountIds, hasMore, isAccount, multiColumn } = this.props;
|
||||
const { accountIds, hasMore, isAccount, multiColumn, isLoading } = this.props;
|
||||
|
||||
if (!isAccount) {
|
||||
return (
|
||||
|
@ -98,6 +92,7 @@ class Following extends ImmutablePureComponent {
|
|||
<ScrollableList
|
||||
scrollKey='following'
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
|
||||
alwaysPrepend
|
||||
|
|
|
@ -4,6 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import Toggle from 'react-toggle';
|
||||
import AsyncSelect from 'react-select/async';
|
||||
import SettingToggle from '../../notifications/components/setting_toggle';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' },
|
||||
|
@ -87,6 +88,8 @@ class ColumnSettings extends React.PureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { settings, onChange } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='column-settings__row'>
|
||||
|
@ -106,6 +109,10 @@ class ColumnSettings extends React.PureComponent {
|
|||
{this.modeSelect('none')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle settings={settings} settingPath={['local']} onChange={onChange} label={<FormattedMessage id='community.column_settings.local_only' defaultMessage='Local only' />} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -97,21 +97,21 @@ class HashtagTimeline extends React.PureComponent {
|
|||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
const { id, tags } = this.props.params;
|
||||
const { id, tags, local } = this.props.params;
|
||||
|
||||
this._subscribe(dispatch, id, tags);
|
||||
dispatch(expandHashtagTimeline(id, { tags }));
|
||||
dispatch(expandHashtagTimeline(id, { tags, local }));
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
const { dispatch, params } = this.props;
|
||||
const { id, tags } = nextProps.params;
|
||||
const { id, tags, local } = nextProps.params;
|
||||
|
||||
if (id !== params.id || !isEqual(tags, params.tags)) {
|
||||
if (id !== params.id || !isEqual(tags, params.tags) || !isEqual(local, params.local)) {
|
||||
this._unsubscribe();
|
||||
this._subscribe(dispatch, id, tags);
|
||||
this.props.dispatch(clearTimeline(`hashtag:${id}`));
|
||||
this.props.dispatch(expandHashtagTimeline(id, { tags }));
|
||||
dispatch(clearTimeline(`hashtag:${id}`));
|
||||
dispatch(expandHashtagTimeline(id, { tags, local }));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,8 +124,8 @@ class HashtagTimeline extends React.PureComponent {
|
|||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
const { id, tags } = this.props.params;
|
||||
this.props.dispatch(expandHashtagTimeline(id, { maxId, tags }));
|
||||
const { id, tags, local } = this.props.params;
|
||||
this.props.dispatch(expandHashtagTimeline(id, { maxId, tags, local }));
|
||||
}
|
||||
|
||||
render () {
|
||||
|
|
|
@ -19,6 +19,7 @@ const messages = defineMessages({
|
|||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'mutes', 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'mutes', 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'mutes', 'isLoading'], true),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
|
@ -29,6 +30,7 @@ class Mutes extends ImmutablePureComponent {
|
|||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired,
|
||||
multiColumn: PropTypes.bool,
|
||||
|
@ -43,7 +45,7 @@ class Mutes extends ImmutablePureComponent {
|
|||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, accountIds, hasMore, multiColumn } = this.props;
|
||||
const { intl, accountIds, hasMore, multiColumn, isLoading } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
|
@ -62,6 +64,7 @@ class Mutes extends ImmutablePureComponent {
|
|||
scrollKey='mutes'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import SettingToggle from '../../notifications/components/setting_toggle';
|
||||
|
||||
export default @injectIntl
|
||||
class ColumnSettings extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { settings, onChange } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
||||
<SettingToggle settings={settings} settingPath={['other', 'onlyRemote']} onChange={onChange} label={<FormattedMessage id='community.column_settings.remote_only' defaultMessage='Remote only' />} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ColumnSettings from 'flavours/glitch/features/community_timeline/components/column_settings';
|
||||
import ColumnSettings from '../components/column_settings';
|
||||
import { changeSetting } from 'flavours/glitch/actions/settings';
|
||||
import { changeColumnParams } from 'flavours/glitch/actions/columns';
|
||||
|
||||
|
|
|
@ -19,11 +19,13 @@ const mapStateToProps = (state, { columnId }) => {
|
|||
const columns = state.getIn(['settings', 'columns']);
|
||||
const index = columns.findIndex(c => c.get('uuid') === uuid);
|
||||
const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']);
|
||||
const onlyRemote = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyRemote']) : state.getIn(['settings', 'public', 'other', 'onlyRemote']);
|
||||
const timelineState = state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`]);
|
||||
|
||||
return {
|
||||
hasUnread: !!timelineState && timelineState.get('unread') > 0,
|
||||
onlyMedia,
|
||||
onlyRemote,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -46,15 +48,16 @@ class PublicTimeline extends React.PureComponent {
|
|||
multiColumn: PropTypes.bool,
|
||||
hasUnread: PropTypes.bool,
|
||||
onlyMedia: PropTypes.bool,
|
||||
onlyRemote: PropTypes.bool,
|
||||
};
|
||||
|
||||
handlePin = () => {
|
||||
const { columnId, dispatch, onlyMedia } = this.props;
|
||||
const { columnId, dispatch, onlyMedia, onlyRemote } = this.props;
|
||||
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
dispatch(addColumn('PUBLIC', { other: { onlyMedia } }));
|
||||
dispatch(addColumn(onlyRemote ? 'REMOTE' : 'PUBLIC', { other: { onlyMedia, onlyRemote } }));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,19 +71,19 @@ class PublicTimeline extends React.PureComponent {
|
|||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
const { dispatch, onlyMedia, onlyRemote } = this.props;
|
||||
|
||||
dispatch(expandPublicTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
|
||||
dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.onlyMedia !== this.props.onlyMedia) {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote) {
|
||||
const { dispatch, onlyMedia, onlyRemote } = this.props;
|
||||
|
||||
this.disconnect();
|
||||
dispatch(expandPublicTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
|
||||
dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,13 +99,13 @@ class PublicTimeline extends React.PureComponent {
|
|||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
const { dispatch, onlyMedia, onlyRemote } = this.props;
|
||||
|
||||
dispatch(expandPublicTimeline({ maxId, onlyMedia }));
|
||||
dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote }));
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, columnId, hasUnread, multiColumn, onlyMedia } = this.props;
|
||||
const { intl, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote } = this.props;
|
||||
const pinned = !!columnId;
|
||||
|
||||
return (
|
||||
|
@ -121,7 +124,7 @@ class PublicTimeline extends React.PureComponent {
|
|||
</ColumnHeader>
|
||||
|
||||
<StatusListContainer
|
||||
timelineId={`public${onlyMedia ? ':media' : ''}`}
|
||||
timelineId={`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
trackScroll={!pinned}
|
||||
scrollKey={`public_timeline-${columnId}`}
|
||||
|
|
|
@ -24,19 +24,25 @@ class HashtagTimeline extends React.PureComponent {
|
|||
isLoading: PropTypes.bool.isRequired,
|
||||
hasMore: PropTypes.bool.isRequired,
|
||||
hashtag: PropTypes.string.isRequired,
|
||||
local: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
local: false,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, hashtag } = this.props;
|
||||
const { dispatch, hashtag, local } = this.props;
|
||||
|
||||
dispatch(expandHashtagTimeline(hashtag));
|
||||
dispatch(expandHashtagTimeline(hashtag, { local }));
|
||||
}
|
||||
|
||||
handleLoadMore = () => {
|
||||
const maxId = this.props.statusIds.last();
|
||||
const { dispatch, hashtag, local, statusIds } = this.props;
|
||||
const maxId = statusIds.last();
|
||||
|
||||
if (maxId) {
|
||||
this.props.dispatch(expandHashtagTimeline(this.props.hashtag, { maxId }));
|
||||
dispatch(expandHashtagTimeline(hashtag, { maxId, local }));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -66,8 +66,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
e.stopPropagation();
|
||||
}
|
||||
|
||||
handleOpenVideo = (media, startTime) => {
|
||||
this.props.onOpenVideo(media, startTime);
|
||||
handleOpenVideo = (media, options) => {
|
||||
this.props.onOpenVideo(media, options);
|
||||
}
|
||||
|
||||
_measureHeight (heightJustChanged) {
|
||||
|
|
|
@ -130,8 +130,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
dispatch(openModal('MEDIA', { media, index }));
|
||||
},
|
||||
|
||||
onOpenVideo (media, time) {
|
||||
dispatch(openModal('VIDEO', { media, time }));
|
||||
onOpenVideo (media, options) {
|
||||
dispatch(openModal('VIDEO', { media, options }));
|
||||
},
|
||||
|
||||
onBlock (status) {
|
||||
|
|
|
@ -316,8 +316,8 @@ class Status extends ImmutablePureComponent {
|
|||
this.props.dispatch(openModal('MEDIA', { media, index }));
|
||||
}
|
||||
|
||||
handleOpenVideo = (media, time) => {
|
||||
this.props.dispatch(openModal('VIDEO', { media, time }));
|
||||
handleOpenVideo = (media, options) => {
|
||||
this.props.dispatch(openModal('VIDEO', { media, options }));
|
||||
}
|
||||
|
||||
handleHotkeyOpenMedia = e => {
|
||||
|
@ -329,7 +329,7 @@ class Status extends ImmutablePureComponent {
|
|||
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||
// TODO: toggle play/paused?
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
this.handleOpenVideo(status.getIn(['media_attachments', 0]), 0);
|
||||
this.handleOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
|
||||
} else {
|
||||
this.handleOpenMedia(status.get('media_attachments'), 0);
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ const componentMap = {
|
|||
'HOME': HomeTimeline,
|
||||
'NOTIFICATIONS': Notifications,
|
||||
'PUBLIC': PublicTimeline,
|
||||
'REMOTE': PublicTimeline,
|
||||
'COMMUNITY': CommunityTimeline,
|
||||
'HASHTAG': HashtagTimeline,
|
||||
'DIRECT': DirectTimeline,
|
||||
|
|
|
@ -16,7 +16,11 @@ export default class VideoModal extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
status: ImmutablePropTypes.map,
|
||||
time: PropTypes.number,
|
||||
options: PropTypes.shape({
|
||||
startTime: PropTypes.number,
|
||||
autoPlay: PropTypes.bool,
|
||||
defaultVolume: PropTypes.number,
|
||||
}),
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
@ -28,7 +32,8 @@ export default class VideoModal extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { media, status, time, onClose } = this.props;
|
||||
const { media, status, onClose } = this.props;
|
||||
const options = this.props.options || {};
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal video-modal'>
|
||||
|
@ -37,7 +42,9 @@ export default class VideoModal extends ImmutablePureComponent {
|
|||
preview={media.get('preview_url')}
|
||||
blurhash={media.get('blurhash')}
|
||||
src={media.get('url')}
|
||||
startTime={time}
|
||||
startTime={options.startTime}
|
||||
autoPlay={options.autoPlay}
|
||||
defaultVolume={options.defaultVolume}
|
||||
onCloseVideo={onClose}
|
||||
detailed
|
||||
alt={media.get('description')}
|
||||
|
|
|
@ -111,6 +111,8 @@ class Video extends React.PureComponent {
|
|||
preventPlayback: PropTypes.bool,
|
||||
blurhash: PropTypes.string,
|
||||
link: PropTypes.node,
|
||||
autoPlay: PropTypes.bool,
|
||||
defaultVolume: PropTypes.number,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -360,6 +362,13 @@ class Video extends React.PureComponent {
|
|||
handleLoadedData = () => {
|
||||
if (this.props.startTime) {
|
||||
this.video.currentTime = this.props.startTime;
|
||||
}
|
||||
|
||||
if (this.props.defaultVolume !== undefined) {
|
||||
this.video.volume = this.props.defaultVolume;
|
||||
}
|
||||
|
||||
if (this.props.autoPlay) {
|
||||
this.video.play();
|
||||
}
|
||||
}
|
||||
|
@ -386,8 +395,14 @@ class Video extends React.PureComponent {
|
|||
height,
|
||||
});
|
||||
|
||||
const options = {
|
||||
startTime: this.video.currentTime,
|
||||
autoPlay: !this.state.paused,
|
||||
defaultVolume: this.state.volume,
|
||||
};
|
||||
|
||||
this.video.pause();
|
||||
this.props.onOpenVideo(media, this.video.currentTime);
|
||||
this.props.onOpenVideo(media, options);
|
||||
}
|
||||
|
||||
handleCloseVideo = () => {
|
||||
|
|
|
@ -99,15 +99,13 @@ function main() {
|
|||
delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
|
||||
|
||||
delegate(document, '.status__content__spoiler-link', 'click', function() {
|
||||
const contentEl = this.parentNode.parentNode.querySelector('.e-content');
|
||||
const statusEl = this.parentNode.parentNode;
|
||||
|
||||
if (contentEl.style.display === 'block') {
|
||||
contentEl.style.display = 'none';
|
||||
this.parentNode.style.marginBottom = 0;
|
||||
if (statusEl.dataset.spoiler === 'expanded') {
|
||||
statusEl.dataset.spoiler = 'folded';
|
||||
this.textContent = (new IntlMessageFormat(messages['status.show_more'] || 'Show more', locale)).format();
|
||||
} else {
|
||||
contentEl.style.display = 'block';
|
||||
this.parentNode.style.marginBottom = null;
|
||||
statusEl.dataset.spoiler = 'expanded';
|
||||
this.textContent = (new IntlMessageFormat(messages['status.show_less'] || 'Show less', locale)).format();
|
||||
}
|
||||
|
||||
|
@ -115,8 +113,8 @@ function main() {
|
|||
});
|
||||
|
||||
[].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => {
|
||||
const contentEl = spoilerLink.parentNode.parentNode.querySelector('.e-content');
|
||||
const message = (contentEl.style.display === 'block') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
|
||||
const statusEl = spoilerLink.parentNode.parentNode;
|
||||
const message = (statusEl.dataset.spoiler === 'expanded') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
|
||||
spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,7 +24,7 @@ const importStatuses = (state, statuses) =>
|
|||
|
||||
const deleteStatus = (state, id, references) => {
|
||||
references.forEach(ref => {
|
||||
state = deleteStatus(state, ref[0], []);
|
||||
state = deleteStatus(state, ref, []);
|
||||
});
|
||||
|
||||
return state.delete(id);
|
||||
|
|
|
@ -94,7 +94,7 @@ const updateTimeline = (state, timeline, status, usePendingItems, filtered) => {
|
|||
}));
|
||||
};
|
||||
|
||||
const deleteStatus = (state, id, accountId, references, exclude_account = null) => {
|
||||
const deleteStatus = (state, id, references, exclude_account = null) => {
|
||||
state.keySeq().forEach(timeline => {
|
||||
if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) {
|
||||
const helper = list => list.filterNot(item => item === id);
|
||||
|
@ -104,7 +104,7 @@ const deleteStatus = (state, id, accountId, references, exclude_account = null)
|
|||
|
||||
// Remove reblogs of deleted status
|
||||
references.forEach(ref => {
|
||||
state = deleteStatus(state, ref[0], ref[1], [], exclude_account);
|
||||
state = deleteStatus(state, ref, [], exclude_account);
|
||||
});
|
||||
|
||||
return state;
|
||||
|
@ -122,8 +122,8 @@ const filterTimelines = (state, relationship, statuses) => {
|
|||
return;
|
||||
}
|
||||
|
||||
references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => [item.get('id'), item.get('account')]);
|
||||
state = deleteStatus(state, status.get('id'), status.get('account'), references, relationship.id);
|
||||
references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => item.get('id'));
|
||||
state = deleteStatus(state, status.get('id'), references, relationship.id);
|
||||
});
|
||||
|
||||
return state;
|
||||
|
@ -155,7 +155,7 @@ export default function timelines(state = initialState, action) {
|
|||
case TIMELINE_UPDATE:
|
||||
return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems, action.filtered);
|
||||
case TIMELINE_DELETE:
|
||||
return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
|
||||
return deleteStatus(state, action.id, action.references, action.reblogOf);
|
||||
case TIMELINE_CLEAR:
|
||||
return clearTimeline(state, action.timeline);
|
||||
case ACCOUNT_BLOCK_SUCCESS:
|
||||
|
|
|
@ -2,12 +2,24 @@ import {
|
|||
NOTIFICATIONS_UPDATE,
|
||||
} from '../actions/notifications';
|
||||
import {
|
||||
FOLLOWERS_FETCH_REQUEST,
|
||||
FOLLOWERS_FETCH_SUCCESS,
|
||||
FOLLOWERS_FETCH_FAIL,
|
||||
FOLLOWERS_EXPAND_REQUEST,
|
||||
FOLLOWERS_EXPAND_SUCCESS,
|
||||
FOLLOWERS_EXPAND_FAIL,
|
||||
FOLLOWING_FETCH_REQUEST,
|
||||
FOLLOWING_FETCH_SUCCESS,
|
||||
FOLLOWING_FETCH_FAIL,
|
||||
FOLLOWING_EXPAND_REQUEST,
|
||||
FOLLOWING_EXPAND_SUCCESS,
|
||||
FOLLOWING_EXPAND_FAIL,
|
||||
FOLLOW_REQUESTS_FETCH_REQUEST,
|
||||
FOLLOW_REQUESTS_FETCH_SUCCESS,
|
||||
FOLLOW_REQUESTS_FETCH_FAIL,
|
||||
FOLLOW_REQUESTS_EXPAND_REQUEST,
|
||||
FOLLOW_REQUESTS_EXPAND_SUCCESS,
|
||||
FOLLOW_REQUESTS_EXPAND_FAIL,
|
||||
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
|
||||
FOLLOW_REQUEST_REJECT_SUCCESS,
|
||||
} from 'flavours/glitch/actions/accounts';
|
||||
|
@ -16,12 +28,20 @@ import {
|
|||
FAVOURITES_FETCH_SUCCESS,
|
||||
} from 'flavours/glitch/actions/interactions';
|
||||
import {
|
||||
BLOCKS_FETCH_REQUEST,
|
||||
BLOCKS_FETCH_SUCCESS,
|
||||
BLOCKS_FETCH_FAIL,
|
||||
BLOCKS_EXPAND_REQUEST,
|
||||
BLOCKS_EXPAND_SUCCESS,
|
||||
BLOCKS_EXPAND_FAIL,
|
||||
} from 'flavours/glitch/actions/blocks';
|
||||
import {
|
||||
MUTES_FETCH_REQUEST,
|
||||
MUTES_FETCH_SUCCESS,
|
||||
MUTES_FETCH_FAIL,
|
||||
MUTES_EXPAND_REQUEST,
|
||||
MUTES_EXPAND_SUCCESS,
|
||||
MUTES_EXPAND_FAIL,
|
||||
} from 'flavours/glitch/actions/mutes';
|
||||
import {
|
||||
DIRECTORY_FETCH_REQUEST,
|
||||
|
@ -47,12 +67,13 @@ const normalizeList = (state, type, id, accounts, next) => {
|
|||
return state.setIn([type, id], ImmutableMap({
|
||||
next,
|
||||
items: ImmutableList(accounts.map(item => item.id)),
|
||||
isLoading: false,
|
||||
}));
|
||||
};
|
||||
|
||||
const appendToList = (state, type, id, accounts, next) => {
|
||||
return state.updateIn([type, id], map => {
|
||||
return map.set('next', next).update('items', list => list.concat(accounts.map(item => item.id)));
|
||||
return map.set('next', next).set('isLoading', false).update('items', list => list.concat(accounts.map(item => item.id)));
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -68,10 +89,22 @@ export default function userLists(state = initialState, action) {
|
|||
return normalizeList(state, 'followers', action.id, action.accounts, action.next);
|
||||
case FOLLOWERS_EXPAND_SUCCESS:
|
||||
return appendToList(state, 'followers', action.id, action.accounts, action.next);
|
||||
case FOLLOWERS_FETCH_REQUEST:
|
||||
case FOLLOWERS_EXPAND_REQUEST:
|
||||
return state.setIn(['followers', action.id, 'isLoading'], true);
|
||||
case FOLLOWERS_FETCH_FAIL:
|
||||
case FOLLOWERS_EXPAND_FAIL:
|
||||
return state.setIn(['followers', action.id, 'isLoading'], false);
|
||||
case FOLLOWING_FETCH_SUCCESS:
|
||||
return normalizeList(state, 'following', action.id, action.accounts, action.next);
|
||||
case FOLLOWING_EXPAND_SUCCESS:
|
||||
return appendToList(state, 'following', action.id, action.accounts, action.next);
|
||||
case FOLLOWING_FETCH_REQUEST:
|
||||
case FOLLOWING_EXPAND_REQUEST:
|
||||
return state.setIn(['following', action.id, 'isLoading'], true);
|
||||
case FOLLOWING_FETCH_FAIL:
|
||||
case FOLLOWING_EXPAND_FAIL:
|
||||
return state.setIn(['following', action.id, 'isLoading'], false);
|
||||
case REBLOGS_FETCH_SUCCESS:
|
||||
return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
|
||||
case FAVOURITES_FETCH_SUCCESS:
|
||||
|
@ -79,9 +112,15 @@ export default function userLists(state = initialState, action) {
|
|||
case NOTIFICATIONS_UPDATE:
|
||||
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
|
||||
case FOLLOW_REQUESTS_FETCH_SUCCESS:
|
||||
return state.setIn(['follow_requests', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
|
||||
return state.setIn(['follow_requests', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next).setIn(['follow_requests', 'isLoading'], false);
|
||||
case FOLLOW_REQUESTS_EXPAND_SUCCESS:
|
||||
return state.updateIn(['follow_requests', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
|
||||
return state.updateIn(['follow_requests', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next).setIn(['follow_requests', 'isLoading'], false);
|
||||
case FOLLOW_REQUESTS_FETCH_REQUEST:
|
||||
case FOLLOW_REQUESTS_EXPAND_REQUEST:
|
||||
return state.setIn(['follow_requests', 'isLoading'], true);
|
||||
case FOLLOW_REQUESTS_FETCH_FAIL:
|
||||
case FOLLOW_REQUESTS_EXPAND_FAIL:
|
||||
return state.setIn(['follow_requests', 'isLoading'], false);
|
||||
case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
|
||||
case FOLLOW_REQUEST_REJECT_SUCCESS:
|
||||
return state.updateIn(['follow_requests', 'items'], list => list.filterNot(item => item === action.id));
|
||||
|
@ -89,10 +128,22 @@ export default function userLists(state = initialState, action) {
|
|||
return state.setIn(['blocks', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
|
||||
case BLOCKS_EXPAND_SUCCESS:
|
||||
return state.updateIn(['blocks', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
|
||||
case BLOCKS_FETCH_REQUEST:
|
||||
case BLOCKS_EXPAND_REQUEST:
|
||||
return state.setIn(['blocks', 'isLoading'], true);
|
||||
case BLOCKS_FETCH_FAIL:
|
||||
case BLOCKS_EXPAND_FAIL:
|
||||
return state.setIn(['blocks', 'isLoading'], false);
|
||||
case MUTES_FETCH_SUCCESS:
|
||||
return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
||||
case MUTES_EXPAND_SUCCESS:
|
||||
return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
||||
case MUTES_FETCH_REQUEST:
|
||||
case MUTES_EXPAND_REQUEST:
|
||||
return state.setIn(['mutes', 'isLoading'], true);
|
||||
case MUTES_FETCH_FAIL:
|
||||
case MUTES_EXPAND_FAIL:
|
||||
return state.setIn(['mutes', 'isLoading'], false);
|
||||
case DIRECTORY_FETCH_SUCCESS:
|
||||
return state.setIn(['directory', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['directory', 'isLoading'], false);
|
||||
case DIRECTORY_EXPAND_SUCCESS:
|
||||
|
|
|
@ -545,13 +545,6 @@ $small-breakpoint: 960px;
|
|||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background-size: 44px 44px;
|
||||
@include avatar-size(44px);
|
||||
}
|
||||
|
||||
.display-name {
|
||||
font-size: 15px;
|
||||
|
||||
|
@ -752,16 +745,15 @@ $small-breakpoint: 960px;
|
|||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.account__avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background-size: 44px 44px;
|
||||
}
|
||||
&__counters__wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__counter {
|
||||
padding: 10px;
|
||||
width: 50%;
|
||||
|
||||
strong {
|
||||
font-family: $font-display, sans-serif;
|
||||
|
|
|
@ -567,6 +567,18 @@ body,
|
|||
}
|
||||
}
|
||||
|
||||
.special-action-button,
|
||||
.back-link {
|
||||
text-align: right;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
@ -904,3 +916,11 @@ a.name-tag,
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account-badges {
|
||||
margin: -2px 0;
|
||||
}
|
||||
|
||||
.dashboard__counters.admin-account-counters {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
|
|
@ -150,3 +150,19 @@ button {
|
|||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-resources {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// NoScript adds a __ns__pop2top class to the full ancestry of blocked elements,
|
||||
// to set the z-index to a high value, which messes with modals and dropdowns.
|
||||
// Blocked elements can in theory only be media and frames/embeds, so they
|
||||
// should only appear in statuses, under divs and articles.
|
||||
body,
|
||||
div,
|
||||
article {
|
||||
.__ns__pop2top {
|
||||
z-index: unset !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,9 +38,14 @@
|
|||
|
||||
.account__avatar {
|
||||
@include avatar-radius();
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background-size: 36px 36px;
|
||||
|
||||
&-inline {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
@ -80,6 +85,12 @@
|
|||
&-base {
|
||||
@include avatar-radius();
|
||||
@include avatar-size(36px);
|
||||
|
||||
img {
|
||||
@include avatar-radius;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-overlay {
|
||||
|
@ -90,6 +101,12 @@
|
|||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
|
||||
img {
|
||||
@include avatar-radius;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -691,3 +691,10 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gifv {
|
||||
video {
|
||||
max-width: 100vw;
|
||||
max-height: 80vh;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -185,6 +185,7 @@
|
|||
|
||||
path:last-child {
|
||||
stroke: lighten($highlight-text-color, 6%) !important;
|
||||
fill: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -133,6 +133,10 @@ code {
|
|||
}
|
||||
}
|
||||
|
||||
.otp-hint {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
@ -276,6 +280,14 @@ code {
|
|||
margin-bottom: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.fields-group.invited-by {
|
||||
margin-bottom: 30px;
|
||||
|
||||
.hint {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input.radio_buttons .radio label {
|
||||
|
@ -626,6 +638,15 @@ code {
|
|||
@media screen and (max-width: 740px) and (min-width: 441px) {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
&.translation-prompt {
|
||||
text-align: unset;
|
||||
color: unset;
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-footer {
|
||||
|
|
|
@ -25,6 +25,36 @@
|
|||
}
|
||||
}
|
||||
|
||||
progress {
|
||||
border: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
|
||||
&::-webkit-progress-bar {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
// Those rules need to be entirely separate or they won't work, hence the
|
||||
// duplication
|
||||
&::-moz-progress-bar {
|
||||
border-radius: 4px;
|
||||
background: darken($ui-primary-color, 5%);
|
||||
}
|
||||
|
||||
&::-ms-fill {
|
||||
border-radius: 4px;
|
||||
background: darken($ui-primary-color, 5%);
|
||||
}
|
||||
|
||||
&::-webkit-progress-value {
|
||||
border-radius: 4px;
|
||||
background: darken($ui-primary-color, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
&__option {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
|
|
@ -124,6 +124,16 @@
|
|||
|
||||
.embed,
|
||||
.public-layout {
|
||||
.status__content[data-spoiler=folded] {
|
||||
.e-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
p:first-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.detailed-status {
|
||||
padding: 15px;
|
||||
}
|
||||
|
@ -135,6 +145,11 @@
|
|||
&__avatar {
|
||||
left: 15px;
|
||||
top: 17px;
|
||||
|
||||
.account__avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
|
@ -162,6 +177,13 @@
|
|||
.video-player {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&__action-bar-button {
|
||||
font-size: 18px;
|
||||
width: 23.1429px;
|
||||
height: 23.1429px;
|
||||
line-height: 23.15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -93,12 +93,6 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.account__avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background-size: 44px 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.trends__item {
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -2,6 +2,45 @@ import EXIF from 'exif-js';
|
|||
|
||||
const MAX_IMAGE_PIXELS = 1638400; // 1280x1280px
|
||||
|
||||
const _browser_quirks = {};
|
||||
|
||||
// Some browsers will automatically draw images respecting their EXIF orientation
|
||||
// while others won't, and the safest way to detect that is to examine how it
|
||||
// is done on a known image.
|
||||
// See https://github.com/w3c/csswg-drafts/issues/4666
|
||||
// and https://github.com/blueimp/JavaScript-Load-Image/commit/1e4df707821a0afcc11ea0720ee403b8759f3881
|
||||
const dropOrientationIfNeeded = (orientation) => new Promise(resolve => {
|
||||
switch (_browser_quirks['image-orientation-automatic']) {
|
||||
case true:
|
||||
resolve(1);
|
||||
break;
|
||||
case false:
|
||||
resolve(orientation);
|
||||
break;
|
||||
default:
|
||||
// black 2x1 JPEG, with the following meta information set:
|
||||
// - EXIF Orientation: 6 (Rotated 90° CCW)
|
||||
const testImageURL =
|
||||
'data:image/jpeg;base64,/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAA' +
|
||||
'AAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA' +
|
||||
'QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE' +
|
||||
'BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAEAAgMBEQACEQEDEQH/x' +
|
||||
'ABKAAEAAAAAAAAAAAAAAAAAAAALEAEAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAA' +
|
||||
'AAAAAEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8H//2Q==';
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const automatic = (img.width === 1 && img.height === 2);
|
||||
_browser_quirks['image-orientation-automatic'] = automatic;
|
||||
resolve(automatic ? 1 : orientation);
|
||||
};
|
||||
img.onerror = () => {
|
||||
_browser_quirks['image-orientation-automatic'] = false;
|
||||
resolve(orientation);
|
||||
};
|
||||
img.src = testImageURL;
|
||||
}
|
||||
});
|
||||
|
||||
const getImageUrl = inputFile => new Promise((resolve, reject) => {
|
||||
if (window.URL && URL.createObjectURL) {
|
||||
try {
|
||||
|
@ -38,7 +77,11 @@ const getOrientation = (img, type = 'image/png') => new Promise(resolve => {
|
|||
|
||||
EXIF.getData(img, () => {
|
||||
const orientation = EXIF.getTag(img, 'Orientation');
|
||||
resolve(orientation);
|
||||
if (orientation !== 1) {
|
||||
dropOrientationIfNeeded(orientation).then(resolve).catch(() => resolve(orientation));
|
||||
} else {
|
||||
resolve(orientation);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -95,7 +138,7 @@ const resizeImage = (img, type = 'image/png') => new Promise((resolve, reject) =
|
|||
.catch(reject);
|
||||
});
|
||||
|
||||
export default inputFile => new Promise((resolve, reject) => {
|
||||
export default inputFile => new Promise((resolve) => {
|
||||
if (!inputFile.type.match(/image.*/) || inputFile.type === 'image/gif') {
|
||||
resolve(inputFile);
|
||||
return;
|
||||
|
@ -110,5 +153,5 @@ export default inputFile => new Promise((resolve, reject) => {
|
|||
resizeImage(img, inputFile.type)
|
||||
.then(resolve)
|
||||
.catch(() => resolve(inputFile));
|
||||
}).catch(reject);
|
||||
}).catch(() => resolve(inputFile));
|
||||
});
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z" fill="#fff"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -73,7 +73,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
|
|||
|
||||
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
|
||||
export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
|
||||
export const connectPublicStream = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`);
|
||||
export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) => connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`);
|
||||
export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
|
||||
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
|
||||
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
|
||||
|
|
|
@ -42,7 +42,7 @@ export function updateTimeline(timeline, status, accept) {
|
|||
export function deleteFromTimelines(id) {
|
||||
return (dispatch, getState) => {
|
||||
const accountId = getState().getIn(['statuses', id, 'account']);
|
||||
const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => [status.get('id'), status.get('account')]);
|
||||
const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id'));
|
||||
const reblogOf = getState().getIn(['statuses', id, 'reblog'], null);
|
||||
|
||||
dispatch({
|
||||
|
@ -107,18 +107,19 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
|
|||
};
|
||||
|
||||
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
|
||||
export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
|
||||
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
|
||||
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
|
||||
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
||||
export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => {
|
||||
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
|
||||
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||
max_id: maxId,
|
||||
any: parseTags(tags, 'any'),
|
||||
all: parseTags(tags, 'all'),
|
||||
none: parseTags(tags, 'none'),
|
||||
local: local,
|
||||
}, done);
|
||||
};
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ class DropdownMenu extends React.PureComponent {
|
|||
document.addEventListener('keydown', this.handleKeyDown, false);
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
if (this.focusedItem && this.props.openedViaKeyboard) {
|
||||
this.focusedItem.focus();
|
||||
this.focusedItem.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ mounted: true });
|
||||
}
|
||||
|
@ -68,20 +68,14 @@ class DropdownMenu extends React.PureComponent {
|
|||
handleKeyDown = e => {
|
||||
const items = Array.from(this.node.getElementsByTagName('a'));
|
||||
const index = items.indexOf(document.activeElement);
|
||||
let element;
|
||||
let element = null;
|
||||
|
||||
switch(e.key) {
|
||||
case 'ArrowDown':
|
||||
element = items[index+1];
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
element = items[index+1] || items[0];
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
element = items[index-1];
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
element = items[index-1] || items[items.length-1];
|
||||
break;
|
||||
case 'Tab':
|
||||
if (e.shiftKey) {
|
||||
|
@ -89,28 +83,23 @@ class DropdownMenu extends React.PureComponent {
|
|||
} else {
|
||||
element = items[index+1] || items[0];
|
||||
}
|
||||
if (element) {
|
||||
element.focus();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
element = items[0];
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
break;
|
||||
case 'End':
|
||||
element = items[items.length-1];
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
this.props.onClose();
|
||||
break;
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
handleItemKeyPress = e => {
|
||||
|
|
|
@ -4,7 +4,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { vote, fetchPoll } from 'mastodon/actions/polls';
|
||||
import Motion from 'mastodon/features/ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
|
@ -28,8 +27,9 @@ class Poll extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
poll: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
refresh: PropTypes.func,
|
||||
onVote: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -100,7 +100,7 @@ class Poll extends ImmutablePureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected)));
|
||||
this.props.onVote(Object.keys(this.state.selected));
|
||||
};
|
||||
|
||||
handleRefresh = () => {
|
||||
|
@ -108,7 +108,7 @@ class Poll extends ImmutablePureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
this.props.dispatch(fetchPoll(this.props.poll.get('id')));
|
||||
this.props.refresh();
|
||||
};
|
||||
|
||||
renderOption (option, optionIndex, showResults) {
|
||||
|
|
|
@ -176,8 +176,8 @@ class Status extends ImmutablePureComponent {
|
|||
return <div className='audio-player' style={{ height: '110px' }} />;
|
||||
}
|
||||
|
||||
handleOpenVideo = (media, startTime) => {
|
||||
this.props.onOpenVideo(media, startTime);
|
||||
handleOpenVideo = (media, options) => {
|
||||
this.props.onOpenVideo(media, options);
|
||||
}
|
||||
|
||||
handleHotkeyOpenMedia = e => {
|
||||
|
@ -190,7 +190,7 @@ class Status extends ImmutablePureComponent {
|
|||
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||
// TODO: toggle play/paused?
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
onOpenVideo(status.getIn(['media_attachments', 0]), 0);
|
||||
onOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
|
||||
} else {
|
||||
onOpenMedia(status.get('media_attachments'), 0);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,25 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import Poll from 'mastodon/components/poll';
|
||||
import { fetchPoll, vote } from 'mastodon/actions/polls';
|
||||
|
||||
const mapDispatchToProps = (dispatch, { pollId }) => ({
|
||||
refresh: debounce(
|
||||
() => {
|
||||
dispatch(fetchPoll(pollId));
|
||||
},
|
||||
1000,
|
||||
{ leading: true },
|
||||
),
|
||||
|
||||
onVote (choices) {
|
||||
dispatch(vote(pollId, choices));
|
||||
},
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { pollId }) => ({
|
||||
poll: state.getIn(['polls', pollId]),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(Poll);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Poll);
|
||||
|
|
|
@ -150,8 +150,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
dispatch(openModal('MEDIA', { media, index }));
|
||||
},
|
||||
|
||||
onOpenVideo (media, time) {
|
||||
dispatch(openModal('VIDEO', { media, time }));
|
||||
onOpenVideo (media, options) {
|
||||
dispatch(openModal('VIDEO', { media, options }));
|
||||
},
|
||||
|
||||
onBlock (status) {
|
||||
|
|
|
@ -38,7 +38,7 @@ export default class TimelineContainer extends React.PureComponent {
|
|||
let timeline;
|
||||
|
||||
if (hashtag) {
|
||||
timeline = <HashtagTimeline hashtag={hashtag} />;
|
||||
timeline = <HashtagTimeline hashtag={hashtag} local={local} />;
|
||||
} else {
|
||||
timeline = <PublicTimeline local={local} />;
|
||||
}
|
||||
|
|
|
@ -192,10 +192,12 @@ class Header extends ImmutablePureComponent {
|
|||
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
|
||||
} else {
|
||||
if (account.getIn(['relationship', 'following'])) {
|
||||
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
|
||||
if (!account.getIn(['relationship', 'muting'])) {
|
||||
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
|
||||
}
|
||||
}
|
||||
|
||||
menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
|
||||
|
|
|
@ -19,6 +19,7 @@ const messages = defineMessages({
|
|||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'blocks', 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'blocks', 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'blocks', 'isLoading'], true),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
|
@ -31,6 +32,7 @@ class Blocks extends ImmutablePureComponent {
|
|||
shouldUpdateScroll: PropTypes.func,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
@ -44,7 +46,7 @@ class Blocks extends ImmutablePureComponent {
|
|||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, accountIds, shouldUpdateScroll, hasMore, multiColumn } = this.props;
|
||||
const { intl, accountIds, shouldUpdateScroll, hasMore, multiColumn, isLoading } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
|
@ -63,6 +65,7 @@ class Blocks extends ImmutablePureComponent {
|
|||
scrollKey='blocks'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
shouldUpdateScroll={shouldUpdateScroll}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
|
|
|
@ -27,6 +27,7 @@ class Option extends React.PureComponent {
|
|||
title: PropTypes.string.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
isPollMultiple: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onToggleMultiple: PropTypes.func.isRequired,
|
||||
|
@ -71,7 +72,7 @@ class Option extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { isPollMultiple, title, index, intl } = this.props;
|
||||
const { isPollMultiple, title, index, autoFocus, intl } = this.props;
|
||||
|
||||
return (
|
||||
<li>
|
||||
|
@ -96,6 +97,7 @@ class Option extends React.PureComponent {
|
|||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
</label>
|
||||
|
||||
|
@ -146,10 +148,12 @@ class PollForm extends ImmutablePureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
const autoFocusIndex = options.indexOf('');
|
||||
|
||||
return (
|
||||
<div className='compose-form__poll-wrapper'>
|
||||
<ul>
|
||||
{options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} {...other} />)}
|
||||
{options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} autoFocus={i === autoFocusIndex} {...other} />)}
|
||||
</ul>
|
||||
|
||||
<div className='poll__footer'>
|
||||
|
|
|
@ -50,7 +50,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
|
|||
const index = items.findIndex(item => {
|
||||
return (item.value === value);
|
||||
});
|
||||
let element;
|
||||
let element = null;
|
||||
|
||||
switch(e.key) {
|
||||
case 'Escape':
|
||||
|
@ -60,18 +60,10 @@ class PrivacyDropdownMenu extends React.PureComponent {
|
|||
this.handleClick(e);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
element = this.node.childNodes[index + 1];
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.props.onChange(element.getAttribute('data-index'));
|
||||
}
|
||||
element = this.node.childNodes[index + 1] || this.node.firstChild;
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
element = this.node.childNodes[index - 1];
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.props.onChange(element.getAttribute('data-index'));
|
||||
}
|
||||
element = this.node.childNodes[index - 1] || this.node.lastChild;
|
||||
break;
|
||||
case 'Tab':
|
||||
if (e.shiftKey) {
|
||||
|
@ -79,28 +71,21 @@ class PrivacyDropdownMenu extends React.PureComponent {
|
|||
} else {
|
||||
element = this.node.childNodes[index + 1] || this.node.firstChild;
|
||||
}
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.props.onChange(element.getAttribute('data-index'));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
element = this.node.firstChild;
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.props.onChange(element.getAttribute('data-index'));
|
||||
}
|
||||
break;
|
||||
case 'End':
|
||||
element = this.node.lastChild;
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.props.onChange(element.getAttribute('data-index'));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.props.onChange(element.getAttribute('data-index'));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
handleClick = e => {
|
||||
|
@ -115,7 +100,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
|
|||
componentDidMount () {
|
||||
document.addEventListener('click', this.handleDocumentClick, false);
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
if (this.focusedItem) this.focusedItem.focus();
|
||||
if (this.focusedItem) this.focusedItem.focus({ preventScroll: true });
|
||||
this.setState({ mounted: true });
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -19,6 +19,7 @@ const messages = defineMessages({
|
|||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'follow_requests', 'items']),
|
||||
isLoading: state.getIn(['user_lists', 'follow_requests', 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']),
|
||||
locked: !!state.getIn(['accounts', me, 'locked']),
|
||||
domain: state.getIn(['meta', 'domain']),
|
||||
|
@ -33,6 +34,7 @@ class FollowRequests extends ImmutablePureComponent {
|
|||
dispatch: PropTypes.func.isRequired,
|
||||
shouldUpdateScroll: PropTypes.func,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
locked: PropTypes.bool,
|
||||
domain: PropTypes.string,
|
||||
|
@ -49,7 +51,7 @@ class FollowRequests extends ImmutablePureComponent {
|
|||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, shouldUpdateScroll, accountIds, hasMore, multiColumn, locked, domain } = this.props;
|
||||
const { intl, shouldUpdateScroll, accountIds, hasMore, multiColumn, locked, domain, isLoading } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
|
@ -77,6 +79,7 @@ class FollowRequests extends ImmutablePureComponent {
|
|||
scrollKey='follow_requests'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
shouldUpdateScroll={shouldUpdateScroll}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
|
|
|
@ -22,6 +22,7 @@ const mapStateToProps = (state, props) => ({
|
|||
isAccount: !!state.getIn(['accounts', props.params.accountId]),
|
||||
accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'followers', props.params.accountId, 'isLoading'], true),
|
||||
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
|
||||
});
|
||||
|
||||
|
@ -34,6 +35,7 @@ class Followers extends ImmutablePureComponent {
|
|||
shouldUpdateScroll: PropTypes.func,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
blockedBy: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
|
@ -58,7 +60,7 @@ class Followers extends ImmutablePureComponent {
|
|||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn } = this.props;
|
||||
const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading } = this.props;
|
||||
|
||||
if (!isAccount) {
|
||||
return (
|
||||
|
@ -85,6 +87,7 @@ class Followers extends ImmutablePureComponent {
|
|||
<ScrollableList
|
||||
scrollKey='followers'
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
shouldUpdateScroll={shouldUpdateScroll}
|
||||
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
|
||||
|
|
|
@ -22,6 +22,7 @@ const mapStateToProps = (state, props) => ({
|
|||
isAccount: !!state.getIn(['accounts', props.params.accountId]),
|
||||
accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'following', props.params.accountId, 'isLoading'], true),
|
||||
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
|
||||
});
|
||||
|
||||
|
@ -34,6 +35,7 @@ class Following extends ImmutablePureComponent {
|
|||
shouldUpdateScroll: PropTypes.func,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
blockedBy: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
|
@ -58,7 +60,7 @@ class Following extends ImmutablePureComponent {
|
|||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn } = this.props;
|
||||
const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading } = this.props;
|
||||
|
||||
if (!isAccount) {
|
||||
return (
|
||||
|
@ -85,6 +87,7 @@ class Following extends ImmutablePureComponent {
|
|||
<ScrollableList
|
||||
scrollKey='following'
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
shouldUpdateScroll={shouldUpdateScroll}
|
||||
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
|
||||
|
|
|
@ -4,6 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import Toggle from 'react-toggle';
|
||||
import AsyncSelect from 'react-select/async';
|
||||
import SettingToggle from '../../notifications/components/setting_toggle';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' },
|
||||
|
@ -87,6 +88,8 @@ class ColumnSettings extends React.PureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { settings, onChange } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='column-settings__row'>
|
||||
|
@ -106,6 +109,10 @@ class ColumnSettings extends React.PureComponent {
|
|||
{this.modeSelect('none')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle settings={settings} settingPath={['local']} onChange={onChange} label={<FormattedMessage id='community.column_settings.local_only' defaultMessage='Local only' />} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue