100 Commits
Author SHA1 Message Date
Gabriel Grubba b8565672b9 DEV: Remove the post event format filter (#43287)
Previously, the events finder could filter events by whether they had a
URL, a location, or both, exposed as the `event_format` parameter added
in #43131.

We are still figuring how should we define the event_format
2026-09-04 16:46:52 -03:00
Gabriel Grubba 923d0e3e17 UX: Add post event host controls (#43130)
Previously, event authors could not configure hosts, and readers could
not see who hosts an event.

This change adds a hosts control to the event builder and displays the
creator and hosts on the event card. The first two hosts are shown
inline; "and N others" opens a menu listing every host, and when the
hosts line would wrap below the creator all hosts collapse into that
menu instead.

![Advanced event settings with
hosts](https://github.com/user-attachments/assets/456ea0ad-a973-4014-8233-d102dede2594)

![Event topic card showing creators and
hosts](https://github.com/user-attachments/assets/b2922af2-f951-4ff9-9955-0156e4e66c14)

When we have many hosts it turns like this:

<img width="1332" height="1246" alt="Screenshot 2026-09-03 at 15 02 21"
src="https://github.com/user-attachments/assets/44d130f0-167f-420f-86a5-d345bb5e82a3"
/>
2026-09-03 17:39:31 -03:00
Gabriel Grubba 542f29bf17 FEATURE: Add hosts to post events (#43129)
Previously, post events could identify their creator but could not
assign event hosts.

This change persists, validates, and serializes hosts as a first-class
post-event attribute.
2026-09-03 17:25:36 -03:00
Gabriel Grubba 2e9dc47bd8 FEATURE: Add generic post event finder filters (#43131)
Previously, the event finder could not filter by tags, search text,
status, or format, and malformed date or limit parameters could fail
unpredictably. ( `?after=foobar` / `?before=2025-13-45` / `?after=now`).
This change adds those generic filters, a lightweight event-card
response, and consistent validation for date and limit parameters.
2026-09-03 11:00:19 -03:00
Gabriel Grubba 8e250d40b0 DEV: Add create-topic-button-click behavior transformer (#43142)
Previously, the create-topic button's click always opened the composer,
so a plugin that needs its own creation flow (for example, an event form
in event categories) had to hide the core button with a CSS hack and
render a second button next to it, losing the drafts menu along the way.

This change wraps the click in a `create-topic-button-click` behavior
transformer with the current category and tag as context, so a plugin
can take over the click (or call `next()` to keep the default) while
core's button, its label and icon transformers, and the drafts menu stay
in place.
2026-09-03 10:28:12 -03:00
Gabriel Grubba f1a073270c FEATURE: Add tag creation and tag editing to AI bot tools (#42801)
Follow-up to #42799 (stacked on it), doing for tags what that PR did for
categories. The admin assistant's prompt says it helps with "tags", but
its only tag write tool applies tags to a topic — it cannot create a tag
ahead of time, rename one, or set a tag description.

- **`create_tag`** — creates a standalone tag with an optional
description, normalizing the name through `DiscourseTagging.clean_tag`
just like manual creation, and logs a `created_tag` staff action.
- **`edit_tag`** — renames an existing tag and/or changes its
description (`can_edit_tag?`), logging the same `renamed_tag` staff
action the tags controller writes.
- **`edit_tags` description fix** — the signature claimed "Adds tags to
a topic. By default appends", but with `replace: true` it replaces the
full tag set and removes omitted tags. The signature and `tags`
parameter now say so; behavior is unchanged.

Both new tools require human approval and attribute the action to the
approving moderator, matching the category tools. They sit inside the
`tagging_enabled` gate in `all_available_tools`, so they disappear
cleanly on sites without tagging.
2026-08-26 13:25:02 -03:00
Gabriel Grubba 2de0e3f963 FEATURE: Add category creation and real category editing to AI bot tools (#42799)
The admin assistant (Discourse Helper Bot) claims it can set up
categories when asked, then fails: its only category write tool,
`edit_category`, despite the name, could only move a topic to a
different category. Admins asking "create these categories for me" got a
capable-sounding yes followed by a refusal.

This adds real category management:

- **`create_category`** — creates a category (or subcategory via
`parent_category_id`) with optional description and colors. Returns the
new category's id and URL so the model can create parents before
subcategories across turns.
- **`edit_category`** — repurposed to do what its name says: edit an
existing category's name, description (revising the definition topic
through the standard `Category#update` path), and colors.
- **`change_topic_category`** — the previous `edit_category` behavior
(recategorizing a topic via `PostRevisor`), preserved under a name that
matches what it does.

All three require human approval before executing and are attributed to
the approving moderator (`attribute_to_approver?`), so guardian checks
and staff action logs (`create_category`, `change_category_settings`)
credit the real approver rather than the bot account. The new tools are
added to the admin assistant persona and `all_available_tools`; the seed
fixture syncs them to existing sites on migrate. A data migration
renames stored `EditCategory` entries in `ai_agents.tools` and pending
`ai_tool_actions` rows to `change_topic_category`, so existing agents
and in-flight approvals keep working.
2026-08-26 13:09:04 -03:00
Gabriel Grubba c78c141f86 FEATURE: Add AI triage filters to the review queue (#42861)
Previously, staff could not identify which AI triage automation sent an
item to the review queue using the existing Type and Reason filters.

This change extends `add_custom_reviewable_filter` with optional Type
and Reason filter aliases, showing AI triage and current automation
names while matching stable automation IDs stored on reviewable scores.


With this, you can filter by the automation name in the `reason` filter:

<img width="1930" height="1036" alt="image"
src="https://github.com/user-attachments/assets/5095564b-249c-46de-89bb-0036dd28577c"
/>


I have an automation that flags any post with the word `silly`
2026-08-26 10:10:53 -03:00
Gabriel GrubbaandClaude Opus 4.7 2855717ef0 FIX: Clear category new/unread count after a topic is removed (#40229)
When a topic is marked as spam and deleted, the existing `/delete`
MessageBus update marks it as deleted in the client topic tracking
state. Deleted states were still eligible for new and unread counts,
though, and the topic could remain in the incoming-topic list.

This change makes the existing new/unread predicates ignore deleted
topics and clears a deleted topic from the current incoming list. No new
server-side correction message is needed.

The regression is covered by the topic tracking state unit tests and a
browser-level spam-deletion spec.

Before:

<img width="1400" height="1400" alt="Category new-topic count remains
after spam deletion"
src="https://github.com/user-attachments/assets/057a0a77-0113-4cf4-8bf8-5b645090cdcb"
/>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-08-24 14:57:29 -03:00
Gabriel Grubba 28910d15a9 FEATURE: Support icon properties in objects settings schemas (#42632) 2026-08-21 11:08:31 -03:00
Gabriel Grubba bba80124b4 FIX: Serialize visible allowed_tag_groups to all users (#42654)
Previously, 361869df7c stopped serializing `allowed_tag_groups` for
users who cannot edit the category. This went further than hiding
restricted names: it also removed the names of tag groups that are
visible to everyone — the same names any user (including anonymous) can
already enumerate through `/tag_groups/filter/search` — which broke
per-category tag-group UIs for everyone except editors.

This change serializes `allowed_tag_groups` on the full category record
for all users, filtered through `TagGroup.visible(guardian)` — the same
visibility filter the public search endpoint applies — so restricted
group names remain hidden from non-editors. Editors keep the unfiltered
list, which the category edit UI round-trips.

Note for reviewers: the one piece of information this exposes that was
not previously readable by non-editors is the mapping of visible tag
groups to a specific category (the group names themselves are already
public via the search endpoint, and the composer's tag chooser already
returns a required group's name to any user who can post). Reported by a
site running a per-category tag-group filter UI, where the field's
removal made the filters render for staff only.
2026-08-17 18:47:10 -03:00
Gabriel Grubba 70520d1ac3 FIX: Link to the right automation page from triage reviewables (#42635)
When an LLM triage automation flags a post, the reviewable's score
reason says:

> **Triggered by the [My automation](/admin/plugins/automation/1)
rule.**

That link is broken — it leads to a page that does not exist.

The automation admin UI is nested under the plugin show route
(`/admin/plugins/:plugin_id`), and registers its own `automation` child
route. So its edit page lives at
`/admin/plugins/automation/automation/:id` — the plugin id, then the
nested route. The link only had one of the two segments.

This was originally broken when automation moved to the new show route
and the URL changed from `/admin/plugins/discourse-automation/:id`.
#35434 dropped the `discourse-` prefix but missed the extra segment, so
the link stayed broken.

Reported by a customer who landed on `/admin/plugins/automation/1` from
their review queue.

The translated locale files carry the same URL but are managed by
Crowdin, so they are left to re-sync from `en`.
2026-08-14 16:55:42 -03:00
Gabriel Grubba cb34503b9d FIX: Prevent deletion of system badges via the admin API (#42452)
Previously, the admin UI hid the delete button for seeded system badges,
but `DELETE /admin/badges/:id` accepted direct requests and destroyed
the badge, its `UserBadge` grants, and any user titles granted from it.

This change rejects the request with a translated 422 error before
anything is logged or destroyed — matching how system flags and
automatic groups are protected — while leaving custom badge deletion
unchanged.

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-08-10 12:07:41 -03:00
Gabriel Grubba f9f1540323 FIX: Report effective welcome banner state for themes without overrides (#42451)
Previously, the `themes-with-setting` endpoint reported themes with no
`ThemeSiteSetting` override as disabled, even though a missing override
means the theme inherits the `enable_welcome_banner` default (true), so
saving unrelated fields on the admin welcome-banner form silently
persisted `false` and hid the banner for those themes.

This change falls back to `SiteSetting.defaults[:enable_welcome_banner]`
only when no override row exists — while still reporting explicit
`false` overrides correctly — matching how
`ThemeSiteSetting.generate_theme_map` and `ThemeSiteSettingResolver`
resolve effective values.

## Source

- Patch Triage: https://patch.discourse.org/patch-triage/1666

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-08-10 11:48:18 -03:00
Gabriel Grubba 88f0ddbfac FIX: Return a validation error for invalid pluralized email templates (#42450)
Pluralized email-template subject and body values are hashes. When an
invalid edit triggered the rollback path, it attempted to store that
hash as a translation override and raised a TypeError.

Track the original override state for scalar values, restore it after a
failed update, and leave pluralized roots untouched. Invalid edits now
return the expected 422 response without creating override records.

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-08-10 11:45:59 -03:00
Gabriel Grubba b405f4d7c9 FIX: Stop Admin Assistant chain at pending approval (#42320)
Previously, approval-required Admin Assistant tools chained into another
model response, which could replace the interactive approval card with a
non-interactive pending-approval summary.

This change adds a default-off `stop_chain_on_pending_approval?` agent
capability, enables it for the Admin Assistant, processes every tool
call in the current response, and then leaves the approval cards as the
authoritative result.
2026-08-04 20:01:03 -03:00
Gabriel Grubba 1ccc390f5a FIX: Prevent moderators from deleting peer moderators (#42248)
## What changed

Small refactor in `can_delete_user?`

All but moderators deleting each other is changed.


## Why

This is inconsistent with the established permission hierarchy that
prevents moderators from destructively acting on other staff members.

We have a precedent for this change in:
- [Prevent moderators from bulk-deleting another moderator's
posts](https://github.com/discourse/discourse/commit/bb35a5a2ebbf93659ffd64184e5d9cdcbeb46e21)
- [Prevent moderators from clearing suspensions and silences from other
staff
accounts](https://github.com/discourse/discourse/commit/541ef5ac4e853ec087da79aa4c303a57be74348)
- [Prevent moderators from modifying the trust levels of admin and staff
accounts](https://github.com/discourse/discourse/commit/2c8063e078f81f7a0bdef607d27e2d48e2b3e93)

## Source

- Patch Triage: https://patch.discourse.org/patch-triage/1484

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-08-04 09:45:57 -03:00
Gabriel Grubba 099285da70 FIX: Validate user note length against max_post_length (#42240)
Previously, staff could persist user notes longer than the configured
maximum post length, which could consume excessive client resources when
rendered.

This change rejects user notes longer than `SiteSetting.max_post_length`
before persistence.

Relates to https://patch.discourse.org/patch-triage/1360
2026-07-31 16:19:10 -03:00
Gabriel Grubba be1c355de1 SECURITY: Prevent topic existence disclosure in profile featuring (#42235)
Previously, the profile feature-topic endpoint returned different
responses for missing topics and existing topics the requester could not
access, revealing whether a topic record existed.

This change checks topic visibility before feature eligibility,
returning a not-found response for both missing and inaccessible topics.

Relates to this patch: https://patch.discourse.org/patch-triage/1350
2026-07-31 16:13:57 -03:00
606bdbfb0f Feature: Add chat integration workflows send action (#40663)
With this PR, we add "Send chat-integration message" node that sends a
message to any chat-integration channel (Slack/Discord/Telegram).




<img width="1258" height="334" alt="Screenshot 2026-07-28 at 00 24 33"
src="https://github.com/user-attachments/assets/37c633fa-46f9-4ec3-b705-3578c8a254cb"
/>
<img width="770" height="344" alt="Screenshot 2026-07-28 at 00 24 19"
src="https://github.com/user-attachments/assets/9bf212ff-cfb7-4269-bf6e-2903020118ca"
/>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
2026-07-28 00:46:16 +02:00
Gabriel Grubba 41543c974f FIX: Prevent secret & themeable site settings from entering AI approvals (#41779)
Previously, the AI change site-setting tool allowed secret and themeable
values to go to `/review`. This should not happen

This change rejects secret and theme-controlled settings before they can
be queued for approval. Similar to what we do in Read Site Settings:


https://github.com/discourse/discourse/blob/main/plugins/discourse-ai/lib/agents/tools/read_site_setting.rb#L44-L86
2026-07-16 15:18:16 -03:00
Gabriel GrubbaandPenar Musaraj 257ce00e0a FEATURE: Change Site Settings AI Tool (#41681)
Previously, AI agents could look up site settings (`search_settings`,
`setting_context`) but had no way to change them.

This change adds a `change_site_setting` tool that requires admin
approval before anything is applied: the proposed change shows up as an
inline review card in the bot conversation, and on approval it is
applied through the `SiteSetting::Update` service — keeping all of its
guardrails (no hidden, deprecated, globally shadowed, or unconfigurable
settings) — credited to the approving admin in the staff action log.


## Demo:



https://github.com/user-attachments/assets/6782d8f4-0182-47dc-92b5-1db77b7a1ee1

---------

Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>
2026-07-14 15:00:08 -03:00
Gabriel Grubba 9c09e988b3 FEATURE: Approve AI moderation actions inline in bot chat DMs (#41565)
> Was stacked on #41497, which has since merged. This PR is now rebased
onto `main` and contains only the chat-approval work.

### What

Lets a moderator approve or reject an AI-bot moderation tool action
(`suspend_user` / `silence_user`) **inside a Chat direct message with
the bot**, instead of leaving for the `/review` queue — the chat
counterpart to the base PR's inline PM/topic card.

When the bot queues one of these actions in a DM, it posts a message
with **Approve / Reject** buttons rendered via the Chat plugin's native
interactive **blocks**. Clicking performs the queued action through the
existing `ReviewableAiToolAction` backend (credited to the approving
moderator) and rewrites the message to its resolved state, removing the
buttons.

### How

- `bot.rb` — `enqueue_tool_for_approval` branches on chat context: in
chat it emits a `:chat_approval` signal; in PM/topic it keeps the
existing inline card.
- `playground.rb` — `reply_to_chat_message` posts a bot chat message
carrying the Approve/Reject blocks, in the **same DM thread as the bot's
reply** (AI-bot DM replies are threaded by design). DM channels only.
- `chat_tool_approval.rb` — builds/parses the button `action_id`s,
builds the blocks, and handles the `chat_message_interaction` event:
performs the reviewable and rewrites the message. Runs synchronously so
the buttons clear before the request returns.
- `entry_point.rb` — registers the `:chat_message_interaction` listener.

### Authorization

`Chat::CreateMessageInteraction` only checks channel visibility, so
staff-gating is enforced here: the handler requires
`Reviewable.viewable_by(user)` **and** `Reviewable#perform` re-checks
(`ensure_performed_by_is_a_real_person!` + the approver's guardian).
Non-staff clicks are ignored. A crafted `action_id` can't target another
reviewable — core only matches `action_id`s present in that message's
own blocks.

### Core-chat changes (3 lines)

The blocks system was built for **create-time-only** blocks; nothing had
ever mutated a message's `blocks` after creation. Clearing the buttons
on approve/reject is the first such case, which required:

- `chat-message.js` — make `blocks` a `@tracked` property (so
reassigning it re-renders).
- `chat-channel-subscription-manager.js` +
`chat-channel-thread-subscription-manager.js` — refresh `message.blocks`
in `handleEditMessage` (so the block-clearing edit reaches the client,
in both the channel and thread views).

All are no-ops for the only other block user (category blocks, which are
never edited after creation).

### Testing

`plugins/discourse-ai/spec/lib/ai_bot/chat_tool_approval_spec.rb` —
action-id round-trip, block shape, staff gating, foreign/stale
action-ids, approve/reject, failure surfacing, and an end-to-end run
through the real `Chat::CreateMessageInteraction` service. The existing
`playground_spec.rb` chat-DM tests (threaded conversation + context)
continue to pass. Verified manually in a bot DM.
2026-07-13 17:08:35 -03:00
Gabriel Grubbaanddiscourse-patch-triage <272280883+discourse-patch-triage[bot]@users.noreply.github.com> 9a094a1e5c SECURITY: Apply category tag visibility to full-text search (#41668)
Full-text search (`Search#tags_search`, which powers the anonymous
`/search/query.json` endpoint) filtered matching tags through
`DiscourseTagging.hidden_tag_names`. That helper only accounts for
tag-group permissions, so tags restricted to a private category via
`CategoryTag` or `CategoryTagGroup` were never filtered out. An
anonymous or unauthorized user could search for a term and receive the
restricted tag's name, slug, and description (including localized
descriptions) in the `tags` array, even with no access to the category
itself. Because the search term can be swept, this allowed enumeration
of otherwise-hidden tag metadata.

This came from Patch Triage:
https://patch.discourse.org/patch-triage/1434

Co-authored-by: discourse-patch-triage <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-07-13 15:16:26 -03:00
Gabriel Grubba 0f8d25c03c FEATURE: Admin filter controls now support URL state (#41609)
See demo:



https://github.com/user-attachments/assets/930ce466-d859-42e2-af93-d6255aa942ce



Now these pages:

- admin/email-logs
- admin/email/templates
- admin/config/customize/components
- admin/config/colors
- admin/badges
- admin/reports
- admin/config/upcoming-changes

Support URL state for their filter controls. This means that you can now
share links to these pages with the current filter state preserved in
the URL.
2026-07-13 09:25:37 -03:00
Gabriel GrubbaandPenar Musaraj cc14e6e402 FEATURE: Add AI agent suspend/silence tools with inline approval review (#41497)
Previously, AI agents had no way to suspend or silence users, and
approving any moderation tool action meant leaving the conversation for
the `/review` queue.

This change adds `suspend_user`/`silence_user` tools that always require
moderator approval, and lets moderators approve or reject those actions
from an inline review card right in the bot conversation — credited to
the approving moderator (not the bot) in the staff action log — instead
of switching to the `/review` queue.


Demo of this feature:



https://github.com/user-attachments/assets/b9605511-e53d-483d-b435-b212694cdf53



https://github.com/user-attachments/assets/383bda5d-93ee-4758-a51e-6162ea0d57ee

---------

Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>
2026-07-10 10:59:51 -03:00
Gabriel Grubba 0242264a56 FEATURE: Support bulk user search in the admin users list (#41572)
Admins can now search for multiple users at once in `/admin/users/list`
by
separating usernames or emails with commas or whitespace — useful for
support workflows where a list of users needs to be checked in one go
(e.g. pasted from a spreadsheet; spaces work because usernames and
emails
can never contain them).

The search is also shareable via URL: typing keeps the address bar in
sync
with a `filter` query param
(`/admin/users/list/active?filter=sam,bob@example.com`),
and visiting such a URL prefills the search box and filters the list.
The
old `username` query param keeps working as a read-only legacy fallback
—
it was a misleading name, since searches also match emails.

## Demo:


https://github.com/user-attachments/assets/c85e4f30-3c06-493b-9fed-bb2057209c5d
2026-07-09 10:34:52 -03:00
Gabriel Grubba 827f20a1a0 SECURITY: Filter private subcategory counts from category listings (#41384)
The category list aggregate topic counts included direct subcategories
without
checking whether the current guardian could see them.

Before we the only possible information that could be gotten was:

  - private subcategory topic counts
  - recency buckets for topic creation: day/week/month/year/all-time

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-07-03 14:40:52 -04:00
Gabriel Grubba 4dbaafd291 FIX: Ignore title parameter during public signup (#41381)
Public account creation accepted `title` from non-admin signup requests,
allowing a new user to assign themselves an arbitrary visible profile
title before normal title authorization checks could apply.

Ignore `title` for non-admin account creation, matching the existing
treatment of primary/flair group parameters. Admin API user creation
remains unchanged.

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-07-03 11:39:58 -03:00
Gabriel Grubba 51d0a79ba8 DEV: outlet for thread actions (#41382)
This adds an outlet for both thread actions in drawer and full page mode
2026-07-03 10:17:14 -03:00
Gabriel GrubbaandClaude Opus 4.8 8bcf5126e8 SECURITY: Honor group visibility in discourse-assign group lookups (#41176)
Add `ensure_can_see_group` to places where we needed it before returning
the group metadata.

Also added
`.visible_groups(@guardian.user).members_visible_groups(@guardian.user)`
to guard `advanced_filter` search

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 13:57:46 -03:00
Gabriel Grubbadiscourse-patch-triage[bot] <272280883+discourse-patch-triage[bot]@users.noreply.github.com>Gerhard Schlager
8f62172de0 FIX: ZIP symlink entries are extracted as real directories (#41168)
## Summary

ZIP symlink entries are extracted as real directories – we should verify
before extracting

## Source

- Patch Triage: https://patch.discourse.org/patch-triage/890

---

🤖 Auto-generated from the patch diff via Patch Triage. Review carefully
before merging.

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>

---------

Co-authored-by: discourse-patch-triage[bot] <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
Co-authored-by: Gerhard Schlager <gerhard.schlager@discourse.org>
2026-06-25 11:16:17 -03:00
Gabriel Grubba fafe1fcc2c FIX: Filter inaccessible topic notifications in user menu messages (#41167)
When a user is removed from a PM, their existing notifications for that
topic weren't cleaned up, so they lingered in the user menu and still
showed the PM title. Nothing beyond the title was exposed(which they had
access to at some point), and removed users don't receive notifications
for new messages.

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-06-24 10:21:52 -03:00
Gabriel Grubba cd61b39932 FIX: add sanitize to category read-only banners (#41058)
We added this in 2016, and at the time, staff could add `<div
style="...">`.

If users want to add styles, they should use themes/theme components.

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-06-19 17:56:12 -03:00
Gabriel GrubbaandClaude Opus 4.8 780e6faeb9 SECURITY: Don't leak hidden tag names in one-per-topic conflict reasons (#41035)
Previously, `Tags::Search` looked up conflicting one-per-topic tag names
with an unscoped `Tag` query, allowing users to disclose hidden tag
names by passing their IDs in `selected_tag_ids`.

This change scopes that lookup to `visible_tags` and returns a generic
conflict reason when no conflicting selected tags are visible, so hidden
tag names are no longer exposed.

Reported in patch-triage/1124

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:26:15 -03:00
Gabriel GrubbaandClaude Opus 4.8 76cc8523f7 FIX: Require channel access to delete your own chat messages (#41003)
Before, a user who left a direct message or lost access to a private
category channel could still delete their own messages there, because
`can_delete_chat?` only checked ownership and channel status — not
current visibility.

This change requires `can_preview_chat_channel?` for self-deletes,
mirroring `can_restore_chat?`, so former members can no longer trash
messages in channels they can no longer access.

This ensures symmetry in guardian methods.

Relates to PATCH-1087

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 10:18:25 -03:00
Gabriel GrubbaandClaude Fable 5 79cbf171c0 DEV: Scope reactions-received to only posts the owner can see (#40837)
`reactions_received` built its post list with
`filter_allowed_categories`, which only scopes by category. We should be
checking it as we would for a regular post list

With this change, this method is in line with `reactions_given`, which
already runs its results through a visibility guard.

relates to: PATCH-1292

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 12:36:54 -03:00
Gabriel GrubbaandClaude Opus 4.8 0bf14de710 FIX: Event image crashed the advanced post-event builder (#40798)
Follow-up to #39835, which converted the post-event builder to FormKit.

The FormKit `image` control expects its field value to be a URL string:
it forwards the value straight to `UppyImageUploader`, which runs it
through `getURLWithCDN`. The advanced builder instead stored the whole
upload object as the field value, so the control handed an object to
`getURLWithCDN` and threw `startsWith is not a function`.

This was erroring on deployed sites, but on locally it was working as
normal.

Store the URL string in the form field (what the image control needs)
while keeping the upload object on the event model (what `buildParams`
needs for the `[event image="…"]` short_url). Everything else behaves as
before.

---

Also updated the modal to use `@inline` so it can be mounted in a
rendering test; it is `undefined` in regular run mode (modals open via
`modal.show`), so there is no behaviour change.


Demo before:



https://github.com/user-attachments/assets/9fd7fe7c-b317-4192-9256-7f46d11b9859



Demo now:



https://github.com/user-attachments/assets/eb9ed34f-a0a5-43b1-abd5-2ca6ea8c8e11

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 13:24:36 -03:00
Gabriel GrubbaandClaude Opus 4.8 00c47da07b DEV: Check featured topic visibility in CurrentUserSerializer (#40755)
Follow-up to #40549, which added a `can_see_topic?` guard to
`UserCardSerializer`

`CurrentUserSerializer` inherits from `BasicUserSerializer` directly, so
it kept its own unguarded `featured_topic` – this PR fixes that


relates to patch/1278

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 10:38:00 -03:00
eccaa62a85 FEATURE: Add recaptcha provider to hcaptcha plugin (#39018)
This is a revival of this PR:
https://github.com/discourse/discourse/pull/33636


### Description
This PR adds ReCaptcha as a Captcha provider and provides a refactor to
allow other Captcha providers to be added more easily. To achieve this,
the following changes were introduced:


- Provider Pattern Introduction in
lib/discourse_hcaptcha/captcha_provider.rb
- Controller refactoring, created a new Parent controller, extracted
common logic
- Serializer for each captcha provider
- Implemented base abstract component for FE captcha logic
- h-captcha-service is now provider agnostic (captcha-service) and
manages both captcha providers
- Added connectors to the UI to allow the chosen captcha to be rendered
- Added problem check and site settings for the newly introduced
ReCaptcha
- `before-create-account` valueTransformer is introduced to allow
captcha validation (or any other data) without modifying the class.

---------

Co-authored-by: Juan Martinez <juan@discourse.org>
Co-authored-by: David Taylor <david@taylorhq.com>
2026-06-09 09:08:26 -05:00
c61d9f0262 DEV: Add can-create-topic-button value transformer to canCreateTopic in navigation.gjs (#40587)
Relates to https://github.com/discourse/discourse/pull/39918

---------

Co-authored-by: David Battersby <info@davidbattersby.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:28:09 -03:00
Gabriel GrubbaandClaude Opus 4.8 1e48a5d535 FIX: Respect poll result visibility for ranked-choice outcomes (#40578)
Ranked-choice polls always serialized `ranked_choice_outcome` (the
computed winner and round-by-round activity) regardless of the poll's
`results` setting, unlike vote counts and voter lists, which are gated
by `Poll#can_see_results?`.


The outcome was therefore displayed to users who should not yet see
results on `on_vote`, `on_close`, and `staff_only` polls — through the
topic view, the vote/remove-vote responses, and the MessageBus
broadcast.

- Gate `ranked_choice_outcome` in `PollSerializer` behind
`can_see_results?`, matching the other result fields.
- Stop manually re-appending the outcome in `vote`/`remove_vote` so the
serializer's gating is authoritative.
- Serialize the MessageBus payload as an anonymous user so the
topic-wide broadcast only carries data any viewer may see, matching
`PollsUpdater.publish_changes`.
- Don't render an empty results panel to a non-staff voter on a
`staff_only` poll; show the ballot and the staff-only notice instead.



Relates to /t/-/185090
And Patch 998

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:04:38 -03:00
Gabriel Grubba c9915c69f2 Revert "FIX: Await channel lookups before redirecting in chat index routes" (#40474)
Reverts discourse/discourse#40284
2026-06-02 10:16:16 -03:00
Gabriel GrubbaandClaude Opus 4.8 6e69e6029d FIX: Destroy pending PM after an unexpected send error (#40461)
Previously, when `Scriptable::Utils.send_pm` raised an unexpected error,
`Jobs::DiscourseAutomation::Tracker#send_pending_pm` only logged it and
left the `PendingPm` row intact, so the scheduled job re-processed the
same failing PM every minute indefinitely.

This change destroys the pending PM in the generic `rescue` block —
mirroring the existing `ActiveRecord::RecordNotSaved` handling — so a
permanently-failing PM is removed after one attempt instead of looping
forever.

Resolves https://patch.discourse.org/patch-triage/14

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:02:51 -03:00
Gabriel Grubba 2fbfd5d48d FIX: Add back description to calendar Description component for backwards compatibility (#40280)
On
https://github.com/discourse/discourse/commit/866ae39d4642f667eb75a663e672d0eaf32db595
we added `description_html` and updated
`/discourse-post-event/description.gjs` to only use it, but previously
we used `description` in the outlet:


https://github.com/discourse/discourse/blob/b4643f7151d37af312aaf8ce99dc5c163372d0c3/plugins/discourse-calendar/assets/javascripts/discourse/components/discourse-post-event/index.gjs#L251-L256

In this pr we add back the description to the component so that it
maintains backwards compatibility with plugins that use the description
outlet.
2026-06-01 12:07:57 -03:00
Gabriel Grubba cd800386f1 FIX: Whisper messages should not create calendar events (#40282)
Before, staff could create a whisper with a local date in a calendar
topic, which created a visible calendar event from a staff-only post.

This change rejects whispered calendar events, so whispers no longer
create visible calendar entries.

Relates to /patch-triage/993
2026-06-01 12:04:51 -03:00
Gabriel Grubba e233b9906d FIX: Await channel lookups before redirecting in chat index routes (#40284)
Before we had a bug because we were using the .then pattern: the
transition continued before the redirect fired. Leading to a brief
flash.

Now beforeModels are using the `async/await` pattern. Specs were added
to cover these changes
2026-06-01 12:02:56 -03:00
Gabriel Grubba f835e133a9 FIX: Add rate limiting to Ad impression (#38670)
Users should be rate-limited on ad impression tracking so that the
reports can better reflect the true performance of the ad
2026-05-26 16:17:37 +10:00
Gabriel GrubbaandClaude Opus 4.7 fd4ef34657 FIX: Filtered RSS feeds advertise the wrong self URL (#40252)
## Summary

his PR fixes a bug where Discourse's RSS feeds for
latest/top/hot/category/user-topics/top-by-period advertised a bare
<link> and <atom:link> (e.g. `/latest.rss`) even when the request
included filters like `exclude_tag=...` or `period=.... `Subscribers
were effectively pointed at a
  different feed than the one they requested.
## Source

- Patch Triage:patch/891
- Original commit: 

---

🤖 Auto-generated from the patch diff via Patch Triage. Review carefully
before merging.

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>


---

Low-risk  and no API change.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 18:21:16 -03:00
Gabriel Grubba 9dc7064af1 FIX: Events should not lose their multiline formatting (#40277)
## Summary

This patch fixes a regression where event descriptions lost their
multiline formatting by ensuring newline characters are converted to
<br> tags in the server-generated description_html. It updates the
linkify_description utility to handle both URL linkification and line
break preservation, ensuring the UI correctly renders multiline text.

## Source

- Patch Triage: https://patch.discourse.org/patch-triage/886
- Original commit: 

---

🤖 Auto-generated from the patch diff via Patch Triage. Review carefully
before merging.

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-05-25 18:08:18 -03:00
Gabriel Grubba e52b385ae1 FIX: type: objects uploads should be stored as IDs (#40178)
Inside `type objects` we accepted upload URLs and converted them into
IDs, but we did not update the original object to have an ID.

Only on consuming should we turn the ID into a URL.

For normal front-end consumers, object upload fields should still be
URLs. Backend consumers should expect upload IDs.

<img width="2295" height="1263" alt="Screenshot 2026-05-20 at 12 19 23"
src="https://github.com/user-attachments/assets/20d411c4-1570-45fc-96c8-02f2b1b3639d"
/>
2026-05-21 13:45:27 -03:00
Gabriel Grubbaanddiscourse-patch-triage[bot] <272280883+discourse-patch-triage[bot]@users.noreply.github.com> a0e3c05a50 FEATURE: extend chat/new-message with prefilling text api (#40007)
Previously, `/chat/new-message` only accepted a `recipients` param to
open a new direct message, with no way to send a message to an existing
channel or pre-fill the composer.

This change adds `channel_id`, `channel` (slug), and `message` params so
external links can drop a user into the right conversation with the
composer ready to send. Currently supported combinations:

  - `/chat/new-message?channel_id=42&message=hi`
  - `/chat/new-message?channel=general&message=hi`
  - `/chat/new-message?recipients=alice,bob`
  - `/chat/new-message?recipients=alice&message=hi`

If both `channel_id`/`channel` and `recipients` are passed, the channel
param wins and `recipients` is ignored.

---------

Co-authored-by: discourse-patch-triage[bot] <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-05-18 13:43:39 -03:00
Gabriel Grubba 7c271a81ab FEATURE: Bot only PMs filter (#39891)
This adds the bot chat filter for PMs. It routes the user to
`/discourse-ai/ai-bot/conversations` on clicking the Bot Chats button
and removes Bot Pms from the regular PM

Demo:


https://github.com/user-attachments/assets/aa0e1877-aacc-48d3-b658-7e6dde22d535
2026-05-18 10:29:03 -03:00
Gabriel Grubba 368f0635c9 UX: Update setting links pointing to meta to use <a> tag (#40002)
Some meta links were just a string without having the <a> tag so they
would look off. This updates the meta links I could find.

I think we can, in the future, update other raw links to all use this
pattern.

Before:

<img width="502" height="144" alt="Screenshot 2026-05-13 at 16 15 00"
src="https://github.com/user-attachments/assets/c1bf9685-6d24-48f7-a87e-3085ff443ca3"
/>


After:

<img width="862" height="498" alt="Screenshot 2026-05-13 at 16 13 04"
src="https://github.com/user-attachments/assets/5595862c-0c8f-4f23-bd89-2be6766a98e3"
/>
2026-05-13 16:35:37 -03:00
Gabriel Grubba b5f7d48840 UX: Ignored users reactions/likes should not show up (#39672)
With this change, a user who is ignoring another no longer sees that
user's likes or reactions: the ignored user is hidden from the
likes/reactions lists, and the like and reaction counts shown to the
ignoring user are adjusted accordingly.

Anonymous viewers and viewers who aren't ignoring anyone see the
original counts.

See it in action in the video:


https://github.com/user-attachments/assets/535e9aaa-0a1c-49eb-a2d4-5fdebf9d7539
2026-05-11 15:32:29 -03:00
Gabriel GrubbaandClaude Opus 4.7 75db3d669d FEATURE: add bootstrap first admin job (#39851)
On https://github.com/discourse/discourse/pull/37694, we ended up
removing one of the nice things of the bootstrap mode, the auto
moderation for the first admin.

I've brought back the job that used to run for the bootstrap mode –
maybe we could just run this code in the `default_current_user_provider`

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 16:16:18 -03:00
Gabriel Grubba a2853178bf DEV: move inline_data check for onbox to be after domain check (#39792)
Small logic cleanup: we should be checking the domain before doing the
oneboxing.

Sites relying on inline GitHub PR status without `github.com` in
`allowed_inline_onebox_domains` will need to add it.

Also adds a defensive domain/blocklist check inside
`Oneboxer.inline_data_for` itself, and refactors `lookup` to use early
returns.
2026-05-06 13:53:23 -03:00
Gabriel Grubba 803177cf35 DEV: moving BasicVoterSerializer to use BasicUserSerializer (#39763)
Removes the duplicate identity-attribute declarations in
BasicVoterSerializer in favor of the canonical chain, leaving only the
voter-specific `direction` attribute on the subclass.
2026-05-05 17:05:04 -03:00
Gabriel Grubbaanddiscourse-patch-triage[bot] <272280883+discourse-patch-triage[bot]@users.noreply.github.com> 6c10d900f3 FIX: Ensure that the current user's guardian is used when running AI tools tests. (#38676)
The tool runner guardian should be the same as the current user running
the tests.

Co-authored-by: discourse-patch-triage[bot] <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-04-29 16:25:52 -03:00
Gabriel Grubba de27c3175b FIX: user action notifications and list should not list ignored users (#39589)
We should filter out ignored users in the notifications feed.

Here is a before:



https://github.com/user-attachments/assets/47d963ce-b66c-4f34-82b8-73c1a63722b7



And after:




https://github.com/user-attachments/assets/c176db1a-cb93-4df5-870b-df95e66f56ac
2026-04-28 14:44:49 -03:00
Gabriel GrubbaandClaude Opus 4.7 bb403ad33a DEV: move value transformer to be behavior in homepage model (#39348)
Moving `custom-homepage-model` from using a value transformer to using a
behavior one because consumers _might_ do async calls, breaking one of
the rules stated in `registerValueTransformer`:


https://github.com/discourse/discourse/blob/86046da198d1db11903dfb06a5437847d044843d/frontend/discourse/app/lib/plugin-api.gjs#L470-L474

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:19:16 -03:00
Gabriel GrubbaandClaude Opus 4.7 6e294019a1 DEV: Add modifier for default_navigation_menu_categories (#39354)
This is useful for plugins that want to dynamically change the default
categories for the default sidebar categories(anonymous and regular
users when registering)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:42:00 -03:00
Gabriel Grubba 7a49f9edfb dev: add custom-homepage-model transformer (#39324)
Previously, themes/plugins customizing the custom homepage had to fetch
data from inside components using AsyncContent or via fetch, which
produced per-component loading spinners after the outlet rendered.

  This adds a custom-homepage-model value transformer so themes and
  plugins can provide the model (sync or async) at route-load time. The
  page renders completely instead of flashing per-block spinners.

Consumers who still want to fetch data after the homepage is loaded will
not be affected by this; the model returning null should not make the
page loading slower
2026-04-17 10:19:17 -03:00
Gabriel Grubba 63454e99f9 UX: move regular buttons to new hover buttons in admin onboarding banner (#38826)
How it looks after the changes:


https://github.com/user-attachments/assets/4c924b72-4226-4252-b475-3717a87f9afd
2026-03-24 13:50:44 -03:00
Gabriel Grubba 3b2b3d5852 UX: Add option to hide action button on modal based on custom logic (#38789)
StartPostingOption can now take a `hideAction` method that will
conditionally hide the action button on the start posting modal. This is
useful for cases where we want to show the option but not allow users to
take the action more than once.
2026-03-23 13:11:07 -03:00
Gabriel Grubba 4ec7d97832 FEATURE: Filter topics by bookmark creation date (#38662)
This PR allows you to filter topics by the date of your bookmarks.

For example, to find all topics & posts you have bookmarked in the last
week you can run the following search:

```
bookmarked-after:2026-03-08 bookmarked-before:2023-03-15
```

`"filtering for topics by date column"` shared example was not used
because:
- This filter only works for logged-in users
- Bookmark setup and specific test `includes post bookmarks`
2026-03-23 09:44:59 -03:00
Gabriel Grubba bb16e3f67a DEV: add isComplete to start posting options (#38764)
It can be helpful for the options to know if the step is already
complete so they can adjust their display accordingly.
2026-03-23 09:43:00 -03:00
Gabriel Grubba dce7b107ec FIX: Fix bypass_bump parameter ignoring explicit false values (#38637)
This PR corrects the precedence of the `bypass_bump` parameter in
posts_controller.

If you passed `bypass_bump: false` at the top level, it was being
ignored if it had a nested `post[bypass_bump]: true` that was also
present.

Now it should be correct – request bypass_bump param should have
precedence over post bypass bump
2026-03-17 09:38:37 -03:00
Gabriel Grubba a96481d822 FIX: Fix trailing colon in redirect URI display for custom scheme URLs (#38625)
A small logic bug for custom scheme URL: some URLs (like
`myapp://callback`) would display with a trailing colon (e.g.,
`callback:`) because `uri.port` is `nil` for non-HTTP scheme.


Also refactored tests
2026-03-16 13:21:23 -03:00
Gabriel Grubba 00afa4169f DEV: remove unused start_with_this string (#38624)
this is not being used, it seems to be a leftover from a refactor
2026-03-16 12:21:44 -03:00
Gabriel Grubba 541ef5ac4e FIX: Moderators being able to clear suspensions and silences from other staff accounts (#38563)
This is an edge case somewhat related to
https://github.com/discourse/discourse/pull/35452

Moderators could unsuspend and unsilence other staff accounts, even tho
they couldn't suspend or silence them in the first place.
2026-03-16 10:30:07 -03:00
Gabriel Grubba 85d0ef095c DEV: change from GET to POST in chat_summary#show (#38495)
Minor cleanup:
- Turned summarize method in chat-modal-channel-summary component to
async/await from promise then/catch.
- Update summary endpoint to use POST instead of GET, as it is
performing an action that changes state and have side effect.
- Added tests
2026-03-11 11:12:36 -03:00
Gabriel Grubba 8c18481d86 DEV: change GET /admin/dashboard/problems.json to POST as it performs a state-changing action (#38462)
Minor cleanup: `/admin/dashboard/problems.json` should be a post as it
runs the ProblemCheck class


No plugin uses this endpoint
2026-03-11 09:50:55 -03:00
Gabriel Grubba 83f77449c6 DEV: thread count should be 0 if threading is disabled in chat (#38383)
Somewhat a follow-up from
https://github.com/discourse/discourse/pull/36719, that change prevented
the blue dot notification from appearing

We now ensure that when threading is disabled, both
`unreadThreadsCountSinceLastViewed` and `watchedThreadsUnreadCount`
return 0
Also added unit testing to `chat-channel ` service.
2026-03-11 09:41:35 -03:00
Gabriel Grubba 52eb4e2ec4 DEV: Change authenticated_with_oauth to use key instead of string. (#38381)
Minor cleanup updating how `authenticated_with_oauth` was being called.

`Rack::Session` object supports indifferent access, all
`authenticated_with_oauth` uses are key-based, and this one was off

Also added tests for `LoginActivator` class.
2026-03-10 18:31:32 -03:00
Gabriel Grubba 06a5e45a53 DEV: await missing on recursive stateFor call in chat drawer router (#38428)
_really_ minor dev cleanup 

This method (`stateFor`) was initially added in
https://github.com/discourse/discourse/pull/28212 – it checks if a route
has a redirect. If it does, it calls this method recursively with'
stateFor'.

In https://github.com/discourse/discourse/pull/29879, `stateFor` got
asyncified, but its recursion calls did not. In this fix, we address it
2026-03-10 15:38:26 -03:00
Gabriel Grubba dd2792d00f DEV: cleanup extra i18n key fallback in status_date_label (#38432)
`default: I18n.t("onebox.github.opened")` should function as a fallback
2026-03-10 15:38:14 -03:00
Gabriel Grubba ebdb3de2e7 FIX: Fix ObjectsSettingValidator error message handling (#38336)
Before, when adding a `type: object` site setting, if it did not match
the schema, the only message returned was `Invalid object.`

With these changes, it is possible to understand which properties did no
match the schema
2026-03-09 14:50:17 -03:00
Gabriel Grubba 55a4534653 DEV: Change reply action in the discoveries controller to be a POST and added tests for discobot-discoveries service (#38338)
Updated the action to be a post as it does make side effects, in our
case, it enqueues a job, and updated the client accordingly – I've
checked if there is any other usage of this endpoint in plugins

Also, improved reply action in the client to allow retries when failed,
ensured duplicate requests are not sent, and added their respective
tests.
2026-03-09 14:50:10 -03:00
Gabriel Grubba e731ddef06 FIX: Failed updates in tag group controller would be logged (#38333)
If TagGroupsController#update failed, it would silently swallow
validation failures and create staff action log entries for every call.
2026-03-06 17:11:12 -03:00
Gabriel Grubba 90c00114aa DEV: Add verification for CDN fallback assets to prevent directory traversal (#38297)
This ensures that the path being accessed is within the fallback
directory.

The same check as
[`static_controller.rb#L276`](https://github.com/discourse/discourse/blob/7caacf32f0006c0cdaaafce160afcae160bf810a/app/controllers/static_controller.rb#L276):

https://github.com/discourse/discourse/blob/7caacf32f0006c0cdaaafce160afcae160bf810a/app/controllers/static_controller.rb#L276
2026-03-05 16:58:50 -03:00
Gabriel Grubba 5516947b75 DEV: Ai bot Signature verification uses different body source than action logic (#38294)
Discord BotController signature verification uses `request.raw_post`,
but action logic reads body via `request.body.read` — This PR unifies
usage.
2026-03-05 16:30:05 -03:00
Gabriel Grubba b4925714f5 DEV: Add informative blocks with category elements to chat (#37932)
Introduces a new "informative" block type for chat messages that can
display category information.

This enables bots to present structured category information and other
informational content in chat with blocks.

This is how it looks:

## Examples:
<img width="227" height="129" alt="Screenshot 2026-02-25 at 14 51 52"
src="https://github.com/user-attachments/assets/40f08255-ae5b-4106-a654-8c709bd588e3"
/>

`{ type: "category", title: "Support", color: "0088CC" }`

---

<img width="291" height="136" alt="Screenshot 2026-02-25 at 14 50 56"
src="https://github.com/user-attachments/assets/64815423-672a-42b8-ac4d-b5ccc816b941"
/>

`{ type: "category", title: "Support", color: "0088CC", description:
"Get help from the community" }`

---

<img width="338" height="154" alt="Screenshot 2026-02-25 at 15 02 07"
src="https://github.com/user-attachments/assets/89f820a5-a4d5-41fd-b3e7-8ea674c19a67"
/>

`{ type: "category", title: "Dev", color: "E45735", description:
"Development related discussions", parent_name: "Meta", parent_color:
"0088CC" }`

---

You can have multiple elements per block and they _shoud_ take only one
row:

<img width="459" height="345" alt="Screenshot 2026-02-25 at 15 32 39"
src="https://github.com/user-attachments/assets/710faf9a-c85c-42b2-ab33-5c7e68ea6496"
/>
<img width="530" height="183" alt="Screenshot 2026-02-25 at 15 33 08"
src="https://github.com/user-attachments/assets/d10d444d-b252-42bc-affc-56cd9f5724ca"
/>


## How to use
The only required params are: `type`,`title` and `color`
```rb
      ChatSDK::Message.create(
        raw: " ",
        blocks: [
          {
            type: "informative",
            elements: [
              {
                type: "category",
                title: "Support",
                color: "0088CC",
                description: "Get help from the community",
              },
            ],
          },
        ],
        guardian: bot_guardian,
        channel_id: bot_channel_id,
      )
```
2026-02-25 18:56:53 -03:00
Gabriel GrubbaandPenar Musaraj cba702c88f FEATURE: Add "Use Predefined Topics" modal to admin onboarding banner (#37826)
Continuing the work from
https://github.com/discourse/discourse/pull/37583 we are adding a few
options to our users to start creating content and topics in their new
community

This makes the predefined topics usage clearer and not random, and this
also adds a way for plugins to hook into the admin onboarding
experience, using the "admin-onboarding-start-posting-options` behavior
object and `StartPostingOption` class

When we have a plugin hooked up to this, it shows the option picker; if
not, it goes directly to predefined options:

Default behavior:



https://github.com/user-attachments/assets/d8e3eaf7-dc36-4f1e-b9e2-5bb12f353e88


with 1+ options:


https://github.com/user-attachments/assets/c05be6b0-4ff8-477b-9a9d-f755846730b5

---------

Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>
2026-02-25 13:55:18 -03:00
Gabriel Grubba edb1c65149 UX: Remove Bootstrap Mode (#37694)
We have been revamping our admin onboarding, and one of the items on our
list was to remove the bootstrap mode.

This mode was disabled by default and hidden.
2026-02-17 16:46:32 -03:00
Gabriel Grubba a5f00f08ab UX: move InviteCollaborators onboarding step to be the first (#37764)
New staff should get their collaborators in there as soon as possible
and figure things out together!

This is how it is looking with these changes:
<img width="850" height="225" alt="Screenshot 2026-02-12 at 11 28 31"
src="https://github.com/user-attachments/assets/fb504b23-205d-4926-a55e-bc0c66d5ef47"
/>
2026-02-17 14:37:51 -03:00
Gabriel GrubbaandPenar Musaraj 07a747eecb FEATURE: onboarding banner for admins (#37583)
We planned to add a new banner for newly created communities to help
guide site owners through initial setup steps.

At the moment, it has 3 items in the checklist:
- Invite collaborators -> Opens invite modal
- Start posting -> Opens composer with a seed icebreaker topic
- Spread the word -> Copies forum link to clipboard

You can see it working in this video:


https://github.com/user-attachments/assets/ed693321-e416-43f9-8182-0243af193688


Current visuals:

<img width="1723" height="393" alt="image"
src="https://github.com/user-attachments/assets/3021c0b1-30e8-4ba6-aa43-aebf71061a8a"
/>



TODO:
- [x] Tests
- [x] Work on CSS

Maybe todos:
- [x] Exclude routes where the banner shouldn't appear
- [x] Add metadata in ice breaker post to  identify it
as an ice breaker topic?
- [x] Check if there is a way to determine if the user is the _first_
admin user?
- [x] Should we have a happy indicator when the user finishes all
tasks(maybe a toast?)

---------

Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>
2026-02-10 14:53:44 -03:00
Gabriel Grubba be5e9aead9 FIX: Calendar not refreshing on navigating between categories (#36851)
Before it was working like this video:

**Now** it works like this:


https://github.com/user-attachments/assets/e98c3509-4a53-4faa-aa7e-f2b68583391a


But **before** navigating between categories did not update and refresh
the events.


https://github.com/user-attachments/assets/0b06b78d-2ba4-4193-a991-0ec52068f78c
2025-12-29 11:17:00 -03:00
Gabriel Grubba 6c9ef61520 FIX: Remove nameless uploads from deleted posts (#36484)
Nameless attachments should be removed too,
now `![](upload://key.png)` will be removed from deleted posts, when
using `Remove Upload Markup From Deleted Posts` automation.
2025-12-08 13:51:35 -03:00
Gabriel Grubba b72cfbaa7a FEATURE: Add modifier hook for tags being saved (#36400)
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
```
2025-12-03 13:40:49 -03:00
Gabriel Grubba 568afbae47 DEV: Add optional s3_stale_while_revalidate and s3_max_age for uploads (#36298)
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.
2025-12-01 14:57:50 -03:00
Gabriel Grubba 239f5058f6 Revert "A11Y: Add title to custom link section" (#36229)
Reverts discourse/discourse#36198

Discussed with @awesomerobot, and this change was more annoying than
helpful, so I'm reverting it
2025-11-25 16:08:09 -03:00
Gabriel Grubba a8cfcfb7ec A11Y: Add title to custom link section (#36198)
Before, we were not adding a title attribute to custom sections that
were added via UI;

_Now_ it should add those automatically:

<img width="581" height="354" alt="Screenshot 2025-11-24 at 18 03 11"
src="https://github.com/user-attachments/assets/0e6aeb22-a0d8-431a-a657-c3f864a30c98"
/>

<img width="1500" height="314" alt="Screenshot 2025-11-24 at 18 03 23"
src="https://github.com/user-attachments/assets/339ede0e-34be-4ba7-88b5-5f4a82588e25"
/>
2025-11-25 09:35:50 -03:00
Gabriel Grubba 2f78101ce2 FIX: use PostRevisor with Topic context when revising posts from deleted topics (#36168)
Relates to https://github.com/discourse/discourse/pull/36020, we did not
account for posts whose topics have been deleted. In such cases, we need
to provide the Topic context to PostRevisor to avoid errors.
2025-11-21 16:58:10 -03:00
Gabriel Grubba 6800d63bfc FIX: type object setting not redirecting on saving (#36150)
With https://github.com/discourse/discourse/pull/35349, now site
settings do not return a body on update, which broke the type object
setting editor's save flow. This commit adds handling for that case.

And also adds testing to ensure that after saving the setting, we are
redirected to another page.

Renamed `admin_objects_theme_setting_editor` to
`admin_objects_setting_editor` since it can be used for both themes and
settings.


Before:


https://github.com/user-attachments/assets/5e019428-e126-4084-80a6-eb324c851427


Now:


https://github.com/user-attachments/assets/9ac59ca2-1896-490a-866c-630a5a201471

A future todo would be to migrate [ThemeController#update_single_setting
](https://github.com/discourse/discourse/blob/71834c898f2f3f5d11df3db6f9a5bab12acbccaa/app/controllers/admin/themes_controller.rb#L343-L365)
to use the same pattern as
[`SiteSettingController#update`](https://github.com/discourse/discourse/blob/d8e7741d9645d39a637037c7720dd6fc5f261284/app/controllers/admin/site_settings_controller.rb#L25-L53)
2025-11-21 10:26:08 -03:00
Gabriel GrubbaandPenar Musaraj 1fba925cfb FEATURE: Add automation to remove uploads from deleted posts (#36020)
This PR introduces a new automation script that removes uploads attached
to deleted posts.

Using this automation will add a revision to deleted posts by the System
User, removing the upload or attachment references. This Automation goes
a 1000 by every run and does not re-run on already ran posts

When the automation cleans the posts, uploads will no longer have
reference to the post and on the next time the [clean up uploads
job](https://github.com/discourse/discourse/blob/f413e1b0de8c326a12da29f7a4bfa3a6eed004d1/app/jobs/scheduled/clean_up_uploads.rb#L1-L79)
runs it will permanently removes the uploads.

Example of automation run:

```
 Hey it is a regular post with a link to [Discourse](https://www.discourse.org) 
 and an image: ![logo.png|100x200](upload://gj6GJHlc1Sa5YXuGz549oXBbcFv.png)
 and a file: [small.pdf|attachment](upload://3bWzVVoRhUXxi7tiPenInoebHyX.pdf) (130 Bytes)
```
When the automation has run in this post, this will be the revisioned
version:

```
 Hey it is a regular post with a link to [Discourse](https://www.discourse.org) 
 and an image:
 and a file: 
```

---------

Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>
2025-11-18 14:21:03 -03:00
Gabriel Grubba ab435cceab FIX: assigned filter group visibility (#35726)
The assigned filter should take into account the visibility of the
group.

This PR fixes that and also adds testing to the `assigned:` filter

relates to: https://github.com/discourse/discourse/pull/35712
2025-11-11 11:35:44 -03:00
Gabriel Grubba 42a0ee0884 FIX: csv import not being casing aware for tag groups (#35837)
Relates to
https://meta.discourse.org/t/tag-taggroup-csv-import-is-case-aware-but-its-not/381450
2025-11-10 11:34:23 -03:00
Gabriel Grubba d45abd55ad FIX: wrong user in action logs page (#35842)
This PR fixes
https://meta.discourse.org/t/staff-action-logs-show-the-wrong-user/387508

Based on https://github.com/discourse/discourse/pull/12096, we should
force the refresh on route change



https://github.com/user-attachments/assets/7995bbff-5132-4175-85f7-5c527acc8d26
2025-11-07 10:25:53 -03:00
Gabriel GrubbaandMark VanLandingham 826e4073c6 FIX: group visibility when filtering for created-by (#35712)
For these filters, we also needed to add `members_visible_groups`.

---------

Co-authored-by: Mark VanLandingham <markvanlan@gmail.com>
2025-10-30 15:25:34 -03:00