Background: mp4 video type was not playing in Safari on macOS or iOS if
served from Discourse instead of S3. The response was a 200 instead of a
206 for partial response. There was a
[change](https://github.com/rack/rack/compare/v2.2.19...v2.2.20) in Rack
v2.2.20 that forces apps to be explicit about X-Accel-Redirect on the
Sendfile command
Change: Be explicit about enabling X-Accel-Redirect in application
middleware for Rack::Sendfile.
Also add guards for environment to not get `X-Accel-Mapping header
missing` on local env logs
This PR adds the `:tags_for_saving` modifier hook to allow plugins to
modify the list of tags before they are saved to a topic. This is useful
for scenarios where additional tags need to be added or existing tags
need to be altered based on custom logic.
example:
```ruby
register_modifier(:tags_for_saving) do |saving_tags, original_tags, guardian, opts|
# Custom logic to modify saving_tags
saving_tags << "additional-tag" unless saving_tags.include?("additional-tag")
saving_tags
end
```
`ActiveModel` doesn’t have a `symbol` type by default, but it can be
quite handy for contracts from our service framework.
This way, it’s easy to get a symbol from an attribute. Any non-blank
value will get converted to a symbol by being converted to a string
before. A blank value will return `nil`.
Now that we have transitioned lightbox to PhotoSwipe, we can remove the
Magnific library and the `SiteSetting.experimental_lightbox` code paths.
The site setting value will be fully removed from the database in a
follow up migration.
Internal ref - /t/165699
There exists a state where the language switcher would be clickable but
have an empty list as the language list will not be returned when the
setting is disabled. This is now fixed.
Uploads in the composer are marked as `secure: true` because they are
drafts. Later when they are published their secure status is changed,
but optimized videos were not being updated as well.
We can't compare **computed** (ok)lch values since those are not stable.
For example, one of the values used in "User color palette selector"
spec:
* `oklch(0.92 0.0708528 68.5036)` - the value hardcoded in the spec
* `oklch(0.92 0.0708528 68.5037)` - the value on my machine (the spec
was failing)
* `oklch(0.92 0.070857 68.5063)` - the value after updating pitchfork
(that's why the [PR](https://github.com/discourse/discourse/pull/36352)
is failing)
All those are effectively the same. When converted to hex they all
resolve to `#ffddb2`.
The "after" block that removes the JSON file was inside the "when
login_required" context, but tests outside that context also create the
file. This caused leftover files when running tests in parallel with
bin/turbo_spec.
Adds a new "Moderation history" section to the review queue insights tab
that displays:
- Number of times the user has been silenced
- Number of times the user has been suspended
- Number of rejected posts
<img width="806" height="440" alt="Screenshot 2025-12-02 at 11 19 22 am"
src="https://github.com/user-attachments/assets/280891fc-2019-43b3-abf5-8848dc57d96e"
/>
Ensure optimized_upload inherits the secure flag from the original
upload
when created. This prevents ACL mismatches that cause 403 errors when
accessing converted videos, especially when video conversion completes
before the post is published.
When moderators review flagged posts, they can now see the email address of the flagged user in the user insights section. This additional context helps moderators more quickly identify spam accounts and determine if a post should be flagged as spam.
Feature respects `moderators_view_emails` site setting.
<img width="795" height="650" alt="Screenshot 2025-12-01 at 4 14 38 pm"
src="https://github.com/user-attachments/assets/f3db2889-e18a-4941-8a4c-bf777a1fb81b"
/>
Video conversion was happening already for chat videos, but was not
updating the chat messages. Now chat messages will be updated with the
optimized video.
This ensures that both "Emoji.exists?" and "Emoji[]" use the same lookup
"algorithm" when trying to match an emoji "name" with an emoji in the
database.
This ensures the normalization of the name into a single value handles
both skin tones and aliases.
Closes#33130
With this change, S3's `stale-while-revalidate` and `max-age` response
directives can be controlled.
By default, off for `stale-while-revalidate`, and both are hidden site
settings.
We currently allow assigning nil to site settings, which lead to implicit casting to other values. Forcing explicit handling of this case helps avoid accidental misconfiguration.
This should not affect changing site settings from the UI, which is already well-behaved in this regard.
This enhances the reviewable timeline by tracking and displaying when
reviewables are claimed and unclaimed by moderators.
Changes include:
- Modified reviewable_claimed_topics_controller to send user information
and claimed status for both claim and unclaim operations
- Added reviewable_histories to ReviewableSerializer to expose history
data
- Updated ReviewableItem component to track claim/unclaim events via
MessageBus and add them to the reviewable's history
- Fixed MessageBus initialization to start in Rails testing environment
to support system tests
<img width="1111" height="741" alt="Screenshot 2025-11-26 at 3 24 28 pm"
src="https://github.com/user-attachments/assets/84e0bf2a-adb3-4ed2-93cb-eda6d771f5af"
/>
This PR adds a new site setting for configuring a community moderation topic that will be linked from the reviewable sidebar.
It involves adding a new site setting type "topic" which uses the TopicChooser component in the front-end.
Syncs category permission info to the client when a user is added to or removed from a group.
* Add Category.set_permission! to compute and set category.permission (full, create_post, readonly, or nil) using Guardian checks.
* Change Group#add and Group#remove to guard against nil users and duplicate membership, send membership notifications when requested, and invoke a new publish flow after membership changes.
* Add Group helpers send_membership_notification and publish_category_updates; publish_category_updates serializes updated category permissions and a deleted_categories list and publishes them to the user via MessageBus, falling back to a client refresh when the group exceeds Group::PUBLISH_CATEGORIES_LIMIT.
* Ensure add/remove trigger publishing so the client receives permission updates or removals immediately after membership changes.
* Update tests to verify Category.set_permission! behavior for admin/full/create_post/readonly/none scenarios and to assert MessageBus payloads for category permission updates, deletions, and the refresh fallback.
Requires `Reviewable UI refresh` to be enabled.
This removes the "IP Lookup" button and includes an IP lookup when a
reviewer switches to the "Insights" tab
<img width="600" alt="image"
src="https://github.com/user-attachments/assets/fa8562e1-0eb3-474c-9a08-f5059a291a99"
/>
The duplicate account check is included behind a link that opens a
modal, which allows the initial IP lookup to happen separately (and a
little faster)
<img width="600" alt="image"
src="https://github.com/user-attachments/assets/e04ad1f5-e37b-47ce-892c-be6d8750cb82"
/>
I've also split out the duplicate account table into a separate
component so we can reuse it in this modal and on individual users'
admin pages.
A couple other minor changes:
* Updated the unlisted text, technically an unlisted topic *is* public
as long as you have the link
* Hid the "flag count" insight if there's only 1 flagger, this should be
apparent based on the flag existing at all so the insight only seems
useful to indicate that multiple people flagged it
#36156 introduced `const imgEl = el.tagName === "IMG" ? el :
el.querySelector("img");` to the lightbox component to capture the image
element on the page.
However, when it comes to applying the lightbox to an element without
`<img>` , i.e., discourse solved, backquote, activity page, etc. The
`imgEl` will turn into null, and the following `data.origSrc =
imgEl.getAttribute("data-orig-src");` will result in `Uncaught
TypeError: can't access property "getAttribute", imgEl is null`,
preventing the lightbox from starting properly.
This commit add a safety check when `imgEl` is used, and add fallback
logic to null if the `<img>` element doesn't exist.
Before:
<img width="1603" height="845" alt="image"
src="https://github.com/user-attachments/assets/6e588a00-53bf-410a-bbac-3f24a820d494"
/>
After:
<img width="1603" height="848" alt="image"
src="https://github.com/user-attachments/assets/07868ade-0e1b-43d2-83cc-c2fec3da9109"
/>
This will automatically update the IDP whenever at least one of the
following site setting changes
- title
- description
- logo url
- small logo url
Ref - meta/t/385471/2
---
Depends on https://github.com/discourse/discourse-login/pull/81
Currently, when uploads have a different SHA1 in their URL from the one
that has been computed (it can happen with secure uploads), and those
uploads are images, then the cooked post processor won’t link them
properly to their post. The post has them as `Post#uploads` but not as
`Post#image_upload`. This in turn will make the associated topic
thumbnail-less when the post is the first one.
To address the issue, it’s a matter of adding fallbacks to
`CookedPostProcessor#update_post_image` when fetching the uploads. This
is something we already do in
`HasPostUploadReferences#link_post_uploads`.
Follow-up to 664e6244b0
1. Introduce `SchemaSettingsObjectValidator.property_values_of_type` to
reduce logic duplication across `SiteSetting`, `ThemeSiteSetting` and
`ThemeSetting` which all supports objects schema setting.
2. Add missing test cases for `ThemeSiteSetting` and `ThemeSetting`
We've identified a memory leak that occurs when rendering custom
templates into server-side plugin outlets where each rendered template
leaves at least 1 object in the heap that can never be claimed back by
the GC. This happens due to ActionView's caching of compiled template
and is [intended
behavior](https://github.com/rails/rails/issues/33019#issuecomment-409379676),
however in our case, this is problematic because it can make memory
usage grow extremely fast if a plugin connects to an outlet on a page
that gets visited a lot (e.g. the topic page).
This PR moves away from using ActionView's `render` method, which
creates a new `ActionView::Template` instance which gets compiled and
cached, to a custom solution that creates an `ActionView::Template`
instance only once for a given template and reuses the instance when the
template is needed again.
In development, to avoid having to restart the server when changing a
plugin's template, the `PLUGIN_OUTLET_TEMPLATE_CACHE` constant where
`ActionView::Template` are kept, gets reset on code reload via the
`to_prepare` hook.
Internal topic: t/169204.
Prior to this change, the SQL query string returned by
`BadgeQueries.sharing_badge`
was simply counting the number of `incoming_links` rows for a given post
and sharer (user_id). This is however incorrect as the sharing badges
description states that the visitors have to be unique.
This commit updates `BadgeQueries.sharing_badge` to count only unique
(ip_address, current_user_id) pairs in incoming_links, so repeated hits
from the same visitor no longer inflate share badge counts.
Currently, when updating several settings in bulk, they are updated in a non-deterministic order. This means that if Setting B depends on Setting A being enabled, and we try to enable B and A together, there's a chance that fails. This is why we need to remove the up-front values_are_valid policy from the service as well.
This change first sorts the settings topologically, i.e. in order of dependency, so if C depends on B depends on A, then we will update them in order [A, B, C].
When a user has been destroyed, any claimed reviewables belonging to
that user are partially orphaned, which the review queue doesn't know
how to handle. Ensuring those reviewables are unclaimed (and cleaning up
any orphaned claims on existing sites) ensures the review queue is able
to cleanly handle claimed reviewables.
## 🔍 Overview
`GlobalSetting` was converting negative integers into strings. This
update ensures that negative integers are treated as the correct type.
When marking a private message as unread via "destroy_timings", two
issues prevented old PMs from appearing in the Unread list:
1. PostTiming.destroy_for used Topic.listable_topics which excludes PMs,
so first_unread_pm_at was never updated. Now it explicitly handles PMs
by calling set_minimum_first_unread_pm!
2. The client-side PM tracking state wasn't notified of the change.
Unlike regular topics which sync state on navigation, PM tracking relies
on MessageBus for updates. Added publish_read call to notify the client.
Internal ref - t/155983
As a last part of the [button
refactor](https://github.com/discourse/discourse/commit/759fc040419d30a52885bbc71065de98013b3033),
these double declarations should no longer be used.
Instead, when using a `btn-transparent` that needs to be a different
colour, use the `--primary`, `--danger`, or `--success` modifiers.
This commit
* adds the modifiers, while keeping the original declarations for safety
* updates the usages I can find
The `type: upload` exists on main site settings, but not under the
`type: objects` schema setting. This adds this feature for `objects` in
Site Settings, Theme Settings, and Theme Site Settings.