Previously I wrote about parsing #tags and @mentions.

Now, let’s create mentions (find users and mention them).

Recently I added mentions to SupeRails. Now users can tag each other by Github username. A tagged user will see that he was mentioned.

TributeJS is a good plugin for adding mentions.

We will use requestjs-rails to make internal GET requests with JS (to get a list of usernames).

Initial setup:

rails new mentionsapp --main -d=postgresql -c=tailwind -a=propshaft
rails g scaffold User username
rails g scaffold message body:text
rails g scaffold mention user:references message:references
rails g stimulus mentions
bin/importmap pin tributejs
bundle add requestjs-rails
bin/rails requestjs:install
bundle add faker

Add some users to the database:

# db/seeds.rb
# User.create username: Faker::Internet.username
usernames = %w[yshmarov marcoroth adrianthedev lucianghinda robzolkos dhh matz]
usernames.each do |username|
  User.create username: username
end

Form field with mentions enabled:

# app/views/messages/form.html.erb
<%= form.text_area :body, required: true, style: 'width: 100%', rows: 3, data: { controller: 'mentions', mentions_target: 'input' } %>

Find user by username and return json:

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @users = if params[:query].present?
               User.where('username ILIKE ?', "%#{params[:query]}%")
             else
               User.none
             end

    respond_to do |format|
      format.json { render json: @users }
    end
  end
end

Stimulus controller to enable TributeJS and search for users:

// app/javascript/controllers/mentions_controller.js
import { Controller } from "@hotwired/stimulus";
import Tribute from "tributejs";
import { get } from "@rails/request.js";

export default class extends Controller {
  static targets = ["input"]

  connect() {
    this.tribute = new Tribute({
      values: async (text, cb) => {
          const response = await get(`/users.json?query=${text}`);
          if (response.ok) {
            const users = await response.json;
            cb(users.map(user => ({ key: user.username, value: user.username })));
          }
      },
      selectTemplate: function (item) {
        return `@${item.original.value}`;
      },
    });
    this.tribute.attach(this.inputTarget);
  }

  disconnect() {
    this.tribute.detach(this.inputTarget);
  }
}

Import TributeJS CSS

After a message is created, parse mentioned usernames and create mentions

# app/models/message.rb
  has_many :mentions

  after_create_commit do
    extract_mentions
  end

  private

  def extract_mentions
    mentioned_usernames = content.scan(/@(\w+)/).flatten
    mentioned_users = User.where(username: mentioned_usernames)
    mentioned_users.each do |mentioned_user|
      mentions.create(user: mentioned_user)
    end
  end
end

The second regex is actually better:

(/@(\w+)/) => @foobar.com

(/@([\w._]+)/) => @foobar.com

Display mentions in a text:

module MessagesHelper
  def postprocess(text)
    text.gsub(/@([\w._]+)/) do |mention|
      username = mention[1..-1]
      link_to mention, "/users/#{username}", class: "text-blue-500"
    end
  end
end
<%= simple_format postprocess(message.body) %>

Finally, test mention creation:

# test/models/mention_test.rb
require 'test_helper'

class MentionTest < ActiveSupport::TestCase
  test 'create mention' do
    user = User.create username: "foo"
    message = Message.create!(content: "Hello @#{user.username}! How are you?")
    assert_equal 1, comment.mentions.count
    assert_equal 1, user.notifications.count
  end
end

That’s it!