Compare commits

...

171 Commits

Author SHA1 Message Date
Jeremy Kescher a53f2cf970
Merge branch 'glitch-soc' into develop 2 months ago
Essem 327da54e23
Use a consumer for identity 2 months ago
Claire ada2ac411a
Merge pull request #2681 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to d702a03a0c
2 months ago
Claire c511e52d1e [Glitch] Add “Learn more” on block modal to inform of federation caveats
Port d702a03a0c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Eugen Rochko 80fda17868 [Glitch] Change mute, block and domain block confirmations in web UI
Port ec19d0a14b to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Claire 3beba00c4e [Glitch] Change Explore icon to compass in advanced interface
Port be7a68b095 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Claire b8f476256e Merge commit 'd702a03a0c35fc631a0fa456532946e6751cbbfd' into glitch-soc/merge-upstream 2 months ago
Renaud Chaput 7fe848b161 [Glitch] Convert `packs/public.jsx` to Typescript
Port c76ae7a5c0 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Claire 1b06e4e1b7 Merge commit 'c76ae7a5c0d247264afa896f081db9d1fd278711' into glitch-soc/merge-upstream
Conflicts:
- `app/javascript/packs/public.jsx`:
  In glitch-soc, this file was split across the following files:
  - `app/javascript/packs/public.jsx`
  - `app/javascript/core/embed.js`
  - `app/javascript/core/settings.js`
  Update all those files accordingly, as well as the related `theme.yml` files.
2 months ago
Claire 6ee7a64465
Reduce differences with upstream in “public” pack (#2680)
* Reduce differences with upstream

* Further reduce pack differences with upstream
2 months ago
Claire 2cdac2bc38
Merge pull request #2679 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 407287573c
2 months ago
Claire d702a03a0c
Add “Learn more” on block modal to inform of federation caveats (#29614) 2 months ago
Eugen Rochko a32f992182 [Glitch] Add domain information to profiles in web UI
Port 407287573c to glitch-soc

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Claire 2c622497c2 Merge commit '407287573c9c602c5490224c43639b39edf7d48a' into glitch-soc/merge-upstream 2 months ago
Eugen Rochko ec19d0a14b
Change mute, block and domain block confirmations in web UI (#29576) 2 months ago
Claire ed1af46108
Merge pull request #2677 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
2 months ago
Claire be7a68b095
Change Explore icon to compass in advanced interface (#29610) 2 months ago
Matt Jankowski 4f4132f1a1
Add diagnostic message for failure during CLI search deploy (#29462) 2 months ago
Renaud Chaput c76ae7a5c0
Convert `packs/public.jsx` to Typescript (#29501) 2 months ago
Eugen Rochko 407287573c
Add domain information to profiles in web UI (#29602)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Claire 2d4ff8f6ed Merge commit 'f445d33fd6aa492df319f4cc4efbd255d7a84f0e' into glitch-soc/merge-upstream
Conflicts:
- `app/views/admin/custom_emojis/new.html.haml`:
  Upstream split some `simple_form` arguments across multiple lines.
  One of the arguments in glitch-soc was different.
  Split as upstream's did, keeping our different limit argument.
- `app/views/admin/settings/appearance/show.html.haml`:
  Upstream split some `simple_form` arguments across multiple lines.
  Glitch-soc had a different field because of the different theming
  system.
  Kept glitch-soc's definition of that form field.
2 months ago
renovate[bot] 52200b6bbf
Update dependency sass to v1.72.0 (#29586)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2 months ago
Claire 2e49bc97b0 Merge commit '7720c684c5bf54e73e8815defe15473777d1c201' into glitch-soc/merge-upstream 2 months ago
Matt Jankowski f445d33fd6
Fix haml-lint `LineLength` cops in app/views/admin (#28680) 2 months ago
Matt Jankowski 7720c684c5
Move common module inclusion in sub classes to `ActivityPub::BaseController` (#29560) 2 months ago
Matt Jankowski 2e91a9bd34
Add `include_pagination_headers` matcher to check `Link` header in api specs (#29596) 2 months ago
github-actions[bot] 6865fda593
New Crowdin Translations (automated) (#29603)
Co-authored-by: GitHub Actions <noreply@github.com>
2 months ago
Matt Jankowski e75b55a6d7
Extract target account on list method in bulk import row service spec (#29601) 2 months ago
Matt Jankowski 838b0bdf2d
Remove unused `Account::Interactions#endorsed?` method (#29463) 2 months ago
Matt Jankowski d39d625561
Use inclusive range in `ActivityTracker#get` (#29413) 2 months ago
Jeremy Kescher 21a23b58ab
Merge branch 'glitch-soc' into develop 2 months ago
Claire b834f41ec1
Merge pull request #2676 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to df6086d402
2 months ago
Jeremy Kescher caccc2e0f4
Merge branch 'glitch-soc' into develop
# Conflicts:
#	README.md
#	app/javascript/flavours/glitch/components/status.jsx
#	app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx
#	app/javascript/flavours/glitch/locales/en.json
#	app/javascript/flavours/glitch/reducers/compose.js
#	app/javascript/flavours/glitch/styles/components.scss
#	app/javascript/mastodon/reducers/compose.js
#	app/models/notification.rb
2 months ago
Eugen Rochko 000a900d3b [Glitch] Fix back button appearing in column header unexpectedly in web UI
Port 30483d618f to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Claire 84c7b272e1 [Glitch] Fix accounts not getting imported into redux store for some filtered notification types
Port 95a5713ff7 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Eugen Rochko 4b2a935c4a [Glitch] Change design of metadata underneath posts in web UI
Port 4991198b70 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Eugen Rochko 042d17ddf1 [Glitch] Fix wrong background color on search results in web UI
Port 1e1d97a787 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
mogaminsk 88f477749c [Glitch] Use sender's `username` to column title in notification request if it's `display_name` is not set
Port 3156d04ec1 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Claire e91ede5be6 Merge commit 'df6086d4027910fb160d531b4fe7ffdec26b0cd7' into glitch-soc/merge-upstream 2 months ago
Claire 31b93a5441
Merge pull request #2674 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 24319836de
2 months ago
Matt Jankowski df6086d402
Extract file size sql calc from media storage cli (#29577) 2 months ago
Matt Jankowski 6c3e718b86
Remove setting of `sensitive` value (default false, not null) in Status model (#29589) 2 months ago
Matt Jankowski 974c7672e5
Extract shared behavior methods in oauth feature spec (#28360) 2 months ago
Matt Jankowski 14aa7f1e15
Use `Account.activitypub` generated scope (#28157) 2 months ago
Matt Jankowski 049d9171eb
Use `with_options` for shared option in `accounts#show` routing block (#28914) 2 months ago
Eugen Rochko 30483d618f
Fix back button appearing in column header unexpectedly in web UI (#29551) 2 months ago
Claire 95a5713ff7
Fix accounts not getting imported into redux store for some filtered notification types (#29588) 2 months ago
Matt Jankowski 0bc17a3d48
Use enum-generated `public_visibility` scope on Status (#28156) 2 months ago
Matt Jankowski 19cbadfbd6
Use enum-generated scope for `IpBlock` in CLI (#28144) 2 months ago
Matt Jankowski 681a89f684
Readability clean up in `ImportVacuum` spec (#28955) 2 months ago
Matt Jankowski 79e7590578
Clean up `activitypub` module route scope near instance actor (#29579) 2 months ago
Matt Jankowski 65e8349980
Clean up `activitypub` module route scope near collections/boxes (#29580) 2 months ago
Matt Jankowski 18ffd4d925
Add module `instances` route scope in api routes (#29581) 2 months ago
Matt Jankowski 01ecc80118
Add module `accounts` route scope in api routes (#29582) 2 months ago
Eugen Rochko 4991198b70
Change design of metadata underneath posts in web UI (#29585) 2 months ago
Matt Jankowski f9100743ec
Add `Api::ErrorHandling` concern for api/base controller (#29574) 2 months ago
Eugen Rochko 1e1d97a787
Fix wrong background color on search results in web UI (#29584) 2 months ago
github-actions[bot] 5aea35de13
New Crowdin Translations (automated) (#29587)
Co-authored-by: GitHub Actions <noreply@github.com>
2 months ago
Matt Jankowski 42875fee52
Add coverage for bad args/options in `CLI::Domains#purge` (#29578) 2 months ago
mogaminsk 3156d04ec1
Use sender's `username` to column title in notification request if it's `display_name` is not set (#29575) 2 months ago
Claire 369e728536 Fix collapsed posts background color 2 months ago
Claire e7b49181af [Glitch] Hide media by default in notification requests
Port a32a126cac to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Claire 65ca37bbaa Merge commit 'a32a126cac42c73236236b5a9bd660765b9c58ee' into glitch-soc/merge-upstream
Conflicts:
- `spec/lib/sanitize/config_spec.rb`:
  Conflict due to glitch-soc having factored the file differently.
  Ported upstream's changes.
2 months ago
Renaud Chaput 663dd49a85 [Glitch] Fix navigation panel icons missing classes
Port acf3f410ae to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Erik Uden e1b64151a2 [Glitch] Fix toggle button color for light (and dark/default) theme
Port 268856d5d9 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Claire 48134bcd10 Merge commit 'acf3f410aef3cfb9e8f5f73042526de9b2f96d13' into glitch-soc/merge-upstream 2 months ago
Claire 08b10cce52 Merge commit 'b43eaa4517107326c7e73b949cec759f841b4a30' into glitch-soc/merge-upstream
Conflicts:
- `spec/controllers/api/v1/accounts/credentials_controller_spec.rb`
  Conflict due to glitch-soc's different note length handling.
  Ported the changes in `spec/requests/api/v1/accounts/credentials_spec.rb` instead.
2 months ago
Matt Jankowski 00d72866a3 [Glitch] Use vanilla JS to get Rails CSRF values
Port 00d94f3ffa to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Claire 06881a8669 Merge commit '2c0441acd7f943a9873b650cf75d33c73d545acf' into glitch-soc/merge-upstream 2 months ago
Eugen Rochko 8c0673037a [Glitch] Change background color in web UI
Port 5b60d4b696

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Claire c6dbb33944 Merge commit 'd4ed7e466c41f19e5f9352700c76e7ffc4d28119' into glitch-soc/merge-upstream 2 months ago
Renaud Chaput 435c46b316 [Glitch] Fix i18n typo
Port af4e44e30a to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Claire 27ffc09847 Merge commit 'af4e44e30a6a2701102a7d573e47e9db42025821' into glitch-soc/merge-upstream 2 months ago
Eugen Rochko b36e96ec90 [Glitch] Change action button to be last on profiles in web UI
Port 19efa1b9f1 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Eugen Rochko 13c9524436 [Glitch] Add notification policies and notification requests in web UI
Port c10bbf5fe3 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Claire a32a126cac
Hide media by default in notification requests (#29572) 2 months ago
Matt Jankowski 71e5f0f48c
Add coverage for suspended instance actor scenario (#29571) 2 months ago
Matt Jankowski 6262ceeb70
Fix `RSpec/DescribedClass` cop (#29472) 2 months ago
Matt Jankowski c09b8a7164
Add `Account.without_internal` scope (#29559)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Renaud Chaput acf3f410ae
Fix navigation panel icons missing classes (#29569) 2 months ago
github-actions[bot] 171948b910
New Crowdin Translations (automated) (#29563)
Co-authored-by: GitHub Actions <noreply@github.com>
2 months ago
Erik Uden 268856d5d9
Fix toggle button color for light (and dark/default) theme (#29553) 2 months ago
Claire b43eaa4517
Refactor notification filtering behavior definition (#29567) 2 months ago
Claire 27fd084cb5
Exempt some notification types from notification filtering (#29565) 2 months ago
Matt Jankowski 46e902f1f3
Merge `api/v1/accounts/credentials` controller spec into existing request spec (#29006) 2 months ago
Matt Jankowski 2c0441acd7
Use rails built-in `tag` methods in `TextFormatter.shortened_link` (#28976) 2 months ago
Matt Jankowski 7e6eb64f1e
Use full snowflake range in `admin/metrics` classes (#29416) 2 months ago
Matt Jankowski 9754967d5f
Move `pagination_max_id` and `pagination_since_id` into api/base controller (#28844) 2 months ago
Matt Jankowski 01b624c4a0
Use `normalizes` on `CustomFilter#context` value (#27602) 2 months ago
Matt Jankowski 71eecbfa1f
Move `api/v2/filters/*` to request spec (#28956) 2 months ago
Matt Jankowski 8349b45d60
Accept extra args that we wont verify in `ap/activity/add_spec` (#29005) 2 months ago
Matt Jankowski 469028b6d3
Remove unneeded `type: :service` from spec/services files (#29304) 2 months ago
Matt Jankowski 3eaac3af73
Use `before_all` block to setup `requests/cache_spec` data (#29437) 2 months ago
Matt Jankowski 19f0590795
Add basic coverage for `TagSearchService` class (#29319) 2 months ago
Matt Jankowski 96013cd576
Reduce `RSpec/ExampleLength` in CSP request spec (#29104) 2 months ago
Matt Jankowski 00d94f3ffa
Use vanilla JS to get Rails CSRF values (#29403) 2 months ago
Matt Jankowski d4ed7e466c
Extract `by_domain_length` scope in `DomainNormalizable` concern (#29517) 2 months ago
Eugen Rochko 5b60d4b696
Change background color in web UI (#29522) 2 months ago
Renaud Chaput af4e44e30a
Fix i18n typo (#29557) 2 months ago
cuithon 0ba9f58e17
chore: fix some typos (#29555)
Signed-off-by: cuithon <dscs@outlook.com>
2 months ago
github-actions[bot] dc36b961aa
New Crowdin Translations (automated) (#29554)
Co-authored-by: GitHub Actions <noreply@github.com>
2 months ago
Matt Jankowski 216cea1e27
Fix incorrect frequency value in `FriendsOfFriendsSource` data (#29550) 2 months ago
Eugen Rochko 3631ddbfc9 [Glitch] Change icons in navigation panel to be filled when active in web UI
Port 16c856729b

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Renaud Chaput 6440651976 [Glitch] Use the server setting to get the max number of poll options in UI
Port b9722dfe2b to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Eugen Rochko efbc8cba17 [Glitch] Change dropdown menu icon to not be replaced by close icon when open in web UI
Port 2347ea813e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Eugen Rochko 777510a696 [Glitch] Add hints for rules
Port 5b3a8737d6 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Claire a5127d0ef8 Merge commit '24319836de6046fb2985ec1a24c30ad7d47584d7' into glitch-soc/merge-upstream
Conflicts:
- `config/routes/api.rb`:
  glitch-soc has an extra `:destroy` action on notifications for historical reasons.
  Kept it for now, while otherwise updating as upstream did.
2 months ago
Matt Jankowski 24319836de
Convert request-based setup into factory setup in push/subscriptions request spec (#29489) 2 months ago
Matt Jankowski a38e424185
Use unchanging github links in docs/comments (#29545) 2 months ago
Eugen Rochko c10bbf5fe3
Add notification policies and notification requests in web UI (#29433) 2 months ago
Eugen Rochko 19efa1b9f1
Change action button to be last on profiles in web UI (#29533) 2 months ago
Eugen Rochko 16c856729b
Change icons in navigation panel to be filled when active in web UI (#29537) 2 months ago
Jeong Arm 4a6ddbc9c0
Normalize idna domain before account unblock domain (#29530) 2 months ago
Matt Jankowski a7284690fc
Add coverage for admin/metrics base classes, simplify subclass generation (#29527) 2 months ago
Renaud Chaput b9722dfe2b
Use the server setting to get the max number of poll options in UI (#29490) 2 months ago
renovate[bot] 6984f94044
Update dependency autoprefixer to v10.4.18 (#29475)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2 months ago
Eugen Rochko 2347ea813e
Change dropdown menu icon to not be replaced by close icon when open in web UI (#29532) 2 months ago
Eugen Rochko 5b3a8737d6
Add hints for rules (#29539) 2 months ago
Matt Jankowski 98ef38e34e
Ensure unique values in fabricators (#29515) 2 months ago
renovate[bot] 6f8ec6d7f8
Update dependency test-prof to v1.3.2 (#29528)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2 months ago
renovate[bot] 7b89d6842e
Update DefinitelyTyped types (non-major) (#29543)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2 months ago
renovate[bot] df3798a6fa
Update dependency eslint-plugin-jsdoc to v48.2.1 (#29544)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2 months ago
github-actions[bot] f85168b189
New Crowdin Translations (automated) (#29467)
Co-authored-by: GitHub Actions <noreply@github.com>
2 months ago
gunchleoc 81400b02b1
Add nds locale to posting languages (#27434) 2 months ago
Claire bb3ad2b2a9
Merge pull request #2669 from ClearlyClaire/glitch-soc/reduce-upstream-differences
Reduce code differences with upstream
2 months ago
Claire 46152a19a6 Convert logo.jsx to Typescript 2 months ago
Sunny Ripert 2b4e9fbc71 [Glitch] Add form element on focal point modal
Port 8515bc7962 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Claire 98cf4b7ba0 Reduce code differences with upstream 2 months ago
neetshin 6996b96fab [Glitch] Make columns-area unscrollable when modal opened
Port 2091ae92be to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Claire a79bd2cd11 Make some CSS differences with upstream more explicit 2 months ago
Claire d09ab957d4 Remove duplicate `captcha_enabled` key in `app/models/form/admin_settings.rb` 2 months ago
Claire e95f2c2b68
Add a glitch-soc local setting to make the post publish toast optional (#2666) 2 months ago
Eugen Rochko e85a2aa18d
Fix interaction settings migration error when encountering no settings (#29529) 2 months ago
github-actions[bot] d002458c7b
New Crowdin Translations (automated) (#2661)
* New Crowdin translations

* Fix bogus translations

---------

Co-authored-by: GitHub Actions <noreply@github.com>
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Claire f407505f75
Merge pull request #2667 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 653ce43abe
2 months ago
renovate[bot] e8605a69d2
Update omniauth packages (#25306)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Renaud Chaput <renchap@gmail.com>
2 months ago
renovate[bot] 509528e2dc
Update dependency pg to v1.5.6 (#29477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2 months ago
Eugen Rochko 50b17f7e10
Add notification policies and notification requests (#29366) 2 months ago
Mashiro 1fc6edfa84 [Glitch] Fix unhandled nullable attachments limitation counter
Port b8bd94ca8e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2 months ago
Claire 442a5cb66c Merge commit '653ce43abe0a928d944a15c433d2c8324f9b5e2a' into glitch-soc/merge-upstream 2 months ago
Claire 3f239facff
Update flavor screenshots (#2664) 2 months ago
Renaud Chaput 653ce43abe
Update `json-jwt` gem to fix CVE-2023-51774 (#29520) 2 months ago
gunchleoc c01f4cebed
Add Mohawk to posting languages (#27115) 2 months ago
gunchleoc 995e15c24a
Add Jawi Malay to posting languages (#29098) 2 months ago
Claire 8c9341a67b
Further reduce pointless CSS differences with upstream (#2665) 2 months ago
Claire a11151d58f
Update our README and include upstream's (#2663) 2 months ago
Mashiro b8bd94ca8e
Fix unhandled nullable attachments limitation counter (#29183) 2 months ago
Claire 8f4cc711c2
Merge pull request #2662 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to f89512fbed
2 months ago
Claire 45e56db8e4 Merge commit 'f89512fbedb547f66a72eefdff047768fb505eb6' into glitch-soc/merge-upstream
Conflicts:
- `README.md`:
  Upstream updated its README, we have a completely different one.
  Kept our version.
2 months ago
renovate[bot] f89512fbed
Update dependency rack-cors to v2.0.2 [SECURITY] (#29507)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2 months ago
renovate[bot] 7265d47342
Update peter-evans/create-pull-request action to v6.0.1 (#29503)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2 months ago
renovate[bot] 4c138ee4eb
Update DefinitelyTyped types (non-major) (#29502)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2 months ago
renovate[bot] cb0f3ecc28
Update eslint (non-major) (#29505)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2 months ago
renovate[bot] 0f7f257139
Update Yarn to v4.1.1 (#29508)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2 months ago
Claire 74998b0636
Merge pull request #2657 from glitch-soc/i18n/crowdin/translations
New Crowdin Translations (automated)
2 months ago
Claire a7d9c5e374 Fix bogus translations 2 months ago
Claire ee8d0b9447
Fix follow suggestions potentially including silenced or blocked accounts (#29306) 2 months ago
GitHub Actions 1b418a3550 New Crowdin translations 2 months ago
Claire 96c6cafc34
Merge pull request #2660 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 18945f62e0
2 months ago
renovate[bot] 68600893d2
Update babel monorepo to v7.24.0 (#29434)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2 months ago
Matt Jankowski 64b8ba36bb
Add `bin/dev` script to launch foreman (#28242) 2 months ago
Dave MacLeod b6b94c971f
Add Interlingue to available_locales (#28630) 2 months ago
gunchleoc 1d5de8b26a
Add Vai to posting languages (#27136) 2 months ago
gunchleoc 5ab944af95
Rename Panjabi to Punjabi (#27117) 2 months ago
Helge 8d22599318
Add Pennsylvania Dutch to languages dropdown (#26634) 2 months ago
Krzysztof Piwowar b4af3639e8
Add Kashubian to languages dropdown (#26024) 2 months ago
Claire 159e500749 Merge commit '18945f62e07617ac44b7a25a61799b0959fe67f7' into glitch-soc/merge-upstream 2 months ago
Matt Jankowski 18945f62e0
Convert more API specs from controller->request style (#29004) 2 months ago
Daniel M Brasil a25014de8f
Improve `IpBlock` model test coverage (#29460) 2 months ago
Claire 1d721b21e1
Add attribution to Tabler.io icons used in the new mailer designs (#29470) 2 months ago
HTeuMeuLeu 934cab7508
New welcome email (#28883)
Co-authored-by: Eugen Rochko <eugen@zeonfederated.com>
2 months ago

@ -123,7 +123,7 @@ module.exports = defineConfig({
'react/react-in-jsx-scope': 'off', // not needed with new JSX transform
'react/self-closing-comp': 'error',
// recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/src/index.js
// recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.8.0/src/index.js#L46
'jsx-a11y/accessible-emoji': 'warn',
'jsx-a11y/click-events-have-key-events': 'off',
'jsx-a11y/label-has-associated-control': 'off',
@ -176,7 +176,7 @@ module.exports = defineConfig({
},
],
// See https://github.com/import-js/eslint-plugin-import/blob/main/config/recommended.js
// See https://github.com/import-js/eslint-plugin-import/blob/v2.29.1/config/recommended.js
'import/extensions': [
'error',
'always',

@ -53,7 +53,7 @@ jobs:
# Create or update the pull request
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6.0.0
uses: peter-evans/create-pull-request@v6.0.1
with:
commit-message: 'New Crowdin translations'
title: 'New Crowdin Translations (automated)'

@ -1,13 +0,0 @@
# This configuration was generated by
# `haml-lint --auto-gen-config`
# on 2024-01-09 11:30:07 -0500 using Haml-Lint version 0.53.0.
# The point is for the user to remove these configuration records
# one by one as the lints are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of Haml-Lint, may require this file to be generated again.
linters:
# Offense count: 1
LineLength:
exclude:
- 'app/views/admin/roles/_form.html.haml'

@ -20,7 +20,7 @@ FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
# Example: v4.2.0-nightly.2023.11.09+something
# Overwrite existance of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"]
# Overwrite existence of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"]
ARG MASTODON_VERSION_PRERELEASE=""
# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="something"]
ARG MASTODON_VERSION_METADATA=""
@ -29,7 +29,7 @@ ARG MASTODON_VERSION_METADATA=""
# See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files
ARG RAILS_SERVE_STATIC_FILES="true"
# Allow to use YJIT compiler
# See: https://github.com/ruby/ruby/blob/master/doc/yjit/yjit.md
# See: https://github.com/ruby/ruby/blob/v3_2_3/doc/yjit/yjit.md
ARG RUBY_YJIT_ENABLE="1"
# Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin]
ARG TZ="Etc/UTC"

@ -112,7 +112,7 @@ group :test do
# RSpec helpers for email specs
gem 'email_spec'
# Extra RSpec extenion methods and helpers for sidekiq
# Extra RSpec extension methods and helpers for sidekiq
gem 'rspec-sidekiq', '~> 4.0'
# Browser integration testing

@ -357,7 +357,7 @@ GEM
jmespath (1.6.2)
json (2.7.1)
json-canonicalization (1.0.0)
json-jwt (1.15.3)
json-jwt (1.15.3.1)
activesupport (>= 4.2)
aes_key_wrap
bindata
@ -465,11 +465,11 @@ GEM
statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.16.3)
bigdecimal (>= 3.0)
omniauth (2.1.1)
omniauth (2.1.2)
hashie (>= 3.4.6)
rack (>= 2.2.3)
rack-protection
omniauth-cas (3.0.0.beta.1)
omniauth-cas (3.0.0)
addressable (~> 2.8)
nokogiri (~> 1.12)
omniauth (~> 2.1)
@ -505,7 +505,7 @@ GEM
parslet (2.0.0)
pastel (0.8.0)
tty-color (~> 0.5)
pg (1.5.5)
pg (1.5.6)
pghero (3.4.1)
activerecord (>= 6)
posix-spawn (0.3.15)
@ -535,7 +535,7 @@ GEM
rack (2.2.8.1)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-cors (2.0.1)
rack-cors (2.0.2)
rack (>= 2.0.0)
rack-oauth2 (1.21.3)
activesupport
@ -543,8 +543,9 @@ GEM
httpclient
json-jwt (>= 1.11.0)
rack (>= 2.1.0)
rack-protection (3.0.5)
rack
rack-protection (3.2.0)
base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4)
rack-proxy (0.7.6)
rack
rack-session (1.0.2)
@ -743,7 +744,7 @@ GEM
unicode-display_width (>= 1.1.1, < 3)
terrapin (1.0.1)
climate_control
test-prof (1.3.1)
test-prof (1.3.2)
thor (1.3.1)
tilt (2.3.0)
timeout (0.4.1)

2
Vagrantfile vendored

@ -188,7 +188,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.post_up_message = <<MESSAGE
To start server
$ vagrant ssh -c "cd /vagrant && foreman start"
$ vagrant ssh -c "cd /vagrant && bin/dev"
MESSAGE
end

@ -1,6 +1,9 @@
# frozen_string_literal: true
class ActivityPub::BaseController < Api::BaseController
include SignatureVerification
include AccountOwnedConcern
skip_before_action :require_authenticated_user!
skip_before_action :require_not_suspended!
skip_around_action :set_locale

@ -1,9 +1,6 @@
# frozen_string_literal: true
class ActivityPub::ClaimsController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
skip_before_action :authenticate_user!
before_action :require_account_signature!

@ -1,9 +1,6 @@
# frozen_string_literal: true
class ActivityPub::CollectionsController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!, if: :authorized_fetch_mode?

@ -1,9 +1,6 @@
# frozen_string_literal: true
class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!

@ -1,9 +1,7 @@
# frozen_string_literal: true
class ActivityPub::InboxesController < ActivityPub::BaseController
include SignatureVerification
include JsonLdHelper
include AccountOwnedConcern
before_action :skip_unknown_actor_activity
before_action :require_actor_signature!

@ -3,9 +3,6 @@
class ActivityPub::OutboxesController < ActivityPub::BaseController
LIMIT = 20
include SignatureVerification
include AccountOwnedConcern
vary_by -> { 'Signature' if authorized_fetch_mode? || page_requested? }
before_action :require_account_signature!, if: :authorized_fetch_mode?

@ -1,9 +1,7 @@
# frozen_string_literal: true
class ActivityPub::RepliesController < ActivityPub::BaseController
include SignatureVerification
include Authorization
include AccountOwnedConcern
DESCENDANTS_LIMIT = 60

@ -53,7 +53,7 @@ module Admin
end
def resource_params
params.require(:rule).permit(:text, :priority)
params.require(:rule).permit(:text, :hint, :priority)
end
end
end

@ -8,6 +8,7 @@ class Api::BaseController < ApplicationController
include Api::AccessTokenTrackingConcern
include Api::CachingConcern
include Api::ContentSecurityPolicy
include Api::ErrorHandling
skip_before_action :require_functional!, unless: :limited_federation_mode?
@ -18,51 +19,6 @@ class Api::BaseController < ApplicationController
protect_from_forgery with: :null_session
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
render json: { error: e.to_s }, status: 422
end
rescue_from ActiveRecord::RecordNotUnique do
render json: { error: 'Duplicate record' }, status: 422
end
rescue_from Date::Error do
render json: { error: 'Invalid date supplied' }, status: 422
end
rescue_from ActiveRecord::RecordNotFound do
render json: { error: 'Record not found' }, status: 404
end
rescue_from HTTP::Error, Mastodon::UnexpectedResponseError do
render json: { error: 'Remote data could not be fetched' }, status: 503
end
rescue_from OpenSSL::SSL::SSLError do
render json: { error: 'Remote SSL certificate could not be verified' }, status: 503
end
rescue_from Mastodon::NotPermittedError do
render json: { error: 'This action is not allowed' }, status: 403
end
rescue_from Seahorse::Client::NetworkingError do |e|
Rails.logger.warn "Storage server error: #{e}"
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
end
rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight do
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
end
rescue_from Mastodon::RateLimitExceededError do
render json: { error: I18n.t('errors.429') }, status: 429
end
rescue_from ActionController::ParameterMissing, Mastodon::InvalidParameterError do |e|
render json: { error: e.to_s }, status: 400
end
def doorkeeper_unauthorized_render_options(error: nil)
{ json: { error: error.try(:description) || 'Not authorized' } }
end
@ -73,6 +29,14 @@ class Api::BaseController < ApplicationController
protected
def pagination_max_id
pagination_collection.last.id
end
def pagination_since_id
pagination_collection.first.id
end
def set_pagination_headers(next_path = nil, prev_path = nil)
links = []
links << [next_path, [%w(rel next)]] if next_path

@ -51,11 +51,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
end
def pagination_max_id
@statuses.last.id
end
def pagination_since_id
@statuses.first.id
def pagination_collection
@statuses
end
end

@ -137,12 +137,8 @@ class Api::V1::Admin::AccountsController < Api::BaseController
api_v1_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty?
end
def pagination_max_id
@accounts.last.id
end
def pagination_since_id
@accounts.first.id
def pagination_collection
@accounts
end
def records_continue?

@ -77,12 +77,8 @@ class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController
api_v1_admin_canonical_email_blocks_url(pagination_params(min_id: pagination_since_id)) unless @canonical_email_blocks.empty?
end
def pagination_max_id
@canonical_email_blocks.last.id
end
def pagination_since_id
@canonical_email_blocks.first.id
def pagination_collection
@canonical_email_blocks
end
def records_continue?

@ -73,12 +73,8 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
api_v1_admin_domain_allows_url(pagination_params(min_id: pagination_since_id)) unless @domain_allows.empty?
end
def pagination_max_id
@domain_allows.last.id
end
def pagination_since_id
@domain_allows.first.id
def pagination_collection
@domain_allows
end
def records_continue?

@ -84,12 +84,8 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
api_v1_admin_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @domain_blocks.empty?
end
def pagination_max_id
@domain_blocks.last.id
end
def pagination_since_id
@domain_blocks.first.id
def pagination_collection
@domain_blocks
end
def records_continue?

@ -70,12 +70,8 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController
api_v1_admin_email_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @email_domain_blocks.empty?
end
def pagination_max_id
@email_domain_blocks.last.id
end
def pagination_since_id
@email_domain_blocks.first.id
def pagination_collection
@email_domain_blocks
end
def records_continue?

@ -75,12 +75,8 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController
api_v1_admin_ip_blocks_url(pagination_params(min_id: pagination_since_id)) unless @ip_blocks.empty?
end
def pagination_max_id
@ip_blocks.last.id
end
def pagination_since_id
@ip_blocks.first.id
def pagination_collection
@ip_blocks
end
def records_continue?

@ -101,12 +101,8 @@ class Api::V1::Admin::ReportsController < Api::BaseController
api_v1_admin_reports_url(pagination_params(min_id: pagination_since_id)) unless @reports.empty?
end
def pagination_max_id
@reports.last.id
end
def pagination_since_id
@reports.first.id
def pagination_collection
@reports
end
def records_continue?

@ -56,12 +56,8 @@ class Api::V1::Admin::TagsController < Api::BaseController
api_v1_admin_tags_url(pagination_params(min_id: pagination_since_id)) unless @tags.empty?
end
def pagination_max_id
@tags.last.id
end
def pagination_since_id
@tags.first.id
def pagination_collection
@tags
end
def records_continue?

@ -54,12 +54,8 @@ class Api::V1::Admin::Trends::Links::PreviewCardProvidersController < Api::BaseC
api_v1_admin_trends_links_preview_card_providers_url(pagination_params(min_id: pagination_since_id)) unless @providers.empty?
end
def pagination_max_id
@providers.last.id
end
def pagination_since_id
@providers.first.id
def pagination_collection
@providers
end
def records_continue?

@ -40,12 +40,8 @@ class Api::V1::BlocksController < Api::BaseController
api_v1_blocks_url pagination_params(since_id: pagination_since_id) unless paginated_blocks.empty?
end
def pagination_max_id
paginated_blocks.last.id
end
def pagination_since_id
paginated_blocks.first.id
def pagination_collection
paginated_blocks
end
def records_continue?

@ -43,12 +43,8 @@ class Api::V1::BookmarksController < Api::BaseController
api_v1_bookmarks_url pagination_params(min_id: pagination_since_id) unless results.empty?
end
def pagination_max_id
results.last.id
end
def pagination_since_id
results.first.id
def pagination_collection
results
end
def records_continue?

@ -41,12 +41,8 @@ class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController
api_v1_crypto_encrypted_messages_url pagination_params(min_id: pagination_since_id) unless @encrypted_messages.empty?
end
def pagination_max_id
@encrypted_messages.last.id
end
def pagination_since_id
@encrypted_messages.first.id
def pagination_collection
@encrypted_messages
end
def records_continue?

@ -50,12 +50,8 @@ class Api::V1::DomainBlocksController < Api::BaseController
api_v1_domain_blocks_url pagination_params(since_id: pagination_since_id) unless @blocks.empty?
end
def pagination_max_id
@blocks.last.id
end
def pagination_since_id
@blocks.first.id
def pagination_collection
@blocks
end
def records_continue?

@ -44,12 +44,8 @@ class Api::V1::EndorsementsController < Api::BaseController
api_v1_endorsements_url pagination_params(since_id: pagination_since_id) unless @accounts.empty?
end
def pagination_max_id
@accounts.last.id
end
def pagination_since_id
@accounts.first.id
def pagination_collection
@accounts
end
def records_continue?

@ -43,12 +43,8 @@ class Api::V1::FavouritesController < Api::BaseController
api_v1_favourites_url pagination_params(min_id: pagination_since_id) unless results.empty?
end
def pagination_max_id
results.last.id
end
def pagination_since_id
results.first.id
def pagination_collection
results
end
def records_continue?

@ -34,12 +34,8 @@ class Api::V1::FollowedTagsController < Api::BaseController
api_v1_followed_tags_url pagination_params(since_id: pagination_since_id) unless @results.empty?
end
def pagination_max_id
@results.last.id
end
def pagination_since_id
@results.first.id
def pagination_collection
@results
end
def records_continue?

@ -71,12 +71,8 @@ class Api::V1::Lists::AccountsController < Api::BaseController
api_v1_list_accounts_url pagination_params(since_id: pagination_since_id) unless @accounts.empty?
end
def pagination_max_id
@accounts.last.id
end
def pagination_since_id
@accounts.first.id
def pagination_collection
@accounts
end
def records_continue?

@ -40,12 +40,8 @@ class Api::V1::MutesController < Api::BaseController
api_v1_mutes_url pagination_params(since_id: pagination_since_id) unless paginated_mutes.empty?
end
def pagination_max_id
paginated_mutes.last.id
end
def pagination_since_id
paginated_mutes.first.id
def pagination_collection
paginated_mutes
end
def records_continue?

@ -0,0 +1,37 @@
# frozen_string_literal: true
class Api::V1::Notifications::PoliciesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, only: :show
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: :update
before_action :require_user!
before_action :set_policy
def show
render json: @policy, serializer: REST::NotificationPolicySerializer
end
def update
@policy.update!(resource_params)
render json: @policy, serializer: REST::NotificationPolicySerializer
end
private
def set_policy
@policy = NotificationPolicy.find_or_initialize_by(account: current_account)
with_read_replica do
@policy.summarize!
end
end
def resource_params
params.permit(
:filter_not_following,
:filter_not_followers,
:filter_new_accounts,
:filter_private_mentions
)
end
end

@ -0,0 +1,79 @@
# frozen_string_literal: true
class Api::V1::Notifications::RequestsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, except: :index
before_action :require_user!
before_action :set_request, except: :index
after_action :insert_pagination_headers, only: :index
def index
with_read_replica do
@requests = load_requests
@relationships = relationships
end
render json: @requests, each_serializer: REST::NotificationRequestSerializer, relationships: @relationships
end
def show
render json: @request, serializer: REST::NotificationRequestSerializer
end
def accept
AcceptNotificationRequestService.new.call(@request)
render_empty
end
def dismiss
@request.update!(dismissed: true)
render_empty
end
private
def load_requests
requests = NotificationRequest.where(account: current_account).where(dismissed: truthy_param?(:dismissed) || false).includes(:last_status, from_account: [:account_stat, :user]).to_a_paginated_by_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
NotificationRequest.preload_cache_collection(requests) do |statuses|
cache_collection(statuses, Status)
end
end
def relationships
StatusRelationshipsPresenter.new(@requests.map(&:last_status), current_user&.account_id)
end
def set_request
@request = NotificationRequest.where(account: current_account).find(params[:id])
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_notifications_requests_url pagination_params(max_id: pagination_max_id) unless @requests.empty?
end
def prev_path
api_v1_notifications_requests_url pagination_params(min_id: pagination_since_id) unless @requests.empty?
end
def pagination_max_id
@requests.last.id
end
def pagination_since_id
@requests.first.id
end
def pagination_params(core_params)
params.slice(:dismissed).permit(:dismissed).merge(core_params)
end
end

@ -58,7 +58,8 @@ class Api::V1::NotificationsController < Api::BaseController
current_account.notifications.without_suspended.browserable(
types: Array(browserable_params[:types]),
exclude_types: Array(browserable_params[:exclude_types]),
from_account_id: browserable_params[:account_id]
from_account_id: browserable_params[:account_id],
include_filtered: truthy_param?(:include_filtered)
)
end
@ -78,19 +79,15 @@ class Api::V1::NotificationsController < Api::BaseController
api_v1_notifications_url pagination_params(min_id: pagination_since_id) unless @notifications.empty?
end
def pagination_max_id
@notifications.last.id
end
def pagination_since_id
@notifications.first.id
def pagination_collection
@notifications
end
def browserable_params
params.permit(:account_id, types: [], exclude_types: [])
params.permit(:account_id, :include_filtered, types: [], exclude_types: [])
end
def pagination_params(core_params)
params.slice(:limit, :account_id, :types, :exclude_types).permit(:limit, :account_id, types: [], exclude_types: []).merge(core_params)
params.slice(:limit, :account_id, :types, :exclude_types, :include_filtered).permit(:limit, :account_id, :include_filtered, types: [], exclude_types: []).merge(core_params)
end
end

@ -63,11 +63,7 @@ class Api::V1::ScheduledStatusesController < Api::BaseController
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
end
def pagination_max_id
@statuses.last.id
end
def pagination_since_id
@statuses.first.id
def pagination_collection
@statuses
end
end

@ -9,12 +9,8 @@ class Api::V1::Timelines::BaseController < Api::BaseController
set_pagination_headers(next_path, prev_path)
end
def pagination_max_id
@statuses.last.id
end
def pagination_since_id
@statuses.first.id
def pagination_collection
@statuses
end
def next_path_params

@ -131,7 +131,7 @@ class ApplicationController < ActionController::Base
end
def single_user_mode?
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists?
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.without_internal.exists?
end
def use_seamless_external_login?

@ -0,0 +1,52 @@
# frozen_string_literal: true
module Api::ErrorHandling
extend ActiveSupport::Concern
included do
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
render json: { error: e.to_s }, status: 422
end
rescue_from ActiveRecord::RecordNotUnique do
render json: { error: 'Duplicate record' }, status: 422
end
rescue_from Date::Error do
render json: { error: 'Invalid date supplied' }, status: 422
end
rescue_from ActiveRecord::RecordNotFound do
render json: { error: 'Record not found' }, status: 404
end
rescue_from HTTP::Error, Mastodon::UnexpectedResponseError do
render json: { error: 'Remote data could not be fetched' }, status: 503
end
rescue_from OpenSSL::SSL::SSLError do
render json: { error: 'Remote SSL certificate could not be verified' }, status: 503
end
rescue_from Mastodon::NotPermittedError do
render json: { error: 'This action is not allowed' }, status: 403
end
rescue_from Seahorse::Client::NetworkingError do |e|
Rails.logger.warn "Storage server error: #{e}"
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
end
rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight do
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
end
rescue_from Mastodon::RateLimitExceededError do
render json: { error: I18n.t('errors.429') }, status: 429
end
rescue_from ActionController::ParameterMissing, Mastodon::InvalidParameterError do |e|
render json: { error: e.to_s }, status: 400
end
end
end

@ -6,6 +6,8 @@ class InstanceActorsController < ActivityPub::BaseController
serialization_scope nil
before_action :set_account
skip_before_action :authenticate_user! # From `AccountOwnedConcern`
skip_before_action :require_functional!
skip_before_action :update_user_sign_in
@ -16,6 +18,11 @@ class InstanceActorsController < ActivityPub::BaseController
private
# Skips various `before_action` from `AccountOwnedConcern`
def account_required?
false
end
def set_account
@account = Account.representative
end

@ -214,7 +214,7 @@ module ApplicationHelper
state_params[:moved_to_account] = current_account.moved_to_account
end
state_params[:owner] = Account.local.without_suspended.where('id > 0').first if single_user_mode?
state_params[:owner] = Account.local.without_suspended.without_internal.first if single_user_mode?
json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json
# rubocop:disable Rails/OutputSafety

@ -109,6 +109,7 @@ module LanguagesHelper
mn: ['Mongolian', 'Монгол хэл'].freeze,
mr: ['Marathi', 'मराठी'].freeze,
ms: ['Malay', 'Bahasa Melayu'].freeze,
'ms-Arab': ['Jawi Malay', 'بهاس ملايو'].freeze,
mt: ['Maltese', 'Malti'].freeze,
my: ['Burmese', 'ဗမာစာ'].freeze,
na: ['Nauru', 'Ekakairũ Naoero'].freeze,
@ -127,7 +128,7 @@ module LanguagesHelper
om: ['Oromo', 'Afaan Oromoo'].freeze,
or: ['Oriya', 'ଓଡ଼ିଆ'].freeze,
os: ['Ossetian', 'ирон æвзаг'].freeze,
pa: ['Panjabi', 'ਪੰਜਾਬੀ'].freeze,
pa: ['Punjabi', 'ਪੰਜਾਬੀ'].freeze,
pi: ['Pāli', 'पाऴि'].freeze,
pl: ['Polish', 'Polski'].freeze,
ps: ['Pashto', 'پښتو'].freeze,
@ -191,15 +192,20 @@ module LanguagesHelper
chr: ['Cherokee', 'ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ'].freeze,
ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze,
cnr: ['Montenegrin', 'crnogorski'].freeze,
csb: ['Kashubian', 'Kaszëbsczi'].freeze,
jbo: ['Lojban', 'la .lojban.'].freeze,
kab: ['Kabyle', 'Taqbaylit'].freeze,
ldn: ['Láadan', 'Láadan'].freeze,
lfn: ['Lingua Franca Nova', 'lingua franca nova'].freeze,
moh: ['Mohawk', 'Kanienʼkéha'].freeze,
nds: ['Low German', 'Plattdüütsch'].freeze,
pdc: ['Pennsylvania Dutch', 'Pennsilfaani-Deitsch'].freeze,
sco: ['Scots', 'Scots'].freeze,
sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze,
smj: ['Lule Sami', 'Julevsámegiella'].freeze,
szl: ['Silesian', 'ślůnsko godka'].freeze,
tok: ['Toki Pona', 'toki pona'].freeze,
vai: ['Vai', 'ꕙꔤ'].freeze,
xal: ['Kalmyk', 'Хальмг келн'].freeze,
zba: ['Balaibalan', 'باليبلن'].freeze,
zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze,

@ -1,25 +0,0 @@
// This file will be loaded on embed pages, regardless of theme.
import 'packs/public-path';
window.addEventListener('message', e => {
const data = e.data || {};
if (!window.parent || data.type !== 'setHeight') {
return;
}
function setEmbedHeight () {
window.parent.postMessage({
type: 'setHeight',
id: data.id,
height: document.getElementsByTagName('html')[0].scrollHeight,
}, '*');
}
if (['interactive', 'complete'].includes(document.readyState)) {
setEmbedHeight();
} else {
document.addEventListener('DOMContentLoaded', setEmbedHeight);
}
});

@ -0,0 +1,41 @@
// This file will be loaded on embed pages, regardless of theme.
import 'packs/public-path';
import ready from '../mastodon/ready';
interface SetHeightMessage {
type: 'setHeight';
id: string;
height: number;
}
function isSetHeightMessage(data: unknown): data is SetHeightMessage {
if (
data &&
typeof data === 'object' &&
'type' in data &&
data.type === 'setHeight'
)
return true;
else return false;
}
window.addEventListener('message', (e) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typings are not correct, it can be null in very rare cases
if (!e.data || !isSetHeightMessage(e.data) || !window.parent) return;
const data = e.data;
ready(() => {
window.parent.postMessage(
{
type: 'setHeight',
id: data.id,
height: document.getElementsByTagName('html')[0].scrollHeight,
},
'*',
);
}).catch((e) => {
console.error('Error in setHeightMessage postMessage', e);
});
});

@ -1,44 +0,0 @@
// This file will be loaded on settings pages, regardless of theme.
import 'packs/public-path';
import Rails from '@rails/ujs';
Rails.delegate(document, '#edit_profile input[type=file]', 'change', ({ target }) => {
const avatar = document.getElementById(target.id + '-preview');
const [file] = target.files || [];
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
avatar.src = url;
});
Rails.delegate(document, '.input-copy input', 'click', ({ target }) => {
target.focus();
target.select();
target.setSelectionRange(0, target.value.length);
});
Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
const input = target.parentNode.querySelector('.input-copy__wrapper input');
const oldReadOnly = input.readonly;
input.readonly = false;
input.focus();
input.select();
input.setSelectionRange(0, input.value.length);
try {
if (document.execCommand('copy')) {
input.blur();
target.parentNode.classList.add('copied');
setTimeout(() => {
target.parentNode.classList.remove('copied');
}, 700);
}
} catch (err) {
console.error(err);
}
input.readonly = oldReadOnly;
});

@ -0,0 +1,70 @@
// This file will be loaded on settings pages, regardless of theme.
import 'packs/public-path';
import Rails from '@rails/ujs';
Rails.delegate(
document,
'#edit_profile input[type=file]',
'change',
({ target }) => {
if (!(target instanceof HTMLInputElement)) return;
const avatar = document.querySelector<HTMLImageElement>(
`img#${target.id}-preview`,
);
if (!avatar) return;
let file: File | undefined;
if (target.files) file = target.files[0];
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
if (url) avatar.src = url;
},
);
Rails.delegate(document, '.input-copy input', 'click', ({ target }) => {
if (!(target instanceof HTMLInputElement)) return;
target.focus();
target.select();
target.setSelectionRange(0, target.value.length);
});
Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
if (!(target instanceof HTMLButtonElement)) return;
const input = target.parentNode?.querySelector<HTMLInputElement>(
'.input-copy__wrapper input',
);
if (!input) return;
const oldReadOnly = input.readOnly;
input.readOnly = false;
input.focus();
input.select();
input.setSelectionRange(0, input.value.length);
try {
if (document.execCommand('copy')) {
input.blur();
const parent = target.parentElement;
if (!parent) return;
parent.classList.add('copied');
setTimeout(() => {
parent.classList.remove('copied');
}, 700);
}
} catch (err) {
console.error(err);
}
input.readOnly = oldReadOnly;
});

@ -7,7 +7,7 @@ pack:
common:
filename: common.js
stylesheet: true
embed: embed.js
embed: embed.ts
error:
home:
inert:
@ -18,7 +18,7 @@ pack:
stylesheet: true
modal:
public:
settings: settings.js
settings: settings.ts
sign_up:
share:
remote_interaction_helper: remote_interaction_helper.ts

@ -66,11 +66,9 @@ export const FOLLOW_REQUESTS_EXPAND_SUCCESS = 'FOLLOW_REQUESTS_EXPAND_SUCCESS';
export const FOLLOW_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL';
export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST';
export const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS';
export const FOLLOW_REQUEST_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL';
export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
export const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST';
@ -93,11 +91,6 @@ export * from './accounts_typed';
export function fetchAccount(id) {
return (dispatch, getState) => {
dispatch(fetchRelationships([id]));
if (getState().getIn(['accounts', id], null) !== null) {
return;
}
dispatch(fetchAccountRequest(id));
api(getState).get(`/api/v1/accounts/${id}`).then(response => {

@ -12,8 +12,6 @@ export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST';
export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
export const BLOCKS_INIT_MODAL = 'BLOCKS_INIT_MODAL';
export function fetchBlocks() {
return (dispatch, getState) => {
dispatch(fetchBlocksRequest());
@ -90,11 +88,12 @@ export function expandBlocksFail(error) {
export function initBlockModal(account) {
return dispatch => {
dispatch({
type: BLOCKS_INIT_MODAL,
account,
});
dispatch(openModal({ modalType: 'BLOCK' }));
dispatch(openModal({
modalType: 'BLOCK',
modalProps: {
accountId: account.get('id'),
acct: account.get('acct'),
},
}));
};
}

@ -266,12 +266,14 @@ export function submitCompose(routerHistory, overridePrivacy = null) {
insertIfOnline('direct');
}
dispatch(showAlert({
message: statusId === null ? messages.published : messages.saved,
action: messages.open,
dismissAfter: 10000,
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
}));
if (getState().getIn(['local_settings', 'show_published_toast'])) {
dispatch(showAlert({
message: statusId === null ? messages.published : messages.saved,
action: messages.open,
dismissAfter: 10000,
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
}));
}
}).catch(function (error) {
dispatch(submitComposeFail(error));
});
@ -820,11 +822,12 @@ export function addPollOption(title) {
};
}
export function changePollOption(index, title) {
export function changePollOption(index, title, maxOptions) {
return {
type: COMPOSE_POLL_OPTION_CHANGE,
index,
title,
maxOptions,
};
}

@ -1,6 +1,8 @@
import api, { getLinks } from '../api';
import { blockDomainSuccess, unblockDomainSuccess } from "./domain_blocks_typed";
import { openModal } from './modal';
export * from "./domain_blocks_typed";
@ -150,3 +152,12 @@ export function expandDomainBlocksFail(error) {
error,
};
}
export const initDomainBlockModal = account => dispatch => dispatch(openModal({
modalType: 'DOMAIN_BLOCK',
modalProps: {
domain: account.get('acct').split('@')[1],
acct: account.get('acct'),
accountId: account.get('id'),
},
}));

@ -12,10 +12,6 @@ export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
export const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION';
export function fetchMutes() {
return (dispatch, getState) => {
dispatch(fetchMutesRequest());
@ -92,26 +88,12 @@ export function expandMutesFail(error) {
export function initMuteModal(account) {
return dispatch => {
dispatch({
type: MUTES_INIT_MODAL,
account,
});
dispatch(openModal({ modalType: 'MUTE' }));
};
}
export function toggleHideNotifications() {
return dispatch => {
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
};
}
export function changeMuteDuration(duration) {
return dispatch => {
dispatch({
type: MUTES_CHANGE_DURATION,
duration,
});
dispatch(openModal({
modalType: 'MUTE',
modalProps: {
accountId: account.get('id'),
acct: account.get('acct'),
},
}));
};
}

@ -57,6 +57,38 @@ export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';
export const NOTIFICATION_POLICY_FETCH_REQUEST = 'NOTIFICATION_POLICY_FETCH_REQUEST';
export const NOTIFICATION_POLICY_FETCH_SUCCESS = 'NOTIFICATION_POLICY_FETCH_SUCCESS';
export const NOTIFICATION_POLICY_FETCH_FAIL = 'NOTIFICATION_POLICY_FETCH_FAIL';
export const NOTIFICATION_REQUESTS_FETCH_REQUEST = 'NOTIFICATION_REQUESTS_FETCH_REQUEST';
export const NOTIFICATION_REQUESTS_FETCH_SUCCESS = 'NOTIFICATION_REQUESTS_FETCH_SUCCESS';
export const NOTIFICATION_REQUESTS_FETCH_FAIL = 'NOTIFICATION_REQUESTS_FETCH_FAIL';
export const NOTIFICATION_REQUESTS_EXPAND_REQUEST = 'NOTIFICATION_REQUESTS_EXPAND_REQUEST';
export const NOTIFICATION_REQUESTS_EXPAND_SUCCESS = 'NOTIFICATION_REQUESTS_EXPAND_SUCCESS';
export const NOTIFICATION_REQUESTS_EXPAND_FAIL = 'NOTIFICATION_REQUESTS_EXPAND_FAIL';
export const NOTIFICATION_REQUEST_FETCH_REQUEST = 'NOTIFICATION_REQUEST_FETCH_REQUEST';
export const NOTIFICATION_REQUEST_FETCH_SUCCESS = 'NOTIFICATION_REQUEST_FETCH_SUCCESS';
export const NOTIFICATION_REQUEST_FETCH_FAIL = 'NOTIFICATION_REQUEST_FETCH_FAIL';
export const NOTIFICATION_REQUEST_ACCEPT_REQUEST = 'NOTIFICATION_REQUEST_ACCEPT_REQUEST';
export const NOTIFICATION_REQUEST_ACCEPT_SUCCESS = 'NOTIFICATION_REQUEST_ACCEPT_SUCCESS';
export const NOTIFICATION_REQUEST_ACCEPT_FAIL = 'NOTIFICATION_REQUEST_ACCEPT_FAIL';
export const NOTIFICATION_REQUEST_DISMISS_REQUEST = 'NOTIFICATION_REQUEST_DISMISS_REQUEST';
export const NOTIFICATION_REQUEST_DISMISS_SUCCESS = 'NOTIFICATION_REQUEST_DISMISS_SUCCESS';
export const NOTIFICATION_REQUEST_DISMISS_FAIL = 'NOTIFICATION_REQUEST_DISMISS_FAIL';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL = 'NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL';
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST';
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS';
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL';
defineMessages({
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
});
@ -402,3 +434,270 @@ export function setBrowserPermission (value) {
value,
};
}
export const fetchNotificationPolicy = () => (dispatch, getState) => {
dispatch(fetchNotificationPolicyRequest());
api(getState).get('/api/v1/notifications/policy').then(({ data }) => {
dispatch(fetchNotificationPolicySuccess(data));
}).catch(err => {
dispatch(fetchNotificationPolicyFail(err));
});
};
export const fetchNotificationPolicyRequest = () => ({
type: NOTIFICATION_POLICY_FETCH_REQUEST,
});
export const fetchNotificationPolicySuccess = policy => ({
type: NOTIFICATION_POLICY_FETCH_SUCCESS,
policy,
});
export const fetchNotificationPolicyFail = error => ({
type: NOTIFICATION_POLICY_FETCH_FAIL,
error,
});
export const updateNotificationsPolicy = params => (dispatch, getState) => {
dispatch(fetchNotificationPolicyRequest());
api(getState).put('/api/v1/notifications/policy', params).then(({ data }) => {
dispatch(fetchNotificationPolicySuccess(data));
}).catch(err => {
dispatch(fetchNotificationPolicyFail(err));
});
};
export const fetchNotificationRequests = () => (dispatch, getState) => {
const params = {};
if (getState().getIn(['notificationRequests', 'isLoading'])) {
return;
}
if (getState().getIn(['notificationRequests', 'items'])?.size > 0) {
params.since_id = getState().getIn(['notificationRequests', 'items', 0, 'id']);
}
dispatch(fetchNotificationRequestsRequest());
api(getState).get('/api/v1/notifications/requests', { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
dispatch(fetchNotificationRequestsSuccess(response.data, next ? next.uri : null));
}).catch(err => {
dispatch(fetchNotificationRequestsFail(err));
});
};
export const fetchNotificationRequestsRequest = () => ({
type: NOTIFICATION_REQUESTS_FETCH_REQUEST,
});
export const fetchNotificationRequestsSuccess = (requests, next) => ({
type: NOTIFICATION_REQUESTS_FETCH_SUCCESS,
requests,
next,
});
export const fetchNotificationRequestsFail = error => ({
type: NOTIFICATION_REQUESTS_FETCH_FAIL,
error,
});
export const expandNotificationRequests = () => (dispatch, getState) => {
const url = getState().getIn(['notificationRequests', 'next']);
if (!url || getState().getIn(['notificationRequests', 'isLoading'])) {
return;
}
dispatch(expandNotificationRequestsRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
dispatch(expandNotificationRequestsSuccess(response.data, next?.uri));
}).catch(err => {
dispatch(expandNotificationRequestsFail(err));
});
};
export const expandNotificationRequestsRequest = () => ({
type: NOTIFICATION_REQUESTS_EXPAND_REQUEST,
});
export const expandNotificationRequestsSuccess = (requests, next) => ({
type: NOTIFICATION_REQUESTS_EXPAND_SUCCESS,
requests,
next,
});
export const expandNotificationRequestsFail = error => ({
type: NOTIFICATION_REQUESTS_EXPAND_FAIL,
error,
});
export const fetchNotificationRequest = id => (dispatch, getState) => {
const current = getState().getIn(['notificationRequests', 'current']);
if (current.getIn(['item', 'id']) === id || current.get('isLoading')) {
return;
}
dispatch(fetchNotificationRequestRequest(id));
api(getState).get(`/api/v1/notifications/requests/${id}`).then(({ data }) => {
dispatch(fetchNotificationRequestSuccess(data));
}).catch(err => {
dispatch(fetchNotificationRequestFail(id, err));
});
};
export const fetchNotificationRequestRequest = id => ({
type: NOTIFICATION_REQUEST_FETCH_REQUEST,
id,
});
export const fetchNotificationRequestSuccess = request => ({
type: NOTIFICATION_REQUEST_FETCH_SUCCESS,
request,
});
export const fetchNotificationRequestFail = (id, error) => ({
type: NOTIFICATION_REQUEST_FETCH_FAIL,
id,
error,
});
export const acceptNotificationRequest = id => (dispatch, getState) => {
dispatch(acceptNotificationRequestRequest(id));
api(getState).post(`/api/v1/notifications/requests/${id}/accept`).then(() => {
dispatch(acceptNotificationRequestSuccess(id));
}).catch(err => {
dispatch(acceptNotificationRequestFail(id, err));
});
};
export const acceptNotificationRequestRequest = id => ({
type: NOTIFICATION_REQUEST_ACCEPT_REQUEST,
id,
});
export const acceptNotificationRequestSuccess = id => ({
type: NOTIFICATION_REQUEST_ACCEPT_SUCCESS,
id,
});
export const acceptNotificationRequestFail = (id, error) => ({
type: NOTIFICATION_REQUEST_ACCEPT_FAIL,
id,
error,
});
export const dismissNotificationRequest = id => (dispatch, getState) => {
dispatch(dismissNotificationRequestRequest(id));
api(getState).post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{
dispatch(dismissNotificationRequestSuccess(id));
}).catch(err => {
dispatch(dismissNotificationRequestFail(id, err));
});
};
export const dismissNotificationRequestRequest = id => ({
type: NOTIFICATION_REQUEST_DISMISS_REQUEST,
id,
});
export const dismissNotificationRequestSuccess = id => ({
type: NOTIFICATION_REQUEST_DISMISS_SUCCESS,
id,
});
export const dismissNotificationRequestFail = (id, error) => ({
type: NOTIFICATION_REQUEST_DISMISS_FAIL,
id,
error,
});
export const fetchNotificationsForRequest = accountId => (dispatch, getState) => {
const current = getState().getIn(['notificationRequests', 'current']);
const params = { account_id: accountId };
if (current.getIn(['item', 'account']) === accountId) {
if (current.getIn(['notifications', 'isLoading'])) {
return;
}
if (current.getIn(['notifications', 'items'])?.size > 0) {
params.since_id = current.getIn(['notifications', 'items', 0, 'id']);
}
}
dispatch(fetchNotificationsForRequestRequest());
api(getState).get('/api/v1/notifications', { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
dispatch(fetchNotificationsForRequestSuccess(response.data, next?.uri));
}).catch(err => {
dispatch(fetchNotificationsForRequestFail(err));
});
};
export const fetchNotificationsForRequestRequest = () => ({
type: NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST,
});
export const fetchNotificationsForRequestSuccess = (notifications, next) => ({
type: NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS,
notifications,
next,
});
export const fetchNotificationsForRequestFail = (error) => ({
type: NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL,
error,
});
export const expandNotificationsForRequest = () => (dispatch, getState) => {
const url = getState().getIn(['notificationRequests', 'current', 'notifications', 'next']);
if (!url || getState().getIn(['notificationRequests', 'current', 'notifications', 'isLoading'])) {
return;
}
dispatch(expandNotificationsForRequestRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
dispatch(expandNotificationsForRequestSuccess(response.data, next?.uri));
}).catch(err => {
dispatch(expandNotificationsForRequestFail(err));
});
};
export const expandNotificationsForRequestRequest = () => ({
type: NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST,
});
export const expandNotificationsForRequestSuccess = (notifications, next) => ({
type: NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS,
notifications,
next,
});
export const expandNotificationsForRequestFail = (error) => ({
type: NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL,
error,
});

@ -1,7 +1,7 @@
import api from '../api';
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
import { importFetchedStatus, importFetchedStatuses } from './importer';
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
import { deleteFromTimelines } from './timelines';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
@ -138,10 +138,10 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
dispatch(deleteStatusSuccess(id));
dispatch(deleteFromTimelines(id));
dispatch(importFetchedAccount(response.data.account));
if (withRedraft) {
dispatch(redraft(status, response.data.text, response.data.content_type));
ensureComposeIsVisible(getState, routerHistory);
}
}).catch(error => {

@ -10,7 +10,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import LinkIcon from '@/material-icons/400-24px/link.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
export default class AttachmentList extends ImmutablePureComponent {

@ -0,0 +1,39 @@
import classNames from 'classnames';
import DoneIcon from '@/material-icons/400-24px/done.svg?react';
import { Icon } from './icon';
interface Props {
value: string;
checked: boolean;
name: string;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
label: React.ReactNode;
}
export const CheckBox: React.FC<Props> = ({
name,
value,
checked,
onChange,
label,
}) => {
return (
<label className='check-box'>
<input
name={name}
type='checkbox'
value={value}
checked={checked}
onChange={onChange}
/>
<span className={classNames('check-box__input', { checked })}>
{checked && <Icon id='check' icon={DoneIcon} />}
</span>
<span>{label}</span>
</label>
);
};

@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import { PureComponent, useCallback } from 'react';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import { FormattedMessage, injectIntl, defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
@ -11,12 +11,11 @@ import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import TuneIcon from '@/material-icons/400-24px/tune.svg?react';
import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
import { ButtonInTabsBar, useColumnsContext } from 'flavours/glitch/features/ui/util/columns_context';
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
import { useAppHistory } from './router';
const messages = defineMessages({
@ -24,10 +23,12 @@ const messages = defineMessages({
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
});
const BackButton = ({ pinned, show }) => {
const BackButton = ({ pinned, show, onlyIcon }) => {
const history = useAppHistory();
const intl = useIntl();
const { multiColumn } = useColumnsContext();
const handleBackClick = useCallback(() => {
@ -40,18 +41,20 @@ const BackButton = ({ pinned, show }) => {
const showButton = history && !pinned && ((multiColumn && history.location?.state?.fromMastodon) || show);
if(!showButton) return null;
return (<button onClick={handleBackClick} className='column-header__back-button'>
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</button>);
if (!showButton) return null;
return (
<button onClick={handleBackClick} className={classNames('column-header__back-button', { 'compact': onlyIcon })} aria-label={intl.formatMessage(messages.back)}>
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
{!onlyIcon && <FormattedMessage id='column_back_button.label' defaultMessage='Back' />}
</button>
);
};
BackButton.propTypes = {
pinned: PropTypes.bool,
show: PropTypes.bool,
onlyIcon: PropTypes.bool,
};
class ColumnHeader extends PureComponent {
@ -146,27 +149,31 @@ class ColumnHeader extends PureComponent {
}
if (multiColumn && pinned) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
pinButton = <button className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
moveButtons = (
<div key='move-buttons' className='column-header__setting-arrows'>
<div className='column-header__setting-arrows'>
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' icon={ChevronLeftIcon} /></button>
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' icon={ChevronRightIcon} /></button>
</div>
);
} else if (multiColumn && this.props.onPin) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' icon={AddIcon} /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
pinButton = <button className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' icon={AddIcon} /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
}
backButton = <BackButton pinned={pinned} show={showBackButton} />;
backButton = <BackButton pinned={pinned} show={showBackButton} onlyIcon={!!title} />;
const collapsedContent = [
extraContent,
];
if (multiColumn) {
collapsedContent.push(pinButton);
collapsedContent.push(moveButtons);
collapsedContent.push(
<div key='buttons' className='column-header__advanced-buttons'>
{pinButton}
{moveButtons}
</div>
);
}
if (this.context.identity.signedIn && (children || (multiColumn && this.props.onPin))) {
@ -178,7 +185,7 @@ class ColumnHeader extends PureComponent {
onClick={this.handleToggleClick}
>
<i className='icon-with-badge'>
<Icon id='sliders' icon={TuneIcon} />
<Icon id='sliders' icon={SettingsIcon} />
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
</i>
</button>
@ -191,16 +198,19 @@ class ColumnHeader extends PureComponent {
<div className={wrapperClassName}>
<h1 className={buttonClassName}>
{hasTitle && (
<button onClick={this.handleTitleClick}>
<Icon id={icon} icon={iconComponent} className='column-header__icon' />
{title}
</button>
<>
{showBackButton && backButton}
<button onClick={this.handleTitleClick} className='column-header__title'>
{!showBackButton && <Icon id={icon} icon={iconComponent} className='column-header__icon' />}
{title}
</button>
</>
)}
{!hasTitle && backButton}
{!hasTitle && showBackButton && backButton}
<div className='column-header__buttons'>
{hasTitle && backButton}
{extraButton}
{collapseButton}
</div>

@ -9,7 +9,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { supportsPassiveEvents } from 'detect-passive-events';
import Overlay from 'react-overlays/Overlay';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { CircularProgress } from 'flavours/glitch/components/circular_progress';
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
@ -298,7 +297,7 @@ class Dropdown extends PureComponent {
}) : (
<IconButton
icon={!open ? icon : 'close'}
iconComponent={!open ? iconComponent : CloseIcon}
iconComponent={iconComponent}
title={title}
active={open}
disabled={disabled}

@ -5,10 +5,7 @@ import { FormattedMessage, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?react';
import { openModal } from 'flavours/glitch/actions/modal';
import { Icon } from 'flavours/glitch/components/icon';
import InlineAccount from 'flavours/glitch/components/inline_account';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
@ -68,7 +65,7 @@ class EditedTimestamp extends PureComponent {
return (
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
<button className='dropdown-menu__text-button'>
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} /> <Icon id='caret-down' icon={ArrowDropDownIcon} />
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: <span className='animated-number'>{intl.formatDate(timestamp, { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</span> }} />
</button>
</DropdownMenu>
);

@ -1,14 +1,12 @@
import logo from '@/images/logo.svg';
export const WordmarkLogo = () => (
export const WordmarkLogo: React.FC = () => (
<svg viewBox='0 0 261 66' className='logo logo--wordmark' role='img'>
<title>Mastodon</title>
<use xlinkHref='#logo-symbol-wordmark' />
</svg>
);
export const SymbolLogo = () => (
export const SymbolLogo: React.FC = () => (
<img src={logo} alt='Mastodon' className='logo logo--icon' />
);
export default WordmarkLogo;

@ -1,6 +1,6 @@
import { FormattedMessage } from 'react-intl';
import illustration from 'flavours/glitch/images/elephant_ui_working.svg';
import illustration from '@/images/elephant_ui_working.svg';
const RegenerationIndicator = () => (
<div className='regeneration-indicator'>

@ -102,7 +102,7 @@ const getUnitDelay = (units: string) => {
};
export const timeAgoString = (
intl: IntlShape,
intl: Pick<IntlShape, 'formatDate' | 'formatMessage'>,
date: Date,
now: number,
year: number,

@ -20,6 +20,8 @@ import Card from '../features/status/components/card';
// to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
import { IdentityConsumer } from '../features/ui/util/identity_consumer';
import { SensitiveMediaContext } from '../features/ui/util/sensitive_media_context';
import { displayMedia, visibleReactions } from '../initial_state';
import AttachmentList from './attachment_list';
@ -73,9 +75,7 @@ export const defaultMediaVisibility = (status, settings) => {
class Status extends ImmutablePureComponent {
static contextTypes = {
identity: PropTypes.object,
};
static contextType = SensitiveMediaContext;
static propTypes = {
containerId: PropTypes.string,
@ -132,8 +132,7 @@ class Status extends ImmutablePureComponent {
isCollapsed: false,
autoCollapsed: false,
isExpanded: undefined,
showMedia: undefined,
statusId: undefined,
showMedia: defaultMediaVisibility(this.props.status, this.props.settings) && !(this.context?.hideMediaByDefault),
revealBehindCW: undefined,
showCard: false,
forceFilter: undefined,
@ -218,12 +217,6 @@ class Status extends ImmutablePureComponent {
updated = true;
}
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
update.showMedia = defaultMediaVisibility(nextProps.status, nextProps.settings);
update.statusId = nextProps.status.get('id');
updated = true;
}
if (nextProps.settings.getIn(['media', 'reveal_behind_cw']) !== prevState.revealBehindCW) {
update.revealBehindCW = nextProps.settings.getIn(['media', 'reveal_behind_cw']);
if (update.revealBehindCW) {
@ -319,6 +312,18 @@ class Status extends ImmutablePureComponent {
if (snapshot !== null && this.props.updateScrollBottom && this.node.offsetTop < snapshot.top) {
this.props.updateScrollBottom(snapshot.height - snapshot.top);
}
// This will potentially cause a wasteful redraw, but in most cases `Status` components are used
// with a `key` directly depending on their `id`, preventing re-use of the component across
// different IDs.
// But just in case this does change, reset the state on status change.
if (this.props.status?.get('id') !== prevProps.status?.get('id')) {
this.setState({
showMedia: defaultMediaVisibility(this.props.status, this.props.settings) && !(this.context?.hideMediaByDefault),
forceFilter: undefined,
});
}
}
componentWillUnmount() {
@ -841,14 +846,18 @@ class Status extends ImmutablePureComponent {
{...statusContentProps}
/>
<StatusReactions
statusId={status.get('id')}
reactions={status.get('reactions')}
numVisible={visibleReactions}
addReaction={this.props.onReactionAdd}
removeReaction={this.props.onReactionRemove}
canReact={this.context.identity.signedIn}
/>
<IdentityConsumer>
{identity => (
<StatusReactions
statusId={status.get('id')}
reactions={status.get('reactions')}
numVisible={visibleReactions}
addReaction={this.props.onReactionAdd}
removeReaction={this.props.onReactionRemove}
canReact={identity.signedIn}
/>
)}
</IdentityConsumer>
{(!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar']))) && (
<StatusActionBar

@ -10,7 +10,6 @@ import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import ExpandMoreIcon from '@/material-icons/400-24px/expand_more.svg?react';
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server';
@ -171,7 +170,8 @@ class About extends PureComponent {
<ol className='rules-list'>
{server.get('rules').map(rule => (
<li key={rule.get('id')}>
<span className='rules-list__text'>{rule.get('text')}</span>
<div className='rules-list__text'>{rule.get('text')}</div>
{rule.get('hint').length > 0 && (<div className='rules-list__hint'>{rule.get('hint')}</div>)}
</li>
))}
</ol>
@ -189,18 +189,20 @@ class About extends PureComponent {
<>
<p><FormattedMessage id='about.domain_blocks.preamble' defaultMessage='Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.' /></p>
<div className='about__domain-blocks'>
{domainBlocks.get('items').map(block => (
<div className='about__domain-blocks__domain' key={block.get('domain')}>
<div className='about__domain-blocks__domain__header'>
<h6><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></h6>
<span className='about__domain-blocks__domain__type' title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity')].title)}</span>
</div>
{domainBlocks.get('items').size > 0 && (
<div className='about__domain-blocks'>
{domainBlocks.get('items').map(block => (
<div className='about__domain-blocks__domain' key={block.get('domain')}>
<div className='about__domain-blocks__domain__header'>
<h6><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></h6>
<span className='about__domain-blocks__domain__type' title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity')].title)}</span>
</div>
<p>{(block.get('comment') || '').length > 0 ? block.get('comment') : <FormattedMessage id='about.domain_blocks.no_reason_available' defaultMessage='Reason not available' />}</p>
</div>
))}
</div>
<p>{(block.get('comment') || '').length > 0 ? block.get('comment') : <FormattedMessage id='about.domain_blocks.no_reason_available' defaultMessage='Reason not available' />}</p>
</div>
))}
</div>
)}
</>
) : (
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>

@ -0,0 +1,86 @@
import PropTypes from 'prop-types';
import { useState, useRef, useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import Overlay from 'react-overlays/Overlay';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import BadgeIcon from '@/material-icons/400-24px/badge.svg?react';
import GlobeIcon from '@/material-icons/400-24px/globe.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
export const DomainPill = ({ domain, username, isSelf }) => {
const [open, setOpen] = useState(false);
const [expanded, setExpanded] = useState(false);
const triggerRef = useRef(null);
const handleClick = useCallback(() => {
setOpen(!open);
}, [open, setOpen]);
const handleExpandClick = useCallback(() => {
setExpanded(!expanded);
}, [expanded, setExpanded]);
return (
<>
<button className={classNames('account__domain-pill', { active: open })} ref={triggerRef} onClick={handleClick}>{domain}</button>
<Overlay show={open} rootClose onHide={handleClick} offset={[5, 5]} target={triggerRef}>
{({ props }) => (
<div {...props} className='account__domain-pill__popout dropdown-animation'>
<div className='account__domain-pill__popout__header'>
<div className='account__domain-pill__popout__header__icon'><Icon icon={BadgeIcon} /></div>
<h3><FormattedMessage id='domain_pill.whats_in_a_handle' defaultMessage="What's in a handle?" /></h3>
</div>
<div className='account__domain-pill__popout__handle'>
<div className='account__domain-pill__popout__handle__label'>{isSelf ? <FormattedMessage id='domain_pill.your_handle' defaultMessage='Your handle:' /> : <FormattedMessage id='domain_pill.their_handle' defaultMessage='Their handle:' />}</div>
<div className='account__domain-pill__popout__handle__handle'>@{username}@{domain}</div>
</div>
<div className='account__domain-pill__popout__parts'>
<div>
<div className='account__domain-pill__popout__parts__icon'><Icon icon={AlternateEmailIcon} /></div>
<div>
<h6><FormattedMessage id='domain_pill.username' defaultMessage='Username' /></h6>
<p>{isSelf ? <FormattedMessage id='domain_pill.your_username' defaultMessage='Your unique identifier on this server. Its possible to find users with the same username on different servers.' /> : <FormattedMessage id='domain_pill.their_username' defaultMessage='Their unique identifier on their server. Its possible to find users with the same username on different servers.' />}</p>
</div>
</div>
<div>
<div className='account__domain-pill__popout__parts__icon'><Icon icon={GlobeIcon} /></div>
<div>
<h6><FormattedMessage id='domain_pill.server' defaultMessage='Server' /></h6>
<p>{isSelf ? <FormattedMessage id='domain_pill.your_server' defaultMessage='Your digital home, where all of your posts live. Dont like this one? Transfer servers at any time and bring your followers, too.' /> : <FormattedMessage id='domain_pill.their_server' defaultMessage='Their digital home, where all of their posts live.' />}</p>
</div>
</div>
</div>
<p>{isSelf ? <FormattedMessage id='domain_pill.who_you_are' defaultMessage='Because your handle says who you are and where you are, people can interact with you across the social web of <button>ActivityPub-powered platforms</button>.' values={{ button: x => <button onClick={handleExpandClick} className='link-button'>{x}</button> }} /> : <FormattedMessage id='domain_pill.who_they_are' defaultMessage='Since handles say who someone is and where they are, you can interact with people across the social web of <button>ActivityPub-powered platforms</button>.' values={{ button: x => <button onClick={handleExpandClick} className='link-button'>{x}</button> }} />}</p>
{expanded && (
<>
<p><FormattedMessage id='domain_pill.activitypub_like_language' defaultMessage='ActivityPub is like the language Mastodon speaks with other social networks.' /></p>
<p><FormattedMessage id='domain_pill.activitypub_lets_connect' defaultMessage='It lets you connect and interact with people not just on Mastodon, but across different social apps too.' /></p>
</>
)}
</div>
)}
</Overlay>
</>
);
};
DomainPill.propTypes = {
username: PropTypes.string.isRequired,
domain: PropTypes.string.isRequired,
isSelf: PropTypes.bool,
};

@ -21,8 +21,9 @@ import { Button } from 'flavours/glitch/components/button';
import { CopyIconButton } from 'flavours/glitch/components/copy_icon_button';
import { Icon } from 'flavours/glitch/components/icon';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state';
import { autoPlayGif, me, domain as localDomain } from 'flavours/glitch/initial_state';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links';
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
@ -30,6 +31,8 @@ import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
import AccountNoteContainer from '../containers/account_note_container';
import FollowRequestNoteContainer from '../containers/follow_request_note_container';
import { DomainPill } from './domain_pill';
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
@ -74,7 +77,7 @@ const messages = defineMessages({
const titleFromAccount = account => {
const displayName = account.get('display_name');
const acct = account.get('acct') === account.get('username') ? `${account.get('username')}@${domain}` : account.get('acct');
const acct = account.get('acct') === account.get('username') ? `${account.get('username')}@${localDomain}` : account.get('acct');
const prefix = displayName.trim().length === 0 ? account.get('username') : displayName;
return `${prefix} (@${acct})`;
@ -166,7 +169,7 @@ class Header extends ImmutablePureComponent {
};
render () {
const { account, hidden, intl, domain } = this.props;
const { account, hidden, intl } = this.props;
const { signedIn, permissions } = this.context.identity;
if (!account) {
@ -206,7 +209,7 @@ class Header extends ImmutablePureComponent {
if (me !== account.get('id')) {
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = '';
actionBtn = <Button disabled><LoadingIndicator /></Button>;
} else if (account.getIn(['relationship', 'requested'])) {
actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
} else if (!account.getIn(['relationship', 'blocking'])) {
@ -313,7 +316,8 @@ class Header extends ImmutablePureComponent {
const displayNameHtml = { __html: account.get('display_name_html') };
const fields = account.get('fields');
const isLocal = account.get('acct').indexOf('@') === -1;
const acct = isLocal && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
const username = account.get('acct').split('@')[0];
const domain = isLocal ? localDomain : account.get('acct').split('@')[1];
const isIndexable = !account.get('noindex');
const badges = [];
@ -347,15 +351,10 @@ class Header extends ImmutablePureComponent {
</a>
<div className='account__header__tabs__buttons'>
{!hidden && (
<>
{actionBtn}
{bellBtn}
{shareBtn}
</>
)}
{!hidden && bellBtn}
{!hidden && shareBtn}
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
{!hidden && actionBtn}
</div>
</div>
@ -363,7 +362,9 @@ class Header extends ImmutablePureComponent {
<h1>
<span dangerouslySetInnerHTML={displayNameHtml} />
<small>
<span>@{acct}</span> {lockedIcon}
<span>@{username}<span className='invisible'>@{domain}</span></span>
<DomainPill username={username} domain={domain} isSelf={me === account.get('id')} />
{lockedIcon}
</small>
</h1>
</div>

@ -12,7 +12,6 @@ import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon';
import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
export default class MediaItem extends ImmutablePureComponent {
static propTypes = {

@ -72,11 +72,7 @@ class Header extends ImmutablePureComponent {
};
handleBlockDomain = () => {
const domain = this.props.account.get('acct').split('@')[1];
if (!domain) return;
this.props.onBlockDomain(domain);
this.props.onBlockDomain(this.props.account);
};
handleUnblockDomain = () => {

@ -15,7 +15,7 @@ import {
mentionCompose,
directCompose,
} from '../../../actions/compose';
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
import { initDomainBlockModal, unblockDomain } from '../../../actions/domain_blocks';
import { openModal } from '../../../actions/modal';
import { initMuteModal } from '../../../actions/mutes';
import { initReport } from '../../../actions/reports';
@ -138,15 +138,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onBlockDomain (domain) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => dispatch(blockDomain(domain)),
},
}));
onBlockDomain (account) {
dispatch(initDomainBlockModal(account));
},
onUnblockDomain (domain) {

@ -18,7 +18,6 @@ import VolumeUpIcon from '@/material-icons/400-24px/volume_up-fill.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
import { formatTime, getPointerPosition, fileNameFromURL } from 'flavours/glitch/features/video';
import { Blurhash } from '../../components/blurhash';
import { displayMedia, useBlurhash } from '../../initial_state';

@ -26,7 +26,7 @@ class ColumnSettings extends PureComponent {
const { settings, onChange, intl } = this.props;
return (
<div>
<div className='column-settings'>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
</div>

@ -7,7 +7,6 @@ import { Helmet } from 'react-helmet';
import { connect } from 'react-redux';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner';
import { domain } from 'flavours/glitch/initial_state';

@ -59,10 +59,11 @@ const Option = ({ multipleChoice, index, title, autoFocus }) => {
const dispatch = useDispatch();
const suggestions = useSelector(state => state.getIn(['compose', 'suggestions']));
const lang = useSelector(state => state.getIn(['compose', 'language']));
const maxOptions = useSelector(state => state.getIn(['server', 'server', 'configuration', 'polls', 'max_options']));
const handleChange = useCallback(({ target: { value } }) => {
dispatch(changePollOption(index, value));
}, [dispatch, index]);
dispatch(changePollOption(index, value, maxOptions));
}, [dispatch, index, maxOptions]);
const handleSuggestionsFetchRequested = useCallback(token => {
dispatch(fetchComposeSuggestions(token));

@ -7,7 +7,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
import { LoadMore } from 'flavours/glitch/components/load_more';
@ -76,11 +75,6 @@ class SearchResults extends ImmutablePureComponent {
return (
<div className='search-results'>
<div className='search-results__header'>
<Icon id='search' icon={SearchIcon} />
<FormattedMessage id='explore.search_results' defaultMessage='Search results' />
</div>
{accounts}
{hashtags}
{statuses}

@ -4,14 +4,24 @@ import { uploadCompose } from '../../../actions/compose';
import { openModal } from '../../../actions/modal';
import UploadButton from '../components/upload_button';
const mapStateToProps = state => ({
disabled: state.getIn(['compose', 'poll']) !== null || state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size + state.getIn(['compose', 'pending_media_attachments']) > 3 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')))),
resetFileKey: state.getIn(['compose', 'resetFileKey']),
});
const mapStateToProps = state => {
const isPoll = state.getIn(['compose', 'poll']) !== null;
const isUploading = state.getIn(['compose', 'is_uploading']);
const readyAttachmentsSize = state.getIn(['compose', 'media_attachments']).size ?? 0;
const pendingAttachmentsSize = state.getIn(['compose', 'pending_media_attachments']).size ?? 0;
const attachmentsSize = readyAttachmentsSize + pendingAttachmentsSize;
const isOverLimit = attachmentsSize > 3;
const hasVideoOrAudio = state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')));
return {
disabled: isPoll || isUploading || isOverLimit || hasVideoOrAudio,
resetFileKey: state.getIn(['compose', 'resetFileKey']),
};
};
const mapDispatchToProps = dispatch => ({
onSelectFile (files) {
onSelectFile(files) {
dispatch(uploadCompose(files));
},

@ -26,18 +26,20 @@ class ColumnSettings extends PureComponent {
const { settings, onChange, intl } = this.props;
return (
<div>
<span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingPath={['conversations']} onChange={onChange} label={<FormattedMessage id='direct.group_by_conversations' defaultMessage='Group by conversation' />} />
</div>
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
<div className='column-settings__row'>
<SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
</div>
<div className='column-settings'>
<section>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingPath={['conversations']} onChange={onChange} label={<FormattedMessage id='direct.group_by_conversations' defaultMessage='Group by conversation' />} />
</div>
</section>
<section>
<h3><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></h3>
<div className='column-settings__row'>
<SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
</div>
</section>
</div>
);
}

@ -9,7 +9,6 @@ import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'flavours/glitch/actions/columns';
import { fetchDirectory, expandDirectory } from 'flavours/glitch/actions/directory';

@ -8,7 +8,6 @@ import { NavLink, Switch, Route } from 'react-router-dom';
import { connect } from 'react-redux';
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import Column from 'flavours/glitch/components/column';

@ -9,7 +9,6 @@ import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';

@ -180,7 +180,7 @@ class SelectFilter extends PureComponent {
<div className='emoji-mart-search'>
<input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} autoFocus />
<button className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? loupeIcon : deleteIcon}</button>
<button type='button' className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? loupeIcon : deleteIcon}</button>
</div>
<div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}>

@ -6,7 +6,6 @@ import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import { NavLink } from 'react-router-dom';
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import { addColumn } from 'flavours/glitch/actions/columns';
import { changeSetting } from 'flavours/glitch/actions/settings';
@ -46,28 +45,37 @@ const ColumnSettings = () => {
);
return (
<div>
<div className='column-settings__row'>
<SettingToggle
settings={settings}
settingPath={['onlyMedia']}
onChange={onChange}
label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />}
/>
<SettingToggle
settings={settings}
settingPath={['allowLocalOnly']}
onChange={onChange}
label={<FormattedMessage id='firehose.column_settings.allow_local_only' defaultMessage='Show local-only posts in "All"' />}
/>
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
<SettingText
settings={settings}
settingPath={['regex', 'body']}
onChange={onChange}
label={intl.formatMessage(messages.filter_regex)}
/>
</div>
<div className='column-settings'>
<section>
<div className='column-settings__row'>
<SettingToggle
settings={settings}
settingPath={['onlyMedia']}
onChange={onChange}
label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />}
/>
<SettingToggle
settings={settings}
settingPath={['allowLocalOnly']}
onChange={onChange}
label={<FormattedMessage id='firehose.column_settings.allow_local_only' defaultMessage='Show local-only posts in "All"' />}
/>
</div>
</section>
<section>
<h3><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></h3>
<div className='column-settings__row'>
<SettingText
settings={settings}
settingPath={['regex', 'body']}
onChange={onChange}
label={intl.formatMessage(messages.filter_regex)}
/>
</div>
</section>
</div>
);
};

@ -11,6 +11,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
@ -22,7 +23,6 @@ import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
import { fetchLists } from 'flavours/glitch/actions/lists';
import { openModal } from 'flavours/glitch/actions/modal';
@ -158,7 +158,7 @@ class GettingStarted extends ImmutablePureComponent {
}
if (showTrends) {
navItems.push(<ColumnLink key='explore' icon='explore' iconComponent={TagIcon} text={intl.formatMessage(messages.explore)} to='/explore' />);
navItems.push(<ColumnLink key='explore' icon='explore' iconComponent={ExploreIcon} text={intl.formatMessage(messages.explore)} to='/explore' />);
}
if (signedIn) {

@ -109,28 +109,28 @@ class ColumnSettings extends PureComponent {
const { settings, onChange } = this.props;
return (
<div>
<div className='column-settings__row'>
<div className='setting-toggle'>
<Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
<span className='setting-toggle__label'>
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
</span>
<div className='column-settings'>
<section>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingPath={['local']} onChange={onChange} label={<FormattedMessage id='community.column_settings.local_only' defaultMessage='Local only' />} />
<div className='setting-toggle'>
<Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
<span className='setting-toggle__label'>
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
</span>
</div>
</div>
</div>
{this.state.open && (
<div className='column-settings__hashtags'>
{this.modeSelect('any')}
{this.modeSelect('all')}
{this.modeSelect('none')}
</div>
)}
<div className='column-settings__row'>
<SettingToggle settings={settings} settingPath={['local']} onChange={onChange} label={<FormattedMessage id='community.column_settings.local_only' defaultMessage='Local only' />} />
</div>
{this.state.open && (
<div className='column-settings__hashtags'>
{this.modeSelect('any')}
{this.modeSelect('all')}
{this.modeSelect('none')}
</div>
)}
</section>
</div>
);
}

@ -35,75 +35,68 @@ export const ColumnSettings: React.FC = () => {
);
return (
<div>
<span className='column-settings__section'>
<FormattedMessage
id='home.column_settings.basic'
defaultMessage='Basic'
/>
</span>
<div className='column-settings'>
<section>
<div className='column-settings__row'>
<SettingToggle
prefix='home_timeline'
settings={settings}
settingPath={['shows', 'reblog']}
onChange={onChange}
label={
<FormattedMessage
id='home.column_settings.show_reblogs'
defaultMessage='Show boosts'
/>
}
/>
<div className='column-settings__row'>
<SettingToggle
prefix='home_timeline'
settings={settings}
settingPath={['shows', 'reblog']}
onChange={onChange}
label={
<FormattedMessage
id='home.column_settings.show_reblogs'
defaultMessage='Show boosts'
/>
}
/>
</div>
<SettingToggle
prefix='home_timeline'
settings={settings}
settingPath={['shows', 'reply']}
onChange={onChange}
label={
<FormattedMessage
id='home.column_settings.show_replies'
defaultMessage='Show replies'
/>
}
/>
<div className='column-settings__row'>
<SettingToggle
prefix='home_timeline'
settings={settings}
settingPath={['shows', 'reply']}
onChange={onChange}
label={
<FormattedMessage
id='home.column_settings.show_replies'
defaultMessage='Show replies'
/>
}
/>
</div>
<SettingToggle
prefix='home_timeline'
settings={settings}
settingPath={['shows', 'direct']}
onChange={onChange}
label={
<FormattedMessage
id='home.column_settings.show_direct'
defaultMessage='Show private mentions'
/>
}
/>
</div>
</section>
<div className='column-settings__row'>
<SettingToggle
prefix='home_timeline'
settings={settings}
settingPath={['shows', 'direct']}
onChange={onChange}
label={
<FormattedMessage
id='home.column_settings.show_direct'
defaultMessage='Show private mentions'
/>
}
/>
</div>
<section aria-labelledby='home-column-advanced'>
<h3 id='home-column-advanced'>
<FormattedMessage
id='home.column_settings.advanced'
defaultMessage='Advanced'
/>
</h3>
<span className='column-settings__section'>
<FormattedMessage
id='home.column_settings.advanced'
defaultMessage='Advanced'
/>
</span>
<div className='column-settings__row'>
<SettingText
prefix='home_timeline'
settings={settings}
settingPath={['regex', 'body']}
onChange={onChange}
label={intl.formatMessage(messages.filter_regex)}
/>
</div>
<div className='column-settings__row'>
<SettingText
prefix='home_timeline'
settings={settings}
settingPath={['regex', 'body']}
onChange={onChange}
label={intl.formatMessage(messages.filter_regex)}
/>
</div>
</section>
</div>
);
};

@ -8,7 +8,6 @@ import { Helmet } from 'react-helmet';
import { connect } from 'react-redux';
import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/actions/announcements';

@ -7,7 +7,6 @@ import { Helmet } from 'react-helmet';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
import Column from 'flavours/glitch/components/column';
import ColumnHeader from 'flavours/glitch/components/column_header';

@ -11,7 +11,6 @@ import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
import { removeFromListAdder, addToListAdder } from '../../../actions/lists';
import { IconButton } from '../../../components/icon_button';

@ -11,7 +11,6 @@ import CancelIcon from '@/material-icons/400-24px/cancel.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
const messages = defineMessages({

@ -193,37 +193,38 @@ class ListTimeline extends PureComponent {
pinned={pinned}
multiColumn={multiColumn}
>
<div className='column-settings__row column-header__links'>
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
<Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
</button>
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
<Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
</button>
</div>
<div className='setting-toggle'>
<Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
</label>
</div>
{ replies_policy !== undefined && (
<div role='group' aria-labelledby={`list-${id}-replies-policy`}>
<span id={`list-${id}-replies-policy`} className='column-settings__section'>
<FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' />
</span>
<div className='column-settings__row'>
{ ['none', 'list', 'followed'].map(policy => (
<RadioButton name='order' key={policy} value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
))}
<div className='column-settings'>
<section className='column-header__links'>
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
<Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
</button>
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
<Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
</button>
</section>
<section>
<div className='setting-toggle'>
<Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
</label>
</div>
</div>
)}
<hr />
</section>
{replies_policy !== undefined && (
<section aria-labelledby={`list-${id}-replies-policy`}>
<h3 id={`list-${id}-replies-policy`}><FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' /></h3>
<div className='column-settings__row'>
{ ['none', 'list', 'followed'].map(policy => (
<RadioButton name='order' key={policy} value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
))}
</div>
</section>
)}
</div>
</ColumnHeader>
<StatusListContainer

@ -9,7 +9,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { fetchLists } from 'flavours/glitch/actions/lists';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';

@ -224,6 +224,14 @@ class LocalSettingsPage extends PureComponent {
>
<FormattedMessage id='settings.show_content_type_choice' defaultMessage='Show content-type choice when authoring toots' />
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['show_published_toast']}
id='mastodon-settings--show_published_toast'
onChange={onChange}
>
<FormattedMessage id='settings.show_published_toast' defaultMessage='Display toast when publishing/saving a post' />
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['side_arm']}

@ -0,0 +1,31 @@
import PropTypes from 'prop-types';
import { useCallback } from 'react';
import Toggle from 'react-toggle';
export const CheckboxWithLabel = ({ checked, disabled, children, onChange }) => {
const handleChange = useCallback(({ target }) => {
onChange(target.checked);
}, [onChange]);
return (
<label className='app-form__toggle'>
<div className='app-form__toggle__label'>
{children}
</div>
<div className='app-form__toggle__toggle'>
<div>
<Toggle checked={checked} onChange={handleChange} disabled={disabled} />
</div>
</div>
</label>
);
};
CheckboxWithLabel.propTypes = {
checked: PropTypes.bool,
disabled: PropTypes.bool,
children: PropTypes.children,
onChange: PropTypes.func,
};

@ -6,7 +6,6 @@ import { FormattedMessage } from 'react-intl';
import DeleteForeverIcon from '@/material-icons/400-24px/delete_forever.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
export default class ClearColumnButton extends PureComponent {
static propTypes = {

@ -7,6 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'flavours/glitch/permissions';
import { CheckboxWithLabel } from './checkbox_with_label';
import ClearColumnButton from './clear_column_button';
import GrantPermissionButton from './grant_permission_button';
import PillBarButton from './pill_bar_button';
@ -27,14 +28,32 @@ export default class ColumnSettings extends PureComponent {
alertsEnabled: PropTypes.bool,
browserSupport: PropTypes.bool,
browserPermission: PropTypes.string,
notificationPolicy: ImmutablePropTypes.map,
onChangePolicy: PropTypes.func.isRequired,
};
onPushChange = (path, checked) => {
this.props.onChange(['push', ...path], checked);
};
handleFilterNotFollowing = checked => {
this.props.onChangePolicy('filter_not_following', checked);
};
handleFilterNotFollowers = checked => {
this.props.onChangePolicy('filter_not_followers', checked);
};
handleFilterNewAccounts = checked => {
this.props.onChangePolicy('filter_new_accounts', checked);
};
handleFilterPrivateMentions = checked => {
this.props.onChangePolicy('filter_private_mentions', checked);
};
render () {
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission } = this.props;
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission, notificationPolicy } = this.props;
const unreadMarkersShowStr = <FormattedMessage id='notifications.column_settings.unread_notifications.highlight' defaultMessage='Highlight unread notifications' />;
const filterBarShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show_bar' defaultMessage='Show filter bar' />;
@ -47,48 +66,68 @@ export default class ColumnSettings extends PureComponent {
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
return (
<div>
<div className='column-settings'>
{alertsEnabled && browserSupport && browserPermission === 'denied' && (
<div className='column-settings__row column-settings__row--with-margin'>
<span className='warning-hint'><FormattedMessage id='notifications.permission_denied' defaultMessage='Desktop notifications are unavailable due to previously denied browser permissions request' /></span>
</div>
<span className='warning-hint'><FormattedMessage id='notifications.permission_denied' defaultMessage='Desktop notifications are unavailable due to previously denied browser permissions request' /></span>
)}
{alertsEnabled && browserSupport && browserPermission === 'default' && (
<div className='column-settings__row column-settings__row--with-margin'>
<span className='warning-hint'>
<FormattedMessage id='notifications.permission_required' defaultMessage='Desktop notifications are unavailable because the required permission has not been granted.' /> <GrantPermissionButton onClick={onRequestNotificationPermission} />
</span>
</div>
<span className='warning-hint'>
<FormattedMessage id='notifications.permission_required' defaultMessage='Desktop notifications are unavailable because the required permission has not been granted.' /> <GrantPermissionButton onClick={onRequestNotificationPermission} />
</span>
)}
<div className='column-settings__row'>
<section>
<ClearColumnButton onClick={onClear} />
</div>
</section>
<div role='group' aria-labelledby='notifications-unread-markers'>
<span id='notifications-unread-markers' className='column-settings__section'>
<FormattedMessage id='notifications.column_settings.unread_notifications.category' defaultMessage='Unread notifications' />
</span>
<section>
<h3><FormattedMessage id='notifications.policy.title' defaultMessage='Filter out notifications from…' /></h3>
<div className='column-settings__row'>
<SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={unreadMarkersShowStr} />
<CheckboxWithLabel checked={notificationPolicy.get('filter_not_following')} onChange={this.handleFilterNotFollowing}>
<strong><FormattedMessage id='notifications.policy.filter_not_following_title' defaultMessage="People you don't follow" /></strong>
<span className='hint'><FormattedMessage id='notifications.policy.filter_not_following_hint' defaultMessage='Until you manually approve them' /></span>
</CheckboxWithLabel>
<CheckboxWithLabel checked={notificationPolicy.get('filter_not_followers')} onChange={this.handleFilterNotFollowers}>
<strong><FormattedMessage id='notifications.policy.filter_not_followers_title' defaultMessage='People not following you' /></strong>
<span className='hint'><FormattedMessage id='notifications.policy.filter_not_followers_hint' defaultMessage='Including people who have been following you fewer than {days, plural, one {one day} other {# days}}' values={{ days: 3 }} /></span>
</CheckboxWithLabel>
<CheckboxWithLabel checked={notificationPolicy.get('filter_new_accounts')} onChange={this.handleFilterNewAccounts}>
<strong><FormattedMessage id='notifications.policy.filter_new_accounts_title' defaultMessage='New accounts' /></strong>
<span className='hint'><FormattedMessage id='notifications.policy.filter_new_accounts.hint' defaultMessage='Created within the past {days, plural, one {one day} other {# days}}' values={{ days: 30 }} /></span>
</CheckboxWithLabel>
<CheckboxWithLabel checked={notificationPolicy.get('filter_private_mentions')} onChange={this.handleFilterPrivateMentions}>
<strong><FormattedMessage id='notifications.policy.filter_private_mentions_title' defaultMessage='Unsolicited private mentions' /></strong>
<span className='hint'><FormattedMessage id='notifications.policy.filter_private_mentions_hint' defaultMessage="Filtered unless it's in reply to your own mention or if you follow the sender" /></span>
</CheckboxWithLabel>
</div>
</div>
</section>
<div role='group' aria-labelledby='notifications-filter-bar'>
<span id='notifications-filter-bar' className='column-settings__section'>
<FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' />
</span>
<section role='group' aria-labelledby='notifications-filter-bar'>
<h3 id='notifications-filter-bar'><FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' /></h3>
<div className='column-settings__row'>
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterBarShowStr} />
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} />
</div>
</div>
</section>
<div role='group' aria-labelledby='notifications-follow'>
<span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
<section role='group' aria-labelledby='notifications-unread-markers'>
<h3 id='notifications-unread-markers'>
<FormattedMessage id='notifications.column_settings.unread_notifications.category' defaultMessage='Unread notifications' />
</h3>
<div className='column-settings__row'>
<SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={unreadMarkersShowStr} />
</div>
</section>
<section role='group' aria-labelledby='notifications-follow'>
<h3 id='notifications-follow'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></h3>
<div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow']} onChange={onChange} label={alertStr} />
@ -96,10 +135,10 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} />
</div>
</div>
</section>
<div role='group' aria-labelledby='notifications-follow-request'>
<span id='notifications-follow-request' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow_request' defaultMessage='New follow requests:' /></span>
<section role='group' aria-labelledby='notifications-follow-request'>
<h3 id='notifications-follow-request'><FormattedMessage id='notifications.column_settings.follow_request' defaultMessage='New follow requests:' /></h3>
<div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow_request']} onChange={onChange} label={alertStr} />
@ -107,10 +146,10 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'follow_request']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'follow_request']} onChange={onChange} label={soundStr} />
</div>
</div>
</section>
<div role='group' aria-labelledby='notifications-favourite'>
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favorites:' /></span>
<section role='group' aria-labelledby='notifications-favourite'>
<h3 id='notifications-favourite'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favorites:' /></h3>
<div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
@ -118,10 +157,10 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'favourite']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
</div>
</div>
</section>
<div role='group' aria-labelledby='notifications-reaction'>
<span id='notifications-reaction' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reaction' defaultMessage='Reactions:' /></span>
<section role='group' aria-labelledby='notifications-reaction'>
<h3 id='notifications-reaction'><FormattedMessage id='notifications.column_settings.reaction' defaultMessage='Reactions:' /></h3>
<div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reaction']} onChange={onChange} label={alertStr} />
@ -129,10 +168,10 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'reaction']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'reaction']} onChange={onChange} label={soundStr} />
</div>
</div>
</section>
<div role='group' aria-labelledby='notifications-mention'>
<span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
<section role='group' aria-labelledby='notifications-mention'>
<h3 id='notifications-mention'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></h3>
<div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'mention']} onChange={onChange} label={alertStr} />
@ -140,10 +179,10 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'mention']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'mention']} onChange={onChange} label={soundStr} />
</div>
</div>
</section>
<div role='group' aria-labelledby='notifications-reblog'>
<span id='notifications-reblog' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
<section role='group' aria-labelledby='notifications-reblog'>
<h3 id='notifications-reblog'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></h3>
<div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
@ -151,10 +190,10 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
</div>
</div>
</section>
<div role='group' aria-labelledby='notifications-poll'>
<span id='notifications-poll' className='column-settings__section'><FormattedMessage id='notifications.column_settings.poll' defaultMessage='Poll results:' /></span>
<section role='group' aria-labelledby='notifications-poll'>
<h3 id='notifications-poll'><FormattedMessage id='notifications.column_settings.poll' defaultMessage='Poll results:' /></h3>
<div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'poll']} onChange={onChange} label={alertStr} />
@ -162,10 +201,10 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'poll']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'poll']} onChange={onChange} label={soundStr} />
</div>
</div>
</section>
<div role='group' aria-labelledby='notifications-status'>
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.status' defaultMessage='New posts:' /></span>
<section role='group' aria-labelledby='notifications-status'>
<h3 id='notifications-status'><FormattedMessage id='notifications.column_settings.status' defaultMessage='New posts:' /></h3>
<div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'status']} onChange={onChange} label={alertStr} />
@ -173,10 +212,10 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'status']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'status']} onChange={onChange} label={soundStr} />
</div>
</div>
</section>
<div role='group' aria-labelledby='notifications-update'>
<span id='notifications-update' className='column-settings__section'><FormattedMessage id='notifications.column_settings.update' defaultMessage='Edits:' /></span>
<section role='group' aria-labelledby='notifications-update'>
<h3 id='notifications-update'><FormattedMessage id='notifications.column_settings.update' defaultMessage='Edits:' /></h3>
<div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'update']} onChange={onChange} label={alertStr} />
@ -184,11 +223,11 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'update']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'update']} onChange={onChange} label={soundStr} />
</div>
</div>
</section>
{((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && (
<div role='group' aria-labelledby='notifications-admin-sign-up'>
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.sign_up' defaultMessage='New sign-ups:' /></span>
<section role='group' aria-labelledby='notifications-admin-sign-up'>
<h3 id='notifications-status'><FormattedMessage id='notifications.column_settings.admin.sign_up' defaultMessage='New sign-ups:' /></h3>
<div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.sign_up']} onChange={onChange} label={alertStr} />
@ -196,12 +235,12 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'admin.sign_up']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'admin.sign_up']} onChange={onChange} label={soundStr} />
</div>
</div>
</section>
)}
{((this.context.identity.permissions & PERMISSION_MANAGE_REPORTS) === PERMISSION_MANAGE_REPORTS) && (
<div role='group' aria-labelledby='notifications-admin-report'>
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.report' defaultMessage='New reports:' /></span>
<section role='group' aria-labelledby='notifications-admin-report'>
<h3 id='notifications-status'><FormattedMessage id='notifications.column_settings.admin.report' defaultMessage='New reports:' /></h3>
<div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.report']} onChange={onChange} label={alertStr} />
@ -209,7 +248,7 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'admin.report']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'admin.report']} onChange={onChange} label={soundStr} />
</div>
</div>
</section>
)}
</div>
);

@ -12,7 +12,6 @@ import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
const tooltips = defineMessages({
mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favorites' },

@ -0,0 +1,49 @@
import { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import ArchiveIcon from '@/material-icons/400-24px/archive.svg?react';
import { fetchNotificationPolicy } from 'flavours/glitch/actions/notifications';
import { Icon } from 'flavours/glitch/components/icon';
import { toCappedNumber } from 'flavours/glitch/utils/numbers';
export const FilteredNotificationsBanner = () => {
const dispatch = useDispatch();
const policy = useSelector(state => state.get('notificationPolicy'));
useEffect(() => {
dispatch(fetchNotificationPolicy());
const interval = setInterval(() => {
dispatch(fetchNotificationPolicy());
}, 120000);
return () => {
clearInterval(interval);
};
}, [dispatch]);
if (policy === null || policy.getIn(['summary', 'pending_notifications_count']) * 1 === 0) {
return null;
}
return (
<Link className='filtered-notifications-banner' to='/notifications/requests'>
<Icon icon={ArchiveIcon} />
<div className='filtered-notifications-banner__text'>
<strong><FormattedMessage id='filtered_notifications_banner.title' defaultMessage='Filtered notifications' /></strong>
<span><FormattedMessage id='filtered_notifications_banner.pending_requests' defaultMessage='Notifications from {count, plural, =0 {no one} one {one person} other {# people}} you may know' values={{ count: policy.getIn(['summary', 'pending_requests_count']) }} /></span>
</div>
<div className='filtered-notifications-banner__badge'>
{toCappedNumber(policy.getIn(['summary', 'pending_notifications_count']))}
</div>
</Link>
);
};

@ -0,0 +1,65 @@
import PropTypes from 'prop-types';
import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import DoneIcon from '@/material-icons/400-24px/done.svg?react';
import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
import { acceptNotificationRequest, dismissNotificationRequest } from 'flavours/glitch/actions/notifications';
import { Avatar } from 'flavours/glitch/components/avatar';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { makeGetAccount } from 'flavours/glitch/selectors';
import { toCappedNumber } from 'flavours/glitch/utils/numbers';
const getAccount = makeGetAccount();
const messages = defineMessages({
accept: { id: 'notification_requests.accept', defaultMessage: 'Accept' },
dismiss: { id: 'notification_requests.dismiss', defaultMessage: 'Dismiss' },
});
export const NotificationRequest = ({ id, accountId, notificationsCount }) => {
const dispatch = useDispatch();
const account = useSelector(state => getAccount(state, accountId));
const intl = useIntl();
const handleDismiss = useCallback(() => {
dispatch(dismissNotificationRequest(id));
}, [dispatch, id]);
const handleAccept = useCallback(() => {
dispatch(acceptNotificationRequest(id));
}, [dispatch, id]);
return (
<div className='notification-request'>
<Link to={`/notifications/requests/${id}`} className='notification-request__link'>
<Avatar account={account} size={36} />
<div className='notification-request__name'>
<div className='notification-request__name__display-name'>
<bdi><strong dangerouslySetInnerHTML={{ __html: account?.get('display_name_html') }} /></bdi>
<span className='filtered-notifications-banner__badge'>{toCappedNumber(notificationsCount)}</span>
</div>
<span>@{account?.get('acct')}</span>
</div>
</Link>
<div className='notification-request__actions'>
<IconButton iconComponent={VolumeOffIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
<IconButton iconComponent={DoneIcon} onClick={handleAccept} title={intl.formatMessage(messages.accept)} />
</div>
</div>
);
};
NotificationRequest.propTypes = {
id: PropTypes.string.isRequired,
accountId: PropTypes.string.isRequired,
notificationsCount: PropTypes.string.isRequired,
};

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save