@mentions

What is @mentions

@mentions is a new feature introduced in Totara 13 which provides an easy way to mention and notify other users during content creation or editing. 

To mention another user you can type the @ character and a user's username (login) in a supported input (text field or editor). For example, '@johnsmith' would notify the user called John Smith. In the Weka editor the user just needs to type @ and start typing the user's full name, which will expand a list of suggested matching names for auto-completion. When the form including the mention is submitted, a notification will be sent to the mentioned user(s) which includes the content title (if applicable), who mentioned them, the content including the mention and a URL to see the full content.

This feature is part of the Totara core, however, it is not applicable system-wide automatically and every input which aims to support @mentions must do it explicitly in the code. This is not complex and instructions on adding mentions are provided below.

While @mentions are supported with any content type, best results can be achieved with the new Weka editor which has extensive support for mentions including suggestion drop-downs and automatic linking to user profiles.

How notifications for @mentions work

When content is submitted, code that processes this content creates and processes it through the content handler. When the handler is called, code also provides the content title, instance ID, component/area, context, and URL of the content item. All this data is used to identify the mention and prepare notification message which will be later sent to the mentioned users.

Main classes 

The core of mentions is implemented using content processors in totara_core\content namespace which allows adding any kind of other content processing in a centralised and controllable manner.

ClassPurpose
totara_core\content\content
Represents content to process.
totara_core\content\content_handler
The main class that searches for processors and handles content through them.
totara_core\content\processor
Abstract class which all content processors must implement.
totara_core\content\processor\mention_processor
Implementation of content processor that searches mentions in the content and sends notifications.
totara_core\task\user_mention_notify_task
An adhoc task that sends notifications to the users.
totara_core\output\mention_message
Class for the template used to generate message. It represents Mustache template located at server/totara/core/templates/mention_message.mustache.

How to add @mentions support

Adding mentions support is a relatively easy task and in the simplest case requires one method call and can be done in one line. However, to get best results it is advised to enable supported editor functionality for the required input as well as use the event-based approach to decouple notification functionality from the rest of the code.

Processing content

To add mentions processing the developer needs to instantiate content handler and process content through it when the content being stored. It requires content ID, so if it is adding content, INSERT call must be performed first.

For example:

use totara_core\content\content_handler;  
// ...
$handler = content_handler::create(); // Instantiate content handler
$handler->handle_with_params(
    $article->get_name(),  // Content title
    $article->get_content(), // Content itself (mentions will be searched here)
    $article->get_format(), // Content format (FORMAT_JSON FORMAT_PLAIN etc)
    $article->get_id(), // Content id
    'engage_article',  // Component name in frankenstyle
    article::CONTENT_AREA, // Component area (similar to file API area)
    $article->get_context()->id, // Context
    $article->get_url() // URL to see content
);

Enabling Weka editor support for input (optional)

In Totara 13 the only editor that fully supports @mentions is Weka. This is a new JSON-based editor which provides native mobile support.

To add the mentions extension, create/edit db/weka_editor.php and add the mention extension into the includeextensions list for required area.

db/weka_editor.php
$editor = [
    'content' => [
        'includeextensions' => [
            '\editor_weka\extension\mention',
            // Other extensions ...
        ],
    ]
];

Afterwards, clarify area for Weka editor:

component.vue
        <Weka
          :id="id"
          component="totara_playlist"
          area="content"
          :doc="content.doc"
          :placeholder="$str('description', 'engage_article')"
          @update="handleUpdate"
        />

Make sure that component is set to the component that has weka_editor.php definition and area is the same as the key inside $editor array.

Triggering and observing event (optional)

While it is not necessary to use events for this, it is considered good practice to trigger an event to observe it for processing as it facilitates decoupling and predictability of code.

To do it, make sure that event exists, it captures all required data, and is fired after content is saved.

Create event:

classes/event/article_created.php
use core\event\base;
//...
final class article_created extends base {
// Standard event code ...
}

Fire event after saving the item:

classes/webapi/resolver/mutation/create.php
// Saving item and retreiving record with id, name, content, format, url, contextid...
$event = article_created::create($record);
$event->trigger();

Observe event (do not forget to bump version):

db/events.php
$observers = [
//...
    [
        'eventname' => \engage_article\event\article_created::class,
        'callback'  => [\engage_article\observer\article_observer::class, 'on_created']
    ],
// ...
];

Handle event in the callback:

classes/observer/article_observer.php
namespace engage_article\observer;
use \engage_article\event\article_created;
use totara_core\content\content_handler;
//...

class article_observer {

    // ...

    public static function on_created(article_created $event): void {
        $article = article::from_resource_id($event->get_item_id());
        $handler = content_handler::create();
        $handler->handle_with_params(
            $article->get_name(),
            $article->get_content(),
            $article->get_format(),
            $article->get_id(),
            'engage_article',
            article::CONTENT_AREA,
            $article->get_context()->id,
            $article->get_url()
        );
    }

    // ...
}

Adjusting language strings (optional)

By default, mention message will state that a specific user has mentioned you in a comment. This is a generic message and it can be adjusted inside your component.

To change this message, define some or all of the following strings in your lang file:

lang/en/engage_article.php
// ...
// [area] must be the same as area used in content handler
// String mentionbody:[area] is used as main content part of the message
$string['mentionbody:content'] = '<strong>{$a->fullname}</strong> has mentioned you in the resource {$a->title}.';
// String mentiontitle:[area] is used as notification subject
$string['mentiontitle:content'] = '{$a} has mentioned you in a resource';
// String mentionview:[area] is used in notification footer before URL navigating to content.
$string['mentionview:content'] = 'View resource';
// ...

You can use different areas for adding support of different mentions within one component.

The mechanism of finding users to @mention

By default when using @mention, the system will try to find all the users within the system, limited to the user's tenant if multitenancy is enabled. The user will only be able to @mention the users that they can see.

For example, user one in tenant A will not be able to search for user two in tenant B. Additionally, user one will not be able to search for system-level users, but only the users that are in the same tenant (tenant A) and all the participants of that tenant.

This works for several places, however, we have enabled hooks to allow plugins to override the logic of searching users within @mention. The hook will only run when there are necessary data provided such as: 'component', 'context' and 'area' where the @mention is being used.  With the hook, plugins are free to provide the list of users that can be used for @mention.

Hook class name: "editor_weka\hook\search_users_by_pattern"

Example of using the hook within watcher:

function on_search_users(editor_weka\hook\search_users_by_pattern $hook): void {
    if ($hook->is_db_run()) {
        // Hook has been run with injected user records. We should skip it.
        return;
    }
    
    $component = $hook->get_component();
    if ('your_system_component_name' !== $component) {
        // @mention is not used in the plugin place.
        return;
    }

    // This is where to actually search for users and inject result to the hook.
    $users = \your_system_component_name\some_class::search_for_users($hook->get_pattern());
    $hook->add_users($users);
   

    // Marking DB run is quite important, because with this flag, once the hook is executed, 
    // the query will return the list of injected users straight away. Hence with out this flag,
    // the query will fallback to the default logics of searching users.
    $hook->mark_db_run();
}