Using CTEs and DISTINCT ON to:
- Pre-filter active users with correct preferences
- Get only first unread message per channel
- Eliminate redundant joins and message scanning
This reduces the query execution time by limiting message scanning and joins to only relevant users and messages.
Internal ref t/142836 & t/139517
This commit will hide the channel when the side panel is present and the width of the viewport is less than 1000px. This is especially useful when you want to focus reading a thread on a small screen.
This change only impacts desktops.
* FIX: `TopicTagsChanged` trigger not working with multiple tags
The check for when had multiple tags was being exclusive, you had to have all tags to trigger the automation, now it's inclusive, you can trigger the automation if you have any of the tags.
* DEV: add specs for when having multiple categories
* DEV: changed to use unions and intersections for tags
added more tests to check for multiple tags
Historically the behavior of this file has been complexified to attempt to answer this use case:
A user has two tabs open, tab 1 is on a topic, tab 2 is on a chat channel. If your active tab is tab 1 and someones sends you a mention in chat. We will show a desktop notification, but in which tab the channel should open if you click it? The changes made years ago said: in tab 2.
I think this is complexifying too much this codepath and is also confusing. You might wonder why this discourse notification you clicked opened in some of your 50 tabs in the background when you had a discourse tab active currently in front of you.
Moreover, a recent change has made the notification to only happen on desktop, but all the subscription stuff was happening regardless of mobile or desktop.
- Clicking the channel title of a collapsed drawer will only open the drawer, and not open settings
- Remove the back button when the drawer is collapsed
- Uses same icon for toggling on chat that composer
- add max-width to minimised drawer + add hover effect
---------
Co-authored-by: chapoi <101828855+chapoi@users.noreply.github.com>
Commit c2a733a95a was applied to an
existing migration that possibly had already run. Due to this some
discourse instances might not have the correct index.
This change removes the original migration and creates a new one so that
it will actually be applied.
This is the missing index that some sites might not have:
```
Missing Index | CREATE INDEX
index_chat_messages_on_chat_channel_id_and_id ON public.chat_messages
USING btree (chat_channel_id, id) WHERE (deleted_at IS NOT NULL)
```
In particular, this applies:
- new `discourse/no-implicit-this` template-lint rule
- `init`/`willDestroy` ordering enforcement
- `lines-between-class-members`
Prior to this fix it would cause an error as we need to pass the user to get the title of the channel.
This commit also adds a test for message interaction serializer.
This optimizes a hot path when users are being removed from one or more groups since we use the `on(:user_removed_from_group)` event to call the `Chat::AutoLeaveChannels` with a `user_id` parameter. In such case, we don't need to clear the membership of all users, just that one user.
DEBUG: Also added a "-- event = <event>" comment on top of the SQL queries used by the "AutoLeaveChannels" so they show up in the logs and hopefully facilitate any performance issues that might arise in the future.
In some rare cases, this was causing the input to be invisible. The
change here means the input will have a smaller max height, but since
here we are limiting this to 25% of the viewport height, it should be
more than fine.
It's also not necessary to include the `chat-header-offset`, it ends up
being only a few pixels' difference (since, again, it is divided by 4).
Blocks allow BOTS to augment the capacities of a chat message. At the moment only one block is available: `actions`, accepting only one type of element: `button`.
<img width="708" alt="Screenshot 2024-11-15 at 19 14 02" src="https://github.com/user-attachments/assets/63f32a29-05b1-4f32-9edd-8d8e1007d705">
# Usage
```ruby
Chat::CreateMessage.call(
params: {
message: "Welcome!",
chat_channel_id: 2,
blocks: [
{
type: "actions",
elements: [
{ value: "foo", type: "button", text: { text: "How can I install themes?", type: "plain_text" } }
]
}
]
},
guardian: Discourse.system_user.guardian
)
```
# Documentation
## Blocks
### Actions
Holds interactive elements: button.
#### Fields
| Field | Type | Description | Required? |
|--------|--------|--------|--------|
| type | string | For an actions block, type is always `actions` | Yes |
| elements | array | An array of interactive elements, maximum 10 elements | Yes |
| block_id | string | An unique identifier for the block, will be generated if not specified. It has to be unique per message | No |
#### Example
```json
{
"type": "actions",
"block_id": "actions_1",
"elements": [...]
}
```
## Elements
### Button
#### Fields
| Field | Type | Description | Required? |
|--------|--------|--------|--------|
| type | string | For a button, type is always `button` | Yes |
| text | object | A text object holding the type and text. Max 75 characters | Yes |
| value | string | The value returned after the interaction has been validated. Maximum length is 2000 characters | No |
| style | string | Can be `primary` , `success` or `danger` | No |
| action_id | string | An unique identifier for the action, will be generated if not specified. It has to be unique per message | No |
#### Example
```json
{
"type": "actions",
"block_id": "actions_1",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Ok"
},
"value": "ok",
"action_id": "button_1"
}
]
}
```
## Interactions
When a user interactions with a button the following flow will happen:
- We send an interaction request to the server
- Server checks if the user can make this interaction
- If the user can make this interaction, the server will:
* `DiscourseEvent.trigger(:chat_message_interaction, interaction)`
* return a JSON document
```json
{
"interaction": {
"user": {
"id": 1,
"username": "j.jaffeux"
},
"channel": {
"id": 1,
"title": "Staff"
},
"message": {
"id": 1,
"text": "test",
"user_id": -1
},
"action": {
"text": {
"text": "How to install themes?",
"type": "plain_text"
},
"type": "button",
"value": "click_me_123",
"action_id": "bf4f30b9-de99-4959-b3f5-632a6a1add04"
}
}
}
```
* Fire a `appEvents.trigger("chat:message_interaction", interaction)`
When using chat in drawer mode, after you've clicked on a chat bookmark in the user menu, clicking any other chat bookmark would "do nothing".
In 8b18fd1556 we added an optimization to prevent the same route from being reloaded, but it ended up breaking the bookmarks.
This commit reverts the changed made the above commit and adds a system specs that ensure we can click two chat bookmarks in the user menu when using chat in drawer mode.
Internal ref - t/134362
Previously we only counted mentions that were made within channels, however for threads this was never implemented.
This change adds a mention count to the ThreadUnreadsQuery, which is used for channel thread lists and the user thread list. We are also expanding channel mentions count to include mentions within threads.
The goal is to have a more consistent urgent badge across chat, in places such as channel lists and the chat header.
The job was removed in 6dfe2fbe16 as part of a performance-related refactor.
The issue was that job was already enqueued in sidekiq and now that the file has been deleted, it's generating a lot of `uninitialized constant Jobs::Chat::AutoJoinChannelBatch` errors.
Restoring this file will help clear the sidekiq queue. We'll remove the job in a few months.
Internal ref - t/141563
This page object also attempts to make the update automation more resilient as we are seeing flakey specs on this codepath. The solution for now is to ensure we have the sequence: click button/button is loading/ button has finished loading.
---------
Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
It splits the hide_profile_and_presence user option and the default_hide_profile_and_presence site setting for more granular control. It keeps the option to hide the profile under /u/username/preferences/interface and adds the presence toggle in the quick user menu.
Co-authored-by: Régis Hanol <regis@hanol.fr>
Chat channels that are linked to a category can be set to automatically join users.
This is handled by subscribing to the following events
- group_destroyed
- user_seen
- user_confirmed_email
- user_added_to_group
- user_removed_from_group
- category_updated
- site_setting_changed (for `chat_allowed_groups`)
As well as a
- hourly background job (`AutoJoinUsers`)
- `CreateCategoryChannel` service
- `UpdateChannel` service
There was however two issues with the current implementation
1. We were triggering a lot of background jobs, mostly because it was decided to batch to auto join/leave into groups of 1000 users, adding a lot of stress to the system
2. We had one "class" (a service or a background job) per "event" and all of them had slightly different ways to select users to join/leave, making it hard to keep everything in sync
This PR "simply" adds two new servicesL `AutoJoinChannels` and `AutoLeaveChannels` that takes care, in an efficient way, of all the cases when users might automatically join a leave a chat channel.
Every other changes come from the fact that we're now always calling either one of those services, depending on the event that happened.
In the making of these classes, a few bugs were encountered and fixed, notably
- A user is only ever able to access chat channels if and only if they're part of a group listed in the `chat_allowed_group` site setting
- A category that has no associated "category groups" is only accessible to staff members (and not "Everyone")
- A silenced user should not be able to automatically join channels
- We should not attempt to automatically join users to deleted chat channels
- There is no need to automatically join users to chat channels that have already more than `max_chat_auto_joined_users` users
Internal - t/135259 & t/70607
* DEV: add specs for auto join/leave channels services
* DEV: less hacky specs
* DEV: no instance variables in specs
https://github.com/mainmatter/qunit-dom/blob/master/API.md#isvisible will return true if offsetWidth or offsetHeight are zero which could happen in this case as the test could run before the image has loaded. By forcing a minimum height in the test we ensure it will be consistent.
Adds channels with unread threads (watching/tracking) to the sorting logic for both public and direct message channels.
Previously channels with unread threads could easily be missed as we didn't bump them to the top when new thread replies were created.
We are also adding a blue unread badge next to DM channels when there is an unread thread, as previously they weren't appearing as unread within the DMs tab (they only showed within the My Threads section).
This change will only prevent a cooked message with [grid] to show [grid] instead the content will be wrapped in `div class="d-image-grid"`. This is only enabled on messages made by bot, as regular users could use grid but have no reason to use it ATM. It will also not apply the decoration which shouldn't change the behavior more than just remove grid markup from the message
`chatThreadPane.isOpened` was returning `false` when it should return `true` due to an incorrect matching on the route.
Note that visiting a thread will focus the composer and the first escape will blur the input, only the second will close the panel.
This is because rules is pointing to the same array MARKDOWN_IT_RULES, which is modified directly. Modifying rules with << changes the original MARKDOWN_IT_RULES array, so every call to something works with the altered array state from previous calls.
The markdown it rule "heading" will only be used when the message is done by a bot, which means an id < 0.
This commit also adds a is-bot css class on messages made by a bot, for finer control.
---------
Co-authored-by: Martin Brennan <mjrbrennan@gmail.com>
The primary key is usually a bigint column, but the foreign key columns
are usually of integer type. This can lead to issues when joining these
columns due to mismatched types and different value ranges.
This was using a temporary plugin / test API to make tests pass. After
more careful consideration, we concluded that it is safe to alter the
tables directly.
Even for larger communities, with about 1M chat messages, the
slowest `ALTER` query runs in about 15 seconds, which well under the 30
seconds query timeout limit. As a result, chat messages will be delayed
for a few seconds, but the system will remain operational.
In the case where:
* Secure uploads were enabled
* Allow unsecure chat uploads was enabled
* For a site with login required enabled
When a chat upload was created, it was being marked as secure. Since
there is no provision for secure uploads in chat, this would lead to
broken uploads/images shown in the channel.
We can use the "public types" functionality of secure uploads to make
sure we never mark chat uploads as secure, and we can revisit this
whenever we get around to allowing secure uploads in chat.
Currently (for Ranked Choice only) a javascript exception is raised on very first vote, preventing the results from being rendered requiring a browser refresh (which doesn't error).
Resolves: TypeError: this.args.rankedChoiceOutcome.round_activity is undefined with simple addition of optional chaining operator.
Allows anonymous users to download the calendar file. Before, they were given the option, but it would fail silently with a `TypeError: Cannot read properties of null (reading 'user_option')`.
There's no UI for it at the moment but when creating a channel or updating it, it's now possible to pass `icon_upload_id` as param. This will be available on the channel as `icon_upload_url`.
A `UserHistory` entry will now be created when an automation is destroyed. This is visible in `/admin/logs/staff_action_logs`. id, name, trigger and script will be logged.
This commit also creates a service `DestroyAutomation` to hold all the destroy automation logic.
---------
Co-authored-by: Martin Brennan <mjrbrennan@gmail.com>
* DEV: join/leave presence chat-reply when streaming
This commit ensures that starting/stopping a chat message with the streaming option will automatically make the creator of the message as present in the chat-reply channel.
* implements start/stop reply
* not needed
When adding threads to DM channels in #29170 we intentionally didn't add them to the My Threads section. However this makes it easy to miss notifications as we don't get the new thread badge on the sidebar and footer tabs (drawer/mobile). However they were also missing from the chat header and sidebar too, which is fixed with this PR.
When a new thread or a reply to an existing thread is created within a DM channel (either 1:1 or group), we now show the standard badges like we do for public channels.
We now also show the green dot in the sidebar for My Threads and public channels when they contain an unread watched thread.
This commit removes the feature flag for the new /about page, enabling it for all sites, and removes the code for old the /about page.
Internal topic: t/140413.
We decided to make contracts immutable once their validations have run.
Indeed, it doesn’t make a lot of sense to modify a contract value
outside the contract itself.
If processing is needed, then it should happen inside the contract
itself.
This change makes use of service workers to determine if we should play chat sounds in the current browser tab. Since users can have multiple tabs open, we currently attempt to play sound across all active tabs.
With this change we iterate over all clients and check if client.focused is true (ie. the current tab/window we have open), if so we allow playing the audio in the current tab and for all other hidden tabs/windows we return false.
---------
Co-authored-by: Bianca Nenciu <nbianca@users.noreply.github.com>
Key changes include:
- `@uppy/aws-s3-multipart` is now part of `@uppy/aws-s3`, and controlled with a boolean
- Some minor changes/renames to Uppy APIs
- Uppy has removed batch signing from their S3 multipart implementation. This commit implements a batching system outside of Uppy to avoid needing one-signing-request-per-part
- Reduces concurrent part uploads to 6, because S3 uses HTTP/1.1 and browsers limit concurrent connections to 6-per-host.
- Upstream drop-target implementation has changed slightly, so we now need `pointer-events: none` on the hover element
This patch replaces the parameters provided to a service through
`params` by the contract object.
That way, it allows better consistency when accessing input params. For
example, if you have a service without a contract, to access a
parameter, you need to use `params[:my_parameter]`. But with a contract,
you do this through `contract.my_parameter`. Now, with this patch,
you’ll be able to access it through `params.my_parameter` or
`params[:my_parameter]`.
Some methods have been added to the contract object to better mimic a
Hash. That way, when accessing/using `params`, you don’t have to think
too much about it:
- `params.my_key` is also accessible through `params[:my_key]`.
- `params.my_key = value` can also be done through `params[:my_key] =
value`.
- `#slice` and `#merge` are available.
- `#to_hash` has been implemented, so the contract object will be
automatically cast as a hash by Ruby depending on the context. For
example, with an AR model, you can do this: `user.update(**params)`.
Currently in services, we don’t make a distinction between input
parameters, options and dependencies.
This can lead to user input modifying the service behavior, whereas it
was not the developer intention.
This patch addresses the issue by changing how data is provided to
services:
- `params` is now used to hold all data coming from outside (typically
user input from a controller) and a contract will take its values from
`params`.
- `options` is a new key to provide options to a service. This typically
allows changing a service behavior at runtime. It is, of course,
totally optional.
- `dependencies` is actually anything else provided to the service (like
`guardian`) and available directly from the context object.
The `service_params` helper in controllers has been updated to reflect
those changes, so most of the existing services didn’t need specific
changes.
The options block has the same DSL as contracts, as it’s also based on
`ActiveModel`. There aren’t any validations, though. Here’s an example:
```ruby
options do
attribute :allow_changing_hidden, :boolean, default: false
end
```
And here’s an example of how to call a service with the new keys:
```ruby
MyService.call(params: { key1: value1, … }, options: { my_option: true }, guardian:, …)
```