1
0
mirror of https://github.com/funamitech/mastodon synced 2024-12-02 16:58:53 +09:00
This commit is contained in:
Noa Himesaka 2024-09-28 23:23:16 +09:00
commit 262ddc00f4
1480 changed files with 18732 additions and 13580 deletions

View File

@ -1,6 +1,7 @@
[production] [production]
defaults defaults
> 0.2% > 0.2%
firefox >= 78
ios >= 15.6 ios >= 15.6
not dead not dead
not OperaMini all not OperaMini all

View File

@ -11,5 +11,8 @@ RUN apt-get update && \
export DEBIAN_FRONTEND=noninteractive && \ export DEBIAN_FRONTEND=noninteractive && \
apt-get -y install --no-install-recommends libicu-dev libidn11-dev ffmpeg imagemagick libvips42 libpam-dev apt-get -y install --no-install-recommends libicu-dev libidn11-dev ffmpeg imagemagick libvips42 libpam-dev
# Disable download prompt for Corepack
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
# Move welcome message to where VS Code expects it # Move welcome message to where VS Code expects it
COPY .devcontainer/welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt COPY .devcontainer/welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt

View File

@ -39,7 +39,7 @@
}, },
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
"postCreateCommand": "COREPACK_ENABLE_DOWNLOAD_PROMPT=0 bin/setup", "postCreateCommand": "bin/setup",
"waitFor": "postCreateCommand", "waitFor": "postCreateCommand",
"customizations": { "customizations": {

View File

@ -69,7 +69,7 @@ services:
hard: -1 hard: -1
libretranslate: libretranslate:
image: libretranslate/libretranslate:v1.5.7 image: libretranslate/libretranslate:v1.6.1
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- lt-data:/home/libretranslate/.local - lt-data:/home/libretranslate/.local

View File

@ -333,7 +333,7 @@ module.exports = defineConfig({
], ],
parserOptions: { parserOptions: {
project: true, projectService: true,
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
}, },

2
.github/codecov.yml vendored
View File

@ -9,3 +9,5 @@ coverage:
default: default:
# GitHub status check is not blocking # GitHub status check is not blocking
informational: true informational: true
github_checks:
annotations: false

View File

@ -7,6 +7,7 @@
':prConcurrentLimitNone', // Remove limit for open PRs at any time. ':prConcurrentLimitNone', // Remove limit for open PRs at any time.
':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour. ':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour.
], ],
rebaseWhen: 'conflicted',
minimumReleaseAge: '3', // Wait 3 days after the package has been published before upgrading it minimumReleaseAge: '3', // Wait 3 days after the package has been published before upgrading it
// packageRules order is important, they are applied from top to bottom and are merged, // packageRules order is important, they are applied from top to bottom and are merged,
// meaning the most important ones must be at the bottom, for example grouping rules // meaning the most important ones must be at the bottom, for example grouping rules

View File

@ -42,11 +42,24 @@ jobs:
with: with:
onlyProduction: 'true' onlyProduction: 'true'
- name: Cache assets from compilation
uses: actions/cache@v4
with:
path: |
public/assets
public/packs
public/packs-test
tmp/cache/webpacker
key: ${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
restore-keys: |
${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}
${{ matrix.mode }}-assets-main
${{ matrix.mode }}-assets
- name: Precompile assets - name: Precompile assets
# Previously had set this, but it's not supported
# export NODE_OPTIONS=--openssl-legacy-provider
run: |- run: |-
./bin/rails assets:precompile bin/rails assets:precompile
- name: Archive asset artifacts - name: Archive asset artifacts
run: | run: |
@ -137,6 +150,19 @@ jobs:
bin/rails db:setup bin/rails db:setup
bin/flatware fan bin/rails db:test:prepare bin/flatware fan bin/rails db:test:prepare
- name: Cache RSpec persistence file
uses: actions/cache@v4
with:
path: |
tmp/rspec/examples.txt
key: rspec-persistence-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
restore-keys: |
rspec-persistence-${{ github.head_ref || github.ref_name }}-${{ github.sha }}-${{ matrix.ruby-version }}
rspec-persistence-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
rspec-persistence-${{ github.head_ref || github.ref_name }}
rspec-persistence-main
rspec-persistence
- run: bin/flatware rspec -r ./spec/flatware_helper.rb - run: bin/flatware rspec -r ./spec/flatware_helper.rb
- name: Upload coverage reports to Codecov - name: Upload coverage reports to Codecov

3
.gitignore vendored
View File

@ -71,3 +71,6 @@ docker-compose.override.yml
# Ignore dotenv .local files # Ignore dotenv .local files
.env*.local .env*.local
# Ignore local-only rspec configuration
.rspec-local

View File

@ -1 +0,0 @@
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/app/.apt/lib/x86_64-linux-gnu:/app/.apt/usr/lib/x86_64-linux-gnu/mesa:/app/.apt/usr/lib/x86_64-linux-gnu/pulseaudio:/app/.apt/usr/lib/x86_64-linux-gnu/openblas-pthread

1
.rspec
View File

@ -1,3 +1,2 @@
--color --color
--require spec_helper --require spec_helper
--format Fuubar

View File

@ -7,8 +7,13 @@ RSpec/Focus: # Require full spec run on CI
Exclude: [] Exclude: []
Rails/Output: # Remove any `puts` debugging Rails/Output: # Remove any `puts` debugging
inherit_mode:
merge:
- Include
Enabled: true Enabled: true
Exclude: [] Exclude: []
Include:
- spec/**/*.rb
Rails/FindEach: # Using `each` could impact performance, use `find_each` Rails/FindEach: # Using `each` could impact performance, use `find_each`
Enabled: true Enabled: true

View File

@ -1,6 +1,6 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp` # `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.65.0. # using RuboCop version 1.66.1.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
@ -35,7 +35,6 @@ Rails/OutputSafety:
# Configuration parameters: AllowedVars. # Configuration parameters: AllowedVars.
Style/FetchEnvVar: Style/FetchEnvVar:
Exclude: Exclude:
- 'app/lib/redis_configuration.rb'
- 'app/lib/translation_service.rb' - 'app/lib/translation_service.rb'
- 'config/environments/production.rb' - 'config/environments/production.rb'
- 'config/initializers/2_limited_federation_mode.rb' - 'config/initializers/2_limited_federation_mode.rb'
@ -44,7 +43,6 @@ Style/FetchEnvVar:
- 'config/initializers/devise.rb' - 'config/initializers/devise.rb'
- 'config/initializers/paperclip.rb' - 'config/initializers/paperclip.rb'
- 'config/initializers/vapid.rb' - 'config/initializers/vapid.rb'
- 'lib/mastodon/redis_config.rb'
- 'lib/tasks/repo.rake' - 'lib/tasks/repo.rake'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
@ -93,7 +91,6 @@ Style/OptionalBooleanParameter:
- 'app/services/fetch_resource_service.rb' - 'app/services/fetch_resource_service.rb'
- 'app/workers/domain_block_worker.rb' - 'app/workers/domain_block_worker.rb'
- 'app/workers/unfollow_follow_worker.rb' - 'app/workers/unfollow_follow_worker.rb'
- 'lib/mastodon/redis_config.rb'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.

View File

@ -1 +1 @@
3.3.4 3.3.5

10
Aptfile
View File

@ -1,5 +1,5 @@
ffmpeg libidn12
libopenblas0-pthread # for idn-ruby on heroku-24 stack
libpq-dev
libxdamage1 # use https://github.com/heroku/heroku-buildpack-activestorage-preview
libxfixes3 # in place for ffmpeg and its dependent packages to reduce slag size

View File

@ -10,21 +10,24 @@ The following changelog entries focus on changes visible to users, administrator
- **Add confirmation interstitial instead of silently redirecting logged-out visitors to remote resources** (#27792, #28902, and #30651 by @ClearlyClaire and @Gargron)\ - **Add confirmation interstitial instead of silently redirecting logged-out visitors to remote resources** (#27792, #28902, and #30651 by @ClearlyClaire and @Gargron)\
This fixes a longstanding open redirect in Mastodon, at the cost of added friction when local links to remote resources are shared. This fixes a longstanding open redirect in Mastodon, at the cost of added friction when local links to remote resources are shared.
- Change `form-action` Content-Security-Policy directive to be more restrictive (#26897 by @ClearlyClaire)
- Update dependencies
### Added ### Added
- **Add experimental server-side notification grouping** (#29889, #30576, #30685, #30688, #30707, #30776, #30779, #30781, #30440, #31062, #31098, #31076, #31111, #31123, #31223, #31214, #31224, #31299, #31325, #31347, #31304, #31326, #31384, #31403, #31433, #31509, #31486, and #31513 by @ClearlyClaire, @mgmn, and @renchap)\ - **Add server-side notification grouping** (#29889, #30576, #30685, #30688, #30707, #30776, #30779, #30781, #30440, #31062, #31098, #31076, #31111, #31123, #31223, #31214, #31224, #31299, #31325, #31347, #31304, #31326, #31384, #31403, #31433, #31509, #31486, #31513, #31592, #31594, #31638, #31746, #31652, #31709, #31725, #31745, #31613, #31657, #31840, #31610 and #31929 by @ClearlyClaire, @Gargron, @mgmn, and @renchap)\
Group notifications of the same type for the same target, so that your notifications no longer get cluttered by boost and favorite notifications as soon as a couple of your posts get traction.\ Group notifications of the same type for the same target, so that your notifications no longer get cluttered by boost and favorite notifications as soon as a couple of your posts get traction.\
This is done server-side so that clients can efficiently get relevant groups without having to go through numerous pages of individual notifications.\ This is done server-side so that clients can efficiently get relevant groups without having to go through numerous pages of individual notifications.\
As part of this, the visual design of the entire notifications feature has been revamped.\ As part of this, the visual design of the entire notifications feature has been revamped.\
This feature is intended to eventually replace the existing notifications column, but for this first beta, users will have to enable it in the “Experimental features” section of the notifications column settings.\ This feature is intended to eventually replace the existing notifications column, but for this first beta, users will have to enable it in the “Experimental features” section of the notifications column settings.\
The API is not final yet, but it consists of: The API is not final yet, but it consists of:
- a new `group_key` attribute to `Notification` entities - a new `group_key` attribute to `Notification` entities
- `GET /api/v2_alpha/notifications`: https://docs.joinmastodon.org/methods/notifications_alpha/#get-grouped - `GET /api/v2/notifications`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-grouped
- `GET /api/v2_alpha/notifications/:group_key`: https://docs.joinmastodon.org/methods/notifications_alpha/#get-notification-group - `GET /api/v2/notifications/:group_key`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-notification-group
- `POST /api/v2_alpha/notifications/:group_key/dimsiss`: https://docs.joinmastodon.org/methods/notifications_alpha/#dismiss-group - `GET /api/v2/notifications/:group_key/accounts`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-group-accounts
- `GET /api/v2_alpha/notifications/:unread_count`: https://docs.joinmastodon.org/methods/notifications_alpha/#unread-group-count - `POST /api/v2/notifications/:group_key/dimsiss`: https://docs.joinmastodon.org/methods/grouped_notifications/#dismiss-group
- **Add notification policies, filtered notifications and notification requests** (#29366, #29529, #29433, #29565, #29567, #29572, #29575, #29588, #29646, #29652, #29658, #29666, #29693, #29699, #29737, #29706, #29570, #29752, #29810, #29826, #30114, #30251, #30559, #29868, #31008, #31011, #30996, #31149, #31220, #31222, #31225, #31242, #31262, #31250, #31273, #31310, #31316, #31322, #31329, #31324, #31331, #31343, #31342, #31309, #31358, #31378, #31406, #31256, #31456, #31419, #31457, #31508, #31540, and #31541 by @ClearlyClaire, @Gargron, @TheEssem, @mgmn, @oneiros, and @renchap)\ - `GET /api/v2/notifications/:unread_count`: https://docs.joinmastodon.org/methods/grouped_notifications/#unread-group-count
- **Add notification policies, filtered notifications and notification requests** (#29366, #29529, #29433, #29565, #29567, #29572, #29575, #29588, #29646, #29652, #29658, #29666, #29693, #29699, #29737, #29706, #29570, #29752, #29810, #29826, #30114, #30251, #30559, #29868, #31008, #31011, #30996, #31149, #31220, #31222, #31225, #31242, #31262, #31250, #31273, #31310, #31316, #31322, #31329, #31324, #31331, #31343, #31342, #31309, #31358, #31378, #31406, #31256, #31456, #31419, #31457, #31508, #31540, #31541, and #31723 by @ClearlyClaire, @Gargron, @TheEssem, @mgmn, @oneiros, and @renchap)\
The old “Block notifications from non-followers”, “Block notifications from people you don't follow” and “Block direct messages from people you don't follow” notification settings have been replaced by a new set of settings found directly in the notification column.\ The old “Block notifications from non-followers”, “Block notifications from people you don't follow” and “Block direct messages from people you don't follow” notification settings have been replaced by a new set of settings found directly in the notification column.\
You can now separately filter or drop notifications from people you don't follow, people who don't follow you, accounts created within the past 30 days, as well as unsolicited private mentions, and accounts limited by the moderation.\ You can now separately filter or drop notifications from people you don't follow, people who don't follow you, accounts created within the past 30 days, as well as unsolicited private mentions, and accounts limited by the moderation.\
Instead of being outright dropped, notifications that you chose to filter are put in a separate “Filtered notifications” box that you can review separately without it clogging your main notifications.\ Instead of being outright dropped, notifications that you chose to filter are put in a separate “Filtered notifications” box that you can review separately without it clogging your main notifications.\
@ -57,19 +60,22 @@ The following changelog entries focus on changes visible to users, administrator
- **Add timeline of public posts about a trending link** (#30381 and #30840 by @Gargron)\ - **Add timeline of public posts about a trending link** (#30381 and #30840 by @Gargron)\
You can now see public posts mentioning currently-trending articles from people who have opted into discovery features.\ You can now see public posts mentioning currently-trending articles from people who have opted into discovery features.\
This adds a new REST API endpoint: https://docs.joinmastodon.org/methods/timelines/#link This adds a new REST API endpoint: https://docs.joinmastodon.org/methods/timelines/#link
- **Add author highlight for news articles whose authors are on the fediverse** (#30398, #30670, #30521, and #30846 by @Gargron)\ - **Add author highlight for news articles whose authors are on the fediverse** (#30398, #30670, #30521, #30846, #31819, and #31900 by @Gargron and @oneiros)\
This adds a mechanism to [highlight the author of news articles](https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/) shared on Mastodon.\ This adds a mechanism to [highlight the author of news articles](https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/) shared on Mastodon.\
Articles hosted outside the fediverse can indicate a fediverse author with a meta tag: Articles hosted outside the fediverse can indicate a fediverse author with a meta tag:
```html ```html
<meta name="fediverse:creator" content="username@domain" /> <meta name="fediverse:creator" content="username@domain" />
``` ```
On the API side, this is represented by a new `authors` attribute to the `PreviewCard` entity: https://docs.joinmastodon.org/entities/PreviewCard/#authors\ On the API side, this is represented by a new `authors` attribute to the `PreviewCard` entity: https://docs.joinmastodon.org/entities/PreviewCard/#authors\
Note that this feature is still work in progress and the tagging format and verification mechanisms may change in future releases. Users can allow arbitrary domains to use `fediverse:creator` to credit them by visiting `/settings/verification`.\
This is federated as a new `attributionDomains` property in the `http://joinmastodon.org/ns` namespace, containing an array of domain names: https://docs.joinmastodon.org/spec/activitypub/#properties-used-1
- **Add in-app notifications for moderation actions and warnings** (#30065, #30082, and #30081 by @ClearlyClaire)\ - **Add in-app notifications for moderation actions and warnings** (#30065, #30082, and #30081 by @ClearlyClaire)\
In addition to email notifications, also notify users of moderation actions or warnings against them directly within the app, so they are less likely to miss important communication from their moderators.\ In addition to email notifications, also notify users of moderation actions or warnings against them directly within the app, so they are less likely to miss important communication from their moderators.\
This adds the `moderation_warning` notification type to the REST API and streaming, with a new [`moderation_warning` attribute](https://docs.joinmastodon.org/entities/Notification/#moderation_warning). This adds the `moderation_warning` notification type to the REST API and streaming, with a new [`moderation_warning` attribute](https://docs.joinmastodon.org/entities/Notification/#moderation_warning).
- **Add domain information to profiles in web UI** (#29602 by @Gargron)\ - **Add domain information to profiles in web UI** (#29602 by @Gargron)\
Clicking the domain of a user in their profile will now open a tooltip with a short explanation about servers and federation. Clicking the domain of a user in their profile will now open a tooltip with a short explanation about servers and federation.
- **Add support for Redis sentinel** (#31694, #31623, #31744, #31767, and #31768 by @ThisIsMissEm and @oneiros)\
See https://docs.joinmastodon.org/admin/scaling/#redis-sentinel
- Add ability to reorder uploaded media before posting in web UI (#28456 by @Gargron) - Add ability to reorder uploaded media before posting in web UI (#28456 by @Gargron)
- Add moderation interface for searching hashtags (#30880 by @ThisIsMissEm) - Add moderation interface for searching hashtags (#30880 by @ThisIsMissEm)
- Add ability for admins to configure instance favicon and logo (#30040, #30208, #30259, #30375, #30734, #31016, and #30205 by @ClearlyClaire, @FawazFarid, @JasonPunyon, @mgmn, and @renchap)\ - Add ability for admins to configure instance favicon and logo (#30040, #30208, #30259, #30375, #30734, #31016, and #30205 by @ClearlyClaire, @FawazFarid, @JasonPunyon, @mgmn, and @renchap)\
@ -77,6 +83,8 @@ The following changelog entries focus on changes visible to users, administrator
- Add `api_versions` to `/api/v2/instance` (#31354 by @ClearlyClaire)\ - Add `api_versions` to `/api/v2/instance` (#31354 by @ClearlyClaire)\
Add API version number to make it easier for clients to detect compatible features going forward.\ Add API version number to make it easier for clients to detect compatible features going forward.\
See API documentation at https://docs.joinmastodon.org/entities/Instance/#api-versions See API documentation at https://docs.joinmastodon.org/entities/Instance/#api-versions
- Add quick links to Administration and Moderation Reports from Web UI (#24838 by @ThisIsMissEm)
- Add link to `/admin/roles` in moderation interface when changing someone's role (#31791 by @ClearlyClaire)
- Add recent audit log entries in federation moderation interface (#27386 by @ThisIsMissEm) - Add recent audit log entries in federation moderation interface (#27386 by @ThisIsMissEm)
- Add profile setup to onboarding in web UI (#27829, #27876, and #28453 by @Gargron) - Add profile setup to onboarding in web UI (#27829, #27876, and #28453 by @Gargron)
- Add prominent share/copy button on profiles in web UI (#27865 and #27889 by @ClearlyClaire and @Gargron) - Add prominent share/copy button on profiles in web UI (#27865 and #27889 by @ClearlyClaire and @Gargron)
@ -113,21 +121,24 @@ The following changelog entries focus on changes visible to users, administrator
- Add support for multiple `redirect_uris` when creating OAuth 2.0 Applications (#29192 by @ThisIsMissEm) - Add support for multiple `redirect_uris` when creating OAuth 2.0 Applications (#29192 by @ThisIsMissEm)
- Add Interlingue and Interlingua to interface languages (#28630 and #30828 by @Dhghomon and @renchap) - Add Interlingue and Interlingua to interface languages (#28630 and #30828 by @Dhghomon and @renchap)
- Add Kashubian, Pennsylvania Dutch, Vai, Jawi Malay, Mohawk and Low German to posting languages (#26024, #26634, #27136, #29098, #27115, and #27434 by @EngineerDali, @HelgeKrueger, and @gunchleoc) - Add Kashubian, Pennsylvania Dutch, Vai, Jawi Malay, Mohawk and Low German to posting languages (#26024, #26634, #27136, #29098, #27115, and #27434 by @EngineerDali, @HelgeKrueger, and @gunchleoc)
- Add validations to `Web::PushSubscription` (#30540 and #30542 by @ThisIsMissEm)
- Add option to use native Ruby driver for Redis through `REDIS_DRIVER=ruby` (#30717 by @vmstan) - Add option to use native Ruby driver for Redis through `REDIS_DRIVER=ruby` (#30717 by @vmstan)
- Add support for libvips in addition to ImageMagick (#30090, #30590, #30597, #30632, #30857, #30869, and #30858 by @ClearlyClaire, @Gargron, and @mjankowski)\ - Add support for libvips in addition to ImageMagick (#30090, #30590, #30597, #30632, #30857, #30869, and #30858 by @ClearlyClaire, @Gargron, and @mjankowski)\
Server admins can now use libvips as a faster and lighter alternative to ImageMagick for processing user-uploaded images.\ Server admins can now use libvips as a faster and lighter alternative to ImageMagick for processing user-uploaded images.\
This requires libvips 8.13 or newer, and needs to be enabled with `MASTODON_USE_LIBVIPS=true`.\ This requires libvips 8.13 or newer, and needs to be enabled with `MASTODON_USE_LIBVIPS=true`.\
This is enabled by default in the official Docker images, and is intended to completely replace ImageMagick in the future. This is enabled by default in the official Docker images, and is intended to completely replace ImageMagick in the future.
- Add validations to `Web::PushSubscription` (#30540 and #30542 by @ThisIsMissEm)
- Add anchors to each authorized application in `/oauth/authorized_applications` (#31677 by @fowl2)
- Add active animation to header settings button (#30221, #30307, and #30388 by @daudix) - Add active animation to header settings button (#30221, #30307, and #30388 by @daudix)
- Add OpenTelemetry instrumentation (#30130, #30322, #30353, and #30350 by @julianocosta89, @renchap, and @robbkidd)\ - Add OpenTelemetry instrumentation (#30130, #30322, #30353, and #30350 by @julianocosta89, @renchap, and @robbkidd)\
See https://docs.joinmastodon.org/admin/config/#otel for documentation See https://docs.joinmastodon.org/admin/config/#otel for documentation
- Add API to get multiple accounts and statuses (#27871 and #30465 by @ClearlyClaire)\ - Add API to get multiple accounts and statuses (#27871 and #30465 by @ClearlyClaire)\
This adds `GET /api/v1/accounts` and `GET /api/v1/statuses` to the REST API, see https://docs.joinmastodon.org/methods/accounts/#index and https://docs.joinmastodon.org/methods/statuses/#index This adds `GET /api/v1/accounts` and `GET /api/v1/statuses` to the REST API, see https://docs.joinmastodon.org/methods/accounts/#index and https://docs.joinmastodon.org/methods/statuses/#index
- Add support for CORS to `POST /oauth/revoke` (#31743 by @ClearlyClaire)
- Add redirection back to previous page after site upload deletion (#30141 by @FawazFarid) - Add redirection back to previous page after site upload deletion (#30141 by @FawazFarid)
- Add RFC8414 OAuth 2.0 server metadata (#29191 by @ThisIsMissEm) - Add RFC8414 OAuth 2.0 server metadata (#29191 by @ThisIsMissEm)
- Add loading indicator and empty result message to advanced interface search (#30085 by @ClearlyClaire) - Add loading indicator and empty result message to advanced interface search (#30085 by @ClearlyClaire)
- Add `profile` OAuth 2.0 scope, allowing more limited access to user data (#29087 and #30357 by @ThisIsMissEm) - Add `profile` OAuth 2.0 scope, allowing more limited access to user data (#29087 and #30357 by @ThisIsMissEm)
- Add global Regexp timeout (#31928 by @ClearlyClaire)
- Add the role ID to the badge component (#29707 by @renchap) - Add the role ID to the badge component (#29707 by @renchap)
- Add diagnostic message for failure during CLI search deploy (#29462 by @mjankowski) - Add diagnostic message for failure during CLI search deploy (#29462 by @mjankowski)
- Add pagination `Link` headers on API accounts/statuses when pinned true (#29442 by @mjankowski) - Add pagination `Link` headers on API accounts/statuses when pinned true (#29442 by @mjankowski)
@ -156,34 +167,43 @@ The following changelog entries focus on changes visible to users, administrator
- **Change icons throughout the web interface** (#27385, #27539, #27555, #27579, #27700, #27817, #28519, #28709, #28064, #28775, #28780, #27924, #29294, #29395, #29537, #29569, #29610, #29612, #29649, #29844, #27780, #30974, #30963, #30962, #30961, #31362, #31363, #31359, #31371, #31360, #31512, #31511, and #31525 by @ClearlyClaire, @Gargron, @arbolitoloco1, @mjankowski, @nclm, @renchap, @ronilaukkarinen, and @zunda)\ - **Change icons throughout the web interface** (#27385, #27539, #27555, #27579, #27700, #27817, #28519, #28709, #28064, #28775, #28780, #27924, #29294, #29395, #29537, #29569, #29610, #29612, #29649, #29844, #27780, #30974, #30963, #30962, #30961, #31362, #31363, #31359, #31371, #31360, #31512, #31511, and #31525 by @ClearlyClaire, @Gargron, @arbolitoloco1, @mjankowski, @nclm, @renchap, @ronilaukkarinen, and @zunda)\
This changes all the interface icons from FontAwesome to Material Symbols for a more modern look, consistent with the official Mastodon Android app.\ This changes all the interface icons from FontAwesome to Material Symbols for a more modern look, consistent with the official Mastodon Android app.\
In addition, better care is given to pixel alignment, and icon variants are used to better highlight active/inactive state. In addition, better care is given to pixel alignment, and icon variants are used to better highlight active/inactive state.
- **Change design of compose form in web UI** (#28119, #29059, #29248, #29372, #29384, #29417, #29456, #29406, #29651, and #29659 by @ClearlyClaire, @Gargron, @eai04191, @hinaloe, and @ronilaukkarinen)\ - **Change design of compose form in web UI** (#28119, #29059, #29248, #29372, #29384, #29417, #29456, #29406, #29651, #29659, and #31889 by @ClearlyClaire, @Gargron, @eai04191, @hinaloe, and @ronilaukkarinen)\
The compose form has been completely redesigned for a more modern and consistent look, as well as spelling out the chosen privacy setting and language name at all times.\ The compose form has been completely redesigned for a more modern and consistent look, as well as spelling out the chosen privacy setting and language name at all times.\
As part of this, the “Unlisted” privacy setting has been renamed to “Quiet public”. As part of this, the “Unlisted” privacy setting has been renamed to “Quiet public”.
- **Change design of confirmation modals in the web UI** (#29576, #29614, #29640, #29644, #30131, #30884, and #31399 by @ClearlyClaire, @Gargron, and @tribela)\ - **Change design of modals in the web UI** (#29576, #29614, #29640, #29644, #30131, #30884, #31399, #31555, #31752, #31801, #31883, #31844, #31864, and #31943 by @ClearlyClaire, @Gargron, @tribela and @vmstan)\
The mute, block, and domain block confirmation modals have been completely redesigned to be clearer and include more detailed information on the action to be performed.\ The mute, block, and domain block confirmation modals have been completely redesigned to be clearer and include more detailed information on the action to be performed.\
They also have a more modern and consistent design, along with other confirmation modals in the application. They also have a more modern and consistent design, along with other confirmation modals in the application.
- **Change colors throughout the web UI** (#29522, #29584, #29653, #29779, #29803, #29809, #29808, #29828, #31034, #31168, #31266, #31348, #31349, #31361, and #31510 by @ClearlyClaire, @Gargron, @renchap, and @vmstan) - **Change colors throughout the web UI** (#29522, #29584, #29653, #29779, #29803, #29809, #29808, #29828, #31034, #31168, #31266, #31348, #31349, #31361, and #31510 by @ClearlyClaire, @Gargron, @renchap, and @vmstan)
- **Change onboarding prompt to follow suggestions carousel in web UI** (#28878 and #29272 by @Gargron) - **Change onboarding prompt to follow suggestions carousel in web UI** (#28878, #29272, and #31912 by @Gargron)
- **Change email templates** (#28416, #28755, #28814, #29064, #28883, #29470, #29607, #29761, #29760, and #29879 by @ClearlyClaire, @Gargron, @hteumeuleu, and @mjankowski)\ - **Change email templates** (#28416, #28755, #28814, #29064, #28883, #29470, #29607, #29761, #29760, and #29879 by @ClearlyClaire, @Gargron, @hteumeuleu, and @mjankowski)\
All emails to end-users have been completely redesigned with a fresh new look, providing more information while making them easier to reand and keeping maximum compatibility across mail clients. All emails to end-users have been completely redesigned with a fresh new look, providing more information while making them easier to read and keeping maximum compatibility across mail clients.
- **Change follow recommendations algorithm** (#28314, #28433, #29017, #29108, #29306, #29550, #29619, and #31474 by @ClearlyClaire, @Gargron, @kernal053, @mjankowski, and @wheatear-dev)\ - **Change follow recommendations algorithm** (#28314, #28433, #29017, #29108, #29306, #29550, #29619, and #31474 by @ClearlyClaire, @Gargron, @kernal053, @mjankowski, and @wheatear-dev)\
This replaces the “past interactions” recommendation algorithm with a “friends of friends” algorithm that suggests accounts followed by people you follow, and a “similar profiles” algorithm that suggests accounts with a profile similar to your most recent follows.\ This replaces the “past interactions” recommendation algorithm with a “friends of friends” algorithm that suggests accounts followed by people you follow, and a “similar profiles” algorithm that suggests accounts with a profile similar to your most recent follows.\
In addition, the implementation has been significantly reworked, and all follow recommendations are now dismissable.\ In addition, the implementation has been significantly reworked, and all follow recommendations are now dismissable.\
This change deprecates the `source` attribute in `Suggestion` entities in the REST API, and replaces it with the new [`sources` attribute](https://docs.joinmastodon.org/entities/Suggestion/#sources). This change deprecates the `source` attribute in `Suggestion` entities in the REST API, and replaces it with the new [`sources` attribute](https://docs.joinmastodon.org/entities/Suggestion/#sources).
- Change account search algorithm (#30803 by @Gargron) - Change account search algorithm (#30803 by @Gargron)
- **Change streaming server to use its own dependencies and its own docker image** (#24702, #27967, #26850, #28112, #28115, #28137, #28138, #28497, #28548, and #30795 by @TheEssem, @ThisIsMissEm, @jippi, @timetinytim, and @vmstan)\ - **Change streaming server to use its own dependencies and its own docker image** (#24702, #27967, #26850, #28112, #28115, #28137, #28138, #28497, #28548, #30795, #31612, and #31615 by @TheEssem, @ThisIsMissEm, @jippi, @renchap, @timetinytim, and @vmstan)\
In order to reduce the amount of runtime dependencies, the streaming server has been moved into a separate package and Docker image.\ In order to reduce the amount of runtime dependencies, the streaming server has been moved into a separate package and Docker image.\
The `mastodon` image does not contain the streaming server anymore, as it has been moved to its own `mastodon-streaming` image.\ The `mastodon` image does not contain the streaming server anymore, as it has been moved to its own `mastodon-streaming` image.\
Administrators may need to update their setup accordingly. Administrators may need to update their setup accordingly.
- Change how content warnings and filters are displayed in web UI (#31365 by @Gargron) - Change how content warnings and filters are displayed in web UI (#31365, and #31761 by @Gargron)
- Change preview card processing to ignore `undefined` as canonical url (#31882 by @oneiros)
- Change embedded posts to use web UI (#31766 by @Gargron)
- Change inner borders in media galleries in web UI (#31852 by @Gargron)
- Change design of hide media button in web UI (#31807 by @Gargron)
- Change labels on thread indicators in web UI (#31806 by @Gargron)
- Change report action buttons to be disabled when action has already been taken (#31773, #31822, and #31899 by @ClearlyClaire and @ThisIsMissEm)
- Change width of columns in advanced web UI (#31762 by @Gargron)
- Change design of unread conversations in web UI (#31763 by @Gargron)
- Change Web UI to allow viewing and severing relationships with suspended accounts (#27667 by @ClearlyClaire)\ - Change Web UI to allow viewing and severing relationships with suspended accounts (#27667 by @ClearlyClaire)\
This also adds a `with_suspended` parameter to `GET /api/v1/accounts/relationships` in the REST API. This also adds a `with_suspended` parameter to `GET /api/v1/accounts/relationships` in the REST API.
- Change preview card image size limit from 2MB to 8MB when using libvips (#31904 by @ClearlyClaire)
- Change avatars border radius (#31390 by @renchap) - Change avatars border radius (#31390 by @renchap)
- Change counters to be displayed on profile timelines in web UI (#30525 by @Gargron) - Change counters to be displayed on profile timelines in web UI (#30525 by @Gargron)
- Change disabled buttons color in light mode to make the difference more visible (#30998 by @renchap) - Change disabled buttons color in light mode to make the difference more visible (#30998 by @renchap)
- Change design of people tab on explore in web UI (#30059 by @Gargron) - Change design of people tab on explore in web UI (#30059 by @Gargron)
- Change sidebar text in web UI (#30696 by @Gargron) - Change sidebar text in web UI (#30696 by @Gargron)
- Change "Follow" to "Follow back" and "Mutual" when appropriate in web UI (#28452 and #28465 by @Gargron and @renchap) - Change "Follow" to "Follow back" and "Mutual" when appropriate in web UI (#28452, #28465, and #31934 by @ClearlyClaire, @Gargron and @renchap)
- Change media to be hidden/blurred by default in report modal (#28522 by @ClearlyClaire) - Change media to be hidden/blurred by default in report modal (#28522 by @ClearlyClaire)
- Change order of the "muting" and "blocking" list options in “Data Exports” (#26088 by @fixermark) - Change order of the "muting" and "blocking" list options in “Data Exports” (#26088 by @fixermark)
- Change admin and moderation notes character limit from 500 to 2000 characters (#30288 by @ThisIsMissEm) - Change admin and moderation notes character limit from 500 to 2000 characters (#30288 by @ThisIsMissEm)
@ -197,6 +217,7 @@ The following changelog entries focus on changes visible to users, administrator
- Change dropdown menu icon to not be replaced by close icon when open in web UI (#29532 by @Gargron) - Change dropdown menu icon to not be replaced by close icon when open in web UI (#29532 by @Gargron)
- Change back button to always appear in advanced web UI (#29551 and #29669 by @Gargron) - Change back button to always appear in advanced web UI (#29551 and #29669 by @Gargron)
- Change border of active compose field search inputs (#29832 and #29839 by @vmstan) - Change border of active compose field search inputs (#29832 and #29839 by @vmstan)
- Change instances of Nokogiri HTML4 parsing to HTML5 (#31812, #31815, #31813, and #31814 by @flavorjones)
- Change link detection to allow `@` at the end of an URL (#31124 by @adamniedzielski) - Change link detection to allow `@` at the end of an URL (#31124 by @adamniedzielski)
- Change User-Agent to use Mastodon as the product, and http.rb as platform details (#31192 by @ClearlyClaire) - Change User-Agent to use Mastodon as the product, and http.rb as platform details (#31192 by @ClearlyClaire)
- Change layout and wording of the Content Retention server settings page (#27733 by @vmstan) - Change layout and wording of the Content Retention server settings page (#27733 by @vmstan)
@ -249,8 +270,17 @@ The following changelog entries focus on changes visible to users, administrator
- Fix various issues when in link preview card generation (#28748, #30017, #30362, #30173, #30853, #30929, #30933, #30957, #30987, and #31144 by @adamniedzielski, @oneiros, @phocks, @timothyjrogers, and @tribela) - Fix various issues when in link preview card generation (#28748, #30017, #30362, #30173, #30853, #30929, #30933, #30957, #30987, and #31144 by @adamniedzielski, @oneiros, @phocks, @timothyjrogers, and @tribela)
- Fix handling of missing links in Webfinger responses (#31030 by @adamniedzielski) - Fix handling of missing links in Webfinger responses (#31030 by @adamniedzielski)
- Fix HTTP 500 error in `/api/v1/polls/:id/votes` when required `choices` parameter is missing (#25598 by @danielmbrasil) - Fix HTTP 500 error in `/api/v1/polls/:id/votes` when required `choices` parameter is missing (#25598 by @danielmbrasil)
- Fix security context sometimes not being added in LD-Signed activities (#31871 by @ClearlyClaire)
- Fix cross-origin loading of `inert.css` polyfill (#30687 by @louis77) - Fix cross-origin loading of `inert.css` polyfill (#30687 by @louis77)
- Fix cutoff of instance name in sign-up form (#30598 by @oneiros) - Fix cutoff of instance name in sign-up form (#30598 by @oneiros)
- Fix invalid date searches returning 503 errors (#31526 by @notchairmk)
- Fix invalid `visibility` values in `POST /api/v1/statuses` returning 500 errors (#31571 by @c960657)
- Fix some components re-rendering spuriously in web UI (#31879 and #31881 by @ClearlyClaire and @Gargron)
- Fix sort order of moderation notes on Reports and Accounts (#31528 by @ThisIsMissEm)
- Fix email language when recipient has no selected locale (#31747 by @ClearlyClaire)
- Fix frequently-used languages not correctly updating in the web UI (#31386 by @c960657)
- Fix `POST /api/v1/statuses` silently ignoring invalid `media_ids` parameter (#31681 by @c960657)
- Fix handling of the `BIND` environment variable in the streaming server (#31624 by @ThisIsMissEm)
- Fix empty `aria-hidden` attribute value in logo resources area (#30570 by @mjankowski) - Fix empty `aria-hidden` attribute value in logo resources area (#30570 by @mjankowski)
- Fix “Redirect URI” field not being marked as required in “New application” form (#30311 by @ThisIsMissEm) - Fix “Redirect URI” field not being marked as required in “New application” form (#30311 by @ThisIsMissEm)
- Fix right-to-left text in preview cards (#30930 by @ClearlyClaire) - Fix right-to-left text in preview cards (#30930 by @ClearlyClaire)

View File

@ -12,7 +12,7 @@ ARG BUILDPLATFORM=${BUILDPLATFORM}
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.x"] # Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.x"]
# renovate: datasource=docker depName=docker.io/ruby # renovate: datasource=docker depName=docker.io/ruby
ARG RUBY_VERSION="3.3.4" ARG RUBY_VERSION="3.3.5"
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] # # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
# renovate: datasource=node-version depName=node # renovate: datasource=node-version depName=node
ARG NODE_MAJOR_VERSION="20" ARG NODE_MAJOR_VERSION="20"

16
Gemfile
View File

@ -47,7 +47,6 @@ gem 'color_diff', '~> 0.1'
gem 'csv', '~> 3.2' gem 'csv', '~> 3.2'
gem 'discard', '~> 1.2' gem 'discard', '~> 1.2'
gem 'doorkeeper', '~> 5.6' gem 'doorkeeper', '~> 5.6'
gem 'ed25519', '~> 1.3'
gem 'fast_blank', '~> 1.0' gem 'fast_blank', '~> 1.0'
gem 'fastimage' gem 'fastimage'
gem 'hiredis', '~> 0.6' gem 'hiredis', '~> 0.6'
@ -99,10 +98,10 @@ gem 'json-ld'
gem 'json-ld-preloaded', '~> 3.2' gem 'json-ld-preloaded', '~> 3.2'
gem 'rdf-normalize', '~> 0.5' gem 'rdf-normalize', '~> 0.5'
gem 'opentelemetry-api', '~> 1.3.0' gem 'opentelemetry-api', '~> 1.4.0'
group :opentelemetry do group :opentelemetry do
gem 'opentelemetry-exporter-otlp', '~> 0.28.0', require: false gem 'opentelemetry-exporter-otlp', '~> 0.29.0', require: false
gem 'opentelemetry-instrumentation-active_job', '~> 0.7.1', require: false gem 'opentelemetry-instrumentation-active_job', '~> 0.7.1', require: false
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.20.1', require: false gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.20.1', require: false
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.21.2', require: false gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.21.2', require: false
@ -111,7 +110,7 @@ group :opentelemetry do
gem 'opentelemetry-instrumentation-http', '~> 0.23.2', require: false gem 'opentelemetry-instrumentation-http', '~> 0.23.2', require: false
gem 'opentelemetry-instrumentation-http_client', '~> 0.22.3', require: false gem 'opentelemetry-instrumentation-http_client', '~> 0.22.3', require: false
gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false
gem 'opentelemetry-instrumentation-pg', '~> 0.28.0', require: false gem 'opentelemetry-instrumentation-pg', '~> 0.29.0', require: false
gem 'opentelemetry-instrumentation-rack', '~> 0.24.1', require: false gem 'opentelemetry-instrumentation-rack', '~> 0.24.1', require: false
gem 'opentelemetry-instrumentation-rails', '~> 0.31.0', require: false gem 'opentelemetry-instrumentation-rails', '~> 0.31.0', require: false
gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false
@ -126,9 +125,6 @@ group :test do
# Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab # Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab
gem 'rspec-github', '~> 2.4', require: false gem 'rspec-github', '~> 2.4', require: false
# RSpec progress bar formatter
gem 'fuubar', '~> 2.5'
# RSpec helpers for email specs # RSpec helpers for email specs
gem 'email_spec' gem 'email_spec'
@ -149,11 +145,13 @@ group :test do
gem 'rails-controller-testing', '~> 1.0' gem 'rails-controller-testing', '~> 1.0'
# Validate schemas in specs # Validate schemas in specs
gem 'json-schema', '~> 4.0' gem 'json-schema', '~> 5.0'
# Test harness fo rack components # Test harness fo rack components
gem 'rack-test', '~> 2.1' gem 'rack-test', '~> 2.1'
gem 'shoulda-matchers'
# Coverage formatter for RSpec test if DISABLE_SIMPLECOV is false # Coverage formatter for RSpec test if DISABLE_SIMPLECOV is false
gem 'simplecov', '~> 0.22', require: false gem 'simplecov', '~> 0.22', require: false
gem 'simplecov-lcov', '~> 0.8', require: false gem 'simplecov-lcov', '~> 0.8', require: false
@ -210,7 +208,7 @@ group :development, :test do
gem 'test-prof' gem 'test-prof'
# RSpec runner for rails # RSpec runner for rails
gem 'rspec-rails', '~> 6.0' gem 'rspec-rails', '~> 7.0'
end end
group :production do group :production do

View File

@ -10,35 +10,35 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.1.3.4) actioncable (7.1.4)
actionpack (= 7.1.3.4) actionpack (= 7.1.4)
activesupport (= 7.1.3.4) activesupport (= 7.1.4)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (7.1.3.4) actionmailbox (7.1.4)
actionpack (= 7.1.3.4) actionpack (= 7.1.4)
activejob (= 7.1.3.4) activejob (= 7.1.4)
activerecord (= 7.1.3.4) activerecord (= 7.1.4)
activestorage (= 7.1.3.4) activestorage (= 7.1.4)
activesupport (= 7.1.3.4) activesupport (= 7.1.4)
mail (>= 2.7.1) mail (>= 2.7.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
actionmailer (7.1.3.4) actionmailer (7.1.4)
actionpack (= 7.1.3.4) actionpack (= 7.1.4)
actionview (= 7.1.3.4) actionview (= 7.1.4)
activejob (= 7.1.3.4) activejob (= 7.1.4)
activesupport (= 7.1.3.4) activesupport (= 7.1.4)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (7.1.3.4) actionpack (7.1.4)
actionview (= 7.1.3.4) actionview (= 7.1.4)
activesupport (= 7.1.3.4) activesupport (= 7.1.4)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
racc racc
rack (>= 2.2.4) rack (>= 2.2.4)
@ -46,15 +46,15 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
actiontext (7.1.3.4) actiontext (7.1.4)
actionpack (= 7.1.3.4) actionpack (= 7.1.4)
activerecord (= 7.1.3.4) activerecord (= 7.1.4)
activestorage (= 7.1.3.4) activestorage (= 7.1.4)
activesupport (= 7.1.3.4) activesupport (= 7.1.4)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.1.3.4) actionview (7.1.4)
activesupport (= 7.1.3.4) activesupport (= 7.1.4)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
@ -64,22 +64,22 @@ GEM
activemodel (>= 4.1) activemodel (>= 4.1)
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (7.1.3.4) activejob (7.1.4)
activesupport (= 7.1.3.4) activesupport (= 7.1.4)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.1.3.4) activemodel (7.1.4)
activesupport (= 7.1.3.4) activesupport (= 7.1.4)
activerecord (7.1.3.4) activerecord (7.1.4)
activemodel (= 7.1.3.4) activemodel (= 7.1.4)
activesupport (= 7.1.3.4) activesupport (= 7.1.4)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (7.1.3.4) activestorage (7.1.4)
actionpack (= 7.1.3.4) actionpack (= 7.1.4)
activejob (= 7.1.3.4) activejob (= 7.1.4)
activerecord (= 7.1.3.4) activerecord (= 7.1.4)
activesupport (= 7.1.3.4) activesupport (= 7.1.4)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (7.1.3.4) activesupport (7.1.4)
base64 base64
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
@ -100,20 +100,20 @@ GEM
attr_required (1.0.2) attr_required (1.0.2)
awrence (1.2.1) awrence (1.2.1)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
aws-partitions (1.966.0) aws-partitions (1.978.0)
aws-sdk-core (3.201.5) aws-sdk-core (3.209.0)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.9) aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.88.0) aws-sdk-kms (1.94.0)
aws-sdk-core (~> 3, >= 3.201.0) aws-sdk-core (~> 3, >= 3.207.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.159.0) aws-sdk-s3 (1.166.0)
aws-sdk-core (~> 3, >= 3.201.0) aws-sdk-core (~> 3, >= 3.207.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sigv4 (1.9.1) aws-sigv4 (1.10.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.3) azure-storage-blob (2.0.3)
azure-storage-common (~> 2.0) azure-storage-common (~> 2.0)
@ -137,14 +137,14 @@ GEM
blurhash (0.1.7) blurhash (0.1.7)
bootsnap (1.18.4) bootsnap (1.18.4)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (6.1.2) brakeman (6.2.1)
racc racc
browser (5.3.1) browser (5.3.1)
brpoplpush-redis_script (0.1.3) brpoplpush-redis_script (0.1.3)
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
redis (>= 1.0, < 6) redis (>= 1.0, < 6)
builder (3.3.0) builder (3.3.0)
bundler-audit (0.9.1) bundler-audit (0.9.2)
bundler (>= 1.2.0, < 3) bundler (>= 1.2.0, < 3)
thor (~> 1.0) thor (~> 1.0)
capybara (3.40.0) capybara (3.40.0)
@ -164,20 +164,22 @@ GEM
activesupport (>= 5.2) activesupport (>= 5.2)
elasticsearch (>= 7.14.0, < 8) elasticsearch (>= 7.14.0, < 8)
elasticsearch-dsl elasticsearch-dsl
childprocess (5.1.0)
logger (~> 1.5)
chunky_png (1.4.0) chunky_png (1.4.0)
climate_control (1.2.0) climate_control (1.2.0)
cocoon (1.2.15) cocoon (1.2.15)
color_diff (0.1) color_diff (0.1)
concurrent-ruby (1.3.4) concurrent-ruby (1.3.4)
connection_pool (2.4.1) connection_pool (2.4.1)
cose (1.3.0) cose (1.3.1)
cbor (~> 0.5.9) cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0) openssl-signature_algorithm (~> 1.0)
crack (1.0.0) crack (1.0.0)
bigdecimal bigdecimal
rexml rexml
crass (1.0.6) crass (1.0.6)
css_parser (1.17.1) css_parser (1.19.0)
addressable addressable
csv (3.3.0) csv (3.3.0)
database_cleaner-active_record (2.2.0) database_cleaner-active_record (2.2.0)
@ -195,7 +197,7 @@ GEM
railties (>= 4.1.0) railties (>= 4.1.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise-two-factor (5.1.0) devise-two-factor (6.0.0)
activesupport (~> 7.0) activesupport (~> 7.0)
devise (~> 4.0) devise (~> 4.0)
railties (~> 7.0) railties (~> 7.0)
@ -206,20 +208,20 @@ GEM
diff-lcs (1.5.1) diff-lcs (1.5.1)
discard (1.3.0) discard (1.3.0)
activerecord (>= 4.2, < 8) activerecord (>= 4.2, < 8)
docile (1.4.0) docile (1.4.1)
domain_name (0.6.20240107) domain_name (0.6.20240107)
doorkeeper (5.7.1) doorkeeper (5.7.1)
railties (>= 5) railties (>= 5)
dotenv (3.1.2) dotenv (3.1.4)
drb (2.2.1) drb (2.2.1)
ed25519 (1.3.0) elasticsearch (7.17.11)
elasticsearch (7.17.10) elasticsearch-api (= 7.17.11)
elasticsearch-api (= 7.17.10) elasticsearch-transport (= 7.17.11)
elasticsearch-transport (= 7.17.10) elasticsearch-api (7.17.11)
elasticsearch-api (7.17.10)
multi_json multi_json
elasticsearch-dsl (0.1.10) elasticsearch-dsl (0.1.10)
elasticsearch-transport (7.17.10) elasticsearch-transport (7.17.11)
base64
faraday (>= 1, < 3) faraday (>= 1, < 3)
multi_json multi_json
email_spec (2.3.0) email_spec (2.3.0)
@ -251,7 +253,7 @@ GEM
faraday-httpclient (1.0.1) faraday-httpclient (1.0.1)
faraday-multipart (1.0.4) faraday-multipart (1.0.4)
multipart-post (~> 2) multipart-post (~> 2)
faraday-net_http (1.0.1) faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0) faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0) faraday-patron (1.0.0)
faraday-rack (1.0.0) faraday-rack (1.0.0)
@ -264,10 +266,11 @@ GEM
ffi-compiler (1.3.2) ffi-compiler (1.3.2)
ffi (>= 1.15.5) ffi (>= 1.15.5)
rake rake
flatware (2.3.2) flatware (2.3.3)
drb
thor (< 2.0) thor (< 2.0)
flatware-rspec (2.3.2) flatware-rspec (2.3.3)
flatware (= 2.3.2) flatware (= 2.3.3)
rspec (>= 3.6) rspec (>= 3.6)
fog-core (2.5.0) fog-core (2.5.0)
builder builder
@ -284,14 +287,11 @@ GEM
fugit (1.11.1) fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11) et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4) raabro (~> 1.4)
fuubar (2.5.1)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
google-protobuf (3.25.4) google-protobuf (3.25.5)
googleapis-common-protos-types (1.14.0) googleapis-common-protos-types (1.15.0)
google-protobuf (~> 3.18) google-protobuf (>= 3.18, < 5.a)
haml (6.3.0) haml (6.3.0)
temple (>= 0.8.2) temple (>= 0.8.2)
thor thor
@ -307,11 +307,12 @@ GEM
rainbow rainbow
rubocop (>= 1.0) rubocop (>= 1.0)
sysexits (~> 1.1) sysexits (~> 1.1)
hashdiff (1.1.0) hashdiff (1.1.1)
hashie (5.0.0) hashie (5.0.0)
hcaptcha (7.1.0) hcaptcha (7.1.0)
json json
highline (3.0.1) highline (3.1.1)
reline
hiredis (0.6.3) hiredis (0.6.3)
hkdf (0.3.0) hkdf (0.3.0)
htmlentities (4.3.4) htmlentities (4.3.4)
@ -329,7 +330,7 @@ GEM
httplog (1.7.0) httplog (1.7.0)
rack (>= 2.0) rack (>= 2.0)
rainbow (>= 2.0.0) rainbow (>= 2.0.0)
i18n (1.14.5) i18n (1.14.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
i18n-tasks (1.0.14) i18n-tasks (1.0.14)
activesupport (>= 4.0.2) activesupport (>= 4.0.2)
@ -342,7 +343,7 @@ GEM
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
terminal-table (>= 1.5.1) terminal-table (>= 1.5.1)
idn-ruby (0.1.5) idn-ruby (0.1.5)
inline_svg (1.9.0) inline_svg (1.10.0)
activesupport (>= 3.0) activesupport (>= 3.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
io-console (0.7.2) io-console (0.7.2)
@ -368,8 +369,8 @@ GEM
json-ld-preloaded (3.3.0) json-ld-preloaded (3.3.0)
json-ld (~> 3.3) json-ld (~> 3.3)
rdf (~> 3.3) rdf (~> 3.3)
json-schema (4.3.1) json-schema (5.0.0)
addressable (>= 2.8) addressable (~> 2.8)
jsonapi-renderer (0.2.2) jsonapi-renderer (0.2.2)
jwt (2.7.1) jwt (2.7.1)
kaminari (1.2.2) kaminari (1.2.2)
@ -391,8 +392,9 @@ GEM
mime-types mime-types
terrapin (>= 0.6.0, < 2.0) terrapin (>= 0.6.0, < 2.0)
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
launchy (2.5.2) launchy (3.0.1)
addressable (~> 2.8) addressable (~> 2.8)
childprocess (~> 5.0)
letter_opener (1.10.0) letter_opener (1.10.0)
launchy (>= 2.2, < 4) launchy (>= 2.2, < 4)
letter_opener_web (3.0.0) letter_opener_web (3.0.0)
@ -404,7 +406,7 @@ GEM
llhttp-ffi (0.5.0) llhttp-ffi (0.5.0)
ffi-compiler (~> 1.0) ffi-compiler (~> 1.0)
rake (~> 13.0) rake (~> 13.0)
logger (1.6.0) logger (1.6.1)
lograge (0.14.0) lograge (0.14.0)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
@ -426,22 +428,22 @@ GEM
addressable (~> 2.5) addressable (~> 2.5)
azure-storage-blob (~> 2.0.1) azure-storage-blob (~> 2.0.1)
hashie (~> 5.0) hashie (~> 5.0)
memory_profiler (1.0.2) memory_profiler (1.1.0)
mime-types (3.5.2) mime-types (3.5.2)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2024.0702) mime-types-data (3.2024.0820)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.7) mini_portile2 (2.8.7)
minitest (5.24.1) minitest (5.25.1)
msgpack (1.7.2) msgpack (1.7.2)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.4.0) multipart-post (2.4.1)
mutex_m (0.2.0) mutex_m (0.2.0)
net-http (0.4.1) net-http (0.4.1)
uri uri
net-http-persistent (4.0.2) net-http-persistent (4.0.2)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.4.14) net-imap (0.4.15)
date date
net-protocol net-protocol
net-ldap (0.19.0) net-ldap (0.19.0)
@ -455,7 +457,7 @@ GEM
nokogiri (1.16.7) nokogiri (1.16.7)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
oj (3.16.5) oj (3.16.6)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
ostruct (>= 0.2) ostruct (>= 0.2)
omniauth (2.1.2) omniauth (2.1.2)
@ -466,12 +468,12 @@ GEM
addressable (~> 2.8) addressable (~> 2.8)
nokogiri (~> 1.12) nokogiri (~> 1.12)
omniauth (~> 2.1) omniauth (~> 2.1)
omniauth-rails_csrf_protection (1.0.1) omniauth-rails_csrf_protection (1.0.2)
actionpack (>= 4.2) actionpack (>= 4.2)
omniauth (~> 2.0) omniauth (~> 2.0)
omniauth-saml (2.1.0) omniauth-saml (2.2.1)
omniauth (~> 2.0) omniauth (~> 2.1)
ruby-saml (~> 1.12) ruby-saml (~> 1.17)
omniauth_openid_connect (0.6.1) omniauth_openid_connect (0.6.1)
omniauth (>= 1.9, < 3) omniauth (>= 1.9, < 3)
openid_connect (~> 1.1) openid_connect (~> 1.1)
@ -489,18 +491,18 @@ GEM
openssl (3.2.0) openssl (3.2.0)
openssl-signature_algorithm (1.3.0) openssl-signature_algorithm (1.3.0)
openssl (> 2.0) openssl (> 2.0)
opentelemetry-api (1.3.0) opentelemetry-api (1.4.0)
opentelemetry-common (0.20.1) opentelemetry-common (0.21.0)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-exporter-otlp (0.28.1) opentelemetry-exporter-otlp (0.29.0)
google-protobuf (>= 3.18) google-protobuf (>= 3.18)
googleapis-common-protos-types (~> 1.3) googleapis-common-protos-types (~> 1.3)
opentelemetry-api (~> 1.1) opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20) opentelemetry-common (~> 0.20)
opentelemetry-sdk (~> 1.2) opentelemetry-sdk (~> 1.2)
opentelemetry-semantic_conventions opentelemetry-semantic_conventions
opentelemetry-helpers-sql-obfuscation (0.1.0) opentelemetry-helpers-sql-obfuscation (0.2.0)
opentelemetry-common (~> 0.20) opentelemetry-common (~> 0.21)
opentelemetry-instrumentation-action_mailer (0.1.0) opentelemetry-instrumentation-action_mailer (0.1.0)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.1) opentelemetry-instrumentation-active_support (~> 0.1)
@ -525,8 +527,9 @@ GEM
opentelemetry-instrumentation-active_support (0.6.0) opentelemetry-instrumentation-active_support (0.6.0)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-base (0.22.3) opentelemetry-instrumentation-base (0.22.6)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-common (~> 0.21)
opentelemetry-registry (~> 0.1) opentelemetry-registry (~> 0.1)
opentelemetry-instrumentation-concurrent_ruby (0.21.4) opentelemetry-instrumentation-concurrent_ruby (0.21.4)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
@ -546,7 +549,7 @@ GEM
opentelemetry-instrumentation-net_http (0.22.7) opentelemetry-instrumentation-net_http (0.22.7)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-pg (0.28.0) opentelemetry-instrumentation-pg (0.29.0)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-helpers-sql-obfuscation opentelemetry-helpers-sql-obfuscation
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
@ -580,25 +583,25 @@ GEM
orm_adapter (0.5.0) orm_adapter (0.5.0)
ostruct (0.6.0) ostruct (0.6.0)
ox (2.14.18) ox (2.14.18)
parallel (1.25.1) parallel (1.26.3)
parser (3.3.4.0) parser (3.3.5.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
parslet (2.0.0) parslet (2.0.0)
pastel (0.8.0) pastel (0.8.0)
tty-color (~> 0.5) tty-color (~> 0.5)
pg (1.5.7) pg (1.5.8)
pghero (3.6.0) pghero (3.6.0)
activerecord (>= 6.1) activerecord (>= 6.1)
premailer (1.23.0) premailer (1.27.0)
addressable addressable
css_parser (>= 1.12.0) css_parser (>= 1.19.0)
htmlentities (>= 4.0.0) htmlentities (>= 4.0.0)
premailer-rails (1.12.0) premailer-rails (1.12.0)
actionmailer (>= 3) actionmailer (>= 3)
net-smtp net-smtp
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
propshaft (0.9.1) propshaft (1.0.0)
actionpack (>= 7.0.0) actionpack (>= 7.0.0)
activesupport (>= 7.0.0) activesupport (>= 7.0.0)
rack rack
@ -606,9 +609,9 @@ GEM
psych (5.1.2) psych (5.1.2)
stringio stringio
public_suffix (6.0.1) public_suffix (6.0.1)
puma (6.4.2) puma (6.4.3)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.3.2) pundit (2.4.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.8.1) racc (1.8.1)
@ -635,20 +638,20 @@ GEM
rackup (1.0.0) rackup (1.0.0)
rack (< 3) rack (< 3)
webrick webrick
rails (7.1.3.4) rails (7.1.4)
actioncable (= 7.1.3.4) actioncable (= 7.1.4)
actionmailbox (= 7.1.3.4) actionmailbox (= 7.1.4)
actionmailer (= 7.1.3.4) actionmailer (= 7.1.4)
actionpack (= 7.1.3.4) actionpack (= 7.1.4)
actiontext (= 7.1.3.4) actiontext (= 7.1.4)
actionview (= 7.1.3.4) actionview (= 7.1.4)
activejob (= 7.1.3.4) activejob (= 7.1.4)
activemodel (= 7.1.3.4) activemodel (= 7.1.4)
activerecord (= 7.1.3.4) activerecord (= 7.1.4)
activestorage (= 7.1.3.4) activestorage (= 7.1.4)
activesupport (= 7.1.3.4) activesupport (= 7.1.4)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.1.3.4) railties (= 7.1.4)
rails-controller-testing (1.0.5) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1) actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1)
@ -663,9 +666,9 @@ GEM
rails-i18n (7.0.9) rails-i18n (7.0.9)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8) railties (>= 6.0.0, < 8)
railties (7.1.3.4) railties (7.1.4)
actionpack (= 7.1.3.4) actionpack (= 7.1.4)
activesupport (= 7.1.3.4) activesupport (= 7.1.4)
irb irb
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
@ -688,17 +691,16 @@ GEM
redlock (1.3.2) redlock (1.3.2)
redis (>= 3.0.0, < 6.0) redis (>= 3.0.0, < 6.0)
regexp_parser (2.9.2) regexp_parser (2.9.2)
reline (0.5.9) reline (0.5.10)
io-console (~> 0.5) io-console (~> 0.5)
request_store (1.6.0) request_store (1.6.0)
rack (>= 1.4) rack (>= 1.4)
responders (3.1.1) responders (3.1.1)
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
rexml (3.3.4) rexml (3.3.7)
strscan
rotp (6.3.0) rotp (6.3.0)
rouge (4.2.1) rouge (4.3.0)
rpam2 (4.0.2) rpam2 (4.0.2)
rqrcode (2.2.0) rqrcode (2.2.0)
chunky_png (~> 1.0) chunky_png (~> 1.0)
@ -708,9 +710,9 @@ GEM
rspec-core (~> 3.13.0) rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0) rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0) rspec-mocks (~> 3.13.0)
rspec-core (3.13.0) rspec-core (3.13.1)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-expectations (3.13.1) rspec-expectations (3.13.2)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-github (2.4.0) rspec-github (2.4.0)
@ -718,10 +720,10 @@ GEM
rspec-mocks (3.13.1) rspec-mocks (3.13.1)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-rails (6.1.4) rspec-rails (7.0.1)
actionpack (>= 6.1) actionpack (>= 7.0)
activesupport (>= 6.1) activesupport (>= 7.0)
railties (>= 6.1) railties (>= 7.0)
rspec-core (~> 3.13) rspec-core (~> 3.13)
rspec-expectations (~> 3.13) rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13) rspec-mocks (~> 3.13)
@ -732,18 +734,17 @@ GEM
rspec-mocks (~> 3.0) rspec-mocks (~> 3.0)
sidekiq (>= 5, < 8) sidekiq (>= 5, < 8)
rspec-support (3.13.1) rspec-support (3.13.1)
rubocop (1.65.1) rubocop (1.66.1)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.4, < 3.0) regexp_parser (>= 2.4, < 3.0)
rexml (>= 3.2.5, < 4.0) rubocop-ast (>= 1.32.2, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.31.3) rubocop-ast (1.32.3)
parser (>= 3.3.1.0) parser (>= 3.3.1.0)
rubocop-capybara (2.21.0) rubocop-capybara (2.21.0)
rubocop (~> 1.41) rubocop (~> 1.41)
@ -762,7 +763,7 @@ GEM
rubocop-rspec (~> 3, >= 3.0.1) rubocop-rspec (~> 3, >= 3.0.1)
ruby-prof (1.7.0) ruby-prof (1.7.0)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-saml (1.16.0) ruby-saml (1.17.0)
nokogiri (>= 1.13.10) nokogiri (>= 1.13.10)
rexml rexml
ruby-vips (2.2.2) ruby-vips (2.2.2)
@ -780,13 +781,15 @@ GEM
scenic (1.8.0) scenic (1.8.0)
activerecord (>= 4.0.0) activerecord (>= 4.0.0)
railties (>= 4.0.0) railties (>= 4.0.0)
selenium-webdriver (4.23.0) selenium-webdriver (4.25.0)
base64 (~> 0.2) base64 (~> 0.2)
logger (~> 1.4) logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0) rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0) websocket (~> 1.0)
semantic_range (3.0.0) semantic_range (3.0.0)
shoulda-matchers (6.4.0)
activesupport (>= 5.2.0)
sidekiq (6.5.12) sidekiq (6.5.12)
connection_pool (>= 2.2.5, < 3) connection_pool (>= 2.2.5, < 3)
rack (~> 2.0) rack (~> 2.0)
@ -821,7 +824,6 @@ GEM
stringio (3.1.1) stringio (3.1.1)
strong_migrations (2.0.0) strong_migrations (2.0.0)
activerecord (>= 6.1) activerecord (>= 6.1)
strscan (3.1.0)
swd (1.3.0) swd (1.3.0)
activesupport (>= 3) activesupport (>= 3)
attr_required (>= 0.0.5) attr_required (>= 0.0.5)
@ -832,11 +834,11 @@ GEM
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 3)
terrapin (1.0.1) terrapin (1.0.1)
climate_control climate_control
test-prof (1.4.0) test-prof (1.4.2)
thor (1.3.1) thor (1.3.2)
tilt (2.3.0) tilt (2.4.0)
timeout (0.4.1) timeout (0.4.1)
tpm-key_attestation (0.12.0) tpm-key_attestation (0.12.1)
bindata (~> 2.4) bindata (~> 2.4)
openssl (> 2.0) openssl (> 2.0)
openssl-signature_algorithm (~> 1.0) openssl-signature_algorithm (~> 1.0)
@ -855,13 +857,13 @@ GEM
unf (~> 0.1.0) unf (~> 0.1.0)
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
tzinfo-data (1.2024.1) tzinfo-data (1.2024.2)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.9.1) unf_ext (0.0.9.1)
unicode-display_width (2.5.0) unicode-display_width (2.5.0)
uri (0.13.0) uri (0.13.1)
validate_email (0.1.6) validate_email (0.1.6)
activemodel (>= 3.0) activemodel (>= 3.0)
mail (>= 2.2.5) mail (>= 2.2.5)
@ -891,7 +893,7 @@ GEM
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
semantic_range (>= 2.3.0) semantic_range (>= 2.3.0)
webrick (1.8.1) webrick (1.8.2)
websocket (1.2.11) websocket (1.2.11)
websocket-driver (0.7.6) websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
@ -900,7 +902,7 @@ GEM
xorcist (1.1.3) xorcist (1.1.3)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.6.17) zeitwerk (2.6.18)
PLATFORMS PLATFORMS
ruby ruby
@ -934,7 +936,6 @@ DEPENDENCIES
discard (~> 1.2) discard (~> 1.2)
doorkeeper (~> 5.6) doorkeeper (~> 5.6)
dotenv dotenv
ed25519 (~> 1.3)
email_spec email_spec
fabrication (~> 2.30) fabrication (~> 2.30)
faker (~> 3.2) faker (~> 3.2)
@ -943,7 +944,6 @@ DEPENDENCIES
flatware-rspec flatware-rspec
fog-core (<= 2.5.0) fog-core (<= 2.5.0)
fog-openstack (~> 1.0) fog-openstack (~> 1.0)
fuubar (~> 2.5)
haml-rails (~> 2.0) haml-rails (~> 2.0)
haml_lint haml_lint
hcaptcha (~> 7.1) hcaptcha (~> 7.1)
@ -959,7 +959,7 @@ DEPENDENCIES
irb (~> 1.8) irb (~> 1.8)
json-ld json-ld
json-ld-preloaded (~> 3.2) json-ld-preloaded (~> 3.2)
json-schema (~> 4.0) json-schema (~> 5.0)
kaminari (~> 1.2) kaminari (~> 1.2)
kt-paperclip (~> 7.2) kt-paperclip (~> 7.2)
letter_opener (~> 1.8) letter_opener (~> 1.8)
@ -980,8 +980,8 @@ DEPENDENCIES
omniauth-rails_csrf_protection (~> 1.0) omniauth-rails_csrf_protection (~> 1.0)
omniauth-saml (~> 2.0) omniauth-saml (~> 2.0)
omniauth_openid_connect (~> 0.6.1) omniauth_openid_connect (~> 0.6.1)
opentelemetry-api (~> 1.3.0) opentelemetry-api (~> 1.4.0)
opentelemetry-exporter-otlp (~> 0.28.0) opentelemetry-exporter-otlp (~> 0.29.0)
opentelemetry-instrumentation-active_job (~> 0.7.1) opentelemetry-instrumentation-active_job (~> 0.7.1)
opentelemetry-instrumentation-active_model_serializers (~> 0.20.1) opentelemetry-instrumentation-active_model_serializers (~> 0.20.1)
opentelemetry-instrumentation-concurrent_ruby (~> 0.21.2) opentelemetry-instrumentation-concurrent_ruby (~> 0.21.2)
@ -990,7 +990,7 @@ DEPENDENCIES
opentelemetry-instrumentation-http (~> 0.23.2) opentelemetry-instrumentation-http (~> 0.23.2)
opentelemetry-instrumentation-http_client (~> 0.22.3) opentelemetry-instrumentation-http_client (~> 0.22.3)
opentelemetry-instrumentation-net_http (~> 0.22.4) opentelemetry-instrumentation-net_http (~> 0.22.4)
opentelemetry-instrumentation-pg (~> 0.28.0) opentelemetry-instrumentation-pg (~> 0.29.0)
opentelemetry-instrumentation-rack (~> 0.24.1) opentelemetry-instrumentation-rack (~> 0.24.1)
opentelemetry-instrumentation-rails (~> 0.31.0) opentelemetry-instrumentation-rails (~> 0.31.0)
opentelemetry-instrumentation-redis (~> 0.25.3) opentelemetry-instrumentation-redis (~> 0.25.3)
@ -1018,7 +1018,7 @@ DEPENDENCIES
redis-namespace (~> 1.10) redis-namespace (~> 1.10)
rqrcode (~> 2.2) rqrcode (~> 2.2)
rspec-github (~> 2.4) rspec-github (~> 2.4)
rspec-rails (~> 6.0) rspec-rails (~> 7.0)
rspec-sidekiq (~> 5.0) rspec-sidekiq (~> 5.0)
rubocop rubocop
rubocop-capybara rubocop-capybara
@ -1033,6 +1033,7 @@ DEPENDENCIES
sanitize (~> 6.0) sanitize (~> 6.0)
scenic (~> 1.7) scenic (~> 1.7)
selenium-webdriver selenium-webdriver
shoulda-matchers
sidekiq (~> 6.5) sidekiq (~> 6.5)
sidekiq-bulk (~> 0.2.0) sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 5.0) sidekiq-scheduler (~> 5.0)
@ -1056,7 +1057,7 @@ DEPENDENCIES
xorcist (~> 1.1) xorcist (~> 1.1)
RUBY VERSION RUBY VERSION
ruby 3.3.2p78 ruby 3.3.4p94
BUNDLED WITH BUNDLED WITH
2.5.11 2.5.18

View File

@ -11,4 +11,4 @@ worker: bundle exec sidekiq
# #
# and let the main app use the separate app: # and let the main app use the separate app:
# #
# heroku config:set STREAMING_API_BASE_URL=wss://<streaming-app>.herokuapp.com -a <main-app> # heroku config:set STREAMING_API_BASE_URL=wss://<streaming-app-random>.herokuapp.com -a <main-app>

View File

@ -90,9 +90,15 @@
} }
}, },
"buildpacks": [ "buildpacks": [
{
"url": "https://github.com/heroku/heroku-buildpack-activestorage-preview"
},
{ {
"url": "https://github.com/heroku/heroku-buildpack-apt" "url": "https://github.com/heroku/heroku-buildpack-apt"
}, },
{
"url": "heroku/nodejs"
},
{ {
"url": "heroku/ruby" "url": "heroku/ruby"
} }
@ -100,5 +106,6 @@
"scripts": { "scripts": {
"postdeploy": "bundle exec rails db:migrate && bundle exec rails db:seed" "postdeploy": "bundle exec rails db:migrate && bundle exec rails db:seed"
}, },
"addons": ["heroku-postgresql", "heroku-redis"] "addons": ["heroku-postgresql", "heroku-redis"],
"stack": "heroku-24"
} }

View File

@ -1,18 +0,0 @@
# frozen_string_literal: true
class ActivityPub::ClaimsController < ActivityPub::BaseController
skip_before_action :authenticate_user!
before_action :require_account_signature!
before_action :set_claim_result
def create
render json: @claim_result, serializer: ActivityPub::OneTimeKeySerializer
end
private
def set_claim_result
@claim_result = ::Keys::ClaimService.new.call(@account.id, params[:id])
end
end

View File

@ -22,8 +22,6 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
@items = @items.map { |item| item.distributable? ? item : ActivityPub::TagManager.instance.uri_for(item) } @items = @items.map { |item| item.distributable? ? item : ActivityPub::TagManager.instance.uri_for(item) }
when 'tags' when 'tags'
@items = for_signed_account { @account.featured_tags } @items = for_signed_account { @account.featured_tags }
when 'devices'
@items = @account.devices
else else
not_found not_found
end end
@ -31,7 +29,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
def set_size def set_size
case params[:id] case params[:id]
when 'featured', 'devices', 'tags' when 'featured', 'tags'
@size = @items.size @size = @items.size
else else
not_found not_found
@ -42,7 +40,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
case params[:id] case params[:id]
when 'featured' when 'featured'
@type = :ordered @type = :ordered
when 'devices', 'tags' when 'tags'
@type = :unordered @type = :unordered
else else
not_found not_found

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
class ActivityPub::LikesController < ActivityPub::BaseController
include Authorization
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_status
def index
expires_in 0, public: @status.distributable? && public_fetch_mode?
render json: likes_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
private
def pundit_user
signed_request_account
end
def set_status
@status = @account.statuses.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
def likes_collection_presenter
ActivityPub::CollectionPresenter.new(
id: account_status_likes_url(@account, @status),
type: :unordered,
size: @status.favourites_count
)
end
end

View File

@ -12,7 +12,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
before_action :set_replies before_action :set_replies
def index def index
expires_in 0, public: public_fetch_mode? expires_in 0, public: @status.distributable? && public_fetch_mode?
render json: replies_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', skip_activities: true render json: replies_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', skip_activities: true
end end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
class ActivityPub::SharesController < ActivityPub::BaseController
include Authorization
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_status
def index
expires_in 0, public: @status.distributable? && public_fetch_mode?
render json: shares_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
private
def pundit_user
signed_request_account
end
def set_status
@status = @account.statuses.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
def shares_collection_presenter
ActivityPub::CollectionPresenter.new(
id: account_status_shares_url(@account, @status),
type: :unordered,
size: @status.reblogs_count
)
end
end

View File

@ -13,7 +13,7 @@ module Admin
redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.created_msg') redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.created_msg')
else else
@account = @account_moderation_note.target_account @account = @account_moderation_note.target_account
@moderation_notes = @account.targeted_moderation_notes.latest @moderation_notes = @account.targeted_moderation_notes.chronological.includes(:account)
@warnings = @account.strikes.custom.latest @warnings = @account.strikes.custom.latest
render 'admin/accounts/show' render 'admin/accounts/show'

View File

@ -33,7 +33,7 @@ module Admin
@deletion_request = @account.deletion_request @deletion_request = @account.deletion_request
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account) @account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
@moderation_notes = @account.targeted_moderation_notes.latest @moderation_notes = @account.targeted_moderation_notes.chronological.includes(:account)
@warnings = @account.strikes.includes(:target_account, :account, :appeal).latest @warnings = @account.strikes.includes(:target_account, :account, :appeal).latest
@domain_block = DomainBlock.rule_for(@account.domain) @domain_block = DomainBlock.rule_for(@account.domain)
end end

View File

@ -7,17 +7,12 @@ module Admin
layout 'admin' layout 'admin'
before_action :set_body_classes
before_action :set_cache_headers before_action :set_cache_headers
after_action :verify_authorized after_action :verify_authorized
private private
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers def set_cache_headers
response.cache_control.replace(private: true, no_store: true) response.cache_control.replace(private: true, no_store: true)
end end

View File

@ -7,12 +7,12 @@ module Admin
def index def index
authorize :dashboard, :index? authorize :dashboard, :index?
@pending_appeals_count = Appeal.pending.async_count
@pending_reports_count = Report.unresolved.async_count
@pending_tags_count = Tag.pending_review.async_count
@pending_users_count = User.pending.async_count
@system_checks = Admin::SystemCheck.perform(current_user) @system_checks = Admin::SystemCheck.perform(current_user)
@time_period = (29.days.ago.to_date...Time.now.utc.to_date) @time_period = (29.days.ago.to_date...Time.now.utc.to_date)
@pending_users_count = User.pending.count
@pending_reports_count = Report.unresolved.count
@pending_tags_count = Tag.pending_review.count
@pending_appeals_count = Appeal.pending.count
end end
end end
end end

View File

@ -21,7 +21,7 @@ module Admin
redirect_to after_create_redirect_path, notice: I18n.t('admin.report_notes.created_msg') redirect_to after_create_redirect_path, notice: I18n.t('admin.report_notes.created_msg')
else else
@report_notes = @report.notes.includes(:account).order(id: :desc) @report_notes = @report.notes.chronological.includes(:account)
@action_logs = @report.history.includes(:target) @action_logs = @report.history.includes(:target)
@form = Admin::StatusBatchAction.new @form = Admin::StatusBatchAction.new
@statuses = @report.statuses.with_includes @statuses = @report.statuses.with_includes

View File

@ -13,7 +13,7 @@ module Admin
authorize @report, :show? authorize @report, :show?
@report_note = @report.notes.new @report_note = @report.notes.new
@report_notes = @report.notes.includes(:account).order(id: :desc) @report_notes = @report.notes.chronological.includes(:account)
@action_logs = @report.history.includes(:target) @action_logs = @report.history.includes(:target)
@form = Admin::StatusBatchAction.new @form = Admin::StatusBatchAction.new
@statuses = @report.statuses.with_includes @statuses = @report.statuses.with_includes

View File

@ -7,7 +7,7 @@ class Api::OEmbedController < Api::BaseController
before_action :require_public_status! before_action :require_public_status!
def show def show
render json: @status, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default render json: @status, serializer: OEmbedSerializer, width: params[:maxwidth], height: params[:maxheight]
end end
private private
@ -23,12 +23,4 @@ class Api::OEmbedController < Api::BaseController
def status_finder def status_finder
StatusFinder.new(params[:url]) StatusFinder.new(params[:url])
end end
def maxwidth_or_default
(params[:maxwidth].presence || 400).to_i
end
def maxheight_or_default
params[:maxheight].present? ? params[:maxheight].to_i : nil
end
end end

View File

@ -1,30 +0,0 @@
# frozen_string_literal: true
class Api::V1::Crypto::DeliveriesController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_current_device
def create
devices.each do |device_params|
DeliverToDeviceService.new.call(current_account, @current_device, device_params)
end
render_empty
end
private
def set_current_device
@current_device = Device.find_by!(access_token: doorkeeper_token)
end
def resource_params
params.require(:device)
params.permit(device: [:account_id, :device_id, :type, :body, :hmac])
end
def devices
Array(resource_params[:device])
end
end

View File

@ -1,47 +0,0 @@
# frozen_string_literal: true
class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController
LIMIT = 80
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_current_device
before_action :set_encrypted_messages, only: :index
after_action :insert_pagination_headers, only: :index
def index
render json: @encrypted_messages, each_serializer: REST::EncryptedMessageSerializer
end
def clear
@current_device.encrypted_messages.up_to(params[:up_to_id]).delete_all
render_empty
end
private
def set_current_device
@current_device = Device.find_by!(access_token: doorkeeper_token)
end
def set_encrypted_messages
@encrypted_messages = @current_device.encrypted_messages.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def next_path
api_v1_crypto_encrypted_messages_url pagination_params(max_id: pagination_max_id) if records_continue?
end
def prev_path
api_v1_crypto_encrypted_messages_url pagination_params(min_id: pagination_since_id) unless @encrypted_messages.empty?
end
def pagination_collection
@encrypted_messages
end
def records_continue?
@encrypted_messages.size == limit_param(LIMIT)
end
end

View File

@ -1,25 +0,0 @@
# frozen_string_literal: true
class Api::V1::Crypto::Keys::ClaimsController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_claim_results
def create
render json: @claim_results, each_serializer: REST::Keys::ClaimResultSerializer
end
private
def set_claim_results
@claim_results = devices.filter_map { |device_params| ::Keys::ClaimService.new.call(current_account, device_params[:account_id], device_params[:device_id]) }
end
def resource_params
params.permit(device: [:account_id, :device_id])
end
def devices
Array(resource_params[:device])
end
end

View File

@ -1,17 +0,0 @@
# frozen_string_literal: true
class Api::V1::Crypto::Keys::CountsController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_current_device
def show
render json: { one_time_keys: @current_device.one_time_keys.count }
end
private
def set_current_device
@current_device = Device.find_by!(access_token: doorkeeper_token)
end
end

View File

@ -1,26 +0,0 @@
# frozen_string_literal: true
class Api::V1::Crypto::Keys::QueriesController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_accounts
before_action :set_query_results
def create
render json: @query_results, each_serializer: REST::Keys::QueryResultSerializer
end
private
def set_accounts
@accounts = Account.where(id: account_ids).includes(:devices)
end
def set_query_results
@query_results = @accounts.filter_map { |account| ::Keys::QueryService.new.call(account) }
end
def account_ids
Array(params[:id]).map(&:to_i)
end
end

View File

@ -1,29 +0,0 @@
# frozen_string_literal: true
class Api::V1::Crypto::Keys::UploadsController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
def create
device = Device.find_or_initialize_by(access_token: doorkeeper_token)
device.transaction do
device.account = current_account
device.update!(resource_params[:device])
if resource_params[:one_time_keys].present? && resource_params[:one_time_keys].is_a?(Enumerable)
resource_params[:one_time_keys].each do |one_time_key_params|
device.one_time_keys.create!(one_time_key_params)
end
end
end
render json: device, serializer: REST::Keys::DeviceSerializer
end
private
def resource_params
params.permit(device: [:device_id, :name, :fingerprint_key, :identity_key], one_time_keys: [:key_id, :key, :signature])
end
end

View File

@ -7,6 +7,8 @@ class Api::V1::Peers::SearchController < Api::BaseController
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
skip_around_action :set_locale skip_around_action :set_locale
LIMIT = 10
vary_by '' vary_by ''
def index def index
@ -35,10 +37,10 @@ class Api::V1::Peers::SearchController < Api::BaseController
field: 'accounts_count', field: 'accounts_count',
modifier: 'log2p', modifier: 'log2p',
}, },
}).limit(10).pluck(:domain) }).limit(LIMIT).pluck(:domain)
else else
domain = normalized_domain domain = normalized_domain
@domains = Instance.searchable.domain_starts_with(domain).limit(10).pluck(:domain) @domains = Instance.searchable.domain_starts_with(domain).limit(LIMIT).pluck(:domain)
end end
rescue Addressable::URI::InvalidURIError rescue Addressable::URI::InvalidURIError
@domains = [] @domains = []

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
class Api::V2::Notifications::AccountsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }
before_action :require_user!
before_action :set_notifications!
after_action :insert_pagination_headers, only: :index
def index
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
end
private
def load_accounts
@paginated_notifications.map(&:from_account)
end
def set_notifications!
@paginated_notifications = begin
current_account
.notifications
.without_suspended
.where(group_key: params[:notification_group_key])
.includes(from_account: [:account_stat, :user])
.paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id],
params[:since_id]
)
end
end
def next_path
api_v2_notification_accounts_url pagination_params(max_id: pagination_max_id) if records_continue?
end
def prev_path
api_v2_notification_accounts_url pagination_params(min_id: pagination_since_id) unless @paginated_notifications.empty?
end
def pagination_collection
@paginated_notifications
end
def records_continue?
@paginated_notifications.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V2Alpha::NotificationsController < Api::BaseController class Api::V2::NotificationsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss] before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss]
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss] before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss]
before_action :require_user! before_action :require_user!
@ -13,7 +13,6 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
def index def index
with_read_replica do with_read_replica do
@notifications = load_notifications @notifications = load_notifications
@group_metadata = load_group_metadata
@grouped_notifications = load_grouped_notifications @grouped_notifications = load_grouped_notifications
@relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id) @relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
@presenter = GroupedNotificationsPresenter.new(@grouped_notifications, expand_accounts: expand_accounts_param) @presenter = GroupedNotificationsPresenter.new(@grouped_notifications, expand_accounts: expand_accounts_param)
@ -22,7 +21,7 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
ActiveRecord::Associations::Preloader.new(records: @presenter.accounts, associations: [:account_stat, { user: :role }]).call ActiveRecord::Associations::Preloader.new(records: @presenter.accounts, associations: [:account_stat, { user: :role }]).call
end end
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#index rendering') do |span| MastodonOTELTracer.in_span('Api::V2::NotificationsController#index rendering') do |span|
statuses = @grouped_notifications.filter_map { |group| group.target_status&.id } statuses = @grouped_notifications.filter_map { |group| group.target_status&.id }
span.add_attributes( span.add_attributes(
@ -34,7 +33,7 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
'app.notification_grouping.expand_accounts_param' => expand_accounts_param 'app.notification_grouping.expand_accounts_param' => expand_accounts_param
) )
render json: @presenter, serializer: REST::DedupNotificationGroupSerializer, relationships: @relationships, group_metadata: @group_metadata, expand_accounts: expand_accounts_param render json: @presenter, serializer: REST::DedupNotificationGroupSerializer, relationships: @relationships, expand_accounts: expand_accounts_param
end end
end end
@ -42,13 +41,13 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
limit = limit_param(DEFAULT_NOTIFICATIONS_COUNT_LIMIT, MAX_NOTIFICATIONS_COUNT_LIMIT) limit = limit_param(DEFAULT_NOTIFICATIONS_COUNT_LIMIT, MAX_NOTIFICATIONS_COUNT_LIMIT)
with_read_replica do with_read_replica do
render json: { count: browserable_account_notifications.paginate_groups_by_min_id(limit, min_id: notification_marker&.last_read_id).count } render json: { count: browserable_account_notifications.paginate_groups_by_min_id(limit, min_id: notification_marker&.last_read_id, grouped_types: params[:grouped_types]).count }
end end
end end
def show def show
@notification = current_account.notifications.without_suspended.find_by!(group_key: params[:id]) @notification = current_account.notifications.without_suspended.find_by!(group_key: params[:group_key])
presenter = GroupedNotificationsPresenter.new([NotificationGroup.from_notification(@notification)]) presenter = GroupedNotificationsPresenter.new(NotificationGroup.from_notifications([@notification]))
render json: presenter, serializer: REST::DedupNotificationGroupSerializer render json: presenter, serializer: REST::DedupNotificationGroupSerializer
end end
@ -58,17 +57,17 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
end end
def dismiss def dismiss
current_account.notifications.where(group_key: params[:id]).destroy_all current_account.notifications.where(group_key: params[:group_key]).destroy_all
render_empty render_empty
end end
private private
def load_notifications def load_notifications
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_notifications') do MastodonOTELTracer.in_span('Api::V2::NotificationsController#load_notifications') do
notifications = browserable_account_notifications.includes(from_account: [:account_stat, :user]).to_a_grouped_paginated_by_id( notifications = browserable_account_notifications.includes(from_account: [:account_stat, :user]).to_a_grouped_paginated_by_id(
limit_param(DEFAULT_NOTIFICATIONS_LIMIT), limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
params_slice(:max_id, :since_id, :min_id) params.slice(:max_id, :since_id, :min_id, :grouped_types).permit(:max_id, :since_id, :min_id, grouped_types: [])
) )
Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses| Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses|
@ -77,22 +76,11 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
end end
end end
def load_group_metadata
return {} if @notifications.empty?
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_group_metadata') do
browserable_account_notifications
.where(group_key: @notifications.filter_map(&:group_key))
.where(id: (@notifications.last.id)..(@notifications.first.id))
.group(:group_key)
.pluck(:group_key, 'min(notifications.id) as min_id', 'max(notifications.id) as max_id', 'max(notifications.created_at) as latest_notification_at')
.to_h { |group_key, min_id, max_id, latest_notification_at| [group_key, { min_id: min_id, max_id: max_id, latest_notification_at: latest_notification_at }] }
end
end
def load_grouped_notifications def load_grouped_notifications
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_grouped_notifications') do return [] if @notifications.empty?
@notifications.map { |notification| NotificationGroup.from_notification(notification, max_id: @group_metadata.dig(notification.group_key, :max_id)) }
MastodonOTELTracer.in_span('Api::V2::NotificationsController#load_grouped_notifications') do
NotificationGroup.from_notifications(@notifications, pagination_range: (@notifications.last.id)..(@notifications.first.id), grouped_types: params[:grouped_types])
end end
end end
@ -113,11 +101,11 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
end end
def next_path def next_path
api_v2_alpha_notifications_url pagination_params(max_id: pagination_max_id) unless @notifications.empty? api_v2_notifications_url pagination_params(max_id: pagination_max_id) unless @notifications.empty?
end end
def prev_path def prev_path
api_v2_alpha_notifications_url pagination_params(min_id: pagination_since_id) unless @notifications.empty? api_v2_notifications_url pagination_params(min_id: pagination_since_id) unless @notifications.empty?
end end
def pagination_collection def pagination_collection
@ -125,11 +113,11 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
end end
def browserable_params def browserable_params
params.permit(:include_filtered, types: [], exclude_types: []) params.slice(:include_filtered, :types, :exclude_types, :grouped_types).permit(:include_filtered, types: [], exclude_types: [], grouped_types: [])
end end
def pagination_params(core_params) def pagination_params(core_params)
params.slice(:limit, :types, :exclude_types, :include_filtered).permit(:limit, :include_filtered, types: [], exclude_types: []).merge(core_params) params.slice(:limit, :include_filtered, :types, :exclude_types, :grouped_types).permit(:limit, :include_filtered, types: [], exclude_types: [], grouped_types: []).merge(core_params)
end end
def expand_accounts_param def expand_accounts_param

View File

@ -9,7 +9,7 @@ class Api::Web::EmbedsController < Api::Web::BaseController
return not_found if @status.hidden? return not_found if @status.hidden?
if @status.local? if @status.local?
render json: @status, serializer: OEmbedSerializer, width: 400 render json: @status, serializer: OEmbedSerializer
else else
return not_found unless user_signed_in? return not_found unless user_signed_in?

View File

@ -11,7 +11,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create] before_action :configure_sign_up_params, only: [:create]
before_action :set_sessions, only: [:edit, :update] before_action :set_sessions, only: [:edit, :update]
before_action :set_strikes, only: [:edit, :update] before_action :set_strikes, only: [:edit, :update]
before_action :set_body_classes, only: [:new, :create, :edit, :update]
before_action :require_not_suspended!, only: [:update] before_action :require_not_suspended!, only: [:update]
before_action :set_cache_headers, only: [:edit, :update] before_action :set_cache_headers, only: [:edit, :update]
before_action :set_rules, only: :new before_action :set_rules, only: :new
@ -104,10 +103,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
private private
def set_body_classes
@body_classes = 'admin' if %w(edit update).include?(action_name)
end
def set_invite def set_invite
@invite = begin @invite = begin
invite = Invite.find_by(code: invite_code) if invite_code.present? invite = Invite.find_by(code: invite_code) if invite_code.present?

View File

@ -20,11 +20,6 @@ class Auth::SessionsController < Devise::SessionsController
p.form_action(false) p.form_action(false)
end end
def check_suspicious!
user = find_user
@login_is_suspicious = suspicious_sign_in?(user) unless user.nil?
end
def create def create
super do |resource| super do |resource|
# We only need to call this if this hasn't already been # We only need to call this if this hasn't already been
@ -101,6 +96,11 @@ class Auth::SessionsController < Devise::SessionsController
private private
def check_suspicious!
user = find_user
@login_is_suspicious = suspicious_sign_in?(user) unless user.nil?
end
def home_paths(resource) def home_paths(resource)
paths = [about_path, '/explore'] paths = [about_path, '/explore']

View File

@ -20,7 +20,7 @@ module AccountControllerConcern
webfinger_account_link, webfinger_account_link,
actor_url_link, actor_url_link,
] ]
) ).to_s
end end
def webfinger_account_link def webfinger_account_link

View File

@ -19,7 +19,7 @@ module Api::Pagination
links = [] links = []
links << [next_path, [%w(rel next)]] if next_path links << [next_path, [%w(rel next)]] if next_path
links << [prev_path, [%w(rel prev)]] if prev_path links << [prev_path, [%w(rel prev)]] if prev_path
response.headers['Link'] = LinkHeader.new(links) unless links.empty? response.headers['Link'] = LinkHeader.new(links).to_s unless links.empty?
end end
def require_valid_pagination_options! def require_valid_pagination_options!

View File

@ -8,6 +8,16 @@ module WebAppControllerConcern
before_action :redirect_unauthenticated_to_permalinks! before_action :redirect_unauthenticated_to_permalinks!
before_action :set_app_body_class before_action :set_app_body_class
content_security_policy do |p|
policy = ContentSecurityPolicy.new
if policy.sso_host.present?
p.form_action policy.sso_host
else
p.form_action :none
end
end
end end
def skip_csrf_meta_tags? def skip_csrf_meta_tags?
@ -21,7 +31,7 @@ module WebAppControllerConcern
def redirect_unauthenticated_to_permalinks! def redirect_unauthenticated_to_permalinks!
return if user_signed_in? # NOTE: Different from upstream because we allow moved users to log in return if user_signed_in? # NOTE: Different from upstream because we allow moved users to log in
permalink_redirector = PermalinkRedirector.new(request.path) permalink_redirector = PermalinkRedirector.new(request.original_fullpath)
return if permalink_redirector.redirect_path.blank? return if permalink_redirector.redirect_path.blank?
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?

View File

@ -7,16 +7,11 @@ class Disputes::BaseController < ApplicationController
skip_before_action :require_functional! skip_before_action :require_functional!
before_action :set_body_classes
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_cache_headers before_action :set_cache_headers
private private
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers def set_cache_headers
response.cache_control.replace(private: true, no_store: true) response.cache_control.replace(private: true, no_store: true)
end end

View File

@ -6,7 +6,6 @@ class Filters::StatusesController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_filter before_action :set_filter
before_action :set_status_filters before_action :set_status_filters
before_action :set_body_classes
before_action :set_cache_headers before_action :set_cache_headers
PER_PAGE = 20 PER_PAGE = 20
@ -42,10 +41,6 @@ class Filters::StatusesController < ApplicationController
'remove' if params[:remove] 'remove' if params[:remove]
end end
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers def set_cache_headers
response.cache_control.replace(private: true, no_store: true) response.cache_control.replace(private: true, no_store: true)
end end

View File

@ -5,7 +5,6 @@ class FiltersController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_filter, only: [:edit, :update, :destroy] before_action :set_filter, only: [:edit, :update, :destroy]
before_action :set_body_classes
before_action :set_cache_headers before_action :set_cache_headers
def index def index
@ -52,10 +51,6 @@ class FiltersController < ApplicationController
params.require(:custom_filter).permit(:title, :expires_in, :filter_action, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]) params.require(:custom_filter).permit(:title, :expires_in, :filter_action, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy])
end end
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers def set_cache_headers
response.cache_control.replace(private: true, no_store: true) response.cache_control.replace(private: true, no_store: true)
end end

View File

@ -6,7 +6,6 @@ class InvitesController < ApplicationController
layout 'admin' layout 'admin'
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_body_classes
before_action :set_cache_headers before_action :set_cache_headers
def index def index
@ -47,10 +46,6 @@ class InvitesController < ApplicationController
params.require(:invite).permit(:max_uses, :expires_in, :autofollow, :comment) params.require(:invite).permit(:max_uses, :expires_in, :autofollow, :comment)
end end
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers def set_cache_headers
response.cache_control.replace(private: true, no_store: true) response.cache_control.replace(private: true, no_store: true)
end end

View File

@ -19,9 +19,7 @@ class MediaController < ApplicationController
redirect_to @media_attachment.file.url(:original) redirect_to @media_attachment.file.url(:original)
end end
def player def player; end
@body_classes = 'player'
end
private private

View File

@ -6,7 +6,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
before_action :store_current_location before_action :store_current_location
before_action :authenticate_resource_owner! before_action :authenticate_resource_owner!
before_action :require_not_suspended!, only: :destroy before_action :require_not_suspended!, only: :destroy
before_action :set_body_classes
before_action :set_cache_headers before_action :set_cache_headers
before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json } before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json }
@ -23,10 +22,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
private private
def set_body_classes
@body_classes = 'admin'
end
def store_current_location def store_current_location
store_location_for(:user, request.url) store_location_for(:user, request.url)
end end

View File

@ -4,7 +4,6 @@ class Redirect::BaseController < ApplicationController
vary_by 'Accept-Language' vary_by 'Accept-Language'
before_action :set_resource before_action :set_resource
before_action :set_app_body_class
def show def show
@redirect_path = ActivityPub::TagManager.instance.url_for(@resource) @redirect_path = ActivityPub::TagManager.instance.url_for(@resource)
@ -14,10 +13,6 @@ class Redirect::BaseController < ApplicationController
private private
def set_app_body_class
@body_classes = 'app-body'
end
def set_resource def set_resource
raise NotImplementedError raise NotImplementedError
end end

View File

@ -6,7 +6,6 @@ class RelationshipsController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_accounts, only: :show before_action :set_accounts, only: :show
before_action :set_relationships, only: :show before_action :set_relationships, only: :show
before_action :set_body_classes
before_action :set_cache_headers before_action :set_cache_headers
helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship? helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship?
@ -68,10 +67,6 @@ class RelationshipsController < ApplicationController
end end
end end
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers def set_cache_headers
response.cache_control.replace(private: true, no_store: true) response.cache_control.replace(private: true, no_store: true)
end end

View File

@ -4,15 +4,10 @@ class Settings::BaseController < ApplicationController
layout 'admin' layout 'admin'
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_body_classes
before_action :set_cache_headers before_action :set_cache_headers
private private
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers def set_cache_headers
response.cache_control.replace(private: true, no_store: true) response.cache_control.replace(private: true, no_store: true)
end end

View File

@ -2,14 +2,30 @@
class Settings::VerificationsController < Settings::BaseController class Settings::VerificationsController < Settings::BaseController
before_action :set_account before_action :set_account
before_action :set_verified_links
def show def show; end
@verified_links = @account.fields.select(&:verified?)
def update
if UpdateAccountService.new.call(@account, account_params)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
redirect_to settings_verification_path, notice: I18n.t('generic.changes_saved_msg')
else
render :show
end
end end
private private
def account_params
params.require(:account).permit(:attribution_domains_as_text)
end
def set_account def set_account
@account = current_account @account = current_account
end end
def set_verified_links
@verified_links = @account.fields.select(&:verified?)
end
end end

View File

@ -4,7 +4,6 @@ class SeveredRelationshipsController < ApplicationController
layout 'admin' layout 'admin'
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_body_classes
before_action :set_cache_headers before_action :set_cache_headers
before_action :set_event, only: [:following, :followers] before_action :set_event, only: [:following, :followers]
@ -51,10 +50,6 @@ class SeveredRelationshipsController < ApplicationController
account.local? ? account.local_username_and_domain : account.acct account.local? ? account.local_username_and_domain : account.acct
end end
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers def set_cache_headers
response.cache_control.replace(private: true, no_store: true) response.cache_control.replace(private: true, no_store: true)
end end

View File

@ -4,13 +4,6 @@ class SharesController < ApplicationController
layout 'modal' layout 'modal'
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_body_classes
def show; end def show; end
private
def set_body_classes
@body_classes = 'modal-layout compose-standalone'
end
end end

View File

@ -5,7 +5,6 @@ class StatusesCleanupController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_policy before_action :set_policy
before_action :set_body_classes
before_action :set_cache_headers before_action :set_cache_headers
def show; end def show; end
@ -34,10 +33,6 @@ class StatusesCleanupController < ApplicationController
params.require(:account_statuses_cleanup_policy).permit(:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :min_favs, :min_reblogs) params.require(:account_statuses_cleanup_policy).permit(:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :min_favs, :min_reblogs)
end end
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers def set_cache_headers
response.cache_control.replace(private: true, no_store: true) response.cache_control.replace(private: true, no_store: true)
end end

View File

@ -11,7 +11,6 @@ class StatusesController < ApplicationController
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_status before_action :set_status
before_action :redirect_to_original, only: :show before_action :redirect_to_original, only: :show
before_action :set_body_classes, only: :embed
after_action :set_link_headers after_action :set_link_headers
@ -51,12 +50,10 @@ class StatusesController < ApplicationController
private private
def set_body_classes
@body_classes = 'with-modals'
end
def set_link_headers def set_link_headers
response.headers['Link'] = LinkHeader.new([[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]]) response.headers['Link'] = LinkHeader.new(
[[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]]
).to_s
end end
def set_status def set_status

View File

@ -19,14 +19,6 @@ module AccountsHelper
end end
end end
def account_action_button(account)
return if account.memorial? || account.moved?
link_to ActivityPub::TagManager.instance.url_for(account), class: 'button logo-button', target: '_new' do
safe_join([logo_as_symbol, t('accounts.follow')])
end
end
def hide_followers_count?(account) def hide_followers_count?(account)
Setting.hide_followers_count || account.user&.settings&.[]('hide_followers_count') Setting.hide_followers_count || account.user&.settings&.[]('hide_followers_count')
end end

View File

@ -5,7 +5,7 @@ module Admin::Trends::StatusesHelper
text = if status.local? text = if status.local?
status.text.split("\n").first status.text.split("\n").first
else else
Nokogiri::HTML(status.text).css('html > body > *').first&.text Nokogiri::HTML5(status.text).css('html > body > *').first&.text
end end
return '' if text.blank? return '' if text.blank?

View File

@ -86,7 +86,7 @@ module ApplicationHelper
def html_title def html_title
safe_join( safe_join(
[content_for(:page_title).to_s.chomp, title] [content_for(:page_title).to_s.chomp, title]
.select(&:present?), .compact_blank,
' - ' ' - '
) )
end end
@ -106,11 +106,16 @@ module ApplicationHelper
end end
def material_symbol(icon, attributes = {}) def material_symbol(icon, attributes = {})
safe_join(
[
inline_svg_tag( inline_svg_tag(
"400-24px/#{icon}.svg", "400-24px/#{icon}.svg",
class: ['icon', "material-#{icon}"].concat(attributes[:class].to_s.split), class: ['icon', "material-#{icon}"].concat(attributes[:class].to_s.split),
role: :img, role: :img,
data: attributes[:data] data: attributes[:data]
),
' ',
]
) )
end end
@ -154,6 +159,7 @@ module ApplicationHelper
def body_classes def body_classes
output = body_class_string.split output = body_class_string.split
output << content_for(:body_classes)
output << "flavour-#{current_flavour.parameterize}" output << "flavour-#{current_flavour.parameterize}"
output << "skin-#{current_skin.parameterize}" output << "skin-#{current_skin.parameterize}"
output << 'system-font' if current_account&.user&.setting_system_font_ui output << 'system-font' if current_account&.user&.setting_system_font_ui

View File

@ -24,24 +24,8 @@ module ContextHelper
indexable: { 'toot' => 'http://joinmastodon.org/ns#', 'indexable' => 'toot:indexable' }, indexable: { 'toot' => 'http://joinmastodon.org/ns#', 'indexable' => 'toot:indexable' },
memorial: { 'toot' => 'http://joinmastodon.org/ns#', 'memorial' => 'toot:memorial' }, memorial: { 'toot' => 'http://joinmastodon.org/ns#', 'memorial' => 'toot:memorial' },
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' }, voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
olm: {
'toot' => 'http://joinmastodon.org/ns#',
'Device' => 'toot:Device',
'Ed25519Signature' => 'toot:Ed25519Signature',
'Ed25519Key' => 'toot:Ed25519Key',
'Curve25519Key' => 'toot:Curve25519Key',
'EncryptedMessage' => 'toot:EncryptedMessage',
'publicKeyBase64' => 'toot:publicKeyBase64',
'deviceId' => 'toot:deviceId',
'claim' => { '@type' => '@id', '@id' => 'toot:claim' },
'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' },
'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' },
'devices' => { '@type' => '@id', '@id' => 'toot:devices' },
'messageFranking' => 'toot:messageFranking',
'messageType' => 'toot:messageType',
'cipherText' => 'toot:cipherText',
},
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
}.freeze }.freeze
def full_context def full_context

View File

@ -200,14 +200,6 @@ module JsonLdHelper
nil nil
end end
def merge_context(context, new_context)
if context.is_a?(Array)
context << new_context
else
[context, new_context]
end
end
def response_successful?(response) def response_successful?(response)
(200...300).cover?(response.code) (200...300).cover?(response.code)
end end

View File

@ -238,9 +238,7 @@ module LanguagesHelper
# Helper for self.sorted_locale_keys # Helper for self.sorted_locale_keys
private_class_method def self.locale_name_for_sorting(locale) private_class_method def self.locale_name_for_sorting(locale)
if locale.blank? || locale == 'und' if (supported_locale = SUPPORTED_LOCALES[locale.to_sym])
'000'
elsif (supported_locale = SUPPORTED_LOCALES[locale.to_sym])
ASCIIFolding.new.fold(supported_locale[1]).downcase ASCIIFolding.new.fold(supported_locale[1]).downcase
elsif (regional_locale = REGIONAL_LOCALE_NAMES[locale.to_sym]) elsif (regional_locale = REGIONAL_LOCALE_NAMES[locale.to_sym])
ASCIIFolding.new.fold(regional_locale).downcase ASCIIFolding.new.fold(regional_locale).downcase

View File

@ -57,26 +57,6 @@ module MediaComponentHelper
end end
end end
def render_card_component(status, **options)
component_params = {
sensitive: sensitive_viewer?(status, current_account),
card: serialize_status_card(status).as_json,
}.merge(**options)
react_component :card, component_params
end
def render_poll_component(status, **options)
component_params = {
disabled: true,
poll: serialize_status_poll(status).as_json,
}.merge(**options)
react_component :poll, component_params do
render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: prefers_autoplay? }
end
end
private private
def serialize_media_attachment(attachment) def serialize_media_attachment(attachment)
@ -86,22 +66,6 @@ module MediaComponentHelper
) )
end end
def serialize_status_card(status)
ActiveModelSerializers::SerializableResource.new(
status.preview_card,
serializer: REST::PreviewCardSerializer
)
end
def serialize_status_poll(status)
ActiveModelSerializers::SerializableResource.new(
status.preloadable_poll,
serializer: REST::PollSerializer,
scope: current_user,
scope_name: :current_user
)
end
def sensitive_viewer?(status, account) def sensitive_viewer?(status, account)
if !account.nil? && account.id == status.account_id if !account.nil? && account.id == status.account_id
status.sensitive status.sensitive

View File

@ -4,6 +4,13 @@ module StatusesHelper
EMBEDDED_CONTROLLER = 'statuses' EMBEDDED_CONTROLLER = 'statuses'
EMBEDDED_ACTION = 'embed' EMBEDDED_ACTION = 'embed'
VISIBLITY_ICONS = {
public: 'globe',
unlisted: 'lock_open',
private: 'lock',
direct: 'alternate_email',
}.freeze
def nothing_here(extra_classes = '') def nothing_here(extra_classes = '')
content_tag(:div, class: "nothing-here #{extra_classes}") do content_tag(:div, class: "nothing-here #{extra_classes}") do
t('accounts.nothing_here') t('accounts.nothing_here')
@ -57,17 +64,8 @@ module StatusesHelper
embedded_view? ? '_blank' : nil embedded_view? ? '_blank' : nil
end end
def fa_visibility_icon(status) def visibility_icon(status)
case status.visibility VISIBLITY_ICONS[status.visibility.to_sym]
when 'public'
material_symbol 'globe'
when 'unlisted'
material_symbol 'lock_open'
when 'private'
material_symbol 'lock'
when 'direct'
material_symbol 'alternate_email'
end
end end
def embedded_view? def embedded_view?

View File

@ -0,0 +1,74 @@
import './public-path';
import { createRoot } from 'react-dom/client';
import { afterInitialRender } from 'mastodon/../hooks/useRenderSignal';
import { start } from '../mastodon/common';
import { Status } from '../mastodon/features/standalone/status';
import { loadPolyfills } from '../mastodon/polyfills';
import ready from '../mastodon/ready';
start();
function loaded() {
const mountNode = document.getElementById('mastodon-status');
if (mountNode) {
const attr = mountNode.getAttribute('data-props');
if (!attr) return;
const props = JSON.parse(attr) as { id: string; locale: string };
const root = createRoot(mountNode);
root.render(<Status {...props} />);
}
}
function main() {
ready(loaded).catch((error: unknown) => {
console.error(error);
});
}
loadPolyfills()
.then(main)
.catch((error: unknown) => {
console.error(error);
});
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;
// We use a timeout to allow for the React page to render before calculating the height
afterInitialRender(() => {
window.parent.postMessage(
{
type: 'setHeight',
id: data.id,
height: document.getElementsByTagName('html')[0]?.scrollHeight,
},
'*',
);
});
});

View File

@ -37,43 +37,6 @@ const messages = defineMessages({
}, },
}); });
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: unknown) => {
console.error('Error in setHeightMessage postMessage', e);
});
});
function loaded() { function loaded() {
const { messages: localeData } = getLocale(); const { messages: localeData } = getLocale();

View File

@ -6,5 +6,4 @@ export const submitAccountNote = createDataLoadingThunk(
({ accountId, note }: { accountId: string; note: string }) => ({ accountId, note }: { accountId: string; note: string }) =>
apiSubmitAccountNote(accountId, note), apiSubmitAccountNote(accountId, note),
(relationship) => ({ relationship }), (relationship) => ({ relationship }),
{ skipLoading: true },
); );

View File

@ -1,4 +1,5 @@
import { browserHistory } from 'flavours/glitch/components/router'; import { browserHistory } from 'flavours/glitch/components/router';
import { debounceWithDispatchAndArguments } from 'flavours/glitch/utils/debounce';
import api, { getLinks } from '../api'; import api, { getLinks } from '../api';
@ -462,6 +463,20 @@ export function expandFollowingFail(id, error) {
}; };
} }
const debouncedFetchRelationships = debounceWithDispatchAndArguments((dispatch, ...newAccountIds) => {
if (newAccountIds.length === 0) {
return;
}
dispatch(fetchRelationshipsRequest(newAccountIds));
api().get(`/api/v1/accounts/relationships?with_suspended=true&${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
dispatch(fetchRelationshipsSuccess({ relationships: response.data }));
}).catch(error => {
dispatch(fetchRelationshipsFail(error));
});
}, { delay: 500 });
export function fetchRelationships(accountIds) { export function fetchRelationships(accountIds) {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
@ -473,13 +488,7 @@ export function fetchRelationships(accountIds) {
return; return;
} }
dispatch(fetchRelationshipsRequest(newAccountIds)); debouncedFetchRelationships(dispatch, ...newAccountIds);
api().get(`/api/v1/accounts/relationships?with_suspended=true&${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
dispatch(fetchRelationshipsSuccess({ relationships: response.data }));
}).catch(error => {
dispatch(fetchRelationshipsFail(error));
});
}; };
} }

View File

@ -1,5 +1,7 @@
import { defineMessages } from 'react-intl'; import { defineMessages } from 'react-intl';
import { AxiosError } from 'axios';
const messages = defineMessages({ const messages = defineMessages({
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' }, unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' }, unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
@ -50,6 +52,11 @@ export const showAlertForError = (error, skipNotFound = false) => {
}); });
} }
// An aborted request, e.g. due to reloading the browser window, it not really error
if (error.code === AxiosError.ECONNABORTED) {
return { type: ALERT_NOOP };
}
console.error(error); console.error(error);
return showAlert({ return showAlert({

View File

@ -1,12 +0,0 @@
import { saveSettings } from './settings';
export const LANGUAGE_USE = 'LANGUAGE_USE';
export const useLanguage = language => dispatch => {
dispatch({
type: LANGUAGE_USE,
language,
});
dispatch(saveSettings());
};

View File

@ -64,7 +64,7 @@ export const synchronouslySubmitMarkers = createAppAsyncThunk(
client.setRequestHeader('Content-Type', 'application/json'); client.setRequestHeader('Content-Type', 'application/json');
client.setRequestHeader('Authorization', `Bearer ${accessToken}`); client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
client.send(JSON.stringify(params)); client.send(JSON.stringify(params));
} catch (e) { } catch {
// Do not make the BeforeUnload handler error out // Do not make the BeforeUnload handler error out
} }
}, },
@ -75,17 +75,7 @@ interface MarkerParam {
} }
function getLastNotificationId(state: RootState): string | undefined { function getLastNotificationId(state: RootState): string | undefined {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access return state.notificationGroups.lastReadId;
const enableBeta = state.settings.getIn(
['notifications', 'groupingBeta'],
false,
) as boolean;
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return enableBeta
? state.notificationGroups.lastReadId
: // @ts-expect-error state.notifications is not yet typed
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
state.getIn(['notifications', 'lastReadId']);
} }
const buildPostMarkersParams = (state: RootState) => { const buildPostMarkersParams = (state: RootState) => {

View File

@ -2,7 +2,7 @@ import { createAction } from '@reduxjs/toolkit';
import { import {
apiClearNotifications, apiClearNotifications,
apiFetchNotifications, apiFetchNotificationGroups,
} from 'flavours/glitch/api/notifications'; } from 'flavours/glitch/api/notifications';
import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts'; import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts';
import type { import type {
@ -18,7 +18,7 @@ import {
selectSettingsNotificationsQuickFilterActive, selectSettingsNotificationsQuickFilterActive,
selectSettingsNotificationsShows, selectSettingsNotificationsShows,
} from 'flavours/glitch/selectors/settings'; } from 'flavours/glitch/selectors/settings';
import type { AppDispatch } from 'flavours/glitch/store'; import type { AppDispatch, RootState } from 'flavours/glitch/store';
import { import {
createAppAsyncThunk, createAppAsyncThunk,
createDataLoadingThunk, createDataLoadingThunk,
@ -32,6 +32,14 @@ function excludeAllTypesExcept(filter: string) {
return allNotificationTypes.filter((item) => item !== filter); return allNotificationTypes.filter((item) => item !== filter);
} }
function getExcludedTypes(state: RootState) {
const activeFilter = selectSettingsNotificationsQuickFilterActive(state);
return activeFilter === 'all'
? selectSettingsNotificationsExcludedTypes(state)
: excludeAllTypesExcept(activeFilter);
}
function dispatchAssociatedRecords( function dispatchAssociatedRecords(
dispatch: AppDispatch, dispatch: AppDispatch,
notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[], notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[],
@ -60,19 +68,15 @@ function dispatchAssociatedRecords(
dispatch(importFetchedStatuses(fetchedStatuses)); dispatch(importFetchedStatuses(fetchedStatuses));
} }
const supportedGroupedNotificationTypes = ['favourite', 'reblog'];
export const fetchNotifications = createDataLoadingThunk( export const fetchNotifications = createDataLoadingThunk(
'notificationGroups/fetch', 'notificationGroups/fetch',
async (_params, { getState }) => { async (_params, { getState }) =>
const activeFilter = apiFetchNotificationGroups({
selectSettingsNotificationsQuickFilterActive(getState()); grouped_types: supportedGroupedNotificationTypes,
exclude_types: getExcludedTypes(getState()),
return apiFetchNotifications({ }),
exclude_types:
activeFilter === 'all'
? selectSettingsNotificationsExcludedTypes(getState())
: excludeAllTypesExcept(activeFilter),
});
},
({ notifications, accounts, statuses }, { dispatch }) => { ({ notifications, accounts, statuses }, { dispatch }) => {
dispatch(importFetchedAccounts(accounts)); dispatch(importFetchedAccounts(accounts));
dispatch(importFetchedStatuses(statuses)); dispatch(importFetchedStatuses(statuses));
@ -92,9 +96,12 @@ export const fetchNotifications = createDataLoadingThunk(
export const fetchNotificationsGap = createDataLoadingThunk( export const fetchNotificationsGap = createDataLoadingThunk(
'notificationGroups/fetchGap', 'notificationGroups/fetchGap',
async (params: { gap: NotificationGap }) => async (params: { gap: NotificationGap }, { getState }) =>
apiFetchNotifications({ max_id: params.gap.maxId }), apiFetchNotificationGroups({
grouped_types: supportedGroupedNotificationTypes,
max_id: params.gap.maxId,
exclude_types: getExcludedTypes(getState()),
}),
({ notifications, accounts, statuses }, { dispatch }) => { ({ notifications, accounts, statuses }, { dispatch }) => {
dispatch(importFetchedAccounts(accounts)); dispatch(importFetchedAccounts(accounts));
dispatch(importFetchedStatuses(statuses)); dispatch(importFetchedStatuses(statuses));
@ -107,8 +114,10 @@ export const fetchNotificationsGap = createDataLoadingThunk(
export const pollRecentNotifications = createDataLoadingThunk( export const pollRecentNotifications = createDataLoadingThunk(
'notificationGroups/pollRecentNotifications', 'notificationGroups/pollRecentNotifications',
async (_params, { getState }) => { async (_params, { getState }) => {
return apiFetchNotifications({ return apiFetchNotificationGroups({
grouped_types: supportedGroupedNotificationTypes,
max_id: undefined, max_id: undefined,
exclude_types: getExcludedTypes(getState()),
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones // In slow mode, we don't want to include notifications that duplicate the already-displayed ones
since_id: usePendingItems since_id: usePendingItems
? getState().notificationGroups.groups.find( ? getState().notificationGroups.groups.find(
@ -183,7 +192,6 @@ export const setNotificationsFilter = createAppAsyncThunk(
path: ['notifications', 'quickFilter', 'active'], path: ['notifications', 'quickFilter', 'active'],
value: filterType, value: filterType,
}); });
// dispatch(expandNotifications({ forceLoad: true }));
void dispatch(fetchNotifications()); void dispatch(fetchNotifications());
dispatch(saveSettings()); dispatch(saveSettings());
}, },

View File

@ -17,6 +17,6 @@ export const updateNotificationsPolicy = createDataLoadingThunk(
(policy: Partial<NotificationPolicy>) => apiUpdateNotificationsPolicy(policy), (policy: Partial<NotificationPolicy>) => apiUpdateNotificationsPolicy(policy),
); );
export const decreasePendingNotificationsCount = createAction<number>( export const decreasePendingRequestsCount = createAction<number>(
'notificationPolicy/decreasePendingNotificationCount', 'notificationPolicy/decreasePendingRequestsCount',
); );

View File

@ -0,0 +1,214 @@
import {
apiFetchNotificationRequest,
apiFetchNotificationRequests,
apiFetchNotifications,
apiAcceptNotificationRequest,
apiDismissNotificationRequest,
apiAcceptNotificationRequests,
apiDismissNotificationRequests,
} from 'flavours/glitch/api/notifications';
import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts';
import type {
ApiNotificationGroupJSON,
ApiNotificationJSON,
} from 'flavours/glitch/api_types/notifications';
import type { ApiStatusJSON } from 'flavours/glitch/api_types/statuses';
import type { AppDispatch } from 'flavours/glitch/store';
import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions';
import { importFetchedAccounts, importFetchedStatuses } from './importer';
import { decreasePendingRequestsCount } from './notification_policies';
// TODO: refactor with notification_groups
function dispatchAssociatedRecords(
dispatch: AppDispatch,
notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[],
) {
const fetchedAccounts: ApiAccountJSON[] = [];
const fetchedStatuses: ApiStatusJSON[] = [];
notifications.forEach((notification) => {
if (notification.type === 'admin.report') {
fetchedAccounts.push(notification.report.target_account);
}
if (notification.type === 'moderation_warning') {
fetchedAccounts.push(notification.moderation_warning.target_account);
}
if ('status' in notification && notification.status) {
fetchedStatuses.push(notification.status);
}
});
if (fetchedAccounts.length > 0)
dispatch(importFetchedAccounts(fetchedAccounts));
if (fetchedStatuses.length > 0)
dispatch(importFetchedStatuses(fetchedStatuses));
}
export const fetchNotificationRequests = createDataLoadingThunk(
'notificationRequests/fetch',
async (_params, { getState }) => {
let sinceId = undefined;
if (getState().notificationRequests.items.length > 0) {
sinceId = getState().notificationRequests.items[0]?.id;
}
return apiFetchNotificationRequests({
since_id: sinceId,
});
},
({ requests, links }, { dispatch }) => {
const next = links.refs.find((link) => link.rel === 'next');
dispatch(importFetchedAccounts(requests.map((request) => request.account)));
return { requests, next: next?.uri };
},
{
condition: (_params, { getState }) =>
!getState().notificationRequests.isLoading,
},
);
export const fetchNotificationRequest = createDataLoadingThunk(
'notificationRequest/fetch',
async ({ id }: { id: string }) => apiFetchNotificationRequest(id),
{
condition: ({ id }, { getState }) =>
!(
getState().notificationRequests.current.item?.id === id ||
getState().notificationRequests.current.isLoading
),
},
);
export const expandNotificationRequests = createDataLoadingThunk(
'notificationRequests/expand',
async (_, { getState }) => {
const nextUrl = getState().notificationRequests.next;
if (!nextUrl) throw new Error('missing URL');
return apiFetchNotificationRequests(undefined, nextUrl);
},
({ requests, links }, { dispatch }) => {
const next = links.refs.find((link) => link.rel === 'next');
dispatch(importFetchedAccounts(requests.map((request) => request.account)));
return { requests, next: next?.uri };
},
{
condition: (_, { getState }) =>
!!getState().notificationRequests.next &&
!getState().notificationRequests.isLoading,
},
);
export const fetchNotificationsForRequest = createDataLoadingThunk(
'notificationRequest/fetchNotifications',
async ({ accountId }: { accountId: string }, { getState }) => {
const sinceId =
// @ts-expect-error current.notifications.items is not yet typed
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
getState().notificationRequests.current.notifications.items[0]?.get(
'id',
) as string | undefined;
return apiFetchNotifications({
since_id: sinceId,
account_id: accountId,
});
},
({ notifications, links }, { dispatch }) => {
const next = links.refs.find((link) => link.rel === 'next');
dispatchAssociatedRecords(dispatch, notifications);
return { notifications, next: next?.uri };
},
{
condition: ({ accountId }, { getState }) => {
const current = getState().notificationRequests.current;
return !(
current.item?.account_id === accountId &&
current.notifications.isLoading
);
},
},
);
export const expandNotificationsForRequest = createDataLoadingThunk(
'notificationRequest/expandNotifications',
async (_, { getState }) => {
const nextUrl = getState().notificationRequests.current.notifications.next;
if (!nextUrl) throw new Error('missing URL');
return apiFetchNotifications(undefined, nextUrl);
},
({ notifications, links }, { dispatch }) => {
const next = links.refs.find((link) => link.rel === 'next');
dispatchAssociatedRecords(dispatch, notifications);
return { notifications, next: next?.uri };
},
{
condition: ({ accountId }: { accountId: string }, { getState }) => {
const url = getState().notificationRequests.current.notifications.next;
return (
!!url &&
!getState().notificationRequests.current.notifications.isLoading &&
getState().notificationRequests.current.item?.account_id === accountId
);
},
},
);
export const acceptNotificationRequest = createDataLoadingThunk(
'notificationRequest/accept',
({ id }: { id: string }) => apiAcceptNotificationRequest(id),
(_data, { dispatch, discardLoadData }) => {
dispatch(decreasePendingRequestsCount(1));
// The payload is not used in any functions
return discardLoadData;
},
);
export const dismissNotificationRequest = createDataLoadingThunk(
'notificationRequest/dismiss',
({ id }: { id: string }) => apiDismissNotificationRequest(id),
(_data, { dispatch, discardLoadData }) => {
dispatch(decreasePendingRequestsCount(1));
// The payload is not used in any functions
return discardLoadData;
},
);
export const acceptNotificationRequests = createDataLoadingThunk(
'notificationRequests/acceptBulk',
({ ids }: { ids: string[] }) => apiAcceptNotificationRequests(ids),
(_data, { dispatch, discardLoadData, actionArg: { ids } }) => {
dispatch(decreasePendingRequestsCount(ids.length));
// The payload is not used in any functions
return discardLoadData;
},
);
export const dismissNotificationRequests = createDataLoadingThunk(
'notificationRequests/dismissBulk',
({ ids }: { ids: string[] }) => apiDismissNotificationRequests(ids),
(_data, { dispatch, discardLoadData, actionArg: { ids } }) => {
dispatch(decreasePendingRequestsCount(ids.length));
// The payload is not used in any functions
return discardLoadData;
},
);

View File

@ -10,7 +10,7 @@ import api, { getLinks } from '../api';
import { unescapeHTML } from '../utils/html'; import { unescapeHTML } from '../utils/html';
import { requestNotificationPermission } from '../utils/notifications'; import { requestNotificationPermission } from '../utils/notifications';
import { fetchFollowRequests, fetchRelationships } from './accounts'; import { fetchFollowRequests } from './accounts';
import { import {
importFetchedAccount, importFetchedAccount,
importFetchedAccounts, importFetchedAccounts,
@ -18,7 +18,6 @@ import {
importFetchedStatuses, importFetchedStatuses,
} from './importer'; } from './importer';
import { submitMarkers } from './markers'; import { submitMarkers } from './markers';
import { decreasePendingNotificationsCount } from './notification_policies';
import { notificationsUpdate } from "./notifications_typed"; import { notificationsUpdate } from "./notifications_typed";
import { register as registerPushNotifications } from './push_notifications'; import { register as registerPushNotifications } from './push_notifications';
import { saveSettings } from './settings'; import { saveSettings } from './settings';
@ -57,26 +56,6 @@ 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_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';
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 NOTIFICATION_REQUESTS_ACCEPT_REQUEST = 'NOTIFICATION_REQUESTS_ACCEPT_REQUEST'; export const NOTIFICATION_REQUESTS_ACCEPT_REQUEST = 'NOTIFICATION_REQUESTS_ACCEPT_REQUEST';
export const NOTIFICATION_REQUESTS_ACCEPT_SUCCESS = 'NOTIFICATION_REQUESTS_ACCEPT_SUCCESS'; export const NOTIFICATION_REQUESTS_ACCEPT_SUCCESS = 'NOTIFICATION_REQUESTS_ACCEPT_SUCCESS';
export const NOTIFICATION_REQUESTS_ACCEPT_FAIL = 'NOTIFICATION_REQUESTS_ACCEPT_FAIL'; export const NOTIFICATION_REQUESTS_ACCEPT_FAIL = 'NOTIFICATION_REQUESTS_ACCEPT_FAIL';
@ -85,32 +64,10 @@ export const NOTIFICATION_REQUESTS_DISMISS_REQUEST = 'NOTIFICATION_REQUESTS_DISM
export const NOTIFICATION_REQUESTS_DISMISS_SUCCESS = 'NOTIFICATION_REQUESTS_DISMISS_SUCCESS'; export const NOTIFICATION_REQUESTS_DISMISS_SUCCESS = 'NOTIFICATION_REQUESTS_DISMISS_SUCCESS';
export const NOTIFICATION_REQUESTS_DISMISS_FAIL = 'NOTIFICATION_REQUESTS_DISMISS_FAIL'; export const NOTIFICATION_REQUESTS_DISMISS_FAIL = 'NOTIFICATION_REQUESTS_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({ defineMessages({
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
}); });
const fetchRelatedRelationships = (dispatch, notifications) => {
const accountIds = notifications.filter(item => ['follow', 'follow_request', 'admin.sign_up'].indexOf(item.type) !== -1).map(item => item.account.id);
if (accountIds.length > 0) {
dispatch(fetchRelationships(accountIds));
}
};
const selectNotificationCountForRequest = (state, id) => {
const requests = state.getIn(['notificationRequests', 'items']);
const thisRequest = requests.find(request => request.get('id') === id);
return thisRequest ? thisRequest.get('notifications_count') : 0;
};
export const loadPending = () => ({ export const loadPending = () => ({
type: NOTIFICATIONS_LOAD_PENDING, type: NOTIFICATIONS_LOAD_PENDING,
}); });
@ -153,8 +110,6 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
dispatch(notificationsUpdate({ notification, preferPendingItems, playSound: playSound && !filtered})); dispatch(notificationsUpdate({ notification, preferPendingItems, playSound: playSound && !filtered}));
fetchRelatedRelationships(dispatch, [notification]);
} else if (playSound && !filtered) { } else if (playSound && !filtered) {
dispatch({ dispatch({
type: NOTIFICATIONS_UPDATE_NOOP, type: NOTIFICATIONS_UPDATE_NOOP,
@ -247,7 +202,6 @@ export function expandNotifications({ maxId = undefined, forceLoad = false }) {
dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account))); dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems)); dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
fetchRelatedRelationships(dispatch, response.data);
dispatch(submitMarkers()); dispatch(submitMarkers());
} catch(error) { } catch(error) {
dispatch(expandNotificationsFail(error, isLoadingMore)); dispatch(expandNotificationsFail(error, isLoadingMore));
@ -432,296 +386,3 @@ export function setBrowserPermission (value) {
value, value,
}; };
} }
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().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().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().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) => {
const count = selectNotificationCountForRequest(getState(), id);
dispatch(acceptNotificationRequestRequest(id));
api().post(`/api/v1/notifications/requests/${id}/accept`).then(() => {
dispatch(acceptNotificationRequestSuccess(id));
dispatch(decreasePendingNotificationsCount(count));
}).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) => {
const count = selectNotificationCountForRequest(getState(), id);
dispatch(dismissNotificationRequestRequest(id));
api().post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{
dispatch(dismissNotificationRequestSuccess(id));
dispatch(decreasePendingNotificationsCount(count));
}).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 acceptNotificationRequests = (ids) => (dispatch, getState) => {
const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0);
dispatch(acceptNotificationRequestsRequest(ids));
api().post(`/api/v1/notifications/requests/accept`, { id: ids }).then(() => {
dispatch(acceptNotificationRequestsSuccess(ids));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(acceptNotificationRequestFail(ids, err));
});
};
export const acceptNotificationRequestsRequest = ids => ({
type: NOTIFICATION_REQUESTS_ACCEPT_REQUEST,
ids,
});
export const acceptNotificationRequestsSuccess = ids => ({
type: NOTIFICATION_REQUESTS_ACCEPT_SUCCESS,
ids,
});
export const acceptNotificationRequestsFail = (ids, error) => ({
type: NOTIFICATION_REQUESTS_ACCEPT_FAIL,
ids,
error,
});
export const dismissNotificationRequests = (ids) => (dispatch, getState) => {
const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0);
dispatch(acceptNotificationRequestsRequest(ids));
api().post(`/api/v1/notifications/requests/dismiss`, { id: ids }).then(() => {
dispatch(dismissNotificationRequestsSuccess(ids));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(dismissNotificationRequestFail(ids, err));
});
};
export const dismissNotificationRequestsRequest = ids => ({
type: NOTIFICATION_REQUESTS_DISMISS_REQUEST,
ids,
});
export const dismissNotificationRequestsSuccess = ids => ({
type: NOTIFICATION_REQUESTS_DISMISS_SUCCESS,
ids,
});
export const dismissNotificationRequestsFail = (ids, error) => ({
type: NOTIFICATION_REQUESTS_DISMISS_FAIL,
ids,
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().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().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,
});

View File

@ -1,18 +1,10 @@
import { createAppAsyncThunk } from 'flavours/glitch/store'; import { createAppAsyncThunk } from 'flavours/glitch/store';
import { fetchNotifications } from './notification_groups'; import { fetchNotifications } from './notification_groups';
import { expandNotifications } from './notifications';
export const initializeNotifications = createAppAsyncThunk( export const initializeNotifications = createAppAsyncThunk(
'notifications/initialize', 'notifications/initialize',
(_, { dispatch, getState }) => { (_, { dispatch }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access void dispatch(fetchNotifications());
const enableBeta = getState().settings.getIn(
['notifications', 'groupingBeta'],
false,
) as boolean;
if (enableBeta) void dispatch(fetchNotifications());
else void dispatch(expandNotifications({}));
}, },
); );

View File

@ -49,11 +49,13 @@ export function fetchStatusRequest(id, skipLoading) {
}; };
} }
export function fetchStatus(id, forceFetch = false) { export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
return (dispatch, getState) => { return (dispatch, getState) => {
const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null; const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
if (alsoFetchContext) {
dispatch(fetchContext(id)); dispatch(fetchContext(id));
}
if (skipLoading) { if (skipLoading) {
return; return;

View File

@ -103,18 +103,14 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
const notificationJSON = JSON.parse(data.payload); const notificationJSON = JSON.parse(data.payload);
dispatch(updateNotifications(notificationJSON, messages, locale)); dispatch(updateNotifications(notificationJSON, messages, locale));
// TODO: remove this once the groups feature replaces the previous one // TODO: remove this once the groups feature replaces the previous one
if(getState().settings.getIn(['notifications', 'groupingBeta'], false)) {
dispatch(processNewNotificationForGroups(notificationJSON)); dispatch(processNewNotificationForGroups(notificationJSON));
}
break; break;
} }
case 'notifications_merged': case 'notifications_merged':
const state = getState(); const state = getState();
if (state.notifications.top || !state.notifications.mounted) if (state.notifications.top || !state.notifications.mounted)
dispatch(expandNotifications({ forceLoad: true, maxId: undefined })); dispatch(expandNotifications({ forceLoad: true, maxId: undefined }));
if(state.settings.getIn(['notifications', 'groupingBeta'], false)) {
dispatch(refreshStaleNotificationGroups()); dispatch(refreshStaleNotificationGroups());
}
break; break;
case 'conversation': case 'conversation':
// @ts-expect-error // @ts-expect-error
@ -139,22 +135,16 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
/** /**
* @param {Function} dispatch * @param {Function} dispatch
* @param {Function} getState
*/ */
async function refreshHomeTimelineAndNotification(dispatch, getState) { async function refreshHomeTimelineAndNotification(dispatch) {
await dispatch(expandHomeTimeline({ maxId: undefined })); await dispatch(expandHomeTimeline({ maxId: undefined }));
// TODO: remove this once the groups feature replaces the previous one
if(getState().settings.getIn(['notifications', 'groupingBeta'], false)) {
// TODO: polling for merged notifications // TODO: polling for merged notifications
try { try {
await dispatch(pollRecentGroupNotifications()); await dispatch(pollRecentGroupNotifications());
} catch (error) { } catch {
// TODO // TODO
} }
} else {
await dispatch(expandNotifications({}));
}
await dispatch(fetchAnnouncements()); await dispatch(fetchAnnouncements());
} }

View File

@ -42,6 +42,9 @@ const authorizationTokenFromInitialState = (): RawAxiosRequestHeaders => {
// eslint-disable-next-line import/no-default-export // eslint-disable-next-line import/no-default-export
export default function api(withAuthorization = true) { export default function api(withAuthorization = true) {
return axios.create({ return axios.create({
transitional: {
clarifyTimeoutError: true,
},
headers: { headers: {
...csrfHeader, ...csrfHeader,
...(withAuthorization ? authorizationTokenFromInitialState() : {}), ...(withAuthorization ? authorizationTokenFromInitialState() : {}),

View File

@ -1,14 +1,44 @@
import api, { apiRequest, getLinks } from 'flavours/glitch/api'; import api, {
import type { ApiNotificationGroupsResultJSON } from 'flavours/glitch/api_types/notifications'; apiRequest,
getLinks,
apiRequestGet,
apiRequestPost,
} from 'flavours/glitch/api';
import type {
ApiNotificationGroupsResultJSON,
ApiNotificationRequestJSON,
ApiNotificationJSON,
} from 'flavours/glitch/api_types/notifications';
export const apiFetchNotifications = async (params?: { export const apiFetchNotifications = async (
params?: {
account_id?: string;
since_id?: string;
},
url?: string,
) => {
const response = await api().request<ApiNotificationJSON[]>({
method: 'GET',
url: url ?? '/api/v1/notifications',
params,
});
return {
notifications: response.data,
links: getLinks(response),
};
};
export const apiFetchNotificationGroups = async (params?: {
url?: string;
grouped_types?: string[];
exclude_types?: string[]; exclude_types?: string[];
max_id?: string; max_id?: string;
since_id?: string; since_id?: string;
}) => { }) => {
const response = await api().request<ApiNotificationGroupsResultJSON>({ const response = await api().request<ApiNotificationGroupsResultJSON>({
method: 'GET', method: 'GET',
url: '/api/v2_alpha/notifications', url: '/api/v2/notifications',
params, params,
}); });
@ -24,3 +54,43 @@ export const apiFetchNotifications = async (params?: {
export const apiClearNotifications = () => export const apiClearNotifications = () =>
apiRequest<undefined>('POST', 'v1/notifications/clear'); apiRequest<undefined>('POST', 'v1/notifications/clear');
export const apiFetchNotificationRequests = async (
params?: {
since_id?: string;
},
url?: string,
) => {
const response = await api().request<ApiNotificationRequestJSON[]>({
method: 'GET',
url: url ?? '/api/v1/notifications/requests',
params,
});
return {
requests: response.data,
links: getLinks(response),
};
};
export const apiFetchNotificationRequest = async (id: string) => {
return apiRequestGet<ApiNotificationRequestJSON>(
`v1/notifications/requests/${id}`,
);
};
export const apiAcceptNotificationRequest = async (id: string) => {
return apiRequestPost(`v1/notifications/requests/${id}/accept`);
};
export const apiDismissNotificationRequest = async (id: string) => {
return apiRequestPost(`v1/notifications/requests/${id}/dismiss`);
};
export const apiAcceptNotificationRequests = async (id: string[]) => {
return apiRequestPost('v1/notifications/requests/accept', { id });
};
export const apiDismissNotificationRequests = async (id: string[]) => {
return apiRequestPost('v1/notifications/requests/dismiss', { id });
};

View File

@ -151,3 +151,12 @@ export interface ApiNotificationGroupsResultJSON {
statuses: ApiStatusJSON[]; statuses: ApiStatusJSON[];
notification_groups: ApiNotificationGroupJSON[]; notification_groups: ApiNotificationGroupJSON[];
} }
export interface ApiNotificationRequestJSON {
id: string;
created_at: string;
updated_at: string;
notifications_count: string;
account: ApiAccountJSON;
last_status?: ApiStatusJSON;
}

View File

@ -5,7 +5,7 @@ export function start() {
try { try {
Rails.start(); Rails.start();
} catch (e) { } catch {
// If called twice // If called twice
} }
} }

View File

@ -0,0 +1,27 @@
/* Significantly rewritten from upstream to keep the old design for now */
import { FormattedMessage } from 'react-intl';
export const ContentWarning: React.FC<{
text: string;
expanded?: boolean;
onClick?: () => void;
icons?: React.ReactNode[];
}> = ({ text, expanded, onClick, icons }) => (
<p>
<span dangerouslySetInnerHTML={{ __html: text }} className='translate' />{' '}
<button
type='button'
className='status__content__spoiler-link'
onClick={onClick}
aria-expanded={expanded}
>
{expanded ? (
<FormattedMessage id='status.show_less' defaultMessage='Show less' />
) : (
<FormattedMessage id='status.show_more' defaultMessage='Show more' />
)}
{icons}
</button>
</p>
);

View File

@ -0,0 +1,90 @@
import { useRef, useState, useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
import { useTimeout } from 'flavours/glitch/hooks/useTimeout';
export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
const inputRef = useRef<HTMLTextAreaElement>(null);
const [copied, setCopied] = useState(false);
const [focused, setFocused] = useState(false);
const [setAnimationTimeout] = useTimeout();
const handleInputClick = useCallback(() => {
setCopied(false);
if (inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
inputRef.current.setSelectionRange(0, value.length);
}
}, [setCopied, value]);
const handleButtonClick = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
void navigator.clipboard.writeText(value);
inputRef.current?.blur();
setCopied(true);
setAnimationTimeout(() => {
setCopied(false);
}, 700);
},
[setCopied, setAnimationTimeout, value],
);
const handleKeyUp = useCallback(
(e: React.KeyboardEvent) => {
if (e.key !== ' ') return;
void navigator.clipboard.writeText(value);
setCopied(true);
setAnimationTimeout(() => {
setCopied(false);
}, 700);
},
[setCopied, setAnimationTimeout, value],
);
const handleFocus = useCallback(() => {
setFocused(true);
}, [setFocused]);
const handleBlur = useCallback(() => {
setFocused(false);
}, [setFocused]);
return (
<div
className={classNames('copy-paste-text', { copied, focused })}
tabIndex={0}
role='button'
onClick={handleInputClick}
onKeyUp={handleKeyUp}
>
<textarea
readOnly
value={value}
ref={inputRef}
onClick={handleInputClick}
onFocus={handleFocus}
onBlur={handleBlur}
/>
<button className='button' onClick={handleButtonClick}>
<Icon id='copy' icon={ContentCopyIcon} />{' '}
{copied ? (
<FormattedMessage id='copypaste.copied' defaultMessage='Copied' />
) : (
<FormattedMessage
id='copypaste.copy_to_clipboard'
defaultMessage='Copy to clipboard'
/>
)}
</button>
</div>
);
};

View File

@ -60,8 +60,8 @@ export default class ErrorBoundary extends PureComponent {
try { try {
textarea.select(); textarea.select();
document.execCommand('copy'); document.execCommand('copy');
} catch (e) { } catch {
// do nothing
} finally { } finally {
document.body.removeChild(textarea); document.body.removeChild(textarea);
} }

View File

@ -0,0 +1,23 @@
import { FormattedMessage } from 'react-intl';
import { StatusBanner, BannerVariant } from './status_banner';
export const FilterWarning: React.FC<{
title: string;
expanded?: boolean;
onClick?: () => void;
}> = ({ title, expanded, onClick }) => (
<StatusBanner
expanded={expanded}
onClick={onClick}
variant={BannerVariant.Blue}
>
<p>
<FormattedMessage
id='filter_warning.matches_filter'
defaultMessage='Matches filter “{title}”'
values={{ title }}
/>
</p>
</StatusBanner>
);

View File

@ -7,6 +7,13 @@ export const WordmarkLogo: React.FC = () => (
</svg> </svg>
); );
export const IconLogo: React.FC = () => (
<svg viewBox='0 0 79 79' className='logo logo--icon' role='img'>
<title>Mastodon</title>
<use xlinkHref='#logo-symbol-icon' />
</svg>
);
export const SymbolLogo: React.FC = () => ( export const SymbolLogo: React.FC = () => (
<img src={logo} alt='Mastodon' className='logo logo--icon' /> <img src={logo} alt='Mastodon' className='logo logo--icon' />
); );

View File

@ -1,7 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
@ -10,17 +10,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { Blurhash } from 'flavours/glitch/components/blurhash'; import { Blurhash } from 'flavours/glitch/components/blurhash';
import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state'; import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
import { IconButton } from './icon_button';
const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: '{number, plural, one {Hide image} other {Hide images}}' },
});
class Item extends PureComponent { class Item extends PureComponent {
static propTypes = { static propTypes = {
@ -220,7 +213,6 @@ class MediaGallery extends PureComponent {
lang: PropTypes.string, lang: PropTypes.string,
size: PropTypes.object, size: PropTypes.object,
onOpenMedia: PropTypes.func.isRequired, onOpenMedia: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
defaultWidth: PropTypes.number, defaultWidth: PropTypes.number,
cacheWidth: PropTypes.func, cacheWidth: PropTypes.func,
visible: PropTypes.bool, visible: PropTypes.bool,
@ -309,7 +301,7 @@ class MediaGallery extends PureComponent {
} }
render () { render () {
const { media, lang, intl, sensitive, letterbox, fullwidth, defaultWidth, autoplay } = this.props; const { media, lang, sensitive, letterbox, fullwidth, defaultWidth, autoplay } = this.props;
const { visible } = this.state; const { visible } = this.state;
const size = media.size; const size = media.size;
const uncached = media.every(attachment => attachment.get('type') === 'unknown'); const uncached = media.every(attachment => attachment.get('type') === 'unknown');
@ -320,7 +312,7 @@ class MediaGallery extends PureComponent {
const style = {}; const style = {};
const computedClass = classNames('media-gallery', { 'full-width': fullwidth }); const computedClass = classNames('media-gallery', `media-gallery--layout-${size}`, { 'full-width': fullwidth });
if (this.isStandaloneEligible()) { // TODO: cropImages setting if (this.isStandaloneEligible()) { // TODO: cropImages setting
style.aspectRatio = `${this.props.media.getIn([0, 'meta', 'small', 'aspect'])}`; style.aspectRatio = `${this.props.media.getIn([0, 'meta', 'small', 'aspect'])}`;
@ -343,9 +335,7 @@ class MediaGallery extends PureComponent {
</span> </span>
</button> </button>
); );
} else if (visible) { } else if (!visible) {
spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible, { number: size })} icon='eye-slash' iconComponent={VisibilityOffIcon} overlay onClick={this.handleOpen} ariaHidden />;
} else {
spoilerButton = ( spoilerButton = (
<button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'> <button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'>
<span className='spoiler-button__overlay__label'> <span className='spoiler-button__overlay__label'>
@ -358,15 +348,23 @@ class MediaGallery extends PureComponent {
return ( return (
<div className={computedClass} style={style} ref={this.handleRef}> <div className={computedClass} style={style} ref={this.handleRef}>
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible && !uncached, 'spoiler-button--click-thru': uncached })}> {(!visible || uncached) && (
<div className={classNames('spoiler-button', { 'spoiler-button--click-thru': uncached })}>
{spoilerButton} {spoilerButton}
</div> </div>
)}
{children} {children}
{(visible && !uncached) && (
<div className='media-gallery__actions'>
<button className='media-gallery__actions__pill' onClick={this.handleOpen}><FormattedMessage id='media_gallery.hide' defaultMessage='Hide' /></button>
</div>
)}
</div> </div>
); );
} }
} }
export default injectIntl(MediaGallery); export default MediaGallery;

View File

@ -153,7 +153,7 @@ class ModalRoot extends PureComponent {
return ( return (
<div className='modal-root' ref={this.setRef}> <div className='modal-root' ref={this.setRef}>
<div style={{ pointerEvents: visible ? 'auto' : 'none' }}> <div style={{ pointerEvents: visible ? 'auto' : 'none' }}>
<div role='presentation' className='modal-root__overlay' onClick={onClose} style={{ backgroundColor: backgroundColor ? `rgba(${backgroundColor.r}, ${backgroundColor.g}, ${backgroundColor.b}, 0.7)` : null }} /> <div role='presentation' className='modal-root__overlay' onClick={onClose} style={{ backgroundColor: backgroundColor ? `rgba(${backgroundColor.r}, ${backgroundColor.g}, ${backgroundColor.b}, 0.9)` : null }} />
<div role='dialog' className='modal-root__container'>{children}</div> <div role='dialog' className='modal-root__container'>{children}</div>
</div> </div>
</div> </div>

View File

@ -2,14 +2,12 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { IconLogo } from 'flavours/glitch/components/logo';
import { AuthorLink } from 'flavours/glitch/features/explore/components/author_link'; import { AuthorLink } from 'flavours/glitch/features/explore/components/author_link';
export const MoreFromAuthor = ({ accountId }) => ( export const MoreFromAuthor = ({ accountId }) => (
<div className='more-from-author'> <div className='more-from-author'>
<svg viewBox='0 0 79 79' className='logo logo--icon' role='img'> <IconLogo />
<use xlinkHref='#logo-symbol-icon' />
</svg>
<FormattedMessage id='link_preview.more_from_author' defaultMessage='More from {name}' values={{ name: <AuthorLink accountId={accountId} /> }} /> <FormattedMessage id='link_preview.more_from_author' defaultMessage='More from {name}' values={{ name: <AuthorLink accountId={accountId} /> }} />
</div> </div>
); );

View File

@ -51,7 +51,8 @@ function normalizePath(
if ( if (
layoutFromWindow() === 'multi-column' && layoutFromWindow() === 'multi-column' &&
!location.pathname?.startsWith('/deck') location.pathname &&
!location.pathname.startsWith('/deck')
) { ) {
location.pathname = `/deck${location.pathname}`; location.pathname = `/deck${location.pathname}`;
} }

View File

@ -59,7 +59,7 @@ const messages = defineMessages({
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
embed: { id: 'status.embed', defaultMessage: 'Embed' }, embed: { id: 'status.embed', defaultMessage: 'Get embed code' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' }, admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
@ -325,13 +325,16 @@ class StatusActionBar extends ImmutablePureComponent {
} }
const filterButton = this.props.onFilter && ( const filterButton = this.props.onFilter && (
<div className='status__action-bar__button-wrapper'>
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' iconComponent={VisibilityIcon} onClick={this.handleHideClick} /> <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' iconComponent={VisibilityIcon} onClick={this.handleHideClick} />
</div>
); );
const canReact = permissions && status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions; const canReact = permissions && status.get('reactions').filter(r => r.get('count') > 0 && r.get('me')).size < maxReactions;
return ( return (
<div className='status__action-bar'> <div className='status__action-bar'>
<div className='status__action-bar__button-wrapper'>
<IconButton <IconButton
className='status__action-bar-button' className='status__action-bar-button'
title={replyTitle} title={replyTitle}
@ -341,13 +344,20 @@ class StatusActionBar extends ImmutablePureComponent {
counter={showReplyCount ? status.get('replies_count') : undefined} counter={showReplyCount ? status.get('replies_count') : undefined}
obfuscateCount obfuscateCount
/> />
</div>
<div className='status__action-bar__button-wrapper'>
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} /> <IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
</div>
<div className='status__action-bar__button-wrapper'>
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} /> <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
<EmojiPickerDropdown className='status__action-bar-button' onPickEmoji={this.handleEmojiPick} title={intl.formatMessage(messages.react)} icon={AddReactionIcon} disabled={!canReact} /> </div>
<div className='status__action-bar__button-wrapper'>
<IconButton className='status__action-bar-button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /> <IconButton className='status__action-bar-button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
</div>
{filterButton} {filterButton}
<div className='status__action-bar__button-wrapper'>
<DropdownMenuContainer <DropdownMenuContainer
scrollKey={scrollKey} scrollKey={scrollKey}
status={status} status={status}
@ -358,6 +368,7 @@ class StatusActionBar extends ImmutablePureComponent {
direction='right' direction='right'
ariaLabel={intl.formatMessage(messages.more)} ariaLabel={intl.formatMessage(messages.more)}
/> />
</div>
<div className='status__action-bar-spacer' /> <div className='status__action-bar-spacer' />
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'> <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'>

View File

@ -0,0 +1,37 @@
import { FormattedMessage } from 'react-intl';
export enum BannerVariant {
Yellow = 'yellow',
Blue = 'blue',
}
export const StatusBanner: React.FC<{
children: React.ReactNode;
variant: BannerVariant;
expanded?: boolean;
onClick?: () => void;
}> = ({ children, variant, expanded, onClick }) => (
<div
className={
variant === BannerVariant.Yellow
? 'content-warning'
: 'content-warning content-warning--filter'
}
>
{children}
<button className='link-button' onClick={onClick}>
{expanded ? (
<FormattedMessage
id='content_warning.hide'
defaultMessage='Hide post'
/>
) : (
<FormattedMessage
id='content_warning.show'
defaultMessage='Show anyway'
/>
)}
</button>
</div>
);

View File

@ -14,6 +14,7 @@ import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
import LinkIcon from '@/material-icons/400-24px/link.svg?react'; import LinkIcon from '@/material-icons/400-24px/link.svg?react';
import MovieIcon from '@/material-icons/400-24px/movie.svg?react'; import MovieIcon from '@/material-icons/400-24px/movie.svg?react';
import MusicNoteIcon from '@/material-icons/400-24px/music_note.svg?react'; import MusicNoteIcon from '@/material-icons/400-24px/music_note.svg?react';
import { ContentWarning } from 'flavours/glitch/components/content_warning';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state'; import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state';
@ -350,7 +351,7 @@ class StatusContent extends PureComponent {
const renderTranslate = this.props.onTranslate && this.props.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale); const renderTranslate = this.props.onTranslate && this.props.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);
const content = { __html: statusContent ?? getStatusContent(status) }; const content = { __html: statusContent ?? getStatusContent(status) };
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') }; const spoilerHtml = status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml');
const language = status.getIn(['translation', 'language']) || status.get('language'); const language = status.getIn(['translation', 'language']) || status.get('language');
const classNames = classnames('status__content', { const classNames = classnames('status__content', {
'status__content--with-action': parseClick && !disabled, 'status__content--with-action': parseClick && !disabled,
@ -375,16 +376,8 @@ class StatusContent extends PureComponent {
</Permalink> </Permalink>
)).reduce((aggregate, item) => [...aggregate, item, ' '], []); )).reduce((aggregate, item) => [...aggregate, item, ' '], []);
let toggleText = null; let spoilerIcons = [];
if (hidden) { if (hidden && mediaIcons) {
toggleText = [
<FormattedMessage
id='status.show_more'
defaultMessage='Show more'
key='0'
/>,
];
if (mediaIcons) {
const mediaComponents = { const mediaComponents = {
'link': LinkIcon, 'link': LinkIcon,
'picture-o': ImageIcon, 'picture-o': ImageIcon,
@ -393,27 +386,16 @@ class StatusContent extends PureComponent {
'music': MusicNoteIcon, 'music': MusicNoteIcon,
}; };
mediaIcons.forEach((mediaIcon, idx) => { spoilerIcons = mediaIcons.map((mediaIcon) => (
toggleText.push(
<Icon <Icon
fixedWidth fixedWidth
className='status__content__spoiler-icon' className='status__content__spoiler-icon'
id={mediaIcon} id={mediaIcon}
icon={mediaComponents[mediaIcon]} icon={mediaComponents[mediaIcon]}
aria-hidden='true' aria-hidden='true'
key={`icon-${idx}`} key={`icon-${mediaIcon}`}
/>,
);
});
}
} else {
toggleText = (
<FormattedMessage
id='status.show_less'
defaultMessage='Show less'
key='0'
/> />
); ));
} }
if (hidden) { if (hidden) {
@ -422,15 +404,7 @@ class StatusContent extends PureComponent {
return ( return (
<div className={classNames} tabIndex={0} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}> <div className={classNames} tabIndex={0} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<p <ContentWarning text={spoilerHtml} expanded={!hidden} onClick={this.handleSpoilerClick} icons={spoilerIcons} />
style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}
>
<span dangerouslySetInnerHTML={spoilerContent} className='translate' lang={language} />
{' '}
<button type='button' className='status__content__spoiler-link' onClick={this.handleSpoilerClick} aria-expanded={!hidden}>
{toggleText}
</button>
</p>
{mentionsPlaceholder} {mentionsPlaceholder}

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