discourse/plugins/chat/spec/lib/chat/notifier_spec.rb
Rafael dos Santos Silva b0873d3ecd
FEATURE: Restore web push quick reply for chat messages (#39614)
**Previously**, the "Reply" action on chat web push notifications was
silently broken — the service worker's chat URL regex stopped matching
when chat routes moved to `/chat/c/:slug/:id`, and the chat-specific
code lived in core with no thread support, URL encoding, or error
handling.

**In this update**, generalize the core service worker into a
plugin-extensible action registry, move chat reply handling into the
chat plugin with proper thread/encoding/fallback support, and convert
single-emoji replies into reactions on the source message (falling back
to a regular message post on any failure).

## Architecture

Core (`app/views/static/service-worker.js.erb`) now exposes:

```js
self.registerNotificationActionHandler(action, handler);
```

Push payloads can carry `actions` (Web Notifications API format) and
`action_data` (an arbitrary object surfaced to the handler at
`event.notification.data.actionData`). The click handler dispatches by
`event.action` name and falls back to the existing focus-or-open
behavior when no handler matches or a handler throws.

`PushNotificationPusher` forwards `actions` / `action_data` when
present.

The chat plugin owns its own service worker
(`plugins/chat/assets/javascripts/service-worker.js`), registered via
`register_service_worker`. It registers a `chat-reply` handler that:

1. Fetches a CSRF token.
2. If the reply is a single emoji grapheme (detected via
`Intl.Segmenter`, with a `\p{Extended_Pictographic}` regex fallback),
`PUT`s to `/chat/:channel_id/react/:message_id`.
3. Otherwise — or if the reaction request fails — `POST`s to
`/chat/:channel_id` with proper `URLSearchParams` encoding and the
source `thread_id` if present.
4. If everything fails, opens the channel so the user can retype.

`Chat::Notifier.push_notification_reply_action(chat_message, user)`
builds the payload in the user's locale with `channel_id`, `message_id`,
and optional `thread_id`.

`Chat::MessageReactor#react!` resolves raw Unicode via
`Emoji.unicode_replacements[emoji] || emoji` before validation, so
smartwatch quick-reply chips (which send raw Unicode like `👍🏽` or ZWJ
sequences) just work. Existing shortcode callers are unaffected.

## Why this matters

Smartwatch notification UIs expose a single "reply" affordance with
canned chips that include short text and emoji. The user can't choose
between "send as message" and "react"; they tap a chip and a string
comes through. Treating a single-grapheme pictographic reply as a
reaction matches user intent on the watch UX without any new product
surface.

## Test plan

- [ ] On a chat-enabled site, subscribe to web push notifications.
- [ ] Have another user post a message that mentions you, or a watched
channel message.
- [ ] On the resulting OS notification, confirm a "Reply" action button
appears.
- [ ] Type a text reply and submit — verify it posts as a message in the
right channel/thread.
- [ ] Submit a single emoji (e.g. 👍 or 👍🏽 with skin tone) — verify it
lands as a reaction on the source message instead of posting a message.
- [ ] Submit a ZWJ sequence (e.g. 👨‍👩‍👧) — verify it lands as a reaction
(or falls back to message post if not in the emoji DB).
- [ ] Sign out / let session expire and submit a reply — verify the
channel opens as a fallback rather than silently failing.
- [ ] Watch via Wear OS / watchOS quick-reply chips and confirm the same
behavior end-to-end.
2026-04-29 12:36:03 -03:00

834 lines
29 KiB
Ruby
Vendored
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# frozen_string_literal: true
describe Chat::Notifier do
describe "#notify_new" do
fab!(:channel, :category_channel)
fab!(:user_1) { Fabricate(:user, refresh_auto_groups: true) }
fab!(:user_2, :user)
before do
@chat_group =
Fabricate(
:group,
users: [user_1, user_2],
mentionable_level: Group::ALIAS_LEVELS[:everyone],
)
SiteSetting.chat_allowed_groups = @chat_group.id
[user_1, user_2].each do |u|
Fabricate(:user_chat_channel_membership, chat_channel: channel, user: u)
end
end
def build_cooked_msg(message_body, user, chat_channel: channel)
Chat::Message.create(
chat_channel: chat_channel,
user: user,
message: message_body,
created_at: 5.minutes.ago,
).tap(&:cook)
end
shared_examples "channel-wide mentions" do
it "returns an empty list when the message doesn't include a channel mention" do
msg = build_cooked_msg(mention.gsub("@", ""), user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[list_key]).to be_empty
end
it "will never include someone who is not accepting channel-wide notifications" do
user_2.user_option.update!(ignore_channel_wide_mention: true)
msg = build_cooked_msg(mention, user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[list_key]).to be_empty
end
it "will never mention when channel is not accepting channel wide mentions" do
channel.update!(allow_channel_wide_mentions: false)
msg = build_cooked_msg(mention, user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[list_key]).to be_empty
end
it "will publish a mention warning" do
channel.update!(allow_channel_wide_mentions: false)
msg = build_cooked_msg(mention, user_1)
messages =
MessageBus.track_publish("/chat/#{channel.id}") do
to_notify = described_class.new(msg, msg.created_at).notify_new
end
global_mentions_disabled_message = messages.first
expect(global_mentions_disabled_message.data[:type].to_sym).to eq(:notice)
expect(global_mentions_disabled_message.data[:text_content]).to eq(
I18n.t("chat.mention_warning.global_mentions_disallowed"),
)
end
it "will respect user's locale on mention warning" do
SiteSetting.allow_user_locale = true
user_1.update!(locale: "pt_BR")
channel.update!(allow_channel_wide_mentions: false)
msg = build_cooked_msg(mention, user_1)
messages =
MessageBus.track_publish("/chat/#{channel.id}") do
to_notify = described_class.new(msg, msg.created_at).notify_new
end
global_mentions_disabled_message = messages.first
expect(global_mentions_disabled_message.data[:type].to_sym).to eq(:notice)
expect(global_mentions_disabled_message.data[:text_content]).to eq(
I18n.t("chat.mention_warning.global_mentions_disallowed", locale: "pt_BR"),
)
end
it "includes all members of a channel except the sender" do
msg = build_cooked_msg(mention, user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[list_key]).to contain_exactly(user_2.id)
end
end
shared_examples "ensure only channel members are notified" do
it "will never include someone outside the channel" do
user3 = Fabricate(:user)
@chat_group.add(user3)
another_channel = Fabricate(:category_channel)
Fabricate(:user_chat_channel_membership, chat_channel: another_channel, user: user3)
msg = build_cooked_msg(mention, user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[list_key]).to contain_exactly(user_2.id)
end
it "will never include someone not following the channel anymore" do
user3 = Fabricate(:user)
@chat_group.add(user3)
Fabricate(
:user_chat_channel_membership,
following: false,
chat_channel: channel,
user: user3,
)
msg = build_cooked_msg(mention, user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[list_key]).to contain_exactly(user_2.id)
end
it "will never include someone who is suspended" do
user3 = Fabricate(:user, suspended_till: 2.years.from_now)
@chat_group.add(user3)
Fabricate(
:user_chat_channel_membership,
following: true,
chat_channel: channel,
user: user3,
)
msg = build_cooked_msg(mention, user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[list_key]).to contain_exactly(user_2.id)
end
end
describe "global_mentions" do
let(:mention) { "hello @all!" }
let(:list_key) { :global_mentions }
include_examples "channel-wide mentions"
include_examples "ensure only channel members are notified"
describe "editing a direct mention into a global mention" do
let(:mention) { "hello @#{user_2.username}!" }
it "doesn't send notifications with :all_mentioned_user_ids as an identifier" do
Jobs.run_immediately!
msg = build_cooked_msg(mention, user_1)
Chat::UpdateMessage.call(
guardian: user_1.guardian,
params: {
message_id: msg.id,
message: "hello @all",
},
)
described_class.new(msg, msg.created_at).notify_edit
notifications = Notification.where(user: user_2)
notifications.each do |notification|
expect(notification.data).not_to include("\"identifier\":\"all_mentioned_user_ids\"")
end
end
end
describe "users ignoring or muting the user creating the message" do
it "does not send notifications to the user who is muting the acting user" do
Fabricate(:muted_user, user: user_2, muted_user: user_1)
msg = build_cooked_msg(mention, user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[list_key]).to be_empty
end
it "does not send notifications to the user who is ignoring the acting user" do
Fabricate(:ignored_user, user: user_2, ignored_user: user_1, expiring_at: 1.day.from_now)
msg = build_cooked_msg(mention, user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:direct_mentions]).to be_empty
end
end
end
describe "here_mentions" do
let(:mention) { "hello @here!" }
let(:list_key) { :here_mentions }
before { user_2.update!(last_seen_at: 4.minutes.ago) }
include_examples "channel-wide mentions"
include_examples "ensure only channel members are notified"
it "includes users seen less than 5 minutes ago" do
msg = build_cooked_msg(mention, user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[list_key]).to contain_exactly(user_2.id)
end
it "excludes users seen more than 5 minutes ago" do
user_2.update!(last_seen_at: 6.minutes.ago)
msg = build_cooked_msg(mention, user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[list_key]).to be_empty
end
it "excludes users mentioned directly" do
msg = build_cooked_msg("hello @here @#{user_2.username}!", user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[list_key]).to be_empty
end
describe "users ignoring or muting the user creating the message" do
it "does not send notifications to the user who is muting the acting user" do
Fabricate(:muted_user, user: user_2, muted_user: user_1)
msg = build_cooked_msg(mention, user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[list_key]).to be_empty
end
it "does not send notifications to the user who is ignoring the acting user" do
Fabricate(:ignored_user, user: user_2, ignored_user: user_1, expiring_at: 1.day.from_now)
msg = build_cooked_msg(mention, user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:direct_mentions]).to be_empty
end
end
end
describe "direct_mentions" do
it "only include mentioned users who are already in the channel" do
user_3 = Fabricate(:user)
@chat_group.add(user_3)
another_channel = Fabricate(:category_channel)
Fabricate(:user_chat_channel_membership, chat_channel: another_channel, user: user_3)
msg = build_cooked_msg("Is @#{user_3.username} here? And @#{user_2.username}", user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:direct_mentions]).to contain_exactly(user_2.id)
end
it "include users as direct mentions even if there's a @here mention" do
msg = build_cooked_msg("Hello @here and @#{user_2.username}", user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:here_mentions]).to be_empty
expect(to_notify[:direct_mentions]).to contain_exactly(user_2.id)
end
it "doesnt attempt to notify bots not in the channel" do
bot = Fabricate(:user, username: "bot", id: -999)
msg = build_cooked_msg("Hello @bot", user_1)
_, inaccessible, _ = described_class.new(msg, msg.created_at).list_users_to_notify
expect(inaccessible[:welcome_to_join]).to be_empty
msg =
build_cooked_msg(
"Hello @bot",
user_1,
chat_channel: Fabricate(:private_category_channel, group: Fabricate(:group)),
)
_, inaccessible, _ = described_class.new(msg, msg.created_at).list_users_to_notify
expect(inaccessible[:unreachable]).to be_empty
end
it "include users as direct mentions even if there's a @all mention" do
msg = build_cooked_msg("Hello @all and @#{user_2.username}", user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:global_mentions]).to be_empty
expect(to_notify[:direct_mentions]).to contain_exactly(user_2.id)
end
describe "users ignoring or muting the user creating the message" do
it "does not publish new mentions to these users" do
Fabricate(:muted_user, user: user_2, muted_user: user_1)
msg = build_cooked_msg("hey @#{user_2.username} stop muting me!", user_1)
Chat::Publisher.expects(:publish_new_mention).never
to_notify = described_class.new(msg, msg.created_at).notify_new
end
it "does not send notifications to the user who is muting the acting user" do
Fabricate(:muted_user, user: user_2, muted_user: user_1)
msg = build_cooked_msg("hey @#{user_2.username} stop muting me!", user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:direct_mentions]).to be_empty
end
it "does not send notifications to the user who is ignoring the acting user" do
Fabricate(:ignored_user, user: user_2, ignored_user: user_1, expiring_at: 1.day.from_now)
msg = build_cooked_msg("hey @#{user_2.username} stop ignoring me!", user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:direct_mentions]).to be_empty
end
end
end
describe "group mentions" do
fab!(:user_3, :user)
fab!(:group) do
Fabricate(
:public_group,
users: [user_2, user_3],
mentionable_level: Group::ALIAS_LEVELS[:everyone],
)
end
fab!(:other_channel, :category_channel)
before { @chat_group.add(user_3) }
let(:mention) { "hello @#{group.name}!" }
let(:list_key) { group.name }
include_examples "ensure only channel members are notified"
it "calls guardian can_join_chat_channel?" do
Guardian.any_instance.expects(:can_join_chat_channel?).at_least_once
msg = build_cooked_msg("Hello @#{group.name} and @#{user_2.username}", user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
end
it "establishes a far-left precedence among group mentions" do
Fabricate(
:user_chat_channel_membership,
chat_channel: channel,
user: user_3,
following: true,
)
msg = build_cooked_msg("Hello @#{@chat_group.name} and @#{group.name}", user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[@chat_group.name]).to contain_exactly(user_2.id, user_3.id)
expect(to_notify[list_key]).to be_empty
second_msg = build_cooked_msg("Hello @#{group.name} and @#{@chat_group.name}", user_1)
to_notify_2 = described_class.new(second_msg, second_msg.created_at).notify_new
expect(to_notify_2[list_key]).to contain_exactly(user_2.id, user_3.id)
expect(to_notify_2[@chat_group.name]).to be_empty
end
it "skips groups with too many members" do
SiteSetting.max_users_notified_per_group_mention = (group.user_count - 1)
msg = build_cooked_msg("Hello @#{group.name}", user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[group.name]).to be_nil
end
it "respects the 'max_mentions_per_chat_message' setting and skips notifications" do
SiteSetting.max_mentions_per_chat_message = 1
msg = build_cooked_msg("Hello @#{user_2.username} and @#{user_3.username}", user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:direct_mentions]).to be_empty
expect(to_notify[group.name]).to be_nil
end
it "respects the max mentions setting and skips notifications when mixing users and groups" do
SiteSetting.max_mentions_per_chat_message = 1
msg = build_cooked_msg("Hello @#{user_2.username} and @#{group.name}", user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:direct_mentions]).to be_empty
expect(to_notify[group.name]).to be_nil
end
describe "users ignoring or muting the user creating the message" do
it "does not send notifications to the user inside the group who is muting the acting user" do
group.add(user_3)
Fabricate(:user_chat_channel_membership, chat_channel: channel, user: user_3)
Fabricate(:muted_user, user: user_2, muted_user: user_1)
msg = build_cooked_msg("Hello @#{group.name}", user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:direct_mentions]).to be_empty
expect(to_notify[group.name]).to contain_exactly(user_3.id)
end
it "does not send notifications to the user inside the group who is ignoring the acting user" do
group.add(user_3)
Fabricate(:user_chat_channel_membership, chat_channel: channel, user: user_3)
Fabricate(:ignored_user, user: user_2, ignored_user: user_1, expiring_at: 1.day.from_now)
msg = build_cooked_msg("Hello @#{group.name}", user_1)
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:direct_mentions]).to be_empty
expect(to_notify[group.name]).to contain_exactly(user_3.id)
end
end
end
describe "unreachable users" do
fab!(:user_3, :user)
it "notifies poster of users who are not allowed to use chat" do
msg = build_cooked_msg("Hello @#{user_3.username}", user_1)
messages =
MessageBus.track_publish("/chat/#{channel.id}") do
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:direct_mentions]).to be_empty
end
unreachable_msg = messages.first
expect(unreachable_msg[:data][:type].to_sym).to eq(:notice)
expect(unreachable_msg[:data][:text_content]).to eq(
I18n.t("chat.mention_warning.cannot_see", first_identifier: user_3.username),
)
end
it "respects user locale on notice about users who are not allowed to use chat" do
SiteSetting.allow_user_locale = true
user_1.update!(locale: "pt_BR")
msg = build_cooked_msg("Hello @#{user_3.username}", user_1)
messages =
MessageBus.track_publish("/chat/#{channel.id}") do
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:direct_mentions]).to be_empty
end
unreachable_msg = messages.first
expect(unreachable_msg[:data][:type].to_sym).to eq(:notice)
expect(unreachable_msg[:data][:text_content]).to eq(
I18n.t(
"chat.mention_warning.cannot_see",
first_identifier: user_3.username,
locale: "pt_BR",
),
)
end
context "when in a personal message" do
let(:personal_chat_channel) do
result =
Chat::CreateDirectMessageChannel.call(
guardian: user_1.guardian,
params: {
target_usernames: [user_1.username, user_2.username],
},
)
service_failed!(result) if result.failure?
result.channel
end
before { @chat_group.add(user_3) }
it "notify posts of users who are not participating in a personal message" do
msg =
build_cooked_msg(
"Hello @#{user_3.username}",
user_1,
chat_channel: personal_chat_channel,
)
messages =
MessageBus.track_publish("/chat/#{personal_chat_channel.id}") do
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:direct_mentions]).to be_empty
end
unreachable_msg = messages.first
expect(unreachable_msg[:data][:type].to_sym).to eq(:notice)
expect(unreachable_msg[:data][:text_content]).to eq(
I18n.t("chat.mention_warning.cannot_see", first_identifier: user_3.username),
)
end
it "notify posts of users who are part of the mentioned group but participating" do
group =
Fabricate(
:public_group,
users: [user_2, user_3],
mentionable_level: Group::ALIAS_LEVELS[:everyone],
)
msg =
build_cooked_msg("Hello @#{group.name}", user_1, chat_channel: personal_chat_channel)
messages =
MessageBus.track_publish("/chat/#{personal_chat_channel.id}") do
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[group.name]).to contain_exactly(user_2.id)
end
unreachable_msg = messages.first
expect(unreachable_msg[:data][:type].to_sym).to eq(:notice)
expect(unreachable_msg[:data][:text_content]).to eq(
I18n.t("chat.mention_warning.cannot_see", first_identifier: user_3.username),
)
end
end
end
describe "users who can be invited to join the channel" do
fab!(:user_3, :user)
before { @chat_group.add(user_3) }
it "can invite chat user without channel membership" do
msg = build_cooked_msg("Hello @#{user_3.username}", user_1)
messages =
MessageBus.track_publish("/chat/#{channel.id}") do
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:direct_mentions]).to be_empty
end
not_participating_msg = messages.first
expect(not_participating_msg[:data][:type].to_sym).to eq(:notice)
expect(not_participating_msg[:data][:text_content]).to be_nil
expect(not_participating_msg[:data][:notice_type].to_sym).to eq(:mention_without_membership)
expect(not_participating_msg[:data][:data]).to eq(
user_ids: [user_3.id],
text:
I18n.t("chat.mention_warning.without_membership", first_identifier: user_3.username),
message_id: msg.id,
)
end
it "cannot invite chat user without channel membership if they are ignoring the user who created the message" do
Fabricate(:ignored_user, user: user_3, ignored_user: user_1)
msg = build_cooked_msg("Hello @#{user_3.username}", user_1)
messages =
MessageBus.track_publish("/chat/#{channel.id}") do
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:direct_mentions]).to be_empty
end
expect(messages).to be_empty
end
it "cannot invite chat user without channel membership if they are muting the user who created the message" do
Fabricate(:muted_user, user: user_3, muted_user: user_1)
msg = build_cooked_msg("Hello @#{user_3.username}", user_1)
messages =
MessageBus.track_publish("/chat/#{channel.id}") do
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:direct_mentions]).to be_empty
end
expect(messages).to be_empty
end
it "can invite chat user who no longer follows the channel" do
Fabricate(
:user_chat_channel_membership,
chat_channel: channel,
user: user_3,
following: false,
)
msg = build_cooked_msg("Hello @#{user_3.username}", user_1)
messages =
MessageBus.track_publish("/chat/#{channel.id}") do
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:direct_mentions]).to be_empty
end
not_participating_msg = messages.first
expect(not_participating_msg[:data][:type].to_sym).to eq(:notice)
expect(not_participating_msg[:data][:text_content]).to be_nil
expect(not_participating_msg[:data][:notice_type].to_sym).to eq(:mention_without_membership)
expect(not_participating_msg[:data][:data]).to eq(
user_ids: [user_3.id],
text:
I18n.t("chat.mention_warning.without_membership", first_identifier: user_3.username),
message_id: msg.id,
)
end
it "can invite other group members to channel" do
group =
Fabricate(
:public_group,
users: [user_2, user_3],
mentionable_level: Group::ALIAS_LEVELS[:everyone],
)
msg = build_cooked_msg("Hello @#{group.name}", user_1)
messages =
MessageBus.track_publish("/chat/#{channel.id}") do
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:direct_mentions]).to be_empty
end
not_participating_msg = messages.first
expect(not_participating_msg[:data][:type].to_sym).to eq(:notice)
expect(not_participating_msg[:data][:text_content]).to be_nil
expect(not_participating_msg[:data][:notice_type].to_sym).to eq(:mention_without_membership)
expect(not_participating_msg[:data][:data]).to eq(
user_ids: [user_3.id],
text:
I18n.t("chat.mention_warning.without_membership", first_identifier: user_3.username),
message_id: msg.id,
)
end
it "cannot invite a member of a group who is ignoring the user who created the message" do
group =
Fabricate(
:public_group,
users: [user_2, user_3],
mentionable_level: Group::ALIAS_LEVELS[:everyone],
)
Fabricate(:ignored_user, user: user_3, ignored_user: user_1, expiring_at: 1.day.from_now)
msg = build_cooked_msg("Hello @#{group.name}", user_1)
messages =
MessageBus.track_publish("/chat/#{channel.id}") do
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:direct_mentions]).to be_empty
end
expect(messages).to be_empty
end
it "cannot invite a member of a group who is muting the user who created the message" do
group =
Fabricate(
:public_group,
users: [user_2, user_3],
mentionable_level: Group::ALIAS_LEVELS[:everyone],
)
Fabricate(:muted_user, user: user_3, muted_user: user_1)
msg = build_cooked_msg("Hello @#{group.name}", user_1)
messages =
MessageBus.track_publish("/chat/#{channel.id}") do
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[:direct_mentions]).to be_empty
end
expect(messages).to be_empty
end
end
describe "enforcing limits when mentioning groups" do
fab!(:user_3, :user)
fab!(:group) do
Fabricate(
:public_group,
users: [user_2, user_3],
mentionable_level: Group::ALIAS_LEVELS[:everyone],
)
end
it "sends a message to the client signaling the group has too many members" do
SiteSetting.max_users_notified_per_group_mention = (group.user_count - 1)
msg = build_cooked_msg("Hello @#{group.name}", user_1)
messages =
MessageBus.track_publish("/chat/#{channel.id}") do
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[group.name]).to be_nil
end
too_many_members_msg = messages.first
expect(too_many_members_msg[:data][:type].to_sym).to eq(:notice)
expect(too_many_members_msg[:data][:text_content]).to eq(
I18n.t("chat.mention_warning.too_many_members", first_identifier: group.name),
)
end
it "sends a message to the client signaling the group doesn't allow mentions" do
group.update!(mentionable_level: Group::ALIAS_LEVELS[:only_admins])
msg = build_cooked_msg("Hello @#{group.name}", user_1)
messages =
MessageBus.track_publish("/chat/#{channel.id}") do
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[group.name]).to be_nil
end
mentions_disabled_msg = messages.first
expect(mentions_disabled_msg[:data][:type].to_sym).to eq(:notice)
expect(mentions_disabled_msg[:data][:text_content]).to eq(
I18n.t("chat.mention_warning.group_mentions_disabled", first_identifier: group.name),
)
end
it "respects user locale on notice about group disallowing mentions" do
SiteSetting.allow_user_locale = true
user_1.update!(locale: "pt_BR")
group.update!(mentionable_level: Group::ALIAS_LEVELS[:only_admins])
msg = build_cooked_msg("Hello @#{group.name}", user_1)
messages =
MessageBus.track_publish("/chat/#{channel.id}") do
to_notify = described_class.new(msg, msg.created_at).notify_new
expect(to_notify[group.name]).to be_nil
end
mentions_disabled_msg = messages.first
expect(mentions_disabled_msg[:data][:type].to_sym).to eq(:notice)
expect(mentions_disabled_msg[:data][:text_content]).to eq(
I18n.t(
"chat.mention_warning.group_mentions_disabled",
first_identifier: group.name,
locale: "pt_BR",
),
)
end
end
end
describe ".push_notification_reply_action" do
fab!(:user)
fab!(:channel, :category_channel)
fab!(:thread, :chat_thread)
it "returns a chat-reply action and channel + message data for non-threaded messages" do
message = Fabricate(:chat_message, chat_channel: channel)
payload = described_class.push_notification_reply_action(message, user)
expect(payload[:actions]).to match(
[
a_hash_including(
action: "chat-reply",
type: "text",
title: I18n.t("discourse_push_notifications.actions.chat_reply.title"),
placeholder: I18n.t("discourse_push_notifications.actions.chat_reply.placeholder"),
),
],
)
expect(payload[:actions].first[:icon]).to include("inline_reply")
expect(payload[:action_data]).to eq(channel_id: channel.id, message_id: message.id)
end
it "includes thread_id when the source message is in a thread" do
message = Fabricate(:chat_message, chat_channel: thread.channel, thread_id: thread.id)
payload = described_class.push_notification_reply_action(message, user)
expect(payload[:action_data]).to eq(
channel_id: thread.channel_id,
message_id: message.id,
thread_id: thread.id,
)
end
it "translates the action title using the user's locale" do
SiteSetting.allow_user_locale = true
user.update!(locale: "fr")
TranslationOverride.upsert!(
"fr",
"discourse_push_notifications.actions.chat_reply.title",
"Répondre",
)
message = Fabricate(:chat_message, chat_channel: channel)
payload = described_class.push_notification_reply_action(message, user)
expect(payload[:actions].first[:title]).to eq("Répondre")
end
end
end