DEV: Use Notice API for mention warnings (#23238)

This PR swaps out the custom pathway to publishing and rendering mention warnings after a message is sent.

ChatPublisher#publish_notice is used, and expanded. Now, instead of only accepting text_content as an argument, component and component_args are accepted and there is a renderer for these components.

Translations moved to server, as notices expect text to be passed in unless a component is rendered

The warnings are rendered at the top now, outside of the scope of the single message that sent it.

I entirely removed the jit_messages_spec b/c it's duplicate testing of other parts of the app. IMO we don't need a backend test for a feature, a component test for the feature AND a system test (that is slow and potentially even flakey due to timing issues with wait) to test the same thing. So jit_messages_spec is gone.
This commit is contained in:
Mark VanLandingham 2023-09-01 09:07:23 -05:00 committed by GitHub
parent ed35ae4dcd
commit 9c65e2140a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 337 additions and 662 deletions

View File

@ -387,31 +387,6 @@ module Chat
end end
end end
def self.publish_inaccessible_mentions(
user_id,
chat_message,
cannot_chat_users,
without_membership,
too_many_members,
mentions_disabled,
global_mentions_disabled
)
MessageBus.publish(
"/chat/#{chat_message.chat_channel_id}",
{
type: :mention_warning,
chat_message_id: chat_message.id,
cannot_see: cannot_chat_users.map { |u| { username: u.username, id: u.id } }.as_json,
without_membership:
without_membership.map { |u| { username: u.username, id: u.id } }.as_json,
groups_with_too_many_members: too_many_members.map(&:name).as_json,
group_mentions_disabled: mentions_disabled.map(&:name).as_json,
global_mentions_disabled: global_mentions_disabled,
},
user_ids: [user_id],
)
end
def self.publish_kick_users(channel_id, user_ids) def self.publish_kick_users(channel_id, user_ids)
MessageBus.publish( MessageBus.publish(
kick_users_message_bus_channel(channel_id), kick_users_message_bus_channel(channel_id),
@ -478,8 +453,19 @@ module Chat
) )
end end
def self.publish_notice(user_id:, channel_id:, text_content:) def self.publish_notice(user_id:, channel_id:, text_content: nil, type: nil, data: nil)
payload = { type: "notice", text_content: text_content, channel_id: channel_id } # Notices are either plain text sent to the client, or a "type" with data. The
# client will then translate that type and data into a front-end component.
if text_content.blank? && type.blank? && data.blank?
raise "Cannot publish notice without text content or a type"
end
payload = { type: "notice", channel_id: channel_id }
if text_content
payload[:text_content] = text_content
else
payload[:notice_type] = type
payload[:data] = data
end
MessageBus.publish("/chat/#{channel_id}", payload, user_ids: [user_id]) MessageBus.publish("/chat/#{channel_id}", payload, user_ids: [user_id])
end end

View File

@ -118,7 +118,6 @@
@message={{@message}} @message={{@message}}
@onRetry={{@resendStagedMessage}} @onRetry={{@resendStagedMessage}}
/> />
<Chat::Message::MentionWarning @message={{@message}} />
</div> </div>
{{#if this.showThreadIndicator}} {{#if this.showThreadIndicator}}

View File

@ -0,0 +1,20 @@
<div class="chat-notices__notice">
{{#if @notice.textContent}}
<p class="chat-notices__notice__content">
{{@notice.textContent}}
</p>
{{else}}
<this.component
@channel={{@channel}}
@notice={{@notice}}
@clearNotice={{this.clearNotice}}
/>
{{/if}}
<DButton
@icon="times"
@action={{this.clearNotice}}
class="btn-flat chat-notices__notice__clear"
/>
</div>

View File

@ -0,0 +1,21 @@
import Component from "@glimmer/component";
import MentionWithoutMembership from "discourse/plugins/chat/discourse/components/chat/notices/mention_without_membership";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
const COMPONENT_DICT = {
mention_without_membership: MentionWithoutMembership,
};
export default class ChatNotices extends Component {
@service("chat-channel-pane-subscriptions-manager") subscriptionsManager;
@action
clearNotice() {
this.subscriptionsManager.clearNotice(this.args.notice);
}
get component() {
return COMPONENT_DICT[this.args.notice.type];
}
}

View File

@ -2,16 +2,6 @@
<ChatRetentionReminder @channel={{@channel}} /> <ChatRetentionReminder @channel={{@channel}} />
{{#each this.noticesForChannel as |notice|}} {{#each this.noticesForChannel as |notice|}}
<div class="chat-notices__notice"> <ChatNotice @notice={{notice}} @channel={{@channel}} />
<p class="chat-notices__notice__content">
{{notice.textContent}}
</p>
<DButton
@icon="times"
@action={{fn this.clearNotice notice}}
class="btn-flat chat-notices__notice__clear"
/>
</div>
{{/each}} {{/each}}
</div> </div>

View File

@ -1,6 +1,5 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import { action } from "@ember/object";
export default class ChatNotices extends Component { export default class ChatNotices extends Component {
@service("chat-channel-pane-subscriptions-manager") subscriptionsManager; @service("chat-channel-pane-subscriptions-manager") subscriptionsManager;
@ -10,9 +9,4 @@ export default class ChatNotices extends Component {
(notice) => notice.channelId === this.args.channel.id (notice) => notice.channelId === this.args.channel.id
); );
} }
@action
clearNotice(notice) {
this.subscriptionsManager.clearNotice(notice);
}
} }

View File

@ -1,62 +0,0 @@
{{#if this.shouldRender}}
<div class="chat-message-mention-warning alert alert-info">
{{#if this.mentionWarning.invitationSent}}
<span
class="chat-message-mention-warning__invitation-sent"
{{chat/later-fn this.onDismissInvitationSent 3000}}
>
{{d-icon "check"}}
<span>
{{i18n
"chat.mention_warning.invitations_sent"
count=this.mentionWarning.withoutMembership.length
}}
</span>
</span>
{{else}}
<DButton
class="chat-message-mention-warning__dismiss-btn btn-flat"
title={{i18n "chat.mention_warning.dismiss"}}
@action={{this.onDismissMentionWarning}}
@icon="times"
/>
{{#if this.mentionWarning.cannotSee}}
<p class="chat-message-mention-warning__text -cannot-see">
{{this.mentionedCannotSeeText}}
</p>
{{/if}}
{{#if this.mentionWarning.withoutMembership}}
<p class="chat-message-mention-warning__text -without-membership">
<span>{{this.mentionedWithoutMembershipText}}</span>
<a href {{on "click" this.onSendInvite bubbles=false}}>
{{i18n "chat.mention_warning.invite"}}
</a>
</p>
{{/if}}
{{#if this.mentionWarning.groupWithMentionsDisabled}}
<p
class="chat-message-mention-warning__text -groups-with-mentions-disabled"
>
{{this.groupsWithDisabledMentions}}
</p>
{{/if}}
{{#if this.mentionWarning.groupsWithTooManyMembers}}
<p
class="chat-message-mention-warning__text -groups-with-too-many-members"
>
{{this.groupsWithTooManyMembers}}
</p>
{{/if}}
{{#if this.mentionWarning.globalMentionsDisabled}}
<p class="chat-message-mention-warning__text -global-mentions-disabled">
{{i18n "chat.mention_warning.channel_wide_mentions_disallowed"}}
</p>
{{/if}}
{{/if}}
</div>
{{/if}}

View File

@ -1,99 +0,0 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
import I18n from "I18n";
export default class ChatMessageMentionWarning extends Component {
@service("chat-api") api;
@action
async onSendInvite() {
const userIds = this.mentionWarning.withoutMembership.mapBy("id");
try {
await this.api.invite(this.args.message.channel.id, userIds, {
messageId: this.args.message.id,
});
this.mentionWarning.invitationSent = true;
} catch (error) {
popupAjaxError(error);
}
}
@action
onDismissInvitationSent() {
this.mentionWarning.invitationSent = false;
}
@action
onDismissMentionWarning() {
this.args.message.mentionWarning = null;
}
get shouldRender() {
return (
this.mentionWarning &&
(this.mentionWarning.groupWithMentionsDisabled?.length ||
this.mentionWarning.cannotSee?.length ||
this.mentionWarning.withoutMembership?.length ||
this.mentionWarning.groupsWithTooManyMembers?.length ||
this.mentionWarning.globalMentionsDisabled)
);
}
get mentionWarning() {
return this.args.message.mentionWarning;
}
get mentionedCannotSeeText() {
return this.#findTranslatedWarning(
"chat.mention_warning.cannot_see",
"chat.mention_warning.cannot_see_multiple",
{
username: this.mentionWarning?.cannotSee?.[0]?.username,
count: this.mentionWarning?.cannotSee?.length,
}
);
}
get mentionedWithoutMembershipText() {
return this.#findTranslatedWarning(
"chat.mention_warning.without_membership",
"chat.mention_warning.without_membership_multiple",
{
username: this.mentionWarning?.withoutMembership?.[0]?.username,
count: this.mentionWarning?.withoutMembership?.length,
}
);
}
get groupsWithDisabledMentions() {
return this.#findTranslatedWarning(
"chat.mention_warning.group_mentions_disabled",
"chat.mention_warning.group_mentions_disabled_multiple",
{
group_name: this.mentionWarning?.groupWithMentionsDisabled?.[0],
count: this.mentionWarning?.groupWithMentionsDisabled?.length,
}
);
}
get groupsWithTooManyMembers() {
return this.#findTranslatedWarning(
"chat.mention_warning.too_many_members",
"chat.mention_warning.too_many_members_multiple",
{
group_name: this.mentionWarning.groupsWithTooManyMembers?.[0],
count: this.mentionWarning.groupsWithTooManyMembers?.length,
}
);
}
#findTranslatedWarning(oneKey, multipleKey, args) {
const translationKey = args.count === 1 ? oneKey : multipleKey;
args.count--;
return I18n.t(translationKey, args);
}
}

View File

@ -0,0 +1,29 @@
<div class="mention-without-membership-notice">
{{#if this.invitationsSent}}
<span
class="mention-without-membership-notice__invitation-sent"
{{chat/later-fn @clearNotice 3000}}
>
{{d-icon "check"}}
<span>
{{i18n
"chat.mention_warning.invitations_sent"
count=this.userIds.length
}}
</span>
</span>
{{else}}
<p class="mention-without-membership-notice__body -without-membership">
<span
class="mention-without-membership-notice__body__text"
>{{@notice.data.text}}</span>
<a
class="mention-without-membership-notice__body__link"
href
{{on "click" this.sendInvitations}}
>
{{i18n "chat.mention_warning.invite"}}
</a>
</p>
{{/if}}
</div>

View File

@ -0,0 +1,31 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
import { tracked } from "@glimmer/tracking";
export default class MentionWithoutMembership extends Component {
@service("chat-api") chatApi;
@tracked invitationsSent = false;
get userIds() {
return this.args.notice.data.user_ids;
}
@action
async sendInvitations(event) {
// preventDefault to avoid a refresh
event.preventDefault();
try {
await this.chatApi.invite(this.args.channel.id, this.userIds, {
messageId: this.args.notice.data.messageId,
});
this.invitationsSent = true;
} catch (error) {
popupAjaxError(error);
}
}
}

View File

@ -1,35 +0,0 @@
<StyleguideExample @title="<Chat::Message::MentionWarning>">
<Styleguide::Component>
<Chat::Message::MentionWarning @message={{this.message}} />
</Styleguide::Component>
<Styleguide::Controls::Row @name="Cannot see">
<DToggleSwitch
@state={{gt this.message.mentionWarning.cannotSee.length 0}}
{{on "click" this.toggleCannotSee}}
/>
</Styleguide::Controls::Row>
<Styleguide::Controls::Row @name="Group with mentions disabled">
<DToggleSwitch
@state={{gt
this.message.mentionWarning.groupWithMentionsDisabled.length
0
}}
{{on "click" this.toggleGroupWithMentionsDisabled}}
/>
</Styleguide::Controls::Row>
<Styleguide::Controls::Row @name="Group with too many members">
<DToggleSwitch
@state={{gt
this.message.mentionWarning.groupsWithTooManyMembers.length
0
}}
{{on "click" this.toggleGroupsWithTooManyMembers}}
/>
</Styleguide::Controls::Row>
<Styleguide::Controls::Row @name="Without membership">
<DToggleSwitch
@state={{gt this.message.mentionWarning.withoutMembership.length 0}}
{{on "click" this.toggleWithoutMembership}}
/>
</Styleguide::Controls::Row>
</StyleguideExample>

View File

@ -1,75 +0,0 @@
import Component from "@glimmer/component";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
export default class ChatMessageMentionWarning extends Component {
@service currentUser;
constructor() {
super(...arguments);
this.message = fabricators.message({ user: this.currentUser });
}
@action
toggleCannotSee() {
if (this.message.mentionWarning?.cannotSee) {
this.message.mentionWarning = null;
} else {
this.message.mentionWarning = fabricators.messageMentionWarning(
this.message,
{
cannot_see: [fabricators.user({ username: "bob" })].map((u) => {
return { username: u.username, id: u.id };
}),
}
);
}
}
@action
toggleGroupWithMentionsDisabled() {
if (this.message.mentionWarning?.groupWithMentionsDisabled) {
this.message.mentionWarning = null;
} else {
this.message.mentionWarning = fabricators.messageMentionWarning(
this.message,
{
group_mentions_disabled: [fabricators.group()].mapBy("name"),
}
);
}
}
@action
toggleGroupsWithTooManyMembers() {
if (this.message.mentionWarning?.groupsWithTooManyMembers) {
this.message.mentionWarning = null;
} else {
this.message.mentionWarning = fabricators.messageMentionWarning(
this.message,
{
groups_with_too_many_members: [
fabricators.group(),
fabricators.group({ name: "Moderators" }),
].mapBy("name"),
}
);
}
}
@action
toggleWithoutMembership() {
if (this.message.mentionWarning?.withoutMembership) {
this.message.mentionWarning = null;
} else {
this.message.mentionWarning = fabricators.messageMentionWarning(
this.message,
{
without_membership: [fabricators.user()].map((u) => {
return { username: u.username, id: u.id };
}),
}
);
}
}
}

View File

@ -13,7 +13,6 @@ import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread"; import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread";
import ChatThreadPreview from "discourse/plugins/chat/discourse/models/chat-thread-preview"; import ChatThreadPreview from "discourse/plugins/chat/discourse/models/chat-thread-preview";
import ChatDirectMessage from "discourse/plugins/chat/discourse/models/chat-direct-message"; import ChatDirectMessage from "discourse/plugins/chat/discourse/models/chat-direct-message";
import ChatMessageMentionWarning from "discourse/plugins/chat/discourse/models/chat-message-mention-warning";
import ChatMessageReaction from "discourse/plugins/chat/discourse/models/chat-message-reaction"; import ChatMessageReaction from "discourse/plugins/chat/discourse/models/chat-message-reaction";
import User from "discourse/models/user"; import User from "discourse/models/user";
import Bookmark from "discourse/models/bookmark"; import Bookmark from "discourse/models/bookmark";
@ -157,10 +156,6 @@ function groupFabricator(args = {}) {
}); });
} }
function messageMentionWarningFabricator(message, args = {}) {
return ChatMessageMentionWarning.create(message, args);
}
function uploadFabricator() { function uploadFabricator() {
return { return {
extension: "jpeg", extension: "jpeg",
@ -191,6 +186,5 @@ export default {
upload: uploadFabricator, upload: uploadFabricator,
category: categoryFabricator, category: categoryFabricator,
directMessage: directMessageFabricator, directMessage: directMessageFabricator,
messageMentionWarning: messageMentionWarningFabricator,
group: groupFabricator, group: groupFabricator,
}; };

View File

@ -1,23 +0,0 @@
import { tracked } from "@glimmer/tracking";
export default class ChatMessageMentionWarning {
static create(message, args = {}) {
return new ChatMessageMentionWarning(message, args);
}
@tracked invitationSent = false;
@tracked cannotSee;
@tracked withoutMembership;
@tracked groupsWithTooManyMembers;
@tracked groupWithMentionsDisabled;
@tracked globalMentionsDisabled;
constructor(message, args = {}) {
this.message = args.message;
this.cannotSee = args.cannot_see;
this.withoutMembership = args.without_membership;
this.groupsWithTooManyMembers = args.groups_with_too_many_members;
this.groupWithMentionsDisabled = args.group_mentions_disabled;
this.globalMentionsDisabled = args.global_mentions_disabled;
}
}

View File

@ -11,5 +11,7 @@ export default class ChatNotice {
constructor(args = {}) { constructor(args = {}) {
this.channelId = args.channel_id; this.channelId = args.channel_id;
this.textContent = args.text_content; this.textContent = args.text_content;
this.type = args.notice_type;
this.data = args.data;
} }
} }

View File

@ -25,14 +25,11 @@ export default class ChatChannelPaneSubscriptionsManager extends ChatPaneBaseSub
} }
handleNotice(data) { handleNotice(data) {
this.notices.push(ChatNotice.create(data)); this.notices.pushObject(ChatNotice.create(data));
} }
clearNotice(notice) { clearNotice(notice) {
const index = this.notices.indexOf(notice); this.notices.removeObject(notice);
if (index > -1) {
this.notices.splice(index, 1);
}
} }
handleThreadOriginalMessageUpdate(data) { handleThreadOriginalMessageUpdate(data) {

View File

@ -1,6 +1,5 @@
import Service, { inject as service } from "@ember/service"; import Service, { inject as service } from "@ember/service";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message"; import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import ChatMessageMentionWarning from "discourse/plugins/chat/discourse/models/chat-message-mention-warning";
import { cloneJSON } from "discourse-common/lib/object"; import { cloneJSON } from "discourse-common/lib/object";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
@ -105,9 +104,6 @@ export default class ChatPaneBaseSubscriptionsManager extends Service {
case "restore": case "restore":
this.handleRestoreMessage(busData); this.handleRestoreMessage(busData);
break; break;
case "mention_warning":
this.handleMentionWarning(busData);
break;
case "self_flagged": case "self_flagged":
this.handleSelfFlaggedMessage(busData); this.handleSelfFlaggedMessage(busData);
break; break;
@ -203,13 +199,6 @@ export default class ChatPaneBaseSubscriptionsManager extends Service {
} }
} }
handleMentionWarning(data) {
const message = this.messagesManager.findMessage(data.chat_message_id);
if (message) {
message.mentionWarning = ChatMessageMentionWarning.create(message, data);
}
}
handleSelfFlaggedMessage(data) { handleSelfFlaggedMessage(data) {
const message = this.messagesManager.findMessage(data.chat_message_id); const message = this.messagesManager.findMessage(data.chat_message_id);
if (message) { if (message) {

View File

@ -1,19 +0,0 @@
.chat-message-mention-warning {
position: relative;
margin-top: 0.25rem;
font-size: var(--font-down-1);
&__dismiss-btn {
position: absolute;
top: 7px;
right: 5px;
}
&__text {
margin: 0.25rem 0;
}
&__invite-sent {
color: var(--tertiary);
}
}

View File

@ -52,7 +52,6 @@
@import "chat-thread-list-header"; @import "chat-thread-list-header";
@import "chat-thread-unread-indicator"; @import "chat-thread-unread-indicator";
@import "chat-thread-participants"; @import "chat-thread-participants";
@import "chat-message-mention-warning";
@import "chat-message-error"; @import "chat-message-error";
@import "chat-message-creator"; @import "chat-message-creator";
@import "chat-user-avatar"; @import "chat-user-avatar";

View File

@ -129,28 +129,11 @@ en:
one: "Last hour" one: "Last hour"
other: "Last %{count} hours" other: "Last %{count} hours"
mention_warning: mention_warning:
dismiss: "dismiss"
cannot_see: "%{username} can't access this channel and was not notified."
cannot_see_multiple:
one: "%{username} and %{count} other user cannot access this channel and were not notified."
other: "%{username} and %{count} other users cannot access this channel and were not notified."
invitations_sent: invitations_sent:
one: "Invitation sent" one: "Invitation sent"
other: "Invitations sent" other: "Invitations sent"
invite: "Invite to channel" invite: "Invite to channel"
without_membership: "%{username} has not joined this channel."
without_membership_multiple:
one: "%{username} and %{count} other user have not joined this channel."
other: "%{username} and %{count} other users have not joined this channel."
group_mentions_disabled: "%{group_name} doesn't allow mentions."
group_mentions_disabled_multiple:
one: "%{group_name} and %{count} other group don't allow mentions."
other: "%{group_name} and %{count} other groups don't allow mentions."
channel_wide_mentions_disallowed: "@here and @all mentions are disabled in this channel." channel_wide_mentions_disallowed: "@here and @all mentions are disabled in this channel."
too_many_members: "%{group_name} has too many members. No one was notified."
too_many_members_multiple:
one: "%{group_name} and %{count} other group have too many members. No one was notified."
other: "%{group_name} and %{count} other groups have too many members. No one was notified."
groups: groups:
header: header:
some: "Some users won't be notified" some: "Some users won't be notified"

View File

@ -138,6 +138,29 @@ en:
multi_user_truncated: multi_user_truncated:
one: "%{comma_separated_usernames} and %{count} other" one: "%{comma_separated_usernames} and %{count} other"
other: "%{comma_separated_usernames} and %{count} others" other: "%{comma_separated_usernames} and %{count} others"
mention_warning:
dismiss: "dismiss"
cannot_see: "%{first_identifier} can't access this channel and was not notified."
cannot_see_multiple:
one: "%{first_identifier} and %{count} other user cannot access this channel and were not notified."
other: "%{first_identifier} and %{count} other users cannot access this channel and were not notified."
invitations_sent:
one: "Invitation sent"
other: "Invitations sent"
invite: "Invite to channel"
without_membership: "%{first_identifier} has not joined this channel."
without_membership_multiple:
one: "%{first_identifier} and %{count} other user have not joined this channel."
other: "%{first_identifier} and %{count} other users have not joined this channel."
group_mentions_disabled: "%{first_identifier} doesn't allow mentions."
group_mentions_disabled_multiple:
one: "%{first_identifier} and %{count} other group don't allow mentions."
other: "%{first_identifier} and %{count} other groups don't allow mentions."
global_mentions_disallowed: "@here and @all mentions are disabled in this channel."
too_many_members: "%{first_identifier} has too many members. No one was notified."
too_many_members_multiple:
one: "%{first_identifier} and %{count} other group have too many members. No one was notified."
other: "%{first_identifier} and %{count} other groups have too many members. No one was notified."
category_channel: category_channel:
errors: errors:

View File

@ -225,24 +225,100 @@ module Chat
end end
def notify_creator_of_inaccessible_mentions(inaccessible) def notify_creator_of_inaccessible_mentions(inaccessible)
group_mentions_disabled = @parsed_mentions.groups_with_disabled_mentions.to_a # Notify when mentioned users can join channel, but don't have a membership
too_many_members = @parsed_mentions.groups_with_too_many_members.to_a if inaccessible[:welcome_to_join].any?
if inaccessible.values.all?(&:blank?) && group_mentions_disabled.empty? && publish_inaccessible_mentions(inaccessible[:welcome_to_join])
too_many_members.empty? && !global_mentions_disabled
return
end end
Chat::Publisher.publish_inaccessible_mentions( # Notify when mentioned users are not able to access the channel
@user.id, publish_unreachable_mentions(inaccessible[:unreachable]) if inaccessible[:unreachable].any?
@chat_message,
inaccessible[:unreachable].to_a, # Notify when `@all` or `@here` is used when channel has global mentions disabled
inaccessible[:welcome_to_join].to_a, publish_global_mentions_disabled if global_mentions_disabled
too_many_members,
group_mentions_disabled, # Notify when groups are mentioned and have mentions disabled
global_mentions_disabled, group_mentions_disabled = @parsed_mentions.groups_with_disabled_mentions.to_a
publish_group_mentions_disabled(group_mentions_disabled) if group_mentions_disabled.any?
# Notify when large groups are mentioned, exceeding `max_users_notified_per_group_mention`
too_many_members = @parsed_mentions.groups_with_too_many_members.to_a
publish_too_many_members_in_group_mention(too_many_members) if too_many_members.any?
end
def publish_inaccessible_mentions(users)
Chat::Publisher.publish_notice(
user_id: @user.id,
channel_id: @chat_channel.id,
type: "mention_without_membership",
data: {
user_ids: users.map(&:id),
text:
mention_warning_text(
single: "chat.mention_warning.without_membership",
multiple: "chat.mention_warning.without_membership_multiple",
first_identifier: users.first.username,
count: users.count,
),
message_id: @chat_message.id,
},
) )
end end
def publish_group_mentions_disabled(groups)
Chat::Publisher.publish_notice(
user_id: @user.id,
channel_id: @chat_channel.id,
text_content:
mention_warning_text(
single: "chat.mention_warning.group_mentions_disabled",
multiple: "chat.mention_warning.group_mentions_disabled_multiple",
first_identifier: groups.first.name,
count: groups.count,
),
)
end
def publish_global_mentions_disabled
Chat::Publisher.publish_notice(
user_id: @user.id,
channel_id: @chat_channel.id,
text_content: I18n.t("chat.mention_warning.global_mentions_disallowed"),
)
end
def publish_unreachable_mentions(users)
Chat::Publisher.publish_notice(
user_id: @user.id,
channel_id: @chat_channel.id,
text_content:
mention_warning_text(
single: "chat.mention_warning.cannot_see",
multiple: "chat.mention_warning.cannot_see_multiple",
first_identifier: users.first.username,
count: users.count,
),
)
end
def publish_too_many_members_in_group_mention(groups)
Chat::Publisher.publish_notice(
user_id: @user.id,
channel_id: @chat_channel.id,
text_content:
mention_warning_text(
single: "chat.mention_warning.too_many_members",
multiple: "chat.mention_warning.too_many_members_multiple",
first_identifier: groups.first.name,
count: groups.count,
),
)
end
def mention_warning_text(single:, multiple:, first_identifier:, count:)
translation_key = count == 1 ? single : multiple
I18n.t(translation_key, first_identifier: first_identifier, count: count - 1)
end
def global_mentions_disabled def global_mentions_disabled
return @global_mentions_disabled if defined?(@global_mentions_disabled) return @global_mentions_disabled if defined?(@global_mentions_disabled)

View File

@ -488,7 +488,7 @@ describe Chat::MessageCreator do
end end
it "publishes inaccessible mentions when user isn't aren't a part of the channel" do it "publishes inaccessible mentions when user isn't aren't a part of the channel" do
Chat::Publisher.expects(:publish_inaccessible_mentions).once Chat::Publisher.expects(:publish_notice).once
described_class.create( described_class.create(
chat_channel: public_chat_channel, chat_channel: public_chat_channel,
user: admin1, user: admin1,
@ -498,7 +498,7 @@ describe Chat::MessageCreator do
it "publishes inaccessible mentions when user doesn't have chat access" do it "publishes inaccessible mentions when user doesn't have chat access" do
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:staff] SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:staff]
Chat::Publisher.expects(:publish_inaccessible_mentions).once Chat::Publisher.expects(:publish_notice).once
described_class.create( described_class.create(
chat_channel: public_chat_channel, chat_channel: public_chat_channel,
user: admin1, user: admin1,
@ -507,7 +507,7 @@ describe Chat::MessageCreator do
end end
it "doesn't publish inaccessible mentions when user is following channel" do it "doesn't publish inaccessible mentions when user is following channel" do
Chat::Publisher.expects(:publish_inaccessible_mentions).never Chat::Publisher.expects(:publish_notice).never
described_class.create( described_class.create(
chat_channel: public_chat_channel, chat_channel: public_chat_channel,
user: admin1, user: admin1,

View File

@ -69,9 +69,10 @@ describe Chat::Notifier do
global_mentions_disabled_message = messages.first global_mentions_disabled_message = messages.first
expect(global_mentions_disabled_message).to be_present expect(global_mentions_disabled_message.data[:type].to_sym).to eq(:notice)
expect(global_mentions_disabled_message.data[:type].to_sym).to eq(:mention_warning) expect(global_mentions_disabled_message.data[:text_content]).to eq(
expect(global_mentions_disabled_message.data[:global_mentions_disabled]).to eq(true) I18n.t("chat.mention_warning.global_mentions_disallowed"),
)
end end
it "includes all members of a channel except the sender" do it "includes all members of a channel except the sender" do
@ -415,10 +416,10 @@ describe Chat::Notifier do
unreachable_msg = messages.first unreachable_msg = messages.first
expect(unreachable_msg).to be_present expect(unreachable_msg[:data][:type].to_sym).to eq(:notice)
expect(unreachable_msg.data[:without_membership]).to be_empty expect(unreachable_msg[:data][:text_content]).to eq(
unreachable_users = unreachable_msg.data[:cannot_see].map { |u| u["id"] } I18n.t("chat.mention_warning.cannot_see", first_identifier: user_3.username),
expect(unreachable_users).to contain_exactly(user_3.id) )
end end
context "when in a personal message" do context "when in a personal message" do
@ -452,10 +453,10 @@ describe Chat::Notifier do
unreachable_msg = messages.first unreachable_msg = messages.first
expect(unreachable_msg).to be_present expect(unreachable_msg[:data][:type].to_sym).to eq(:notice)
expect(unreachable_msg.data[:without_membership]).to be_empty expect(unreachable_msg[:data][:text_content]).to eq(
unreachable_users = unreachable_msg.data[:cannot_see].map { |u| u["id"] } I18n.t("chat.mention_warning.cannot_see", first_identifier: user_3.username),
expect(unreachable_users).to contain_exactly(user_3.id) )
end end
it "notify posts of users who are part of the mentioned group but participating" do it "notify posts of users who are part of the mentioned group but participating" do
@ -477,10 +478,10 @@ describe Chat::Notifier do
unreachable_msg = messages.first unreachable_msg = messages.first
expect(unreachable_msg).to be_present expect(unreachable_msg[:data][:type].to_sym).to eq(:notice)
expect(unreachable_msg.data[:without_membership]).to be_empty expect(unreachable_msg[:data][:text_content]).to eq(
unreachable_users = unreachable_msg.data[:cannot_see].map { |u| u["id"] } I18n.t("chat.mention_warning.cannot_see", first_identifier: user_3.username),
expect(unreachable_users).to contain_exactly(user_3.id) )
end end
end end
end end
@ -502,11 +503,15 @@ describe Chat::Notifier do
not_participating_msg = messages.first not_participating_msg = messages.first
expect(not_participating_msg).to be_present expect(not_participating_msg[:data][:type].to_sym).to eq(:notice)
expect(not_participating_msg.data[:cannot_see]).to be_empty expect(not_participating_msg[:data][:text_content]).to be_nil
not_participating_users = expect(not_participating_msg[:data][:notice_type].to_sym).to eq(:mention_without_membership)
not_participating_msg.data[:without_membership].map { |u| u["id"] } expect(not_participating_msg[:data][:data]).to eq(
expect(not_participating_users).to contain_exactly(user_3.id) user_ids: [user_3.id],
text:
I18n.t("chat.mention_warning.without_membership", first_identifier: user_3.username),
message_id: msg.id,
)
end end
it "cannot invite chat user without channel membership if they are ignoring the user who created the message" do it "cannot invite chat user without channel membership if they are ignoring the user who created the message" do
@ -555,11 +560,15 @@ describe Chat::Notifier do
not_participating_msg = messages.first not_participating_msg = messages.first
expect(not_participating_msg).to be_present expect(not_participating_msg[:data][:type].to_sym).to eq(:notice)
expect(not_participating_msg.data[:cannot_see]).to be_empty expect(not_participating_msg[:data][:text_content]).to be_nil
not_participating_users = expect(not_participating_msg[:data][:notice_type].to_sym).to eq(:mention_without_membership)
not_participating_msg.data[:without_membership].map { |u| u["id"] } expect(not_participating_msg[:data][:data]).to eq(
expect(not_participating_users).to contain_exactly(user_3.id) user_ids: [user_3.id],
text:
I18n.t("chat.mention_warning.without_membership", first_identifier: user_3.username),
message_id: msg.id,
)
end end
it "can invite other group members to channel" do it "can invite other group members to channel" do
@ -580,11 +589,15 @@ describe Chat::Notifier do
not_participating_msg = messages.first not_participating_msg = messages.first
expect(not_participating_msg).to be_present expect(not_participating_msg[:data][:type].to_sym).to eq(:notice)
expect(not_participating_msg.data[:cannot_see]).to be_empty expect(not_participating_msg[:data][:text_content]).to be_nil
not_participating_users = expect(not_participating_msg[:data][:notice_type].to_sym).to eq(:mention_without_membership)
not_participating_msg.data[:without_membership].map { |u| u["id"] } expect(not_participating_msg[:data][:data]).to eq(
expect(not_participating_users).to contain_exactly(user_3.id) user_ids: [user_3.id],
text:
I18n.t("chat.mention_warning.without_membership", first_identifier: user_3.username),
message_id: msg.id,
)
end end
it "cannot invite a member of a group who is ignoring the user who created the message" do it "cannot invite a member of a group who is ignoring the user who created the message" do
@ -650,9 +663,11 @@ describe Chat::Notifier do
end end
too_many_members_msg = messages.first too_many_members_msg = messages.first
expect(too_many_members_msg).to be_present
too_many_members = too_many_members_msg.data[:groups_with_too_many_members] expect(too_many_members_msg[:data][:type].to_sym).to eq(:notice)
expect(too_many_members).to contain_exactly(group.name) expect(too_many_members_msg[:data][:text_content]).to eq(
I18n.t("chat.mention_warning.too_many_members", first_identifier: group.name),
)
end end
it "sends a message to the client signaling the group doesn't allow mentions" do it "sends a message to the client signaling the group doesn't allow mentions" do
@ -667,9 +682,11 @@ describe Chat::Notifier do
end end
mentions_disabled_msg = messages.first mentions_disabled_msg = messages.first
expect(mentions_disabled_msg).to be_present
mentions_disabled = mentions_disabled_msg.data[:group_mentions_disabled] expect(mentions_disabled_msg[:data][:type].to_sym).to eq(:notice)
expect(mentions_disabled).to contain_exactly(group.name) expect(mentions_disabled_msg[:data][:text_content]).to eq(
I18n.t("chat.mention_warning.group_mentions_disabled", first_identifier: group.name),
)
end end
end end
end end

View File

@ -1,94 +0,0 @@
# frozen_string_literal: true
RSpec.describe "JIT messages", type: :system do
fab!(:channel_1) { Fabricate(:chat_channel) }
fab!(:current_user) { Fabricate(:user) }
fab!(:other_user) { Fabricate(:user) }
let(:chat) { PageObjects::Pages::Chat.new }
let(:channel) { PageObjects::Pages::ChatChannel.new }
before do
channel_1.add(current_user)
chat_system_bootstrap
sign_in(current_user)
end
context "when mentioning a user" do
context "when user is not on the channel" do
it "displays a mention warning" do
Jobs.run_immediately!
chat.visit_channel(channel_1)
channel.send_message("hi @#{other_user.username}")
expect(page).to have_content(
I18n.t("js.chat.mention_warning.without_membership", username: other_user.username),
wait: 5,
)
end
end
context "when user cant access the channel" do
fab!(:group_1) { Fabricate(:group) }
fab!(:private_channel_1) { Fabricate(:private_category_channel, group: group_1) }
before do
group_1.add(current_user)
private_channel_1.add(current_user)
end
it "displays a mention warning" do
Jobs.run_immediately!
chat.visit_channel(private_channel_1)
channel.send_message("hi @#{other_user.username}")
expect(page).to have_content(
I18n.t("js.chat.mention_warning.cannot_see", username: other_user.username),
wait: 5,
)
end
end
end
context "when category channel permission is readonly for everyone" do
fab!(:group_1) { Fabricate(:group) }
fab!(:private_channel_1) { Fabricate(:private_category_channel, group: group_1) }
before do
group_1.add(current_user)
private_channel_1.add(current_user)
end
it "displays a mention warning" do
Jobs.run_immediately!
chat.visit_channel(private_channel_1)
channel.send_message("hi @#{other_user.username}")
expect(page).to have_content(
I18n.t("js.chat.mention_warning.cannot_see", username: other_user.username),
wait: 5,
)
end
end
context "when mention a group" do
context "when group can't be mentioned" do
fab!(:group_1) { Fabricate(:group, mentionable_level: Group::ALIAS_LEVELS[:nobody]) }
it "displays a mention warning" do
Jobs.run_immediately!
chat.visit_channel(channel_1)
channel.send_message("hi @#{group_1.name}")
expect(page).to have_content(
I18n.t("js.chat.mention_warning.group_mentions_disabled", group_name: group_1.name),
wait: 5,
)
end
end
end
end

View File

@ -1,118 +0,0 @@
import { render } from "@ember/test-helpers";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import hbs from "htmlbars-inline-precompile";
import { module, test } from "qunit";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
module(
"Discourse Chat | Component | Chat::Message::MentionWarning",
function (hooks) {
setupRenderingTest(hooks);
const template = hbs`
<Chat::Message::MentionWarning @message={{this.message}} />
`;
test("without memberships", async function (assert) {
this.message = fabricators.message();
this.message.mentionWarning = fabricators.messageMentionWarning(
this.message,
{
without_membership: [fabricators.user()].map((u) => {
return { username: u.username, id: u.id };
}),
}
);
await render(template);
assert
.dom(".chat-message-mention-warning__text.-without-membership")
.exists();
});
test("cannot see channel", async function (assert) {
this.message = fabricators.message();
this.message.mentionWarning = fabricators.messageMentionWarning(
this.message,
{
cannot_see: [fabricators.user()].map((u) => {
return { username: u.username, id: u.id };
}),
}
);
await render(template);
assert.dom(".chat-message-mention-warning__text.-cannot-see").exists();
});
test("cannot see channel", async function (assert) {
this.message = fabricators.message();
this.message.mentionWarning = fabricators.messageMentionWarning(
this.message,
{
cannot_see: [fabricators.user()].map((u) => {
return { username: u.username, id: u.id };
}),
}
);
await render(template);
assert.dom(".chat-message-mention-warning__text.-cannot-see").exists();
});
test("too many groups", async function (assert) {
this.message = fabricators.message();
this.message.mentionWarning = fabricators.messageMentionWarning(
this.message,
{
groups_with_too_many_members: [fabricators.group()].mapBy("name"),
}
);
await render(template);
assert
.dom(
".chat-message-mention-warning__text.-groups-with-too-many-members"
)
.exists();
});
test("groups with mentions disabled", async function (assert) {
this.message = fabricators.message();
this.message.mentionWarning = fabricators.messageMentionWarning(
this.message,
{
group_mentions_disabled: [fabricators.group()].mapBy("name"),
}
);
await render(template);
assert
.dom(
".chat-message-mention-warning__text.-groups-with-mentions-disabled"
)
.exists();
});
test("displays a warning when global mentions are disabled", async function (assert) {
this.message = fabricators.message();
this.message.mentionWarning = fabricators.messageMentionWarning(
this.message,
{
global_mentions_disabled: true,
}
);
await render(template);
assert
.dom(".chat-message-mention-warning__text.-global-mentions-disabled")
.exists();
});
}
);

View File

@ -1,3 +1,5 @@
import I18n from "I18n";
import pretender from "discourse/tests/helpers/create-pretender";
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import hbs from "htmlbars-inline-precompile"; import hbs from "htmlbars-inline-precompile";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators"; import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
@ -62,4 +64,52 @@ module("Discourse Chat | Component | chat-notice", function (hooks) {
"Notice was cleared" "Notice was cleared"
); );
}); });
test("MentionWithoutMembership notice renders", async function (assert) {
this.channel = fabricators.channel();
this.manager = this.container.lookup(
"service:chatChannelPaneSubscriptionsManager"
);
const text = "Joffrey can't chat, hermano";
this.manager.handleNotice({
channel_id: this.channel.id,
notice_type: "mention_without_membership",
data: { user_ids: [1], message_id: 1, text },
});
await render(hbs`<ChatNotices @channel={{this.channel}} />`);
assert.strictEqual(
queryAll(
".chat-notices .chat-notices__notice .mention-without-membership-notice"
).length,
1,
"Notice is present"
);
assert.dom(".mention-without-membership-notice__body__text").hasText(text);
assert
.dom(".mention-without-membership-notice__body__link")
.hasText(I18n.t("chat.mention_warning.invite"));
pretender.put(`/chat/${this.channel.id}/invite`, () => {
return [200, { "Content-Type": "application/json" }, {}];
});
await click(
query(".mention-without-membership-notice__body__link"),
"Invites the user"
);
// I would love to test that the invitation sent text is present here but
// dismiss is called right away instead of waiting 3 seconds.. Not much we can
// do about this - at least we are testing that nothing broke all the way through
// clearing the notice
assert.strictEqual(
queryAll(
".chat-notices .chat-notices__notice .mention-without-membership-notice"
).length,
0,
"Notice has been cleared"
);
});
}); });