Commit Graph

146 Commits

Author SHA1 Message Date
8b212d95ed DEV: skip watch word when message is created by bots (#31959)
Especially with AI we have no exact control on the words and this could
create and endless loop of errors.
2025-03-31 20:09:04 +02:00
d3c2bd015d FEATURE: Implement chat transcripts in rich editor (#31819)
This commit allows the ProseMirror rich editor to display chat
transcripts copied from chat using the "Copy" button.

The BBCode usually looks something like this:

```
[chat quote="hunter;29856;2025-03-20T07:13:04Z" channel="design gems 🎉" channelId="95"]
haha **ok** _cool_
[/chat]
```

But there are several variations that must be accounted for:

* Single message from single user
* Multiple messages from a single and multiple users
* Messages inside chat threads

The rich transcript extension has to ignore many of the chat transcript
markdown
tokens because they simply aren't necessary -- none of the ProseMirror
nodes need
to be editable. So, we basically recreate the same HTML that the chat
transcript markdown
rule does in the `toDOM()` function. Maybe in future we want to make the
markdown rule
do less and have this HTML creation in one place, but for now we need to
mirror in both files.

---------

Co-authored-by: Renato Atilio <renato@discourse.org>
2025-03-31 14:04:22 +10:00
a43ad984b1 FEATURE: allow adding members to new 1-1 DM channels (#31948)
This change allows more flexibility when starting a 1-1 direct message
with another user. If there are no messages in the new DM channel then
we should still allow them to add additional users.
2025-03-21 12:20:21 +04:00
d38acc5df1 DEV: discourse-emojis gem (#31408)
This commit moves most of emoji logic into the discourse-emojis gem:
https://github.com/discourse/discourse-emojis/

Most notably:
- images are now symlinked from the gem
- the gem provides path to the json files

Search aliases have also been made asynchronous and memoized. When you
will search for an emoji we will now load the aliases and store the list
for future use.

---------

Co-authored-by: David Taylor <david@taylorhq.com>
2025-03-03 13:09:08 +01:00
e92e05b22e FIX: Error when leaving group DM channel (#31537)
Followup b5147a4634f0fd5c98262f949a8c766bfd73d290

When we aliased `leave` to `remove` and renamed
the method in `DirectMessageChannel` in the previous
commit, this inadvertantly caused an error when
unfollowing group channels in the channel list.

When clicking the X in the channel list, we hit
ChannelsCurrentUserMembershipFollowsController for the
current user and the channel, which is supposed to only
unfollow the channel for all channel types including DMs.

Group DMs have a different Leave behaviour vs Unfollow.
Leaving the channel altogether is done from the channel
settings page, the "Leave channel" button, and that
deletes the user's membership and DM user record from that
channel.

So, we were trying to do the leave channel behaviour in the
unfollow channel controller, which was returning the wrong
record for the serializer (a User not a Membership)

This fixes the issue and removes a bit of delegate/alias indirection
which was making the code a bit harder to fllow and search, even
though it was more succinct. Also adds missing specs that would
have caught this regression.
2025-02-27 14:26:07 +10:00
1d7663d63c FEATURE: Allow chat incoming webhooks to work without .json extension (#31497)
This provides a slightly nicer-looking URL, and also helps when external
systems have strict validations on the webhook URL.
2025-02-25 15:04:57 +00:00
e26a1175d7 FEATURE: Initial version of experimental admin search (#31299)
This feature allows admins to find what they are
looking for in the admin interface via a search modal.
This replaces the admin sidebar filter
as the focus of the Ctrl+/ command, but the sidebar
filter can also still be used. Perhaps at some point
we may remove it or change the shortcut.

The search modal presents the following data for filtering:

* A list of all admin pages, the same as the sidebar,
   except also showing "third level" pages like
   "Email > Skipped"
* All site settings
* Themes
* Components
* Reports

Admins can also filter which types of items are shown in the modal,
for example hiding Settings if they know they are looking for a Page.

In this PR, I also have the following fixes:

* Site setting filters now clear when moving between
   filtered site setting pages, previously it was super
   sticky from Ember
* Many translations were moved around, instead of being
   in various namespaces for the sidebar links and the admin
   page titles and descriptions, now everything is under
   `admin.config` namespace, this makes it way easier to reuse
   this text for pages, search, and sidebar, and if you change it
   in one place then it is changed everywhere.

---------

Co-authored-by: Ella <ella.estigoy@gmail.com>
2025-02-21 11:59:24 +10:00
b5147a4634 FEATURE: Allow admins to remove users from chat DMs (#31366)
This feature allows admins to remove users from channels.
2025-02-20 15:16:57 +08:00
bb12f8275d DEV: Only include custom admin UIs in the plugins index tabs (#31192)
In the current admin index page, all plugins show up as tabs. This includes plugins with auto-generated config routes.

This changes the tabs to include only plugins with custom UIs.
2025-02-05 15:02:46 +08:00
3d47a1268c SECURITY: Ability to bypass disabling chat of users 2025-02-04 13:32:42 -03:00
503f9b6f02 DEV: Use default admin routes for plugins with settings (#30941)
This change adds a sidebar link for each plugin that fulfils the following criteria:

- Does not have an explicit admin route defined in the plugin.
- Has at least one site setting (not including enabled/disabled.)

That sidebar link leads to the automatically generated plugin show settings page.
2025-02-04 14:57:28 +08:00
1f483f48a0 FIX: remove complicated 'chat_duplicate_message_sensitivity' site setting (#30516)
And change the "formula" to check for duplicate messages to

- no duplicate check in 1:1 DMs
- only duplicate check in group DMs / channels, for posts made by the
same user, in the past 10 seconds

Internal ref - t/144262
2025-01-13 12:32:51 +01:00
6740a340ca DEV: unifies emoji picker (#28277)
The chat emoji picker is renamed emoji-picker, and the old emoji-picker is removed.

This commit doesn't attempt to fully rework a new emoji-picker but instead tries to migrate everything to one picker (the chat one) and add small changes.

Other notable changes:
- all the favorite emojis code has been mixed into one service which is able to store one state per context, favorites emojis will be stored for all topics, and for each chat channel. Meaning that if you always use a specific emoji in a channel, it will only show as favorite emoji in this channel.
- a lot of static code has been removed which should improve initial load perf of discourse. Initially this code was around to improve the performance of the emoji picker rendering.
- the emojis are now stored, once the full list has been loaded, if you close and reopen the picker it won't have to load them again.

List of components:

- `<EmojiPicker />` will render a button which will open a dropdown
- `<EmojiPickerContent />` represents the content of the dropdown alone, it's useful when you want to render a picker from an action which is not the default picker button
- `<EmojiPickerDetached />` just a simple wrapper over `<EmojiPickerContent />` to make it easier to use it with `this.menu.show(...)`

---------

Co-authored-by: Renato Atilio <renatoat@gmail.com>
2025-01-08 11:41:36 +01:00
a4c2653db8 DEV: Update rubocop-discourse (#30552)
…and autofix the issues
2025-01-04 13:48:21 +01:00
582de0ffe3 DEV: adds blocks support to chat messages (#29782)
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)`
2024-11-19 07:07:58 +01:00
234133bd3b UX: Split hide_profile_and_presence user option (#29632)
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>
2024-11-12 22:22:58 -03:00
6dfe2fbe16 PERF: auto join & leave chat channels (#29193)
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
2024-11-12 15:00:59 +11:00
6158a1ae29 DEV: Refactor the Chat::CreateThread service a bit
Follow best practices from our docs.
2024-11-06 15:53:43 +01:00
f79dd5c8b5 DEV: Stop injecting a service result object in the caller object
Currently, when calling a service with its block form, a `#result`
method is automatically created on the caller object. Even if it never
clashed so far, this could happen.

This patch removes that method, and instead use a more classical way of
doing things: the result object is now provided as an argument to the
main block. This means if we need to access the result object in an
outcome block, it will be done like this from now on:
```ruby
MyService.call(params) do |result|
  on_success do
    # do something with the result object
    do_something(result)
  end
end
```

In the same vein, this patch introduces the ability to match keys from
the result object in the outcome blocks, like we already do with step
definitions in a service. For example:
```ruby
on_success do |model:, contract:|
  do_something(model, contract)
end
```
Instead of
```ruby
on_success do
  do_something(result.model, result.contract)
end
```
2024-10-22 16:58:54 +02:00
08e9364573 DEV: Refactor some services from chat
Extracted from https://github.com/discourse/discourse/pull/29129.

This patch makes the code more compliant with the upcoming service docs best practices.
2024-10-21 16:16:25 +02:00
93e02069b0 DEV: Don’t provide an array to site settings group lists in specs
This is extracted from https://github.com/discourse/discourse/pull/29129.

In some chat specs, we provide an array as a value for group lists like
`chat_allowed_groups`, which is wrong. This results in a value like
`"1|2|[3]"` instead of `"1|2|3"`.
2024-10-17 11:25:31 +02:00
492cf52bab FIX: trashed channel thread view bug (#29192)
When chat channels are deleted, some users may be able to click the thread before it gets removed from the UI. This leads to a 500 error causing log noise. We can use the safe navigational operator to prevent calling chatable when the channel is not found (due to deleted_at constraint in query).
2024-10-14 17:45:50 +04:00
2193667e1f FIX: Plugin JS failing to load would break admin interface (#29139)
If a plugin's JS fails to load for some reason, most commonly
ad blockers, the entire admin interface would break. This is because
we are adding links to the admin routes for plugins that define
them in the sidebar.

We have a fix for this already in the plugin list which shows a warning
to the admin. This fix just prevents the broken link from rendering
in the sidebar if the route is not valid.
2024-10-11 09:26:10 +10:00
ed6c9d1545 DEV: Call Discourse.redis.flushdb after the end of each test (#29117)
There have been too many flaky tests as a result of leaking state in
Redis so it is easier to resolve them by ensuring we flush Redis'
database.

Locally on my machine, calling `Discourse.redis.flushdb` takes around
0.1ms which means this change will have very little impact on test
runtimes.
2024-10-09 07:19:31 +08:00
a7a9148b1e DEV: consolidate chat channel notification settings (#29080)
On the chat channel settings page, we want to show a single Send push notifications setting instead of the current Desktop notifications and Mobile push notifications settings.

For existing users, use the Mobile push notifications setting value for the new Send push notifications setting.
2024-10-08 13:13:01 +04:00
61c1d35f17 FEATURE: Convert chat plugin UI to new show plugin and admin UI guidelines (#28632)
This commit converts the current chat plugin UI into the
new "show plugin" UI already followed by AI and Gamification.

In the process, I also:

* Made a dedicated /new route to create new webhooks
* Converted the webhook form to FormKit
* Made some fixes and improvements to the `AdminPluginConfigPage`, `AdminPageHeader`,
   and `AdminPageSubheader` generic components, so more plugins can
   adopt the UI guidelines too. This includes adding a header outlet so plugins
   can add action buttons to the plugin show page header.
* Fixes the submit button loading state for FormKit (by Joffrey)

---------

Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
2024-09-10 15:16:16 +10:00
ea58140032 DEV: Remove summarization code (#27373) 2024-07-02 08:51:47 -07:00
a07ddf4ec0 UX: Show chat and message buttons on your own profile (#27600) 2024-06-25 07:52:17 -03:00
5d33ea1f6e FIX: correctly load channels in chat webhooks
In 4e7a75a7ece3205ce9f3f188b5e016bf75a869c0, we moved to a single admin plugin page and added a few fields to the "plugin serializer" but we already had a proper route with the correct serializers to properly load channels.

This fixes it by removing the "add_to_serializer" calls and changed the calls to "/admin/plugins/chat.json" to the proper "/admin/plugins/chat/hooks.json" route.

Meta - https://meta.discourse.org/t/names-are-missing-from-list-when-creating-new-chat-channel-webhooks/308481
2024-06-10 17:30:38 +02:00
1a42249bd8 UX: Show message and chat buttons on hidden profiles (#27326) 2024-06-10 10:38:22 -03:00
5aefda1dee FIX: allows listing messages of any thread (#27259)
Before this fix we could only list messages of a thread if it was part of a `threading_enabled` channel or if the thread was set to `force`.

Due to our design of also using a thread id when this is just a chain of replies so we can switch from threading enabled to disabled at any time, we will allow `Chat:: ListChannelThreadMessages` to list the messages of any thread, the only important requirements are:
- having a thread id
- being able to access this thread

To allow this, this commit simply removes the check on `threading_enabled` or `force`.
2024-05-30 10:20:40 +02:00
c39a4de139 FIX: load existing chat dm channel via url (#26998)
When users click a link that points to an existing group chat, we should reopen that chat instead of creating a new group chat so users can more easily continue ongoing conversations.
2024-05-24 12:12:49 +04:00
02469d5795 FIX: chat replies are not always in a thread (#27023)
When you reply to a chat message, we [always create a thread][1]. But when the channel we're in doesn't have threading enabled, the reply is _technically_ not a thread.

This changes the `in_thread?` method to check for both the presence of a `thread_id` and to ensure that the channel has `threading_enabled`.

Internal ref - t/128103/3

[1]: e6e3eaf472/plugins/chat/app/services/chat/create_message.rb (L110-L115)
2024-05-16 16:10:23 +02:00
26c8eab1f3 FIX: allows bots to create/update/stream messages (#26900)
Prior to this commit, only system users had this pass.

Another significant change of the PR, is to make membership of a channel the angular stone of the permission check to create/update/stop streaming a message. The idea being, if you are a member of a channel already we don't need to check if you can join it AGAIN.

We also have `Chat::AutoRemove::HandleCategoryUpdated` which will deal with permissions change so it's simpler and less prone to error to consider the membership as the only source of truth.
2024-05-07 15:17:42 +02:00
79870d3a1e DEV: Fix random typos (#26881) 2024-05-06 20:52:48 +02:00
f72f63660a FIX: an existing member of a channel is allowed to join (#26884)
There's no point checking if a user can join a channel if they are already part of it. This case was frequent when using `enforce_membership: true` for custom bots for example.
2024-05-06 17:14:20 +02:00
0c8f531909 FEATURE: encourage users to set chat thread titles (#26617)
This change encourages users to title their threads to make it easier for other users to join in on conversations that matter to them.

The creator of the chat thread will receive a toast notification prompting them to add a thread title when on mobile and the thread has at least 5 sent replies.
2024-04-29 17:20:01 +08:00
52e8d57293 FEATURE: implements last read message for threads (#26702)
This commit will now allow us to track read position in a thread and returns to this position when you open the thread.

Note this commit is also extracting the following components to make it possible:
- `<ChatMessagesScroller />`
- `<ChatMessagesContainer />`

The `UpdateUserThreadLastRead` has been updated to allow this.

Various refactorings have also been done to the code and specs to improve the support of last read.
2024-04-25 10:47:54 +02:00
4c860995e0 DEV: Remove unnecessary rails_helper requiring (#26364) 2024-03-26 11:32:01 +01:00
bbb8595107 PERF: defer loading channels (#26155)
Prior to this change we would pre-load all the user channels which making initial page load slower. This change will make them be loaded right after initial load. In the past this was not possible as the channels would have to be loaded on each page transition. However since about a year, we made the channels to be cached on the frontend and no other request will be needed.

I have decided for now to not show a loading state in the sidebar as I think it would be noise, but we can reconsider this later.

Note given we don't have the channels loaded at first certain things where harder to accomplish. The biggest UX change of this commit is that we removed all the complex logic of computing the best channel to display when you load /chat. We will now store the id of the last channel you visited and will use this id to decide which channel to show.
2024-03-18 08:35:07 +01:00
8cf2f909f5 DEV: Dedicated route for current user notification counts (#26106)
Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
2024-03-15 12:08:37 -04:00
9afb0b29f8 FEATURE: filter additional keywords for the sidebar (#26148)
With the new admin sidebar restructure, we have a link to "Installed plugins". We would like to ensure that when the admin is searching for a plugin name like "akismet" or "automation" this link will be visible. Also when entering the plugins page, related plugins should be highlighted.
2024-03-14 12:28:08 +11:00
4e7a75a7ec DEV: Single admin plugin page for consistent admin plugin UX (#26024)
This commit adds new plugin show routes (`/admin/plugins/:plugin_id`) as we move
towards every plugin having a consistent UI/landing page.

As part of this, we are introducing a consistent way for plugins
to show an inner sidebar in their config page, via a new plugin
API `register_admin_config_nav_routes`

This accepts an array of links with a label/text, and an
ember route. Once this commit is merged we can start the process
of conforming other plugins to follow this pattern, as well
as supporting a single-page version of this for simpler plugins
that don't require an inner sidebar.

Part of /t/122841 internally
2024-03-13 13:15:12 +10:00
821402d024 DEV: removes default service actions (#26078)
Previously services would let you define a high level default `def default_actions_for_service; end` which would define various handlers like `on_success`, after months of usage we consider the cons are superior to the pros here.

Two mains cons:
- people would often not understand where the handling was coming from
- it's easy to miss a case when you write your specs
2024-03-07 12:10:43 +01:00
76953cc356 FEATURE: allows to force a thread (#25987)
Forcing a thread will work even in channel which don't have `threading_enabled` or in direct message channels.

For now this feature is only available through the `ChatSDK`:

```ruby
ChatSDK::Message.create(in_reply_to_id: 1, guardian: guardian, raw: "foo bar baz", channel_id: 2, force_thread: true)
```
2024-03-06 12:03:42 +01:00
b3a1199493 FEATURE: Hide user status when user is hiding public profile and presence (#24300)
Users can hide their public profile and presence information by checking 
“Hide my public profile and presence features” on the 
`u/{username}/preferences/interface` page. In that case, we also don't 
want to return user status from the server.

This work has been started in https://github.com/discourse/discourse/pull/23946. 
The current PR fixes all the remaining places in Core.

Note that the actual fix is quite simple – a5802f484d. 
But we had a fair amount of duplication in the code responsible for 
the user status serialization, so I had to dry that up first. The refactoring 
as well as adding some additional tests is the main part of this PR.
2024-02-26 17:40:48 +04:00
d8d756cd2f DEV: chat streaming (#25736)
This commit introduces the possibility to stream messages. To allow plugins to use streaming this commit also ships a `ChatSDK` library to allow to interact with few parts of discourse chat.

```ruby
ChatSDK::Message.create_with_stream(raw: "test") do |helper|
  5.times do |i|
    is_streaming = helper.stream(raw: "more #{i}")
    next if !is_streaming
    sleep 2
  end
end
```

This commit also introduces all the frontend parts:
- messages can now be marked as streaming
- when streaming their content will be updated when a new content is appended
- a special UI will be showing (a blinking indicator)
- a cancel button allows the user to stop the streaming, when cancelled `helper.stream(...)` will return `false`, and the plugin can decide exit early
2024-02-20 09:49:19 +01:00
3cc73cfd1e FIX: Always preload admin plugin list for admin in sidebar (#25606)
When we show the links to installed plugins in the admin
sidebar (for plugins that have custom admin routes) we were
previously only doing this if you opened /admin, not if you
navigated there from the main forum. We should just always
preload this data if the user is admin.

This commit also changes `admin_sidebar_enabled_groups` to
not be sent to the client as part of ongoing efforts to
not check groups on the client, since not all a user's groups
may be serialized.
2024-02-09 12:52:22 +10:00
57ea56ee05 DEV: Remove full group refreshes from tests (#25414)
We have all these calls to Group.refresh_automatic_groups! littered throughout the tests. Including tests that are seemingly unrelated to groups. This is because automatic group memberships aren't fabricated when making a vanilla user. There are two places where you'd want to use this:

You have fabricated a user that needs a certain trust level (which is now based on group membership.)
You need the system user to have a certain trust level.
In the first case, we can pass refresh_auto_groups: true to the fabricator instead. This is a more lightweight operation that only considers a single user, instead of all users in all groups.

The second case is no longer a thing after #25400.
2024-01-25 14:28:26 +08:00
04d2ec45b4 DEV: remove user thread count route (#25385)
Removes a now redundant route for the user thread count.
2024-01-24 10:32:34 +08:00