mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-21 01:53:41 +08:00
**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.
834 lines
29 KiB
Ruby
Vendored
834 lines
29 KiB
Ruby
Vendored
# 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 "doesn’t 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
|