diff --git a/AGENTS.md b/AGENTS.md index 31d795b9..317bf5cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,3 +75,8 @@ Blue-green deployment via GitHub Actions. Docker multi-arch images (amd64 + arm6 **Production env vars:** `SECRET_KEY_BASE`, `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY`, `SSL_DOMAIN` (or `DISABLE_SSL`), `SENTRY_DSN` **Data persistence:** Mount volume to `/rails/storage` (SQLite DB + file attachments) + +## Pull Request Guidelines + +- Do not include a "Test plan" section in PR descriptions +- Keep PR descriptions concise with a summary of changes diff --git a/Gemfile b/Gemfile index 655f1f1e..4feb190f 100644 --- a/Gemfile +++ b/Gemfile @@ -45,6 +45,7 @@ gem "kredis" gem "platform_agent" gem "thruster" gem "redcarpet" +gem "gemoji" group :development, :test do gem "debug" @@ -55,6 +56,7 @@ end group :test do gem "capybara" + gem "minitest", "< 6.0" # Rails doesn't yet support minitest 6.0's API changes gem "mocha" gem "selenium-webdriver" gem "webmock", require: false diff --git a/Gemfile.lock b/Gemfile.lock index 9e3a98e4..880bb62a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -172,6 +172,7 @@ GEM geared_pagination (1.2.0) activesupport (>= 5.0) addressable (>= 2.5.0) + gemoji (4.1.0) globalid (1.3.0) activesupport (>= 6.1) hashdiff (1.2.0) @@ -212,8 +213,7 @@ GEM mini_magick (5.3.1) logger mini_mime (1.1.5) - minitest (6.0.1) - prism (~> 1.5) + minitest (5.27.0) mocha (2.7.1) ruby2_keywords (>= 0.0.5) mono_logger (1.1.2) @@ -418,10 +418,12 @@ DEPENDENCIES debug faker geared_pagination + gemoji image_processing (>= 1.2) importmap-rails! jbuilder kredis + minitest (< 6.0) mocha net-http-persistent ostruct diff --git a/app/models/message.rb b/app/models/message.rb index e607f6ab..6eff926e 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -1,5 +1,5 @@ class Message < ApplicationRecord - include Attachment, Broadcasts, Mentionee, Pagination, Searchable + include Attachment, Broadcasts, Emojification, Mentionee, Pagination, Searchable belongs_to :room, touch: true belongs_to :creator, class_name: "User", default: -> { Current.user } diff --git a/app/models/message/emojification.rb b/app/models/message/emojification.rb new file mode 100644 index 00000000..10cbeb56 --- /dev/null +++ b/app/models/message/emojification.rb @@ -0,0 +1,23 @@ +module Message::Emojification + extend ActiveSupport::Concern + + EMOJI_SHORTCODE_PATTERN = /:([a-z0-9_+-]+):/i + + included do + before_save :emojify_body, if: -> { body.changed? } + end + + private + def emojify_body + return unless body.body.present? + + html = body.body.to_html + emojified_html = html.gsub(EMOJI_SHORTCODE_PATTERN) do |match| + shortcode = $1.downcase + emoji = Emoji.find_by_alias(shortcode) + emoji ? emoji.raw : match + end + + self.body = emojified_html if html != emojified_html + end +end diff --git a/test/models/message/emojification_test.rb b/test/models/message/emojification_test.rb new file mode 100644 index 00000000..9fb54bd2 --- /dev/null +++ b/test/models/message/emojification_test.rb @@ -0,0 +1,84 @@ +require "test_helper" + +class Message::EmojificationTest < ActiveSupport::TestCase + test "converts shortcode to emoji on create" do + message = rooms(:designers).messages.create!( + body: "I :heart: this!", + client_message_id: "emoji-create", + creator: users(:david) + ) + + assert_includes message.body.body.to_html, "\u2764" + assert_not_includes message.body.body.to_html, ":heart:" + end + + test "converts shortcode to emoji on update" do + message = rooms(:designers).messages.create!( + body: "Hello world", + client_message_id: "emoji-update", + creator: users(:david) + ) + + message.update!(body: "Hello :wave:") + + assert_includes message.body.body.to_html, "\u{1F44B}" + assert_not_includes message.body.body.to_html, ":wave:" + end + + test "preserves unknown shortcodes" do + message = rooms(:designers).messages.create!( + body: "This is :notarealcode: right?", + client_message_id: "emoji-unknown", + creator: users(:david) + ) + + assert_includes message.body.body.to_html, ":notarealcode:" + end + + test "converts multiple occurrences of same shortcode" do + message = rooms(:designers).messages.create!( + body: ":heart: and :heart: and :heart:", + client_message_id: "emoji-multiple", + creator: users(:david) + ) + + assert_equal 3, message.body.body.to_html.scan("\u2764").count + assert_not_includes message.body.body.to_html, ":heart:" + end + + test "case insensitive matching" do + message = rooms(:designers).messages.create!( + body: ":HEART: and :Heart: and :heart:", + client_message_id: "emoji-case", + creator: users(:david) + ) + + assert_equal 3, message.body.body.to_html.scan("\u2764").count + assert_not_includes message.body.body.to_html, ":HEART:" + assert_not_includes message.body.body.to_html, ":Heart:" + assert_not_includes message.body.body.to_html, ":heart:" + end + + test "converts shortcodes within HTML content" do + message = rooms(:designers).messages.create!( + body: "
Check this :thumbsup:
", + client_message_id: "emoji-html", + creator: users(:david) + ) + + assert_includes message.body.body.to_html, "\u{1F44D}" + assert_not_includes message.body.body.to_html, ":thumbsup:" + end + + test "converts multiple different shortcodes" do + message = rooms(:designers).messages.create!( + body: ":heart: :thumbsup: :100:", + client_message_id: "emoji-different", + creator: users(:david) + ) + + assert_includes message.body.body.to_html, "\u2764" + assert_includes message.body.body.to_html, "\u{1F44D}" + assert_includes message.body.body.to_html, "\u{1F4AF}" + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 9c1310c5..f7688eae 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -2,7 +2,6 @@ require_relative "../config/environment" require "rails/test_help" -require "minitest/unit" require "mocha/minitest" require "webmock/minitest" require "turbo/broadcastable/test_helper"