DEV: better highlighting of mentions (#28403)
This commit improves the hilight-ing of mentions in posts and chat messages. - `@here` and `@all` will generate a `<a class="mention --wide">` - bots will generate a `<a class="mention --bot">` - current user will generate a `<a class="mention --current">` To achieve this change the following value transformer has been added: "mentions-class". It will be run in posts and chat messages after the mention is rendered. A bug were bots were not considered in mentioned users has also been fixed as part of this PR.
This commit is contained in:
parent
35b748e7f4
commit
ccb1861ada
|
@ -0,0 +1,21 @@
|
||||||
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
initialize() {
|
||||||
|
withPluginApi("1.36.0", (api) => {
|
||||||
|
api.registerValueTransformer("mentions-class", ({ value, context }) => {
|
||||||
|
const { user } = context;
|
||||||
|
|
||||||
|
if (user.id < 0) {
|
||||||
|
value.push("--bot");
|
||||||
|
} else if (user.id === api.getCurrentUser()?.id) {
|
||||||
|
value.push("--current");
|
||||||
|
} else if (user.username === "here" || user.username === "all") {
|
||||||
|
value.push("--wide");
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
|
@ -7,6 +7,7 @@ export const VALUE_TRANSFORMERS = Object.freeze([
|
||||||
// use only lowercase names
|
// use only lowercase names
|
||||||
"category-description-text",
|
"category-description-text",
|
||||||
"category-display-name",
|
"category-display-name",
|
||||||
|
"mentions-class",
|
||||||
"header-notifications-avatar-size",
|
"header-notifications-avatar-size",
|
||||||
"home-logo-href",
|
"home-logo-href",
|
||||||
"home-logo-image-url",
|
"home-logo-image-url",
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { isValidLink } from "discourse/lib/click-track";
|
||||||
import { number } from "discourse/lib/formatter";
|
import { number } from "discourse/lib/formatter";
|
||||||
import highlightHTML, { unhighlightHTML } from "discourse/lib/highlight-html";
|
import highlightHTML, { unhighlightHTML } from "discourse/lib/highlight-html";
|
||||||
import highlightSearch from "discourse/lib/highlight-search";
|
import highlightSearch from "discourse/lib/highlight-search";
|
||||||
|
import { applyValueTransformer } from "discourse/lib/transformer";
|
||||||
import {
|
import {
|
||||||
destroyUserStatusOnMentions,
|
destroyUserStatusOnMentions,
|
||||||
updateUserStatusOnMention,
|
updateUserStatusOnMention,
|
||||||
|
@ -66,16 +67,15 @@ export default class PostCooked {
|
||||||
// todo should be a better way of detecting if it is composer preview
|
// todo should be a better way of detecting if it is composer preview
|
||||||
this._isInComposerPreview = !this.decoratorHelper;
|
this._isInComposerPreview = !this.decoratorHelper;
|
||||||
|
|
||||||
const cookedDiv = this._computeCooked();
|
this.cookedDiv = this._computeCooked();
|
||||||
this.cookedDiv = cookedDiv;
|
|
||||||
|
|
||||||
this._insertQuoteControls(cookedDiv);
|
this._insertQuoteControls(this.cookedDiv);
|
||||||
this._showLinkCounts(cookedDiv);
|
this._showLinkCounts(this.cookedDiv);
|
||||||
this._applySearchHighlight(cookedDiv);
|
this._applySearchHighlight(this.cookedDiv);
|
||||||
this._initUserStatusOnMentions();
|
this._decorateMentions();
|
||||||
this._decorateAndAdopt(cookedDiv);
|
this._decorateAndAdopt(this.cookedDiv);
|
||||||
|
|
||||||
return cookedDiv;
|
return this.cookedDiv;
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
@ -365,9 +365,7 @@ export default class PostCooked {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(this.attrs.firstPost || this.attrs.embeddedPost) &&
|
(this.attrs.firstPost || this.attrs.embeddedPost) &&
|
||||||
this.ignoredUsers &&
|
this.ignoredUsers?.includes?.(this.attrs.username)
|
||||||
this.ignoredUsers.length > 0 &&
|
|
||||||
this.ignoredUsers.includes(this.attrs.username)
|
|
||||||
) {
|
) {
|
||||||
cookedDiv.classList.add("post-ignored");
|
cookedDiv.classList.add("post-ignored");
|
||||||
cookedDiv.innerHTML = I18n.t("post.ignored");
|
cookedDiv.innerHTML = I18n.t("post.ignored");
|
||||||
|
@ -378,44 +376,65 @@ export default class PostCooked {
|
||||||
return cookedDiv;
|
return cookedDiv;
|
||||||
}
|
}
|
||||||
|
|
||||||
_initUserStatusOnMentions() {
|
_decorateMentions() {
|
||||||
if (!this._isInComposerPreview) {
|
if (!this._isInComposerPreview) {
|
||||||
this._trackMentionedUsersStatus();
|
|
||||||
this._rerenderUserStatusOnMentions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_rerenderUserStatusOnMentions() {
|
|
||||||
destroyUserStatusOnMentions();
|
destroyUserStatusOnMentions();
|
||||||
this._post()?.mentioned_users?.forEach((user) =>
|
|
||||||
this._rerenderUserStatusOnMention(this.cookedDiv, user)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_rerenderUserStatusOnMention(postElement, user) {
|
this._extractMentions().forEach(({ mentions, user }) => {
|
||||||
const href = getURL(`/u/${user.username.toLowerCase()}`);
|
if (!this._isInComposerPreview) {
|
||||||
const mentions = postElement.querySelectorAll(`a.mention[href="${href}"]`);
|
this._trackMentionedUserStatus(user);
|
||||||
|
this._rerenderUserStatusOnMentions(mentions, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = applyValueTransformer("mentions-class", [], {
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
|
||||||
|
mentions.forEach((mention) => {
|
||||||
|
mention.classList.add(...classes);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_rerenderUserStatusOnMentions(mentions, user) {
|
||||||
mentions.forEach((mention) => {
|
mentions.forEach((mention) => {
|
||||||
updateUserStatusOnMention(
|
updateUserStatusOnMention(
|
||||||
getOwnerWithFallback(this._post()),
|
getOwnerWithFallback(this),
|
||||||
mention,
|
mention,
|
||||||
user.status
|
user.status
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_trackMentionedUsersStatus() {
|
_rerenderUsersStatusOnMentions() {
|
||||||
this._post()?.mentioned_users?.forEach((user) => {
|
this._extractMentions().forEach(({ mentions, user }) => {
|
||||||
user.statusManager?.trackStatus?.();
|
this._rerenderUserStatusOnMentions(mentions, user);
|
||||||
user.on?.("status-changed", this, "_rerenderUserStatusOnMentions");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_extractMentions() {
|
||||||
|
return (
|
||||||
|
this._post()?.mentioned_users?.map((user) => {
|
||||||
|
const href = getURL(`/u/${user.username.toLowerCase()}`);
|
||||||
|
const mentions = this.cookedDiv.querySelectorAll(
|
||||||
|
`a.mention[href="${href}"]`
|
||||||
|
);
|
||||||
|
|
||||||
|
return { user, mentions };
|
||||||
|
}) || []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_trackMentionedUserStatus(user) {
|
||||||
|
user.statusManager?.trackStatus?.();
|
||||||
|
user.on?.("status-changed", this, "_rerenderUsersStatusOnMentions");
|
||||||
|
}
|
||||||
|
|
||||||
_stopTrackingMentionedUsersStatus() {
|
_stopTrackingMentionedUsersStatus() {
|
||||||
this._post()?.mentioned_users?.forEach((user) => {
|
this._post()?.mentioned_users?.forEach((user) => {
|
||||||
user.statusManager?.stopTrackingStatus?.();
|
user.statusManager?.stopTrackingStatus?.();
|
||||||
user.off?.("status-changed", this, "_rerenderUserStatusOnMentions");
|
user.off?.("status-changed", this, "_rerenderUsersStatusOnMentions");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { visit } from "@ember/test-helpers";
|
||||||
|
import { test } from "qunit";
|
||||||
|
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||||
|
|
||||||
|
acceptance("mentions-class transformer", function (needs) {
|
||||||
|
needs.user();
|
||||||
|
|
||||||
|
test("applying a value transformation", async function (assert) {
|
||||||
|
await visit("/t/internationalization-localization/280");
|
||||||
|
|
||||||
|
assert.dom(".mention[href='/u/eviltrout']").hasClass("--current");
|
||||||
|
});
|
||||||
|
});
|
|
@ -667,6 +667,7 @@ export default {
|
||||||
avatar_template: "/images/avatar.png",
|
avatar_template: "/images/avatar.png",
|
||||||
uploaded_avatar_id: 5253,
|
uploaded_avatar_id: 5253,
|
||||||
created_at: "2013-02-07T14:02:07.869Z",
|
created_at: "2013-02-07T14:02:07.869Z",
|
||||||
|
mentioned_users: [{id: 19, username: "eviltrout"}],
|
||||||
cooked:
|
cooked:
|
||||||
'<p><aside class="quote" data-post="3" data-topic="280"><div class="title">\n<div class="quote-controls"></div>\n<img width="20" height="20" src="/user_avatar/meta.discourse.org/codinghorror/40/5297.png" class="avatar">codinghorror said:</div>\n<blockquote><p>So you could replace that lookup table with the "de" one to get German.</p></blockquote></aside></p>\n\n<p>The problem I see here is that this file is likely two grow and change massively over the next couple months, and tracking these changes in order to keep a localized file up to date is going to be a <em>bitch</em>. </p>\n\n<p>I wonder where there is a tool that can compare two yml structures and point out which nodes are missing? That would help keep track of new strings.</p>\n\n<p>Re keeping track of <em>changed</em> strings, <a class="mention" href="/u/eviltrout">@eviltrout</a> I found this very interesting: <a href="http://stackoverflow.com/questions/4232922/why-do-people-use-plain-english-as-translation-placeholders" rel="nofollow">http://stackoverflow.com/questions/4232922/why-do-people-use-plain-english-as-translation-placeholders</a> if plain English placeholders were used, any change in strings would lead to a <em>new</em> node in the yml file, making keeping the translation up to date easier. Maybe worth thinking about in the future.</p>',
|
'<p><aside class="quote" data-post="3" data-topic="280"><div class="title">\n<div class="quote-controls"></div>\n<img width="20" height="20" src="/user_avatar/meta.discourse.org/codinghorror/40/5297.png" class="avatar">codinghorror said:</div>\n<blockquote><p>So you could replace that lookup table with the "de" one to get German.</p></blockquote></aside></p>\n\n<p>The problem I see here is that this file is likely two grow and change massively over the next couple months, and tracking these changes in order to keep a localized file up to date is going to be a <em>bitch</em>. </p>\n\n<p>I wonder where there is a tool that can compare two yml structures and point out which nodes are missing? That would help keep track of new strings.</p>\n\n<p>Re keeping track of <em>changed</em> strings, <a class="mention" href="/u/eviltrout">@eviltrout</a> I found this very interesting: <a href="http://stackoverflow.com/questions/4232922/why-do-people-use-plain-english-as-translation-placeholders" rel="nofollow">http://stackoverflow.com/questions/4232922/why-do-people-use-plain-english-as-translation-placeholders</a> if plain English placeholders were used, any change in strings would lead to a <em>new</em> node in the yml file, making keeping the translation up to date easier. Maybe worth thinking about in the future.</p>',
|
||||||
post_number: 5,
|
post_number: 5,
|
||||||
|
|
|
@ -262,6 +262,18 @@ $hpad: 0.65em;
|
||||||
border-radius: 0.6em;
|
border-radius: 0.6em;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-wrap: nowrap;
|
text-wrap: nowrap;
|
||||||
|
|
||||||
|
&.--bot {
|
||||||
|
background: var(--highlight-low-or-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.--wide {
|
||||||
|
background: var(--highlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.--current {
|
||||||
|
background: var(--tertiary-low);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin nav-active() {
|
@mixin nav-active() {
|
||||||
|
|
|
@ -15,9 +15,11 @@ import { eq, not } from "truth-helpers";
|
||||||
import DButton from "discourse/components/d-button";
|
import DButton from "discourse/components/d-button";
|
||||||
import concatClass from "discourse/helpers/concat-class";
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
import optionalService from "discourse/lib/optional-service";
|
import optionalService from "discourse/lib/optional-service";
|
||||||
|
import { applyValueTransformer } from "discourse/lib/transformer";
|
||||||
import { updateUserStatusOnMention } from "discourse/lib/update-user-status-on-mention";
|
import { updateUserStatusOnMention } from "discourse/lib/update-user-status-on-mention";
|
||||||
import isZoomed from "discourse/lib/zoom-check";
|
import isZoomed from "discourse/lib/zoom-check";
|
||||||
import discourseDebounce from "discourse-common/lib/debounce";
|
import discourseDebounce from "discourse-common/lib/debounce";
|
||||||
|
import getURL from "discourse-common/lib/get-url";
|
||||||
import discourseLater from "discourse-common/lib/later";
|
import discourseLater from "discourse-common/lib/later";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
import I18n from "discourse-i18n";
|
import I18n from "discourse-i18n";
|
||||||
|
@ -95,11 +97,6 @@ export default class ChatMessage extends Component {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super(...arguments);
|
|
||||||
this.initMentionedUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
get pane() {
|
get pane() {
|
||||||
return this.threadContext ? this.chatThreadPane : this.chatChannelPane;
|
return this.threadContext ? this.chatThreadPane : this.chatChannelPane;
|
||||||
}
|
}
|
||||||
|
@ -214,6 +211,8 @@ export default class ChatMessage extends Component {
|
||||||
@action
|
@action
|
||||||
didInsertMessage(element) {
|
didInsertMessage(element) {
|
||||||
this.messageContainer = element;
|
this.messageContainer = element;
|
||||||
|
this.initMentionedUsers();
|
||||||
|
this.decorateMentions(element);
|
||||||
this.debounceDecorateCookedMessage();
|
this.debounceDecorateCookedMessage();
|
||||||
this.refreshStatusOnMentions();
|
this.refreshStatusOnMentions();
|
||||||
}
|
}
|
||||||
|
@ -239,6 +238,46 @@ export default class ChatMessage extends Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initMentionedUsers() {
|
||||||
|
this.args.message.mentionedUsers.forEach((user) => {
|
||||||
|
if (!user.statusManager.isTrackingStatus()) {
|
||||||
|
user.statusManager.trackStatus();
|
||||||
|
user.on("status-changed", this, "refreshStatusOnMentions");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
decorateMentions(cooked) {
|
||||||
|
if (this.args.message.channel.allowChannelWideMentions) {
|
||||||
|
const wideMentions = [...cooked.querySelectorAll("span.mention")];
|
||||||
|
MENTION_KEYWORDS.forEach((keyword) => {
|
||||||
|
const mentions = wideMentions.filter((node) => {
|
||||||
|
return node.textContent.trim() === `@${keyword}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const classes = applyValueTransformer("mentions-class", [], {
|
||||||
|
user: { username: keyword },
|
||||||
|
});
|
||||||
|
|
||||||
|
mentions.forEach((mention) => {
|
||||||
|
mention.classList.add(...classes);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.args.message.mentionedUsers.forEach((user) => {
|
||||||
|
const href = getURL(`/u/${user.username.toLowerCase()}`);
|
||||||
|
const mentions = cooked.querySelectorAll(`a.mention[href="${href}"]`);
|
||||||
|
const classes = applyValueTransformer("mentions-class", [], {
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
|
||||||
|
mentions.forEach((mention) => {
|
||||||
|
mention.classList.add(...classes);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
decorateCookedMessage(message) {
|
decorateCookedMessage(message) {
|
||||||
schedule("afterRender", () => {
|
schedule("afterRender", () => {
|
||||||
|
@ -248,18 +287,6 @@ export default class ChatMessage extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
initMentionedUsers() {
|
|
||||||
this.args.message.mentionedUsers.forEach((user) => {
|
|
||||||
if (user.statusManager.isTrackingStatus()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
user.statusManager.trackStatus();
|
|
||||||
user.on("status-changed", this, "refreshStatusOnMentions");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get show() {
|
get show() {
|
||||||
return (
|
return (
|
||||||
!this.args.message?.deletedAt ||
|
!this.args.message?.deletedAt ||
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { getOwnerWithFallback } from "discourse-common/lib/get-owner";
|
||||||
import { replaceIcon } from "discourse-common/lib/icon-library";
|
import { replaceIcon } from "discourse-common/lib/icon-library";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
import I18n from "discourse-i18n";
|
import I18n from "discourse-i18n";
|
||||||
import { MENTION_KEYWORDS } from "discourse/plugins/chat/discourse/components/chat-message";
|
|
||||||
import { clearChatComposerButtons } from "discourse/plugins/chat/discourse/lib/chat-composer-buttons";
|
import { clearChatComposerButtons } from "discourse/plugins/chat/discourse/lib/chat-composer-buttons";
|
||||||
import ChannelHashtagType from "discourse/plugins/chat/discourse/lib/hashtag-types/channel";
|
import ChannelHashtagType from "discourse/plugins/chat/discourse/lib/hashtag-types/channel";
|
||||||
import ChatHeaderIcon from "../components/chat/header/icon";
|
import ChatHeaderIcon from "../components/chat/header/icon";
|
||||||
|
@ -164,24 +163,6 @@ export default {
|
||||||
document.body.classList.remove("chat-drawer-active");
|
document.body.classList.remove("chat-drawer-active");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
api.decorateChatMessage(function (chatMessage, chatChannel) {
|
|
||||||
if (!this.currentUser) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const highlightable = [`@${this.currentUser.username}`];
|
|
||||||
if (chatChannel.allowChannelWideMentions) {
|
|
||||||
highlightable.push(...MENTION_KEYWORDS.map((k) => `@${k}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
chatMessage.querySelectorAll(".mention").forEach((node) => {
|
|
||||||
const mention = node.textContent.trim();
|
|
||||||
if (highlightable.includes(mention)) {
|
|
||||||
node.classList.add("highlighted", "valid-mention");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,8 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.mention {
|
a.mention,
|
||||||
|
span.mention.--wide {
|
||||||
@include mention;
|
@include mention;
|
||||||
|
|
||||||
&.highlighted {
|
&.highlighted {
|
||||||
|
@ -79,7 +80,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// unlinked, invalid mention
|
// unlinked, invalid mention
|
||||||
span.mention {
|
span.mention:not(--wide) {
|
||||||
color: var(--primary-high);
|
color: var(--primary-high);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -86,7 +86,7 @@ module Chat
|
||||||
end
|
end
|
||||||
|
|
||||||
def chat_users
|
def chat_users
|
||||||
User.distinct.joins(:user_option).real.where(user_options: { chat_enabled: true })
|
User.distinct.joins(:user_option).where(user_options: { chat_enabled: true })
|
||||||
end
|
end
|
||||||
|
|
||||||
def mentionable_groups
|
def mentionable_groups
|
||||||
|
|
|
@ -145,6 +145,15 @@ RSpec.describe Chat::ParsedMentions do
|
||||||
expect(result).to contain_exactly(channel_member_1.username, channel_member_2.username)
|
expect(result).to contain_exactly(channel_member_1.username, channel_member_2.username)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "returns bots who were mentioned directly" do
|
||||||
|
message = create_message("mentioning @system")
|
||||||
|
|
||||||
|
mentions = described_class.new(message)
|
||||||
|
result = mentions.direct_mentions.pluck(:username)
|
||||||
|
|
||||||
|
expect(result).to contain_exactly(Discourse.system_user.username)
|
||||||
|
end
|
||||||
|
|
||||||
it "returns a user when self-mentioning" do
|
it "returns a user when self-mentioning" do
|
||||||
message = create_message("Hey @#{channel_member_1.username}", user: channel_member_1)
|
message = create_message("Hey @#{channel_member_1.username}", user: channel_member_1)
|
||||||
|
|
||||||
|
|
|
@ -176,7 +176,8 @@ RSpec.describe "Chat channel", type: :system do
|
||||||
Fabricate(
|
Fabricate(
|
||||||
:chat_message,
|
:chat_message,
|
||||||
chat_channel: channel_1,
|
chat_channel: channel_1,
|
||||||
message: "hello @here @all @#{current_user.username} @#{other_user.username} @unexisting",
|
message:
|
||||||
|
"hello @here @all @#{current_user.username} @#{other_user.username} @unexisting @system",
|
||||||
user: other_user,
|
user: other_user,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -191,14 +192,12 @@ RSpec.describe "Chat channel", type: :system do
|
||||||
it "highlights the mentions" do
|
it "highlights the mentions" do
|
||||||
chat_page.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
|
|
||||||
expect(page).to have_selector(".mention.highlighted.valid-mention", text: "@here")
|
expect(page).to have_selector(".mention.--wide", text: "@here")
|
||||||
expect(page).to have_selector(".mention.highlighted.valid-mention", text: "@all")
|
expect(page).to have_selector(".mention.--wide", text: "@all")
|
||||||
expect(page).to have_selector(
|
expect(page).to have_selector(".mention.--current", text: "@#{current_user.username}")
|
||||||
".mention.highlighted.valid-mention",
|
|
||||||
text: "@#{current_user.username}",
|
|
||||||
)
|
|
||||||
expect(page).to have_selector(".mention", text: "@#{other_user.username}")
|
expect(page).to have_selector(".mention", text: "@#{other_user.username}")
|
||||||
expect(page).to have_selector(".mention", text: "@unexisting")
|
expect(page).to have_selector(".mention", text: "@unexisting")
|
||||||
|
expect(page).to have_selector(".mention.--bot", text: "@system")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "renders user status on mentions" do
|
it "renders user status on mentions" do
|
||||||
|
|
Loading…
Reference in New Issue