diff --git a/.github/ISSUE_TEMPLATE/1.bug_report.yml b/.github/ISSUE_TEMPLATE/1.bug_report.yml
index 9cdf813f7b..cdd08d2b0d 100644
--- a/.github/ISSUE_TEMPLATE/1.bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/1.bug_report.yml
@@ -31,6 +31,11 @@ body:
description: What happened?
validations:
required: true
+ - type: textarea
+ attributes:
+ label: Detailed description
+ validations:
+ required: false
- type: textarea
attributes:
label: Specifications
@@ -38,5 +43,14 @@ body:
What version or commit hash of Mastodon did you find this bug in?
If a front-end issue, what browser and operating systems were you using?
+ placeholder: |
+ Mastodon 3.5.3 (or Edge)
+ Ruby 2.7.6 (or v3.1.2)
+ Node.js 16.18.0
+
+ Google Chrome 106.0.5249.119
+ Firefox 105.0.3
+
+ etc...
validations:
required: true
diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml
index 3d232120be..58c5587231 100644
--- a/.github/workflows/build-image.yml
+++ b/.github/workflows/build-image.yml
@@ -34,7 +34,8 @@ jobs:
latest=auto
tags: |
type=edge,branch=main
- type=match,pattern=v(.*),group=0
+ type=pep440,pattern={{raw}}
+ type=pep440,pattern=v{{major}}.{{minor}}
type=ref,event=pr
- uses: docker/build-push-action@v3
with:
@@ -42,5 +43,5 @@ jobs:
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
- cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/mastodon:latest
+ cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/mastodon:edge
cache-to: type=inline
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ed4cdd881a..c161d95dea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,132 @@ Changelog
All notable changes to this project will be documented in this file.
+## [Unreleased]
+
+Some of the features in this release have been funded through the [NGI0 Discovery](https://nlnet.nl/discovery) Fund, a fund established by [NLnet](https://nlnet.nl/) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu/) programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 825322.
+
+### Added
+
+- Add ability to filter followed accounts' posts by language ([Gargron](https://github.com/mastodon/mastodon/pull/19095), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19268))
+- **Add ability to follow hashtags** ([Gargron](https://github.com/mastodon/mastodon/pull/18809), [Gargron](https://github.com/mastodon/mastodon/pull/18862), [Gargron](https://github.com/mastodon/mastodon/pull/19472), [noellabo](https://github.com/mastodon/mastodon/pull/18924))
+- Add ability to filter individual posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18945))
+- **Add ability to translate posts** ([Gargron](https://github.com/mastodon/mastodon/pull/19218), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19433), [Gargron](https://github.com/mastodon/mastodon/pull/19453), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19434), [Gargron](https://github.com/mastodon/mastodon/pull/19388), [ykzts](https://github.com/mastodon/mastodon/pull/19244), [Gargron](https://github.com/mastodon/mastodon/pull/19245))
+- Add featured tags to web UI ([noellabo](https://github.com/mastodon/mastodon/pull/19408), [noellabo](https://github.com/mastodon/mastodon/pull/19380), [noellabo](https://github.com/mastodon/mastodon/pull/19358), [noellabo](https://github.com/mastodon/mastodon/pull/19409), [Gargron](https://github.com/mastodon/mastodon/pull/19382), [ykzts](https://github.com/mastodon/mastodon/pull/19418), [noellabo](https://github.com/mastodon/mastodon/pull/19403), [noellabo](https://github.com/mastodon/mastodon/pull/19404), [Gargron](https://github.com/mastodon/mastodon/pull/19398))
+- **Add support for language preferences for trending statuses and links** ([Gargron](https://github.com/mastodon/mastodon/pull/18288), [Gargron](https://github.com/mastodon/mastodon/pull/19349), [ykzts](https://github.com/mastodon/mastodon/pull/19335))
+ - Previously, you could only see trends in your current language
+ - For less popular languages, that meant empty trends
+ - Now, trends in your preferred languages' are shown on top, with others beneath
+- Add server rules to sign-up flow ([Gargron](https://github.com/mastodon/mastodon/pull/19296))
+- Add privacy icons to report modal in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19190))
+- Add `noopener` to links to remote profiles in web UI ([shleeable](https://github.com/mastodon/mastodon/pull/19014))
+- Add warning for sensitive audio posts in web UI ([rgroothuijsen](https://github.com/mastodon/mastodon/pull/17885))
+- Add language attribute to posts in web UI ([tribela](https://github.com/mastodon/mastodon/pull/18544))
+- Add meta tag for official iOS app ([Gargron](https://github.com/mastodon/mastodon/pull/16599))
+- Add support for uploading WebP files ([Saiv46](https://github.com/mastodon/mastodon/pull/18506))
+- Add support for uploading `audio/vnd.wave` files ([tribela](https://github.com/mastodon/mastodon/pull/18737))
+- Add more debug information when processing remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15605), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19209))
+- **Add retention policy for cached content and media** ([Gargron](https://github.com/mastodon/mastodon/pull/19232), [zunda](https://github.com/mastodon/mastodon/pull/19478), [Gargron](https://github.com/mastodon/mastodon/pull/19458), [Gargron](https://github.com/mastodon/mastodon/pull/19248))
+ - Set for how long remote posts or media should be cached on your server
+ - Hands-off alternative to `tootctl` commands
+- **Add customizable user roles** ([Gargron](https://github.com/mastodon/mastodon/pull/18641), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18812), [Gargron](https://github.com/mastodon/mastodon/pull/19040), [tribela](https://github.com/mastodon/mastodon/pull/18825), [tribela](https://github.com/mastodon/mastodon/pull/18826), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18776), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18777), [unextro](https://github.com/mastodon/mastodon/pull/18786), [tribela](https://github.com/mastodon/mastodon/pull/18824), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19436))
+ - Previously, there were 3 hard-coded roles, user, moderator, and admin
+ - Create your own roles and decide which permissions they should have
+- Add notifications for new reports ([Gargron](https://github.com/mastodon/mastodon/pull/18697), [Gargron](https://github.com/mastodon/mastodon/pull/19475))
+- Add ability to select all accounts matching search for batch actions in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/19053), [Gargron](https://github.com/mastodon/mastodon/pull/19054))
+- Add ability to view previous edits of a status in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/19462))
+- Add ability to block sign-ups from IP ([Gargron](https://github.com/mastodon/mastodon/pull/19037))
+- **Add webhooks to admin UI** ([Gargron](https://github.com/mastodon/mastodon/pull/18510))
+- Add admin API for managing domain allows ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18668))
+- Add admin API for managing domain blocks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18247))
+- Add admin API for managing e-mail domain blocks ([Gargron](https://github.com/mastodon/mastodon/pull/19066))
+- Add admin API for managing canonical e-mail blocks ([Gargron](https://github.com/mastodon/mastodon/pull/19067))
+- Add admin API for managing IP blocks ([Gargron](https://github.com/mastodon/mastodon/pull/19065))
+- Add `services` and `metadata` to the NodeInfo endpoint ([MFTabriz](https://github.com/mastodon/mastodon/pull/18563))
+- Add `--remove-role` option to `tootctl accounts modify` ([Gargron](https://github.com/mastodon/mastodon/pull/19477))
+- Add `--days` option to `tootctl media refresh` ([tribela](https://github.com/mastodon/mastodon/pull/18425))
+- Add `EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION` environment variable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18642))
+- Add `IP_RETENTION_PERIOD` and `SESSION_RETENTION_PERIOD` environment variables ([kescherCode](https://github.com/mastodon/mastodon/pull/18757))
+- Add `http_hidden_proxy` environment variable ([tribela](https://github.com/mastodon/mastodon/pull/18427))
+
+### Changed
+
+- **Change brand color and logotypes** ([Gargron](https://github.com/mastodon/mastodon/pull/18592), [Gargron](https://github.com/mastodon/mastodon/pull/18639), [Gargron](https://github.com/mastodon/mastodon/pull/18691), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18634), [Gargron](https://github.com/mastodon/mastodon/pull/19254), [mayaeh](https://github.com/mastodon/mastodon/pull/18710))
+- **Change post editing to be enabled in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/19103))
+- **Change web UI to work for logged-out users** ([Gargron](https://github.com/mastodon/mastodon/pull/18961), [Gargron](https://github.com/mastodon/mastodon/pull/19250), [Gargron](https://github.com/mastodon/mastodon/pull/19294), [Gargron](https://github.com/mastodon/mastodon/pull/19306), [Gargron](https://github.com/mastodon/mastodon/pull/19315), [ykzts](https://github.com/mastodon/mastodon/pull/19322), [Gargron](https://github.com/mastodon/mastodon/pull/19412), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19437), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19415), [Gargron](https://github.com/mastodon/mastodon/pull/19348), [Gargron](https://github.com/mastodon/mastodon/pull/19295), [Gargron](https://github.com/mastodon/mastodon/pull/19422), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19414), [Gargron](https://github.com/mastodon/mastodon/pull/19319), [Gargron](https://github.com/mastodon/mastodon/pull/19345), [Gargron](https://github.com/mastodon/mastodon/pull/19310), [Gargron](https://github.com/mastodon/mastodon/pull/19301), [Gargron](https://github.com/mastodon/mastodon/pull/19423), [ykzts](https://github.com/mastodon/mastodon/pull/19471), [ykzts](https://github.com/mastodon/mastodon/pull/19333), [ykzts](https://github.com/mastodon/mastodon/pull/19337), [ykzts](https://github.com/mastodon/mastodon/pull/19272), [ykzts](https://github.com/mastodon/mastodon/pull/19468), [Gargron](https://github.com/mastodon/mastodon/pull/19466), [Gargron](https://github.com/mastodon/mastodon/pull/19457), [Gargron](https://github.com/mastodon/mastodon/pull/19426), [Gargron](https://github.com/mastodon/mastodon/pull/19427), [Gargron](https://github.com/mastodon/mastodon/pull/19421), [Gargron](https://github.com/mastodon/mastodon/pull/19417), [Gargron](https://github.com/mastodon/mastodon/pull/19413), [Gargron](https://github.com/mastodon/mastodon/pull/19397), [Gargron](https://github.com/mastodon/mastodon/pull/19387), [Gargron](https://github.com/mastodon/mastodon/pull/19396), [Gargron](https://github.com/mastodon/mastodon/pull/19385), [ykzts](https://github.com/mastodon/mastodon/pull/19334), [ykzts](https://github.com/mastodon/mastodon/pull/19329), [Gargron](https://github.com/mastodon/mastodon/pull/19324), [Gargron](https://github.com/mastodon/mastodon/pull/19318), [Gargron](https://github.com/mastodon/mastodon/pull/19316), [Gargron](https://github.com/mastodon/mastodon/pull/19263), [trwnh](https://github.com/mastodon/mastodon/pull/19305), [ykzts](https://github.com/mastodon/mastodon/pull/19273))
+ - The web app can now be accessed without being logged in
+ - No more `/web` prefix on web app paths
+ - Profiles, posts, and other public pages now use the same interface for logged in and logged out users
+ - The web app displays a server information banner
+ - Pop-up windows for remote interaction have been replaced with a modal window
+ - No need to type in your username for remote interaction, copy-paste-to-search method explained
+ - Various hints throughout the app explain what the different timelines are
+ - New about page design
+ - New privacy policy page design shows when the policy was last updated
+ - All sections of the web app now have appropriate window titles
+ - The layout of the interface has been streamlined between different screen sizes
+ - Posts now use more horizontal space
+- Change label of publish button to be "Publish" again in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18583))
+- Change language to be carried over on reply in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18557))
+- Change "Unfollow" to "Cancel follow request" when request still pending in web UI ([prplecake](https://github.com/mastodon/mastodon/pull/19363))
+- **Change post filtering system** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18058), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19050), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18894), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/19051), [noellabo](https://github.com/mastodon/mastodon/pull/18923), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18956), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18744))
+ - Filtered keywords and phrases can now be grouped into named categories
+ - Filtered posts show which exact filter was hit
+ - Individual posts can be added to a filter
+ - You can peek inside filtered posts anyway
+- Change path of privacy policy page from `/terms` to `/privacy-policy` ([Gargron](https://github.com/mastodon/mastodon/pull/19249))
+- Change how hashtags are normalized ([Gargron](https://github.com/mastodon/mastodon/pull/18795), [Gargron](https://github.com/mastodon/mastodon/pull/18863), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18854))
+- Change public timelines to be filtered by current locale by default ([Gargron](https://github.com/mastodon/mastodon/pull/19291))
+- Change settings area to be separated into categories in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/19407))
+- Change "No accounts selected" errors to use the appropriate noun in admin UI ([prplecake](https://github.com/mastodon/mastodon/pull/19356))
+- Change e-mail domain blocks to match subdomains of blocked domains ([Gargron](https://github.com/mastodon/mastodon/pull/18979))
+- Change custom emoji file size limit from 50 KB to 256 KB ([Gargron](https://github.com/mastodon/mastodon/pull/18788))
+- Change "Allow trends without prior review" setting to also work for trending posts ([Gargron](https://github.com/mastodon/mastodon/pull/17977))
+- Change search API to be accessible without being logged in ([Gargron](https://github.com/mastodon/mastodon/pull/18963), [Gargron](https://github.com/mastodon/mastodon/pull/19326))
+- Change following and followers API to be accessible without being logged in ([Gargron](https://github.com/mastodon/mastodon/pull/18964))
+- Change Helm configuration ([deepy](https://github.com/mastodon/mastodon/pull/18997), [jgsmith](https://github.com/mastodon/mastodon/pull/18415), [deepy](https://github.com/mastodon/mastodon/pull/18941))
+
+### Removed
+
+- Remove setting that disables account deletes ([Gargron](https://github.com/mastodon/mastodon/pull/17683))
+- Remove digest e-mails ([Gargron](https://github.com/mastodon/mastodon/pull/17985))
+- Remove unnecessary sections from welcome e-mail ([Gargron](https://github.com/mastodon/mastodon/pull/19299))
+- Remove item titles from RSS feeds ([Gargron](https://github.com/mastodon/mastodon/pull/18640))
+- Remove volume number from hashtags in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19253))
+- Remove Nanobox configuration ([tonyjiang](https://github.com/mastodon/mastodon/pull/17881))
+
+### Fixed
+
+- Fix OCR not working due to Content Security Policy in web UI ([prplecake](https://github.com/mastodon/mastodon/pull/18817))
+- Fix `nofollow` rel being removed in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19455))
+- Fix language dropdown causing zoom on mobile devices in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19428))
+- Fix button to dismiss suggestions not showing up in search results in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19325))
+- Fix language dropdown sometimes not appearing in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/19246))
+- Fix quickly switching notification filters resulting in empty or incorrect list in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19052), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18960))
+- Fix media modal link button in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18877))
+- Fix error upon successful account migration ([Gargron](https://github.com/mastodon/mastodon/pull/19386))
+- Fix negatives values in search index causing queries to fail ([Gargron](https://github.com/mastodon/mastodon/pull/19464), [Gargron](https://github.com/mastodon/mastodon/pull/19481))
+- Fix error when searching for invalid URL ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18580))
+- Fix IP blocks not having a unique index ([Gargron](https://github.com/mastodon/mastodon/pull/19456))
+- Fix remote account in contact account setting not being used ([Gargron](https://github.com/mastodon/mastodon/pull/19351))
+- Fix swallowing mentions of unconfirmed/unapproved users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19191))
+- Fix incorrect and slow cache invalidation when blocking domain and removing media attachments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19062))
+- Fix HTTPs redirect behaviour when running as I2P service ([gi-yt](https://github.com/mastodon/mastodon/pull/18929))
+- Fix deleted pinned posts potentially counting towards the pinned posts limit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19005))
+- Fix compatibility with OpenSSL 3.0 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18449))
+- Fix error when a remote report includes a private post the server has no access to ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18760))
+- Fix suspicious sign-in mails never being sent ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18599))
+- Fix fallback locale when somehow user's locale is an empty string ([tribela](https://github.com/mastodon/mastodon/pull/18543))
+- Fix avatar/header not being deleted locally when deleted on remote account ([tribela](https://github.com/mastodon/mastodon/pull/18973))
+- Fix missing `,` in Blurhash validation ([noellabo](https://github.com/mastodon/mastodon/pull/18660))
+- Fix order by most recent not working for relationships page in admin UI ([tribela](https://github.com/mastodon/mastodon/pull/18996))
+- Fix uncaught error when invalid date is supplied to API ([Gargron](https://github.com/mastodon/mastodon/pull/19480))
+- Fix REST API sometimes returning HTML on error ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19135))
+- Fix ambiguous column names in `tootctl media refresh` ([tribela](https://github.com/mastodon/mastodon/pull/19206))
+- Fix ambiguous column names in `tootctl search deploy` ([mashirozx](https://github.com/mastodon/mastodon/pull/18993))
+- Fix `CDN_HOST` not being used in some asset URLs ([tribela](https://github.com/mastodon/mastodon/pull/18662))
+- Fix `CAS_DISPLAY_NAME`, `SAML_DISPLAY_NAME` and `OIDC_DISPLAY_NAME` being ignored ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18568))
+- Fix various typos in comments throughout the codebase ([luzpaz](https://github.com/mastodon/mastodon/pull/18604))
+
## [3.5.3] - 2022-05-26
### Added
@@ -75,7 +201,7 @@ All notable changes to this project will be documented in this file.
- Remove IP matching from e-mail domain blocks ([Gargron](https://github.com/mastodon/mastodon/pull/18190))
- The IPs of the blocked e-mail domain or its MX records are no longer checked
- Previously it was too easy to block e-mail providers by mistake
-
+
## Fixed
- Fix compatibility with Friendica's pinned posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18254), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/18260))
diff --git a/Gemfile b/Gemfile
index dc9b330ca7..c19a1b69ba 100644
--- a/Gemfile
+++ b/Gemfile
@@ -72,6 +72,7 @@ gem 'rack-attack', '~> 6.6'
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
gem 'rails-i18n', '~> 6.0'
gem 'rails-settings-cached', '~> 0.6'
+gem 'redcarpet', '~> 3.5'
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 2.1'
@@ -98,8 +99,6 @@ gem 'json-ld'
gem 'json-ld-preloaded', '~> 3.2'
gem 'rdf-normalize', '~> 0.5'
-gem 'redcarpet', '~> 3.5'
-
group :development, :test do
gem 'fabrication', '~> 2.30'
gem 'fuubar', '~> 2.5'
diff --git a/Gemfile.lock b/Gemfile.lock
index 939ed55d0f..bfe69991d4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -272,7 +272,7 @@ GEM
fog-json (>= 1.0)
ipaddress (>= 0.8)
formatador (0.2.5)
- fugit (1.5.3)
+ fugit (1.7.1)
et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4)
fuubar (2.5.1)
@@ -600,7 +600,7 @@ GEM
nokogiri (>= 1.10.5)
rexml
ruby2_keywords (0.0.5)
- rufus-scheduler (3.8.1)
+ rufus-scheduler (3.8.2)
fugit (~> 1.1, >= 1.1.6)
safety_net_attestation (0.4.0)
jwt (~> 2.0)
@@ -617,10 +617,10 @@ GEM
redis (>= 4.5.0, < 5)
sidekiq-bulk (0.2.0)
sidekiq
- sidekiq-scheduler (4.0.2)
+ sidekiq-scheduler (4.0.3)
redis (>= 4.2.0)
rufus-scheduler (~> 3.2)
- sidekiq (>= 4)
+ sidekiq (>= 4, < 7)
tilt (>= 1.4.0)
sidekiq-unique-jobs (7.1.27)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
@@ -649,7 +649,7 @@ GEM
sshkit (1.21.2)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
- stackprof (0.2.21)
+ stackprof (0.2.22)
statsd-ruby (1.5.0)
stoplight (3.0.0)
strong_migrations (0.7.9)
@@ -664,7 +664,7 @@ GEM
terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0)
thor (1.2.1)
- tilt (2.0.10)
+ tilt (2.0.11)
tpm-key_attestation (0.11.0)
bindata (~> 2.4)
openssl (> 2.0, < 3.1)
diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb
index d3f03374fe..1043486140 100644
--- a/app/controllers/about_controller.rb
+++ b/app/controllers/about_controller.rb
@@ -1,72 +1,19 @@
# frozen_string_literal: true
class AboutController < ApplicationController
- include RegistrationSpamConcern
+ include WebAppControllerConcern
- before_action :set_pack
+ skip_before_action :require_functional!
- layout 'public'
-
- before_action :require_open_federation!, only: [:show, :more]
- before_action :set_body_classes, only: :show
before_action :set_instance_presenter
- before_action :set_expires_in, only: [:more]
- before_action :set_registration_form_time, only: :show
- skip_before_action :require_functional!, only: [:more]
-
- def show; end
-
- def more
- flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor]
-
- toc_generator = TOCGenerator.new(@instance_presenter.extended_description)
-
- @rules = Rule.ordered
- @contents = toc_generator.html
- @table_of_contents = toc_generator.toc
- @blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks?
+ def show
+ expires_in 0, public: true unless user_signed_in?
end
- helper_method :display_blocks?
- helper_method :display_blocks_rationale?
- helper_method :public_fetch_mode?
- helper_method :new_user
-
private
- def require_open_federation!
- not_found if whitelist_mode?
- end
-
- def display_blocks?
- Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?)
- end
-
- def display_blocks_rationale?
- Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)
- end
-
- def new_user
- User.new.tap do |user|
- user.build_account
- user.build_invite_request
- end
- end
-
- def set_pack
- use_pack 'public'
- end
-
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
-
- def set_body_classes
- @hide_navbar = true
- end
-
- def set_expires_in
- expires_in 0, public: true
- end
end
diff --git a/app/controllers/account_follow_controller.rb b/app/controllers/account_follow_controller.rb
deleted file mode 100644
index 33394074db..0000000000
--- a/app/controllers/account_follow_controller.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-class AccountFollowController < ApplicationController
- include AccountControllerConcern
-
- before_action :authenticate_user!
-
- def create
- FollowService.new.call(current_user.account, @account, with_rate_limit: true)
- redirect_to account_path(@account)
- end
-end
diff --git a/app/controllers/account_unfollow_controller.rb b/app/controllers/account_unfollow_controller.rb
deleted file mode 100644
index 378ec86dc6..0000000000
--- a/app/controllers/account_unfollow_controller.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-class AccountUnfollowController < ApplicationController
- include AccountControllerConcern
-
- before_action :authenticate_user!
-
- def create
- UnfollowService.new.call(current_user.account, @account)
- redirect_to account_path(@account)
- end
-end
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 02f3c3dd7c..f36a0c8599 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -9,7 +9,6 @@ class AccountsController < ApplicationController
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
- before_action :set_body_classes
skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) }
skip_before_action :require_functional!, unless: :whitelist_mode?
@@ -17,26 +16,7 @@ class AccountsController < ApplicationController
def show
respond_to do |format|
format.html do
- use_pack 'public'
expires_in 0, public: true unless user_signed_in?
-
- @pinned_statuses = []
- @endorsed_accounts = @account.endorsed_accounts.to_a.sample(4)
- @featured_hashtags = @account.featured_tags.order(statuses_count: :desc)
-
- if current_account && @account.blocking?(current_account)
- @statuses = []
- return
- end
-
- @pinned_statuses = cached_filtered_status_pins if show_pinned_statuses?
- @statuses = cached_filtered_status_page
- @rss_url = rss_url
-
- unless @statuses.empty?
- @older_url = older_url if @statuses.last.id > filtered_statuses.last.id
- @newer_url = newer_url if @statuses.first.id < filtered_statuses.first.id
- end
end
format.rss do
@@ -56,18 +36,6 @@ class AccountsController < ApplicationController
private
- def set_body_classes
- @body_classes = 'with-modals'
- end
-
- def show_pinned_statuses?
- [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
- end
-
- def filtered_pinned_statuses
- @account.pinned_statuses.not_local_only.where(visibility: [:public, :unlisted])
- end
-
def filtered_statuses
default_statuses.tap do |statuses|
statuses.merge!(hashtag_scope) if tag_requested?
@@ -114,26 +82,6 @@ class AccountsController < ApplicationController
end
end
- def older_url
- pagination_url(max_id: @statuses.last.id)
- end
-
- def newer_url
- pagination_url(min_id: @statuses.first.id)
- end
-
- def pagination_url(max_id: nil, min_id: nil)
- if tag_requested?
- short_account_tag_url(@account, params[:tag], max_id: max_id, min_id: min_id)
- elsif media_requested?
- short_account_media_url(@account, max_id: max_id, min_id: min_id)
- elsif replies_requested?
- short_account_with_replies_url(@account, max_id: max_id, min_id: min_id)
- else
- short_account_url(@account, max_id: max_id, min_id: min_id)
- end
- end
-
def media_requested?
request.path.split('.').first.end_with?('/media') && !tag_requested?
end
@@ -146,13 +94,6 @@ class AccountsController < ApplicationController
request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
end
- def cached_filtered_status_pins
- cache_collection(
- filtered_pinned_statuses,
- Status
- )
- end
-
def cached_filtered_status_page
cache_collection_paginated_by_id(
filtered_statuses,
diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb
index 1fae60f5b0..431dc1524f 100644
--- a/app/controllers/admin/custom_emojis_controller.rb
+++ b/app/controllers/admin/custom_emojis_controller.rb
@@ -34,7 +34,7 @@ module Admin
@form = Form::CustomEmojiBatch.new(form_custom_emoji_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
- flash[:alert] = I18n.t('admin.accounts.no_account_selected')
+ flash[:alert] = I18n.t('admin.custom_emojis.no_emoji_selected')
rescue Mastodon::NotPermittedError
flash[:alert] = I18n.t('admin.custom_emojis.not_permitted')
rescue ActiveRecord::RecordInvalid => e
diff --git a/app/controllers/admin/export_domain_blocks_controller.rb b/app/controllers/admin/export_domain_blocks_controller.rb
index db8863551c..545bd94edd 100644
--- a/app/controllers/admin/export_domain_blocks_controller.rb
+++ b/app/controllers/admin/export_domain_blocks_controller.rb
@@ -62,7 +62,7 @@ module Admin
def export_data
CSV.generate(headers: export_headers, write_headers: true) do |content|
- DomainBlock.with_user_facing_limitations.each do |instance|
+ DomainBlock.with_limitations.each do |instance|
content << [instance.domain, instance.severity, instance.reject_media, instance.reject_reports, instance.public_comment, instance.obfuscate]
end
end
diff --git a/app/controllers/admin/ip_blocks_controller.rb b/app/controllers/admin/ip_blocks_controller.rb
index a87520f4ed..1bd7ec8059 100644
--- a/app/controllers/admin/ip_blocks_controller.rb
+++ b/app/controllers/admin/ip_blocks_controller.rb
@@ -5,7 +5,7 @@ module Admin
def index
authorize :ip_block, :index?
- @ip_blocks = IpBlock.page(params[:page])
+ @ip_blocks = IpBlock.order(ip: :asc).page(params[:page])
@form = Form::IpBlockBatch.new
end
diff --git a/app/controllers/admin/settings/about_controller.rb b/app/controllers/admin/settings/about_controller.rb
new file mode 100644
index 0000000000..86327fe397
--- /dev/null
+++ b/app/controllers/admin/settings/about_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Admin::Settings::AboutController < Admin::SettingsController
+ private
+
+ def after_update_redirect_path
+ admin_settings_about_path
+ end
+end
diff --git a/app/controllers/admin/settings/appearance_controller.rb b/app/controllers/admin/settings/appearance_controller.rb
new file mode 100644
index 0000000000..39b2448d80
--- /dev/null
+++ b/app/controllers/admin/settings/appearance_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Admin::Settings::AppearanceController < Admin::SettingsController
+ private
+
+ def after_update_redirect_path
+ admin_settings_appearance_path
+ end
+end
diff --git a/app/controllers/admin/settings/branding_controller.rb b/app/controllers/admin/settings/branding_controller.rb
new file mode 100644
index 0000000000..4a4d76f497
--- /dev/null
+++ b/app/controllers/admin/settings/branding_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Admin::Settings::BrandingController < Admin::SettingsController
+ private
+
+ def after_update_redirect_path
+ admin_settings_branding_path
+ end
+end
diff --git a/app/controllers/admin/settings/content_retention_controller.rb b/app/controllers/admin/settings/content_retention_controller.rb
new file mode 100644
index 0000000000..b88336a2c4
--- /dev/null
+++ b/app/controllers/admin/settings/content_retention_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Admin::Settings::ContentRetentionController < Admin::SettingsController
+ private
+
+ def after_update_redirect_path
+ admin_settings_content_retention_path
+ end
+end
diff --git a/app/controllers/admin/settings/discovery_controller.rb b/app/controllers/admin/settings/discovery_controller.rb
new file mode 100644
index 0000000000..be4b57f79a
--- /dev/null
+++ b/app/controllers/admin/settings/discovery_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Admin::Settings::DiscoveryController < Admin::SettingsController
+ private
+
+ def after_update_redirect_path
+ admin_settings_discovery_path
+ end
+end
diff --git a/app/controllers/admin/settings/registrations_controller.rb b/app/controllers/admin/settings/registrations_controller.rb
new file mode 100644
index 0000000000..b4a74349c0
--- /dev/null
+++ b/app/controllers/admin/settings/registrations_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Admin::Settings::RegistrationsController < Admin::SettingsController
+ private
+
+ def after_update_redirect_path
+ admin_settings_registrations_path
+ end
+end
diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb
index dc1c79b7fd..338a3638c4 100644
--- a/app/controllers/admin/settings_controller.rb
+++ b/app/controllers/admin/settings_controller.rb
@@ -2,7 +2,7 @@
module Admin
class SettingsController < BaseController
- def edit
+ def show
authorize :settings, :show?
@admin_settings = Form::AdminSettings.new
@@ -15,14 +15,18 @@ module Admin
if @admin_settings.save
flash[:notice] = I18n.t('generic.changes_saved_msg')
- redirect_to edit_admin_settings_path
+ redirect_to after_update_redirect_path
else
- render :edit
+ render :show
end
end
private
+ def after_update_redirect_path
+ raise NotImplementedError
+ end
+
def settings_params
params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS)
end
diff --git a/app/controllers/admin/site_uploads_controller.rb b/app/controllers/admin/site_uploads_controller.rb
index cacecedb0a..a5d2cf41cf 100644
--- a/app/controllers/admin/site_uploads_controller.rb
+++ b/app/controllers/admin/site_uploads_controller.rb
@@ -9,7 +9,7 @@ module Admin
@site_upload.destroy!
- redirect_to edit_admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
+ redirect_to admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
end
private
diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb
index 084921cebb..b80cd20f56 100644
--- a/app/controllers/admin/statuses_controller.rb
+++ b/app/controllers/admin/statuses_controller.rb
@@ -3,18 +3,23 @@
module Admin
class StatusesController < BaseController
before_action :set_account
- before_action :set_statuses
+ before_action :set_statuses, except: :show
+ before_action :set_status, only: :show
PER_PAGE = 20
def index
- authorize :status, :index?
+ authorize [:admin, :status], :index?
@status_batch_action = Admin::StatusBatchAction.new
end
+ def show
+ authorize [:admin, @status], :show?
+ end
+
def batch
- authorize :status, :index?
+ authorize [:admin, :status], :index?
@status_batch_action = Admin::StatusBatchAction.new(admin_status_batch_action_params.merge(current_account: current_account, report_id: params[:report_id], type: action_from_button))
@status_batch_action.save!
@@ -32,6 +37,7 @@ module Admin
def after_create_redirect_path
report_id = @status_batch_action&.report_id || params[:report_id]
+
if report_id.present?
admin_report_path(report_id)
else
@@ -43,6 +49,10 @@ module Admin
@account = Account.find(params[:account_id])
end
+ def set_status
+ @status = @account.statuses.find(params[:id])
+ end
+
def set_statuses
@statuses = Admin::StatusFilter.new(@account, filter_params).results.preload(:application, :preloadable_poll, :media_attachments, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, active_mentions: :account]).page(params[:page]).per(PER_PAGE)
end
diff --git a/app/controllers/admin/trends/links/preview_card_providers_controller.rb b/app/controllers/admin/trends/links/preview_card_providers_controller.rb
index 97dee8eca4..768b79f8db 100644
--- a/app/controllers/admin/trends/links/preview_card_providers_controller.rb
+++ b/app/controllers/admin/trends/links/preview_card_providers_controller.rb
@@ -14,7 +14,7 @@ class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseControll
@form = Trends::PreviewCardProviderBatch.new(trends_preview_card_provider_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
- flash[:alert] = I18n.t('admin.accounts.no_account_selected')
+ flash[:alert] = I18n.t('admin.trends.links.publishers.no_publisher_selected')
ensure
redirect_to admin_trends_links_preview_card_providers_path(filter_params)
end
diff --git a/app/controllers/admin/trends/links_controller.rb b/app/controllers/admin/trends/links_controller.rb
index a497eae411..83d68eba63 100644
--- a/app/controllers/admin/trends/links_controller.rb
+++ b/app/controllers/admin/trends/links_controller.rb
@@ -4,6 +4,7 @@ class Admin::Trends::LinksController < Admin::BaseController
def index
authorize :preview_card, :review?
+ @locales = PreviewCardTrend.pluck('distinct language')
@preview_cards = filtered_preview_cards.page(params[:page])
@form = Trends::PreviewCardBatch.new
end
@@ -14,7 +15,7 @@ class Admin::Trends::LinksController < Admin::BaseController
@form = Trends::PreviewCardBatch.new(trends_preview_card_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
- flash[:alert] = I18n.t('admin.accounts.no_account_selected')
+ flash[:alert] = I18n.t('admin.trends.links.no_link_selected')
ensure
redirect_to admin_trends_links_path(filter_params)
end
diff --git a/app/controllers/admin/trends/statuses_controller.rb b/app/controllers/admin/trends/statuses_controller.rb
index c538962f99..3d8b53ea8a 100644
--- a/app/controllers/admin/trends/statuses_controller.rb
+++ b/app/controllers/admin/trends/statuses_controller.rb
@@ -2,19 +2,20 @@
class Admin::Trends::StatusesController < Admin::BaseController
def index
- authorize :status, :review?
+ authorize [:admin, :status], :review?
+ @locales = StatusTrend.pluck('distinct language')
@statuses = filtered_statuses.page(params[:page])
@form = Trends::StatusBatch.new
end
def batch
- authorize :status, :review?
+ authorize [:admin, :status], :review?
@form = Trends::StatusBatch.new(trends_status_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
- flash[:alert] = I18n.t('admin.accounts.no_account_selected')
+ flash[:alert] = I18n.t('admin.trends.statuses.no_status_selected')
ensure
redirect_to admin_trends_statuses_path(filter_params)
end
diff --git a/app/controllers/admin/trends/tags_controller.rb b/app/controllers/admin/trends/tags_controller.rb
index 98dd6c8ec2..f5946448ae 100644
--- a/app/controllers/admin/trends/tags_controller.rb
+++ b/app/controllers/admin/trends/tags_controller.rb
@@ -14,7 +14,7 @@ class Admin::Trends::TagsController < Admin::BaseController
@form = Trends::TagBatch.new(trends_tag_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
- flash[:alert] = I18n.t('admin.accounts.no_account_selected')
+ flash[:alert] = I18n.t('admin.trends.tags.no_tag_selected')
ensure
redirect_to admin_trends_tags_path(filter_params)
end
diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index 7ce6599c52..c46fde65b2 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -24,6 +24,10 @@ class Api::BaseController < ApplicationController
render json: { error: 'Duplicate record' }, status: 422
end
+ rescue_from Date::Error do
+ render json: { error: 'Invalid date supplied' }, status: 422
+ end
+
rescue_from ActiveRecord::RecordNotFound do
render json: { error: 'Record not found' }, status: 404
end
diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb
index 0dee02e94f..ae7f7d0762 100644
--- a/app/controllers/api/v1/admin/accounts_controller.rb
+++ b/app/controllers/api/v1/admin/accounts_controller.rb
@@ -60,14 +60,13 @@ class Api::V1::Admin::AccountsController < Api::BaseController
def reject
authorize @account.user, :reject?
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
- render json: @account, serializer: REST::Admin::AccountSerializer
+ render_empty
end
def destroy
authorize @account, :destroy?
- json = render_to_body json: @account, serializer: REST::Admin::AccountSerializer
Admin::AccountDeletionWorker.perform_async(@account.id)
- render json: json
+ render_empty
end
def unsensitive
diff --git a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb
index bf8a6a1311..9ef1b3be71 100644
--- a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb
+++ b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb
@@ -35,20 +35,16 @@ class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController
def create
authorize :canonical_email_block, :create?
-
@canonical_email_block = CanonicalEmailBlock.create!(resource_params)
log_action :create, @canonical_email_block
-
render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer
end
def destroy
authorize @canonical_email_block, :destroy?
-
@canonical_email_block.destroy!
log_action :destroy, @canonical_email_block
-
- render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer
+ render_empty
end
private
diff --git a/app/controllers/api/v1/admin/domain_allows_controller.rb b/app/controllers/api/v1/admin/domain_allows_controller.rb
index 59aa807d6f..0658199f0f 100644
--- a/app/controllers/api/v1/admin/domain_allows_controller.rb
+++ b/app/controllers/api/v1/admin/domain_allows_controller.rb
@@ -43,7 +43,7 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
authorize @domain_allow, :destroy?
UnallowDomainService.new.call(@domain_allow)
log_action :destroy, @domain_allow
- render json: @domain_allow, serializer: REST::Admin::DomainAllowSerializer
+ render_empty
end
private
diff --git a/app/controllers/api/v1/admin/domain_blocks_controller.rb b/app/controllers/api/v1/admin/domain_blocks_controller.rb
index de8fd9d089..df5b1b3fcb 100644
--- a/app/controllers/api/v1/admin/domain_blocks_controller.rb
+++ b/app/controllers/api/v1/admin/domain_blocks_controller.rb
@@ -40,7 +40,6 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
def update
authorize @domain_block, :update?
-
@domain_block.update(domain_block_params)
severity_changed = @domain_block.severity_changed?
@domain_block.save!
@@ -53,7 +52,7 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
authorize @domain_block, :destroy?
UnblockDomainService.new.call(@domain_block)
log_action :destroy, @domain_block
- render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer
+ render_empty
end
private
diff --git a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb
index ac16f70b05..e53d0b1573 100644
--- a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb
+++ b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb
@@ -39,11 +39,9 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController
def destroy
authorize @email_domain_block, :destroy?
-
@email_domain_block.destroy!
log_action :destroy, @email_domain_block
-
- render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer
+ render_empty
end
private
diff --git a/app/controllers/api/v1/admin/ip_blocks_controller.rb b/app/controllers/api/v1/admin/ip_blocks_controller.rb
index f13d632671..201ab6b1ff 100644
--- a/app/controllers/api/v1/admin/ip_blocks_controller.rb
+++ b/app/controllers/api/v1/admin/ip_blocks_controller.rb
@@ -20,10 +20,8 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController
def create
authorize :ip_block, :create?
-
@ip_block = IpBlock.create!(resource_params)
log_action :create, @ip_block
-
render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
end
@@ -39,20 +37,16 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController
def update
authorize @ip_block, :update?
-
@ip_block.update(resource_params)
log_action :update, @ip_block
-
render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
end
def destroy
authorize @ip_block, :destroy?
-
@ip_block.destroy!
log_action :destroy, @ip_block
-
- render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
+ render_empty
end
private
diff --git a/app/controllers/api/v1/featured_tags_controller.rb b/app/controllers/api/v1/featured_tags_controller.rb
index c1ead4f540..edb42a94ea 100644
--- a/app/controllers/api/v1/featured_tags_controller.rb
+++ b/app/controllers/api/v1/featured_tags_controller.rb
@@ -13,12 +13,12 @@ class Api::V1::FeaturedTagsController < Api::BaseController
end
def create
- @featured_tag = current_account.featured_tags.create!(featured_tag_params)
- render json: @featured_tag, serializer: REST::FeaturedTagSerializer
+ featured_tag = CreateFeaturedTagService.new.call(current_account, featured_tag_params[:name])
+ render json: featured_tag, serializer: REST::FeaturedTagSerializer
end
def destroy
- @featured_tag.destroy!
+ RemoveFeaturedTagWorker.perform_async(current_account.id, @featured_tag.id)
render_empty
end
diff --git a/app/controllers/api/v1/instances/domain_blocks_controller.rb b/app/controllers/api/v1/instances/domain_blocks_controller.rb
new file mode 100644
index 0000000000..37a6906fb6
--- /dev/null
+++ b/app/controllers/api/v1/instances/domain_blocks_controller.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class Api::V1::Instances::DomainBlocksController < Api::BaseController
+ skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
+
+ before_action :require_enabled_api!
+ before_action :set_domain_blocks
+
+ def index
+ expires_in 3.minutes, public: true
+ render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: (Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?))
+ end
+
+ private
+
+ def require_enabled_api!
+ head 404 unless Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?)
+ end
+
+ def set_domain_blocks
+ @domain_blocks = DomainBlock.with_user_facing_limitations.by_severity
+ end
+end
diff --git a/app/controllers/api/v1/instances/extended_descriptions_controller.rb b/app/controllers/api/v1/instances/extended_descriptions_controller.rb
new file mode 100644
index 0000000000..c72e16cff2
--- /dev/null
+++ b/app/controllers/api/v1/instances/extended_descriptions_controller.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController
+ skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
+
+ before_action :set_extended_description
+
+ def show
+ expires_in 3.minutes, public: true
+ render json: @extended_description, serializer: REST::ExtendedDescriptionSerializer
+ end
+
+ private
+
+ def set_extended_description
+ @extended_description = ExtendedDescription.current
+ end
+end
diff --git a/app/controllers/api/v1/instances/privacy_policies_controller.rb b/app/controllers/api/v1/instances/privacy_policies_controller.rb
new file mode 100644
index 0000000000..dbd69f54d4
--- /dev/null
+++ b/app/controllers/api/v1/instances/privacy_policies_controller.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class Api::V1::Instances::PrivacyPoliciesController < Api::BaseController
+ skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
+
+ before_action :set_privacy_policy
+
+ def show
+ expires_in 1.day, public: true
+ render json: @privacy_policy, serializer: REST::PrivacyPolicySerializer
+ end
+
+ private
+
+ def set_privacy_policy
+ @privacy_policy = PrivacyPolicy.current
+ end
+end
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index b2cee3e92e..f8069b720e 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -66,6 +66,7 @@ class Api::V1::StatusesController < Api::BaseController
text: status_params[:status],
media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive],
+ language: status_params[:language],
spoiler_text: status_params[:spoiler_text],
poll: status_params[:poll],
content_type: status_params[:content_type]
@@ -79,6 +80,7 @@ class Api::V1::StatusesController < Api::BaseController
authorize @status, :destroy?
@status.discard
+ StatusPin.find_by(status: @status)&.destroy
@status.account.statuses_count = @status.account.statuses_count - 1
json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb
index 493fe4776a..2ee5aaf18f 100644
--- a/app/controllers/api/v1/timelines/public_controller.rb
+++ b/app/controllers/api/v1/timelines/public_controller.rb
@@ -35,6 +35,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
def public_feed
PublicFeed.new(
current_account,
+ locale: content_locale,
local: truthy_param?(:local),
remote: truthy_param?(:remote),
only_media: truthy_param?(:only_media),
diff --git a/app/controllers/api/v1/trends/links_controller.rb b/app/controllers/api/v1/trends/links_controller.rb
index 1a9f918f29..8ff3b364e2 100644
--- a/app/controllers/api/v1/trends/links_controller.rb
+++ b/app/controllers/api/v1/trends/links_controller.rb
@@ -28,7 +28,9 @@ class Api::V1::Trends::LinksController < Api::BaseController
end
def links_from_trends
- Trends.links.query.allowed.in_locale(content_locale)
+ scope = Trends.links.query.allowed.in_locale(content_locale)
+ scope = scope.filtered_for(current_account) if user_signed_in?
+ scope
end
def insert_pagination_headers
diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb
index 77eeab5b0b..b084eae425 100644
--- a/app/controllers/api/v2/search_controller.rb
+++ b/app/controllers/api/v2/search_controller.rb
@@ -5,8 +5,8 @@ class Api::V2::SearchController < Api::BaseController
RESULTS_LIMIT = (ENV['MAX_SEARCH_RESULTS'] || 20).to_i
- before_action -> { doorkeeper_authorize! :read, :'read:search' }
- before_action :require_user!
+ before_action -> { authorize_if_got_token! :read, :'read:search' }
+ before_action :validate_search_params!
def index
@search = Search.new(search_results)
@@ -19,6 +19,16 @@ class Api::V2::SearchController < Api::BaseController
private
+ def validate_search_params!
+ params.require(:q)
+
+ return if user_signed_in?
+
+ return render json: { error: 'Search queries pagination is not supported without authentication' }, status: 401 if params[:offset].present?
+
+ render json: { error: 'Search queries that resolve remote resources are not supported without authentication' }, status: 401 if truthy_param?(:resolve)
+ end
+
def search_results
SearchService.new.call(
params[:q],
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index 486edcdcb8..edef0d5bbe 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -15,6 +15,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController
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]
+ before_action :set_rules, only: :new
+ before_action :require_rules_acceptance!, only: :new
before_action :set_registration_form_time, only: :new
skip_before_action :require_functional!, only: [:edit, :update]
@@ -56,7 +58,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up) do |u|
- u.permit({ account_attributes: [:username], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password)
+ u.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password)
end
end
@@ -143,6 +145,19 @@ class Auth::RegistrationsController < Devise::RegistrationsController
forbidden if current_account.suspended?
end
+ def set_rules
+ @rules = Rule.ordered
+ end
+
+ def require_rules_acceptance!
+ return if @rules.empty? || (session[:accept_token].present? && params[:accept] == session[:accept_token])
+
+ @accept_token = session[:accept_token] = SecureRandom.hex
+ @invite_code = invite_code
+
+ set_locale { render :rules }
+ end
+
def set_cache_headers
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
end
diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb
index 11eac0eb6b..2f7d84df04 100644
--- a/app/controllers/concerns/account_controller_concern.rb
+++ b/app/controllers/concerns/account_controller_concern.rb
@@ -3,13 +3,12 @@
module AccountControllerConcern
extend ActiveSupport::Concern
+ include WebAppControllerConcern
include AccountOwnedConcern
FOLLOW_PER_PAGE = 12
included do
- layout 'public'
-
before_action :set_instance_presenter
before_action :set_link_headers, if: -> { request.format.nil? || request.format == :html }
end
diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb
new file mode 100644
index 0000000000..b6050c9138
--- /dev/null
+++ b/app/controllers/concerns/web_app_controller_concern.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module WebAppControllerConcern
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :set_pack
+ before_action :redirect_unauthenticated_to_permalinks!
+ before_action :set_app_body_class
+ before_action :set_referrer_policy_header
+ end
+
+ def set_app_body_class
+ @body_classes = 'app-body'
+ end
+
+ def set_referrer_policy_header
+ response.headers['Referrer-Policy'] = 'origin'
+ end
+
+ def redirect_unauthenticated_to_permalinks!
+ return if user_signed_in?
+
+ redirect_path = PermalinkRedirector.new(request.path).redirect_path
+
+ redirect_to(redirect_path) if redirect_path.present?
+ end
+
+ def set_pack
+ use_pack 'home'
+ end
+end
diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb
deleted file mode 100644
index 2263f286b3..0000000000
--- a/app/controllers/directories_controller.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-class DirectoriesController < ApplicationController
- layout 'public'
-
- before_action :authenticate_user!, if: :whitelist_mode?
- before_action :require_enabled!
- before_action :set_instance_presenter
- before_action :set_accounts
- before_action :set_pack
-
- skip_before_action :require_functional!, unless: :whitelist_mode?
-
- def index
- render :index
- end
-
- private
-
- def set_pack
- use_pack 'share'
- end
-
- def require_enabled!
- return not_found unless Setting.profile_directory
- end
-
- def set_accounts
- @accounts = Account.local.discoverable.by_recent_status.page(params[:page]).per(20).tap do |query|
- query.merge!(Account.not_excluded_by_account(current_account)) if current_account
- end
- end
-
- def set_instance_presenter
- @instance_presenter = InstancePresenter.new
- end
-end
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index 0d9e624ef8..35ce31f806 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -3,6 +3,7 @@
class FollowerAccountsController < ApplicationController
include AccountControllerConcern
include SignatureVerification
+ include WebAppControllerConcern
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
@@ -13,12 +14,7 @@ class FollowerAccountsController < ApplicationController
def index
respond_to do |format|
format.html do
- use_pack 'public'
expires_in 0, public: true unless user_signed_in?
-
- next if @account.hide_collections?
-
- follows
end
format.json do
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index 68e1a79be8..f84dca1e5a 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -3,6 +3,7 @@
class FollowingAccountsController < ApplicationController
include AccountControllerConcern
include SignatureVerification
+ include WebAppControllerConcern
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
@@ -13,12 +14,7 @@ class FollowingAccountsController < ApplicationController
def index
respond_to do |format|
format.html do
- use_pack 'public'
expires_in 0, public: true unless user_signed_in?
-
- next if @account.hide_collections?
-
- follows
end
format.json do
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 61b1690fab..d8ee82a7a2 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -1,47 +1,16 @@
# frozen_string_literal: true
class HomeController < ApplicationController
- before_action :redirect_unauthenticated_to_permalinks!
+ include WebAppControllerConcern
- before_action :set_pack
- before_action :set_referrer_policy_header
before_action :set_instance_presenter
def index
- @body_classes = 'app-body'
+ expires_in 0, public: true unless user_signed_in?
end
private
- def redirect_unauthenticated_to_permalinks!
- return if user_signed_in?
-
- redirect_path = PermalinkRedirector.new(request.path).redirect_path
- redirect_path ||= default_redirect_path
-
- redirect_to(redirect_path) if redirect_path.present?
- end
-
- def set_pack
- use_pack 'home'
- end
-
- def default_redirect_path
- if whitelist_mode?
- new_user_session_path
- elsif request.path.start_with?('/web')
- nil
- elsif single_user_mode?
- short_account_path(Account.local.without_suspended.where('id > 0').first)
- else
- about_path
- end
- end
-
- def set_referrer_policy_header
- response.headers['Referrer-Policy'] = 'origin'
- end
-
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
diff --git a/app/controllers/privacy_controller.rb b/app/controllers/privacy_controller.rb
index 126bb01285..2c98bf3bf4 100644
--- a/app/controllers/privacy_controller.rb
+++ b/app/controllers/privacy_controller.rb
@@ -1,28 +1,19 @@
# frozen_string_literal: true
class PrivacyController < ApplicationController
- layout 'public'
-
- before_action :set_pack
-
- before_action :set_instance_presenter
- before_action :set_expires_in
+ include WebAppControllerConcern
skip_before_action :require_functional!
- def show; end
+ before_action :set_instance_presenter
+
+ def show
+ expires_in 0, public: true if current_account.nil?
+ end
private
- def set_pack
- use_pack 'public'
- end
-
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
-
- def set_expires_in
- expires_in 0, public: true
- end
end
diff --git a/app/controllers/public_timelines_controller.rb b/app/controllers/public_timelines_controller.rb
deleted file mode 100644
index eb5bb191b9..0000000000
--- a/app/controllers/public_timelines_controller.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-class PublicTimelinesController < ApplicationController
- before_action :set_pack
- layout 'public'
-
- before_action :authenticate_user!, if: :whitelist_mode?
- before_action :require_enabled!
- before_action :set_body_classes
- before_action :set_instance_presenter
-
- def show; end
-
- private
-
- def require_enabled!
- not_found unless Setting.timeline_preview
- end
-
- def set_body_classes
- @body_classes = 'with-modals'
- end
-
- def set_instance_presenter
- @instance_presenter = InstancePresenter.new
- end
-
- def set_pack
- use_pack 'about'
- end
-end
diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb
deleted file mode 100644
index 93a0a74764..0000000000
--- a/app/controllers/remote_follow_controller.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-class RemoteFollowController < ApplicationController
- include AccountOwnedConcern
-
- layout 'modal'
-
- before_action :set_pack
- before_action :set_body_classes
-
- skip_before_action :require_functional!
-
- def new
- @remote_follow = RemoteFollow.new(session_params)
- end
-
- def create
- @remote_follow = RemoteFollow.new(resource_params)
-
- if @remote_follow.valid?
- session[:remote_follow] = @remote_follow.acct
- redirect_to @remote_follow.subscribe_address_for(@account)
- else
- render :new
- end
- end
-
- private
-
- def resource_params
- params.require(:remote_follow).permit(:acct)
- end
-
- def session_params
- { acct: session[:remote_follow] || current_account&.username }
- end
-
- def set_pack
- use_pack 'modal'
- end
-
- def set_body_classes
- @body_classes = 'modal-layout'
- @hide_header = true
- end
-end
diff --git a/app/controllers/remote_interaction_controller.rb b/app/controllers/remote_interaction_controller.rb
deleted file mode 100644
index a277bfa103..0000000000
--- a/app/controllers/remote_interaction_controller.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-# frozen_string_literal: true
-
-class RemoteInteractionController < ApplicationController
- include Authorization
-
- layout 'modal'
-
- before_action :authenticate_user!, if: :whitelist_mode?
- before_action :set_interaction_type
- before_action :set_status
- before_action :set_body_classes
- before_action :set_pack
-
- skip_before_action :require_functional!, unless: :whitelist_mode?
-
- def new
- @remote_follow = RemoteFollow.new(session_params)
- end
-
- def create
- @remote_follow = RemoteFollow.new(resource_params)
-
- if @remote_follow.valid?
- session[:remote_follow] = @remote_follow.acct
- redirect_to @remote_follow.interact_address_for(@status)
- else
- render :new
- end
- end
-
- private
-
- def resource_params
- params.require(:remote_follow).permit(:acct)
- end
-
- def session_params
- { acct: session[:remote_follow] || current_account&.username }
- end
-
- def set_status
- @status = Status.find(params[:id])
- authorize @status, :show?
- rescue Mastodon::NotPermittedError
- not_found
- end
-
- def set_body_classes
- @body_classes = 'modal-layout'
- @hide_header = true
- end
-
- def set_pack
- use_pack 'modal'
- end
-
- def set_interaction_type
- @interaction_type = %w(reply reblog favourite).include?(params[:type]) ? params[:type] : 'reply'
- end
-end
diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb
index e0dd5edcb1..bb096567a9 100644
--- a/app/controllers/settings/deletes_controller.rb
+++ b/app/controllers/settings/deletes_controller.rb
@@ -4,7 +4,6 @@ class Settings::DeletesController < Settings::BaseController
skip_before_action :require_functional!
before_action :require_not_suspended!
- before_action :check_enabled_deletion
def show
@confirmation = Form::DeleteConfirmation.new
@@ -21,10 +20,6 @@ class Settings::DeletesController < Settings::BaseController
private
- def check_enabled_deletion
- redirect_to root_path unless Setting.open_deletion
- end
-
def resource_params
params.require(:form_delete_confirmation).permit(:password, :username)
end
diff --git a/app/controllers/settings/featured_tags_controller.rb b/app/controllers/settings/featured_tags_controller.rb
index aadff7c835..b3db04a426 100644
--- a/app/controllers/settings/featured_tags_controller.rb
+++ b/app/controllers/settings/featured_tags_controller.rb
@@ -10,9 +10,9 @@ class Settings::FeaturedTagsController < Settings::BaseController
end
def create
- @featured_tag = current_account.featured_tags.new(featured_tag_params)
+ @featured_tag = CreateFeaturedTagService.new.call(current_account, featured_tag_params[:name], force: false)
- if @featured_tag.save
+ if @featured_tag.valid?
redirect_to settings_featured_tags_path
else
set_featured_tags
@@ -23,7 +23,7 @@ class Settings::FeaturedTagsController < Settings::BaseController
end
def destroy
- @featured_tag.destroy!
+ RemoveFeaturedTagWorker.perform_async(current_account.id, @featured_tag.id)
redirect_to settings_featured_tags_path
end
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index 55cc3790f3..6986176ea8 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -1,21 +1,19 @@
# frozen_string_literal: true
class StatusesController < ApplicationController
+ include WebAppControllerConcern
include StatusControllerConcern
include SignatureAuthentication
include Authorization
include AccountOwnedConcern
- layout 'public'
-
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_status
before_action :set_instance_presenter
before_action :set_link_headers
before_action :redirect_to_original, only: :show
- before_action :set_referrer_policy_header, only: :show
before_action :set_cache_headers
- before_action :set_body_classes
+ before_action :set_body_classes, only: :embed
skip_around_action :set_locale, if: -> { request.format == :json }
skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode?
@@ -27,11 +25,7 @@ class StatusesController < ApplicationController
def show
respond_to do |format|
format.html do
- use_pack 'public'
-
expires_in 10.seconds, public: true if current_account.nil?
- set_ancestors
- set_descendants
end
format.json do
@@ -80,8 +74,4 @@ class StatusesController < ApplicationController
def redirect_to_original
redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog?
end
-
- def set_referrer_policy_header
- response.headers['Referrer-Policy'] = 'origin' unless @status.distributable?
- end
end
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index 315eabb3d9..f0a0993506 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -2,18 +2,16 @@
class TagsController < ApplicationController
include SignatureVerification
+ include WebAppControllerConcern
PAGE_SIZE = 20
PAGE_SIZE_MAX = 200
- layout 'public'
-
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :authenticate_user!, if: :whitelist_mode?
before_action :set_local
before_action :set_tag
before_action :set_statuses
- before_action :set_body_classes
before_action :set_instance_presenter
skip_before_action :require_functional!, unless: :whitelist_mode?
@@ -21,8 +19,7 @@ class TagsController < ApplicationController
def show
respond_to do |format|
format.html do
- use_pack 'about'
- expires_in 0, public: true
+ expires_in 0, public: true unless user_signed_in?
end
format.rss do
@@ -55,10 +52,6 @@ class TagsController < ApplicationController
end
end
- def set_body_classes
- @body_classes = 'with-modals'
- end
-
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb
index 2a17b69e35..e15aee6df1 100644
--- a/app/helpers/accounts_helper.rb
+++ b/app/helpers/accounts_helper.rb
@@ -20,54 +20,10 @@ module AccountsHelper
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([logo_as_symbol, 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([logo_as_symbol, 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([logo_as_symbol, 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([logo_as_symbol, t('accounts.follow')])
- end
- end
- end
+ return if account.memorial? || account.moved?
- 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)
- 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 account.user_role&.highlighted?
- content_tag(:div, content_tag(:div, account.user_role.name, class: "account-role user-role-#{account.user_role.id}"), class: 'roles')
+ link_to ActivityPub::TagManager.instance.url_for(account), class: 'button logo-button', target: '_new' do
+ safe_join([logo_as_symbol, t('accounts.follow')])
end
end
diff --git a/app/helpers/admin/settings_helper.rb b/app/helpers/admin/settings_helper.rb
index f99a2b8c8a..552a3ee5a8 100644
--- a/app/helpers/admin/settings_helper.rb
+++ b/app/helpers/admin/settings_helper.rb
@@ -1,14 +1,6 @@
# frozen_string_literal: true
module Admin::SettingsHelper
- def site_upload_delete_hint(hint, var)
- upload = SiteUpload.find_by(var: var.to_s)
- return hint unless upload
-
- link = link_to t('admin.site_uploads.delete'), admin_site_upload_path(upload), data: { method: :delete }
- safe_join([hint, link], '
'.html_safe)
- end
-
def captcha_available?
ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present?
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 62739b964b..c5374a195e 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -87,10 +87,6 @@ module ApplicationHelper
link_to label, omniauth_authorize_path(:user, provider), class: "button button-#{provider}", method: :post
end
- def open_deletion?
- Setting.open_deletion
- end
-
def locale_direction
if RTL_LOCALES.include?(I18n.locale)
'rtl'
@@ -199,10 +195,7 @@ module ApplicationHelper
def render_initial_state
state_params = {
- settings: {
- known_fediverse: Setting.show_known_fediverse_at_about_page,
- },
-
+ settings: {},
text: [params[:title], params[:text], params[:url]].compact.join(' '),
}
@@ -219,6 +212,10 @@ module ApplicationHelper
state_params[:admin] = Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, ''))
end
+ if single_user_mode?
+ state_params[:owner] = Account.local.without_suspended.where('id > 0').first
+ end
+
json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json
# rubocop:disable Rails/OutputSafety
content_tag(:script, json_escape(json).html_safe, id: 'initial-state', type: 'application/json')
diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb
index 4da68500a1..f41104709e 100644
--- a/app/helpers/home_helper.rb
+++ b/app/helpers/home_helper.rb
@@ -23,7 +23,7 @@ module HomeHelper
else
link_to(path || ActivityPub::TagManager.instance.url_for(account), class: 'account__display-name') do
content_tag(:div, class: 'account__avatar-wrapper') do
- image_tag(full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url), class: 'account__avatar')
+ image_tag(full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url), class: 'account__avatar', width: 46, height: 46)
end +
content_tag(:span, class: 'display-name') do
content_tag(:bdi) do
diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb
index 4077e19bdf..e5bae2c6b0 100644
--- a/app/helpers/languages_helper.rb
+++ b/app/helpers/languages_helper.rb
@@ -97,7 +97,7 @@ module LanguagesHelper
lg: ['Ganda', 'Luganda'].freeze,
li: ['Limburgish', 'Limburgs'].freeze,
ln: ['Lingala', 'Lingála'].freeze,
- lo: ['Lao', 'ພາສາ'].freeze,
+ lo: ['Lao', 'ລາວ'].freeze,
lt: ['Lithuanian', 'lietuvių kalba'].freeze,
lu: ['Luba-Katanga', 'Tshiluba'].freeze,
lv: ['Latvian', 'latviešu valoda'].freeze,
diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js
index c84f566a3f..3175ff5607 100644
--- a/app/javascript/core/admin.js
+++ b/app/javascript/core/admin.js
@@ -4,6 +4,24 @@ import 'packs/public-path';
import { delegate } from '@rails/ujs';
import ready from '../mastodon/ready';
+const setAnnouncementEndsAttributes = (target) => {
+ const valid = target?.value && target?.validity?.valid;
+ const element = document.querySelector('input[type="datetime-local"]#announcement_ends_at');
+ if (valid) {
+ element.classList.remove('optional');
+ element.required = true;
+ element.min = target.value;
+ } else {
+ element.classList.add('optional');
+ element.removeAttribute('required');
+ element.removeAttribute('min');
+ }
+};
+
+delegate(document, 'input[type="datetime-local"]#announcement_starts_at', 'change', ({ target }) => {
+ setAnnouncementEndsAttributes(target);
+});
+
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
const showSelectAll = () => {
@@ -143,6 +161,20 @@ const onChangeRegistrationMode = (target) => {
});
};
+const convertUTCDateTimeToLocal = (value) => {
+ const date = new Date(value + 'Z');
+ const twoChars = (x) => (x.toString().padStart(2, '0'));
+ return `${date.getFullYear()}-${twoChars(date.getMonth()+1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`;
+};
+
+const convertLocalDatetimeToUTC = (value) => {
+ const re = /^([0-9]{4,})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2})/;
+ const match = re.exec(value);
+ const date = new Date(match[1], match[2] - 1, match[3], match[4], match[5]);
+ const fullISO8601 = date.toISOString();
+ return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6);
+};
+
delegate(document, '#form_admin_settings_registrations_mode', 'change', ({ target }) => onChangeRegistrationMode(target));
ready(() => {
@@ -170,4 +202,26 @@ ready(() => {
e.target.href = url;
}
});
+
+ [].forEach.call(document.querySelectorAll('input[type="datetime-local"]'), element => {
+ if (element.value) {
+ element.value = convertUTCDateTimeToLocal(element.value);
+ }
+ if (element.placeholder) {
+ element.placeholder = convertUTCDateTimeToLocal(element.placeholder);
+ }
+ });
+
+ delegate(document, 'form', 'submit', ({ target }) => {
+ [].forEach.call(target.querySelectorAll('input[type="datetime-local"]'), element => {
+ if (element.value && element.validity.valid) {
+ element.value = convertLocalDatetimeToUTC(element.value);
+ }
+ });
+ });
+
+ const announcementStartsAt = document.querySelector('input[type="datetime-local"]#announcement_starts_at');
+ if (announcementStartsAt) {
+ setAnnouncementEndsAttributes(announcementStartsAt);
+ }
});
diff --git a/app/javascript/core/public.js b/app/javascript/core/public.js
index b67fb13e54..5c7a51f447 100644
--- a/app/javascript/core/public.js
+++ b/app/javascript/core/public.js
@@ -6,28 +6,6 @@ import ready from '../mastodon/ready';
const { delegate } = require('@rails/ujs');
const { length } = require('stringz');
-delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
- if (button !== 0) {
- return true;
- }
- window.location.href = target.href;
- return false;
-});
-
-delegate(document, '.modal-button', 'click', e => {
- e.preventDefault();
-
- let href;
-
- if (e.target.nodeName !== 'A') {
- href = e.target.parentNode.href;
- } else {
- href = e.target.href;
- }
-
- window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
-});
-
const getProfileAvatarAnimationHandler = (swapTo) => {
//animate avatar gifs on the profile page when moused over
return ({ target }) => {
diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js
index 02aa4f144c..15476fc590 100644
--- a/app/javascript/flavours/glitch/actions/compose.js
+++ b/app/javascript/flavours/glitch/actions/compose.js
@@ -1,19 +1,21 @@
-import api from '../api';
-import { CancelToken, isCancel } from 'axios';
+import axios from 'axios';
import { throttle } from 'lodash';
+import { defineMessages } from 'react-intl';
+import api from 'flavours/glitch/api';
import { search as emojiSearch } from 'flavours/glitch/features/emoji/emoji_mart_search_light';
-import { useEmoji } from './emojis';
-import { tagHistory } from '../settings';
+import { tagHistory } from 'flavours/glitch/settings';
import { recoverHashtags } from 'flavours/glitch/utils/hashtag';
import resizeImage from 'flavours/glitch/utils/resize_image';
+import { showAlert, showAlertForError } from './alerts';
+import { useEmoji } from './emojis';
import { importFetchedAccounts } from './importer';
-import { updateTimeline } from './timelines';
-import { showAlertForError } from './alerts';
-import { showAlert } from './alerts';
import { openModal } from './modal';
-import { defineMessages } from 'react-intl';
+import { updateTimeline } from './timelines';
-let cancelFetchComposeSuggestionsAccounts, cancelFetchComposeSuggestionsTags;
+/** @type {AbortController | undefined} */
+let fetchComposeSuggestionsAccountsController;
+/** @type {AbortController | undefined} */
+let fetchComposeSuggestionsTagsController;
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
export const COMPOSE_CYCLE_ELEFRIEND = 'COMPOSE_CYCLE_ELEFRIEND';
@@ -25,11 +27,13 @@ export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
export const COMPOSE_RESET = 'COMPOSE_RESET';
-export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
-export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
-export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
-export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
-export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
+
+export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
+export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
+export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
+export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
+export const COMPOSE_UPLOAD_PROCESSING = 'COMPOSE_UPLOAD_PROCESSING';
+export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
export const THUMBNAIL_UPLOAD_REQUEST = 'THUMBNAIL_UPLOAD_REQUEST';
export const THUMBNAIL_UPLOAD_SUCCESS = 'THUMBNAIL_UPLOAD_SUCCESS';
@@ -83,10 +87,8 @@ const messages = defineMessages({
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
});
-const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1);
-
export const ensureComposeIsVisible = (getState, routerHistory) => {
- if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) {
+ if (!getState().getIn(['compose', 'mounted'])) {
routerHistory.push('/publish');
}
};
@@ -307,13 +309,16 @@ export function uploadCompose(files) {
if (status === 200) {
dispatch(uploadComposeSuccess(data, f));
} else if (status === 202) {
+ dispatch(uploadComposeProcessing());
+
let tryCount = 1;
+
const poll = () => {
api(getState).get(`/api/v1/media/${data.id}`).then(response => {
if (response.status === 200) {
dispatch(uploadComposeSuccess(response.data, f));
} else if (response.status === 206) {
- let retryAfter = (Math.log2(tryCount) || 1) * 1000;
+ const retryAfter = (Math.log2(tryCount) || 1) * 1000;
tryCount += 1;
setTimeout(() => poll(), retryAfter);
}
@@ -328,6 +333,10 @@ export function uploadCompose(files) {
};
};
+export const uploadComposeProcessing = () => ({
+ type: COMPOSE_UPLOAD_PROCESSING,
+});
+
export const uploadThumbnail = (id, file) => (dispatch, getState) => {
dispatch(uploadThumbnailRequest());
@@ -472,8 +481,8 @@ export function undoUploadCompose(media_id) {
};
export function clearComposeSuggestions() {
- if (cancelFetchComposeSuggestionsAccounts) {
- cancelFetchComposeSuggestionsAccounts();
+ if (fetchComposeSuggestionsAccountsController) {
+ fetchComposeSuggestionsAccountsController.abort();
}
return {
type: COMPOSE_SUGGESTIONS_CLEAR,
@@ -481,14 +490,14 @@ export function clearComposeSuggestions() {
};
const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => {
- if (cancelFetchComposeSuggestionsAccounts) {
- cancelFetchComposeSuggestionsAccounts();
+ if (fetchComposeSuggestionsAccountsController) {
+ fetchComposeSuggestionsAccountsController.abort();
}
+ fetchComposeSuggestionsAccountsController = new AbortController();
+
api(getState).get('/api/v1/accounts/search', {
- cancelToken: new CancelToken(cancel => {
- cancelFetchComposeSuggestionsAccounts = cancel;
- }),
+ signal: fetchComposeSuggestionsAccountsController.signal,
params: {
q: token.slice(1),
@@ -499,9 +508,11 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) =>
dispatch(importFetchedAccounts(response.data));
dispatch(readyComposeSuggestionsAccounts(token, response.data));
}).catch(error => {
- if (!isCancel(error)) {
+ if (!axios.isCancel(error)) {
dispatch(showAlertForError(error));
}
+ }).finally(() => {
+ fetchComposeSuggestionsAccountsController = undefined;
});
}, 200, { leading: true, trailing: true });
@@ -511,16 +522,16 @@ const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => {
};
const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
- if (cancelFetchComposeSuggestionsTags) {
- cancelFetchComposeSuggestionsTags();
+ if (fetchComposeSuggestionsTagsController) {
+ fetchComposeSuggestionsTagsController.abort();
}
dispatch(updateSuggestionTags(token));
+ fetchComposeSuggestionsTagsController = new AbortController();
+
api(getState).get('/api/v2/search', {
- cancelToken: new CancelToken(cancel => {
- cancelFetchComposeSuggestionsTags = cancel;
- }),
+ signal: fetchComposeSuggestionsTagsController.signal,
params: {
type: 'hashtags',
@@ -531,9 +542,11 @@ const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
}).then(({ data }) => {
dispatch(readyComposeSuggestionsTags(token, data.hashtags));
}).catch(error => {
- if (!isCancel(error)) {
+ if (!axios.isCancel(error)) {
dispatch(showAlertForError(error));
}
+ }).finally(() => {
+ fetchComposeSuggestionsTagsController = undefined;
});
}, 200, { leading: true, trailing: true });
diff --git a/app/javascript/flavours/glitch/actions/featured_tags.js b/app/javascript/flavours/glitch/actions/featured_tags.js
new file mode 100644
index 0000000000..18bb615394
--- /dev/null
+++ b/app/javascript/flavours/glitch/actions/featured_tags.js
@@ -0,0 +1,34 @@
+import api from '../api';
+
+export const FEATURED_TAGS_FETCH_REQUEST = 'FEATURED_TAGS_FETCH_REQUEST';
+export const FEATURED_TAGS_FETCH_SUCCESS = 'FEATURED_TAGS_FETCH_SUCCESS';
+export const FEATURED_TAGS_FETCH_FAIL = 'FEATURED_TAGS_FETCH_FAIL';
+
+export const fetchFeaturedTags = (id) => (dispatch, getState) => {
+ if (getState().getIn(['user_lists', 'featured_tags', id, 'items'])) {
+ return;
+ }
+
+ dispatch(fetchFeaturedTagsRequest(id));
+
+ api(getState).get(`/api/v1/accounts/${id}/featured_tags`)
+ .then(({ data }) => dispatch(fetchFeaturedTagsSuccess(id, data)))
+ .catch(err => dispatch(fetchFeaturedTagsFail(id, err)));
+};
+
+export const fetchFeaturedTagsRequest = (id) => ({
+ type: FEATURED_TAGS_FETCH_REQUEST,
+ id,
+});
+
+export const fetchFeaturedTagsSuccess = (id, tags) => ({
+ type: FEATURED_TAGS_FETCH_SUCCESS,
+ id,
+ tags,
+});
+
+export const fetchFeaturedTagsFail = (id, error) => ({
+ type: FEATURED_TAGS_FETCH_FAIL,
+ id,
+ error,
+});
diff --git a/app/javascript/flavours/glitch/actions/search.js b/app/javascript/flavours/glitch/actions/search.js
index 7767826933..f21c0058ba 100644
--- a/app/javascript/flavours/glitch/actions/search.js
+++ b/app/javascript/flavours/glitch/actions/search.js
@@ -29,7 +29,8 @@ export function clearSearch() {
export function submitSearch() {
return (dispatch, getState) => {
- const value = getState().getIn(['search', 'value']);
+ const value = getState().getIn(['search', 'value']);
+ const signedIn = !!getState().getIn(['meta', 'me']);
if (value.length === 0) {
dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, ''));
@@ -41,7 +42,7 @@ export function submitSearch() {
api(getState).get('/api/v2/search', {
params: {
q: value,
- resolve: true,
+ resolve: signedIn,
limit: 10,
},
}).then(response => {
diff --git a/app/javascript/flavours/glitch/actions/server.js b/app/javascript/flavours/glitch/actions/server.js
index af8fef780f..31d4aea100 100644
--- a/app/javascript/flavours/glitch/actions/server.js
+++ b/app/javascript/flavours/glitch/actions/server.js
@@ -5,6 +5,14 @@ export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST';
export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS';
export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL';
+export const EXTENDED_DESCRIPTION_REQUEST = 'EXTENDED_DESCRIPTION_REQUEST';
+export const EXTENDED_DESCRIPTION_SUCCESS = 'EXTENDED_DESCRIPTION_SUCCESS';
+export const EXTENDED_DESCRIPTION_FAIL = 'EXTENDED_DESCRIPTION_FAIL';
+
+export const SERVER_DOMAIN_BLOCKS_FETCH_REQUEST = 'SERVER_DOMAIN_BLOCKS_FETCH_REQUEST';
+export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS';
+export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL';
+
export const fetchServer = () => (dispatch, getState) => {
dispatch(fetchServerRequest());
@@ -28,3 +36,56 @@ const fetchServerFail = error => ({
type: SERVER_FETCH_FAIL,
error,
});
+
+export const fetchExtendedDescription = () => (dispatch, getState) => {
+ dispatch(fetchExtendedDescriptionRequest());
+
+ api(getState)
+ .get('/api/v1/instance/extended_description')
+ .then(({ data }) => dispatch(fetchExtendedDescriptionSuccess(data)))
+ .catch(err => dispatch(fetchExtendedDescriptionFail(err)));
+};
+
+const fetchExtendedDescriptionRequest = () => ({
+ type: EXTENDED_DESCRIPTION_REQUEST,
+});
+
+const fetchExtendedDescriptionSuccess = description => ({
+ type: EXTENDED_DESCRIPTION_SUCCESS,
+ description,
+});
+
+const fetchExtendedDescriptionFail = error => ({
+ type: EXTENDED_DESCRIPTION_FAIL,
+ error,
+});
+
+export const fetchDomainBlocks = () => (dispatch, getState) => {
+ dispatch(fetchDomainBlocksRequest());
+
+ api(getState)
+ .get('/api/v1/instance/domain_blocks')
+ .then(({ data }) => dispatch(fetchDomainBlocksSuccess(true, data)))
+ .catch(err => {
+ if (err.response.status === 404) {
+ dispatch(fetchDomainBlocksSuccess(false, []));
+ } else {
+ dispatch(fetchDomainBlocksFail(err));
+ }
+ });
+};
+
+const fetchDomainBlocksRequest = () => ({
+ type: SERVER_DOMAIN_BLOCKS_FETCH_REQUEST,
+});
+
+const fetchDomainBlocksSuccess = (isAvailable, blocks) => ({
+ type: SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS,
+ isAvailable,
+ blocks,
+});
+
+const fetchDomainBlocksFail = error => ({
+ type: SERVER_DOMAIN_BLOCKS_FETCH_FAIL,
+ error,
+});
diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js
index ef1e4dbbb9..a1c4dd43ad 100644
--- a/app/javascript/flavours/glitch/actions/timelines.js
+++ b/app/javascript/flavours/glitch/actions/timelines.js
@@ -156,8 +156,8 @@ export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => ex
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
-export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
-export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
+export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, tagged, max_id: maxId });
+export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
diff --git a/app/javascript/flavours/glitch/api.js b/app/javascript/flavours/glitch/api.js
index 645ef65006..6bbddbef66 100644
--- a/app/javascript/flavours/glitch/api.js
+++ b/app/javascript/flavours/glitch/api.js
@@ -1,20 +1,31 @@
+// @ts-check
+
import axios from 'axios';
import LinkHeader from 'http-link-header';
import ready from './ready';
+/**
+ * @param {import('axios').AxiosResponse} response
+ * @returns {LinkHeader}
+ */
export const getLinks = response => {
const value = response.headers.link;
if (!value) {
- return { refs: [] };
+ return new LinkHeader();
}
return LinkHeader.parse(value);
};
+/** @type {import('axios').RawAxiosRequestHeaders} */
const csrfHeader = {};
+/**
+ * @returns {void}
+ */
const setCSRFHeader = () => {
+ /** @type {HTMLMetaElement | null} */
const csrfToken = document.querySelector('meta[name=csrf-token]');
if (csrfToken) {
@@ -24,6 +35,10 @@ const setCSRFHeader = () => {
ready(setCSRFHeader);
+/**
+ * @param {() => import('immutable').Map} getState
+ * @returns {import('axios').RawAxiosRequestHeaders}
+ */
const authorizationHeaderFromState = getState => {
const accessToken = getState && getState().getIn(['meta', 'access_token'], '');
@@ -36,17 +51,25 @@ const authorizationHeaderFromState = getState => {
};
};
-export default getState => axios.create({
- headers: {
- ...csrfHeader,
- ...authorizationHeaderFromState(getState),
- },
+/**
+ * @param {() => import('immutable').Map} getState
+ * @returns {import('axios').AxiosInstance}
+ */
+export default function api(getState) {
+ return axios.create({
+ headers: {
+ ...csrfHeader,
+ ...authorizationHeaderFromState(getState),
+ },
- transformResponse: [function (data) {
- try {
- return JSON.parse(data);
- } catch(Exception) {
- return data;
- }
- }],
-});
+ transformResponse: [
+ function (data) {
+ try {
+ return JSON.parse(data);
+ } catch {
+ return data;
+ }
+ },
+ ],
+ });
+}
diff --git a/app/javascript/flavours/glitch/components/avatar.js b/app/javascript/flavours/glitch/components/avatar.js
index ce91d401d1..38fd99af59 100644
--- a/app/javascript/flavours/glitch/components/avatar.js
+++ b/app/javascript/flavours/glitch/components/avatar.js
@@ -70,6 +70,8 @@ export default class Avatar extends React.PureComponent {
onMouseLeave={this.handleMouseLeave}
style={style}
data-avatar-of={account && `@${account.get('acct')}`}
+ role='img'
+ aria-label={account?.get('acct')}
/>
);
}
diff --git a/app/javascript/flavours/glitch/components/dismissable_banner.js b/app/javascript/flavours/glitch/components/dismissable_banner.js
new file mode 100644
index 0000000000..ff52a619d2
--- /dev/null
+++ b/app/javascript/flavours/glitch/components/dismissable_banner.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import IconButton from './icon_button';
+import PropTypes from 'prop-types';
+import { injectIntl, defineMessages } from 'react-intl';
+import { bannerSettings } from 'flavours/glitch/settings';
+
+const messages = defineMessages({
+ dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' },
+});
+
+export default @injectIntl
+class DismissableBanner extends React.PureComponent {
+
+ static propTypes = {
+ id: PropTypes.string.isRequired,
+ children: PropTypes.node,
+ intl: PropTypes.object.isRequired,
+ };
+
+ state = {
+ visible: !bannerSettings.get(this.props.id),
+ };
+
+ handleDismiss = () => {
+ const { id } = this.props;
+ this.setState({ visible: false }, () => bannerSettings.set(id, true));
+ }
+
+ render () {
+ const { visible } = this.state;
+
+ if (!visible) {
+ return null;
+ }
+
+ const { children, intl } = this.props;
+
+ return (
+