Merge branch 'master' of https://github.com/glitch-soc/mastodon
commit
c79eb6aedc
|
@ -3,7 +3,7 @@ version: 2
|
|||
aliases:
|
||||
- &defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.6-stretch-node
|
||||
- image: circleci/ruby:2.7-buster-node
|
||||
environment: &ruby_environment
|
||||
BUNDLE_APP_CONFIG: ./.bundle/
|
||||
DB_HOST: localhost
|
||||
|
@ -39,7 +39,6 @@ aliases:
|
|||
steps:
|
||||
- checkout
|
||||
- *attach_workspace
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-node-dependencies-{{ checksum "yarn.lock" }}
|
||||
|
@ -49,7 +48,6 @@ aliases:
|
|||
key: v1-node-dependencies-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ./node_modules/
|
||||
|
||||
- *persist_to_workspace
|
||||
|
||||
- &install_system_dependencies
|
||||
|
@ -58,16 +56,25 @@ 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
|
||||
- *restore_ruby_dependencies
|
||||
- run: bundle install --clean --jobs 16 --path ./vendor/bundle/ --retry 3 --with pam_authentication --without development production && bundle clean
|
||||
- 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
|
||||
- save_cache:
|
||||
key: v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
|
||||
paths:
|
||||
|
@ -82,10 +89,8 @@ aliases:
|
|||
- &test_steps
|
||||
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
|
||||
|
@ -98,21 +103,21 @@ jobs:
|
|||
<<: *defaults
|
||||
<<: *install_steps
|
||||
|
||||
install-ruby2.7:
|
||||
<<: *defaults
|
||||
<<: *install_ruby_dependencies
|
||||
|
||||
install-ruby2.6:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.6-buster-node
|
||||
environment: *ruby_environment
|
||||
<<: *install_ruby_dependencies
|
||||
|
||||
install-ruby2.5:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.5-stretch-node
|
||||
environment: *ruby_environment
|
||||
<<: *install_ruby_dependencies
|
||||
|
||||
install-ruby2.4:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.4-stretch-node
|
||||
- image: circleci/ruby:2.5-buster-node
|
||||
environment: *ruby_environment
|
||||
<<: *install_ruby_dependencies
|
||||
|
||||
|
@ -121,20 +126,47 @@ jobs:
|
|||
steps:
|
||||
- *attach_workspace
|
||||
- *install_system_dependencies
|
||||
- run:
|
||||
name: Precompile assets
|
||||
command: ./bin/rails assets:precompile
|
||||
no_output_timeout: 40m
|
||||
- run: ./bin/rails assets:precompile
|
||||
- persist_to_workspace:
|
||||
root: ~/projects/
|
||||
paths:
|
||||
- ./mastodon/public/assets
|
||||
- ./mastodon/public/packs-test/
|
||||
|
||||
test-migrations:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.7-buster-node
|
||||
environment: *ruby_environment
|
||||
- image: circleci/postgres:10.6-alpine
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
- image: circleci/redis:5-alpine
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- *install_system_dependencies
|
||||
- run:
|
||||
name: Create database
|
||||
command: ./bin/rails parallel:create
|
||||
- run:
|
||||
name: Run migrations
|
||||
command: ./bin/rails parallel:migrate
|
||||
|
||||
test-ruby2.7:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.7-buster-node
|
||||
environment: *ruby_environment
|
||||
- image: circleci/postgres:10.6-alpine
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
- image: circleci/redis:5-alpine
|
||||
<<: *test_steps
|
||||
|
||||
test-ruby2.6:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.6-stretch-node
|
||||
- image: circleci/ruby:2.6-buster-node
|
||||
environment: *ruby_environment
|
||||
- image: circleci/postgres:10.6-alpine
|
||||
environment:
|
||||
|
@ -145,18 +177,7 @@ jobs:
|
|||
test-ruby2.5:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.5-stretch-node
|
||||
environment: *ruby_environment
|
||||
- image: circleci/postgres:10.6-alpine
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
- image: circleci/redis:5-alpine
|
||||
<<: *test_steps
|
||||
|
||||
test-ruby2.4:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.4-stretch-node
|
||||
- image: circleci/ruby:2.5-buster-node
|
||||
environment: *ruby_environment
|
||||
- image: circleci/postgres:10.6-alpine
|
||||
environment:
|
||||
|
@ -167,7 +188,7 @@ jobs:
|
|||
test-webui:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/node:12.9-stretch
|
||||
- image: circleci/node:12-buster
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- run: ./bin/retry yarn test:jest
|
||||
|
@ -187,20 +208,27 @@ workflows:
|
|||
build-and-test:
|
||||
jobs:
|
||||
- install
|
||||
- install-ruby2.7:
|
||||
requires:
|
||||
- install
|
||||
- install-ruby2.6:
|
||||
requires:
|
||||
- install
|
||||
- install-ruby2.7
|
||||
- install-ruby2.5:
|
||||
requires:
|
||||
- install
|
||||
- install-ruby2.6
|
||||
- install-ruby2.4:
|
||||
requires:
|
||||
- install
|
||||
- install-ruby2.6
|
||||
- install-ruby2.7
|
||||
- build:
|
||||
requires:
|
||||
- install-ruby2.6
|
||||
- install-ruby2.7
|
||||
- test-migrations:
|
||||
requires:
|
||||
- install-ruby2.7
|
||||
- test-ruby2.7:
|
||||
requires:
|
||||
- install-ruby2.7
|
||||
- build
|
||||
- test-ruby2.6:
|
||||
requires:
|
||||
- install-ruby2.6
|
||||
|
@ -209,13 +237,9 @@ workflows:
|
|||
requires:
|
||||
- install-ruby2.5
|
||||
- build
|
||||
- test-ruby2.4:
|
||||
requires:
|
||||
- install-ruby2.4
|
||||
- build
|
||||
- test-webui:
|
||||
requires:
|
||||
- install
|
||||
- check-i18n:
|
||||
requires:
|
||||
- install-ruby2.6
|
||||
- install-ruby2.7
|
||||
|
|
|
@ -27,10 +27,10 @@ plugins:
|
|||
enabled: true
|
||||
eslint:
|
||||
enabled: true
|
||||
channel: eslint-5
|
||||
channel: eslint-6
|
||||
rubocop:
|
||||
enabled: true
|
||||
channel: rubocop-0-71
|
||||
channel: rubocop-0-76
|
||||
sass-lint:
|
||||
enabled: true
|
||||
exclude_patterns:
|
||||
|
|
|
@ -183,6 +183,11 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
|
|||
# LDAP_BIND_DN=
|
||||
# LDAP_PASSWORD=
|
||||
# LDAP_UID=cn
|
||||
# LDAP_MAIL=mail
|
||||
# LDAP_SEARCH_FILTER=(|(%{uid}=%{email})(%{mail}=%{email}))
|
||||
# LDAP_UID_CONVERSION_ENABLED=true
|
||||
# LDAP_UID_CONVERSION_SEARCH=., -
|
||||
# LDAP_UID_CONVERSION_REPLACE=_
|
||||
|
||||
# PAM authentication (optional)
|
||||
# PAM authentication uses for the email generation the "email" pam variable
|
||||
|
@ -226,8 +231,8 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
|
|||
|
||||
# Optional SAML authentication (cf. omniauth-saml)
|
||||
# SAML_ENABLED=true
|
||||
# SAML_ACS_URL=
|
||||
# SAML_ISSUER=http://localhost:3000/auth/auth/saml/callback
|
||||
# SAML_ACS_URL=http://localhost:3000/auth/auth/saml/callback
|
||||
# SAML_ISSUER=https://example.com
|
||||
# SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO
|
||||
# SAML_IDP_CERT=
|
||||
# SAML_IDP_CERT_FINGERPRINT=
|
||||
|
|
|
@ -89,6 +89,11 @@ SMTP_FROM_ADDRESS=notifications@example.com
|
|||
# Access-Control-Allow-Origin: https://example.com/
|
||||
# CDN_HOST=https://assets.example.com
|
||||
|
||||
# Optional list of hosts that are allowed to serve media for your instance
|
||||
# This is useful if you include external media in your custom CSS or about page,
|
||||
# or if your data storage provider makes use of redirects to other domains.
|
||||
# EXTRA_DATA_HOSTS=https://data.example1.com|https://data.example2.com
|
||||
|
||||
# S3 (optional)
|
||||
# The attachment host must allow cross origin request from WEB_DOMAIN or
|
||||
# LOCAL_DOMAIN if WEB_DOMAIN is not set. For example, the server may have the
|
||||
|
@ -203,7 +208,11 @@ STREAMING_CLUSTER_NUM=1
|
|||
# LDAP_BIND_DN=
|
||||
# LDAP_PASSWORD=
|
||||
# LDAP_UID=cn
|
||||
# LDAP_SEARCH_FILTER=%{uid}=%{email}
|
||||
# LDAP_MAIL=mail
|
||||
# LDAP_SEARCH_FILTER=(|(%{uid}=%{email})(%{mail}=%{email}))
|
||||
# LDAP_UID_CONVERSION_ENABLED=true
|
||||
# LDAP_UID_CONVERSION_SEARCH=., -
|
||||
# LDAP_UID_CONVERSION_REPLACE=_
|
||||
|
||||
# PAM authentication (optional)
|
||||
# PAM authentication uses for the email generation the "email" pam variable
|
||||
|
@ -247,8 +256,8 @@ STREAMING_CLUSTER_NUM=1
|
|||
|
||||
# Optional SAML authentication (cf. omniauth-saml)
|
||||
# SAML_ENABLED=true
|
||||
# SAML_ACS_URL=
|
||||
# SAML_ISSUER=http://localhost:3000/auth/auth/saml/callback
|
||||
# SAML_ACS_URL=http://localhost:3000/auth/auth/saml/callback
|
||||
# SAML_ISSUER=https://example.com
|
||||
# SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO
|
||||
# SAML_IDP_CERT=
|
||||
# SAML_IDP_CERT_FINGERPRINT=
|
||||
|
@ -271,3 +280,13 @@ STREAMING_CLUSTER_NUM=1
|
|||
# http_proxy=http://gateway.local:8118
|
||||
# Access control for hidden service.
|
||||
# ALLOW_ACCESS_TO_HIDDEN_SERVICE=true
|
||||
|
||||
# Authorized fetch mode (optional)
|
||||
# Require remote servers to authentify when fetching toots, see
|
||||
# https://docs.joinmastodon.org/admin/config/#authorized_fetch
|
||||
# AUTHORIZED_FETCH=true
|
||||
|
||||
# Whitelist mode (optional)
|
||||
# Only allow federation with whitelisted domains, see
|
||||
# https://docs.joinmastodon.org/admin/config/#whitelist_mode
|
||||
# WHITELIST_MODE=true
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Node.js
|
||||
NODE_ENV=test
|
||||
NODE_ENV=tests
|
||||
# Federation
|
||||
LOCAL_DOMAIN=cb6e6126.ngrok.io
|
||||
LOCAL_HTTPS=true
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
VAGRANT=true
|
||||
LOCAL_DOMAIN=mastodon.local
|
||||
BIND=0.0.0.0
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Mastodon Meta Discussion Board
|
||||
url: https://discourse.joinmastodon.org/
|
||||
about: Please ask and answer questions here.
|
|
@ -0,0 +1,10 @@
|
|||
daysUntilStale: 120
|
||||
daysUntilClose: 7
|
||||
exemptLabels:
|
||||
- security
|
||||
staleLabel: wontfix
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
only: pulls
|
|
@ -13,6 +13,7 @@
|
|||
/db/*.sqlite3-journal
|
||||
|
||||
# Ignore all logfiles and tempfiles.
|
||||
.eslintcache
|
||||
/log/*
|
||||
!/log/.keep
|
||||
/tmp
|
||||
|
@ -23,6 +24,7 @@ public/packs
|
|||
public/packs-test
|
||||
.env
|
||||
.env.production
|
||||
.env.development
|
||||
node_modules/
|
||||
build/
|
||||
|
||||
|
@ -55,6 +57,9 @@ npm-debug.log
|
|||
yarn-error.log
|
||||
yarn-debug.log
|
||||
|
||||
# Ignore vagrant log files
|
||||
ubuntu-xenial-16.04-cloudimg-console.log
|
||||
|
||||
# Ignore Docker option files
|
||||
docker-compose.override.yml
|
||||
|
||||
|
|
|
@ -71,6 +71,9 @@ Naming/MemoizedInstanceVariableName:
|
|||
Rails:
|
||||
Enabled: true
|
||||
|
||||
Rails/EnumHash:
|
||||
Enabled: false
|
||||
|
||||
Rails/HasAndBelongsToMany:
|
||||
Enabled: false
|
||||
|
||||
|
@ -102,6 +105,9 @@ Style/Documentation:
|
|||
Style/DoubleNegation:
|
||||
Enabled: true
|
||||
|
||||
Style/FormatStringToken:
|
||||
Enabled: false
|
||||
|
||||
Style/FrozenStringLiteralComment:
|
||||
Enabled: true
|
||||
|
||||
|
|
164
CHANGELOG.md
164
CHANGELOG.md
|
@ -3,6 +3,170 @@ Changelog
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [3.1.0] - 2020-02-09
|
||||
### Added
|
||||
|
||||
- Add bookmarks ([ThibG](https://github.com/tootsuite/mastodon/pull/7107), [Gargron](https://github.com/tootsuite/mastodon/pull/12494), [Gomasy](https://github.com/tootsuite/mastodon/pull/12381))
|
||||
- Add announcements ([Gargron](https://github.com/tootsuite/mastodon/pull/12662), [Gargron](https://github.com/tootsuite/mastodon/pull/12967), [Gargron](https://github.com/tootsuite/mastodon/pull/12970), [Gargron](https://github.com/tootsuite/mastodon/pull/12963), [Gargron](https://github.com/tootsuite/mastodon/pull/12950), [Gargron](https://github.com/tootsuite/mastodon/pull/12990), [Gargron](https://github.com/tootsuite/mastodon/pull/12949), [Gargron](https://github.com/tootsuite/mastodon/pull/12989), [Gargron](https://github.com/tootsuite/mastodon/pull/12964), [Gargron](https://github.com/tootsuite/mastodon/pull/12965), [ThibG](https://github.com/tootsuite/mastodon/pull/12958), [ThibG](https://github.com/tootsuite/mastodon/pull/12957), [Gargron](https://github.com/tootsuite/mastodon/pull/12955), [ThibG](https://github.com/tootsuite/mastodon/pull/12946), [ThibG](https://github.com/tootsuite/mastodon/pull/12954))
|
||||
- Add number animations in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/12948), [Gargron](https://github.com/tootsuite/mastodon/pull/12971))
|
||||
- Add `kab`, `is`, `kn`, `mr`, `ur` to available locales ([Gargron](https://github.com/tootsuite/mastodon/pull/12882), [BoFFire](https://github.com/tootsuite/mastodon/pull/12962), [Gargron](https://github.com/tootsuite/mastodon/pull/12379))
|
||||
- Add profile filter category ([ThibG](https://github.com/tootsuite/mastodon/pull/12918))
|
||||
- Add ability to add oneself to lists ([ThibG](https://github.com/tootsuite/mastodon/pull/12271))
|
||||
- Add hint how to contribute translations to preferences page ([Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/12736))
|
||||
- Add signatures to statuses in archive takeout ([noellabo](https://github.com/tootsuite/mastodon/pull/12649))
|
||||
- Add support for `magnet:` and `xmpp` links ([ThibG](https://github.com/tootsuite/mastodon/pull/12905), [ThibG](https://github.com/tootsuite/mastodon/pull/12709))
|
||||
- Add `follow_request` notification type ([ThibG](https://github.com/tootsuite/mastodon/pull/12198))
|
||||
- Add ability to filter reports by account domain in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12154))
|
||||
- Add link to search for users connected from the same IP address to admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12157))
|
||||
- Add link to reports targeting a specific domain in admin view ([ThibG](https://github.com/tootsuite/mastodon/pull/12513))
|
||||
- Add support for EventSource streaming in web UI ([BenLubar](https://github.com/tootsuite/mastodon/pull/12887))
|
||||
- Add hotkey for opening media attachments in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12498), [Kjwon15](https://github.com/tootsuite/mastodon/pull/12546))
|
||||
- Add relationship-based options to status dropdowns in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/12377), [ThibG](https://github.com/tootsuite/mastodon/pull/12535), [Gargron](https://github.com/tootsuite/mastodon/pull/12430))
|
||||
- Add support for submitting media description with `ctrl`+`enter` in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12272))
|
||||
- Add download button to audio and video players in web UI ([NimaBoscarino](https://github.com/tootsuite/mastodon/pull/12179))
|
||||
- Add setting for whether to crop images in timelines in web UI ([duxovni](https://github.com/tootsuite/mastodon/pull/12126))
|
||||
- Add support for `Event` activities ([tcitworld](https://github.com/tootsuite/mastodon/pull/12637))
|
||||
- Add basic support for `Group` actors ([noellabo](https://github.com/tootsuite/mastodon/pull/12071))
|
||||
- Add `S3_OVERRIDE_PATH_STYLE` environment variable ([Gargron](https://github.com/tootsuite/mastodon/pull/12594))
|
||||
- Add `S3_OPEN_TIMEOUT` environment variable ([tateisu](https://github.com/tootsuite/mastodon/pull/12459))
|
||||
- Add `LDAP_MAIL` environment variable ([madmath03](https://github.com/tootsuite/mastodon/pull/12053))
|
||||
- Add `LDAP_UID_CONVERSION_ENABLED` environment variable ([madmath03](https://github.com/tootsuite/mastodon/pull/12461))
|
||||
- Add `--remote-only` option to `tootctl emoji purge` ([ThibG](https://github.com/tootsuite/mastodon/pull/12810))
|
||||
- Add `tootctl media remove-orphans` ([Gargron](https://github.com/tootsuite/mastodon/pull/12568), [Gargron](https://github.com/tootsuite/mastodon/pull/12571))
|
||||
- Add `tootctl media lookup` command ([irlcatgirl](https://github.com/tootsuite/mastodon/pull/12283))
|
||||
- Add cache for OEmbed endpoints to avoid extra HTTP requests ([Gargron](https://github.com/tootsuite/mastodon/pull/12403))
|
||||
- Add support for KaiOS arrow navigation to public pages ([nolanlawson](https://github.com/tootsuite/mastodon/pull/12251))
|
||||
- Add `discoverable` to accounts in REST API ([trwnh](https://github.com/tootsuite/mastodon/pull/12508))
|
||||
- Add admin setting to disable default follows ([ArisuOngaku](https://github.com/tootsuite/mastodon/pull/12566))
|
||||
- Add support for LDAP and PAM in the OAuth password grant strategy ([ntl-purism](https://github.com/tootsuite/mastodon/pull/12390), [Gargron](https://github.com/tootsuite/mastodon/pull/12743))
|
||||
- Allow support for `Accept`/`Reject` activities with a non-embedded object ([puckipedia](https://github.com/tootsuite/mastodon/pull/12199))
|
||||
- Add "Show thread" button to public profiles ([Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/13000))
|
||||
|
||||
### Changed
|
||||
|
||||
- Change `last_status_at` to be a date, not datetime in REST API ([ThibG](https://github.com/tootsuite/mastodon/pull/12966))
|
||||
- Change followers page to relationships page in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/12927), [Gargron](https://github.com/tootsuite/mastodon/pull/12934))
|
||||
- Change reported media attachments to always be hidden in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/12879), [ThibG](https://github.com/tootsuite/mastodon/pull/12907))
|
||||
- Change string from "Disable" to "Disable login" in admin UI ([nileshkumar](https://github.com/tootsuite/mastodon/pull/12201))
|
||||
- Change report page structure in admin UI ([Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/12615))
|
||||
- Change swipe sensitivity to be lower on small screens in web UI ([umonaca](https://github.com/tootsuite/mastodon/pull/12168))
|
||||
- Change audio/video playback to stop playback when out of view in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/12486))
|
||||
- Change media description label based on upload type in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12270))
|
||||
- Change large numbers to render without decimal units in web UI ([noellabo](https://github.com/tootsuite/mastodon/pull/12706))
|
||||
- Change "Add a choice" button to be disabled rather than hidden when poll limit reached in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12319), [hinaloe](https://github.com/tootsuite/mastodon/pull/12544))
|
||||
- Change `tootctl statuses remove` to keep statuses favourited or bookmarked by local users ([ThibG](https://github.com/tootsuite/mastodon/pull/11267), [Gomasy](https://github.com/tootsuite/mastodon/pull/12818))
|
||||
- Change domain block behavior to update user records (fast) before deleting data (slower) ([ThibG](https://github.com/tootsuite/mastodon/pull/12247))
|
||||
- Change behaviour to strip audio metadata on uploads ([hugogameiro](https://github.com/tootsuite/mastodon/pull/12171))
|
||||
- Change accepted length of remote media descriptions from 420 to 1,500 characters ([ThibG](https://github.com/tootsuite/mastodon/pull/12262))
|
||||
- Change preferences pages structure ([Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/12497), [mayaeh](https://github.com/tootsuite/mastodon/pull/12517), [Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/12801), [Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/12797), [Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/12799), [Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/12793))
|
||||
- Change format of titles in RSS ([devkral](https://github.com/tootsuite/mastodon/pull/8596))
|
||||
- Change favourite icon animation from spring-based motion to CSS animation in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12175))
|
||||
- Change minimum required Node.js version to 10, and default to 12 ([Shleeble](https://github.com/tootsuite/mastodon/pull/12791), [mkody](https://github.com/tootsuite/mastodon/pull/12906), [Shleeble](https://github.com/tootsuite/mastodon/pull/12703))
|
||||
- Change spam check to exempt server staff ([ThibG](https://github.com/tootsuite/mastodon/pull/12874))
|
||||
- Change to fallback to to `Create` audience when `object` has no defined audience ([ThibG](https://github.com/tootsuite/mastodon/pull/12249))
|
||||
- Change Twemoji library to 12.1.3 in web UI ([koyuawsmbrtn](https://github.com/tootsuite/mastodon/pull/12342))
|
||||
- Change blocked users to be hidden from following/followers lists ([ThibG](https://github.com/tootsuite/mastodon/pull/12733))
|
||||
- Change signature verification to ignore signatures with invalid host ([Gargron](https://github.com/tootsuite/mastodon/pull/13033))
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove unused dependencies ([ykzts](https://github.com/tootsuite/mastodon/pull/12861), [mayaeh](https://github.com/tootsuite/mastodon/pull/12826), [ThibG](https://github.com/tootsuite/mastodon/pull/12822), [ykzts](https://github.com/tootsuite/mastodon/pull/12533))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix some translatable strings being used wrongly ([Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/12569), [Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/12589), [Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/12502), [mayaeh](https://github.com/tootsuite/mastodon/pull/12231))
|
||||
- Fix headline of public timeline page when set to local-only ([ykzts](https://github.com/tootsuite/mastodon/pull/12224))
|
||||
- Fix space between tabs not being spread evenly in web UI ([Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/12944), [Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/12961), [Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/12446))
|
||||
- Fix interactive delays in database migrations with no TTY ([Gargron](https://github.com/tootsuite/mastodon/pull/12969))
|
||||
- Fix status overflowing in report dialog in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12959))
|
||||
- Fix unlocalized dropdown button title in web UI ([Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/12947))
|
||||
- Fix media attachments without file being uploadable ([Gargron](https://github.com/tootsuite/mastodon/pull/12562))
|
||||
- Fix unfollow confirmations in profile directory in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12922))
|
||||
- Fix duplicate `description` meta tag on accounts public pages ([ThibG](https://github.com/tootsuite/mastodon/pull/12923))
|
||||
- Fix slow query of federated timeline ([notozeki](https://github.com/tootsuite/mastodon/pull/12886))
|
||||
- Fix not all of account's active IPs showing up in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/12909), [Gargron](https://github.com/tootsuite/mastodon/pull/12943))
|
||||
- Fix search by IP not using alternative browser sessions in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/12904))
|
||||
- Fix “X new items” not showing up for slow mode on empty timelines in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12875))
|
||||
- Fix OEmbed endpoint being inaccessible in secure mode ([Gargron](https://github.com/tootsuite/mastodon/pull/12864))
|
||||
- Fix proofs API being inaccessible in secure mode ([Gargron](https://github.com/tootsuite/mastodon/pull/12495))
|
||||
- Fix Ruby 2.7 incompatibilities ([ThibG](https://github.com/tootsuite/mastodon/pull/12831), [ThibG](https://github.com/tootsuite/mastodon/pull/12824), [Shleeble](https://github.com/tootsuite/mastodon/pull/12759), [zunda](https://github.com/tootsuite/mastodon/pull/12769))
|
||||
- Fix invalid poll votes being accepted in REST API ([ThibG](https://github.com/tootsuite/mastodon/pull/12601))
|
||||
- Fix old migrations failing because of strong migrations update ([ThibG](https://github.com/tootsuite/mastodon/pull/12787), [ThibG](https://github.com/tootsuite/mastodon/pull/12692))
|
||||
- Fix reuse of detailed status components in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12792))
|
||||
- Fix base64-encoded file uploads not being possible in REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/12748), [Gargron](https://github.com/tootsuite/mastodon/pull/12857))
|
||||
- Fix error due to missing authentication call in filters controller ([Gargron](https://github.com/tootsuite/mastodon/pull/12746))
|
||||
- Fix uncaught unknown format error in host meta controller ([Gargron](https://github.com/tootsuite/mastodon/pull/12747))
|
||||
- Fix URL search not returning private toots user has access to ([ThibG](https://github.com/tootsuite/mastodon/pull/12742), [ThibG](https://github.com/tootsuite/mastodon/pull/12336))
|
||||
- Fix cache digesting log noise on status embeds ([Gargron](https://github.com/tootsuite/mastodon/pull/12750))
|
||||
- Fix slowness due to layout thrashing when reloading a large set of statuses in web UI ([panarom](https://github.com/tootsuite/mastodon/pull/12661), [panarom](https://github.com/tootsuite/mastodon/pull/12744), [Gargron](https://github.com/tootsuite/mastodon/pull/12712))
|
||||
- Fix error when fetching followers/following from REST API when user has network hidden ([Gargron](https://github.com/tootsuite/mastodon/pull/12716))
|
||||
- Fix IDN mentions not being processed, IDN domains not being rendered ([Gargron](https://github.com/tootsuite/mastodon/pull/12715), [Gargron](https://github.com/tootsuite/mastodon/pull/13035), [Gargron](https://github.com/tootsuite/mastodon/pull/13030))
|
||||
- Fix error when searching for empty phrase ([Gargron](https://github.com/tootsuite/mastodon/pull/12711))
|
||||
- Fix backups stopping due to read timeouts ([chr-1x](https://github.com/tootsuite/mastodon/pull/12281))
|
||||
- Fix batch actions on non-pending tags in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12537))
|
||||
- Fix sample `SAML_ACS_URL`, `SAML_ISSUER` ([orlea](https://github.com/tootsuite/mastodon/pull/12669))
|
||||
- Fix manual scrolling issue on Firefox/Windows in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12648))
|
||||
- Fix archive takeout failing if total dump size exceeds 2GB ([scd31](https://github.com/tootsuite/mastodon/pull/12602), [Gargron](https://github.com/tootsuite/mastodon/pull/12653))
|
||||
- Fix custom emoji category creation silently erroring out on duplicate category ([ThibG](https://github.com/tootsuite/mastodon/pull/12647))
|
||||
- Fix link crawler not specifying preferred content type ([ThibG](https://github.com/tootsuite/mastodon/pull/12646))
|
||||
- Fix featured hashtag setting page erroring out instead of rejecting invalid tags ([ThibG](https://github.com/tootsuite/mastodon/pull/12436))
|
||||
- Fix tooltip messages of single/multiple-choice polls switcher being reversed in web UI ([acid-chicken](https://github.com/tootsuite/mastodon/pull/12616))
|
||||
- Fix typo in help text of `tootctl statuses remove` ([trwnh](https://github.com/tootsuite/mastodon/pull/12603))
|
||||
- Fix generic HTTP 500 error on duplicate records ([Gargron](https://github.com/tootsuite/mastodon/pull/12563))
|
||||
- Fix old migration failing with new status default scope ([ThibG](https://github.com/tootsuite/mastodon/pull/12493))
|
||||
- Fix errors when using search API with no query ([Gargron](https://github.com/tootsuite/mastodon/pull/12541), [trwnh](https://github.com/tootsuite/mastodon/pull/12549))
|
||||
- Fix poll options not being selectable via keyboard in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12538))
|
||||
- Fix conversations not having an unread indicator in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/12506))
|
||||
- Fix lost focus when modals open/close in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12437))
|
||||
- Fix pending upload count not being decremented on error in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12499))
|
||||
- Fix empty poll options not being removed on remote poll update ([ThibG](https://github.com/tootsuite/mastodon/pull/12484))
|
||||
- Fix OCR with delete & redraft in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12465))
|
||||
- Fix blur behind closed registration message ([ThibG](https://github.com/tootsuite/mastodon/pull/12442))
|
||||
- Fix OEmbed discovery not handling different URL variants in query ([Gargron](https://github.com/tootsuite/mastodon/pull/12439))
|
||||
- Fix link crawler crashing on `<a>` tags without `href` ([ThibG](https://github.com/tootsuite/mastodon/pull/12159))
|
||||
- Fix whitelisted subdomains being ignored in whitelist mode ([noiob](https://github.com/tootsuite/mastodon/pull/12435))
|
||||
- Fix broken audit log in whitelist mode in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12303))
|
||||
- Fix unread indicator not honoring "Only media" option in local and federated timelines in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12330))
|
||||
- Fix error when rebuilding home feeds ([dariusk](https://github.com/tootsuite/mastodon/pull/12324))
|
||||
- Fix relationship caches being broken as result of a follow request ([ThibG](https://github.com/tootsuite/mastodon/pull/12299))
|
||||
- Fix more items than the limit being uploadable in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12300))
|
||||
- Fix various issues with account migration ([ThibG](https://github.com/tootsuite/mastodon/pull/12301))
|
||||
- Fix filtered out items being counted as pending items in slow mode in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12266))
|
||||
- Fix notification filters not applying to poll options ([ThibG](https://github.com/tootsuite/mastodon/pull/12269))
|
||||
- Fix notification message for user's own poll saying it's a poll they voted on in web UI ([ykzts](https://github.com/tootsuite/mastodon/pull/12219))
|
||||
- Fix polls with an expiration not showing up as expired in web UI ([noellabo](https://github.com/tootsuite/mastodon/pull/12222))
|
||||
- Fix volume slider having an offset between cursor and slider in Chromium in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12158))
|
||||
- Fix Vagrant image not accepting connections ([shrft](https://github.com/tootsuite/mastodon/pull/12180))
|
||||
- Fix batch actions being hidden on small screens in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12183))
|
||||
- Fix incoming federation not working in whitelist mode ([ThibG](https://github.com/tootsuite/mastodon/pull/12185))
|
||||
- Fix error when passing empty `source` param to `PUT /api/v1/accounts/update_credentials` ([jglauche](https://github.com/tootsuite/mastodon/pull/12259))
|
||||
- Fix HTTP-based streaming API being cacheable by proxies ([BenLubar](https://github.com/tootsuite/mastodon/pull/12945))
|
||||
- Fix users being able to register while `tootctl self-destruct` is in progress ([Kjwon15](https://github.com/tootsuite/mastodon/pull/12877))
|
||||
- Fix microformats detection in link crawler not ignoring `h-card` links ([nightpool](https://github.com/tootsuite/mastodon/pull/12189))
|
||||
- Fix outline on full-screen video in web UI ([hinaloe](https://github.com/tootsuite/mastodon/pull/12176))
|
||||
- Fix TLD domain blocks not being editable ([ThibG](https://github.com/tootsuite/mastodon/pull/12805))
|
||||
- Fix Nanobox deploy hooks ([danhunsaker](https://github.com/tootsuite/mastodon/pull/12663))
|
||||
- Fix needlessly complicated SQL query when performing account search amongst followings ([ThibG](https://github.com/tootsuite/mastodon/pull/12302))
|
||||
- Fix favourites count not updating when unfavouriting in web UI ([NimaBoscarino](https://github.com/tootsuite/mastodon/pull/12140))
|
||||
- Fix occasional crash on scroll in Chromium in web UI ([hinaloe](https://github.com/tootsuite/mastodon/pull/12274))
|
||||
- Fix intersection observer not working in single-column mode web UI ([panarom](https://github.com/tootsuite/mastodon/pull/12735))
|
||||
- Fix voting issue with remote polls that contain trailing spaces ([ThibG](https://github.com/tootsuite/mastodon/pull/12515))
|
||||
- Fix dynamic elements not working in pgHero due to CSP rules ([ykzts](https://github.com/tootsuite/mastodon/pull/12489))
|
||||
- Fix overly verbose backtraces when delivering ActivityPub payloads ([zunda](https://github.com/tootsuite/mastodon/pull/12798))
|
||||
- Fix rendering `<a>` without `href` when scheme unsupported ([Gargron](https://github.com/tootsuite/mastodon/pull/13040))
|
||||
- Fix unfiltered params error when generating ActivityPub tag pagination ([Gargron](https://github.com/tootsuite/mastodon/pull/13049))
|
||||
- Fix malformed HTML causing uncaught error ([Gargron](https://github.com/tootsuite/mastodon/pull/13042))
|
||||
- Fix native share button not being displayed for unlisted toots ([ThibG](https://github.com/tootsuite/mastodon/pull/13045))
|
||||
- Fix remote convertible media attachments (e.g. GIFs) not being saved ([Gargron](https://github.com/tootsuite/mastodon/pull/13032))
|
||||
- Fix account query not using faster index ([abcang](https://github.com/tootsuite/mastodon/pull/13016))
|
||||
- Fix error when sending moderation notification ([renatolond](https://github.com/tootsuite/mastodon/pull/13014))
|
||||
|
||||
### Security
|
||||
|
||||
- Fix OEmbed leaking information about existence of non-public statuses ([Gargron](https://github.com/tootsuite/mastodon/pull/12930))
|
||||
- Fix password change/reset not immediately invalidating other sessions ([Gargron](https://github.com/tootsuite/mastodon/pull/12928))
|
||||
- Fix settings pages being cacheable by the browser ([Gargron](https://github.com/tootsuite/mastodon/pull/12714))
|
||||
|
||||
## [3.0.1] - 2019-10-10
|
||||
### Added
|
||||
|
||||
|
|
|
@ -48,13 +48,13 @@ If your contributions are accepted into Mastodon, you can request to be paid thr
|
|||
|
||||
## Bug reports
|
||||
|
||||
Bug reports and feature suggestions can be submitted to [GitHub Issues](https://github.com/tootsuite/mastodon/issues). Please make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected in the past using the search function. Please also use descriptive, concise titles.
|
||||
Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/tootsuite/mastodon/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected.
|
||||
|
||||
## Translations
|
||||
|
||||
You can submit translations via [Crowdin](https://crowdin.com/project/mastodon). They are periodically merged into the codebase.
|
||||
|
||||
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin]
|
||||
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)](https://crowdin.com/project/mastodon)
|
||||
|
||||
## Pull requests
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ FROM ubuntu:18.04 as build-dep
|
|||
# Use bash for the shell
|
||||
SHELL ["bash", "-c"]
|
||||
|
||||
# Install Node
|
||||
ENV NODE_VER="12.11.1"
|
||||
# Install Node v12 (LTS)
|
||||
ENV NODE_VER="12.14.0"
|
||||
RUN echo "Etc/UTC" > /etc/localtime && \
|
||||
apt update && \
|
||||
apt -y install wget python && \
|
||||
|
@ -58,7 +58,9 @@ RUN npm install -g yarn && \
|
|||
COPY Gemfile* package.json yarn.lock /opt/mastodon/
|
||||
|
||||
RUN cd /opt/mastodon && \
|
||||
bundle install -j$(nproc) --deployment --without development test && \
|
||||
bundle config set deployment 'true' && \
|
||||
bundle config set without 'development test' && \
|
||||
bundle install -j$(nproc) && \
|
||||
yarn install --pure-lockfile
|
||||
|
||||
FROM ubuntu:18.04
|
||||
|
@ -123,3 +125,4 @@ RUN cd ~ && \
|
|||
# Set the work dir and the container entry point
|
||||
WORKDIR /opt/mastodon
|
||||
ENTRYPOINT ["/tini", "--"]
|
||||
EXPOSE 3000 4000
|
||||
|
|
89
Gemfile
89
Gemfile
|
@ -1,21 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
ruby '>= 2.4.0', '< 2.7.0'
|
||||
ruby '>= 2.4.0', '< 3.0.0'
|
||||
|
||||
gem 'pkg-config', '~> 1.3'
|
||||
gem 'pkg-config', '~> 1.4'
|
||||
|
||||
gem 'puma', '~> 4.2'
|
||||
gem 'rails', '~> 5.2.3'
|
||||
gem 'puma', '~> 4.3'
|
||||
gem 'rails', '~> 5.2.4'
|
||||
gem 'sprockets', '~> 3.7.2'
|
||||
gem 'thor', '~> 0.20'
|
||||
gem 'rack', '~> 2.1.2'
|
||||
|
||||
gem 'thwait', '~> 0.1.0'
|
||||
gem 'e2mmap', '~> 0.1.0'
|
||||
|
||||
gem 'hamlit-rails', '~> 0.2'
|
||||
gem 'pg', '~> 1.1'
|
||||
gem 'pg', '~> 1.2'
|
||||
gem 'makara', '~> 0.4'
|
||||
gem 'pghero', '~> 2.3'
|
||||
gem 'pghero', '~> 2.4'
|
||||
gem 'dotenv-rails', '~> 2.7'
|
||||
|
||||
gem 'aws-sdk-s3', '~> 1.48', require: false
|
||||
gem 'aws-sdk-s3', '~> 1.60', require: false
|
||||
gem 'fog-core', '<= 2.1.0'
|
||||
gem 'fog-openstack', '~> 0.3', require: false
|
||||
gem 'paperclip', '~> 6.0'
|
||||
|
@ -27,10 +32,10 @@ gem 'active_model_serializers', '~> 0.10'
|
|||
gem 'addressable', '~> 2.7'
|
||||
gem 'bootsnap', '~> 1.4', require: false
|
||||
gem 'browser'
|
||||
gem 'charlock_holmes', '~> 0.7.6'
|
||||
gem 'charlock_holmes', '~> 0.7.7'
|
||||
gem 'iso-639'
|
||||
gem 'chewy', '~> 5.1'
|
||||
gem 'cld3', '~> 3.2.4'
|
||||
gem 'cld3', '~> 3.2.6'
|
||||
gem 'devise', '~> 4.7'
|
||||
gem 'devise-two-factor', '~> 3.1'
|
||||
|
||||
|
@ -38,7 +43,7 @@ group :pam_authentication, optional: true do
|
|||
gem 'devise_pam_authenticatable2', '~> 9.2'
|
||||
end
|
||||
|
||||
gem 'net-ldap', '~> 0.10'
|
||||
gem 'net-ldap', '~> 0.16'
|
||||
gem 'omniauth-cas', '~> 1.1'
|
||||
gem 'omniauth-saml', '~> 1.10'
|
||||
gem 'omniauth', '~> 1.9'
|
||||
|
@ -49,36 +54,35 @@ gem 'fast_blank', '~> 1.0'
|
|||
gem 'fastimage'
|
||||
gem 'goldfinger', '~> 2.1'
|
||||
gem 'hiredis', '~> 0.6'
|
||||
gem 'redis-namespace', '~> 1.5'
|
||||
gem 'redis-namespace', '~> 1.7'
|
||||
gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b'
|
||||
gem 'html2text'
|
||||
gem 'htmlentities', '~> 4.3'
|
||||
gem 'http', '~> 3.3'
|
||||
gem 'http', '~> 4.3'
|
||||
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.3'
|
||||
gem 'httplog', '~> 1.4.2'
|
||||
gem 'idn-ruby', require: 'idn'
|
||||
gem 'kaminari', '~> 1.1'
|
||||
gem 'link_header', '~> 0.0'
|
||||
gem 'mime-types', '~> 3.3', require: 'mime/types/columnar'
|
||||
gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar'
|
||||
gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532'
|
||||
gem 'nokogiri', '~> 1.10'
|
||||
gem 'nsa', '~> 0.2'
|
||||
gem 'oj', '~> 3.9'
|
||||
gem 'ostatus2', '~> 2.0'
|
||||
gem 'ox', '~> 2.11'
|
||||
gem 'oj', '~> 3.10'
|
||||
gem 'ox', '~> 2.12'
|
||||
gem 'parslet'
|
||||
gem 'parallel', '~> 1.17'
|
||||
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.1'
|
||||
gem 'rack-cors', '~> 1.0', require: 'rack/cors'
|
||||
gem 'rack-attack', '~> 6.2'
|
||||
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
|
||||
gem 'rails-i18n', '~> 5.1'
|
||||
gem 'rails-settings-cached', '~> 0.6'
|
||||
gem 'redis', '~> 4.1', require: ['redis', 'redis/connection/hiredis']
|
||||
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
||||
gem 'rqrcode', '~> 0.10'
|
||||
gem 'rqrcode', '~> 1.1'
|
||||
gem 'ruby-progressbar', '~> 1.10'
|
||||
gem 'sanitize', '~> 5.1'
|
||||
gem 'sidekiq', '~> 5.2'
|
||||
|
@ -86,30 +90,30 @@ gem 'sidekiq-scheduler', '~> 3.0'
|
|||
gem 'sidekiq-unique-jobs', '~> 6.0'
|
||||
gem 'sidekiq-bulk', '~>0.2.0'
|
||||
gem 'simple-navigation', '~> 4.1'
|
||||
gem 'simple_form', '~> 4.1'
|
||||
gem 'simple_form', '~> 5.0'
|
||||
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
|
||||
gem 'stoplight', '~> 2.1.3'
|
||||
gem 'strong_migrations', '~> 0.4'
|
||||
gem 'stoplight', '~> 2.2.0'
|
||||
gem 'strong_migrations', '~> 0.5'
|
||||
gem 'tty-command', '~> 0.9', require: false
|
||||
gem 'tty-prompt', '~> 0.19', require: false
|
||||
gem 'tty-prompt', '~> 0.20', require: false
|
||||
gem 'twitter-text', '~> 1.14'
|
||||
gem 'tzinfo-data', '~> 1.2019'
|
||||
gem 'webpacker', '~> 4.0'
|
||||
gem 'webpacker', '~> 4.2'
|
||||
gem 'webpush'
|
||||
|
||||
gem 'json-ld', git: 'https://github.com/ruby-rdf/json-ld.git', ref: 'e742697a0906e74e8bb777ef98137bc3955d981d'
|
||||
gem 'json-ld'
|
||||
gem 'json-ld-preloaded', '~> 3.0'
|
||||
gem 'rdf-normalize', '~> 0.3'
|
||||
gem 'rdf-normalize', '~> 0.4'
|
||||
|
||||
gem 'redcarpet', '~> 3.4'
|
||||
|
||||
group :development, :test do
|
||||
gem 'fabrication', '~> 2.20'
|
||||
gem 'fuubar', '~> 2.4'
|
||||
gem 'fabrication', '~> 2.21'
|
||||
gem 'fuubar', '~> 2.5'
|
||||
gem 'i18n-tasks', '~> 0.9', require: false
|
||||
gem 'pry-byebug', '~> 3.7'
|
||||
gem 'pry-rails', '~> 0.3'
|
||||
gem 'rspec-rails', '~> 3.8'
|
||||
gem 'rspec-rails', '~> 3.9'
|
||||
end
|
||||
|
||||
group :production, :test do
|
||||
|
@ -117,29 +121,29 @@ group :production, :test do
|
|||
end
|
||||
|
||||
group :test do
|
||||
gem 'capybara', '~> 3.29'
|
||||
gem 'capybara', '~> 3.30'
|
||||
gem 'climate_control', '~> 0.2'
|
||||
gem 'faker', '~> 2.5'
|
||||
gem 'microformats', '~> 4.1'
|
||||
gem 'faker', '~> 2.10'
|
||||
gem 'microformats', '~> 4.2'
|
||||
gem 'rails-controller-testing', '~> 1.0'
|
||||
gem 'rspec-sidekiq', '~> 3.0'
|
||||
gem 'simplecov', '~> 0.17', require: false
|
||||
gem 'webmock', '~> 3.7'
|
||||
gem 'parallel_tests', '~> 2.29'
|
||||
gem 'webmock', '~> 3.8'
|
||||
gem 'parallel_tests', '~> 2.30'
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem 'active_record_query_trace', '~> 1.6'
|
||||
gem 'annotate', '~> 2.7'
|
||||
gem 'active_record_query_trace', '~> 1.7'
|
||||
gem 'annotate', '~> 3.0'
|
||||
gem 'better_errors', '~> 2.5'
|
||||
gem 'binding_of_caller', '~> 0.7'
|
||||
gem 'bullet', '~> 6.0'
|
||||
gem 'bullet', '~> 6.1'
|
||||
gem 'letter_opener', '~> 1.7'
|
||||
gem 'letter_opener_web', '~> 1.3'
|
||||
gem 'memory_profiler'
|
||||
gem 'rubocop', '~> 0.74', require: false
|
||||
gem 'rubocop-rails', '~> 2.3', require: false
|
||||
gem 'brakeman', '~> 4.6', require: false
|
||||
gem 'rubocop', '~> 0.79', require: false
|
||||
gem 'rubocop-rails', '~> 2.4', require: false
|
||||
gem 'brakeman', '~> 4.7', require: false
|
||||
gem 'bundler-audit', '~> 0.6', require: false
|
||||
|
||||
gem 'capistrano', '~> 3.11'
|
||||
|
@ -147,7 +151,6 @@ group :development do
|
|||
gem 'capistrano-rbenv', '~> 2.1'
|
||||
gem 'capistrano-yarn', '~> 2.0'
|
||||
|
||||
gem 'derailed_benchmarks'
|
||||
gem 'stackprof'
|
||||
end
|
||||
|
||||
|
|
450
Gemfile.lock
450
Gemfile.lock
|
@ -13,19 +13,6 @@ GIT
|
|||
specs:
|
||||
posix-spawn (0.3.13)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/ruby-rdf/json-ld.git
|
||||
revision: e742697a0906e74e8bb777ef98137bc3955d981d
|
||||
ref: e742697a0906e74e8bb777ef98137bc3955d981d
|
||||
specs:
|
||||
json-ld (3.0.2)
|
||||
htmlentities (~> 4.3)
|
||||
json-canonicalization (~> 0.1)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
multi_json (~> 1.13)
|
||||
rack (>= 1.6, < 3.0)
|
||||
rdf (~> 3.0, >= 3.0.8)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/tmm1/http_parser.rb
|
||||
revision: 54b17ba8c7d8d20a16dfc65d1775241833219cf2
|
||||
|
@ -44,25 +31,25 @@ GIT
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (5.2.3)
|
||||
actionpack (= 5.2.3)
|
||||
actioncable (5.2.4.1)
|
||||
actionpack (= 5.2.4.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailer (5.2.3)
|
||||
actionpack (= 5.2.3)
|
||||
actionview (= 5.2.3)
|
||||
activejob (= 5.2.3)
|
||||
actionmailer (5.2.4.1)
|
||||
actionpack (= 5.2.4.1)
|
||||
actionview (= 5.2.4.1)
|
||||
activejob (= 5.2.4.1)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (5.2.3)
|
||||
actionview (= 5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
rack (~> 2.0)
|
||||
actionpack (5.2.4.1)
|
||||
actionview (= 5.2.4.1)
|
||||
activesupport (= 5.2.4.1)
|
||||
rack (~> 2.0, >= 2.0.8)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
actionview (5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
actionview (5.2.4.1)
|
||||
activesupport (= 5.2.4.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
|
@ -72,32 +59,32 @@ GEM
|
|||
activemodel (>= 4.1, < 6.1)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
active_record_query_trace (1.6.2)
|
||||
activejob (5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
active_record_query_trace (1.7)
|
||||
activejob (5.2.4.1)
|
||||
activesupport (= 5.2.4.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
activerecord (5.2.3)
|
||||
activemodel (= 5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
activemodel (5.2.4.1)
|
||||
activesupport (= 5.2.4.1)
|
||||
activerecord (5.2.4.1)
|
||||
activemodel (= 5.2.4.1)
|
||||
activesupport (= 5.2.4.1)
|
||||
arel (>= 9.0)
|
||||
activestorage (5.2.3)
|
||||
actionpack (= 5.2.3)
|
||||
activerecord (= 5.2.3)
|
||||
activestorage (5.2.4.1)
|
||||
actionpack (= 5.2.4.1)
|
||||
activerecord (= 5.2.4.1)
|
||||
marcel (~> 0.3.1)
|
||||
activesupport (5.2.3)
|
||||
activesupport (5.2.4.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.7.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
airbrussh (1.3.4)
|
||||
airbrussh (1.4.0)
|
||||
sshkit (>= 1.6.1, != 1.7.0)
|
||||
annotate (2.7.5)
|
||||
annotate (3.0.3)
|
||||
activerecord (>= 3.2, < 7.0)
|
||||
rake (>= 10.4, < 13.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
arel (9.0.0)
|
||||
ast (2.4.0)
|
||||
attr_encrypted (3.1.0)
|
||||
|
@ -105,37 +92,36 @@ GEM
|
|||
av (0.9.0)
|
||||
cocaine (~> 0.5.3)
|
||||
aws-eventstream (1.0.3)
|
||||
aws-partitions (1.207.0)
|
||||
aws-sdk-core (3.65.1)
|
||||
aws-partitions (1.261.0)
|
||||
aws-sdk-core (3.86.0)
|
||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||
aws-partitions (~> 1.0)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.24.0)
|
||||
aws-sdk-core (~> 3, >= 3.61.1)
|
||||
aws-sdk-kms (1.27.0)
|
||||
aws-sdk-core (~> 3, >= 3.71.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.48.0)
|
||||
aws-sdk-core (~> 3, >= 3.61.1)
|
||||
aws-sdk-s3 (1.60.1)
|
||||
aws-sdk-core (~> 3, >= 3.83.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.1.0)
|
||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||
bcrypt (3.1.12)
|
||||
benchmark-ips (2.7.2)
|
||||
better_errors (2.5.1)
|
||||
coderay (>= 1.0.0)
|
||||
erubi (>= 1.0.0)
|
||||
rack (>= 0.9.0)
|
||||
binding_of_caller (0.8.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
blurhash (0.1.3)
|
||||
blurhash (0.1.4)
|
||||
ffi (~> 1.10.0)
|
||||
bootsnap (1.4.5)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (4.6.1)
|
||||
browser (2.6.1)
|
||||
builder (3.2.3)
|
||||
bullet (6.0.2)
|
||||
brakeman (4.7.2)
|
||||
browser (3.0.3)
|
||||
builder (3.2.4)
|
||||
bullet (6.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
bundler-audit (0.6.1)
|
||||
|
@ -153,12 +139,12 @@ GEM
|
|||
capistrano-rails (1.4.0)
|
||||
capistrano (~> 3.1)
|
||||
capistrano-bundler (~> 1.1)
|
||||
capistrano-rbenv (2.1.4)
|
||||
capistrano-rbenv (2.1.6)
|
||||
capistrano (~> 3.1)
|
||||
sshkit (~> 1.3)
|
||||
capistrano-yarn (2.0.2)
|
||||
capistrano (~> 3.0)
|
||||
capybara (3.29.0)
|
||||
capybara (3.30.0)
|
||||
addressable
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (~> 1.8)
|
||||
|
@ -168,14 +154,14 @@ GEM
|
|||
xpath (~> 3.2)
|
||||
case_transform (0.2)
|
||||
activesupport
|
||||
charlock_holmes (0.7.6)
|
||||
charlock_holmes (0.7.7)
|
||||
chewy (5.1.0)
|
||||
activesupport (>= 4.0)
|
||||
elasticsearch (>= 2.0.0)
|
||||
elasticsearch-dsl
|
||||
chunky_png (1.3.10)
|
||||
cld3 (3.2.4)
|
||||
ffi (>= 1.1.0, < 1.11.0)
|
||||
chunky_png (1.3.11)
|
||||
cld3 (3.2.6)
|
||||
ffi (>= 1.1.0, < 1.12.0)
|
||||
climate_control (0.2.0)
|
||||
cocaine (0.5.8)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
|
@ -184,19 +170,10 @@ GEM
|
|||
connection_pool (2.2.2)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.4)
|
||||
css_parser (1.7.0)
|
||||
crass (1.0.6)
|
||||
css_parser (1.7.1)
|
||||
addressable
|
||||
debug_inspector (0.0.3)
|
||||
derailed_benchmarks (1.4.0)
|
||||
benchmark-ips (~> 2)
|
||||
get_process_mem (~> 0)
|
||||
heapy (~> 0)
|
||||
memory_profiler (~> 0)
|
||||
rack (>= 1)
|
||||
rake (> 10, < 13)
|
||||
ruby-statistics (>= 2.1)
|
||||
thor (~> 0.19)
|
||||
devise (4.7.1)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
|
@ -216,14 +193,15 @@ GEM
|
|||
discard (1.1.0)
|
||||
activerecord (>= 4.2, < 7)
|
||||
docile (1.3.2)
|
||||
domain_name (0.5.20180417)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (5.2.1)
|
||||
doorkeeper (5.2.3)
|
||||
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.3.0)
|
||||
elasticsearch-api (= 7.3.0)
|
||||
elasticsearch-transport (= 7.3.0)
|
||||
|
@ -235,18 +213,21 @@ GEM
|
|||
multi_json
|
||||
encryptor (3.0.0)
|
||||
equatable (0.6.1)
|
||||
erubi (1.8.0)
|
||||
erubi (1.9.0)
|
||||
et-orbi (1.1.6)
|
||||
tzinfo
|
||||
excon (0.62.0)
|
||||
fabrication (2.20.2)
|
||||
faker (2.5.0)
|
||||
i18n (~> 1.6.0)
|
||||
faraday (0.15.4)
|
||||
excon (0.71.0)
|
||||
fabrication (2.21.0)
|
||||
faker (2.10.1)
|
||||
i18n (>= 1.6, < 2)
|
||||
faraday (1.0.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
fast_blank (1.0.0)
|
||||
fastimage (2.1.7)
|
||||
ffi (1.10.0)
|
||||
ffi-compiler (1.0.1)
|
||||
ffi (>= 1.0.0)
|
||||
rake
|
||||
fog-core (2.1.0)
|
||||
builder
|
||||
excon (~> 0.58)
|
||||
|
@ -263,20 +244,18 @@ GEM
|
|||
fugit (1.1.6)
|
||||
et-orbi (~> 1.1, >= 1.1.6)
|
||||
raabro (~> 1.1)
|
||||
fuubar (2.4.1)
|
||||
fuubar (2.5.0)
|
||||
rspec-core (~> 3.0)
|
||||
ruby-progressbar (~> 1.4)
|
||||
get_process_mem (0.2.4)
|
||||
ffi (~> 1.0)
|
||||
globalid (0.4.2)
|
||||
activesupport (>= 4.2.0)
|
||||
goldfinger (2.1.0)
|
||||
goldfinger (2.1.1)
|
||||
addressable (~> 2.5)
|
||||
http (~> 3.0)
|
||||
http (~> 4.0)
|
||||
nokogiri (~> 1.8)
|
||||
oj (~> 3.0)
|
||||
hamlit (2.9.3)
|
||||
temple (>= 0.8.0)
|
||||
hamlit (2.11.0)
|
||||
temple (>= 0.8.2)
|
||||
thor
|
||||
tilt
|
||||
hamlit-rails (0.2.3)
|
||||
|
@ -288,28 +267,29 @@ GEM
|
|||
concurrent-ruby (~> 1.0)
|
||||
hashdiff (1.0.0)
|
||||
hashie (3.6.0)
|
||||
heapy (0.1.4)
|
||||
highline (2.0.1)
|
||||
highline (2.0.3)
|
||||
hiredis (0.6.3)
|
||||
hkdf (0.3.0)
|
||||
html2text (0.2.1)
|
||||
nokogiri (~> 1.6)
|
||||
htmlentities (4.3.4)
|
||||
http (3.3.0)
|
||||
http (4.3.0)
|
||||
addressable (~> 2.3)
|
||||
http-cookie (~> 1.0)
|
||||
http-form_data (~> 2.0)
|
||||
http_parser.rb (~> 0.6.0)
|
||||
http-form_data (~> 2.2)
|
||||
http-parser (~> 1.2.0)
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
http-form_data (2.1.1)
|
||||
http-form_data (2.2.0)
|
||||
http-parser (1.2.1)
|
||||
ffi-compiler (>= 1.0, < 2.0)
|
||||
http_accept_language (2.1.1)
|
||||
httplog (1.3.2)
|
||||
httplog (1.4.2)
|
||||
rack (>= 1.0)
|
||||
rainbow (>= 2.0.0)
|
||||
i18n (1.6.0)
|
||||
i18n (1.8.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-tasks (0.9.29)
|
||||
i18n-tasks (0.9.30)
|
||||
activesupport (>= 4.0.2)
|
||||
ast (>= 2.1.0)
|
||||
erubi
|
||||
|
@ -322,11 +302,18 @@ GEM
|
|||
idn-ruby (0.1.0)
|
||||
ipaddress (0.8.3)
|
||||
iso-639 (0.2.8)
|
||||
jaro_winkler (1.5.3)
|
||||
jaro_winkler (1.5.4)
|
||||
jmespath (1.4.0)
|
||||
json (2.2.0)
|
||||
json-canonicalization (0.1.0)
|
||||
json-ld-preloaded (3.0.4)
|
||||
json (2.3.0)
|
||||
json-canonicalization (0.2.0)
|
||||
json-ld (3.1.0)
|
||||
htmlentities (~> 4.3)
|
||||
json-canonicalization (~> 0.1)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
multi_json (~> 1.14)
|
||||
rack (~> 2.0)
|
||||
rdf (~> 3.1)
|
||||
json-ld-preloaded (3.0.6)
|
||||
json-ld (~> 3.0)
|
||||
multi_json (~> 1.12)
|
||||
rdf (~> 3.0)
|
||||
|
@ -358,7 +345,7 @@ GEM
|
|||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.2.3)
|
||||
loofah (2.4.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
|
@ -371,26 +358,26 @@ GEM
|
|||
redis (>= 3.0.5)
|
||||
memory_profiler (0.9.14)
|
||||
method_source (0.9.2)
|
||||
microformats (4.1.0)
|
||||
json (~> 2.1)
|
||||
nokogiri (~> 1.8, >= 1.8.3)
|
||||
mime-types (3.3)
|
||||
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.0904)
|
||||
mime-types-data (3.2019.1009)
|
||||
mimemagic (0.3.3)
|
||||
mini_mime (1.0.2)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.12.0)
|
||||
minitest (5.14.0)
|
||||
msgpack (1.3.1)
|
||||
multi_json (1.13.1)
|
||||
multi_json (1.14.1)
|
||||
multipart-post (2.1.1)
|
||||
necromancer (0.5.0)
|
||||
net-ldap (0.16.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)
|
||||
nio4r (2.5.1)
|
||||
nokogiri (1.10.4)
|
||||
nio4r (2.5.2)
|
||||
nokogiri (1.10.7)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
nokogumbo (2.0.1)
|
||||
nokogiri (~> 1.8, >= 1.8.4)
|
||||
|
@ -399,7 +386,7 @@ GEM
|
|||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sidekiq (>= 3.5)
|
||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||
oj (3.9.1)
|
||||
oj (3.10.1)
|
||||
omniauth (1.9.0)
|
||||
hashie (>= 3.4.6, < 3.7.0)
|
||||
rack (>= 1.6.2, < 3)
|
||||
|
@ -411,11 +398,7 @@ GEM
|
|||
omniauth (~> 1.3, >= 1.3.2)
|
||||
ruby-saml (~> 1.7)
|
||||
orm_adapter (0.5.0)
|
||||
ostatus2 (2.0.3)
|
||||
addressable (~> 2.5)
|
||||
http (~> 3.0)
|
||||
nokogiri (~> 1.8)
|
||||
ox (2.11.0)
|
||||
ox (2.12.1)
|
||||
paperclip (6.0.0)
|
||||
activemodel (>= 4.2.0)
|
||||
activesupport (>= 4.2.0)
|
||||
|
@ -425,19 +408,19 @@ GEM
|
|||
paperclip-av-transcoder (0.6.4)
|
||||
av (~> 0.9.0)
|
||||
paperclip (>= 2.5.2)
|
||||
parallel (1.17.0)
|
||||
parallel_tests (2.29.2)
|
||||
parallel (1.19.1)
|
||||
parallel_tests (2.30.1)
|
||||
parallel
|
||||
parser (2.6.4.0)
|
||||
parser (2.7.0.2)
|
||||
ast (~> 2.4.0)
|
||||
parslet (1.8.2)
|
||||
pastel (0.7.3)
|
||||
equatable (~> 0.6)
|
||||
tty-color (~> 0.5)
|
||||
pg (1.1.4)
|
||||
pghero (2.3.0)
|
||||
pg (1.2.2)
|
||||
pghero (2.4.1)
|
||||
activerecord (>= 5)
|
||||
pkg-config (1.3.9)
|
||||
pkg-config (1.4.0)
|
||||
premailer (1.11.1)
|
||||
addressable
|
||||
css_parser (>= 1.6.0)
|
||||
|
@ -454,34 +437,35 @@ GEM
|
|||
pry (~> 0.10)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.1)
|
||||
puma (4.2.0)
|
||||
public_suffix (4.0.3)
|
||||
puma (4.3.1)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.1.6)
|
||||
rack (2.0.7)
|
||||
rack-attack (6.1.0)
|
||||
rack (2.1.2)
|
||||
rack-attack (6.2.2)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-cors (1.0.3)
|
||||
rack-protection (2.0.5)
|
||||
rack-cors (1.1.1)
|
||||
rack (>= 2.0.0)
|
||||
rack-protection (2.0.7)
|
||||
rack
|
||||
rack-proxy (0.6.5)
|
||||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rails (5.2.3)
|
||||
actioncable (= 5.2.3)
|
||||
actionmailer (= 5.2.3)
|
||||
actionpack (= 5.2.3)
|
||||
actionview (= 5.2.3)
|
||||
activejob (= 5.2.3)
|
||||
activemodel (= 5.2.3)
|
||||
activerecord (= 5.2.3)
|
||||
activestorage (= 5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
rails (5.2.4.1)
|
||||
actioncable (= 5.2.4.1)
|
||||
actionmailer (= 5.2.4.1)
|
||||
actionpack (= 5.2.4.1)
|
||||
actionview (= 5.2.4.1)
|
||||
activejob (= 5.2.4.1)
|
||||
activemodel (= 5.2.4.1)
|
||||
activerecord (= 5.2.4.1)
|
||||
activestorage (= 5.2.4.1)
|
||||
activesupport (= 5.2.4.1)
|
||||
bundler (>= 1.3.0)
|
||||
railties (= 5.2.3)
|
||||
railties (= 5.2.4.1)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-controller-testing (1.0.4)
|
||||
actionpack (>= 5.0.1.x)
|
||||
|
@ -490,26 +474,26 @@ GEM
|
|||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.2.0)
|
||||
loofah (~> 2.2, >= 2.2.2)
|
||||
rails-html-sanitizer (1.3.0)
|
||||
loofah (~> 2.3)
|
||||
rails-i18n (5.1.3)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 5.0, < 6)
|
||||
rails-settings-cached (0.6.6)
|
||||
rails (>= 4.2.0)
|
||||
railties (5.2.3)
|
||||
actionpack (= 5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
railties (5.2.4.1)
|
||||
actionpack (= 5.2.4.1)
|
||||
activesupport (= 5.2.4.1)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.19.0, < 2.0)
|
||||
rainbow (3.0.0)
|
||||
rake (12.3.3)
|
||||
rdf (3.0.12)
|
||||
rake (13.0.1)
|
||||
rdf (3.1.1)
|
||||
hamster (~> 3.0)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
rdf-normalize (0.3.3)
|
||||
rdf (>= 2.2, < 4.0)
|
||||
rdf-normalize (0.4.0)
|
||||
rdf (~> 3.1)
|
||||
redcarpet (3.4.0)
|
||||
redis (4.1.3)
|
||||
redis-actionpack (5.0.2)
|
||||
|
@ -519,7 +503,7 @@ GEM
|
|||
redis-activesupport (5.0.4)
|
||||
activesupport (>= 3, < 6)
|
||||
redis-store (>= 1.3, < 2)
|
||||
redis-namespace (1.6.0)
|
||||
redis-namespace (1.7.0)
|
||||
redis (>= 3.0.4)
|
||||
redis-rack (2.0.4)
|
||||
rack (>= 1.5, < 3)
|
||||
|
@ -531,49 +515,50 @@ GEM
|
|||
redis-store (1.5.0)
|
||||
redis (>= 2.2, < 5)
|
||||
regexp_parser (1.6.0)
|
||||
request_store (1.4.1)
|
||||
request_store (1.5.0)
|
||||
rack (>= 1.4)
|
||||
responders (3.0.0)
|
||||
actionpack (>= 5.0)
|
||||
railties (>= 5.0)
|
||||
rotp (2.1.2)
|
||||
rpam2 (4.0.2)
|
||||
rqrcode (0.10.1)
|
||||
rqrcode (1.1.2)
|
||||
chunky_png (~> 1.0)
|
||||
rspec-core (3.8.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-expectations (3.8.2)
|
||||
rqrcode_core (~> 0.1)
|
||||
rqrcode_core (0.1.1)
|
||||
rspec-core (3.9.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-expectations (3.9.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-mocks (3.8.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-mocks (3.9.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-rails (3.8.2)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-rails (3.9.0)
|
||||
actionpack (>= 3.0)
|
||||
activesupport (>= 3.0)
|
||||
railties (>= 3.0)
|
||||
rspec-core (~> 3.8.0)
|
||||
rspec-expectations (~> 3.8.0)
|
||||
rspec-mocks (~> 3.8.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-core (~> 3.9.0)
|
||||
rspec-expectations (~> 3.9.0)
|
||||
rspec-mocks (~> 3.9.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-sidekiq (3.0.3)
|
||||
rspec-core (~> 3.0, >= 3.0.0)
|
||||
sidekiq (>= 2.4.0)
|
||||
rspec-support (3.8.0)
|
||||
rubocop (0.74.0)
|
||||
rspec-support (3.9.0)
|
||||
rubocop (0.79.0)
|
||||
jaro_winkler (~> 1.5.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.6)
|
||||
parser (>= 2.7.0.1)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 1.7)
|
||||
rubocop-rails (2.3.2)
|
||||
rubocop-rails (2.4.1)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 0.72.0)
|
||||
ruby-progressbar (1.10.1)
|
||||
ruby-saml (1.9.0)
|
||||
nokogiri (>= 1.5.10)
|
||||
ruby-statistics (2.1.1)
|
||||
rufus-scheduler (3.5.2)
|
||||
fugit (~> 1.1, >= 1.1.5)
|
||||
safe_yaml (1.0.5)
|
||||
|
@ -593,13 +578,13 @@ GEM
|
|||
rufus-scheduler (~> 3.2)
|
||||
sidekiq (>= 3)
|
||||
tilt (>= 1.4.0)
|
||||
sidekiq-unique-jobs (6.0.13)
|
||||
sidekiq-unique-jobs (6.0.18)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||
sidekiq (>= 4.0, < 7.0)
|
||||
thor (~> 0)
|
||||
simple-navigation (4.1.0)
|
||||
activesupport (>= 2.3.2)
|
||||
simple_form (4.1.0)
|
||||
simple_form (5.0.1)
|
||||
actionpack (>= 5.0)
|
||||
activemodel (>= 5.0)
|
||||
simplecov (0.17.1)
|
||||
|
@ -617,62 +602,63 @@ GEM
|
|||
sshkit (1.20.0)
|
||||
net-scp (>= 1.1.2)
|
||||
net-ssh (>= 2.8.0)
|
||||
stackprof (0.2.12)
|
||||
stackprof (0.2.15)
|
||||
statsd-ruby (1.4.0)
|
||||
stoplight (2.1.3)
|
||||
stoplight (2.2.0)
|
||||
streamio-ffmpeg (3.0.2)
|
||||
multi_json (~> 1.8)
|
||||
strong_migrations (0.4.1)
|
||||
strong_migrations (0.5.1)
|
||||
activerecord (>= 5)
|
||||
temple (0.8.1)
|
||||
temple (0.8.2)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
terrapin (0.6.0)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
thor (0.20.3)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.9)
|
||||
thwait (0.1.0)
|
||||
tilt (2.0.10)
|
||||
tty-color (0.5.0)
|
||||
tty-command (0.9.0)
|
||||
pastel (~> 0.7.0)
|
||||
tty-cursor (0.7.0)
|
||||
tty-prompt (0.19.0)
|
||||
tty-prompt (0.20.0)
|
||||
necromancer (~> 0.5.0)
|
||||
pastel (~> 0.7.0)
|
||||
tty-reader (~> 0.6.0)
|
||||
tty-reader (0.6.0)
|
||||
tty-reader (~> 0.7.0)
|
||||
tty-reader (0.7.0)
|
||||
tty-cursor (~> 0.7)
|
||||
tty-screen (~> 0.7)
|
||||
wisper (~> 2.0.0)
|
||||
tty-screen (0.7.0)
|
||||
twitter-text (1.14.7)
|
||||
unf (~> 0.1.0)
|
||||
tzinfo (1.2.5)
|
||||
tzinfo (1.2.6)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo-data (1.2019.3)
|
||||
tzinfo (>= 1.0.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.5)
|
||||
unicode-display_width (1.6.0)
|
||||
uniform_notifier (1.12.1)
|
||||
unf_ext (0.0.7.6)
|
||||
unicode-display_width (1.6.1)
|
||||
uniform_notifier (1.13.0)
|
||||
warden (1.2.8)
|
||||
rack (>= 2.0.6)
|
||||
webmock (3.7.6)
|
||||
webmock (3.8.0)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webpacker (4.0.7)
|
||||
webpacker (4.2.2)
|
||||
activesupport (>= 4.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 4.2)
|
||||
webpush (0.3.8)
|
||||
hkdf (~> 0.2)
|
||||
jwt (~> 2.0)
|
||||
websocket-driver (0.7.0)
|
||||
websocket-driver (0.7.1)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.3)
|
||||
wisper (2.0.0)
|
||||
websocket-extensions (0.1.4)
|
||||
wisper (2.0.1)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
|
||||
|
@ -681,57 +667,57 @@ PLATFORMS
|
|||
|
||||
DEPENDENCIES
|
||||
active_model_serializers (~> 0.10)
|
||||
active_record_query_trace (~> 1.6)
|
||||
active_record_query_trace (~> 1.7)
|
||||
addressable (~> 2.7)
|
||||
annotate (~> 2.7)
|
||||
aws-sdk-s3 (~> 1.48)
|
||||
annotate (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.60)
|
||||
better_errors (~> 2.5)
|
||||
binding_of_caller (~> 0.7)
|
||||
blurhash (~> 0.1)
|
||||
bootsnap (~> 1.4)
|
||||
brakeman (~> 4.6)
|
||||
brakeman (~> 4.7)
|
||||
browser
|
||||
bullet (~> 6.0)
|
||||
bullet (~> 6.1)
|
||||
bundler-audit (~> 0.6)
|
||||
capistrano (~> 3.11)
|
||||
capistrano-rails (~> 1.4)
|
||||
capistrano-rbenv (~> 2.1)
|
||||
capistrano-yarn (~> 2.0)
|
||||
capybara (~> 3.29)
|
||||
charlock_holmes (~> 0.7.6)
|
||||
capybara (~> 3.30)
|
||||
charlock_holmes (~> 0.7.7)
|
||||
chewy (~> 5.1)
|
||||
cld3 (~> 3.2.4)
|
||||
cld3 (~> 3.2.6)
|
||||
climate_control (~> 0.2)
|
||||
concurrent-ruby
|
||||
connection_pool
|
||||
derailed_benchmarks
|
||||
devise (~> 4.7)
|
||||
devise-two-factor (~> 3.1)
|
||||
devise_pam_authenticatable2 (~> 9.2)
|
||||
discard (~> 1.1)
|
||||
doorkeeper (~> 5.2)
|
||||
dotenv-rails (~> 2.7)
|
||||
fabrication (~> 2.20)
|
||||
faker (~> 2.5)
|
||||
e2mmap (~> 0.1.0)
|
||||
fabrication (~> 2.21)
|
||||
faker (~> 2.10)
|
||||
fast_blank (~> 1.0)
|
||||
fastimage
|
||||
fog-core (<= 2.1.0)
|
||||
fog-openstack (~> 0.3)
|
||||
fuubar (~> 2.4)
|
||||
fuubar (~> 2.5)
|
||||
goldfinger (~> 2.1)
|
||||
hamlit-rails (~> 0.2)
|
||||
health_check!
|
||||
hiredis (~> 0.6)
|
||||
html2text
|
||||
htmlentities (~> 4.3)
|
||||
http (~> 3.3)
|
||||
http (~> 4.3)
|
||||
http_accept_language (~> 2.1)
|
||||
http_parser.rb (~> 0.6)!
|
||||
httplog (~> 1.3)
|
||||
httplog (~> 1.4.2)
|
||||
i18n-tasks (~> 0.9)
|
||||
idn-ruby
|
||||
iso-639
|
||||
json-ld!
|
||||
json-ld
|
||||
json-ld-preloaded (~> 3.0)
|
||||
kaminari (~> 1.1)
|
||||
letter_opener (~> 1.7)
|
||||
|
@ -741,49 +727,49 @@ DEPENDENCIES
|
|||
makara (~> 0.4)
|
||||
mario-redis-lock (~> 1.2)
|
||||
memory_profiler
|
||||
microformats (~> 4.1)
|
||||
mime-types (~> 3.3)
|
||||
net-ldap (~> 0.10)
|
||||
microformats (~> 4.2)
|
||||
mime-types (~> 3.3.1)
|
||||
net-ldap (~> 0.16)
|
||||
nilsimsa!
|
||||
nokogiri (~> 1.10)
|
||||
nsa (~> 0.2)
|
||||
oj (~> 3.9)
|
||||
oj (~> 3.10)
|
||||
omniauth (~> 1.9)
|
||||
omniauth-cas (~> 1.1)
|
||||
omniauth-saml (~> 1.10)
|
||||
ostatus2 (~> 2.0)
|
||||
ox (~> 2.11)
|
||||
ox (~> 2.12)
|
||||
paperclip (~> 6.0)
|
||||
paperclip-av-transcoder (~> 0.6)
|
||||
parallel (~> 1.17)
|
||||
parallel_tests (~> 2.29)
|
||||
parallel (~> 1.19)
|
||||
parallel_tests (~> 2.30)
|
||||
parslet
|
||||
pg (~> 1.1)
|
||||
pghero (~> 2.3)
|
||||
pkg-config (~> 1.3)
|
||||
pg (~> 1.2)
|
||||
pghero (~> 2.4)
|
||||
pkg-config (~> 1.4)
|
||||
posix-spawn!
|
||||
premailer-rails
|
||||
private_address_check (~> 0.5)
|
||||
pry-byebug (~> 3.7)
|
||||
pry-rails (~> 0.3)
|
||||
puma (~> 4.2)
|
||||
puma (~> 4.3)
|
||||
pundit (~> 2.1)
|
||||
rack-attack (~> 6.1)
|
||||
rack-cors (~> 1.0)
|
||||
rails (~> 5.2.3)
|
||||
rack (~> 2.1.2)
|
||||
rack-attack (~> 6.2)
|
||||
rack-cors (~> 1.1)
|
||||
rails (~> 5.2.4)
|
||||
rails-controller-testing (~> 1.0)
|
||||
rails-i18n (~> 5.1)
|
||||
rails-settings-cached (~> 0.6)
|
||||
rdf-normalize (~> 0.3)
|
||||
rdf-normalize (~> 0.4)
|
||||
redcarpet (~> 3.4)
|
||||
redis (~> 4.1)
|
||||
redis-namespace (~> 1.5)
|
||||
redis-namespace (~> 1.7)
|
||||
redis-rails (~> 5.0)
|
||||
rqrcode (~> 0.10)
|
||||
rspec-rails (~> 3.8)
|
||||
rqrcode (~> 1.1)
|
||||
rspec-rails (~> 3.9)
|
||||
rspec-sidekiq (~> 3.0)
|
||||
rubocop (~> 0.74)
|
||||
rubocop-rails (~> 2.3)
|
||||
rubocop (~> 0.79)
|
||||
rubocop-rails (~> 2.4)
|
||||
ruby-progressbar (~> 1.10)
|
||||
sanitize (~> 5.1)
|
||||
sidekiq (~> 5.2)
|
||||
|
@ -791,24 +777,20 @@ DEPENDENCIES
|
|||
sidekiq-scheduler (~> 3.0)
|
||||
sidekiq-unique-jobs (~> 6.0)
|
||||
simple-navigation (~> 4.1)
|
||||
simple_form (~> 4.1)
|
||||
simple_form (~> 5.0)
|
||||
simplecov (~> 0.17)
|
||||
sprockets (~> 3.7.2)
|
||||
sprockets-rails (~> 3.2)
|
||||
stackprof
|
||||
stoplight (~> 2.1.3)
|
||||
stoplight (~> 2.2.0)
|
||||
streamio-ffmpeg (~> 3.0)
|
||||
strong_migrations (~> 0.4)
|
||||
strong_migrations (~> 0.5)
|
||||
thor (~> 0.20)
|
||||
thwait (~> 0.1.0)
|
||||
tty-command (~> 0.9)
|
||||
tty-prompt (~> 0.19)
|
||||
tty-prompt (~> 0.20)
|
||||
twitter-text (~> 1.14)
|
||||
tzinfo-data (~> 1.2019)
|
||||
webmock (~> 3.7)
|
||||
webpacker (~> 4.0)
|
||||
webmock (~> 3.8)
|
||||
webpacker (~> 4.2)
|
||||
webpush
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.6.5p114
|
||||
|
||||
BUNDLED WITH
|
||||
1.17.3
|
||||
|
|
|
@ -12,7 +12,7 @@ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
|||
sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main'
|
||||
|
||||
# Add repo for NodeJS
|
||||
curl -sL https://deb.nodesource.com/setup_8.x | sudo bash -
|
||||
curl -sL https://deb.nodesource.com/setup_10.x | sudo bash -
|
||||
|
||||
# Add firewall rule to redirect 80 to PORT and save
|
||||
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{ENV["PORT"]}
|
||||
|
|
|
@ -7,6 +7,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
|||
|
||||
before_action :skip_unknown_actor_delete
|
||||
before_action :require_signature!
|
||||
skip_before_action :authenticate_user!
|
||||
|
||||
def create
|
||||
upgrade_account
|
||||
|
|
|
@ -109,21 +109,7 @@ module Admin
|
|||
end
|
||||
|
||||
def filter_params
|
||||
params.permit(
|
||||
:local,
|
||||
:remote,
|
||||
:by_domain,
|
||||
:active,
|
||||
:pending,
|
||||
:disabled,
|
||||
:silenced,
|
||||
:suspended,
|
||||
:username,
|
||||
:display_name,
|
||||
:email,
|
||||
:ip,
|
||||
:staff
|
||||
)
|
||||
params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::AnnouncementsController < Admin::BaseController
|
||||
before_action :set_announcements, only: :index
|
||||
before_action :set_announcement, except: [:index, :new, :create]
|
||||
|
||||
def index
|
||||
authorize :announcement, :index?
|
||||
end
|
||||
|
||||
def new
|
||||
authorize :announcement, :create?
|
||||
|
||||
@announcement = Announcement.new
|
||||
end
|
||||
|
||||
def create
|
||||
authorize :announcement, :create?
|
||||
|
||||
@announcement = Announcement.new(resource_params)
|
||||
|
||||
if @announcement.save
|
||||
PublishScheduledAnnouncementWorker.perform_async(@announcement.id) if @announcement.published?
|
||||
log_action :create, @announcement
|
||||
redirect_to admin_announcements_path, notice: @announcement.published? ? I18n.t('admin.announcements.published_msg') : I18n.t('admin.announcements.scheduled_msg')
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
authorize :announcement, :update?
|
||||
end
|
||||
|
||||
def update
|
||||
authorize :announcement, :update?
|
||||
|
||||
if @announcement.update(resource_params)
|
||||
PublishScheduledAnnouncementWorker.perform_async(@announcement.id) if @announcement.published?
|
||||
log_action :update, @announcement
|
||||
redirect_to admin_announcements_path, notice: I18n.t('admin.announcements.updated_msg')
|
||||
else
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
def publish
|
||||
authorize :announcement, :update?
|
||||
@announcement.publish!
|
||||
PublishScheduledAnnouncementWorker.perform_async(@announcement.id)
|
||||
log_action :update, @announcement
|
||||
redirect_to admin_announcements_path, notice: I18n.t('admin.announcements.published_msg')
|
||||
end
|
||||
|
||||
def unpublish
|
||||
authorize :announcement, :update?
|
||||
@announcement.unpublish!
|
||||
UnpublishAnnouncementWorker.perform_async(@announcement.id)
|
||||
log_action :update, @announcement
|
||||
redirect_to admin_announcements_path, notice: I18n.t('admin.announcements.unpublished_msg')
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize :announcement, :destroy?
|
||||
@announcement.destroy!
|
||||
UnpublishAnnouncementWorker.perform_async(@announcement.id) if @announcement.published?
|
||||
log_action :destroy, @announcement
|
||||
redirect_to admin_announcements_path, notice: I18n.t('admin.announcements.destroyed_msg')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_announcements
|
||||
@announcements = AnnouncementFilter.new(filter_params).results.page(params[:page])
|
||||
end
|
||||
|
||||
def set_announcement
|
||||
@announcement = Announcement.find(params[:id])
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.slice(*AnnouncementFilter::KEYS).permit(*AnnouncementFilter::KEYS)
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:announcement).permit(:text, :scheduled_at, :starts_at, :ends_at, :all_day)
|
||||
end
|
||||
end
|
|
@ -2,10 +2,6 @@
|
|||
|
||||
module Admin
|
||||
class CustomEmojisController < BaseController
|
||||
include ObfuscateFilename
|
||||
|
||||
obfuscate_filename [:custom_emoji, :image]
|
||||
|
||||
def index
|
||||
authorize :custom_emoji, :index?
|
||||
|
||||
|
@ -52,7 +48,7 @@ module Admin
|
|||
end
|
||||
|
||||
def filter_params
|
||||
params.slice(:local, :remote, :by_domain, :shortcode, :page).permit(:local, :remote, :by_domain, :shortcode, :page)
|
||||
params.slice(:page, *CustomEmojiFilter::KEYS).permit(:page, *CustomEmojiFilter::KEYS)
|
||||
end
|
||||
|
||||
def action_from_button
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class FollowersController < BaseController
|
||||
before_action :set_account
|
||||
|
||||
PER_PAGE = 40
|
||||
|
||||
def index
|
||||
authorize :account, :index?
|
||||
@followers = @account.followers.local.recent.page(params[:page]).per(PER_PAGE)
|
||||
end
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -62,7 +62,7 @@ module Admin
|
|||
end
|
||||
|
||||
def filter_params
|
||||
params.permit(:limited, :by_domain)
|
||||
params.slice(*InstanceFilter::KEYS).permit(*InstanceFilter::KEYS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -47,7 +47,7 @@ module Admin
|
|||
end
|
||||
|
||||
def filter_params
|
||||
params.permit(:available, :expired)
|
||||
params.slice(*InviteFilter::KEYS).permit(*InviteFilter::KEYS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class RelationshipsController < BaseController
|
||||
before_action :set_account
|
||||
|
||||
PER_PAGE = 40
|
||||
|
||||
def index
|
||||
authorize :account, :index?
|
||||
|
||||
@accounts = RelationshipFilter.new(@account, filter_params).results.page(params[:page]).per(PER_PAGE)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.slice(*RelationshipFilter::KEYS).permit(*RelationshipFilter::KEYS)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -52,11 +52,7 @@ module Admin
|
|||
end
|
||||
|
||||
def filter_params
|
||||
params.permit(
|
||||
:account_id,
|
||||
:resolved,
|
||||
:target_account_id
|
||||
)
|
||||
params.slice(*ReportFilter::KEYS).permit(*ReportFilter::KEYS)
|
||||
end
|
||||
|
||||
def set_report
|
||||
|
|
|
@ -73,7 +73,7 @@ module Admin
|
|||
end
|
||||
|
||||
def filter_params
|
||||
params.slice(:directory, :reviewed, :unreviewed, :pending_review, :page, :popular, :active, :name).permit(:directory, :reviewed, :unreviewed, :pending_review, :page, :popular, :active, :name)
|
||||
params.slice(:page, *TagFilter::KEYS).permit(:page, *TagFilter::KEYS)
|
||||
end
|
||||
|
||||
def tag_params
|
||||
|
|
|
@ -20,6 +20,10 @@ class Api::BaseController < ApplicationController
|
|||
render json: { error: e.to_s }, status: 422
|
||||
end
|
||||
|
||||
rescue_from ActiveRecord::RecordNotUnique do
|
||||
render json: { error: 'Duplicate record' }, status: 422
|
||||
end
|
||||
|
||||
rescue_from ActiveRecord::RecordNotFound do
|
||||
render json: { error: 'Record not found' }, status: 404
|
||||
end
|
||||
|
@ -81,7 +85,7 @@ class Api::BaseController < ApplicationController
|
|||
end
|
||||
|
||||
def require_authenticated_user!
|
||||
render json: { error: 'This API requires an authenticated user' }, status: 401 unless current_user
|
||||
render json: { error: 'This method requires an authenticated user' }, status: 401 unless current_user
|
||||
end
|
||||
|
||||
def require_user!
|
||||
|
|
|
@ -1,15 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::OEmbedController < Api::BaseController
|
||||
respond_to :json
|
||||
skip_before_action :require_authenticated_user!
|
||||
|
||||
before_action :set_status
|
||||
before_action :require_public_status!
|
||||
|
||||
def show
|
||||
@status = status_finder.status
|
||||
render json: @status, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_status
|
||||
@status = status_finder.status
|
||||
end
|
||||
|
||||
def require_public_status!
|
||||
not_found if @status.hidden?
|
||||
end
|
||||
|
||||
def status_finder
|
||||
StatusFinder.new(params[:url])
|
||||
end
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
class Api::ProofsController < Api::BaseController
|
||||
include AccountOwnedConcern
|
||||
|
||||
skip_before_action :require_authenticated_user!
|
||||
|
||||
before_action :set_provider
|
||||
|
||||
def index
|
||||
|
|
|
@ -25,7 +25,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
|
|||
end
|
||||
|
||||
def user_settings_params
|
||||
return nil unless params.key?(:source)
|
||||
return nil if params[:source].blank?
|
||||
|
||||
source_params = params.require(:source)
|
||||
|
||||
|
|
|
@ -21,11 +21,13 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
|
|||
def load_accounts
|
||||
return [] if hide_results?
|
||||
|
||||
default_accounts.merge(paginated_follows).to_a
|
||||
scope = default_accounts
|
||||
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
|
||||
scope.merge(paginated_follows).to_a
|
||||
end
|
||||
|
||||
def hide_results?
|
||||
(@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||
(@account.user_hides_network? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||
end
|
||||
|
||||
def default_accounts
|
||||
|
|
|
@ -21,11 +21,13 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
|
|||
def load_accounts
|
||||
return [] if hide_results?
|
||||
|
||||
default_accounts.merge(paginated_follows).to_a
|
||||
scope = default_accounts
|
||||
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
|
||||
scope.merge(paginated_follows).to_a
|
||||
end
|
||||
|
||||
def hide_results?
|
||||
(@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||
(@account.user_hides_network? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||
end
|
||||
|
||||
def default_accounts
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Announcements::ReactionsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:favourites' }
|
||||
before_action :require_user!
|
||||
|
||||
before_action :set_announcement
|
||||
before_action :set_reaction, except: :update
|
||||
|
||||
def update
|
||||
@announcement.announcement_reactions.create!(account: current_account, name: params[:id])
|
||||
render_empty
|
||||
end
|
||||
|
||||
def destroy
|
||||
@reaction.destroy!
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_reaction
|
||||
@reaction = @announcement.announcement_reactions.where(account: current_account).find_by!(name: params[:id])
|
||||
end
|
||||
|
||||
def set_announcement
|
||||
@announcement = Announcement.published.find(params[:announcement_id])
|
||||
end
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::AnnouncementsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: :dismiss
|
||||
before_action :require_user!
|
||||
before_action :set_announcements, only: :index
|
||||
before_action :set_announcement, except: :index
|
||||
|
||||
def index
|
||||
render json: @announcements, each_serializer: REST::AnnouncementSerializer
|
||||
end
|
||||
|
||||
def dismiss
|
||||
AnnouncementMute.create!(account: current_account, announcement: @announcement)
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_announcements
|
||||
@announcements = begin
|
||||
Announcement.published.chronological
|
||||
end
|
||||
end
|
||||
|
||||
def set_announcement
|
||||
@announcement = Announcement.published.find(params[:id])
|
||||
end
|
||||
end
|
|
@ -26,10 +26,9 @@ class Api::V1::BookmarksController < Api::BaseController
|
|||
end
|
||||
|
||||
def results
|
||||
@_results ||= account_bookmarks.paginate_by_max_id(
|
||||
@_results ||= account_bookmarks.paginate_by_id(
|
||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||
params[:max_id],
|
||||
params[:since_id]
|
||||
params_slice(:max_id, :since_id, :min_id)
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -42,15 +41,11 @@ class Api::V1::BookmarksController < Api::BaseController
|
|||
end
|
||||
|
||||
def next_path
|
||||
if records_continue?
|
||||
api_v1_bookmarks_url pagination_params(max_id: pagination_max_id)
|
||||
end
|
||||
api_v1_bookmarks_url pagination_params(max_id: pagination_max_id) if records_continue?
|
||||
end
|
||||
|
||||
def prev_path
|
||||
unless results.empty?
|
||||
api_v1_bookmarks_url pagination_params(since_id: pagination_since_id)
|
||||
end
|
||||
api_v1_bookmarks_url pagination_params(min_id: pagination_since_id) unless results.empty?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
|
|
|
@ -4,9 +4,6 @@ class Api::V1::MediaController < Api::BaseController
|
|||
before_action -> { doorkeeper_authorize! :write, :'write:media' }
|
||||
before_action :require_user!
|
||||
|
||||
include ObfuscateFilename
|
||||
obfuscate_filename :file
|
||||
|
||||
respond_to :json
|
||||
|
||||
def create
|
||||
|
|
|
@ -51,6 +51,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
|||
|
||||
def data_params
|
||||
return {} if params[:data].blank?
|
||||
params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention, :poll])
|
||||
params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,7 +17,9 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
|
|||
private
|
||||
|
||||
def load_accounts
|
||||
default_accounts.merge(paginated_favourites).to_a
|
||||
scope = default_accounts
|
||||
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
|
||||
scope.merge(paginated_favourites).to_a
|
||||
end
|
||||
|
||||
def default_accounts
|
||||
|
|
|
@ -17,7 +17,9 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
|
|||
private
|
||||
|
||||
def load_accounts
|
||||
default_accounts.merge(paginated_statuses).to_a
|
||||
scope = default_accounts
|
||||
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
|
||||
scope.merge(paginated_statuses).to_a
|
||||
end
|
||||
|
||||
def default_accounts
|
||||
|
|
|
@ -7,15 +7,21 @@ class Api::Web::EmbedsController < Api::Web::BaseController
|
|||
|
||||
def create
|
||||
status = StatusFinder.new(params[:url]).status
|
||||
|
||||
return not_found if status.hidden?
|
||||
|
||||
render json: status, serializer: OEmbedSerializer, width: 400
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
oembed = FetchOEmbedService.new.call(params[:url])
|
||||
oembed[:html] = Formatter.instance.sanitize(oembed[:html], Sanitize::Config::MASTODON_OEMBED) if oembed[:html].present?
|
||||
|
||||
if oembed
|
||||
render json: oembed
|
||||
else
|
||||
render json: {}, status: :not_found
|
||||
return not_found if oembed.nil?
|
||||
|
||||
begin
|
||||
oembed[:html] = Formatter.instance.sanitize(oembed[:html], Sanitize::Config::MASTODON_OEMBED)
|
||||
rescue ArgumentError
|
||||
return not_found
|
||||
end
|
||||
|
||||
render json: oembed
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,6 +19,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||
data = {
|
||||
alerts: {
|
||||
follow: alerts_enabled,
|
||||
follow_request: false,
|
||||
favourite: alerts_enabled,
|
||||
reblog: alerts_enabled,
|
||||
mention: alerts_enabled,
|
||||
|
@ -58,6 +59,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||
end
|
||||
|
||||
def data_params
|
||||
@data_params ||= params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention, :poll])
|
||||
@data_params ||= params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,6 +25,7 @@ class ApplicationController < ActionController::Base
|
|||
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
|
||||
rescue_from ActionController::UnknownFormat, with: :not_acceptable
|
||||
rescue_from ActionController::ParameterMissing, with: :bad_request
|
||||
rescue_from Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request
|
||||
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
||||
rescue_from Mastodon::NotPermittedError, with: :forbidden
|
||||
rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
|
||||
|
@ -212,12 +213,11 @@ class ApplicationController < ActionController::Base
|
|||
|
||||
def respond_with_error(code)
|
||||
respond_to do |format|
|
||||
format.any { head code }
|
||||
|
||||
format.html do
|
||||
format.any do
|
||||
use_pack 'error'
|
||||
render "errors/#{code}", layout: 'error', status: code
|
||||
render "errors/#{code}", layout: 'error', status: code, formats: [:html]
|
||||
end
|
||||
format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,12 @@ class Auth::PasswordsController < Devise::PasswordsController
|
|||
|
||||
layout 'auth'
|
||||
|
||||
def update
|
||||
super do |resource|
|
||||
resource.session_activations.destroy_all if resource.errors.empty?
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_validity_of_reset_password_token
|
||||
|
|
|
@ -11,6 +11,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
before_action :set_instance_presenter, only: [:new, :create, :update]
|
||||
before_action :set_body_classes, only: [:new, :create, :edit, :update]
|
||||
before_action :require_not_suspended!, only: [:update]
|
||||
before_action :set_cache_headers, only: [:edit, :update]
|
||||
|
||||
skip_before_action :require_functional!, only: [:edit, :update]
|
||||
|
||||
|
@ -22,10 +23,17 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
not_found
|
||||
end
|
||||
|
||||
def update
|
||||
super do |resource|
|
||||
resource.clear_other_sessions(current_session.session_id) if resource.saved_change_to_encrypted_password?
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def update_resource(resource, params)
|
||||
params[:password] = nil if Devise.pam_authentication && resource.encrypted_password.blank?
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
|
@ -114,4 +122,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
def require_not_suspended!
|
||||
forbidden if current_account.suspended?
|
||||
end
|
||||
|
||||
def set_cache_headers
|
||||
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ObfuscateFilename
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
def obfuscate_filename(path)
|
||||
before_action do
|
||||
file = params.dig(*path)
|
||||
next if file.nil?
|
||||
|
||||
file.original_filename = SecureRandom.hex(8) + File.extname(file.original_filename)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -160,6 +160,8 @@ module SignatureVerification
|
|||
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) }
|
||||
account
|
||||
end
|
||||
rescue Mastodon::HostValidationError
|
||||
nil
|
||||
end
|
||||
|
||||
def stoplight_wrap_request(&block)
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FiltersController < ApplicationController
|
||||
include Authorization
|
||||
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_filters, only: :index
|
||||
before_action :set_filter, only: [:edit, :update, :destroy]
|
||||
before_action :set_pack
|
||||
|
|
|
@ -19,7 +19,6 @@ class FollowerAccountsController < ApplicationController
|
|||
next if @account.user_hides_network?
|
||||
|
||||
follows
|
||||
@relationships = AccountRelationshipsPresenter.new(follows.map(&:account_id), current_user.account_id) if user_signed_in?
|
||||
end
|
||||
|
||||
format.json do
|
||||
|
@ -38,7 +37,11 @@ class FollowerAccountsController < ApplicationController
|
|||
private
|
||||
|
||||
def follows
|
||||
@follows ||= Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
|
||||
return @follows if defined?(@follows)
|
||||
|
||||
scope = Follow.where(target_account: @account)
|
||||
scope = scope.where.not(account_id: current_account.excluded_from_timeline_account_ids) if user_signed_in?
|
||||
@follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
|
||||
end
|
||||
|
||||
def page_requested?
|
||||
|
|
|
@ -19,7 +19,6 @@ class FollowingAccountsController < ApplicationController
|
|||
next if @account.user_hides_network?
|
||||
|
||||
follows
|
||||
@relationships = AccountRelationshipsPresenter.new(follows.map(&:target_account_id), current_user.account_id) if user_signed_in?
|
||||
end
|
||||
|
||||
format.json do
|
||||
|
@ -38,7 +37,11 @@ class FollowingAccountsController < ApplicationController
|
|||
private
|
||||
|
||||
def follows
|
||||
@follows ||= Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
|
||||
return @follows if defined?(@follows)
|
||||
|
||||
scope = Follow.where(account: @account)
|
||||
scope = scope.where.not(target_account_id: current_account.excluded_from_timeline_account_ids) if user_signed_in?
|
||||
@follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
|
||||
end
|
||||
|
||||
def page_requested?
|
||||
|
|
|
@ -6,6 +6,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
|||
before_action :store_current_location
|
||||
before_action :authenticate_resource_owner!
|
||||
before_action :set_pack
|
||||
before_action :set_cache_headers
|
||||
|
||||
include Localized
|
||||
|
||||
|
@ -32,4 +33,8 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
|||
def truthy_param?(key)
|
||||
ActiveModel::Type::Boolean.new.cast(params[key])
|
||||
end
|
||||
|
||||
def set_cache_headers
|
||||
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,53 +20,13 @@ class RelationshipsController < ApplicationController
|
|||
rescue ActionController::ParameterMissing
|
||||
# Do nothing
|
||||
ensure
|
||||
redirect_to relationships_path(current_params)
|
||||
redirect_to relationships_path(filter_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_accounts
|
||||
@accounts = relationships_scope.page(params[:page]).per(40)
|
||||
end
|
||||
|
||||
def relationships_scope
|
||||
scope = begin
|
||||
if following_relationship?
|
||||
current_account.following.eager_load(:account_stat).reorder(nil)
|
||||
else
|
||||
current_account.followers.eager_load(:account_stat).reorder(nil)
|
||||
end
|
||||
end
|
||||
|
||||
scope.merge!(Follow.recent) if params[:order].blank? || params[:order] == 'recent'
|
||||
scope.merge!(Account.by_recent_status) if params[:order] == 'active'
|
||||
scope.merge!(mutual_relationship_scope) if mutual_relationship?
|
||||
scope.merge!(moved_account_scope) if params[:status] == 'moved'
|
||||
scope.merge!(primary_account_scope) if params[:status] == 'primary'
|
||||
scope.merge!(by_domain_scope) if params[:by_domain].present?
|
||||
scope.merge!(dormant_account_scope) if params[:activity] == 'dormant'
|
||||
|
||||
scope
|
||||
end
|
||||
|
||||
def mutual_relationship_scope
|
||||
Account.where(id: current_account.following)
|
||||
end
|
||||
|
||||
def moved_account_scope
|
||||
Account.where.not(moved_to_account_id: nil)
|
||||
end
|
||||
|
||||
def primary_account_scope
|
||||
Account.where(moved_to_account_id: nil)
|
||||
end
|
||||
|
||||
def dormant_account_scope
|
||||
AccountStat.where(last_status_at: nil).or(AccountStat.where(AccountStat.arel_table[:last_status_at].lt(1.month.ago)))
|
||||
end
|
||||
|
||||
def by_domain_scope
|
||||
Account.where(domain: params[:by_domain])
|
||||
@accounts = RelationshipFilter.new(current_account, filter_params).results.page(params[:page]).per(40)
|
||||
end
|
||||
|
||||
def form_account_batch_params
|
||||
|
@ -85,8 +45,8 @@ class RelationshipsController < ApplicationController
|
|||
params[:relationship] == 'followed_by'
|
||||
end
|
||||
|
||||
def current_params
|
||||
params.slice(:page, :status, :relationship, :by_domain, :activity, :order).permit(:page, :status, :relationship, :by_domain, :activity, :order)
|
||||
def filter_params
|
||||
params.slice(:page, *RelationshipFilter::KEYS).permit(:page, *RelationshipFilter::KEYS)
|
||||
end
|
||||
|
||||
def action_from_button
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class Settings::BaseController < ApplicationController
|
||||
before_action :set_pack
|
||||
before_action :set_body_classes
|
||||
before_action :set_cache_headers
|
||||
|
||||
private
|
||||
|
||||
|
@ -13,4 +14,8 @@ class Settings::BaseController < ApplicationController
|
|||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
|
||||
def set_cache_headers
|
||||
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -60,6 +60,7 @@ class Settings::PreferencesController < Settings::BaseController
|
|||
:setting_use_blurhash,
|
||||
:setting_use_pending_items,
|
||||
:setting_trends,
|
||||
:setting_crop_images,
|
||||
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag),
|
||||
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
||||
)
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::ProfilesController < Settings::BaseController
|
||||
include ObfuscateFilename
|
||||
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_account
|
||||
|
||||
obfuscate_filename [:account, :avatar]
|
||||
obfuscate_filename [:account, :header]
|
||||
|
||||
def show
|
||||
@account.build_fields
|
||||
end
|
||||
|
|
|
@ -49,7 +49,7 @@ class StatusesController < ApplicationController
|
|||
|
||||
def embed
|
||||
use_pack 'embed'
|
||||
raise ActiveRecord::RecordNotFound if @status.hidden?
|
||||
return not_found if @status.hidden?
|
||||
|
||||
expires_in 180, public: true
|
||||
response.headers['X-Frame-Options'] = 'ALLOWALL'
|
||||
|
@ -71,7 +71,7 @@ class StatusesController < ApplicationController
|
|||
@status = @account.statuses.find(params[:id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
raise ActiveRecord::RecordNotFound
|
||||
not_found
|
||||
end
|
||||
|
||||
def set_instance_presenter
|
||||
|
|
|
@ -25,7 +25,7 @@ class TagsController < ApplicationController
|
|||
format.rss do
|
||||
expires_in 0, public: true
|
||||
|
||||
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none)).limit(PAGE_SIZE)
|
||||
@statuses = HashtagQueryService.new.call(@tag, filter_params).limit(PAGE_SIZE)
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
|
||||
render xml: RSS::TagSerializer.render(@tag, @statuses)
|
||||
|
@ -34,7 +34,7 @@ class TagsController < ApplicationController
|
|||
format.json do
|
||||
expires_in 3.minutes, public: public_fetch_mode?
|
||||
|
||||
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id])
|
||||
@statuses = HashtagQueryService.new.call(@tag, filter_params, current_account, params[: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'
|
||||
|
@ -58,10 +58,14 @@ class TagsController < ApplicationController
|
|||
|
||||
def collection_presenter
|
||||
ActivityPub::CollectionPresenter.new(
|
||||
id: tag_url(@tag, params.slice(:any, :all, :none)),
|
||||
id: tag_url(@tag, filter_params),
|
||||
type: :ordered,
|
||||
size: @tag.statuses.count,
|
||||
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
|
||||
)
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.slice(:any, :all, :none).permit(:any, :all, :none)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,12 +8,8 @@ module WellKnown
|
|||
|
||||
def show
|
||||
@webfinger_template = "#{webfinger_url}?resource={uri}"
|
||||
|
||||
respond_to do |format|
|
||||
format.xml { render content_type: 'application/xrd+xml' }
|
||||
end
|
||||
|
||||
expires_in 3.days, public: true
|
||||
render content_type: 'application/xrd+xml', formats: [:xml]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AccountsHelper
|
||||
def display_name(account, **options)
|
||||
if options[:custom_emojify]
|
||||
Formatter.instance.format_display_name(account, **options)
|
||||
else
|
||||
account.display_name.presence || account.username
|
||||
end
|
||||
end
|
||||
|
||||
def acct(account)
|
||||
if account.local?
|
||||
"@#{account.acct}@#{site_hostname}"
|
||||
else
|
||||
"@#{account.pretty_acct}"
|
||||
end
|
||||
end
|
||||
|
||||
def account_action_button(account)
|
||||
if user_signed_in?
|
||||
if account.id == current_user.account_id
|
||||
link_to settings_profile_url, class: 'button logo-button' do
|
||||
safe_join([svg_logo, t('settings.edit_profile')])
|
||||
end
|
||||
elsif current_account.following?(account) || current_account.requested?(account)
|
||||
link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do
|
||||
safe_join([svg_logo, t('accounts.unfollow')])
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do
|
||||
safe_join([svg_logo, t('accounts.follow')])
|
||||
end
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do
|
||||
safe_join([svg_logo, t('accounts.follow')])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def minimal_account_action_button(account)
|
||||
if user_signed_in?
|
||||
return if account.id == current_user.account_id
|
||||
|
||||
if current_account.following?(account) || current_account.requested?(account)
|
||||
link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do
|
||||
fa_icon('user-times fw')
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do
|
||||
fa_icon('user-plus fw')
|
||||
end
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do
|
||||
fa_icon('user-plus fw')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def account_badge(account, all: false)
|
||||
if account.bot?
|
||||
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
|
||||
elsif account.group?
|
||||
content_tag(:div, content_tag(:div, t('accounts.roles.group'), class: 'account-role group'), class: 'roles')
|
||||
elsif (Setting.show_staff_badge && account.user_staff?) || all
|
||||
content_tag(:div, class: 'roles') do
|
||||
if all && !account.user_staff?
|
||||
content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role')
|
||||
elsif account.user_admin?
|
||||
content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin')
|
||||
elsif account.user_moderator?
|
||||
content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def hide_followers_count?(account)
|
||||
Setting.hide_followers_count || account.user&.setting_hide_followers_count
|
||||
end
|
||||
|
||||
def account_description(account)
|
||||
prepend_stats = [
|
||||
[
|
||||
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.posts', count: account.statuses_count),
|
||||
].join(' '),
|
||||
|
||||
[
|
||||
number_to_human(account.following_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.following', count: account.following_count),
|
||||
].join(' '),
|
||||
]
|
||||
|
||||
unless hide_followers_count?(account)
|
||||
prepend_stats << [
|
||||
number_to_human(account.followers_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.followers', count: account.followers_count),
|
||||
].join(' ')
|
||||
end
|
||||
|
||||
[prepend_stats.join(', '), account.note].join(' · ')
|
||||
end
|
||||
|
||||
def svg_logo
|
||||
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
|
||||
end
|
||||
|
||||
def svg_logo_full
|
||||
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678')
|
||||
end
|
||||
end
|
|
@ -22,6 +22,8 @@ module Admin::ActionLogsHelper
|
|||
log.recorded_changes.slice('severity', 'reject_media')
|
||||
elsif log.target_type == 'Status' && log.action == :update
|
||||
log.recorded_changes.slice('sensitive')
|
||||
elsif log.target_type == 'Announcement' && log.action == :update
|
||||
log.recorded_changes.slice('text', 'starts_at', 'ends_at', 'all_day')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -44,12 +46,16 @@ module Admin::ActionLogsHelper
|
|||
'flag'
|
||||
when 'DomainBlock'
|
||||
'lock'
|
||||
when 'DomainAllow'
|
||||
'plus-circle'
|
||||
when 'EmailDomainBlock'
|
||||
'envelope'
|
||||
when 'Status'
|
||||
'pencil'
|
||||
when 'AccountWarning'
|
||||
'warning'
|
||||
when 'Announcement'
|
||||
'bullhorn'
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -86,12 +92,14 @@ module Admin::ActionLogsHelper
|
|||
record.shortcode
|
||||
when 'Report'
|
||||
link_to "##{record.id}", admin_report_path(record)
|
||||
when 'DomainBlock', 'EmailDomainBlock'
|
||||
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock'
|
||||
link_to record.domain, "https://#{record.domain}"
|
||||
when 'Status'
|
||||
link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record)
|
||||
when 'AccountWarning'
|
||||
link_to record.target_account.acct, admin_account_path(record.target_account_id)
|
||||
when 'Announcement'
|
||||
link_to "##{record.id}", edit_admin_announcement_path(record.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -99,7 +107,7 @@ module Admin::ActionLogsHelper
|
|||
case type
|
||||
when 'CustomEmoji'
|
||||
attributes['shortcode']
|
||||
when 'DomainBlock', 'EmailDomainBlock'
|
||||
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock'
|
||||
link_to attributes['domain'], "https://#{attributes['domain']}"
|
||||
when 'Status'
|
||||
tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count'))
|
||||
|
@ -109,6 +117,8 @@ module Admin::ActionLogsHelper
|
|||
else
|
||||
I18n.t('admin.action_logs.deleted_status')
|
||||
end
|
||||
when 'Announcement'
|
||||
"##{attributes['id']}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin::AnnouncementsHelper
|
||||
def time_range(announcement)
|
||||
if announcement.all_day?
|
||||
safe_join([l(announcement.starts_at.to_date), ' - ', l(announcement.ends_at.to_date)])
|
||||
else
|
||||
safe_join([l(announcement.starts_at), ' - ', l(announcement.ends_at)])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,15 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin::FilterHelper
|
||||
ACCOUNT_FILTERS = %i(local remote by_domain active pending silenced suspended username display_name email ip staff).freeze
|
||||
REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
|
||||
INVITE_FILTER = %i(available expired).freeze
|
||||
CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
|
||||
TAGS_FILTERS = %i(directory reviewed unreviewed pending_review popular active name).freeze
|
||||
INSTANCES_FILTERS = %i(limited by_domain).freeze
|
||||
FOLLOWERS_FILTERS = %i(relationship status by_domain activity order).freeze
|
||||
|
||||
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS + FOLLOWERS_FILTERS
|
||||
FILTERS = [
|
||||
AccountFilter::KEYS,
|
||||
CustomEmojiFilter::KEYS,
|
||||
ReportFilter::KEYS,
|
||||
TagFilter::KEYS,
|
||||
InstanceFilter::KEYS,
|
||||
InviteFilter::KEYS,
|
||||
RelationshipFilter::KEYS,
|
||||
AnnouncementFilter::KEYS,
|
||||
].flatten.freeze
|
||||
|
||||
def filter_link_to(text, link_to_params, link_class_params = link_to_params)
|
||||
new_url = filtered_url_for(link_to_params)
|
||||
|
|
|
@ -6,7 +6,7 @@ module DomainControlHelper
|
|||
|
||||
domain = begin
|
||||
if uri_or_domain.include?('://')
|
||||
Addressable::URI.parse(uri_or_domain).domain
|
||||
Addressable::URI.parse(uri_or_domain).host
|
||||
else
|
||||
uri_or_domain
|
||||
end
|
||||
|
|
|
@ -13,13 +13,13 @@ module RoutingHelper
|
|||
end
|
||||
|
||||
def full_asset_url(source, **options)
|
||||
source = ActionController::Base.helpers.asset_url(source, options) unless use_storage?
|
||||
source = ActionController::Base.helpers.asset_url(source, **options) unless use_storage?
|
||||
|
||||
URI.join(root_url, source).to_s
|
||||
end
|
||||
|
||||
def full_pack_url(source, **options)
|
||||
full_asset_url(asset_pack_path(source, options))
|
||||
full_asset_url(asset_pack_path(source, **options))
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -32,15 +32,19 @@ module SettingsHelper
|
|||
hy: 'Հայերեն',
|
||||
id: 'Bahasa Indonesia',
|
||||
io: 'Ido',
|
||||
is: 'Íslenska',
|
||||
it: 'Italiano',
|
||||
ja: '日本語',
|
||||
ka: 'ქართული',
|
||||
kab: 'Taqbaylit',
|
||||
kk: 'Қазақша',
|
||||
kn: 'ಕನ್ನಡ',
|
||||
ko: '한국어',
|
||||
lt: 'Lietuvių',
|
||||
lv: 'Latviešu',
|
||||
mk: 'Македонски',
|
||||
ml: 'മലയാളം',
|
||||
mr: 'मराठी',
|
||||
ms: 'Bahasa Melayu',
|
||||
nl: 'Nederlands',
|
||||
nn: 'Nynorsk',
|
||||
|
@ -63,6 +67,7 @@ module SettingsHelper
|
|||
th: 'ไทย',
|
||||
tr: 'Türkçe',
|
||||
uk: 'Українська',
|
||||
ur: 'اُردُو',
|
||||
'zh-CN': '简体中文',
|
||||
'zh-HK': '繁體中文(香港)',
|
||||
'zh-TW': '繁體中文(臺灣)',
|
||||
|
|
|
@ -4,80 +4,6 @@ module StatusesHelper
|
|||
EMBEDDED_CONTROLLER = 'statuses'
|
||||
EMBEDDED_ACTION = 'embed'
|
||||
|
||||
def display_name(account, **options)
|
||||
if options[:custom_emojify]
|
||||
Formatter.instance.format_display_name(account, options)
|
||||
else
|
||||
account.display_name.presence || account.username
|
||||
end
|
||||
end
|
||||
|
||||
def account_action_button(account)
|
||||
if user_signed_in?
|
||||
if account.id == current_user.account_id
|
||||
link_to settings_profile_url, class: 'button logo-button' do
|
||||
safe_join([svg_logo, t('settings.edit_profile')])
|
||||
end
|
||||
elsif current_account.following?(account) || current_account.requested?(account)
|
||||
link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do
|
||||
safe_join([svg_logo, t('accounts.unfollow')])
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do
|
||||
safe_join([svg_logo, t('accounts.follow')])
|
||||
end
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do
|
||||
safe_join([svg_logo, t('accounts.follow')])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def minimal_account_action_button(account)
|
||||
if user_signed_in?
|
||||
return if account.id == current_user.account_id
|
||||
|
||||
if current_account.following?(account) || current_account.requested?(account)
|
||||
link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do
|
||||
fa_icon('user-times fw')
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do
|
||||
fa_icon('user-plus fw')
|
||||
end
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do
|
||||
fa_icon('user-plus fw')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def svg_logo
|
||||
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
|
||||
end
|
||||
|
||||
def svg_logo_full
|
||||
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678')
|
||||
end
|
||||
|
||||
def account_badge(account, all: false)
|
||||
if account.bot?
|
||||
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
|
||||
elsif (Setting.show_staff_badge && account.user_staff?) || all
|
||||
content_tag(:div, class: 'roles') do
|
||||
if all && !account.user_staff?
|
||||
content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role')
|
||||
elsif account.user_admin?
|
||||
content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin')
|
||||
elsif account.user_moderator?
|
||||
content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def link_to_more(url)
|
||||
link_to t('statuses.show_more'), url, class: 'load-more load-gap'
|
||||
end
|
||||
|
@ -88,33 +14,6 @@ module StatusesHelper
|
|||
end
|
||||
end
|
||||
|
||||
def hide_followers_count?(account)
|
||||
Setting.hide_followers_count || account.user&.setting_hide_followers_count
|
||||
end
|
||||
|
||||
def account_description(account)
|
||||
prepend_stats = [
|
||||
[
|
||||
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.posts', count: account.statuses_count),
|
||||
].join(' '),
|
||||
|
||||
[
|
||||
number_to_human(account.following_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.following', count: account.following_count),
|
||||
].join(' '),
|
||||
]
|
||||
|
||||
unless hide_followers_count?(account)
|
||||
prepend_stats << [
|
||||
number_to_human(account.followers_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.followers', count: account.followers_count),
|
||||
].join(' ')
|
||||
end
|
||||
|
||||
[prepend_stats.join(', '), account.note].join(' · ')
|
||||
end
|
||||
|
||||
def media_summary(status)
|
||||
attachments = { image: 0, video: 0 }
|
||||
|
||||
|
@ -160,14 +59,6 @@ module StatusesHelper
|
|||
embedded_view? ? '_blank' : nil
|
||||
end
|
||||
|
||||
def acct(account)
|
||||
if account.local?
|
||||
"@#{account.acct}@#{Rails.configuration.x.local_domain}"
|
||||
else
|
||||
"@#{account.acct}"
|
||||
end
|
||||
end
|
||||
|
||||
def style_classes(status, is_predecessor, is_successor, include_threads)
|
||||
classes = ['entry']
|
||||
classes << 'entry-predecessor' if is_predecessor
|
||||
|
|
|
@ -47,7 +47,25 @@ const onDomainBlockSeverityChange = (target) => {
|
|||
|
||||
delegate(document, '#domain_block_severity', 'change', ({ target }) => onDomainBlockSeverityChange(target));
|
||||
|
||||
const onEnableBootstrapTimelineAccountsChange = (target) => {
|
||||
const bootstrapTimelineAccountsField = document.querySelector('#form_admin_settings_bootstrap_timeline_accounts');
|
||||
|
||||
if (bootstrapTimelineAccountsField) {
|
||||
bootstrapTimelineAccountsField.disabled = !target.checked;
|
||||
if (target.checked) {
|
||||
bootstrapTimelineAccountsField.parentElement.classList.remove('disabled');
|
||||
} else {
|
||||
bootstrapTimelineAccountsField.parentElement.classList.add('disabled');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
delegate(document, '#form_admin_settings_enable_bootstrap_timeline_accounts', 'change', ({ target }) => onEnableBootstrapTimelineAccountsChange(target));
|
||||
|
||||
ready(() => {
|
||||
const input = document.getElementById('domain_block_severity');
|
||||
if (input) onDomainBlockSeverityChange(input);
|
||||
const domainBlockSeverityInput = document.getElementById('domain_block_severity');
|
||||
if (domainBlockSeverityInput) onDomainBlockSeverityChange(domainBlockSeverityInput);
|
||||
|
||||
const enableBootstrapTimelineAccounts = document.getElementById('form_admin_settings_enable_bootstrap_timeline_accounts');
|
||||
if (enableBootstrapTimelineAccounts) onEnableBootstrapTimelineAccountsChange(enableBootstrapTimelineAccounts);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
import api from 'flavours/glitch/util/api';
|
||||
import { normalizeAnnouncement } from './importer/normalizer';
|
||||
|
||||
export const ANNOUNCEMENTS_FETCH_REQUEST = 'ANNOUNCEMENTS_FETCH_REQUEST';
|
||||
export const ANNOUNCEMENTS_FETCH_SUCCESS = 'ANNOUNCEMENTS_FETCH_SUCCESS';
|
||||
export const ANNOUNCEMENTS_FETCH_FAIL = 'ANNOUNCEMENTS_FETCH_FAIL';
|
||||
export const ANNOUNCEMENTS_UPDATE = 'ANNOUNCEMENTS_UPDATE';
|
||||
export const ANNOUNCEMENTS_DELETE = 'ANNOUNCEMENTS_DELETE';
|
||||
|
||||
export const ANNOUNCEMENTS_DISMISS_REQUEST = 'ANNOUNCEMENTS_DISMISS_REQUEST';
|
||||
export const ANNOUNCEMENTS_DISMISS_SUCCESS = 'ANNOUNCEMENTS_DISMISS_SUCCESS';
|
||||
export const ANNOUNCEMENTS_DISMISS_FAIL = 'ANNOUNCEMENTS_DISMISS_FAIL';
|
||||
|
||||
export const ANNOUNCEMENTS_REACTION_ADD_REQUEST = 'ANNOUNCEMENTS_REACTION_ADD_REQUEST';
|
||||
export const ANNOUNCEMENTS_REACTION_ADD_SUCCESS = 'ANNOUNCEMENTS_REACTION_ADD_SUCCESS';
|
||||
export const ANNOUNCEMENTS_REACTION_ADD_FAIL = 'ANNOUNCEMENTS_REACTION_ADD_FAIL';
|
||||
|
||||
export const ANNOUNCEMENTS_REACTION_REMOVE_REQUEST = 'ANNOUNCEMENTS_REACTION_REMOVE_REQUEST';
|
||||
export const ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS = 'ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS';
|
||||
export const ANNOUNCEMENTS_REACTION_REMOVE_FAIL = 'ANNOUNCEMENTS_REACTION_REMOVE_FAIL';
|
||||
|
||||
export const ANNOUNCEMENTS_REACTION_UPDATE = 'ANNOUNCEMENTS_REACTION_UPDATE';
|
||||
|
||||
export const ANNOUNCEMENTS_TOGGLE_SHOW = 'ANNOUNCEMENTS_TOGGLE_SHOW';
|
||||
|
||||
const noOp = () => {};
|
||||
|
||||
export const fetchAnnouncements = (done = noOp) => (dispatch, getState) => {
|
||||
dispatch(fetchAnnouncementsRequest());
|
||||
|
||||
api(getState).get('/api/v1/announcements').then(response => {
|
||||
dispatch(fetchAnnouncementsSuccess(response.data.map(x => normalizeAnnouncement(x))));
|
||||
}).catch(error => {
|
||||
dispatch(fetchAnnouncementsFail(error));
|
||||
}).finally(() => {
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchAnnouncementsRequest = () => ({
|
||||
type: ANNOUNCEMENTS_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const fetchAnnouncementsSuccess = announcements => ({
|
||||
type: ANNOUNCEMENTS_FETCH_SUCCESS,
|
||||
announcements,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const fetchAnnouncementsFail= error => ({
|
||||
type: ANNOUNCEMENTS_FETCH_FAIL,
|
||||
error,
|
||||
skipLoading: true,
|
||||
skipAlert: true,
|
||||
});
|
||||
|
||||
export const updateAnnouncements = announcement => ({
|
||||
type: ANNOUNCEMENTS_UPDATE,
|
||||
announcement: normalizeAnnouncement(announcement),
|
||||
});
|
||||
|
||||
export const dismissAnnouncement = announcementId => (dispatch, getState) => {
|
||||
dispatch(dismissAnnouncementRequest(announcementId));
|
||||
|
||||
api(getState).post(`/api/v1/announcements/${announcementId}/dismiss`).then(() => {
|
||||
dispatch(dismissAnnouncementSuccess(announcementId));
|
||||
}).catch(error => {
|
||||
dispatch(dismissAnnouncementFail(announcementId, error));
|
||||
});
|
||||
};
|
||||
|
||||
export const dismissAnnouncementRequest = announcementId => ({
|
||||
type: ANNOUNCEMENTS_DISMISS_REQUEST,
|
||||
id: announcementId,
|
||||
});
|
||||
|
||||
export const dismissAnnouncementSuccess = announcementId => ({
|
||||
type: ANNOUNCEMENTS_DISMISS_SUCCESS,
|
||||
id: announcementId,
|
||||
});
|
||||
|
||||
export const dismissAnnouncementFail = (announcementId, error) => ({
|
||||
type: ANNOUNCEMENTS_DISMISS_FAIL,
|
||||
id: announcementId,
|
||||
error,
|
||||
});
|
||||
|
||||
export const addReaction = (announcementId, name) => (dispatch, getState) => {
|
||||
const announcement = getState().getIn(['announcements', 'items']).find(x => x.get('id') === announcementId);
|
||||
|
||||
let alreadyAdded = false;
|
||||
|
||||
if (announcement) {
|
||||
const reaction = announcement.get('reactions').find(x => x.get('name') === name);
|
||||
if (reaction && reaction.get('me')) {
|
||||
alreadyAdded = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!alreadyAdded) {
|
||||
dispatch(addReactionRequest(announcementId, name, alreadyAdded));
|
||||
}
|
||||
|
||||
api(getState).put(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => {
|
||||
dispatch(addReactionSuccess(announcementId, name, alreadyAdded));
|
||||
}).catch(err => {
|
||||
if (!alreadyAdded) {
|
||||
dispatch(addReactionFail(announcementId, name, err));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const addReactionRequest = (announcementId, name) => ({
|
||||
type: ANNOUNCEMENTS_REACTION_ADD_REQUEST,
|
||||
id: announcementId,
|
||||
name,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const addReactionSuccess = (announcementId, name) => ({
|
||||
type: ANNOUNCEMENTS_REACTION_ADD_SUCCESS,
|
||||
id: announcementId,
|
||||
name,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const addReactionFail = (announcementId, name, error) => ({
|
||||
type: ANNOUNCEMENTS_REACTION_ADD_FAIL,
|
||||
id: announcementId,
|
||||
name,
|
||||
error,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const removeReaction = (announcementId, name) => (dispatch, getState) => {
|
||||
dispatch(removeReactionRequest(announcementId, name));
|
||||
|
||||
api(getState).delete(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => {
|
||||
dispatch(removeReactionSuccess(announcementId, name));
|
||||
}).catch(err => {
|
||||
dispatch(removeReactionFail(announcementId, name, err));
|
||||
});
|
||||
};
|
||||
|
||||
export const removeReactionRequest = (announcementId, name) => ({
|
||||
type: ANNOUNCEMENTS_REACTION_REMOVE_REQUEST,
|
||||
id: announcementId,
|
||||
name,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const removeReactionSuccess = (announcementId, name) => ({
|
||||
type: ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS,
|
||||
id: announcementId,
|
||||
name,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const removeReactionFail = (announcementId, name, error) => ({
|
||||
type: ANNOUNCEMENTS_REACTION_REMOVE_FAIL,
|
||||
id: announcementId,
|
||||
name,
|
||||
error,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const updateReaction = reaction => ({
|
||||
type: ANNOUNCEMENTS_REACTION_UPDATE,
|
||||
reaction,
|
||||
});
|
||||
|
||||
export const toggleShowAnnouncements = () => ({
|
||||
type: ANNOUNCEMENTS_TOGGLE_SHOW,
|
||||
});
|
||||
|
||||
export const deleteAnnouncement = id => ({
|
||||
type: ANNOUNCEMENTS_DELETE,
|
||||
id,
|
||||
});
|
|
@ -91,9 +91,11 @@ export function cycleElefriendCompose() {
|
|||
|
||||
export function replyCompose(status, routerHistory) {
|
||||
return (dispatch, getState) => {
|
||||
const prependCWRe = getState().getIn(['local_settings', 'prepend_cw_re']);
|
||||
dispatch({
|
||||
type: COMPOSE_REPLY,
|
||||
status: status,
|
||||
prependCWRe: prependCWRe,
|
||||
});
|
||||
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
|
@ -232,10 +234,11 @@ export function uploadCompose(files) {
|
|||
return function (dispatch, getState) {
|
||||
const uploadLimit = 4;
|
||||
const media = getState().getIn(['compose', 'media_attachments']);
|
||||
const pending = getState().getIn(['compose', 'pending_media_attachments']);
|
||||
const progress = new Array(files.length).fill(0);
|
||||
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
|
||||
|
||||
if (files.length + media.size > uploadLimit) {
|
||||
if (files.length + media.size + pending > uploadLimit) {
|
||||
dispatch(showAlert(undefined, messages.uploadErrorLimit));
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,12 @@ const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
|
|||
return obj;
|
||||
}, {});
|
||||
|
||||
export function searchTextFromRawStatus (status) {
|
||||
const spoilerText = status.spoiler_text || '';
|
||||
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
||||
return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
||||
}
|
||||
|
||||
export function normalizeAccount(account) {
|
||||
account = { ...account };
|
||||
|
||||
|
@ -68,7 +74,6 @@ export function normalizeStatus(status, normalOldStatus) {
|
|||
|
||||
export function normalizePoll(poll) {
|
||||
const normalPoll = { ...poll };
|
||||
|
||||
const emojiMap = makeEmojiMap(normalPoll);
|
||||
|
||||
normalPoll.options = poll.options.map((option, index) => ({
|
||||
|
@ -79,3 +84,12 @@ export function normalizePoll(poll) {
|
|||
|
||||
return normalPoll;
|
||||
}
|
||||
|
||||
export function normalizeAnnouncement(announcement) {
|
||||
const normalAnnouncement = { ...announcement };
|
||||
const emojiMap = makeEmojiMap(normalAnnouncement);
|
||||
|
||||
normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);
|
||||
|
||||
return normalAnnouncement;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
import api from 'flavours/glitch/util/api';
|
||||
|
||||
export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST';
|
||||
export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS';
|
||||
export const MARKERS_FETCH_FAIL = 'MARKERS_FETCH_FAIL';
|
||||
|
||||
export const submitMarkers = () => (dispatch, getState) => {
|
||||
const accessToken = getState().getIn(['meta', 'access_token'], '');
|
||||
const params = {};
|
||||
|
||||
const lastHomeId = getState().getIn(['timelines', 'home', 'items', 0]);
|
||||
const lastNotificationId = getState().getIn(['notifications', 'items', 0, 'id']);
|
||||
const lastNotificationId = getState().getIn(['notifications', 'lastReadId']);
|
||||
|
||||
if (lastHomeId) {
|
||||
params.home = {
|
||||
|
@ -11,7 +17,7 @@ export const submitMarkers = () => (dispatch, getState) => {
|
|||
};
|
||||
}
|
||||
|
||||
if (lastNotificationId) {
|
||||
if (lastNotificationId && lastNotificationId !== '0') {
|
||||
params.notifications = {
|
||||
last_read_id: lastNotificationId,
|
||||
};
|
||||
|
@ -28,3 +34,39 @@ export const submitMarkers = () => (dispatch, getState) => {
|
|||
client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
|
||||
client.send(JSON.stringify(params));
|
||||
};
|
||||
|
||||
export const fetchMarkers = () => (dispatch, getState) => {
|
||||
const params = { timeline: ['notifications'] };
|
||||
|
||||
dispatch(fetchMarkersRequest());
|
||||
|
||||
api(getState).get('/api/v1/markers', { params }).then(response => {
|
||||
dispatch(fetchMarkersSuccess(response.data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchMarkersFail(error));
|
||||
});
|
||||
};
|
||||
|
||||
export function fetchMarkersRequest() {
|
||||
return {
|
||||
type: MARKERS_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchMarkersSuccess(markers) {
|
||||
return {
|
||||
type: MARKERS_FETCH_SUCCESS,
|
||||
markers,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchMarkersFail(error) {
|
||||
return {
|
||||
type: MARKERS_FETCH_FAIL,
|
||||
error,
|
||||
skipLoading: true,
|
||||
skipAlert: true,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@ import { unescapeHTML } from 'flavours/glitch/util/html';
|
|||
import { getFiltersRegex } from 'flavours/glitch/selectors';
|
||||
import { usePendingItems as preferPendingItems } from 'flavours/glitch/util/initial_state';
|
||||
import compareId from 'flavours/glitch/util/compare_id';
|
||||
import { searchTextFromRawStatus } from 'flavours/glitch/actions/importer/normalizer';
|
||||
|
||||
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
||||
|
||||
|
@ -71,7 +72,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
|||
if (notification.type === 'mention') {
|
||||
const dropRegex = filters[0];
|
||||
const regex = filters[1];
|
||||
const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content);
|
||||
const searchIndex = searchTextFromRawStatus(notification.status);
|
||||
|
||||
if (dropRegex && dropRegex.test(searchIndex)) {
|
||||
return;
|
||||
|
@ -120,7 +121,7 @@ const excludeTypesFromSettings = state => state.getIn(['settings', 'notification
|
|||
|
||||
|
||||
const excludeTypesFromFilter = filter => {
|
||||
const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention', 'poll']);
|
||||
const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll']);
|
||||
return allTypes.filterNot(item => item === filter).toJS();
|
||||
};
|
||||
|
||||
|
@ -167,9 +168,9 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
|
|||
|
||||
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
|
||||
fetchRelatedRelationships(dispatch, response.data);
|
||||
done();
|
||||
}).catch(error => {
|
||||
dispatch(expandNotificationsFail(error, isLoadingMore));
|
||||
}).finally(() => {
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
@ -198,6 +199,7 @@ export function expandNotificationsFail(error, isLoadingMore) {
|
|||
type: NOTIFICATIONS_EXPAND_FAIL,
|
||||
error,
|
||||
skipLoading: !isLoadingMore,
|
||||
skipAlert: !isLoadingMore,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -8,6 +8,12 @@ import {
|
|||
} from './timelines';
|
||||
import { updateNotifications, expandNotifications } from './notifications';
|
||||
import { updateConversations } from './conversations';
|
||||
import {
|
||||
fetchAnnouncements,
|
||||
updateAnnouncements,
|
||||
updateReaction as updateAnnouncementsReaction,
|
||||
deleteAnnouncement,
|
||||
} from './announcements';
|
||||
import { fetchFilters } from './filters';
|
||||
import { getLocale } from 'mastodon/locales';
|
||||
|
||||
|
@ -44,6 +50,15 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
|
|||
case 'filters_changed':
|
||||
dispatch(fetchFilters());
|
||||
break;
|
||||
case 'announcement':
|
||||
dispatch(updateAnnouncements(JSON.parse(data.payload)));
|
||||
break;
|
||||
case 'announcement.reaction':
|
||||
dispatch(updateAnnouncementsReaction(JSON.parse(data.payload)));
|
||||
break;
|
||||
case 'announcement.delete':
|
||||
dispatch(deleteAnnouncement(data.payload));
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -51,7 +66,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
|
|||
}
|
||||
|
||||
const refreshHomeTimelineAndNotification = (dispatch, done) => {
|
||||
dispatch(expandHomeTimeline({}, () => dispatch(expandNotifications({}, done))));
|
||||
dispatch(expandHomeTimeline({}, () =>
|
||||
dispatch(expandNotifications({}, () =>
|
||||
dispatch(fetchAnnouncements(done))))));
|
||||
};
|
||||
|
||||
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
|
||||
|
|
|
@ -2,7 +2,9 @@ import { importFetchedStatus, importFetchedStatuses } from './importer';
|
|||
import api, { getLinks } from 'flavours/glitch/util/api';
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import compareId from 'flavours/glitch/util/compare_id';
|
||||
import { usePendingItems as preferPendingItems } from 'flavours/glitch/util/initial_state';
|
||||
import { me, usePendingItems as preferPendingItems } from 'flavours/glitch/util/initial_state';
|
||||
import { getFiltersRegex } from 'flavours/glitch/selectors';
|
||||
import { searchTextFromRawStatus } from 'flavours/glitch/actions/importer/normalizer';
|
||||
|
||||
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
|
||||
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
|
||||
|
@ -23,11 +25,21 @@ export const loadPending = timeline => ({
|
|||
});
|
||||
|
||||
export function updateTimeline(timeline, status, accept) {
|
||||
return dispatch => {
|
||||
return (dispatch, getState) => {
|
||||
if (typeof accept === 'function' && !accept(status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filters = getFiltersRegex(getState(), { contextType: timeline });
|
||||
const dropRegex = filters[0];
|
||||
const regex = filters[1];
|
||||
const text = searchTextFromRawStatus(status);
|
||||
let filtered = false;
|
||||
|
||||
if (status.account.id !== me) {
|
||||
filtered = (dropRegex && dropRegex.test(text)) || (regex && regex.test(text));
|
||||
}
|
||||
|
||||
dispatch(importFetchedStatus(status));
|
||||
|
||||
dispatch({
|
||||
|
@ -35,6 +47,7 @@ export function updateTimeline(timeline, status, accept) {
|
|||
timeline,
|
||||
status,
|
||||
usePendingItems: preferPendingItems,
|
||||
filtered
|
||||
});
|
||||
};
|
||||
};
|
||||
|
@ -96,11 +109,12 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
|
|||
|
||||
api(getState).get(path, { params }).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
|
||||
done();
|
||||
}).catch(error => {
|
||||
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
|
||||
}).finally(() => {
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedNumber } from 'react-intl';
|
||||
import TransitionMotion from 'react-motion/lib/TransitionMotion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import { reduceMotion } from 'flavours/glitch/util/initial_state';
|
||||
|
||||
export default class AnimatedNumber extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
direction: 1,
|
||||
};
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.value > this.props.value) {
|
||||
this.setState({ direction: 1 });
|
||||
} else if (nextProps.value < this.props.value) {
|
||||
this.setState({ direction: -1 });
|
||||
}
|
||||
}
|
||||
|
||||
willEnter = () => {
|
||||
const { direction } = this.state;
|
||||
|
||||
return { y: -1 * direction };
|
||||
}
|
||||
|
||||
willLeave = () => {
|
||||
const { direction } = this.state;
|
||||
|
||||
return { y: spring(1 * direction, { damping: 35, stiffness: 400 }) };
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value } = this.props;
|
||||
const { direction } = this.state;
|
||||
|
||||
if (reduceMotion) {
|
||||
return <FormattedNumber value={value} />;
|
||||
}
|
||||
|
||||
const styles = [{
|
||||
key: `${value}`,
|
||||
data: value,
|
||||
style: { y: spring(0, { damping: 35, stiffness: 400 }) },
|
||||
}];
|
||||
|
||||
return (
|
||||
<TransitionMotion styles={styles} willEnter={this.willEnter} willLeave={this.willLeave}>
|
||||
{items => (
|
||||
<span className='animated-number'>
|
||||
{items.map(({ key, data, style }) => (
|
||||
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}><FormattedNumber value={data} /></span>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
</TransitionMotion>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -25,7 +25,7 @@ export default class AttachmentList extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<li key={attachment.get('id')}>
|
||||
<a href={displayUrl} target='_blank' rel='noopener'><Icon id='link' /> {filename(displayUrl)}</a>
|
||||
<a href={displayUrl} target='_blank' rel='noopener noreferrer'><Icon id='link' /> {filename(displayUrl)}</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
@ -46,7 +46,7 @@ export default class AttachmentList extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<li key={attachment.get('id')}>
|
||||
<a href={displayUrl} target='_blank' rel='noopener'>{filename(displayUrl)}</a>
|
||||
<a href={displayUrl} target='_blank' rel='noopener noreferrer'>{filename(displayUrl)}</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -2,18 +2,14 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { createPortal } from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
|
||||
import NotificationPurgeButtonsContainer from 'flavours/glitch/containers/notification_purge_buttons_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
|
||||
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
|
||||
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
|
||||
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
|
||||
enterNotifCleaning : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
|
@ -28,26 +24,21 @@ class ColumnHeader extends React.PureComponent {
|
|||
title: PropTypes.node,
|
||||
icon: PropTypes.string,
|
||||
active: PropTypes.bool,
|
||||
localSettings : ImmutablePropTypes.map,
|
||||
multiColumn: PropTypes.bool,
|
||||
extraButton: PropTypes.node,
|
||||
showBackButton: PropTypes.bool,
|
||||
notifCleaning: PropTypes.bool, // true only for the notification column
|
||||
notifCleaningActive: PropTypes.bool,
|
||||
onEnterCleaningMode: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
pinned: PropTypes.bool,
|
||||
placeholder: PropTypes.bool,
|
||||
onPin: PropTypes.func,
|
||||
onMove: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
intl: PropTypes.object.isRequired,
|
||||
appendContent: PropTypes.node,
|
||||
};
|
||||
|
||||
state = {
|
||||
collapsed: true,
|
||||
animating: false,
|
||||
animatingNCD: false,
|
||||
};
|
||||
|
||||
historyBack = (skip) => {
|
||||
|
@ -89,10 +80,6 @@ class ColumnHeader extends React.PureComponent {
|
|||
this.setState({ animating: false });
|
||||
}
|
||||
|
||||
handleTransitionEndNCD = () => {
|
||||
this.setState({ animatingNCD: false });
|
||||
}
|
||||
|
||||
handlePin = () => {
|
||||
if (!this.props.pinned) {
|
||||
this.historyBack();
|
||||
|
@ -100,16 +87,9 @@ class ColumnHeader extends React.PureComponent {
|
|||
this.props.onPin();
|
||||
}
|
||||
|
||||
onEnterCleaningMode = () => {
|
||||
this.setState({ animatingNCD: true });
|
||||
this.props.onEnterCleaningMode(!this.props.notifCleaningActive);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive, placeholder } = this.props;
|
||||
const { collapsed, animating, animatingNCD } = this.state;
|
||||
|
||||
let title = this.props.title;
|
||||
const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent } = this.props;
|
||||
const { collapsed, animating } = this.state;
|
||||
|
||||
const wrapperClassName = classNames('column-header__wrapper', {
|
||||
'active': active,
|
||||
|
@ -128,20 +108,8 @@ class ColumnHeader extends React.PureComponent {
|
|||
'active': !collapsed,
|
||||
});
|
||||
|
||||
const notifCleaningButtonClassName = classNames('column-header__button', {
|
||||
'active': notifCleaningActive,
|
||||
});
|
||||
|
||||
const notifCleaningDrawerClassName = classNames('ncd column-header__collapsible', {
|
||||
'collapsed': !notifCleaningActive,
|
||||
'animating': animatingNCD,
|
||||
});
|
||||
|
||||
let extraContent, pinButton, moveButtons, backButton, collapseButton;
|
||||
|
||||
//*glitch
|
||||
const msgEnterNotifCleaning = intl.formatMessage(messages.enterNotifCleaning);
|
||||
|
||||
if (children) {
|
||||
extraContent = (
|
||||
<div key='extra-content' className='column-header__collapsible__extra'>
|
||||
|
@ -202,33 +170,17 @@ class ColumnHeader extends React.PureComponent {
|
|||
<div className='column-header__buttons'>
|
||||
{hasTitle && backButton}
|
||||
{extraButton}
|
||||
{ notifCleaning ? (
|
||||
<button
|
||||
aria-label={msgEnterNotifCleaning}
|
||||
title={msgEnterNotifCleaning}
|
||||
onClick={this.onEnterCleaningMode}
|
||||
className={notifCleaningButtonClassName}
|
||||
>
|
||||
<Icon id='eraser' />
|
||||
</button>
|
||||
) : null}
|
||||
{collapseButton}
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
{ notifCleaning ? (
|
||||
<div className={notifCleaningDrawerClassName} onTransitionEnd={this.handleTransitionEndNCD}>
|
||||
<div className='column-header__collapsible-inner nopad-drawer'>
|
||||
{(notifCleaningActive || animatingNCD) ? (<NotificationPurgeButtonsContainer />) : null }
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className={collapsibleClassName} tabIndex={collapsed ? -1 : null} onTransitionEnd={this.handleTransitionEnd}>
|
||||
<div className='column-header__collapsible-inner'>
|
||||
{(!collapsed || animating) && collapsedContent}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{appendContent}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -78,6 +78,7 @@ export default class DisplayName extends React.PureComponent {
|
|||
target='_blank'
|
||||
onClick={(e) => onAccountClick(a.get('id'), e)}
|
||||
title={`@${a.get('acct')}`}
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<bdi key={a.get('id')}>
|
||||
<strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} />
|
||||
|
@ -90,7 +91,7 @@ export default class DisplayName extends React.PureComponent {
|
|||
}
|
||||
|
||||
suffix = (
|
||||
<a href={account.get('url')} target='_blank' onClick={(e) => onAccountClick(account.get('id'), e)}>
|
||||
<a href={account.get('url')} target='_blank' onClick={(e) => onAccountClick(account.get('id'), e)} rel='noopener noreferrer'>
|
||||
<span className='display-name__account'>@{acct}</span>
|
||||
</a>
|
||||
);
|
||||
|
|
|
@ -143,7 +143,7 @@ class DropdownMenu extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<li className='dropdown-menu__item' key={`${text}-${i}`}>
|
||||
<a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
|
||||
<a href={href} target='_blank' rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
|
||||
{text}
|
||||
</a>
|
||||
</li>
|
||||
|
@ -184,7 +184,7 @@ export default class Dropdown extends React.PureComponent {
|
|||
icon: PropTypes.string.isRequired,
|
||||
items: PropTypes.array.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
ariaLabel: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
status: ImmutablePropTypes.map,
|
||||
isUserTouching: PropTypes.func,
|
||||
|
@ -197,7 +197,7 @@ export default class Dropdown extends React.PureComponent {
|
|||
};
|
||||
|
||||
static defaultProps = {
|
||||
ariaLabel: 'Menu',
|
||||
title: 'Menu',
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -277,14 +277,14 @@ export default class Dropdown extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { icon, items, size, ariaLabel, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props;
|
||||
const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props;
|
||||
const open = this.state.id === openDropdownId;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IconButton
|
||||
icon={icon}
|
||||
title={ariaLabel}
|
||||
title={title}
|
||||
active={open}
|
||||
disabled={disabled}
|
||||
size={size}
|
||||
|
|
|
@ -56,7 +56,7 @@ export default class ErrorBoundary extends React.PureComponent {
|
|||
<FormattedMessage
|
||||
id='web_app_crash.report_issue'
|
||||
defaultMessage='Report a bug in the {issuetracker}'
|
||||
values={{ issuetracker: <a href='https://github.com/glitch-soc/mastodon/issues' rel='noopener' target='_blank'><FormattedMessage id='web_app_crash.issue_tracker' defaultMessage='issue tracker' /></a> }}
|
||||
values={{ issuetracker: <a href='https://github.com/glitch-soc/mastodon/issues' rel='noopener noreferrer' target='_blank'><FormattedMessage id='web_app_crash.issue_tracker' defaultMessage='issue tracker' /></a> }}
|
||||
/>
|
||||
{ debugInfo !== '' && (
|
||||
<details>
|
||||
|
|
|
@ -24,7 +24,6 @@ export default class IconButton extends React.PureComponent {
|
|||
disabled: PropTypes.bool,
|
||||
inverted: PropTypes.bool,
|
||||
animate: PropTypes.bool,
|
||||
flip: PropTypes.bool,
|
||||
overlay: PropTypes.bool,
|
||||
tabIndex: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
|
@ -39,6 +38,21 @@ export default class IconButton extends React.PureComponent {
|
|||
tabIndex: '0',
|
||||
};
|
||||
|
||||
state = {
|
||||
activate: false,
|
||||
deactivate: false,
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (!nextProps.animate) return;
|
||||
|
||||
if (this.props.active && !nextProps.active) {
|
||||
this.setState({ activate: false, deactivate: true });
|
||||
} else if (!this.props.active && nextProps.active) {
|
||||
this.setState({ activate: true, deactivate: false });
|
||||
}
|
||||
}
|
||||
|
||||
handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -81,86 +95,49 @@ export default class IconButton extends React.PureComponent {
|
|||
|
||||
const {
|
||||
active,
|
||||
animate,
|
||||
className,
|
||||
disabled,
|
||||
expanded,
|
||||
icon,
|
||||
inverted,
|
||||
flip,
|
||||
overlay,
|
||||
pressed,
|
||||
tabIndex,
|
||||
title,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
activate,
|
||||
deactivate,
|
||||
} = this.state;
|
||||
|
||||
const classes = classNames(className, 'icon-button', {
|
||||
active,
|
||||
disabled,
|
||||
inverted,
|
||||
activate,
|
||||
deactivate,
|
||||
overlayed: overlay,
|
||||
});
|
||||
|
||||
const flipDeg = flip ? -180 : -360;
|
||||
const rotateDeg = active ? flipDeg : 0;
|
||||
|
||||
const motionDefaultStyle = {
|
||||
rotate: rotateDeg,
|
||||
};
|
||||
|
||||
const springOpts = {
|
||||
stiffness: this.props.flip ? 60 : 120,
|
||||
damping: 7,
|
||||
};
|
||||
const motionStyle = {
|
||||
rotate: animate ? spring(rotateDeg, springOpts) : 0,
|
||||
};
|
||||
|
||||
if (!animate) {
|
||||
// Perf optimization: avoid unnecessary <Motion> components unless
|
||||
// we actually need to animate.
|
||||
return (
|
||||
<button
|
||||
aria-label={title}
|
||||
aria-pressed={pressed}
|
||||
aria-expanded={expanded}
|
||||
title={title}
|
||||
className={classes}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
style={style}
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon id={icon} fixedWidth aria-hidden='true' />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Motion defaultStyle={motionDefaultStyle} style={motionStyle}>
|
||||
{({ rotate }) =>
|
||||
(<button
|
||||
aria-label={title}
|
||||
aria-pressed={pressed}
|
||||
aria-expanded={expanded}
|
||||
title={title}
|
||||
className={classes}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
style={style}
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon id={icon} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />
|
||||
{this.props.label}
|
||||
</button>)
|
||||
}
|
||||
</Motion>
|
||||
<button
|
||||
aria-label={title}
|
||||
aria-pressed={pressed}
|
||||
aria-expanded={expanded}
|
||||
title={title}
|
||||
className={classes}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
style={style}
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon id={icon} fixedWidth aria-hidden='true' />
|
||||
{this.props.label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ class Item extends React.PureComponent {
|
|||
onClick: PropTypes.func.isRequired,
|
||||
displayWidth: PropTypes.number,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
autoplay: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -68,9 +69,13 @@ class Item extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
getAutoPlay() {
|
||||
return this.props.autoplay || autoPlayGif;
|
||||
}
|
||||
|
||||
hoverToPlay () {
|
||||
const { attachment } = this.props;
|
||||
return !autoPlayGif && attachment.get('type') === 'gifv';
|
||||
return !this.getAutoPlay() && attachment.get('type') === 'gifv';
|
||||
}
|
||||
|
||||
handleClick = (e) => {
|
||||
|
@ -179,7 +184,7 @@ class Item extends React.PureComponent {
|
|||
if (attachment.get('type') === 'unknown') {
|
||||
return (
|
||||
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
||||
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}>
|
||||
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} target='_blank' rel='noopener noreferrer'>
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
|
||||
</a>
|
||||
</div>
|
||||
|
@ -207,6 +212,7 @@ class Item extends React.PureComponent {
|
|||
href={attachment.get('remote_url') || originalUrl}
|
||||
onClick={this.handleClick}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<img
|
||||
className={letterbox ? 'letterbox' : null}
|
||||
|
@ -221,7 +227,7 @@ class Item extends React.PureComponent {
|
|||
</a>
|
||||
);
|
||||
} else if (attachment.get('type') === 'gifv') {
|
||||
const autoPlay = !isIOS() && autoPlayGif;
|
||||
const autoPlay = !isIOS() && this.getAutoPlay();
|
||||
|
||||
thumbnail = (
|
||||
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
|
||||
|
@ -270,6 +276,7 @@ class MediaGallery extends React.PureComponent {
|
|||
defaultWidth: PropTypes.number,
|
||||
cacheWidth: PropTypes.func,
|
||||
visible: PropTypes.bool,
|
||||
autoplay: PropTypes.bool,
|
||||
onToggleVisibility: PropTypes.func,
|
||||
};
|
||||
|
||||
|
@ -327,7 +334,7 @@ class MediaGallery extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { media, intl, sensitive, letterbox, fullwidth, defaultWidth } = this.props;
|
||||
const { media, intl, sensitive, letterbox, fullwidth, defaultWidth, autoplay } = this.props;
|
||||
const { visible } = this.state;
|
||||
const size = media.take(4).size;
|
||||
const uncached = media.every(attachment => attachment.get('type') === 'unknown');
|
||||
|
@ -349,9 +356,9 @@ class MediaGallery extends React.PureComponent {
|
|||
}
|
||||
|
||||
if (this.isStandaloneEligible()) {
|
||||
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />;
|
||||
children = <Item standalone autoplay={autoplay} onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />;
|
||||
} else {
|
||||
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} displayWidth={width} visible={visible || uncached} />);
|
||||
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} displayWidth={width} visible={visible || uncached} />);
|
||||
}
|
||||
|
||||
if (uncached) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import 'wicg-inert';
|
||||
import createHistory from 'history/createBrowserHistory';
|
||||
|
||||
export default class ModalRoot extends React.PureComponent {
|
||||
|
@ -61,15 +62,22 @@ export default class ModalRoot extends React.PureComponent {
|
|||
} else if (!nextProps.children) {
|
||||
this.setState({ revealed: false });
|
||||
}
|
||||
if (!nextProps.children && !!this.props.children) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
this.activeElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (!this.props.children && !!prevProps.children) {
|
||||
this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
|
||||
|
||||
// Because of the wicg-inert polyfill, the activeElement may not be
|
||||
// immediately selectable, we have to wait for observers to run, as
|
||||
// described in https://github.com/WICG/inert#performance-and-gotchas
|
||||
Promise.resolve().then(() => {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
this.activeElement = null;
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
this.handleModalClose();
|
||||
}
|
||||
if (this.props.children) {
|
||||
|
|
|
@ -39,7 +39,8 @@ class Poll extends ImmutablePureComponent {
|
|||
|
||||
static getDerivedStateFromProps (props, state) {
|
||||
const { poll, intl } = props;
|
||||
const expired = poll.get('expired') || (new Date(poll.get('expires_at'))).getTime() < intl.now();
|
||||
const expires_at = poll.get('expires_at');
|
||||
const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < intl.now();
|
||||
return (expired === state.expired) ? null : { expired };
|
||||
}
|
||||
|
||||
|
@ -66,9 +67,7 @@ class Poll extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleOptionChange = e => {
|
||||
const { target: { value } } = e;
|
||||
|
||||
_toggleOption = value => {
|
||||
if (this.props.poll.get('multiple')) {
|
||||
const tmp = { ...this.state.selected };
|
||||
if (tmp[value]) {
|
||||
|
@ -82,8 +81,20 @@ class Poll extends ImmutablePureComponent {
|
|||
tmp[value] = true;
|
||||
this.setState({ selected: tmp });
|
||||
}
|
||||
}
|
||||
|
||||
handleOptionChange = ({ target: { value } }) => {
|
||||
this._toggleOption(value);
|
||||
};
|
||||
|
||||
handleOptionKeyPress = (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
this._toggleOption(e.target.getAttribute('data-index'));
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
handleVote = () => {
|
||||
if (this.props.disabled) {
|
||||
return;
|
||||
|
@ -134,7 +145,17 @@ class Poll extends ImmutablePureComponent {
|
|||
disabled={disabled}
|
||||
/>
|
||||
|
||||
{!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
|
||||
{!showResults && (
|
||||
<span
|
||||
className={classNames('poll__input', { checkbox: poll.get('multiple'), active })}
|
||||
tabIndex='0'
|
||||
role={poll.get('multiple') ? 'checkbox' : 'radio'}
|
||||
onKeyPress={this.handleOptionKeyPress}
|
||||
aria-checked={active}
|
||||
aria-label={option.get('title')}
|
||||
data-index={optionIndex}
|
||||
/>
|
||||
)}
|
||||
{showResults && <span className='poll__number'>
|
||||
{!!voted && <Icon id='check' className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />}
|
||||
{Math.round(percent)}%
|
||||
|
|
|
@ -3,6 +3,7 @@ import { injectIntl, defineMessages } from 'react-intl';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
const messages = defineMessages({
|
||||
today: { id: 'relative_time.today', defaultMessage: 'today' },
|
||||
just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
|
||||
seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' },
|
||||
minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
|
||||
|
@ -65,12 +66,14 @@ const getUnitDelay = units => {
|
|||
}
|
||||
};
|
||||
|
||||
export const timeAgoString = (intl, date, now, year) => {
|
||||
export const timeAgoString = (intl, date, now, year, timeGiven = true) => {
|
||||
const delta = now - date.getTime();
|
||||
|
||||
let relativeTime;
|
||||
|
||||
if (delta < 10 * SECOND) {
|
||||
if (delta < DAY && !timeGiven) {
|
||||
relativeTime = intl.formatMessage(messages.today);
|
||||
} else if (delta < 10 * SECOND) {
|
||||
relativeTime = intl.formatMessage(messages.just_now);
|
||||
} else if (delta < 7 * DAY) {
|
||||
if (delta < MINUTE) {
|
||||
|
@ -91,12 +94,14 @@ export const timeAgoString = (intl, date, now, year) => {
|
|||
return relativeTime;
|
||||
};
|
||||
|
||||
const timeRemainingString = (intl, date, now) => {
|
||||
const timeRemainingString = (intl, date, now, timeGiven = true) => {
|
||||
const delta = date.getTime() - now;
|
||||
|
||||
let relativeTime;
|
||||
|
||||
if (delta < 10 * SECOND) {
|
||||
if (delta < DAY && !timeGiven) {
|
||||
relativeTime = intl.formatMessage(messages.today);
|
||||
} else if (delta < 10 * SECOND) {
|
||||
relativeTime = intl.formatMessage(messages.moments_remaining);
|
||||
} else if (delta < MINUTE) {
|
||||
relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) });
|
||||
|
@ -173,8 +178,9 @@ class RelativeTimestamp extends React.Component {
|
|||
render () {
|
||||
const { timestamp, intl, year, futureDate } = this.props;
|
||||
|
||||
const timeGiven = timestamp.includes('T');
|
||||
const date = new Date(timestamp);
|
||||
const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now) : timeAgoString(intl, date, this.state.now, year);
|
||||
const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven);
|
||||
|
||||
return (
|
||||
<time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>
|
||||
|
|
|
@ -376,6 +376,22 @@ class Status extends ImmutablePureComponent {
|
|||
this.props.onOpenVideo(media, startTime);
|
||||
}
|
||||
|
||||
handleHotkeyOpenMedia = e => {
|
||||
const { status, onOpenMedia, onOpenVideo } = this.props;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
if (status.get('media_attachments').size > 0) {
|
||||
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);
|
||||
} else {
|
||||
onOpenMedia(status.get('media_attachments'), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleHotkeyReply = e => {
|
||||
e.preventDefault();
|
||||
this.props.onReply(this.props.status, this.context.router.history);
|
||||
|
@ -503,6 +519,7 @@ class Status extends ImmutablePureComponent {
|
|||
bookmark: this.handleHotkeyBookmark,
|
||||
toggleCollapse: this.handleHotkeyCollapse,
|
||||
toggleSensitive: this.handleHotkeyToggleSensitive,
|
||||
openMedia: this.handleHotkeyOpenMedia,
|
||||
};
|
||||
|
||||
if (hidden) {
|
||||
|
|
|
@ -265,7 +265,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
replyTitle = intl.formatMessage(messages.replyAll);
|
||||
}
|
||||
|
||||
const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && (
|
||||
const shareButton = ('share' in navigator) && publicStatus && (
|
||||
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
|
||||
);
|
||||
|
||||
|
|
|
@ -42,6 +42,14 @@ const isLinkMisleading = (link) => {
|
|||
const linkText = linkTextParts.join('');
|
||||
const targetURL = new URL(link.href);
|
||||
|
||||
if (targetURL.protocol === 'magnet:') {
|
||||
return !linkText.startsWith('magnet:');
|
||||
}
|
||||
|
||||
if (targetURL.protocol === 'xmpp:') {
|
||||
return !(linkText === targetURL.href || 'xmpp:' + linkText === targetURL.href);
|
||||
}
|
||||
|
||||
// The following may not work with international domain names
|
||||
if (textMatchesTarget(linkText, targetURL.origin, targetURL.host) || textMatchesTarget(linkText.toLowerCase(), targetURL.origin, targetURL.host)) {
|
||||
return false;
|
||||
|
@ -120,9 +128,19 @@ export default class StatusContent extends React.PureComponent {
|
|||
if (tagLinks && isLinkMisleading(link)) {
|
||||
// Add a tag besides the link to display its origin
|
||||
|
||||
const url = new URL(link.href);
|
||||
const tag = document.createElement('span');
|
||||
tag.classList.add('link-origin-tag');
|
||||
tag.textContent = `[${new URL(link.href).host}]`;
|
||||
switch (url.protocol) {
|
||||
case 'xmpp:':
|
||||
tag.textContent = `[${url.href}]`;
|
||||
break;
|
||||
case 'magnet:':
|
||||
tag.textContent = '(magnet)';
|
||||
break;
|
||||
default:
|
||||
tag.textContent = `[${url.host}]`;
|
||||
}
|
||||
link.insertAdjacentText('beforeend', ' ');
|
||||
link.insertAdjacentElement('beforeend', tag);
|
||||
}
|
||||
|
@ -133,7 +151,7 @@ export default class StatusContent extends React.PureComponent {
|
|||
}
|
||||
|
||||
link.setAttribute('target', '_blank');
|
||||
link.setAttribute('rel', 'noopener');
|
||||
link.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ export default class StatusHeader extends React.PureComponent {
|
|||
target='_blank'
|
||||
className='status__avatar'
|
||||
onClick={this.handleAccountClick}
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
{statusAvatar}
|
||||
</a>
|
||||
|
@ -64,6 +65,7 @@ export default class StatusHeader extends React.PureComponent {
|
|||
target='_blank'
|
||||
className='status__display-name'
|
||||
onClick={this.handleAccountClick}
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<DisplayName account={account} others={otherAccounts} />
|
||||
</a>
|
||||
|
|
|
@ -103,7 +103,7 @@ class StatusIcons extends React.PureComponent {
|
|||
{collapsible ? (
|
||||
<IconButton
|
||||
className='status__collapse-button'
|
||||
animate flip
|
||||
animate
|
||||
active={collapsed}
|
||||
title={
|
||||
collapsed ?
|
||||
|
|
|
@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
import { me } from 'flavours/glitch/util/initial_state';
|
||||
|
||||
export default class StatusPrepend extends React.PureComponent {
|
||||
|
||||
|
@ -64,12 +65,21 @@ export default class StatusPrepend extends React.PureComponent {
|
|||
/>
|
||||
);
|
||||
case 'poll':
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='notification.poll'
|
||||
defaultMessage='A poll you have voted in has ended'
|
||||
/>
|
||||
);
|
||||
if (me === account.get('id')) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='notification.own_poll'
|
||||
defaultMessage='Your poll has ended'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='notification.poll'
|
||||
defaultMessage='A poll you have voted in has ended'
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -214,7 +214,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
|||
title={intl.formatMessage(messages.editFilter)}
|
||||
href={filterEditLink(filter.get('id'))}
|
||||
>
|
||||
<Icon icon='pencil' />
|
||||
<Icon id='pencil' />
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
|
|
|
@ -232,9 +232,18 @@ class Header extends ImmutablePureComponent {
|
|||
const content = { __html: account.get('note_emojified') };
|
||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||
const fields = account.get('fields');
|
||||
const badge = account.get('bot') ? (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>) : null;
|
||||
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
|
||||
|
||||
let badge;
|
||||
|
||||
if (account.get('bot')) {
|
||||
badge = (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>);
|
||||
} else if (account.get('group')) {
|
||||
badge = (<div className='account-role group'><FormattedMessage id='account.badges.group' defaultMessage='Group' /></div>);
|
||||
} else {
|
||||
badge = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
|
||||
<div className='account__header__image'>
|
||||
|
@ -247,7 +256,7 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
<div className='account__header__bar'>
|
||||
<div className='account__header__tabs'>
|
||||
<a className='avatar' href={account.get('url')} rel='noopener' target='_blank'>
|
||||
<a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'>
|
||||
<Avatar account={account} size={90} />
|
||||
</a>
|
||||
|
||||
|
@ -276,10 +285,10 @@ class Header extends ImmutablePureComponent {
|
|||
<dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} />
|
||||
|
||||
<dd className='verified'>
|
||||
<a href={proof.get('proof_url')} target='_blank' rel='noopener'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
|
||||
<a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
|
||||
<Icon id='check' className='verified__mark' />
|
||||
</span></a>
|
||||
<a href={proof.get('profile_url')} target='_blank' rel='noopener'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a>
|
||||
<a href={proof.get('profile_url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a>
|
||||
</dd>
|
||||
</dl>
|
||||
))}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { decode } from 'blurhash';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
import { autoPlayGif, displayMedia } from 'flavours/glitch/util/initial_state';
|
||||
import classNames from 'classnames';
|
||||
import { decode } from 'blurhash';
|
||||
import { isIOS } from 'flavours/glitch/util/is_mobile';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
export default class MediaItem extends ImmutablePureComponent {
|
||||
|
||||
|
@ -148,7 +148,7 @@ export default class MediaItem extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<div className='account-gallery__item' style={{ width, height }}>
|
||||
<a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick} title={title}>
|
||||
<a className='media-gallery__item-thumbnail' href={status.get('url')} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'>
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} />
|
||||
{visible ? thumbnail : icon}
|
||||
</a>
|
||||
|
|
|
@ -112,6 +112,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.account_timeline' defaultMessage='No toots here!' />}
|
||||
bindToDocument={!multiColumn}
|
||||
timelineId='account'
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
|
|
|
@ -12,6 +12,7 @@ const messages = defineMessages({
|
|||
pause: { id: 'video.pause', defaultMessage: 'Pause' },
|
||||
mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
|
||||
unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
|
||||
download: { id: 'video.download', defaultMessage: 'Download file' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
|
@ -202,6 +203,7 @@ class Audio extends React.PureComponent {
|
|||
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
|
||||
|
||||
<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
|
||||
|
||||
<div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
|
||||
|
||||
<span
|
||||
|
@ -217,6 +219,14 @@ class Audio extends React.PureComponent {
|
|||
<span className='video-player__time-total'>{formatTime(this.state.duration || Math.floor(this.props.duration))}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='video-player__buttons right'>
|
||||
<button type='button' aria-label={intl.formatMessage(messages.download)}>
|
||||
<a className='video-player__download__icon' href={this.props.src} download>
|
||||
<Icon id={'download'} fixedWidth />
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -14,15 +14,16 @@ const messages = defineMessages({
|
|||
title: { id: 'column.community', defaultMessage: 'Local timeline' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { onlyMedia, columnId }) => {
|
||||
const mapStateToProps = (state, { columnId }) => {
|
||||
const uuid = 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', 'community', 'other', 'onlyMedia']);
|
||||
const timelineState = state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`]);
|
||||
|
||||
return {
|
||||
hasUnread: !!timelineState && (timelineState.get('unread') > 0 || timelineState.get('pendingItems').size > 0),
|
||||
onlyMedia: (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'community', 'other', 'onlyMedia']),
|
||||
hasUnread: !!timelineState && timelineState.get('unread') > 0,
|
||||
onlyMedia,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
handleSubmit = (overriddenVisibility = null) => {
|
||||
const { textarea: { value }, uploadForm } = this;
|
||||
const {
|
||||
onChange,
|
||||
|
@ -106,6 +106,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
text,
|
||||
mediaDescriptionConfirmation,
|
||||
onMediaDescriptionConfirm,
|
||||
onChangeVisibility,
|
||||
} = this.props;
|
||||
|
||||
// If something changes inside the textarea, then we update the
|
||||
|
@ -124,6 +125,9 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
const firstWithoutDescription = media.find(item => !item.get('description'));
|
||||
onMediaDescriptionConfirm(this.context.router ? this.context.router.history : null, firstWithoutDescription.get('id'));
|
||||
} else if (onSubmit) {
|
||||
if (onChangeVisibility && overriddenVisibility) {
|
||||
onChangeVisibility(overriddenVisibility);
|
||||
}
|
||||
onSubmit(this.context.router ? this.context.router.history : null);
|
||||
}
|
||||
}
|
||||
|
@ -152,13 +156,9 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
// Handles the secondary submit button.
|
||||
handleSecondarySubmit = () => {
|
||||
const {
|
||||
onChangeVisibility,
|
||||
sideArm,
|
||||
} = this.props;
|
||||
if (sideArm !== 'none' && onChangeVisibility) {
|
||||
onChangeVisibility(sideArm);
|
||||
}
|
||||
this.handleSubmit();
|
||||
this.handleSubmit(sideArm === 'none' ? null : sideArm);
|
||||
}
|
||||
|
||||
// Selects a suggestion from the autofill.
|
||||
|
|
|
@ -38,8 +38,12 @@ class Publisher extends ImmutablePureComponent {
|
|||
sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']),
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { countText, disabled, intl, onSecondarySubmit, onSubmit, privacy, sideArm } = this.props;
|
||||
const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm } = this.props;
|
||||
|
||||
const diff = maxChars - length(countText || '');
|
||||
const computedClass = classNames('composer--publisher', {
|
||||
|
@ -105,7 +109,7 @@ class Publisher extends ImmutablePureComponent {
|
|||
}
|
||||
}()}
|
||||
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${privacy}.short` })}`}
|
||||
onClick={onSubmit}
|
||||
onClick={this.handleSubmit}
|
||||
disabled={disabled || diff < 0}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -12,11 +12,12 @@ function mapStateToProps (state) {
|
|||
const spoilersAlwaysOn = state.getIn(['local_settings', 'always_show_spoilers_field']);
|
||||
const poll = state.getIn(['compose', 'poll']);
|
||||
const media = state.getIn(['compose', 'media_attachments']);
|
||||
const pending_media = state.getIn(['compose', 'pending_media_attachments']);
|
||||
return {
|
||||
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','),
|
||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||
hasPoll: !!poll,
|
||||
allowMedia: !poll && (media ? media.size < 4 && !media.some(item => ['video', 'audio'].includes(item.get('type'))) : true),
|
||||
allowMedia: !poll && (media ? media.size + pending_media < 4 && !media.some(item => ['video', 'audio'].includes(item.get('type'))) : pending_media < 4),
|
||||
hasMedia: media && !!media.size,
|
||||
allowPoll: !(media && !!media.size),
|
||||
showContentTypeChoice: state.getIn(['local_settings', 'show_content_type_choice']),
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue