Compare commits

...

51 Commits

Author SHA1 Message Date
anna 60d54db810
merge with catstodon/main 3 months ago
Jeremy Kescher e995f70ed3
Fix certain jsx files being in an old state in relation to the emoji reactions PR 3 months ago
Jeremy Kescher 55aef6a70c
Fix scss styles being in an old state 3 months ago
Jeremy Kescher 6d6a5561f8
Catstodon rev reset due to upstream version bump 3 months ago
Jeremy Kescher ce878a78bb
Merge branch 'glitch-soc' into develop 3 months ago
Claire d7d477047e Bump version to v4.3.0-alpha.3 (#29241) 3 months ago
Claire 6c1b6194f7 Merge pull request from GHSA-jhrq-qvrm-qr36
* Fix insufficient Content-Type checking of fetched ActivityStreams objects

* Allow JSON-LD documents with multiple profiles
3 months ago
renovate[bot] 63080e10a9 Update dependency pg to v1.5.5 (#29230)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months ago
Claire e7ca82762d Fix user creation failure handling in OAuth paths (#29207) 3 months ago
Jeremy Kescher f044841a6e
Catstodon rev +1.0.18 3 months ago
Jeremy Kescher 1c9291adcd
Merge branch 'glitch-soc' into develop
# Conflicts:
#	app/javascript/flavours/glitch/styles/components/accounts.scss
#	app/javascript/flavours/glitch/styles/components/status.scss
3 months ago
Claire 486e4bc7d3 Fix OmniAuth tests (#29201) 3 months ago
Claire 40439399c6 Bump version to v4.3.0-alpha.2 (#29200) 3 months ago
Claire 53b73ed6a2 Merge pull request from GHSA-vm39-j3vx-pch3
* Prevent different identities from a same SSO provider from accessing a same account

* Lock auth provider changes behind `ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH=true`

* Rename methods to avoid confusion between OAuth and OmniAuth
3 months ago
Emelia Smith 436419cc2f Merge pull request from GHSA-7w3c-p9j8-mq3x
* Ensure destruction of OAuth Applications notifies streaming

Due to doorkeeper using a dependent: delete_all relationship, the destroy of an OAuth Application bypassed the existing AccessTokenExtension callbacks for announcing destructing of access tokens.

* Ensure password resets revoke access to Streaming API

* Improve performance of deleting OAuth tokens

---------

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
3 months ago
Claire 3c3ef8b9d3 Add `sidekiq_unique_jobs:delete_all_locks` task and disable `sidekiq-unique-jobs` UI by default (#29199) 3 months ago
Emelia Smith eae436c5a8 Disable administrative doorkeeper routes (#29187) 3 months ago
Emelia Smith b83076647e Ignore legacy moderator and admin columns on User model (#29188) 3 months ago
renovate[bot] 7100106f3f Update dependency sidekiq-unique-jobs to v7.1.33 (#29175)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months ago
renovate[bot] 04da8f887b Update dependency pghero to v3.4.1 (#29144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months ago
renovate[bot] d9d0a6ef33 Update dependency irb to v1.11.2 (#29135)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months ago
renovate[bot] 93b365bfc0 Update dependency webmock to v3.20.0 (#29120)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months ago
Claire e3c4e90646
Fix new list button being blank (#2616) 3 months ago
Claire 084d051e6c
Merge pull request #2608 from ClearlyClaire/glitch-soc/refactor/monolithic-components-css
Merge SCSS in a single `components.scss` file
3 months ago
Claire c249c10f74 Fix account suggestion size inconsistency with upstream 3 months ago
Jeremy Kescher 924912bf9b
Catstodon rev +1.0.17 3 months ago
Essem 4cbaf46950
Hydrate reactions on streaming API 3 months ago
Essem d61b323c84
Purge status reactions on account delete 3 months ago
Claire d391e01b59 Revert notification bar color for now 3 months ago
Claire 3f89cec3c9 Further reduce differences with upstream 3 months ago
Claire f6f62002c7 Remove leftovers from glitch-soc's layout option 3 months ago
Claire 189b70cd6a Merge SCSS in a single `components.scss` file 3 months ago
Jeremy Kescher e4a333c6ae
Adjust initial_state_serializer to state of updated emoji reactions PR 3 months ago
Jeremy Kescher 40ef7b3849
Merge branch 'glitch-soc' into develop
# Conflicts:
#	app/javascript/flavours/glitch/initial_state.js
#	app/serializers/initial_state_serializer.rb
3 months ago
Claire 8c76a208ed
Merge pull request #2607 from ClearlyClaire/glitch-soc/cherry-pick-upstream
Cherry-pick upstream changes
3 months ago
Eugen Rochko 65eb943b9d Fix confirmation e-mails when signing up through an app (#29064) 3 months ago
Claire 19f1ffe287 Fix self-destruct schedule not actually replacing initial schedule (#29049) 3 months ago
renovate[bot] 5ea36a3606 Update dependency brakeman to v6.1.2 (#29062)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months ago
renovate[bot] b27cad27c2 Update dependency bootsnap to '~> 1.18.0' (#29019)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months ago
renovate[bot] 63f1f1465a Update dependency tzinfo-data to v1.2024.1 (#29052)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months ago
Matt Jankowski 4cd00c0dd9 Update `nsa` gem to version 0.3.0 (#29065) 3 months ago
renovate[bot] c6f7e3a1fb Update dependency haml_lint to v0.56.0 (#29082)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months ago
renovate[bot] 93aa783aa1 Update dependency nokogiri to v1.16.2 [SECURITY] (#29106)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months ago
renovate[bot] 868333c86f Update dependency capybara to v3.40.0 (#28966)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months ago
renovate[bot] ac49514bd6 Update dependency chewy to v7.5.1 (#29018)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months ago
github-actions[bot] a2611d782c
New Crowdin Translations (automated) (#2588)
* New Crowdin translations

* Fix bogus translation files

---------

Co-authored-by: GitHub Actions <noreply@github.com>
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
3 months ago
JS Moore 541cbdd963
Add env variable support for number of followable hashtags in feed column (#2500)
* Add env variable support for number of followable hashtags in feed column.

* Add a note about performance concerns for higher values.

See discussion in https://github.com/glitch-soc/mastodon/pull/2500

* Update .devcontainer/docker-compose.yml

---------

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
3 months ago
Essem 08e511cecb
Fix doodle modal icons (#2597) 3 months ago
Jeremy Kescher a08e32d1e1
Merge branch 'glitch-soc' into develop 4 months ago
Claire 5bc39b3196 Fix build-security docker tags 4 months ago
Claire c3936cbbe3 Temporary hack to correctly tag the security docker image… 4 months ago

@ -251,6 +251,11 @@ SMTP_FROM_ADDRESS=notifications@example.com
# Maximum allowed character count # Maximum allowed character count
MAX_TOOT_CHARS=500 MAX_TOOT_CHARS=500
# Maximum allowed hashtags to follow in a feed column
# Note that setting this value higher may cause significant
# database load
MAX_FEED_HASHTAGS=4
# Maximum number of pinned posts # Maximum number of pinned posts
MAX_PINNED_TOOTS=5 MAX_PINNED_TOOTS=5

@ -36,7 +36,7 @@ jobs:
tags: | tags: |
type=raw,value=edge type=raw,value=edge
type=raw,value=nightly type=raw,value=nightly
type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }} type=raw,value=${{ needs.compute-suffix.outputs.prerelease }}
secrets: inherit secrets: inherit
build-image-streaming: build-image-streaming:
@ -57,5 +57,5 @@ jobs:
tags: | tags: |
type=raw,value=edge type=raw,value=edge
type=raw,value=nightly type=raw,value=nightly
type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }} type=raw,value=${{ needs.compute-suffix.outputs.prerelease }}
secrets: inherit secrets: inherit

@ -2,6 +2,101 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [4.2.7] - 2024-02-16
### Fixed
- Fix OmniAuth tests and edge cases in error handling ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29201), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/29207))
- Fix new installs by upgrading to the latest release of the `nsa` gem, instead of a no longer existing commit ([mjankowski](https://github.com/mastodon/mastodon/pull/29065))
### Security
- Fix insufficient checking of remote posts ([GHSA-jhrq-qvrm-qr36](https://github.com/mastodon/mastodon/security/advisories/GHSA-jhrq-qvrm-qr36))
## [4.2.6] - 2024-02-14
### Security
- Update the `sidekiq-unique-jobs` dependency (see [GHSA-cmh9-rx85-xj38](https://github.com/mhenrixon/sidekiq-unique-jobs/security/advisories/GHSA-cmh9-rx85-xj38))
In addition, we have disabled the web interface for `sidekiq-unique-jobs` out of caution.
If you need it, you can re-enable it by setting `ENABLE_SIDEKIQ_UNIQUE_JOBS_UI=true`.
If you only need to clear all locks, you can now use `bundle exec rake sidekiq_unique_jobs:delete_all_locks`.
- Update the `nokogiri` dependency (see [GHSA-xc9x-jj77-9p9j](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-xc9x-jj77-9p9j))
- Disable administrative Doorkeeper routes ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/29187))
- Fix ongoing streaming sessions not being invalidated when applications get deleted in some cases ([GHSA-7w3c-p9j8-mq3x](https://github.com/mastodon/mastodon/security/advisories/GHSA-7w3c-p9j8-mq3x))
In some rare cases, the streaming server was not notified of access tokens revocation on application deletion.
- Change external authentication behavior to never reattach a new identity to an existing user by default ([GHSA-vm39-j3vx-pch3](https://github.com/mastodon/mastodon/security/advisories/GHSA-vm39-j3vx-pch3))
Up until now, Mastodon has allowed new identities from external authentication providers to attach to an existing local user based on their verified e-mail address.
This allowed upgrading users from a database-stored password to an external authentication provider, or move from one authentication provider to another.
However, this behavior may be unexpected, and means that when multiple authentication providers are configured, the overall security would be that of the least secure authentication provider.
For these reasons, this behavior is now locked under the `ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH` environment variable.
In addition, regardless of this environment variable, Mastodon will refuse to attach two identities from the same authentication provider to the same account.
## [4.2.5] - 2024-02-01
### Security
- Fix insufficient origin validation (CVE-2024-23832, [GHSA-3fjr-858r-92rw](https://github.com/mastodon/mastodon/security/advisories/GHSA-3fjr-858r-92rw))
## [4.2.4] - 2024-01-24
### Fixed
- Fix error when processing remote files with unusually long names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28823))
- Fix processing of compacted single-item JSON-LD collections ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28816))
- Retry 401 errors on replies fetching ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/28788))
- Fix `RecordNotUnique` errors in LinkCrawlWorker ([tribela](https://github.com/mastodon/mastodon/pull/28748))
- Fix Mastodon not correctly processing HTTP Signatures with query strings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28443), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28476))
- Fix potential redirection loop of streaming endpoint ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28665))
- Fix streaming API redirection ignoring the port of `streaming_api_base_url` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28558))
- Fix error when processing link preview with an array as `inLanguage` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28252))
- Fix unsupported time zone or locale preventing sign-up ([Gargron](https://github.com/mastodon/mastodon/pull/28035))
- Fix "Hide these posts from home" list setting not refreshing when switching lists ([brianholley](https://github.com/mastodon/mastodon/pull/27763))
- Fix missing background behind dismissable banner in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/27479))
- Fix line wrapping of language selection button with long locale codes ([gunchleoc](https://github.com/mastodon/mastodon/pull/27100), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27127))
- Fix `Undo Announce` activity not being sent to non-follower authors ([MitarashiDango](https://github.com/mastodon/mastodon/pull/18482))
- Fix N+1s because of association preloaders not actually getting called ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28339))
- Fix empty column explainer getting cropped under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28337))
- Fix `LinkCrawlWorker` error when encountering empty OEmbed response ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28268))
- Fix call to inefficient `delete_matched` cache method in domain blocks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28367))
### Security
- Add rate-limit of TOTP authentication attempts at controller level ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28801))
## [4.2.3] - 2023-12-05
### Fixed
- Fix dependency on `json-canonicalization` version that has been made unavailable since last release
## [4.2.2] - 2023-12-04
### Changed
- Change dismissed banners to be stored server-side ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27055))
- Change GIF max matrix size error to explicitly mention GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27927))
- Change `Follow` activities delivery to bypass availability check ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/27586))
- Change single-column navigation notice to be displayed outside of the logo container ([renchap](https://github.com/mastodon/mastodon/pull/27462), [renchap](https://github.com/mastodon/mastodon/pull/27476))
- Change Content-Security-Policy to be tighter on media paths ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26889))
- Change post language code to include country code when relevant ([gunchleoc](https://github.com/mastodon/mastodon/pull/27099), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27207))
### Fixed
- Fix upper border radius of onboarding columns ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27890))
- Fix incoming status creation date not being restricted to standard ISO8601 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27655), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28081))
- Fix some posts from threads received out-of-order sometimes not being inserted into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27653))
- Fix posts from force-sensitized accounts being able to trend ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27620))
- Fix error when trying to delete already-deleted file with OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27569))
- Fix batch attachment deletion when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27554))
- Fix processing LDSigned activities from actors with unknown public keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27474))
- Fix error and incorrect URLs in `/api/v1/accounts/:id/featured_tags` for remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27459))
- Fix report processing notice not mentioning the report number when performing a custom action ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27442))
- Fix handling of `inLanguage` attribute in preview card processing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27423))
- Fix own posts being removed from home timeline when unfollowing a used hashtag ([kmycode](https://github.com/mastodon/mastodon/pull/27391))
- Fix some link anchors being recognized as hashtags ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27271), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27584))
- Fix format-dependent redirects being cached regardless of requested format ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27634))
## [4.2.1] - 2023-10-10 ## [4.2.1] - 2023-10-10
### Added ### Added

@ -26,7 +26,7 @@ gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10' gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.8' gem 'addressable', '~> 2.8'
gem 'bootsnap', '~> 1.17.0', require: false gem 'bootsnap', '~> 1.18.0', require: false
gem 'browser' gem 'browser'
gem 'charlock_holmes', '~> 0.7.7' gem 'charlock_holmes', '~> 0.7.7'
gem 'chewy', '~> 7.3' gem 'chewy', '~> 7.3'
@ -63,7 +63,7 @@ gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar' gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.15' gem 'nokogiri', '~> 1.15'
gem 'nsa', github: 'jhawthorn/nsa', ref: 'e020fcc3a54d993ab45b7194d89ab720296c111b' gem 'nsa'
gem 'oj', '~> 3.14' gem 'oj', '~> 3.14'
gem 'ox', '~> 2.14' gem 'ox', '~> 2.14'
gem 'parslet' gem 'parslet'

@ -7,17 +7,6 @@ GIT
hkdf (~> 0.2) hkdf (~> 0.2)
jwt (~> 2.0) jwt (~> 2.0)
GIT
remote: https://github.com/jhawthorn/nsa.git
revision: e020fcc3a54d993ab45b7194d89ab720296c111b
ref: e020fcc3a54d993ab45b7194d89ab720296c111b
specs:
nsa (0.2.8)
activesupport (>= 4.2, < 7.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0)
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
@ -155,9 +144,9 @@ GEM
binding_of_caller (1.0.0) binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
blurhash (0.1.7) blurhash (0.1.7)
bootsnap (1.17.1) bootsnap (1.18.3)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (6.1.1) brakeman (6.1.2)
racc racc
browser (5.3.1) browser (5.3.1)
brpoplpush-redis_script (0.1.3) brpoplpush-redis_script (0.1.3)
@ -167,11 +156,11 @@ GEM
bundler-audit (0.9.1) bundler-audit (0.9.1)
bundler (>= 1.2.0, < 3) bundler (>= 1.2.0, < 3)
thor (~> 1.0) thor (~> 1.0)
capybara (3.39.2) capybara (3.40.0)
addressable addressable
matrix matrix
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (~> 1.8) nokogiri (~> 1.11)
rack (>= 1.6.0) rack (>= 1.6.0)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
@ -180,7 +169,7 @@ GEM
activesupport activesupport
cbor (0.5.9.6) cbor (0.5.9.6)
charlock_holmes (0.7.7) charlock_holmes (0.7.7)
chewy (7.5.0) chewy (7.5.1)
activesupport (>= 5.2) activesupport (>= 5.2)
elasticsearch (>= 7.12.0, < 7.14.0) elasticsearch (>= 7.12.0, < 7.14.0)
elasticsearch-dsl elasticsearch-dsl
@ -193,7 +182,8 @@ GEM
cose (1.3.0) cose (1.3.0)
cbor (~> 0.5.9) cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0) openssl-signature_algorithm (~> 1.0)
crack (0.4.5) crack (0.4.6)
bigdecimal
rexml rexml
crass (1.0.6) crass (1.0.6)
css_parser (1.14.0) css_parser (1.14.0)
@ -319,13 +309,13 @@ GEM
activesupport (>= 5.1) activesupport (>= 5.1)
haml (>= 4.0.6) haml (>= 4.0.6)
railties (>= 5.1) railties (>= 5.1)
haml_lint (0.55.0) haml_lint (0.56.0)
haml (>= 5.0) haml (>= 5.0)
parallel (~> 1.10) parallel (~> 1.10)
rainbow rainbow
rubocop (>= 1.0) rubocop (>= 1.0)
sysexits (~> 1.1) sysexits (~> 1.1)
hashdiff (1.0.1) hashdiff (1.1.0)
hashie (5.0.0) hashie (5.0.0)
hcaptcha (7.1.0) hcaptcha (7.1.0)
json json
@ -361,7 +351,7 @@ GEM
terminal-table (>= 1.5.1) terminal-table (>= 1.5.1)
idn-ruby (0.1.5) idn-ruby (0.1.5)
io-console (0.7.2) io-console (0.7.2)
irb (1.11.1) irb (1.11.2)
rdoc rdoc
reline (>= 0.4.2) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
@ -465,9 +455,14 @@ GEM
net-smtp (0.4.0.1) net-smtp (0.4.0.1)
net-protocol net-protocol
nio4r (2.5.9) nio4r (2.5.9)
nokogiri (1.16.0) nokogiri (1.16.2)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nsa (0.3.0)
activesupport (>= 4.2, < 7.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.16.3) oj (3.16.3)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
omniauth (2.1.1) omniauth (2.1.1)
@ -510,8 +505,8 @@ GEM
parslet (2.0.0) parslet (2.0.0)
pastel (0.8.0) pastel (0.8.0)
tty-color (~> 0.5) tty-color (~> 0.5)
pg (1.5.4) pg (1.5.5)
pghero (3.4.0) pghero (3.4.1)
activerecord (>= 6) activerecord (>= 6)
posix-spawn (0.3.15) posix-spawn (0.3.15)
premailer (1.21.0) premailer (1.21.0)
@ -712,7 +707,7 @@ GEM
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8) sidekiq (>= 6, < 8)
tilt (>= 1.4.0) tilt (>= 1.4.0)
sidekiq-unique-jobs (7.1.31) sidekiq-unique-jobs (7.1.33)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0) brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
redis (< 5.0) redis (< 5.0)
@ -771,7 +766,7 @@ GEM
unf (~> 0.1.0) unf (~> 0.1.0)
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
tzinfo-data (1.2023.4) tzinfo-data (1.2024.1)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
@ -798,7 +793,7 @@ GEM
webfinger (1.2.0) webfinger (1.2.0)
activesupport activesupport
httpclient (>= 2.4) httpclient (>= 2.4)
webmock (3.19.1) webmock (3.20.0)
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
@ -829,7 +824,7 @@ DEPENDENCIES
better_errors (~> 2.9) better_errors (~> 2.9)
binding_of_caller (~> 1.0) binding_of_caller (~> 1.0)
blurhash (~> 0.1) blurhash (~> 0.1)
bootsnap (~> 1.17.0) bootsnap (~> 1.18.0)
brakeman (~> 6.0) brakeman (~> 6.0)
browser browser
bundler-audit (~> 0.9) bundler-audit (~> 0.9)
@ -886,7 +881,7 @@ DEPENDENCIES
net-http (~> 0.4.0) net-http (~> 0.4.0)
net-ldap (~> 0.18) net-ldap (~> 0.18)
nokogiri (~> 1.15) nokogiri (~> 1.15)
nsa! nsa
oj (~> 3.14) oj (~> 3.14)
omniauth (~> 2.0) omniauth (~> 2.0)
omniauth-cas (~> 3.0.0.beta.1) omniauth-cas (~> 3.0.0.beta.1)

@ -7,7 +7,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def self.provides_callback_for(provider) def self.provides_callback_for(provider)
define_method provider do define_method provider do
@provider = provider @provider = provider
@user = User.find_for_oauth(request.env['omniauth.auth'], current_user) @user = User.find_for_omniauth(request.env['omniauth.auth'], current_user)
if @user.persisted? if @user.persisted?
record_login_activity record_login_activity
@ -17,6 +17,9 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
session["devise.#{provider}_data"] = request.env['omniauth.auth'] session["devise.#{provider}_data"] = request.env['omniauth.auth']
redirect_to new_user_registration_url redirect_to new_user_registration_url
end end
rescue ActiveRecord::RecordInvalid
flash[:alert] = I18n.t('devise.failure.omniauth_user_creation_failure') if is_navigational_format?
redirect_to new_user_session_url
end end
end end

@ -174,7 +174,19 @@ module JsonLdHelper
build_request(uri, on_behalf_of, options: request_options).perform do |response| build_request(uri, on_behalf_of, options: request_options).perform do |response|
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
body_to_json(response.body_with_limit) if response.code == 200 body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response)
end
end
def valid_activitypub_content_type?(response)
return true if response.mime_type == 'application/activity+json'
# When the mime type is `application/ld+json`, we need to check the profile,
# but `http.rb` does not parse it for us.
return false unless response.mime_type == 'application/ld+json'
response.headers[HTTP::Headers::CONTENT_TYPE]&.split(';')&.map(&:strip)&.any? do |str|
str.start_with?('profile="') && str[9...-1].split.include?('https://www.w3.org/ns/activitystreams')
end end
end end

@ -338,7 +338,7 @@ class StatusActionBar extends ImmutablePureComponent {
onClick={this.handleNoOp} // EmojiPickerDropdown handles that onClick={this.handleNoOp} // EmojiPickerDropdown handles that
title={intl.formatMessage(messages.react)} title={intl.formatMessage(messages.react)}
disabled={!canReact} disabled={!canReact}
icon='add-reaction' icon='add_reaction'
iconComponent={AddReactionIcon} iconComponent={AddReactionIcon}
/> />
); );

@ -131,7 +131,7 @@ export default class StatusPrepend extends PureComponent {
iconComponent = StarIcon; iconComponent = StarIcon;
break; break;
case 'reaction': case 'reaction':
iconId = 'add-reaction'; iconId = 'add_reaction';
iconComponent = AddReactionIcon; iconComponent = AddReactionIcon;
break; break;
case 'featured': case 'featured':

@ -15,7 +15,7 @@ export default class AutosuggestAccount extends ImmutablePureComponent {
return ( return (
<div className='autosuggest-account' title={account.get('acct')}> <div className='autosuggest-account' title={account.get('acct')}>
<div className='autosuggest-account-icon'><Avatar account={account} size={24} /></div> <div className='autosuggest-account-icon'><Avatar account={account} size={18} /></div>
<DisplayName account={account} inline /> <DisplayName account={account} inline />
</div> </div>
); );

@ -70,7 +70,6 @@ class ComposeForm extends ImmutablePureComponent {
singleColumn: PropTypes.bool, singleColumn: PropTypes.bool,
lang: PropTypes.string, lang: PropTypes.string,
advancedOptions: ImmutablePropTypes.map, advancedOptions: ImmutablePropTypes.map,
layout: PropTypes.string,
media: ImmutablePropTypes.list, media: ImmutablePropTypes.list,
sideArm: PropTypes.string, sideArm: PropTypes.string,
sensitive: PropTypes.bool, sensitive: PropTypes.bool,
@ -259,7 +258,6 @@ class ComposeForm extends ImmutablePureComponent {
intl, intl,
advancedOptions, advancedOptions,
isSubmitting, isSubmitting,
layout,
onChangeSpoilerness, onChangeSpoilerness,
onPaste, onPaste,
privacy, privacy,
@ -313,7 +311,7 @@ class ComposeForm extends ImmutablePureComponent {
onSuggestionsClearRequested={this.onSuggestionsClearRequested} onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected} onSuggestionSelected={this.onSuggestionSelected}
onPaste={onPaste} onPaste={onPaste}
autoFocus={!showSearch && !isMobile(window.innerWidth, layout)} autoFocus={!showSearch && !isMobile(window.innerWidth)}
lang={this.props.lang} lang={this.props.lang}
> >
<TextareaIcons advancedOptions={advancedOptions} /> <TextareaIcons advancedOptions={advancedOptions} />

@ -38,8 +38,8 @@ export default class NavigationBar extends ImmutablePureComponent {
{ profileLink !== undefined && ( { profileLink !== undefined && (
<a <a
className='edit'
href={profileLink} href={profileLink}
className='navigation-bar__profile-edit'
><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a> ><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
)} )}
</div> </div>

@ -76,7 +76,7 @@ class SearchResults extends ImmutablePureComponent {
return ( return (
<div className='drawer--results'> <div className='search-results'>
<header className='search-results__header'> <header className='search-results__header'>
<Icon id='search' icon={SearchIcon} /> <Icon id='search' icon={SearchIcon} />
<FormattedMessage id='explore.search_results' defaultMessage='Search results' /> <FormattedMessage id='explore.search_results' defaultMessage='Search results' />

@ -71,7 +71,6 @@ const mapStateToProps = state => ({
isInReply: state.getIn(['compose', 'in_reply_to']) !== null, isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
lang: state.getIn(['compose', 'language']), lang: state.getIn(['compose', 'language']),
advancedOptions: state.getIn(['compose', 'advanced_options']), advancedOptions: state.getIn(['compose', 'advanced_options']),
layout: state.getIn(['local_settings', 'layout']),
media: state.getIn(['compose', 'media_attachments']), media: state.getIn(['compose', 'media_attachments']),
sideArm: sideArmPrivacy(state), sideArm: sideArmPrivacy(state),
sensitive: state.getIn(['compose', 'sensitive']), sensitive: state.getIn(['compose', 'sensitive']),

@ -9,6 +9,8 @@ import { NonceProvider } from 'react-select';
import AsyncSelect from 'react-select/async'; import AsyncSelect from 'react-select/async';
import Toggle from 'react-toggle'; import Toggle from 'react-toggle';
import { maxFeedHashtags } from 'flavours/glitch/initial_state';
import SettingToggle from '../../notifications/components/setting_toggle'; import SettingToggle from '../../notifications/components/setting_toggle';
const messages = defineMessages({ const messages = defineMessages({
@ -46,9 +48,9 @@ class ColumnSettings extends PureComponent {
onSelect = mode => value => { onSelect = mode => value => {
const oldValue = this.tags(mode); const oldValue = this.tags(mode);
// Prevent changes that add more than 4 tags, but allow removing // Prevent changes that add more than the number of configured
// tags that were already added before // tags, but allow removing tags that were already added before
if ((value.length > 4) && !(value < oldValue)) { if ((value.length > maxFeedHashtags) && !(value < oldValue)) {
return; return;
} }

@ -68,7 +68,7 @@ class NewListForm extends PureComponent {
<Button <Button
disabled={disabled || !value} disabled={disabled || !value}
title={title} text={title}
onClick={this.handleClick} onClick={this.handleClick}
/> />
</form> </form>

@ -89,7 +89,7 @@ class FilterBar extends PureComponent {
onClick={this.onClick('reaction')} onClick={this.onClick('reaction')}
title={intl.formatMessage(tooltips.reactions)} title={intl.formatMessage(tooltips.reactions)}
> >
<Icon id='add-reaction' icon={AddReactionIcon} /> <Icon id='add_reaction' icon={AddReactionIcon} />
</button> </button>
<button <button
className={selectedFilter === 'reblog' ? 'active' : ''} className={selectedFilter === 'reblog' ? 'active' : ''}

@ -244,11 +244,11 @@ class ActionBar extends PureComponent {
const canReact = signedIn && status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions; const canReact = signedIn && status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions;
const reactButton = ( const reactButton = (
<IconButton <IconButton
className='add-reaction' className='add-reaction-icon'
onClick={this.handleNoOp} // EmojiPickerDropdown handles that onClick={this.handleNoOp} // EmojiPickerDropdown handles that
title={intl.formatMessage(messages.react)} title={intl.formatMessage(messages.react)}
disabled={!canReact} disabled={!canReact}
icon='add-reaction' icon='add_reaction'
iconComponent={AddReactionIcon} iconComponent={AddReactionIcon}
/> />
); );

@ -9,6 +9,10 @@ import { connect } from 'react-redux';
import Atrament from 'atrament'; // the doodling library import Atrament from 'atrament'; // the doodling library
import { debounce, mapValues } from 'lodash'; import { debounce, mapValues } from 'lodash';
import ColorsIcon from '@/material-icons/400-24px/colors.svg?react';
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import UndoIcon from '@/material-icons/400-24px/undo.svg?react';
import { doodleSet, uploadCompose } from 'flavours/glitch/actions/compose'; import { doodleSet, uploadCompose } from 'flavours/glitch/actions/compose';
import { Button } from 'flavours/glitch/components/button'; import { Button } from 'flavours/glitch/components/button';
import { IconButton } from 'flavours/glitch/components/icon_button'; import { IconButton } from 'flavours/glitch/components/icon_button';
@ -584,10 +588,10 @@ class DoodleModal extends ImmutablePureComponent {
</div> </div>
</div> </div>
<div className='doodle-toolbar'> <div className='doodle-toolbar'>
<IconButton icon='pencil' title='Draw' label='Draw' onClick={this.setModeDraw} size={18} active={this.mode === 'draw'} inverted /> <IconButton icon='pencil' iconComponent={EditIcon} title='Draw' label='Draw' onClick={this.setModeDraw} size={18} active={this.mode === 'draw'} inverted />
<IconButton icon='bath' title='Fill' label='Fill' onClick={this.setModeFill} size={18} active={this.mode === 'fill'} inverted /> <IconButton icon='bath' iconComponent={ColorsIcon} title='Fill' label='Fill' onClick={this.setModeFill} size={18} active={this.mode === 'fill'} inverted />
<IconButton icon='undo' title='Undo' label='Undo' onClick={this.undo} size={18} inverted /> <IconButton icon='undo' iconComponent={UndoIcon} title='Undo' label='Undo' onClick={this.undo} size={18} inverted />
<IconButton icon='trash' title='Clear' label='Clear' onClick={this.handleClearBtn} size={18} inverted /> <IconButton icon='trash' iconComponent={DeleteIcon} title='Clear' label='Clear' onClick={this.handleClearBtn} size={18} inverted />
</div> </div>
<div className='doodle-palette'> <div className='doodle-palette'>
{ {

@ -602,18 +602,7 @@ class UI extends PureComponent {
const { draggingOver } = this.state; const { draggingOver } = this.state;
const { children, isWide, location, dropdownMenuIsOpen, layout, moved } = this.props; const { children, isWide, location, dropdownMenuIsOpen, layout, moved } = this.props;
const columnsClass = layout => { const className = classNames('ui', {
switch (layout) {
case 'single':
return 'single-column';
case 'multiple':
return 'multi-columns';
default:
return 'auto-columns';
}
};
const className = classNames('ui', columnsClass(layout), {
'wide': isWide, 'wide': isWide,
'system-font': this.props.systemFontUi, 'system-font': this.props.systemFontUi,
'hicolor-privacy-icons': this.props.hicolorPrivacyIcons, 'hicolor-privacy-icons': this.props.hicolorPrivacyIcons,

@ -78,6 +78,7 @@ export const hasMultiColumnPath = initialPath === '/'
* @property {InitialStateMeta} meta * @property {InitialStateMeta} meta
* @property {object} local_settings * @property {object} local_settings
* @property {number} max_toot_chars * @property {number} max_toot_chars
* @property {number} max_feed_hashtags
* @property {PollLimits} poll_limits * @property {PollLimits} poll_limits
* @property {number} max_reactions * @property {number} max_reactions
*/ */
@ -144,6 +145,7 @@ export const sso_redirect = getMeta('sso_redirect');
// Glitch-soc-specific settings // Glitch-soc-specific settings
export const maxChars = (initialState && initialState.max_toot_chars) || 500; export const maxChars = (initialState && initialState.max_toot_chars) || 500;
export const maxFeedHashtags = (initialState && initialState.max_feed_hashtags) || 4;
export const favouriteModal = getMeta('favourite_modal'); export const favouriteModal = getMeta('favourite_modal');
export const pollLimits = (initialState && initialState.poll_limits); export const pollLimits = (initialState && initialState.poll_limits);
export const defaultContentType = getMeta('default_content_type'); export const defaultContentType = getMeta('default_content_type');

@ -1,4 +1,51 @@
{ {
"about.fork_disclaimer": "Glitch-soc adalah perangkat lunak sumber terbuka yang merupakan fork dari Mastodon.",
"account.disclaimer_full": "Informasi di bawah ini mungkin tidak mencerminkan profil pengguna secara lengkap.",
"account.follows": "Mengikuti",
"account.joined": "Bergabung {date}",
"account.suspended_disclaimer_full": "Pengguna ini telah ditangguhkan oleh moderator.",
"account.view_full_profile": "Tampilkan profil lengkap",
"advanced_options.icon_title": "Opsi lanjutan",
"advanced_options.local-only.long": "Jangan mengunggah ke instance lain",
"advanced_options.local-only.short": "Hanya lokal",
"advanced_options.local-only.tooltip": "Postingan ini hanya untuk lokal",
"advanced_options.threaded_mode.long": "Secara otomatis membuka balasan pada postingan",
"advanced_options.threaded_mode.short": "Mode Utasan",
"advanced_options.threaded_mode.tooltip": "Mode utasan dinyalakan",
"boost_modal.missing_description": "Toot ini berisi beberapa media tanpa deskripsi",
"column.favourited_by": "Disukai oleh",
"column.heading": "Lainnya",
"column.reblogged_by": "Dibagikan oleh",
"column.subheading": "Opsi lain-lain",
"column_header.profile": "Profil",
"column_subheading.lists": "Daftar",
"column_subheading.navigation": "Penelusuran",
"community.column_settings.allow_local_only": "Tampilkan toot lokal saja",
"compose.attach": "Lampirkan...",
"compose.attach.doodle": "Gambar sesuatu",
"compose.attach.upload": "Unggah file",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "Bahasa Markdown",
"compose.content-type.plain": "Teks biasa",
"compose_form.poll.multiple_choices": "Izinkan beberapa pilihan",
"compose_form.poll.single_choice": "Izinkan hanya satu pilihan",
"compose_form.spoiler": "Sembunyikan teks di balik peringatan",
"confirmation_modal.do_not_ask_again": "Jangan minta konfirmasi lagi",
"confirmations.deprecated_settings.confirm": "Gunakan preferensi Mastodon",
"confirmations.deprecated_settings.message": "Beberapa {app_settings} khusus perangkat Glitch-soc yang Anda gunakan telah digantikan oleh {preferences} Mastodon dan akan diganti:",
"confirmations.missing_media_description.confirm": "Tetap kirim",
"confirmations.missing_media_description.edit": "Sunting media",
"confirmations.missing_media_description.message": "Setidaknya satu lampiran media tidak memiliki deskripsi. Pertimbangkan untuk mendeskripsikan semua lampiran media untuk pengguna tunanetra sebelum mengirim toot Anda.",
"confirmations.unfilter.author": "Penulis",
"confirmations.unfilter.confirm": "Tampilkan",
"confirmations.unfilter.edit_filter": "Ubah saringan",
"content-type.change": "Jenis konten",
"direct.group_by_conversations": "Grupkan berdasarkan percakapan",
"endorsed_accounts_editor.endorsed_accounts": "Akun pilihan",
"favourite_modal.combo": "Anda dapat menekan {combo} untuk melewati ini lain kali",
"firehose.column_settings.allow_local_only": "Tampilkan postingan khusus lokal di \"Semua\"",
"home.column_settings.advanced": "Lanjutan",
"home.column_settings.filter_regex": "Saring dengan ekspresi reguler",
"settings.content_warnings": "Content warnings", "settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences" "settings.preferences": "Preferences"
} }

@ -1,4 +1,52 @@
{ {
"account.follows": "Följer",
"account.joined": "Gick med {date}",
"account.suspended_disclaimer_full": "Denna användare har stängts av av en moderator.",
"account.view_full_profile": "Visa full profil",
"advanced_options.icon_title": "Avancerade inställningar",
"advanced_options.local-only.long": "Lägg inte ut på andra instanser",
"advanced_options.local-only.short": "Endast lokalt",
"advanced_options.local-only.tooltip": "Detta inlägg är endast tillgängligt lokalt",
"advanced_options.threaded_mode.long": "Öppnar automatiskt ett svar vid publicering",
"advanced_options.threaded_mode.short": "Tråd-läge",
"advanced_options.threaded_mode.tooltip": "Tråd-läge på",
"boost_modal.missing_description": "Denna toot innehåller viss media utan beskrivning",
"column.favourited_by": "Favoritmarkerad av",
"column.heading": "Övrigt",
"column.reblogged_by": "Boostad av",
"column.subheading": "Övriga val",
"column_header.profile": "Profil",
"column_subheading.lists": "Listor",
"column_subheading.navigation": "Navigering",
"community.column_settings.allow_local_only": "Visa endast lokala toots",
"compose.attach": "Bifoga...",
"compose.attach.doodle": "Rita något",
"compose.attach.upload": "Ladda upp en fil",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "Markdown",
"compose.content-type.plain": "Klartext",
"compose_form.poll.multiple_choices": "Tillåt flera val",
"compose_form.poll.single_choice": "Tillåt ett val",
"compose_form.spoiler": "Göm text bakom varning",
"confirmation_modal.do_not_ask_again": "Fråga mig inte igen",
"confirmations.deprecated_settings.confirm": "Använd Mastodon-preferenser",
"confirmations.deprecated_settings.message": "Några av de glitch-soc-enhetsspecifika {app_settings} som du använder har ersatts av Mastodon-{preferences} och kommer att åsidosättas:",
"confirmations.missing_media_description.confirm": "Lägg ut ändå",
"confirmations.missing_media_description.edit": "Redigera media",
"confirmations.missing_media_description.message": "Minst en mediebilaga saknar beskrivning. Överväg att beskriva all media för synskadade innan du skickar din toot.",
"confirmations.unfilter.author": "Användare",
"confirmations.unfilter.confirm": "Visa",
"confirmations.unfilter.edit_filter": "Redigera filter",
"confirmations.unfilter.filters": "Matchande {count, plural, one {filter} other {filters}}",
"content-type.change": "Innehållstyp",
"direct.group_by_conversations": "Sortera efter konversation",
"endorsed_accounts_editor.endorsed_accounts": "Utvalda konton",
"favourite_modal.combo": "Du kan trycka på {combo} för att skippa detta nästa gång",
"firehose.column_settings.allow_local_only": "Visa endast lokala inlägg i \"Alla\"",
"home.column_settings.advanced": "Avancerat",
"home.column_settings.filter_regex": "Filtrera bort med reguljära uttryck",
"home.column_settings.show_direct": "Visa privata omnämningar",
"home.settings": "Kolumninställningar",
"settings.content_warnings": "Content warnings", "settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences" "settings.preferences": "Preferences"
} }

@ -14,9 +14,7 @@ const initialState = ImmutableMap({
export default function meta(state = initialState, action) { export default function meta(state = initialState, action) {
switch(action.type) { switch(action.type) {
case STORE_HYDRATE: case STORE_HYDRATE:
return state.merge(action.state.get('meta')) return state.merge(action.state.get('meta')).set('permissions', action.state.getIn(['role', 'permissions']));
.set('permissions', action.state.getIn(['role', 'permissions']))
.set('layout', layoutFromWindow(action.state.getIn(['local_settings', 'layout'])));
case changeLayout.type: case changeLayout.type:
return state.set('layout', action.payload.layout); return state.set('layout', action.payload.layout);
default: default:

@ -10,37 +10,6 @@
background-size: $size $size; background-size: $size $size;
} }
@mixin single-column($media, $parent: '&') {
.auto-columns #{$parent} {
@media #{$media} {
@content;
}
}
.single-column #{$parent} {
@content;
}
}
@mixin limited-single-column($media, $parent: '&') {
.auto-columns #{$parent},
.single-column #{$parent} {
@media #{$media} {
@content;
}
}
}
@mixin multi-columns($media, $parent: '&') {
.auto-columns #{$parent} {
@media #{$media} {
@content;
}
}
.multi-columns #{$parent} {
@content;
}
}
@mixin fullwidth-gallery { @mixin fullwidth-gallery {
&.full-width { &.full-width {
margin-left: -14px; margin-left: -14px;

File diff suppressed because it is too large Load Diff

@ -1,298 +0,0 @@
.image {
position: relative;
overflow: hidden;
&__preview {
position: absolute;
top: 0;
inset-inline-start: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
&.loaded &__preview {
display: none;
}
img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
border: 0;
background: transparent;
opacity: 0;
}
&.loaded img {
opacity: 1;
}
}
.link-footer {
flex: 0 0 auto;
padding: 10px;
padding-top: 20px;
z-index: 1;
font-size: 13px;
p {
color: $dark-text-color;
margin-bottom: 20px;
.version {
white-space: nowrap;
}
strong {
font-weight: 500;
}
a {
color: $dark-text-color;
text-decoration: underline;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
}
}
.about {
padding: 20px;
@media screen and (min-width: $no-gap-breakpoint) {
border-radius: 4px;
}
&__footer {
color: $dark-text-color;
text-align: center;
font-size: 15px;
line-height: 22px;
margin-top: 20px;
}
&__header {
margin-bottom: 30px;
&__hero {
width: 100%;
height: auto;
aspect-ratio: 1.9;
background: lighten($ui-base-color, 4%);
border-radius: 8px;
margin-bottom: 30px;
}
h1,
p {
text-align: center;
}
h1 {
font-size: 24px;
line-height: 1.5;
font-weight: 700;
margin-bottom: 10px;
}
p {
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: $darker-text-color;
}
}
&__meta {
background: lighten($ui-base-color, 4%);
border-radius: 4px;
display: flex;
margin-bottom: 30px;
font-size: 15px;
&__column {
box-sizing: border-box;
width: 50%;
padding: 20px;
}
&__divider {
width: 0;
border: 0;
border-style: solid;
border-color: lighten($ui-base-color, 8%);
border-left-width: 1px;
min-height: calc(100% - 60px);
flex: 0 0 auto;
}
h4 {
font-size: 15px;
text-transform: uppercase;
color: $darker-text-color;
font-weight: 500;
margin-bottom: 20px;
}
@media screen and (width <= 600px) {
display: block;
h4 {
text-align: center;
}
&__column {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
&__divider {
min-height: 0;
width: 100%;
border-left-width: 0;
border-top-width: 1px;
}
}
.layout-multiple-columns & {
display: block;
h4 {
text-align: center;
}
&__column {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
&__divider {
min-height: 0;
width: 100%;
border-left-width: 0;
border-top-width: 1px;
}
}
}
&__mail {
color: $primary-text-color;
text-decoration: none;
font-weight: 500;
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
.link-footer {
padding: 0;
margin-top: 60px;
text-align: center;
font-size: 15px;
line-height: 22px;
@media screen and (min-width: $no-gap-breakpoint) {
display: none;
}
}
.account {
padding: 0;
border: 0;
}
.account__avatar-wrapper {
margin-inline-start: 0;
}
.account__relationship {
display: none;
}
&__section {
margin-bottom: 10px;
&__title {
display: flex;
align-items: center;
gap: 6px;
font-size: 17px;
font-weight: 600;
line-height: 22px;
padding: 20px;
border-radius: 4px;
background: lighten($ui-base-color, 4%);
color: $highlight-text-color;
cursor: pointer;
}
&.active &__title {
border-radius: 4px 4px 0 0;
}
&__body {
border: 1px solid lighten($ui-base-color, 4%);
border-top: 0;
padding: 20px;
font-size: 15px;
line-height: 22px;
}
}
&__domain-blocks {
margin-top: 30px;
background: darken($ui-base-color, 4%);
border: 1px solid lighten($ui-base-color, 4%);
border-radius: 4px;
&__domain {
border-bottom: 1px solid lighten($ui-base-color, 4%);
padding: 10px;
font-size: 15px;
color: $darker-text-color;
&:nth-child(2n) {
background: darken($ui-base-color, 2%);
}
&:last-child {
border-bottom: 0;
}
&__header {
display: flex;
gap: 10px;
justify-content: space-between;
font-weight: 500;
margin-bottom: 4px;
}
h6 {
color: $secondary-text-color;
font-size: inherit;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
p {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}

@ -1,845 +0,0 @@
.account {
padding: 10px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
color: inherit;
text-decoration: none;
.account__display-name {
flex: 1 1 auto;
display: flex;
align-items: center;
gap: 10px;
color: $darker-text-color;
overflow: hidden;
text-decoration: none;
font-size: 14px;
.display-name {
margin-bottom: 4px;
}
.display-name strong {
display: inline;
}
}
&--minimal {
.account__display-name {
.display-name {
margin-bottom: 0;
}
.display-name strong {
display: block;
}
}
}
&__note {
font-size: 14px;
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
margin-top: 10px;
color: $darker-text-color;
&--missing {
color: $dark-text-color;
}
p {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
a {
color: inherit;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
}
}
.account__wrapper {
display: flex;
gap: 10px;
align-items: center;
}
.account__avatar-wrapper {
float: left;
}
.account__avatar {
@include avatar-radius;
display: block;
position: relative;
overflow: hidden;
img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
&-inline {
display: inline-block;
vertical-align: middle;
margin-inline-end: 5px;
}
&-composite {
@include avatar-radius;
overflow: hidden;
position: relative;
& > div {
@include avatar-radius;
float: left;
position: relative;
box-sizing: border-box;
}
.account__avatar {
width: 100% !important;
height: 100% !important;
}
&__label {
display: block;
position: absolute;
top: 50%;
inset-inline-start: 50%;
transform: translate(-50%, -50%);
color: $primary-text-color;
text-shadow: 1px 1px 2px $base-shadow-color;
font-weight: 700;
font-size: 15px;
}
}
}
.account__avatar-overlay {
position: relative;
&-overlay {
position: absolute;
bottom: 0;
inset-inline-end: 0;
z-index: 1;
}
}
.account__relationship {
white-space: nowrap;
display: flex;
align-items: center;
gap: 4px;
}
.account__header__wrapper {
flex: 0 0 auto;
background: lighten($ui-base-color, 4%);
}
.account__disclaimer {
display: flex;
padding: 10px;
gap: 5px;
color: $dark-text-color;
align-items: center;
strong {
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
a {
font-weight: 500;
color: inherit;
text-decoration: underline;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
}
.account__action-bar {
border-top: 1px solid lighten($ui-base-color, 8%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
line-height: 36px;
overflow: hidden;
flex: 0 0 auto;
display: flex;
}
.account__action-bar-links {
display: flex;
flex: 1 1 auto;
line-height: 18px;
text-align: center;
}
.account__action-bar__tab {
text-decoration: none;
overflow: hidden;
flex: 0 1 100%;
border-inline-start: 1px solid lighten($ui-base-color, 8%);
padding: 10px 0;
border-bottom: 4px solid transparent;
&:first-child {
border-inline-start: 0;
}
&.active {
border-bottom: 4px solid $ui-highlight-color;
}
& > span {
display: block;
text-transform: uppercase;
font-size: 11px;
color: $darker-text-color;
}
strong {
display: block;
font-size: 15px;
font-weight: 500;
color: $primary-text-color;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
abbr {
color: $highlight-text-color;
}
}
.account-authorize {
padding: 14px 10px;
.detailed-status__display-name {
display: block;
margin-bottom: 15px;
overflow: hidden;
}
}
.account-authorize__avatar {
float: left;
margin-inline-end: 10px;
}
.notification__report {
padding: 8px 10px;
padding-inline-start: 68px;
position: relative;
border-bottom: 1px solid lighten($ui-base-color, 8%);
min-height: 54px;
&__details {
display: flex;
justify-content: space-between;
align-items: center;
color: $darker-text-color;
font-size: 15px;
line-height: 22px;
strong {
font-weight: 500;
}
}
&__avatar {
position: absolute;
inset-inline-start: 10px;
top: 10px;
}
}
.notification__message {
margin-inline-start: 42px;
padding-top: 8px;
padding-inline-start: 26px;
cursor: default;
color: $darker-text-color;
font-size: 15px;
position: relative;
align-items: center;
.icon {
color: $highlight-text-color;
width: 18px;
height: 18px;
}
.icon-star {
color: $gold-star;
}
> span {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
}
.account--panel {
background: lighten($ui-base-color, 4%);
border-top: 1px solid lighten($ui-base-color, 8%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
display: flex;
flex-direction: row;
padding: 10px 0;
}
.account--panel__button,
.detailed-status__button {
flex: 1 1 auto;
text-align: center;
}
.detailed-status__button .emoji-button {
padding: 0;
}
.relationship-tag {
color: $white;
margin-bottom: 4px;
display: block;
background-color: rgba($black, 0.45);
text-transform: uppercase;
font-size: 11px;
font-weight: 500;
padding: 4px;
border-radius: 4px;
opacity: 0.7;
&:hover {
opacity: 1;
}
}
.account-gallery__container {
display: flex;
flex-wrap: wrap;
padding: 4px 2px;
}
.account-gallery__item {
border: 0;
box-sizing: border-box;
display: block;
position: relative;
border-radius: 4px;
overflow: hidden;
margin: 2px;
&__icons {
position: absolute;
top: 50%;
inset-inline-start: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
}
}
.notification__filter-bar,
.account__section-headline {
background: darken($ui-base-color, 4%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
cursor: default;
display: flex;
flex-shrink: 0;
button {
background: transparent;
border: 0;
margin: 0;
}
button,
a {
display: block;
flex: 1 1 auto;
color: $darker-text-color;
padding: 15px 0;
font-size: 14px;
font-weight: 500;
text-align: center;
text-decoration: none;
position: relative;
&.active {
color: $primary-text-color;
&::before {
display: block;
content: '';
position: absolute;
bottom: -1px;
left: 0;
width: 100%;
height: 3px;
border-radius: 4px;
background: $highlight-text-color;
}
}
}
&.directory__section-headline {
background: darken($ui-base-color, 2%);
border-bottom-color: transparent;
a,
button {
&.active {
&::before {
display: none;
}
&::after {
border-color: transparent transparent darken($ui-base-color, 7%);
}
}
}
}
}
.account__moved-note {
padding: 14px 10px;
padding-bottom: 16px;
background: lighten($ui-base-color, 4%);
border-top: 1px solid lighten($ui-base-color, 8%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
&__message {
position: relative;
margin-inline-start: 58px;
color: $dark-text-color;
padding: 8px 0;
padding-top: 0;
padding-bottom: 4px;
font-size: 14px;
> span {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
}
&__icon-wrapper {
inset-inline-start: -26px;
position: absolute;
}
.detailed-status__display-avatar {
position: relative;
}
.detailed-status__display-name {
margin-bottom: 0;
}
}
.account__header__content {
color: $darker-text-color;
font-size: 14px;
font-weight: 400;
overflow: hidden;
word-break: normal;
word-wrap: break-word;
p {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
a {
color: inherit;
text-decoration: underline;
&:hover {
text-decoration: none;
}
}
}
.account__header {
overflow: hidden;
&.inactive {
opacity: 0.5;
.account__header__image,
.account__avatar {
filter: grayscale(100%);
}
}
&__info {
position: absolute;
top: 10px;
inset-inline-start: 10px;
}
&__image {
overflow: hidden;
height: 145px;
position: relative;
background: darken($ui-base-color, 4%);
img {
object-fit: cover;
display: block;
width: 100%;
height: 100%;
margin: 0;
}
}
&__bar {
position: relative;
background: lighten($ui-base-color, 4%);
padding: 5px;
border-bottom: 1px solid lighten($ui-base-color, 12%);
.avatar {
display: block;
flex: 0 0 auto;
width: 94px;
.account__avatar {
background: darken($ui-base-color, 8%);
border: 2px solid lighten($ui-base-color, 4%);
}
}
}
&__badges {
display: flex;
flex-wrap: wrap;
gap: 8px;
.account-role {
line-height: unset;
}
}
&__tabs {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 7px 10px;
margin-top: -55px;
gap: 8px;
overflow: hidden;
margin-inline-start: -2px; // aligns the pfp with content below
&__buttons {
display: flex;
align-items: center;
gap: 8px;
padding-top: 55px;
overflow: hidden;
.button {
flex-shrink: 1;
white-space: nowrap;
@media screen and (max-width: $no-gap-breakpoint) {
min-width: 0;
}
}
.icon-button {
border: 1px solid lighten($ui-base-color, 12%);
border-radius: 4px;
box-sizing: content-box;
padding: 5px;
.icon {
width: 24px;
height: 24px;
}
}
}
&__name {
padding: 5px 10px;
.emojione {
width: 22px;
height: 22px;
}
h1 {
font-size: 16px;
line-height: 24px;
color: $primary-text-color;
font-weight: 500;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
small {
display: block;
font-size: 14px;
color: $darker-text-color;
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
span {
user-select: all;
}
.icon-lock {
height: 16px;
width: 16px;
position: relative;
top: 3px;
}
}
}
}
.spacer {
flex: 1 1 auto;
}
}
&__bio {
overflow: hidden;
margin: 0 -5px;
.account__header__content {
padding: 20px 15px;
padding-bottom: 5px;
color: $primary-text-color;
}
.account__header__joined {
font-size: 14px;
padding: 5px 15px;
color: $darker-text-color;
.columns-area--mobile & {
padding-inline-start: 20px;
padding-inline-end: 20px;
}
}
.account__header__fields {
margin: 0;
border-top: 1px solid lighten($ui-base-color, 12%);
a {
color: lighten($ui-highlight-color, 8%);
}
dl:first-child .verified {
border-radius: 0 4px 0 0;
}
.icon {
width: 18px;
height: 18px;
vertical-align: middle;
}
dd {
display: flex;
align-items: center;
gap: 4px;
}
.verified a {
color: $valid-value-color;
}
}
}
&__extra {
margin-top: 4px;
&__links {
font-size: 14px;
color: $darker-text-color;
padding: 10px 0;
a {
display: inline-block;
color: $darker-text-color;
text-decoration: none;
padding: 5px 10px;
font-weight: 500;
strong {
font-weight: 700;
color: $primary-text-color;
}
}
}
}
&__account-note {
margin: 0 -5px;
padding: 10px 15px;
display: flex;
flex-direction: column;
font-size: 14px;
font-weight: 400;
border-top: 1px solid lighten($ui-base-color, 12%);
border-bottom: 1px solid lighten($ui-base-color, 12%);
label {
display: block;
font-size: 12px;
font-weight: 500;
color: $darker-text-color;
text-transform: uppercase;
margin-bottom: 5px;
}
&__content {
white-space: pre-wrap;
padding: 10px 0;
}
strong {
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
}
textarea {
display: block;
box-sizing: border-box;
width: calc(100% + 20px);
color: $secondary-text-color;
background: $ui-base-color;
padding: 10px;
margin: 0 -10px;
font-family: inherit;
font-size: 14px;
resize: none;
border: 0;
outline: 0;
border-radius: 4px;
&::placeholder {
color: $dark-text-color;
opacity: 1;
}
}
}
}
.account__contents {
overflow: hidden;
}
.account__details {
display: flex;
flex-wrap: wrap;
column-gap: 1em;
}
.verified-badge {
display: inline-flex;
align-items: center;
color: $valid-value-color;
gap: 4px;
overflow: hidden;
white-space: nowrap;
> span {
overflow: hidden;
text-overflow: ellipsis;
}
a {
color: inherit;
font-weight: 500;
text-decoration: none;
}
.icon {
width: 16px;
height: 16px;
}
}
.moved-account-banner,
.follow-request-banner,
.account-memorial-banner {
padding: 20px;
background: lighten($ui-base-color, 4%);
display: flex;
align-items: center;
flex-direction: column;
&__message {
color: $darker-text-color;
padding: 8px 0;
padding-top: 0;
padding-bottom: 4px;
font-size: 14px;
font-weight: 500;
text-align: center;
margin-bottom: 16px;
}
&__action {
display: flex;
justify-content: space-between;
align-items: center;
gap: 15px;
width: 100%;
}
.detailed-status__display-name {
margin-bottom: 0;
}
}
.follow-request-banner .button {
width: 100%;
}
.account-memorial-banner__message {
margin-bottom: 0;
}

@ -1,233 +0,0 @@
.announcements__item__content {
word-wrap: break-word;
overflow-y: auto;
.emojione {
width: 20px;
height: 20px;
margin: -3px 0 0;
}
p {
margin-bottom: 10px;
white-space: pre-wrap;
&:last-child {
margin-bottom: 0;
}
}
a {
color: $secondary-text-color;
text-decoration: none;
&:hover {
text-decoration: underline;
}
&.mention {
&:hover {
text-decoration: none;
span {
text-decoration: underline;
}
}
}
&.unhandled-link {
color: $highlight-text-color;
}
}
}
.announcements {
background: lighten($ui-base-color, 8%);
font-size: 13px;
display: flex;
align-items: flex-end;
&__mastodon {
width: 124px;
flex: 0 0 auto;
@media screen and (max-width: 124px + 300px) {
display: none;
}
}
&__container {
width: calc(100% - 124px);
flex: 0 0 auto;
position: relative;
@media screen and (max-width: 124px + 300px) {
width: 100%;
}
}
&__item {
box-sizing: border-box;
width: 100%;
padding: 15px;
position: relative;
font-size: 15px;
line-height: 20px;
word-wrap: break-word;
font-weight: 400;
max-height: 50vh;
overflow: hidden;
display: flex;
flex-direction: column;
&__range {
display: block;
font-weight: 500;
margin-bottom: 10px;
padding-inline-end: 18px;
}
&__unread {
position: absolute;
top: 19px;
inset-inline-end: 19px;
display: block;
background: $highlight-text-color;
border-radius: 50%;
width: 0.625rem;
height: 0.625rem;
}
}
&__pagination {
padding: 15px;
color: $darker-text-color;
position: absolute;
bottom: 3px;
inset-inline-end: 0;
}
}
.layout-multiple-columns .announcements__mastodon {
display: none;
}
.layout-multiple-columns .announcements__container {
width: 100%;
}
.reactions-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-top: 15px;
margin-inline-start: -2px;
width: calc(100% - (90px - 33px));
&__item {
flex-shrink: 0;
background: lighten($ui-base-color, 12%);
border: 0;
border-radius: 3px;
margin: 2px;
cursor: pointer;
user-select: none;
padding: 0 6px;
display: flex;
align-items: center;
transition: all 100ms ease-in;
transition-property: background-color, color;
&__emoji {
display: block;
margin: 3px 0;
width: 16px;
height: 16px;
img {
display: block;
margin: 0;
width: 100%;
height: 100%;
min-width: auto;
min-height: auto;
vertical-align: bottom;
object-fit: contain;
}
}
&__count {
display: block;
min-width: 9px;
font-size: 13px;
font-weight: 500;
text-align: center;
margin-inline-start: 6px;
color: $darker-text-color;
}
&:hover,
&:focus,
&:active {
background: lighten($ui-base-color, 16%);
transition: all 200ms ease-out;
transition-property: background-color, color;
&__count {
color: lighten($darker-text-color, 4%);
}
}
&.active {
transition: all 100ms ease-in;
transition-property: background-color, color;
background-color: mix(
lighten($ui-base-color, 12%),
$ui-highlight-color,
80%
);
.reactions-bar__item__count {
color: lighten($highlight-text-color, 8%);
}
}
}
.emoji-picker-dropdown {
margin: 2px;
}
&:hover .emoji-button {
opacity: 0.85;
}
.emoji-button {
color: $darker-text-color;
margin: 0;
font-size: 16px;
width: auto;
flex-shrink: 0;
padding: 0 6px;
height: 22px;
display: flex;
align-items: center;
opacity: 0.5;
transition: all 100ms ease-in;
transition-property: background-color, color;
&:hover,
&:active,
&:focus {
opacity: 1;
color: lighten($darker-text-color, 4%);
transition: all 200ms ease-out;
transition-property: background-color, color;
}
}
&--empty {
.emoji-button {
padding: 0;
}
}
}

@ -1,668 +0,0 @@
.compose-form {
padding: 10px;
.emoji-picker-dropdown {
position: absolute;
top: 0;
inset-inline-end: 0;
::-webkit-scrollbar-track:hover,
::-webkit-scrollbar-track:active {
background-color: rgba($base-overlay-background, 0.3);
}
}
}
.character-counter {
cursor: default;
font-family: $font-sans-serif, sans-serif;
font-size: 14px;
font-weight: 600;
color: $lighter-text-color;
&.character-counter--over {
color: $warning-red;
}
}
.no-reduce-motion .spoiler-input {
transition:
height 0.4s ease,
opacity 0.4s ease;
}
.spoiler-input {
height: 0;
transform-origin: bottom;
opacity: 0;
&.spoiler-input--visible {
height: 36px;
margin-bottom: 11px;
opacity: 1;
}
input {
display: block;
box-sizing: border-box;
margin: 0;
border: 0;
border-radius: 4px;
padding: 10px;
width: 100%;
outline: 0;
color: $inverted-text-color;
background: $simple-background-color;
font-size: 14px;
font-family: inherit;
resize: vertical;
&::placeholder {
color: $dark-text-color;
}
&:focus {
outline: 0;
}
@include single-column('screen and (max-width: 630px)') {
font-size: 16px;
}
}
}
.compose-form__warning {
color: $inverted-text-color;
margin-bottom: 15px;
background: $ui-primary-color;
box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3);
padding: 8px 10px;
border-radius: 4px;
font-size: 13px;
font-weight: 400;
a {
color: $lighter-text-color;
font-weight: 500;
text-decoration: underline;
&:active,
&:focus,
&:hover {
text-decoration: none;
}
}
}
.compose-form__sensitive-button {
padding: 10px;
padding-top: 0;
font-size: 14px;
font-weight: 500;
&.active {
color: $highlight-text-color;
}
input[type='checkbox'] {
appearance: none;
display: inline-block;
position: relative;
border: 1px solid $ui-primary-color;
box-sizing: border-box;
width: 18px;
height: 18px;
flex: 0 0 auto;
margin-inline-start: 5px;
margin-inline-end: 10px;
top: -1px;
border-radius: 4px;
vertical-align: middle;
cursor: inherit;
&:checked {
border-color: $highlight-text-color;
background: $highlight-text-color
url("data:image/svg+xml;utf8,<svg width='18' height='18' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M4.5 8.5L8 12l6-6' stroke='white' stroke-width='1.5'/></svg>")
center center no-repeat;
}
}
}
.reply-indicator {
margin: 0 0 10px;
border-radius: 4px;
padding: 10px;
background: $ui-primary-color;
min-height: 23px;
overflow-y: auto;
flex: 0 2 auto;
}
.reply-indicator__header {
margin-bottom: 5px;
overflow: hidden;
}
.reply-indicator__cancel {
float: right;
line-height: 24px;
}
.reply-indicator__display-name {
color: $inverted-text-color;
display: block;
max-width: 100%;
line-height: 24px;
overflow: hidden;
text-decoration: none;
& > .display-name {
line-height: unset;
height: unset;
}
}
.reply-indicator__display-avatar {
float: left;
margin-inline-end: 5px;
}
.reply-indicator__content {
position: relative;
font-size: 14px;
line-height: 20px;
word-wrap: break-word;
font-weight: 400;
overflow: hidden;
padding-top: 5px;
color: $inverted-text-color;
white-space: pre-wrap;
p,
pre {
margin-bottom: 20px;
white-space: pre-wrap;
&:last-child {
margin-bottom: 0;
}
}
a {
color: $lighter-text-color;
text-decoration: none;
&:hover {
text-decoration: underline;
}
&.mention {
&:hover {
text-decoration: none;
span {
text-decoration: underline;
}
}
}
}
.emojione {
width: 20px;
height: 20px;
margin: -5px 0 0;
}
}
.compose-form .compose-form__autosuggest-wrapper {
position: relative;
}
.compose-form .autosuggest-textarea,
.compose-form .autosuggest-input {
position: relative;
width: 100%;
label {
.autosuggest-textarea__textarea {
display: block;
box-sizing: border-box;
margin: 0;
border: 0;
border-radius: 4px 4px 0 0;
padding: 10px 32px 0 10px;
width: 100%;
min-height: 100px;
outline: 0;
color: $inverted-text-color;
background: $simple-background-color;
font-size: 14px;
font-family: inherit;
resize: none;
scrollbar-color: initial;
&::placeholder {
color: $dark-text-color;
}
&::-webkit-scrollbar {
all: unset;
}
&:focus {
outline: 0;
}
@include single-column('screen and (max-width: 630px)') {
font-size: 16px;
}
@include limited-single-column('screen and (max-width: 600px)') {
height: 100px !important; // prevent auto-resize textarea
resize: vertical;
}
}
}
}
.compose-form__textarea-icons {
display: block;
position: absolute;
top: 29px;
inset-inline-end: 5px;
bottom: 5px;
overflow: hidden;
& > .textarea_icon {
display: block;
margin-top: 2px;
margin-inline-start: 2px;
width: 24px;
height: 24px;
color: $lighter-text-color;
font-size: 18px;
line-height: 24px;
text-align: center;
opacity: 0.8;
}
}
.autosuggest-textarea__suggestions-wrapper {
position: relative;
height: 0;
}
.autosuggest-textarea__suggestions {
box-sizing: border-box;
display: none;
position: absolute;
top: 100%;
width: 100%;
z-index: 99;
box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
background: $ui-secondary-color;
border-radius: 0 0 4px 4px;
color: $inverted-text-color;
font-size: 14px;
padding: 6px;
}
.autosuggest-textarea__suggestions--visible {
display: block;
}
.autosuggest-textarea__suggestions__item {
padding: 10px;
cursor: pointer;
border-radius: 4px;
&:hover,
&:focus,
&:active,
&.selected {
background: darken($ui-secondary-color, 10%);
}
.autosuggest-account,
.autosuggest-emoji,
.autosuggest-hashtag {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
line-height: 18px;
font-size: 14px;
}
.autosuggest-hashtag {
justify-content: space-between;
&__name {
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
strong {
font-weight: 500;
}
&__uses {
flex: 0 0 auto;
text-align: end;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.autosuggest-account-icon,
.autosuggest-emoji img {
margin-inline-end: 8px;
}
.autosuggest-account .display-name > span {
color: $lighter-text-color;
}
}
.compose-form__upload-wrapper {
overflow: hidden;
}
.compose-form__uploads-wrapper {
display: flex;
flex-direction: row;
flex-wrap: wrap;
font-family: inherit;
padding: 5px;
overflow: hidden;
}
.compose-form__upload {
flex: 1 1 0;
margin: 5px;
min-width: 40%;
.compose-form__upload-thumbnail {
position: relative;
border-radius: 4px;
height: 140px;
width: 100%;
background-color: $base-shadow-color;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
overflow: hidden;
& > .close {
mix-blend-mode: difference;
}
}
.icon-button {
flex: 0 1 auto;
color: $secondary-text-color;
font-size: 14px;
font-weight: 500;
padding: 10px;
font-family: inherit;
&:hover,
&:focus,
&:active {
color: lighten($secondary-text-color, 7%);
}
}
&__warning {
position: absolute;
z-index: 2;
bottom: 0;
inset-inline-start: 0;
inset-inline-end: 0;
box-sizing: border-box;
background: linear-gradient(
0deg,
rgba($base-shadow-color, 0.8) 0,
rgba($base-shadow-color, 0.35) 80%,
transparent
);
}
}
.compose-form__upload__actions {
background: linear-gradient(
180deg,
rgba($base-shadow-color, 0.8) 0,
rgba($base-shadow-color, 0.35) 80%,
transparent
);
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.upload-progress {
display: flex;
padding: 10px;
color: $darker-text-color;
overflow: hidden;
gap: 10px;
.icon {
width: 24px;
height: 24px;
}
span {
display: block;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
}
}
.upload-progress__message {
flex: 1 1 auto;
}
.upload-progress__backdrop {
position: relative;
margin-top: 5px;
border-radius: 6px;
width: 100%;
height: 6px;
background: darken($simple-background-color, 8%);
}
.upload-progress__tracker {
position: absolute;
top: 0;
inset-inline-start: 0;
height: 6px;
border-radius: 6px;
background: $ui-highlight-color;
}
.compose-form__modifiers {
color: $inverted-text-color;
font-family: inherit;
font-size: 14px;
background: $simple-background-color;
}
.compose-form__buttons-wrapper {
padding: 10px;
background: darken($simple-background-color, 8%);
border-radius: 0 0 4px 4px;
height: 27px;
display: flex;
justify-content: space-between;
flex: 0 0 auto;
}
.compose-form__buttons {
display: flex;
flex: 0 0 auto;
gap: 2px;
.icon-button {
height: 100%;
}
& .icon-button,
& .text-icon-button {
box-sizing: content-box;
padding: 0 3px;
}
}
.character-counter__wrapper {
align-self: center;
margin-inline-end: 4px;
}
.privacy-dropdown__dropdown {
border-radius: 4px;
box-shadow: var(--dropdown-shadow);
background: $simple-background-color;
overflow: hidden;
transform-origin: 50% 0;
}
.privacy-dropdown__option {
display: flex;
align-items: center;
padding: 10px;
color: $inverted-text-color;
cursor: pointer;
.privacy-dropdown__option__content {
flex: 1 1 auto;
color: $lighter-text-color;
&:not(:first-child) {
margin-inline-start: 10px;
}
strong {
display: block;
color: $inverted-text-color;
font-weight: 500;
}
}
&:hover,
&.active {
background: $ui-highlight-color;
color: $primary-text-color;
.privacy-dropdown__option__content {
color: $primary-text-color;
strong {
color: $primary-text-color;
}
}
}
&.active:hover {
background: lighten($ui-highlight-color, 4%);
}
}
.compose-form__publish {
display: flex;
justify-content: flex-end;
min-width: 0;
flex: 0 0 auto;
column-gap: 5px;
.compose-form__publish-button-wrapper {
padding-top: 10px;
button {
padding: 7px 10px;
text-align: center;
& > span {
display: flex;
gap: 5px;
align-items: center;
}
}
.side_arm {
height: 100%;
}
}
}
.language-dropdown {
&__dropdown {
background: $simple-background-color;
box-shadow: var(--dropdown-shadow);
border-radius: 4px;
overflow: hidden;
z-index: 2;
&.top {
transform-origin: 50% 100%;
}
&.bottom {
transform-origin: 50% 0;
}
.emoji-mart-search {
padding-inline-end: 10px;
}
.emoji-mart-search-icon {
inset-inline-end: 10px + 5px;
}
.emoji-mart-scroll {
padding: 0 10px 10px;
}
&__results {
&__item {
cursor: pointer;
color: $inverted-text-color;
font-weight: 500;
padding: 10px;
border-radius: 4px;
display: flex;
gap: 6px;
align-items: center;
&:focus,
&:active,
&:hover {
background: $ui-secondary-color;
}
&__common-name {
color: $darker-text-color;
}
&.active {
background: $ui-highlight-color;
color: $primary-text-color;
outline: 0;
.language-dropdown__dropdown__results__item__common-name {
color: $secondary-text-color;
}
&:hover {
background: lighten($ui-highlight-color, 4%);
}
}
}
}
}
}

@ -1,68 +0,0 @@
.scrollable .account-card {
margin: 10px;
background: lighten($ui-base-color, 8%);
}
.scrollable .account-card__title__avatar {
img,
.account__avatar {
border-color: lighten($ui-base-color, 8%);
}
}
.scrollable .account-card__bio::after {
background: linear-gradient(
to left,
lighten($ui-base-color, 8%),
transparent
);
}
.filter-form {
background: $ui-base-color;
&__column {
padding: 10px 15px;
padding-bottom: 0;
}
.radio-button {
display: block;
}
}
.radio-button {
font-size: 14px;
position: relative;
display: inline-block;
padding: 6px 0;
line-height: 18px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
input[type='radio'],
input[type='checkbox'] {
display: none;
}
&__input {
display: inline-block;
position: relative;
border: 1px solid $ui-primary-color;
box-sizing: border-box;
width: 18px;
height: 18px;
flex: 0 0 auto;
margin-inline-end: 10px;
top: -1px;
border-radius: 50%;
vertical-align: middle;
&.checked {
border-color: lighten($ui-highlight-color, 4%);
background: lighten($ui-highlight-color, 4%);
}
}
}

@ -1,23 +0,0 @@
.domain {
padding: 10px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
.domain__domain-name {
flex: 1 1 auto;
display: block;
color: $primary-text-color;
text-decoration: none;
font-size: 14px;
font-weight: 500;
}
}
.domain__wrapper {
display: flex;
}
.domain_buttons {
height: 18px;
padding: 10px;
white-space: nowrap;
}

@ -1,309 +0,0 @@
.drawer {
width: 300px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow-y: hidden;
padding: 10px 5px;
flex: none;
&:first-child {
padding-inline-start: 10px;
}
&:last-child {
padding-inline-end: 10px;
}
@include single-column('screen and (max-width: 630px)') {
flex: auto;
}
@include limited-single-column('screen and (max-width: 630px)') {
&,
&:first-child,
&:last-child {
padding: 0;
}
}
.wide & {
min-width: 300px;
max-width: 400px;
flex: 1 1 200px;
}
@include single-column('screen and (max-width: 630px)') {
:root & {
// Overrides `.wide` for single-column view
flex: auto;
width: 100%;
min-width: 0;
max-width: none;
padding: 0;
}
}
.react-swipeable-view-container & {
height: 100%;
}
}
.drawer__tab {
display: flex;
flex: 1 1 auto;
padding: 13px 3px 11px;
color: $darker-text-color;
text-decoration: none;
text-align: center;
font-size: 16px;
align-items: center;
justify-content: center;
border-bottom: 2px solid transparent;
}
.drawer__header {
flex: none;
font-size: 16px;
background: $ui-base-color;
margin-bottom: 10px;
display: flex;
flex-direction: row;
border-radius: 4px;
overflow: hidden;
a:hover {
background: lighten($ui-base-color, 3%);
}
}
.search {
position: relative;
margin-bottom: 10px;
flex: none;
@include limited-single-column(
'screen and (max-width: #{$no-gap-breakpoint})'
) {
margin-bottom: 0;
}
@include single-column('screen and (max-width: 630px)') {
font-size: 16px;
}
}
.navigation-bar {
padding: 10px;
color: $darker-text-color;
display: flex;
align-items: center;
a {
color: inherit;
text-decoration: none;
}
.acct {
display: block;
color: $secondary-text-color;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.navigation-bar__actions {
position: relative;
.compose__action-bar .icon-button {
pointer-events: auto;
transform: scale(1, 1) translate(0, 0);
opacity: 1;
.icon {
width: 24px;
height: 24px;
}
}
}
}
.navigation-bar__profile {
display: flex;
flex-direction: column;
flex: 1 1 auto;
margin-inline-start: 8px;
}
.drawer--results {
overflow-x: hidden;
overflow-y: scroll;
}
.search-results__section {
border-bottom: 1px solid lighten($ui-base-color, 8%);
&:last-child {
border-bottom: 0;
}
&__header {
background: darken($ui-base-color, 4%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
padding: 15px;
font-weight: 500;
font-size: 14px;
color: $darker-text-color;
display: flex;
justify-content: space-between;
h3 {
display: flex;
align-items: center;
gap: 5px;
}
button {
color: $highlight-text-color;
padding: 0;
border: 0;
background: 0;
font: inherit;
&:hover,
&:active,
&:focus {
text-decoration: underline;
}
}
}
.account:last-child,
& > div:last-child .status {
border-bottom: 0;
}
& > .hashtag {
display: block;
padding: 10px;
color: $secondary-text-color;
text-decoration: none;
&:hover,
&:active,
&:focus {
color: lighten($secondary-text-color, 4%);
text-decoration: underline;
}
}
}
.drawer__pager {
box-sizing: border-box;
padding: 0;
flex-grow: 1;
position: relative;
overflow: hidden;
display: flex;
border-radius: 4px;
}
.drawer__inner {
position: absolute;
top: 0;
inset-inline-start: 0;
background: $ui-base-color;
box-sizing: border-box;
padding: 0;
display: flex;
flex-direction: column;
overflow: hidden;
overflow-y: auto;
width: 100%;
height: 100%;
&.darker {
background: $ui-base-color;
}
}
.drawer__inner__mastodon {
background: $ui-base-color
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>')
no-repeat bottom / 100% auto;
flex: 1;
min-height: 47px;
display: none;
> img {
display: block;
object-fit: contain;
object-position: bottom left;
width: 85%;
height: 100%;
pointer-events: none;
user-select: none;
}
> .mastodon {
display: block;
width: 100%;
height: 100%;
border: 0;
cursor: inherit;
}
@media screen and (height >= 640px) {
display: block;
}
}
.pseudo-drawer {
background: lighten($ui-base-color, 13%);
font-size: 13px;
text-align: start;
}
.drawer__backdrop {
cursor: pointer;
position: absolute;
top: 0;
inset-inline-start: 0;
width: 100%;
height: 100%;
background: rgba($base-overlay-background, 0.5);
}
@for $i from 0 through 3 {
.mbstobon-#{$i} .drawer__inner__mastodon {
@if $i == 3 {
background:
url('~flavours/glitch/images/wave-drawer.png')
no-repeat
bottom /
100%
auto,
$ui-base-color;
} @else {
background:
url('~flavours/glitch/images/wave-drawer-glitched.png')
no-repeat
bottom /
100%
auto,
$ui-base-color;
}
& > .mastodon {
background: url('~flavours/glitch/images/mbstobon-ui-#{$i}.png')
no-repeat
left
bottom /
contain;
@if $i != 3 {
filter: contrast(50%) brightness(50%);
}
}
}
}

@ -1,106 +0,0 @@
.emojione {
font-size: inherit;
vertical-align: middle;
object-fit: contain;
margin: -0.2ex 0.15em 0.2ex;
width: 16px;
height: 16px;
img {
width: auto;
}
}
.emoji-picker-dropdown__menu {
background: $simple-background-color;
position: relative;
box-shadow: var(--dropdown-shadow);
border-radius: 4px;
margin-top: 5px;
z-index: 2;
.emoji-mart-scroll {
transition: opacity 200ms ease;
}
&.selecting .emoji-mart-scroll {
opacity: 0.5;
}
}
.emoji-picker-dropdown__modifiers {
position: absolute;
top: 60px;
inset-inline-end: 11px;
cursor: pointer;
}
.emoji-picker-dropdown__modifiers__menu {
position: absolute;
z-index: 4;
top: -4px;
inset-inline-start: -8px;
background: $simple-background-color;
border-radius: 4px;
box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
overflow: hidden;
button {
display: block;
cursor: pointer;
border: 0;
padding: 4px 8px;
background: transparent;
&:hover,
&:focus,
&:active {
background: rgba($ui-secondary-color, 0.4);
}
}
.emoji-mart-emoji {
height: 22px;
}
}
.emoji-mart-emoji {
span {
background-repeat: no-repeat;
}
}
.emoji-button {
display: block;
padding-top: 5px;
padding-bottom: 2px;
padding-inline-start: 2px;
padding-inline-end: 5px;
outline: 0;
cursor: pointer;
img {
filter: grayscale(100%);
opacity: 0.8;
display: block;
margin: 0;
width: 22px;
height: 22px;
}
&:hover,
&:active,
&:focus {
img {
opacity: 1;
filter: none;
border-radius: 100%;
}
}
&:focus-visible {
img {
outline: $ui-button-icon-focus-outline;
}
}
}

@ -1,143 +0,0 @@
.account-card__header {
position: relative;
}
.explore__search-header {
background: darken($ui-base-color, 4%);
justify-content: center;
align-items: center;
padding: 15px;
.search {
width: 100%;
margin-bottom: 0;
}
.search__input {
border: 1px solid lighten($ui-base-color, 8%);
padding: 10px;
}
.search__popout {
border: 1px solid lighten($ui-base-color, 8%);
}
.search .icon {
top: 9px;
inset-inline-end: 10px;
color: $dark-text-color;
}
}
.explore__search-results {
flex: 1 1 auto;
display: flex;
flex-direction: column;
background: $ui-base-color;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
.story {
display: flex;
align-items: center;
color: $primary-text-color;
text-decoration: none;
padding: 15px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
gap: 15px;
&:last-child {
border-bottom: 0;
}
&:hover,
&:active,
&:focus {
color: $highlight-text-color;
.story__details__publisher,
.story__details__shared {
color: $highlight-text-color;
}
}
&__details {
flex: 1 1 auto;
&__publisher {
color: $darker-text-color;
margin-bottom: 8px;
}
&__title {
font-size: 19px;
line-height: 24px;
font-weight: 500;
margin-bottom: 8px;
}
&__shared {
color: $darker-text-color;
}
strong {
font-weight: 500;
}
}
&__thumbnail {
flex: 0 0 auto;
position: relative;
width: 120px;
height: 120px;
.skeleton {
width: 100%;
height: 100%;
}
img {
border-radius: 8px;
display: block;
margin: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
&__preview {
border-radius: 8px;
display: block;
margin: 0;
width: 100%;
height: 100%;
object-fit: fill;
position: absolute;
top: 0;
inset-inline-start: 0;
z-index: 0;
&--hidden {
display: none;
}
}
}
&.expanded {
flex-direction: column;
.story__thumbnail {
order: 1;
width: 100%;
height: auto;
aspect-ratio: 1.91 / 1;
}
.story__details {
order: 2;
width: 100%;
flex: 0 0 auto;
}
}
}

@ -1,24 +0,0 @@
@import 'misc';
@import 'accounts';
@import 'domains';
@import 'status';
@import 'modal';
@import 'compose_form';
@import 'columns';
@import 'regeneration_indicator';
@import 'directory';
@import 'search';
@import 'emoji';
@import 'doodle';
@import 'drawer';
@import 'media';
@import 'sensitive';
@import 'lists';
@import 'emoji_picker';
@import 'local_settings';
@import 'single_column';
@import 'announcements';
@import 'explore';
@import 'signed_out';
@import 'privacy_policy';
@import 'about';

@ -1,94 +0,0 @@
.list-editor {
background: $ui-base-color;
flex-direction: column;
border-radius: 8px;
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
width: 380px;
overflow: hidden;
@media screen and (width <= 420px) {
width: 90%;
}
h4 {
padding: 15px 0;
background: lighten($ui-base-color, 13%);
font-weight: 500;
font-size: 16px;
text-align: center;
border-radius: 8px 8px 0 0;
}
.drawer__pager {
height: 50vh;
}
.drawer__inner {
border-radius: 0 0 8px 8px;
&.backdrop {
width: calc(100% - 60px);
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
border-radius: 0 0 0 8px;
}
}
&__accounts {
overflow-y: auto;
}
.account__display-name {
&:hover strong {
text-decoration: none;
}
}
.account__avatar {
cursor: default;
}
.search {
margin-bottom: 0;
}
}
.list-adder {
background: $ui-base-color;
flex-direction: column;
border-radius: 8px;
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
width: 380px;
overflow: hidden;
@media screen and (width <= 420px) {
width: 90%;
}
&__account {
background: lighten($ui-base-color, 13%);
}
&__lists {
background: lighten($ui-base-color, 13%);
height: 50vh;
border-radius: 0 0 8px 8px;
overflow-y: auto;
}
.list {
padding: 10px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
.list__wrapper {
display: flex;
}
.list__display-name {
flex: 1 1 auto;
overflow: hidden;
text-decoration: none;
font-size: 16px;
padding: 10px;
}
}

@ -1,802 +0,0 @@
.video-error-cover {
align-items: center;
background: $base-overlay-background;
color: $primary-text-color;
cursor: pointer;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
margin-top: 8px;
position: relative;
text-align: center;
z-index: 100;
}
.media-spoiler {
background: $base-overlay-background;
color: $darker-text-color;
border: 0;
width: 100%;
height: 100%;
&:hover,
&:active,
&:focus {
color: lighten($darker-text-color, 8%);
}
.status__content > & {
margin-top: 15px; // Add margin when used bare for NSFW video player
}
@include fullwidth-gallery;
}
.media-spoiler__warning {
display: block;
font-size: 14px;
}
.media-spoiler__trigger {
display: block;
font-size: 11px;
font-weight: 500;
}
.media-gallery__item__badges {
position: absolute;
bottom: 6px;
inset-inline-start: 6px;
display: flex;
gap: 2px;
}
.media-gallery__alt__label,
.media-gallery__gifv__label {
display: flex;
align-items: center;
justify-content: center;
color: $white;
background: rgba($black, 0.65);
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
z-index: 1;
pointer-events: none;
line-height: 18px;
.icon {
width: 15px;
height: 15px;
}
}
.media-gallery {
box-sizing: border-box;
margin-top: 8px;
overflow: hidden;
border-radius: 4px;
position: relative;
width: 100%;
min-height: 64px;
display: grid;
grid-template-columns: 50% 50%;
grid-template-rows: 50% 50%;
gap: 2px;
@include fullwidth-gallery;
}
.media-gallery__item {
border: 0;
box-sizing: border-box;
display: block;
position: relative;
border-radius: 4px;
overflow: hidden;
&--tall {
grid-row: span 2;
}
&--wide {
grid-column: span 2;
}
.full-width & {
border-radius: 0;
}
&.letterbox {
background: $base-shadow-color;
}
}
.media-gallery__item-thumbnail {
cursor: zoom-in;
display: block;
text-decoration: none;
color: $secondary-text-color;
position: relative;
z-index: 1;
&,
img {
height: 100%;
width: 100%;
object-fit: contain;
&:not(.letterbox) {
height: 100%;
object-fit: cover;
}
}
}
.media-gallery__preview {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
top: 0;
inset-inline-start: 0;
z-index: 0;
background: $base-overlay-background;
&--hidden {
display: none;
}
}
.media-gallery__gifv {
height: 100%;
overflow: hidden;
position: relative;
width: 100%;
display: flex;
justify-content: center;
}
.media-gallery__item-gifv-thumbnail {
cursor: zoom-in;
height: 100%;
width: 100%;
object-fit: contain;
user-select: none;
&:not(.letterbox) {
height: 100%;
object-fit: cover;
}
}
.media-gallery__item-thumbnail-label {
clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
clip: rect(1px, 1px, 1px, 1px);
overflow: hidden;
position: absolute;
}
.video-modal__container {
max-width: 100vw;
max-height: 100vh;
}
.audio-modal__container {
width: 50vw;
}
.media-modal {
width: 100%;
height: 100%;
position: relative;
&__close,
&__zoom-button {
color: rgba($white, 0.7);
&:hover,
&:focus,
&:active {
color: $white;
background-color: rgba($white, 0.15);
}
&:focus {
background-color: rgba($white, 0.3);
}
}
}
.media-modal__closer {
position: absolute;
top: 0;
inset-inline-start: 0;
inset-inline-end: 0;
bottom: 0;
}
.media-modal__navigation {
position: absolute;
top: 0;
inset-inline-start: 0;
inset-inline-end: 0;
bottom: 0;
pointer-events: none;
transition: opacity 0.3s linear;
will-change: opacity;
* {
pointer-events: auto;
}
&.media-modal__navigation--hidden {
opacity: 0;
* {
pointer-events: none;
}
}
}
.media-modal__nav {
background: transparent;
box-sizing: border-box;
border: 0;
color: rgba($white, 0.7);
cursor: pointer;
display: flex;
align-items: center;
font-size: 24px;
height: 20vmax;
margin: auto 0;
padding: 30px 15px;
position: absolute;
top: 0;
bottom: 0;
&:hover,
&:focus,
&:active {
color: $white;
}
}
.media-modal__nav--left {
inset-inline-start: 0;
}
.media-modal__nav--right {
inset-inline-end: 0;
}
.media-modal__overlay {
max-width: 600px;
position: absolute;
inset-inline-start: 0;
inset-inline-end: 0;
bottom: 0;
margin: 0 auto;
.picture-in-picture__footer {
border-radius: 0;
background: transparent;
padding: 20px 0;
.icon-button {
color: $white;
&:hover,
&:focus,
&:active {
color: $white;
background-color: rgba($white, 0.15);
}
&:focus {
background-color: rgba($white, 0.3);
}
&.active {
color: $highlight-text-color;
&:hover,
&:focus,
&:active {
background: rgba($highlight-text-color, 0.15);
}
&:focus {
background: rgba($highlight-text-color, 0.3);
}
}
&.star-icon.active {
color: $gold-star;
&:hover,
&:focus,
&:active {
background: rgba($gold-star, 0.15);
}
&:focus {
background: rgba($gold-star, 0.3);
}
}
&.disabled {
color: $white;
background-color: transparent;
cursor: default;
opacity: 0.4;
}
}
}
}
.media-modal__pagination {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.media-modal__page-dot {
flex: 0 0 auto;
background-color: $white;
opacity: 0.4;
height: 6px;
width: 6px;
border-radius: 50%;
margin: 0 4px;
padding: 0;
border: 0;
font-size: 0;
transition: opacity 0.2s ease-in-out;
&.active {
opacity: 1;
}
}
.media-modal__close {
position: absolute;
inset-inline-end: 8px;
top: 8px;
z-index: 100;
}
.detailed,
.fullscreen {
.video-player__volume__current,
.video-player__volume::before {
bottom: 27px;
}
.video-player__volume__handle {
bottom: 23px;
}
}
.audio-player {
overflow: hidden;
box-sizing: border-box;
position: relative;
background: darken($ui-base-color, 8%);
border-radius: 4px;
padding-bottom: 44px;
width: 100%;
&.editable {
border-radius: 0;
height: 100%;
}
&.inactive {
audio,
.video-player__controls {
visibility: hidden;
}
}
.video-player__volume::before,
.video-player__seek::before {
background: currentColor;
opacity: 0.15;
}
.video-player__seek__buffer {
background: currentColor;
opacity: 0.2;
}
.video-player__buttons button,
.video-player__buttons a {
color: currentColor;
opacity: 0.75;
&:active,
&:hover,
&:focus {
color: currentColor;
opacity: 1;
}
}
.video-player__time-sep,
.video-player__time-total,
.video-player__time-current {
color: currentColor;
}
.video-player__seek::before,
.video-player__seek__buffer,
.video-player__seek__progress {
top: 0;
}
.video-player__seek__handle {
top: -4px;
}
.video-player__controls {
padding-top: 10px;
background: transparent;
}
}
.video-player {
overflow: hidden;
position: relative;
background: $base-shadow-color;
max-width: 100%;
border-radius: 4px;
box-sizing: border-box;
color: $white;
display: flex;
align-items: center;
&.editable {
border-radius: 0;
height: 100% !important;
}
&:focus {
outline: 0;
}
.detailed-status & {
width: 100%;
height: 100%;
}
@include fullwidth-gallery;
video {
display: block;
max-width: 100vw;
max-height: 80vh;
z-index: 1;
position: relative;
}
&.fullscreen {
width: 100% !important;
height: 100% !important;
margin: 0;
video {
max-width: 100% !important;
max-height: 100% !important;
width: 100% !important;
height: 100% !important;
outline: 0;
}
}
&.inline {
video {
object-fit: contain;
}
}
&__controls {
position: absolute;
direction: ltr;
z-index: 2;
bottom: 0;
inset-inline-start: 0;
inset-inline-end: 0;
box-sizing: border-box;
background: linear-gradient(
0deg,
rgba($base-shadow-color, 0.85) 0,
rgba($base-shadow-color, 0.45) 60%,
transparent
);
padding: 0 15px;
opacity: 0;
transition: opacity 0.1s ease;
&.active {
opacity: 1;
}
}
&.inactive {
video,
.video-player__controls {
visibility: hidden;
}
}
&__spoiler {
display: none;
position: absolute;
top: 0;
inset-inline-start: 0;
width: 100%;
height: 100%;
z-index: 4;
border: 0;
background: $base-shadow-color;
color: $darker-text-color;
transition: none;
pointer-events: none;
&.active {
display: block;
pointer-events: auto;
&:hover,
&:active,
&:focus {
color: lighten($darker-text-color, 7%);
}
}
&__title {
display: block;
font-size: 14px;
}
&__subtitle {
display: block;
font-size: 11px;
font-weight: 500;
}
}
&__buttons-bar {
display: flex;
justify-content: space-between;
padding-bottom: 8px;
margin: 0 -5px;
.video-player__download__icon {
color: inherit;
.fa,
&:active .fa,
&:hover .fa,
&:focus .fa {
color: inherit;
}
}
}
&__buttons {
display: flex;
flex: 0 1 auto;
min-width: 30px;
align-items: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
gap: 5px;
.player-button {
display: inline-block;
outline: 0;
padding: 5px;
flex: 0 0 auto;
background: transparent;
border: 0;
color: rgba($white, 0.75);
&:active,
&:hover,
&:focus {
color: $white;
}
}
}
&__time {
display: inline;
flex: 0 1 auto;
overflow: hidden;
text-overflow: ellipsis;
margin: 0 5px;
}
&__time-sep,
&__time-total,
&__time-current {
font-size: 14px;
font-weight: 500;
}
&__time-current {
color: $white;
}
&__time-sep {
display: inline-block;
margin: 0 6px;
}
&__time-sep,
&__time-total {
color: $white;
}
&__volume {
flex: 0 0 auto;
display: inline-flex;
cursor: pointer;
height: 24px;
position: relative;
overflow: hidden;
.no-reduce-motion & {
transition: all 100ms linear;
}
&.active {
overflow: visible;
width: 50px;
margin-inline-end: 16px;
}
&::before {
content: '';
width: 50px;
background: rgba($white, 0.35);
border-radius: 4px;
display: block;
position: absolute;
height: 4px;
inset-inline-start: 0;
top: 50%;
transform: translate(0, -50%);
}
&__current {
display: block;
position: absolute;
height: 4px;
border-radius: 4px;
inset-inline-start: 0;
top: 50%;
transform: translate(0, -50%);
background: lighten($ui-highlight-color, 8%);
}
&__handle {
position: absolute;
z-index: 3;
border-radius: 50%;
width: 12px;
height: 12px;
top: 50%;
inset-inline-start: 0;
margin-inline-start: -6px;
transform: translate(0, -50%);
background: lighten($ui-highlight-color, 8%);
box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
opacity: 0;
.no-reduce-motion & {
transition: opacity 100ms linear;
}
}
&.active &__handle {
opacity: 1;
}
}
&__link {
padding: 2px 10px;
a {
text-decoration: none;
font-size: 14px;
font-weight: 500;
color: $white;
&:hover,
&:active,
&:focus {
text-decoration: underline;
}
}
}
&__seek {
cursor: pointer;
height: 24px;
position: relative;
&::before {
content: '';
width: 100%;
background: rgba($white, 0.35);
border-radius: 4px;
display: block;
position: absolute;
height: 4px;
top: 14px;
}
&__progress,
&__buffer {
display: block;
position: absolute;
height: 4px;
border-radius: 4px;
top: 14px;
background: lighten($ui-highlight-color, 8%);
}
&__buffer {
background: rgba($white, 0.2);
}
&__handle {
position: absolute;
z-index: 3;
opacity: 0;
border-radius: 50%;
width: 12px;
height: 12px;
top: 10px;
margin-inline-start: -6px;
background: lighten($ui-highlight-color, 8%);
box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
.no-reduce-motion & {
transition: opacity 0.1s ease;
}
&.active {
opacity: 1;
}
}
&:hover {
.video-player__seek__handle {
opacity: 1;
}
}
}
&.detailed,
&.fullscreen {
.video-player__buttons {
.player-button {
padding-top: 10px;
padding-bottom: 10px;
}
}
}
}
.gifv {
video {
max-width: 100vw;
max-height: 80vh;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1,209 +0,0 @@
.privacy-policy {
background: $ui-base-color;
padding: 20px;
@media screen and (min-width: $no-gap-breakpoint) {
border-radius: 4px;
}
&__body {
margin-top: 20px;
}
}
.prose {
color: $secondary-text-color;
font-size: 15px;
line-height: 22px;
p,
ul,
ol {
margin-top: 1.25em;
margin-bottom: 1.25em;
}
img {
margin-top: 2em;
margin-bottom: 2em;
max-width: 100%;
}
video {
margin-top: 2em;
margin-bottom: 2em;
max-width: 100%;
}
figure {
margin-top: 2em;
margin-bottom: 2em;
figcaption {
font-size: 0.875em;
line-height: 1.4285714;
margin-top: 0.8571429em;
}
}
figure > * {
margin-top: 0;
margin-bottom: 0;
}
h1 {
font-size: 1.5em;
margin-top: 0;
margin-bottom: 1em;
line-height: 1.33;
}
h2 {
font-size: 1.25em;
margin-top: 1.6em;
margin-bottom: 0.6em;
line-height: 1.6;
}
h3,
h4,
h5,
h6 {
margin-top: 1.5em;
margin-bottom: 0.5em;
line-height: 1.5;
}
ol {
counter-reset: list-counter;
}
li {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
ol > li {
counter-increment: list-counter;
&::before {
content: counter(list-counter) '.';
position: absolute;
inset-inline-start: 0;
}
}
ul > li::before {
content: '';
position: absolute;
background-color: $darker-text-color;
border-radius: 50%;
width: 0.375em;
height: 0.375em;
top: 0.5em;
inset-inline-start: 0.25em;
}
ul > li,
ol > li {
position: relative;
padding-inline-start: 1.75em;
}
& > ul > li p {
margin-top: 0.75em;
margin-bottom: 0.75em;
}
& > ul > li > *:first-child {
margin-top: 1.25em;
}
& > ul > li > *:last-child {
margin-bottom: 1.25em;
}
& > ol > li > *:first-child {
margin-top: 1.25em;
}
& > ol > li > *:last-child {
margin-bottom: 1.25em;
}
ul ul,
ul ol,
ol ul,
ol ol {
margin-top: 0.75em;
margin-bottom: 0.75em;
}
h1,
h2,
h3,
h4,
h5,
h6,
strong,
b {
color: $primary-text-color;
font-weight: 700;
}
em,
i {
font-style: italic;
}
a {
color: $highlight-text-color;
text-decoration: underline;
&:focus,
&:hover,
&:active {
text-decoration: none;
}
}
code {
font-size: 0.875em;
background: darken($ui-base-color, 8%);
border-radius: 4px;
padding: 0.2em 0.3em;
}
hr {
border: 0;
border-top: 1px solid lighten($ui-base-color, 4%);
margin-top: 3em;
margin-bottom: 3em;
}
hr + * {
margin-top: 0;
}
h2 + * {
margin-top: 0;
}
h3 + * {
margin-top: 0;
}
h4 + *,
h5 + *,
h6 + * {
margin-top: 0;
}
& > :first-child {
margin-top: 0;
}
& > :last-child {
margin-bottom: 0;
}
}

@ -1,43 +0,0 @@
.regeneration-indicator {
text-align: center;
font-size: 16px;
font-weight: 500;
color: $dark-text-color;
background: $ui-base-color;
cursor: default;
display: flex;
flex: 1 1 auto;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
&__figure {
&,
img {
display: block;
width: auto;
height: 160px;
margin: 0;
}
}
&--without-header {
padding-top: 20px + 48px;
}
&__label {
margin-top: 30px;
strong {
display: block;
margin-bottom: 10px;
color: $dark-text-color;
}
span {
font-size: 15px;
font-weight: 400;
}
}
}

@ -1,329 +0,0 @@
.search {
margin-bottom: 10px;
position: relative;
&__popout {
box-sizing: border-box;
display: none;
position: absolute;
inset-inline-start: 0;
margin-top: -2px;
width: 100%;
background: $ui-base-color;
border-radius: 0 0 4px 4px;
box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
z-index: 99;
font-size: 13px;
padding: 15px 5px;
h4 {
text-transform: uppercase;
color: $dark-text-color;
font-weight: 500;
padding: 0 10px;
margin-bottom: 10px;
}
.icon-button {
padding: 0;
}
.icon {
width: 18px;
height: 18px;
}
&__menu {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
&__message {
color: $dark-text-color;
padding: 0 10px;
}
&__item {
display: block;
box-sizing: border-box;
width: 100%;
border: 0;
font: inherit;
background: transparent;
color: $darker-text-color;
padding: 10px;
cursor: pointer;
border-radius: 4px;
text-align: start;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
&--flex {
display: flex;
justify-content: space-between;
}
.icon-button {
transition: none;
}
&:hover,
&:focus,
&:active,
&.selected {
background: $ui-highlight-color;
color: $primary-text-color;
.icon-button {
color: $primary-text-color;
}
}
mark {
background: transparent;
font-weight: 700;
color: $primary-text-color;
}
span {
overflow: inherit;
text-overflow: inherit;
}
}
}
}
&.active {
.search__popout {
display: block;
}
}
}
.search__input {
@include search-input;
display: block;
padding: 15px;
padding-inline-end: 30px;
line-height: 18px;
font-size: 16px;
&::placeholder {
color: lighten($darker-text-color, 4%);
}
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
&:focus {
background: lighten($ui-base-color, 4%);
}
}
.search__icon {
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus {
outline: 0 !important;
}
.icon {
position: absolute;
top: 13px;
inset-inline-end: 10px;
display: inline-block;
opacity: 0;
transition: all 100ms linear;
transition-property: color, transform, opacity;
font-size: 18px;
width: 24px;
height: 24px;
color: $secondary-text-color;
cursor: default;
pointer-events: none;
&.active {
pointer-events: auto;
opacity: 0.3;
}
}
.icon-search {
transform: rotate(0deg);
&.active {
pointer-events: auto;
opacity: 0.3;
}
}
.icon-times-circle {
top: 17px;
transform: rotate(0deg);
color: $action-button-color;
cursor: pointer;
&.active {
transform: rotate(90deg);
opacity: 1;
}
&:hover {
color: lighten($action-button-color, 7%);
}
}
}
.search-results__header {
color: $dark-text-color;
background: lighten($ui-base-color, 2%);
padding: 15px;
font-weight: 500;
font-size: 16px;
cursor: default;
display: flex;
align-items: center;
gap: 5px;
}
.search-results__info {
padding: 20px;
color: $darker-text-color;
text-align: center;
}
.trends {
&__item {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
gap: 15px;
&:last-child {
border-bottom: 0;
}
&__name {
flex: 1 1 auto;
color: $dark-text-color;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
strong {
font-weight: 500;
}
a {
color: $darker-text-color;
text-decoration: none;
font-size: 14px;
font-weight: 500;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:hover,
&:focus,
&:active {
span {
text-decoration: underline;
}
}
}
}
&__current {
flex: 0 0 auto;
font-size: 24px;
font-weight: 500;
text-align: end;
color: $secondary-text-color;
text-decoration: none;
}
&__sparkline {
flex: 0 0 auto;
width: 50px;
path:first-child {
fill: rgba($highlight-text-color, 0.25) !important;
fill-opacity: 1 !important;
}
path:last-child {
stroke: lighten($highlight-text-color, 6%) !important;
fill: none !important;
}
}
&--requires-review {
.trends__item__name {
color: $gold-star;
a {
color: $gold-star;
}
}
.trends__item__current {
color: $gold-star;
}
.trends__item__sparkline {
path:first-child {
fill: rgba($gold-star, 0.25) !important;
}
path:last-child {
stroke: lighten($gold-star, 6%) !important;
}
}
}
&--disabled {
.trends__item__name {
color: lighten($ui-base-color, 12%);
a {
color: lighten($ui-base-color, 12%);
}
}
.trends__item__current {
color: lighten($ui-base-color, 12%);
}
.trends__item__sparkline {
path:first-child {
fill: rgba(lighten($ui-base-color, 12%), 0.25) !important;
}
path:last-child {
stroke: lighten(lighten($ui-base-color, 12%), 6%) !important;
}
}
}
}
&--compact &__item {
padding: 10px;
padding-inline-end: 28px;
}
}

@ -1,26 +0,0 @@
.sensitive-info {
display: flex;
flex-direction: row;
align-items: center;
position: absolute;
top: 4px;
inset-inline-start: 4px;
z-index: 100;
}
.sensitive-marker {
margin: 0 3px;
border-radius: 2px;
padding: 2px 6px;
color: rgba($primary-text-color, 0.8);
background: rgba($base-overlay-background, 0.5);
font-size: 12px;
line-height: 18px;
text-transform: uppercase;
opacity: 0.9;
transition: opacity 0.1s ease;
.media-gallery:hover & {
opacity: 1;
}
}

@ -1,106 +0,0 @@
.sign-in-banner {
padding: 10px;
p {
color: $darker-text-color;
margin-bottom: 20px;
a {
color: $secondary-text-color;
text-decoration: none;
unicode-bidi: isolate;
&:hover {
text-decoration: underline;
}
}
}
.button {
margin-bottom: 10px;
}
}
.server-banner {
padding: 20px 0;
&__introduction {
color: $darker-text-color;
margin-bottom: 20px;
strong {
font-weight: 600;
}
a {
color: inherit;
text-decoration: underline;
&:hover,
&:active,
&:focus {
text-decoration: none;
}
}
}
&__hero {
display: block;
border-radius: 4px;
width: 100%;
height: auto;
margin-bottom: 20px;
aspect-ratio: 1.9;
border: 0;
background: $ui-base-color;
object-fit: cover;
}
&__description {
margin-bottom: 20px;
}
&__meta {
display: flex;
gap: 10px;
max-width: 100%;
&__column {
flex: 0 0 auto;
width: calc(50% - 5px);
overflow: hidden;
}
}
&__number {
font-weight: 600;
color: $primary-text-color;
font-size: 14px;
}
&__number-label {
color: $darker-text-color;
font-weight: 500;
font-size: 14px;
}
h4 {
text-transform: uppercase;
color: $darker-text-color;
margin-bottom: 10px;
font-weight: 600;
}
.account {
padding: 0;
border: 0;
}
.account__avatar-wrapper {
margin-inline-start: 0;
}
.spacer {
margin: 10px 0;
}
}

@ -1,319 +0,0 @@
.compose-panel {
width: 285px;
margin-top: 10px;
display: flex;
flex-direction: column;
height: calc(100% - 10px);
overflow-y: hidden;
.hero-widget {
box-shadow: none;
&__text,
&__img,
&__img img {
border-radius: 0;
}
&__text {
padding: 15px;
color: $secondary-text-color;
strong {
font-weight: 700;
color: $primary-text-color;
}
}
}
.search__input {
line-height: 18px;
font-size: 16px;
padding: 15px;
padding-inline-end: 30px;
}
.search__icon .fa {
top: 15px;
}
.navigation-bar {
flex: 0 1 48px;
}
.compose-form {
flex: 1;
display: flex;
flex-direction: column;
min-height: 310px;
}
.compose-form__autosuggest-wrapper {
overflow-y: auto;
background-color: $white;
border-radius: 4px 4px 0 0;
flex: 0 1 auto;
}
.autosuggest-textarea__textarea {
overflow-y: hidden;
}
}
.navigation-panel {
margin-top: 10px;
margin-bottom: 10px;
height: calc(100% - 20px);
overflow-y: auto;
display: flex;
flex-direction: column;
& > a {
flex: 0 0 auto;
}
.logo {
height: 30px;
width: auto;
}
}
.navigation-panel,
.compose-panel {
hr {
flex: 0 0 auto;
border: 0;
background: transparent;
border-top: 1px solid lighten($ui-base-color, 4%);
margin: 10px 0;
}
.flex-spacer {
background: transparent;
}
}
.columns-area--mobile {
flex-direction: column;
width: 100%;
margin: 0 auto;
.column,
.drawer {
width: 100%;
height: 100%;
padding: 0;
}
.account-card {
margin-bottom: 0;
}
.filter-form {
display: flex;
flex-wrap: wrap;
}
.autosuggest-textarea__textarea {
font-size: 16px;
}
.search__input {
line-height: 18px;
font-size: 16px;
padding: 15px;
padding-inline-end: 30px;
}
.search__icon .fa {
top: 15px;
}
.scrollable {
overflow: visible;
@supports (display: grid) {
contain: content;
}
}
@media screen and (min-width: $no-gap-breakpoint) {
padding: 10px 0;
padding-top: 0;
}
.detailed-status {
padding: 15px;
.media-gallery,
.video-player,
.audio-player {
margin-top: 15px;
}
}
.account__header__bar {
padding: 5px 10px;
}
.navigation-bar,
.compose-form {
padding: 15px;
}
.compose-form .compose-form__publish .compose-form__publish-button-wrapper {
padding-top: 15px;
}
.notification__report {
padding: 15px;
padding-inline-start: (48px + 15px * 2);
min-height: 48px + 2px;
&__avatar {
inset-inline-start: 15px;
top: 17px;
}
}
.status {
padding: 15px;
min-height: 48px + 2px;
.media-gallery,
&__action-bar,
.video-player,
.audio-player {
margin-top: 10px;
}
}
.account {
padding: 15px 10px;
&__header__bio {
margin: 0 -10px;
}
}
.notification {
&__message {
padding-top: 15px;
}
.status {
padding-top: 8px;
}
.account {
padding-top: 8px;
}
}
}
@media screen and (min-width: $no-gap-breakpoint) {
.react-swipeable-view-container .columns-area--mobile {
height: calc(100% - 10px) !important;
}
.getting-started__wrapper {
margin-bottom: 10px;
}
.search-page .search {
display: none;
}
.navigation-panel__legal {
display: none;
}
}
@media screen and (max-width: $no-gap-breakpoint - 1px) {
$sidebar-width: 285px;
.columns-area__panels__main {
width: calc(100% - $sidebar-width);
}
.columns-area__panels {
min-height: calc(100vh - $ui-header-height);
}
.columns-area__panels__pane--navigational {
min-width: $sidebar-width;
.columns-area__panels__pane__inner {
width: $sidebar-width;
}
.navigation-panel {
margin: 0;
background: $ui-base-color;
border-inline-start: 1px solid lighten($ui-base-color, 8%);
height: 100vh;
}
.navigation-panel__sign-in-banner,
.navigation-panel__logo,
.navigation-panel__banner,
.getting-started__trends {
display: none;
}
.column-link__icon {
font-size: 18px;
}
}
.layout-single-column .ui__header {
display: flex;
background: $ui-base-color;
border-bottom: 1px solid lighten($ui-base-color, 8%);
}
.column-header,
.column-back-button,
.scrollable,
.error-column {
border-radius: 0 !important;
}
}
@media screen and (max-width: $no-gap-breakpoint - 285px - 1px) {
$sidebar-width: 55px;
.columns-area__panels__main {
width: calc(100% - $sidebar-width);
}
.columns-area__panels__pane--navigational {
min-width: $sidebar-width;
.columns-area__panels__pane__inner {
width: $sidebar-width;
}
.column-link span {
display: none;
}
.list-panel {
display: none;
}
}
}
.explore__search-header {
display: none;
}
@media screen and (max-width: $no-gap-breakpoint - 1px) {
.columns-area__panels__pane--compositional {
display: none;
}
.explore__search-header {
display: flex;
}
}

File diff suppressed because it is too large Load Diff

@ -1,14 +1,14 @@
.emoji-mart { .emoji-mart {
font-size: 13px;
display: inline-block;
color: $inverted-text-color;
&, &,
* { * {
box-sizing: border-box; box-sizing: border-box;
line-height: 1.15; line-height: 1.15;
} }
font-size: 13px;
display: inline-block;
color: $inverted-text-color;
.emoji-mart-emoji { .emoji-mart-emoji {
padding: 6px; padding: 6px;
} }
@ -64,17 +64,17 @@
} }
.emoji-mart-anchor-bar { .emoji-mart-anchor-bar {
bottom: 0; bottom: -1px;
} }
} }
.emoji-mart-anchor-bar { .emoji-mart-anchor-bar {
position: absolute; position: absolute;
bottom: -3px; bottom: -5px;
inset-inline-start: 0; inset-inline-start: 0;
width: 100%; width: 100%;
height: 3px; height: 4px;
background-color: darken($ui-highlight-color, 3%); background-color: $highlight-text-color;
} }
.emoji-mart-anchors { .emoji-mart-anchors {
@ -173,7 +173,7 @@
} }
&:hover::before { &:hover::before {
z-index: 0; z-index: -1;
content: ''; content: '';
position: absolute; position: absolute;
top: 0; top: 0;

@ -13,8 +13,9 @@
@import 'forms'; @import 'forms';
@import 'accounts'; @import 'accounts';
@import 'statuses'; @import 'statuses';
@import 'components/index'; @import 'components';
@import 'polls'; @import 'polls';
@import 'emoji_picker';
@import 'about'; @import 'about';
@import 'tables'; @import 'tables';
@import 'admin'; @import 'admin';
@ -22,3 +23,5 @@
@import 'rtl'; @import 'rtl';
@import 'dashboard'; @import 'dashboard';
@import 'rich_text'; @import 'rich_text';
@import 'glitch_local_settings';
@import 'glitch_doodle';

@ -51,16 +51,6 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS'; export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL'; export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
export const REACTION_UPDATE = 'REACTION_UPDATE';
export const REACTION_ADD_REQUEST = 'REACTION_ADD_REQUEST';
export const REACTION_ADD_SUCCESS = 'REACTION_ADD_SUCCESS';
export const REACTION_ADD_FAIL = 'REACTION_ADD_FAIL';
export const REACTION_REMOVE_REQUEST = 'REACTION_REMOVE_REQUEST';
export const REACTION_REMOVE_SUCCESS = 'REACTION_REMOVE_SUCCESS';
export const REACTION_REMOVE_FAIL = 'REACTION_REMOVE_FAIL';
export function reblog(status, visibility) { export function reblog(status, visibility) {
return function (dispatch, getState) { return function (dispatch, getState) {
dispatch(reblogRequest(status)); dispatch(reblogRequest(status));
@ -526,89 +516,3 @@ export function unpinFail(status, error) {
skipLoading: true, skipLoading: true,
}; };
} }
export function addReaction(statusId, name, url) {
return (dispatch, getState) => {
const status = getState().get('statuses').get(statusId);
let alreadyAdded = false;
if (status) {
const reaction = status.get('reactions').find(x => x.get('name') === name);
if (reaction && reaction.get('me')) {
alreadyAdded = true;
}
}
if (!alreadyAdded) {
dispatch(addReactionRequest(statusId, name, url));
}
api(getState).post(`/api/v1/statuses/${statusId}/react/${encodeURIComponent(name)}`).then(() => {
dispatch(addReactionSuccess(statusId, name));
}).catch(err => {
if (!alreadyAdded) {
dispatch(addReactionFail(statusId, name, err));
}
});
};
}
export function addReactionRequest(statusId, name, url) {
return {
type: REACTION_ADD_REQUEST,
id: statusId,
name,
url,
};
}
export function addReactionSuccess(statusId, name) {
return {
type: REACTION_ADD_SUCCESS,
id: statusId,
name,
};
}
export function addReactionFail(statusId, name, error) {
return {
type: REACTION_ADD_FAIL,
id: statusId,
name,
error,
};
}
export function removeReaction(statusId, name) {
return (dispatch, getState) => {
dispatch(removeReactionRequest(statusId, name));
api(getState).post(`/api/v1/statuses/${statusId}/unreact/${encodeURIComponent(name)}`).then(() => {
dispatch(removeReactionSuccess(statusId, name));
}).catch(err => {
dispatch(removeReactionFail(statusId, name, err));
});
};
}
export function removeReactionRequest(statusId, name) {
return {
type: REACTION_REMOVE_REQUEST,
id: statusId,
name,
};
}
export function removeReactionSuccess(statusId, name) {
return {
type: REACTION_REMOVE_SUCCESS,
id: statusId,
name,
};
}
export function removeReactionFail(statusId, name) {
return {
type: REACTION_REMOVE_FAIL,
id: statusId,
name,
};
}

@ -130,7 +130,6 @@ const excludeTypesFromFilter = filter => {
'follow', 'follow',
'follow_request', 'follow_request',
'favourite', 'favourite',
'reaction',
'reblog', 'reblog',
'mention', 'mention',
'poll', 'poll',

@ -1,170 +0,0 @@
import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif, reduceMotion } from '../initial_state';
import spring from 'react-motion/lib/spring';
import TransitionMotion from 'react-motion/lib/TransitionMotion';
import classNames from 'classnames';
import React from 'react';
import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light';
import { AnimatedNumber } from './animated_number';
import { assetHost } from '../utils/config';
export default class StatusReactions extends ImmutablePureComponent {
static propTypes = {
statusId: PropTypes.string.isRequired,
reactions: ImmutablePropTypes.list.isRequired,
numVisible: PropTypes.number,
addReaction: PropTypes.func.isRequired,
canReact: PropTypes.bool.isRequired,
removeReaction: PropTypes.func.isRequired,
};
willEnter() {
return { scale: reduceMotion ? 1 : 0 };
}
willLeave() {
return { scale: reduceMotion ? 0 : spring(0, { stiffness: 170, damping: 26 }) };
}
render() {
const { reactions, numVisible } = this.props;
let visibleReactions = reactions
.filter(x => x.get('count') > 0)
.sort((a, b) => b.get('count') - a.get('count'));
if (numVisible >= 0) {
visibleReactions = visibleReactions.filter((_, i) => i < numVisible);
}
const styles = visibleReactions.map(reaction => ({
key: reaction.get('name'),
data: reaction,
style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) },
})).toArray();
return (
<TransitionMotion styles={styles} willEnter={this.willEnter} willLeave={this.willLeave}>
{items => (
<div className={classNames('reactions-bar', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
{items.map(({ key, data, style }) => (
<Reaction
key={key}
statusId={this.props.statusId}
reaction={data}
style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }}
addReaction={this.props.addReaction}
removeReaction={this.props.removeReaction}
canReact={this.props.canReact}
/>
))}
</div>
)}
</TransitionMotion>
);
}
}
class Reaction extends ImmutablePureComponent {
static propTypes = {
statusId: PropTypes.string,
reaction: ImmutablePropTypes.map.isRequired,
addReaction: PropTypes.func.isRequired,
removeReaction: PropTypes.func.isRequired,
canReact: PropTypes.bool.isRequired,
style: PropTypes.object,
};
state = {
hovered: false,
};
handleClick = () => {
const { reaction, statusId, addReaction, removeReaction } = this.props;
if (reaction.get('me')) {
removeReaction(statusId, reaction.get('name'));
} else {
addReaction(statusId, reaction.get('name'));
}
}
handleMouseEnter = () => this.setState({ hovered: true })
handleMouseLeave = () => this.setState({ hovered: false })
render() {
const { reaction } = this.props;
return (
<button
className={classNames('reactions-bar__item', { active: reaction.get('me') })}
onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
disabled={!this.props.canReact}
style={this.props.style}
>
<span className='reactions-bar__item__emoji'>
<Emoji
hovered={this.state.hovered}
emoji={reaction.get('name')}
url={reaction.get('url')}
staticUrl={reaction.get('static_url')}
/>
</span>
<span className='reactions-bar__item__count'>
<AnimatedNumber value={reaction.get('count')} />
</span>
</button>
);
}
}
class Emoji extends React.PureComponent {
static propTypes = {
emoji: PropTypes.string.isRequired,
hovered: PropTypes.bool.isRequired,
url: PropTypes.string,
staticUrl: PropTypes.string,
};
render() {
const { emoji, hovered, url, staticUrl } = this.props;
if (unicodeMapping[emoji]) {
const { filename, shortCode } = unicodeMapping[this.props.emoji];
const title = shortCode ? `:${shortCode}:` : '';
return (
<img
draggable='false'
className='emojione'
alt={emoji}
title={title}
src={`${assetHost}/emoji/${filename}.svg`}
/>
);
} else {
const filename = (autoPlayGif || hovered) ? url : staticUrl;
const shortCode = `:${emoji}:`;
return (
<img
draggable='false'
className='emojione custom-emoji'
alt={shortCode}
title={shortCode}
src={filename}
/>
);
}
}
}

@ -30,8 +30,6 @@ import {
unbookmark, unbookmark,
pin, pin,
unpin, unpin,
addReaction,
removeReaction,
} from '../actions/interactions'; } from '../actions/interactions';
import { openModal } from '../actions/modal'; import { openModal } from '../actions/modal';
import { initMuteModal } from '../actions/mutes'; import { initMuteModal } from '../actions/mutes';
@ -137,14 +135,6 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
} }
}, },
onReactionAdd (statusId, name, url) {
dispatch(addReaction(statusId, name, url));
},
onReactionRemove (statusId, name) {
dispatch(removeReaction(statusId, name));
},
onEmbed (status) { onEmbed (status) {
dispatch(openModal({ dispatch(openModal({
modalType: 'EMBED', modalType: 'EMBED',

@ -322,7 +322,6 @@ class EmojiPickerDropdown extends PureComponent {
onSkinTone: PropTypes.func.isRequired, onSkinTone: PropTypes.func.isRequired,
skinTone: PropTypes.number.isRequired, skinTone: PropTypes.number.isRequired,
button: PropTypes.node, button: PropTypes.node,
disabled: PropTypes.bool,
}; };
state = { state = {
@ -356,7 +355,7 @@ class EmojiPickerDropdown extends PureComponent {
}; };
onToggle = (e) => { onToggle = (e) => {
if (!this.state.disabled && !this.state.loading && (!e.key || e.key === 'Enter')) { if (!this.state.loading && (!e.key || e.key === 'Enter')) {
if (this.state.active) { if (this.state.active) {
this.onHideDropdown(); this.onHideDropdown();
} else { } else {

@ -119,17 +119,6 @@ export default class ColumnSettings extends PureComponent {
</div> </div>
</div> </div>
<div role='group' aria-labelledby='notifications-reaction'>
<span id='notifications-reaction' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reaction' defaultMessage='Reactions:' /></span>
<div className='column-settings__pillbar'>
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reaction']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'reaction']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'reaction']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'reaction']} onChange={onChange} label={soundStr} />
</div>
</div>
<div role='group' aria-labelledby='notifications-mention'> <div role='group' aria-labelledby='notifications-mention'>
<span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span> <span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>

@ -46,8 +46,6 @@ import {
unreblog, unreblog,
pin, pin,
unpin, unpin,
addReaction,
removeReaction,
} from '../../actions/interactions'; } from '../../actions/interactions';
import { openModal } from '../../actions/modal'; import { openModal } from '../../actions/modal';
import { initMuteModal } from '../../actions/mutes'; import { initMuteModal } from '../../actions/mutes';
@ -267,19 +265,6 @@ class Status extends ImmutablePureComponent {
} }
}; };
handleReactionAdd = (statusId, name, url) => {
const { dispatch } = this.props;
const { signedIn } = this.context.identity;
if (signedIn) {
dispatch(addReaction(statusId, name, url));
}
};
handleReactionRemove = (statusId, name) => {
this.props.dispatch(removeReaction(statusId, name));
};
handlePin = (status) => { handlePin = (status) => {
if (status.get('pinned')) { if (status.get('pinned')) {
this.props.dispatch(unpin(status)); this.props.dispatch(unpin(status));
@ -720,15 +705,12 @@ class Status extends ImmutablePureComponent {
status={status} status={status}
onOpenVideo={this.handleOpenVideo} onOpenVideo={this.handleOpenVideo}
onOpenMedia={this.handleOpenMedia} onOpenMedia={this.handleOpenMedia}
onReactionAdd={this.handleReactionAdd}
onReactionRemove={this.handleReactionRemove}
onToggleHidden={this.handleToggleHidden} onToggleHidden={this.handleToggleHidden}
onTranslate={this.handleTranslate} onTranslate={this.handleTranslate}
domain={domain} domain={domain}
showMedia={this.state.showMedia} showMedia={this.state.showMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility} onToggleMediaVisibility={this.handleToggleMediaVisibility}
pictureInPicture={pictureInPicture} pictureInPicture={pictureInPicture}
emojiMap={this.props.emojiMap}
/> />
<ActionBar <ActionBar
@ -736,7 +718,6 @@ class Status extends ImmutablePureComponent {
status={status} status={status}
onReply={this.handleReplyClick} onReply={this.handleReplyClick}
onFavourite={this.handleFavouriteClick} onFavourite={this.handleFavouriteClick}
onReactionAdd={this.handleReactionAdd}
onReblog={this.handleReblogClick} onReblog={this.handleReblogClick}
onBookmark={this.handleBookmarkClick} onBookmark={this.handleBookmarkClick}
onDelete={this.handleDeleteClick} onDelete={this.handleDeleteClick}

@ -34,7 +34,6 @@ const initialState = ImmutableMap({
follow: false, follow: false,
follow_request: false, follow_request: false,
favourite: false, favourite: false,
reaction: false,
reblog: false, reblog: false,
mention: false, mention: false,
poll: false, poll: false,
@ -58,7 +57,6 @@ const initialState = ImmutableMap({
follow_request: false, follow_request: false,
favourite: true, favourite: true,
reblog: true, reblog: true,
reaction: true,
mention: true, mention: true,
poll: true, poll: true,
status: true, status: true,
@ -72,7 +70,6 @@ const initialState = ImmutableMap({
follow_request: false, follow_request: false,
favourite: true, favourite: true,
reblog: true, reblog: true,
reaction: true,
mention: true, mention: true,
poll: true, poll: true,
status: true, status: true,

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M346-140 100-386q-10-10-15-22t-5-25q0-13 5-25t15-22l230-229-106-106 62-65 400 400q10 10 14.5 22t4.5 25q0 13-4.5 25T686-386L440-140q-10 10-22 15t-25 5q-13 0-25-5t-22-15Zm47-506L179-432h428L393-646Zm399 526q-36 0-61-25.5T706-208q0-27 13.5-51t30.5-47l42-54 44 54q16 23 30 47t14 51q0 37-26 62.5T792-120Z"/></svg>

After

Width:  |  Height:  |  Size: 405 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M346-140 100-386q-10-10-15-22t-5-25q0-13 5-25t15-22l230-229-106-106 62-65 400 400q10 10 14.5 22t4.5 25q0 13-4.5 25T686-386L440-140q-10 10-22 15t-25 5q-13 0-25-5t-22-15Zm47-506L179-432h428L393-646Zm399 526q-36 0-61-25.5T706-208q0-27 13.5-51t30.5-47l42-54 44 54q16 23 30 47t14 51q0 37-26 62.5T792-120Z"/></svg>

After

Width:  |  Height:  |  Size: 405 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M280-200v-80h284q63 0 109.5-40T720-420q0-60-46.5-100T564-560H312l104 104-56 56-200-200 200-200 56 56-104 104h252q97 0 166.5 63T800-420q0 94-69.5 157T564-200H280Z"/></svg>

After

Width:  |  Height:  |  Size: 267 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M280-200v-80h284q63 0 109.5-40T720-420q0-60-46.5-100T564-560H312l104 104-56 56-200-200 200-200 56 56-104 104h252q97 0 166.5 63T800-420q0 94-69.5 157T564-200H280Z"/></svg>

After

Width:  |  Height:  |  Size: 267 B

@ -1136,7 +1136,6 @@ body > [data-popper-placement] {
.status__content, .status__content,
.status__action-bar, .status__action-bar,
.reactions-bar,
.media-gallery, .media-gallery,
.video-player, .video-player,
.audio-player, .audio-player,
@ -1190,10 +1189,6 @@ body > [data-popper-placement] {
} }
} }
} }
.reactions-bar--empty {
margin-top: 0;
}
} }
.status__relative-time { .status__relative-time {
@ -1335,16 +1330,6 @@ body > [data-popper-placement] {
align-items: center; align-items: center;
gap: 18px; gap: 18px;
margin-top: 16px; margin-top: 16px;
& > .emoji-picker-dropdown > .emoji-button {
padding: 0;
}
}
.status__action-bar-button {
.fa-plus {
padding-top: 1px;
}
} }
.detailed-status__action-bar-dropdown { .detailed-status__action-bar-dropdown {
@ -4236,10 +4221,6 @@ a.status-card {
text-align: center; text-align: center;
} }
.detailed-status__button .emoji-button {
padding: 0;
}
.column-settings__outer { .column-settings__outer {
background: lighten($ui-base-color, 8%); background: lighten($ui-base-color, 8%);
padding: 15px; padding: 15px;

@ -4,14 +4,34 @@ module ApplicationExtension
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
include Redisable
has_many :created_users, class_name: 'User', foreign_key: 'created_by_application_id', inverse_of: :created_by_application has_many :created_users, class_name: 'User', foreign_key: 'created_by_application_id', inverse_of: :created_by_application
validates :name, length: { maximum: 60 } validates :name, length: { maximum: 60 }
validates :website, url: true, length: { maximum: 2_000 }, if: :website? validates :website, url: true, length: { maximum: 2_000 }, if: :website?
validates :redirect_uri, length: { maximum: 2_000 } validates :redirect_uri, length: { maximum: 2_000 }
# The relationship used between Applications and AccessTokens is using
# dependent: delete_all, which means the ActiveRecord callback in
# AccessTokenExtension is not run, so instead we manually announce to
# streaming that these tokens are being deleted.
before_destroy :push_to_streaming_api, prepend: true
end end
def confirmation_redirect_uri def confirmation_redirect_uri
redirect_uri.lines.first.strip redirect_uri.lines.first.strip
end end
def push_to_streaming_api
# TODO: #28793 Combine into a single topic
payload = Oj.dump(event: :kill)
access_tokens.in_batches do |tokens|
redis.pipelined do |pipeline|
tokens.ids.each do |id|
pipeline.publish("timeline:access_token:#{id}", payload)
end
end
end
end
end end

@ -32,6 +32,7 @@ class StatusCacheHydrator
payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: @status.id) payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: @status.id)
payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: @status.id) if @status.account_id == account_id payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: @status.id) if @status.account_id == account_id
payload[:filtered] = mapped_applied_custom_filter(account_id, @status) payload[:filtered] = mapped_applied_custom_filter(account_id, @status)
payload[:reactions] = serialized_reactions(account_id)
if payload[:poll] if payload[:poll]
payload[:poll][:voted] = @status.account_id == account_id payload[:poll][:voted] = @status.account_id == account_id
@ -57,6 +58,7 @@ class StatusCacheHydrator
payload[:reblog][:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: @status.reblog_of_id) payload[:reblog][:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: @status.reblog_of_id)
payload[:reblog][:pinned] = StatusPin.exists?(account_id: account_id, status_id: @status.reblog_of_id) if @status.reblog.account_id == account_id payload[:reblog][:pinned] = StatusPin.exists?(account_id: account_id, status_id: @status.reblog_of_id) if @status.reblog.account_id == account_id
payload[:reblog][:filtered] = payload[:filtered] payload[:reblog][:filtered] = payload[:filtered]
payload[:reblog][:reactions] = serialized_reactions(account_id)
if payload[:reblog][:poll] if payload[:reblog][:poll]
if @status.reblog.account_id == account_id if @status.reblog.account_id == account_id
@ -71,6 +73,7 @@ class StatusCacheHydrator
payload[:favourited] = payload[:reblog][:favourited] payload[:favourited] = payload[:reblog][:favourited]
payload[:reblogged] = payload[:reblog][:reblogged] payload[:reblogged] = payload[:reblog][:reblogged]
payload[:reactions] = payload[:reblog][:reactions]
end end
end end
@ -87,6 +90,16 @@ class StatusCacheHydrator
).as_json ).as_json
end end
def serialized_reactions(account_id)
reactions = @status.reactions(account_id)
ActiveModelSerializers::SerializableResource.new(
reactions,
each_serializer: REST::ReactionSerializer,
scope: account_id, # terrible
scope_name: :current_user
).as_json
end
def payload_application def payload_application
@status.application.present? ? serialized_status_application_json : nil @status.application.present? ? serialized_status_application_json : nil
end end

@ -25,7 +25,15 @@ module User::LdapAuthenticable
resource = joins(:account).find_by(accounts: { username: safe_username }) resource = joins(:account).find_by(accounts: { username: safe_username })
if resource.blank? if resource.blank?
resource = new(email: attributes[Devise.ldap_mail.to_sym].first, agreement: true, account_attributes: { username: safe_username }, admin: false, external: true, confirmed_at: Time.now.utc) resource = new(
email: attributes[Devise.ldap_mail.to_sym].first,
agreement: true,
account_attributes: {
username: safe_username,
},
external: true,
confirmed_at: Time.now.utc
)
resource.save! resource.save!
end end

@ -19,17 +19,18 @@ module User::Omniauthable
end end
class_methods do class_methods do
def find_for_oauth(auth, signed_in_resource = nil) def find_for_omniauth(auth, signed_in_resource = nil)
# EOLE-SSO Patch # EOLE-SSO Patch
auth.uid = (auth.uid[0][:uid] || auth.uid[0][:user]) if auth.uid.is_a? Hashie::Array auth.uid = (auth.uid[0][:uid] || auth.uid[0][:user]) if auth.uid.is_a? Hashie::Array
identity = Identity.find_for_oauth(auth) identity = Identity.find_for_omniauth(auth)
# If a signed_in_resource is provided it always overrides the existing user # If a signed_in_resource is provided it always overrides the existing user
# to prevent the identity being locked with accidentally created accounts. # to prevent the identity being locked with accidentally created accounts.
# Note that this may leave zombie accounts (with no associated identity) which # Note that this may leave zombie accounts (with no associated identity) which
# can be cleaned up at a later date. # can be cleaned up at a later date.
user = signed_in_resource || identity.user user = signed_in_resource || identity.user
user ||= create_for_oauth(auth) user ||= reattach_for_auth(auth)
user ||= create_for_auth(auth)
if identity.user.nil? if identity.user.nil?
identity.user = user identity.user = user
@ -39,19 +40,35 @@ module User::Omniauthable
user user
end end
def create_for_oauth(auth) private
# Check if the user exists with provided email. If no email was provided,
# we assign a temporary email and ask the user to verify it on
# the next step via Auth::SetupController.show
strategy = Devise.omniauth_configs[auth.provider.to_sym].strategy def reattach_for_auth(auth)
assume_verified = strategy&.security&.assume_email_is_verified # If allowed, check if a user exists with the provided email address,
email_is_verified = auth.info.verified || auth.info.verified_email || auth.info.email_verified || assume_verified # and return it if they does not have an associated identity with the
email = auth.info.verified_email || auth.info.email # current authentication provider.
# This can be used to provide a choice of alternative auth providers
# or provide smooth gradual transition between multiple auth providers,
# but this is discouraged because any insecure provider will put *all*
# local users at risk, regardless of which provider they registered with.
return unless ENV['ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH'] == 'true'
user = User.find_by(email: email) if email_is_verified email, email_is_verified = email_from_auth(auth)
return unless email_is_verified
return user unless user.nil? user = User.find_by(email: email)
return if user.nil? || Identity.exists?(provider: auth.provider, user_id: user.id)
user
end
def create_for_auth(auth)
# Create a user for the given auth params. If no email was provided,
# we assign a temporary email and ask the user to verify it on
# the next step via Auth::SetupController.show
email, email_is_verified = email_from_auth(auth)
user = User.new(user_params_from_auth(email, auth)) user = User.new(user_params_from_auth(email, auth))
@ -66,7 +83,14 @@ module User::Omniauthable
user user
end end
private def email_from_auth(auth)
strategy = Devise.omniauth_configs[auth.provider.to_sym].strategy
assume_verified = strategy&.security&.assume_email_is_verified
email_is_verified = auth.info.verified || auth.info.verified_email || auth.info.email_verified || assume_verified
email = auth.info.verified_email || auth.info.email
[email, email_is_verified]
end
def user_params_from_auth(email, auth) def user_params_from_auth(email, auth)
{ {

@ -32,7 +32,6 @@ module User::PamAuthenticable
self.email = "#{account.username}@#{find_pam_suffix}" if email.nil? && find_pam_suffix self.email = "#{account.username}@#{find_pam_suffix}" if email.nil? && find_pam_suffix
self.confirmed_at = Time.now.utc self.confirmed_at = Time.now.utc
self.admin = false
self.account = account self.account = account
self.external = true self.external = true

@ -17,7 +17,7 @@ class Identity < ApplicationRecord
validates :uid, presence: true, uniqueness: { scope: :provider } validates :uid, presence: true, uniqueness: { scope: :provider }
validates :provider, presence: true validates :provider, presence: true
def self.find_for_oauth(auth) def self.find_for_omniauth(auth)
find_or_create_by(uid: auth.uid, provider: auth.provider) find_or_create_by(uid: auth.uid, provider: auth.provider)
end end
end end

@ -283,10 +283,10 @@ class Status < ApplicationRecord
@emojis = CustomEmoji.from_text(fields.join(' '), account.domain) @emojis = CustomEmoji.from_text(fields.join(' '), account.domain)
end end
def reactions(account = nil) def reactions(account_id = nil)
grouped_ordered_status_reactions.select( grouped_ordered_status_reactions.select(
[:name, :custom_emoji_id, 'COUNT(*) as count'].tap do |values| [:name, :custom_emoji_id, 'COUNT(*) as count'].tap do |values|
values << value_for_reaction_me_column(account) values << value_for_reaction_me_column(account_id)
end end
).to_a.tap do |records| ).to_a.tap do |records|
ActiveRecord::Associations::Preloader.new(records: records, associations: :custom_emoji).call ActiveRecord::Associations::Preloader.new(records: records, associations: :custom_emoji).call
@ -488,15 +488,15 @@ class Status < ApplicationRecord
) )
end end
def value_for_reaction_me_column(account) def value_for_reaction_me_column(account_id)
if account.nil? if account_id.nil?
'FALSE AS me' 'FALSE AS me'
else else
<<~SQL.squish <<~SQL.squish
EXISTS( EXISTS(
SELECT 1 SELECT 1
FROM status_reactions inner_reactions FROM status_reactions inner_reactions
WHERE inner_reactions.account_id = #{account.id} WHERE inner_reactions.account_id = #{account_id}
AND inner_reactions.status_id = status_reactions.status_id AND inner_reactions.status_id = status_reactions.status_id
AND inner_reactions.name = status_reactions.name AND inner_reactions.name = status_reactions.name
AND ( AND (

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class TagFeed < PublicFeed class TagFeed < PublicFeed
LIMIT_PER_MODE = 4 LIMIT_PER_MODE = (ENV['MAX_FEED_HASHTAGS'] || 4).to_i
# @param [Tag] tag # @param [Tag] tag
# @param [Account] account # @param [Account] account

@ -51,6 +51,8 @@ class User < ApplicationRecord
last_sign_in_ip last_sign_in_ip
skip_sign_in_token skip_sign_in_token
filtered_languages filtered_languages
admin
moderator
) )
include LanguagesHelper include LanguagesHelper
@ -342,6 +344,16 @@ class User < ApplicationRecord
Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch| Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch|
batch.update_all(revoked_at: Time.now.utc) batch.update_all(revoked_at: Time.now.utc)
Web::PushSubscription.where(access_token_id: batch).delete_all Web::PushSubscription.where(access_token_id: batch).delete_all
# Revoke each access token for the Streaming API, since `update_all``
# doesn't trigger ActiveRecord Callbacks:
# TODO: #28793 Combine into a single topic
payload = Oj.dump(event: :kill)
redis.pipelined do |pipeline|
batch.ids.each do |id|
pipeline.publish("timeline:access_token:#{id}", payload)
end
end
end end
end end

@ -5,7 +5,7 @@ class InitialStateSerializer < ActiveModel::Serializer
attributes :meta, :compose, :accounts, attributes :meta, :compose, :accounts,
:media_attachments, :settings, :media_attachments, :settings,
:max_toot_chars, :poll_limits, :max_toot_chars, :max_feed_hashtags, :poll_limits,
:languages, :max_reactions :languages, :max_reactions
attribute :critical_updates_pending, if: -> { object&.role&.can?(:view_devops) && SoftwareUpdate.check_enabled? } attribute :critical_updates_pending, if: -> { object&.role&.can?(:view_devops) && SoftwareUpdate.check_enabled? }
@ -21,6 +21,10 @@ class InitialStateSerializer < ActiveModel::Serializer
StatusReactionValidator::LIMIT StatusReactionValidator::LIMIT
end end
def max_feed_hashtags
TagFeed::LIMIT_PER_MODE
end
def poll_limits def poll_limits
{ {
min_options: PollValidator::MIN_OPTIONS, min_options: PollValidator::MIN_OPTIONS,
@ -34,8 +38,8 @@ class InitialStateSerializer < ActiveModel::Serializer
def meta def meta
store = default_meta_store store = default_meta_store
if object.current_account if object_account
store[:me] = object.current_account.id.to_s store[:me] = object_account.id.to_s
store[:unfollow_modal] = object_account_user.setting_unfollow_modal store[:unfollow_modal] = object_account_user.setting_unfollow_modal
store[:boost_modal] = object_account_user.setting_boost_modal store[:boost_modal] = object_account_user.setting_boost_modal
store[:favourite_modal] = object_account_user.setting_favourite_modal store[:favourite_modal] = object_account_user.setting_favourite_modal
@ -135,6 +139,10 @@ class InitialStateSerializer < ActiveModel::Serializer
} }
end end
def object_account
object.current_account
end
def object_account_user def object_account_user
object.current_account.user object.current_account.user
end end

@ -158,7 +158,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
end end
def reactions def reactions
object.reactions(current_user&.account) object.reactions(current_user&.account&.id)
end end
private private

@ -147,6 +147,7 @@ class DeleteAccountService < BaseService
purge_polls! purge_polls!
purge_generated_notifications! purge_generated_notifications!
purge_favourites! purge_favourites!
purge_status_reactions!
purge_bookmarks! purge_bookmarks!
purge_feeds! purge_feeds!
purge_other_associations! purge_other_associations!
@ -193,6 +194,15 @@ class DeleteAccountService < BaseService
end end
end end
def purge_status_reactions!
@account.status_reactions.in_batches do |status_reactions|
ids = status_reactions.pluck(:status_id)
Chewy.strategy.current.update(StatusesIndex, ids) if Chewy.enabled?
Rails.cache.delete_multi(ids.map { |id| "statuses/#{id}" })
status_reactions.delete_all
end
end
def purge_bookmarks! def purge_bookmarks!
@account.bookmarks.in_batches do |bookmarks| @account.bookmarks.in_batches do |bookmarks|
Chewy.strategy.current.update(StatusesIndex, bookmarks.pluck(:status_id)) if Chewy.enabled? Chewy.strategy.current.update(StatusesIndex, bookmarks.pluck(:status_id)) if Chewy.enabled?

@ -44,7 +44,7 @@ class FetchResourceService < BaseService
@response_code = response.code @response_code = response.code
return nil if response.code != 200 return nil if response.code != 200
if ['application/activity+json', 'application/ld+json'].include?(response.mime_type) if valid_activitypub_content_type?(response)
body = response.body_with_limit body = response.body_with_limit
json = body_to_json(body) json = body_to_json(body)

@ -8,9 +8,7 @@
%td.email-inner-card-td.email-prose %td.email-inner-card-td.email-prose
%p= t @resource.approved? ? 'devise.mailer.confirmation_instructions.explanation' : 'devise.mailer.confirmation_instructions.explanation_when_pending', host: site_hostname %p= t @resource.approved? ? 'devise.mailer.confirmation_instructions.explanation' : 'devise.mailer.confirmation_instructions.explanation_when_pending', host: site_hostname
- if @resource.created_by_application - if @resource.created_by_application
= render 'application/mailer/button', text: t('settings.account_settings'), url: edit_user_registration_url = render 'application/mailer/button', text: t('devise.mailer.confirmation_instructions.action_with_app', app: @resource.created_by_application.name), url: confirmation_url(@resource, confirmation_token: @token, redirect_to_app: 'true')
= link_to confirmation_url(@resource, confirmation_token: @token, redirect_to_app: 'true') do
%span= t 'devise.mailer.confirmation_instructions.action_with_app', app: @resource.created_by_application.name
- else - else
= render 'application/mailer/button', text: t('devise.mailer.confirmation_instructions.action'), url: confirmation_url(@resource, confirmation_token: @token) = render 'application/mailer/button', text: t('devise.mailer.confirmation_instructions.action'), url: confirmation_url(@resource, confirmation_token: @token)
%p= t 'devise.mailer.confirmation_instructions.extra_html', terms_path: about_more_url, policy_path: privacy_policy_url %p= t 'devise.mailer.confirmation_instructions.extra_html', terms_path: about_more_url, policy_path: privacy_policy_url

@ -21,9 +21,14 @@ Doorkeeper.configure do
user unless user&.otp_required_for_login? user unless user&.otp_required_for_login?
end end
# If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. # Doorkeeper provides some administrative interfaces for managing OAuth
# Applications, allowing creation, edit, and deletion of applications from the
# server. At present, these administrative routes are not integrated into
# Mastodon, and as such, we've disabled them by always return a 403 forbidden
# response for them. This does not affect the ability for users to manage
# their own OAuth Applications.
admin_authenticator do admin_authenticator do
current_user&.admin? || redirect_to(new_user_session_url) head 403
end end
# Authorization Code expiration time (default 10 minutes). # Authorization Code expiration time (default 10 minutes).

@ -26,6 +26,7 @@ Sidekiq.configure_server do |config|
'queue' => 'scheduler', 'queue' => 'scheduler',
}, },
} }
SidekiqScheduler::Scheduler.instance.reload_schedule!
end end
end end

@ -12,6 +12,7 @@ en:
last_attempt: You have one more attempt before your account is locked. last_attempt: You have one more attempt before your account is locked.
locked: Your account is locked. locked: Your account is locked.
not_found_in_database: Invalid %{authentication_keys} or password. not_found_in_database: Invalid %{authentication_keys} or password.
omniauth_user_creation_failure: Error creating an account for this identity.
pending: Your account is still under review. pending: Your account is still under review.
timeout: Your session expired. Please login again to continue. timeout: Your session expired. Please login again to continue.
unauthenticated: You need to login or sign up before continuing. unauthenticated: You need to login or sign up before continuing.

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'sidekiq_unique_jobs/web' require 'sidekiq_unique_jobs/web' if ENV['ENABLE_SIDEKIQ_UNIQUE_JOBS_UI'] == true
require 'sidekiq-scheduler/web' require 'sidekiq-scheduler/web'
class RedirectWithVary < ActionDispatch::Routing::PathRedirect class RedirectWithVary < ActionDispatch::Routing::PathRedirect

@ -56,7 +56,7 @@ services:
web: web:
build: . build: .
image: ghcr.io/mastodon/mastodon:v4.2.0 image: ghcr.io/mastodon/mastodon:v4.2.7
restart: always restart: always
env_file: .env.production env_file: .env.production
command: bundle exec puma -C config/puma.rb command: bundle exec puma -C config/puma.rb
@ -77,7 +77,7 @@ services:
streaming: streaming:
build: . build: .
image: ghcr.io/mastodon/mastodon:v4.2.0 image: ghcr.io/mastodon/mastodon:v4.2.7
restart: always restart: always
env_file: .env.production env_file: .env.production
command: node ./streaming command: node ./streaming
@ -95,7 +95,7 @@ services:
sidekiq: sidekiq:
build: . build: .
image: ghcr.io/mastodon/mastodon:v4.2.0 image: ghcr.io/mastodon/mastodon:v4.2.7
restart: always restart: always
env_file: .env.production env_file: .env.production
command: bundle exec sidekiq command: bundle exec sidekiq

@ -17,7 +17,7 @@ module Mastodon
end end
def default_prerelease def default_prerelease
'alpha.1' 'alpha.3'
end end
def prerelease def prerelease
@ -29,7 +29,7 @@ module Mastodon
end end
def catstodon_revision def catstodon_revision
'1.0.15' '1.0.0'
end end
def nyastodon_revision def nyastodon_revision

@ -0,0 +1,11 @@
# frozen_string_literal: true
namespace :sidekiq_unique_jobs do
task delete_all_locks: :environment do
digests = SidekiqUniqueJobs::Digests.new
digests.delete_by_pattern('*', count: digests.count)
expiring_digests = SidekiqUniqueJobs::ExpiringDigests.new
expiring_digests.delete_by_pattern('*', count: expiring_digests.count)
end
end

@ -56,15 +56,15 @@ describe JsonLdHelper do
describe '#fetch_resource' do describe '#fetch_resource' do
context 'when the second argument is false' do context 'when the second argument is false' do
it 'returns resource even if the retrieved ID and the given URI does not match' do it 'returns resource even if the retrieved ID and the given URI does not match' do
stub_request(:get, 'https://bob.test/').to_return body: '{"id": "https://alice.test/"}' stub_request(:get, 'https://bob.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://alice.test/').to_return body: '{"id": "https://alice.test/"}' stub_request(:get, 'https://alice.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' })
expect(fetch_resource('https://bob.test/', false)).to eq({ 'id' => 'https://alice.test/' }) expect(fetch_resource('https://bob.test/', false)).to eq({ 'id' => 'https://alice.test/' })
end end
it 'returns nil if the object identified by the given URI and the object identified by the retrieved ID does not match' do it 'returns nil if the object identified by the given URI and the object identified by the retrieved ID does not match' do
stub_request(:get, 'https://mallory.test/').to_return body: '{"id": "https://marvin.test/"}' stub_request(:get, 'https://mallory.test/').to_return(body: '{"id": "https://marvin.test/"}', headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://marvin.test/').to_return body: '{"id": "https://alice.test/"}' stub_request(:get, 'https://marvin.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' })
expect(fetch_resource('https://mallory.test/', false)).to be_nil expect(fetch_resource('https://mallory.test/', false)).to be_nil
end end
@ -72,7 +72,7 @@ describe JsonLdHelper do
context 'when the second argument is true' do context 'when the second argument is true' do
it 'returns nil if the retrieved ID and the given URI does not match' do it 'returns nil if the retrieved ID and the given URI does not match' do
stub_request(:get, 'https://mallory.test/').to_return body: '{"id": "https://alice.test/"}' stub_request(:get, 'https://mallory.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' })
expect(fetch_resource('https://mallory.test/', true)).to be_nil expect(fetch_resource('https://mallory.test/', true)).to be_nil
end end
end end
@ -80,12 +80,12 @@ describe JsonLdHelper do
describe '#fetch_resource_without_id_validation' do describe '#fetch_resource_without_id_validation' do
it 'returns nil if the status code is not 200' do it 'returns nil if the status code is not 200' do
stub_request(:get, 'https://host.test/').to_return status: 400, body: '{}' stub_request(:get, 'https://host.test/').to_return(status: 400, body: '{}', headers: { 'Content-Type': 'application/activity+json' })
expect(fetch_resource_without_id_validation('https://host.test/')).to be_nil expect(fetch_resource_without_id_validation('https://host.test/')).to be_nil
end end
it 'returns hash' do it 'returns hash' do
stub_request(:get, 'https://host.test/').to_return status: 200, body: '{}' stub_request(:get, 'https://host.test/').to_return(status: 200, body: '{}', headers: { 'Content-Type': 'application/activity+json' })
expect(fetch_resource_without_id_validation('https://host.test/')).to eq({}) expect(fetch_resource_without_id_validation('https://host.test/')).to eq({})
end end
end end

@ -35,7 +35,7 @@ RSpec.describe ActivityPub::Activity::Announce do
context 'when sender is followed by a local account' do context 'when sender is followed by a local account' do
before do before do
Fabricate(:account).follow!(sender) Fabricate(:account).follow!(sender)
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json)) stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' })
subject.perform subject.perform
end end
@ -120,7 +120,7 @@ RSpec.describe ActivityPub::Activity::Announce do
let(:object_json) { 'https://example.com/actor/hello-world' } let(:object_json) { 'https://example.com/actor/hello-world' }
before do before do
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json)) stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' })
end end
context 'when the relay is enabled' do context 'when the relay is enabled' do

@ -3,19 +3,19 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Identity do RSpec.describe Identity do
describe '.find_for_oauth' do describe '.find_for_omniauth' do
let(:auth) { Fabricate(:identity, user: Fabricate(:user)) } let(:auth) { Fabricate(:identity, user: Fabricate(:user)) }
it 'calls .find_or_create_by' do it 'calls .find_or_create_by' do
allow(described_class).to receive(:find_or_create_by) allow(described_class).to receive(:find_or_create_by)
described_class.find_for_oauth(auth) described_class.find_for_omniauth(auth)
expect(described_class).to have_received(:find_or_create_by).with(uid: auth.uid, provider: auth.provider) expect(described_class).to have_received(:find_or_create_by).with(uid: auth.uid, provider: auth.provider)
end end
it 'returns an instance of Identity' do it 'returns an instance of Identity' do
expect(described_class.find_for_oauth(auth)).to be_instance_of described_class expect(described_class.find_for_omniauth(auth)).to be_instance_of described_class
end end
end end
end end

@ -438,7 +438,10 @@ RSpec.describe User do
let!(:access_token) { Fabricate(:access_token, resource_owner_id: user.id) } let!(:access_token) { Fabricate(:access_token, resource_owner_id: user.id) }
let!(:web_push_subscription) { Fabricate(:web_push_subscription, access_token: access_token) } let!(:web_push_subscription) { Fabricate(:web_push_subscription, access_token: access_token) }
let(:redis_pipeline_stub) { instance_double(Redis::Namespace, publish: nil) }
before do before do
allow(redis).to receive(:pipelined).and_yield(redis_pipeline_stub)
user.reset_password! user.reset_password!
end end
@ -455,6 +458,10 @@ RSpec.describe User do
expect(Doorkeeper::AccessToken.active_for(user).count).to eq 0 expect(Doorkeeper::AccessToken.active_for(user).count).to eq 0
end end
it 'revokes streaming access for all access tokens' do
expect(redis_pipeline_stub).to have_received(:publish).with("timeline:access_token:#{access_token.id}", Oj.dump(event: :kill)).once
end
it 'removes push subscriptions' do it 'removes push subscriptions' do
expect(Web::PushSubscription.where(user: user).or(Web::PushSubscription.where(access_token: access_token)).count).to eq 0 expect(Web::PushSubscription.where(user: user).or(Web::PushSubscription.where(access_token: access_token)).count).to eq 0
expect { web_push_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound) expect { web_push_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound)

@ -0,0 +1,83 @@
# frozen_string_literal: true
require 'rails_helper'
describe 'Disabled OAuth routes' do
# These routes are disabled via the doorkeeper configuration for
# `admin_authenticator`, as these routes should only be accessible by server
# administrators. For now, these routes are not properly designed and
# integrated into Mastodon, so we're disabling them completely
describe 'GET /oauth/applications' do
it 'returns 403 forbidden' do
get oauth_applications_path
expect(response).to have_http_status(403)
end
end
describe 'POST /oauth/applications' do
it 'returns 403 forbidden' do
post oauth_applications_path
expect(response).to have_http_status(403)
end
end
describe 'GET /oauth/applications/new' do
it 'returns 403 forbidden' do
get new_oauth_application_path
expect(response).to have_http_status(403)
end
end
describe 'GET /oauth/applications/:id' do
let(:application) { Fabricate(:application, scopes: 'read') }
it 'returns 403 forbidden' do
get oauth_application_path(application)
expect(response).to have_http_status(403)
end
end
describe 'PATCH /oauth/applications/:id' do
let(:application) { Fabricate(:application, scopes: 'read') }
it 'returns 403 forbidden' do
patch oauth_application_path(application)
expect(response).to have_http_status(403)
end
end
describe 'PUT /oauth/applications/:id' do
let(:application) { Fabricate(:application, scopes: 'read') }
it 'returns 403 forbidden' do
put oauth_application_path(application)
expect(response).to have_http_status(403)
end
end
describe 'DELETE /oauth/applications/:id' do
let(:application) { Fabricate(:application, scopes: 'read') }
it 'returns 403 forbidden' do
delete oauth_application_path(application)
expect(response).to have_http_status(403)
end
end
describe 'GET /oauth/applications/:id/edit' do
let(:application) { Fabricate(:application, scopes: 'read') }
it 'returns 403 forbidden' do
get edit_oauth_application_path(application)
expect(response).to have_http_status(403)
end
end
end

@ -39,16 +39,35 @@ describe 'OmniAuth callbacks' do
Fabricate(:user, email: 'user@host.example') Fabricate(:user, email: 'user@host.example')
end end
it 'matches the existing user, creates an identity, and redirects to root path' do context 'when ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH is set to true' do
expect { subject } around do |example|
.to not_change(User, :count) ClimateControl.modify ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH: 'true' do
.and change(Identity, :count) example.run
.by(1) end
.and change(LoginActivity, :count) end
.by(1)
it 'matches the existing user, creates an identity, and redirects to root path' do
expect { subject }
.to not_change(User, :count)
.and change(Identity, :count)
.by(1)
.and change(LoginActivity, :count)
.by(1)
expect(Identity.find_by(user: User.last).uid).to eq('123')
expect(response).to redirect_to(root_path)
end
end
expect(Identity.find_by(user: User.last).uid).to eq('123') context 'when ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH is not set to true' do
expect(response).to redirect_to(root_path) it 'does not match the existing user or create an identity, and redirects to login page' do
expect { subject }
.to not_change(User, :count)
.and not_change(Identity, :count)
.and not_change(LoginActivity, :count)
expect(response).to redirect_to(new_user_session_url)
end
end end
end end
@ -96,7 +115,7 @@ describe 'OmniAuth callbacks' do
context 'when a user cannot be built' do context 'when a user cannot be built' do
before do before do
allow(User).to receive(:find_for_oauth).and_return(User.new) allow(User).to receive(:find_for_omniauth).and_return(User.new)
end end
it 'redirects to the new user signup page' do it 'redirects to the new user signup page' do

@ -72,11 +72,11 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
shared_examples 'sets pinned posts' do shared_examples 'sets pinned posts' do
before do before do
stub_request(:get, 'https://example.com/account/pinned/known').to_return(status: 200, body: Oj.dump(status_json_pinned_known)) stub_request(:get, 'https://example.com/account/pinned/known').to_return(status: 200, body: Oj.dump(status_json_pinned_known), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://example.com/account/pinned/unknown-inlined').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_inlined)) stub_request(:get, 'https://example.com/account/pinned/unknown-inlined').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_inlined), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://example.com/account/pinned/unknown-unreachable').to_return(status: 404) stub_request(:get, 'https://example.com/account/pinned/unknown-unreachable').to_return(status: 404)
stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable)) stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://example.com/account/collections/featured').to_return(status: 200, body: Oj.dump(featured_with_null)) stub_request(:get, 'https://example.com/account/collections/featured').to_return(status: 200, body: Oj.dump(featured_with_null), headers: { 'Content-Type': 'application/activity+json' })
subject.call(actor, note: true, hashtag: false) subject.call(actor, note: true, hashtag: false)
end end
@ -94,7 +94,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
describe '#call' do describe '#call' do
context 'when the endpoint is a Collection' do context 'when the endpoint is a Collection' do
before do before do
stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload)) stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
end end
it_behaves_like 'sets pinned posts' it_behaves_like 'sets pinned posts'
@ -111,7 +111,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
end end
before do before do
stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload)) stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
end end
it_behaves_like 'sets pinned posts' it_behaves_like 'sets pinned posts'
@ -120,7 +120,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
let(:items) { 'https://example.com/account/pinned/unknown-reachable' } let(:items) { 'https://example.com/account/pinned/unknown-reachable' }
before do before do
stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable)) stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' })
subject.call(actor, note: true, hashtag: false) subject.call(actor, note: true, hashtag: false)
end end
@ -147,7 +147,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
end end
before do before do
stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload)) stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
end end
it_behaves_like 'sets pinned posts' it_behaves_like 'sets pinned posts'
@ -156,7 +156,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
let(:items) { 'https://example.com/account/pinned/unknown-reachable' } let(:items) { 'https://example.com/account/pinned/unknown-reachable' }
before do before do
stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable)) stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' })
subject.call(actor, note: true, hashtag: false) subject.call(actor, note: true, hashtag: false)
end end

@ -38,7 +38,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService, type: :service d
describe '#call' do describe '#call' do
context 'when the endpoint is a Collection' do context 'when the endpoint is a Collection' do
before do before do
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload)) stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
end end
it_behaves_like 'sets featured tags' it_behaves_like 'sets featured tags'
@ -46,7 +46,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService, type: :service d
context 'when the account already has featured tags' do context 'when the account already has featured tags' do
before do before do
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload)) stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
actor.featured_tags.create!(name: 'FoO') actor.featured_tags.create!(name: 'FoO')
actor.featured_tags.create!(name: 'baz') actor.featured_tags.create!(name: 'baz')
@ -67,7 +67,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService, type: :service d
end end
before do before do
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload)) stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
end end
it_behaves_like 'sets featured tags' it_behaves_like 'sets featured tags'
@ -88,7 +88,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService, type: :service d
end end
before do before do
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload)) stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
end end
it_behaves_like 'sets featured tags' it_behaves_like 'sets featured tags'

@ -44,7 +44,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
before do before do
actor[:inbox] = nil actor[:inbox] = nil
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end end
@ -67,7 +67,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
before do before do
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end end
@ -93,7 +93,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
before do before do
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end end
@ -125,7 +125,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } } let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
before do before do
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end end
@ -148,7 +148,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } } let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
before do before do
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end end

@ -44,7 +44,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
before do before do
actor[:inbox] = nil actor[:inbox] = nil
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end end
@ -67,7 +67,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
before do before do
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end end
@ -93,7 +93,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } } let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
before do before do
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end end
@ -125,7 +125,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } } let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
before do before do
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end end
@ -148,7 +148,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } } let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
before do before do
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end end

@ -50,7 +50,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService, type: :service do
end end
before do before do
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end end
@ -59,7 +59,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService, type: :service do
context 'when the key is a sub-object from the actor' do context 'when the key is a sub-object from the actor' do
before do before do
stub_request(:get, public_key_id).to_return(body: Oj.dump(actor)) stub_request(:get, public_key_id).to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
end end
it 'returns the expected account' do it 'returns the expected account' do
@ -71,7 +71,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService, type: :service do
let(:public_key_id) { 'https://example.com/alice-public-key.json' } let(:public_key_id) { 'https://example.com/alice-public-key.json' }
before do before do
stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] }))) stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] })), headers: { 'Content-Type': 'application/activity+json' })
end end
it 'returns the expected account' do it 'returns the expected account' do
@ -84,7 +84,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService, type: :service do
let(:actor_public_key) { 'https://example.com/alice-public-key.json' } let(:actor_public_key) { 'https://example.com/alice-public-key.json' }
before do before do
stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] }))) stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] })), headers: { 'Content-Type': 'application/activity+json' })
end end
it 'returns the nil' do it 'returns the nil' do

@ -58,7 +58,7 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do
context 'when passing the URL to the collection' do context 'when passing the URL to the collection' do
before do before do
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload)) stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
end end
it 'spawns workers for up to 5 replies on the same server' do it 'spawns workers for up to 5 replies on the same server' do
@ -93,7 +93,7 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do
context 'when passing the URL to the collection' do context 'when passing the URL to the collection' do
before do before do
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload)) stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
end end
it 'spawns workers for up to 5 replies on the same server' do it 'spawns workers for up to 5 replies on the same server' do
@ -132,7 +132,7 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do
context 'when passing the URL to the collection' do context 'when passing the URL to the collection' do
before do before do
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload)) stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
end end
it 'spawns workers for up to 5 replies on the same server' do it 'spawns workers for up to 5 replies on the same server' do

@ -60,7 +60,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService, type: :service do
describe '#call' do describe '#call' do
context 'when the endpoint is a Collection of actor URIs' do context 'when the endpoint is a Collection of actor URIs' do
before do before do
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload)) stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
end end
it_behaves_like 'synchronizes followers' it_behaves_like 'synchronizes followers'
@ -77,7 +77,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService, type: :service do
end end
before do before do
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload)) stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
end end
it_behaves_like 'synchronizes followers' it_behaves_like 'synchronizes followers'
@ -98,7 +98,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService, type: :service do
end end
before do before do
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload)) stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
end end
it_behaves_like 'synchronizes followers' it_behaves_like 'synchronizes followers'

@ -21,7 +21,7 @@ describe ActivityPub::FetchRepliesWorker do
describe 'perform' do describe 'perform' do
it 'performs a request if the collection URI is from the same host' do it 'performs a request if the collection URI is from the same host' do
stub_request(:get, 'https://example.com/statuses_replies/1').to_return(status: 200, body: json) stub_request(:get, 'https://example.com/statuses_replies/1').to_return(status: 200, body: json, headers: { 'Content-Type': 'application/activity+json' })
subject.perform(status.id, 'https://example.com/statuses_replies/1') subject.perform(status.id, 'https://example.com/statuses_replies/1')
expect(a_request(:get, 'https://example.com/statuses_replies/1')).to have_been_made.once expect(a_request(:get, 'https://example.com/statuses_replies/1')).to have_been_made.once
end end

Loading…
Cancel
Save