REFACTOR: <ChatMessage> component (#22172)

- Moves `<ChatMessageInfo />` to `<Chat::Message::Info />`
- Moves `<ChatMessageAvatar />` to `<Chat::Message::Avatar />`
- Moves `<ChatMessageLeftGutter />` to `<Chat::Message::LeftGutter />`, adds tests
- Creates `<Chat::Message::Error />`
- Creates `<Chat::Message::MentionWarning />`, adds tests and a styleguide
- Creates a model for ChatMessageMentionWarning, adds fabricator for it
- Keeps the enter/leave viewport logic inside the `<ChatMessage />` component instead of bubbling it to the channel and thread components
- Adds a scale animation when clicking a reaction
- Creates `chat/later-fn` modifier which accepts a function and a delay. It allows to call a function Xms after a component has been inserted, it's useful for animations.
- Moves css code out of chat-message into relevant files
- Deletes unused code

<!-- NOTE: All pull requests should have tests (rspec in Ruby, qunit in JavaScript). If your code does not include test coverage, please include an explanation of why it was omitted. -->
This commit is contained in:
Joffrey JAFFEUX 2023-06-19 09:50:54 +02:00 committed by GitHub
parent 626eda4c91
commit cbb9396353
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 801 additions and 484 deletions

View File

@ -42,8 +42,6 @@
<ChatMessage <ChatMessage
@message={{message}} @message={{message}}
@resendStagedMessage={{this.resendStagedMessage}} @resendStagedMessage={{this.resendStagedMessage}}
@messageDidEnterViewport={{this.messageDidEnterViewport}}
@messageDidLeaveViewport={{this.messageDidLeaveViewport}}
@context="channel" @context="channel"
/> />
{{/each}} {{/each}}

View File

@ -491,16 +491,6 @@ export default class ChatLivePane extends Component {
}); });
} }
@action
messageDidEnterViewport(message) {
message.visible = true;
}
@action
messageDidLeaveViewport(message) {
message.visible = false;
}
@debounce(READ_INTERVAL_MS) @debounce(READ_INTERVAL_MS)
updateLastReadMessage() { updateLastReadMessage() {
schedule("afterRender", () => { schedule("afterRender", () => {

View File

@ -1,61 +0,0 @@
<div
class="chat-message-info"
{{did-insert this.trackStatus}}
{{will-destroy this.stopTrackingStatus}}
>
{{#if @message.chatWebhookEvent}}
{{#if @message.chatWebhookEvent.username}}
<span
class={{concat-class
"chat-message-info__username"
this.usernameClasses
}}
>
{{@message.chatWebhookEvent.username}}
</span>
{{/if}}
<span class="chat-message-info__bot-indicator">
{{i18n "chat.bot"}}
</span>
{{else}}
<span
role="button"
class={{concat-class
"chat-message-info__username"
this.usernameClasses
"clickable"
}}
data-user-card={{@message.user.username}}
>
<span class="chat-message-info__username__name">{{this.name}}</span>
{{#if this.showStatus}}
<div class="chat-message-info__status">
<UserStatusMessage @status={{@message.user.status}} />
</div>
{{/if}}
</span>
{{/if}}
<span class="chat-message-info__date">
{{format-chat-date @message}}
</span>
{{#if @message.bookmark}}
<span class="chat-message-info__bookmark">
<BookmarkIcon @bookmark={{@message.bookmark}} />
</span>
{{/if}}
{{#if this.isFlagged}}
<span class="chat-message-info__flag">
{{#if @message.reviewableId}}
<LinkTo @route="review.show" @model={{@message.reviewableId}}>
{{d-icon "flag" title="chat.flagged"}}
</LinkTo>
{{else}}
{{d-icon "flag" title="chat.you_flagged"}}
{{/if}}
</span>
{{/if}}
</div>

View File

@ -37,10 +37,8 @@
this.onLongPressCancel this.onLongPressCancel
}} }}
{{chat/track-message {{chat/track-message
(hash (fn (mut @message.visible) true)
didEnterViewport=(fn @messageDidEnterViewport @message) (fn (mut @message.visible) false)
didLeaveViewport=(fn @messageDidLeaveViewport @message)
)
}} }}
> >
{{#if this.show}} {{#if this.show}}
@ -54,7 +52,7 @@
{{/if}} {{/if}}
{{#if this.deletedAndCollapsed}} {{#if this.deletedAndCollapsed}}
<div class="chat-message-text chat-message-deleted"> <div class="chat-message-text -deleted">
<DButton <DButton
@class="btn-flat chat-message-expand" @class="btn-flat chat-message-expand"
@action={{this.expand}} @action={{this.expand}}
@ -62,7 +60,7 @@
/> />
</div> </div>
{{else if this.hiddenAndCollapsed}} {{else if this.hiddenAndCollapsed}}
<div class="chat-message-hidden"> <div class="chat-message-text -hidden">
<DButton <DButton
@class="btn-flat chat-message-expand" @class="btn-flat chat-message-expand"
@action={{this.expand}} @action={{this.expand}}
@ -76,15 +74,16 @@
{{/unless}} {{/unless}}
{{#if this.hideUserInfo}} {{#if this.hideUserInfo}}
<ChatMessageLeftGutter @message={{@message}} /> <Chat::Message::LeftGutter @message={{@message}} />
{{else}} {{else}}
<ChatMessageAvatar @message={{@message}} /> <Chat::Message::Avatar @message={{@message}} />
{{/if}} {{/if}}
<div class="chat-message-content"> <div class="chat-message-content">
{{#unless this.hideUserInfo}} <Chat::Message::Info
<ChatMessageInfo @message={{@message}} /> @message={{@message}}
{{/unless}} @show={{not this.hideUserInfo}}
/>
<ChatMessageText <ChatMessageText
@cooked={{@message.cooked}} @cooked={{@message.cooked}}
@ -93,12 +92,6 @@
> >
{{#if @message.reactions.length}} {{#if @message.reactions.length}}
<div class="chat-message-reaction-list"> <div class="chat-message-reaction-list">
{{#if this.reactionLabel}}
<div class="reaction-users-list">
{{replace-emoji this.reactionLabel}}
</div>
{{/if}}
{{#each @message.reactions as |reaction|}} {{#each @message.reactions as |reaction|}}
<ChatMessageReaction <ChatMessageReaction
@reaction={{reaction}} @reaction={{reaction}}
@ -108,8 +101,7 @@
/> />
{{/each}} {{/each}}
{{#if this.chat.userCanInteractWithChat}} {{#if this.shouldRenderOpenEmojiPickerButton}}
{{#unless this.site.mobileView}}
<DButton <DButton
@class="chat-message-react-btn" @class="chat-message-react-btn"
@action={{this.messageInteractor.openEmojiPicker}} @action={{this.messageInteractor.openEmojiPicker}}
@ -117,83 +109,16 @@
@title="chat.react" @title="chat.react"
@forwardEvent={{true}} @forwardEvent={{true}}
/> />
{{/unless}}
{{/if}} {{/if}}
</div> </div>
{{/if}} {{/if}}
</ChatMessageText> </ChatMessageText>
{{#if @message.error}} <Chat::Message::Error
<div class="chat-send-error"> @message={{@message}}
{{#if (eq @message.error "network_error")}} @onRetry={{@resendStagedMessage}}
<DButton
class="retry-staged-message-btn"
@action={{fn @resendStagedMessage @message}}
@icon="exclamation-circle"
>
<span class="retry-staged-message-btn__title">
{{i18n "chat.retry_staged_message.title"}}
</span>
<span class="retry-staged-message-btn__action">
{{i18n "chat.retry_staged_message.action"}}
</span>
</DButton>
{{else}}
{{@message.error}}
{{/if}}
</div>
{{/if}}
{{#if this.mentionWarning}}
<div class="alert alert-info chat-message-mention-warning">
{{#if this.mentionWarning.invitation_sent}}
{{d-icon "check"}}
<span>
{{i18n
"chat.mention_warning.invitations_sent"
count=this.mentionWarning.without_membership.length
}}
</span>
{{else}}
<FlatButton
@class="dismiss-mention-warning"
@title="chat.mention_warning.dismiss"
@action={{this.dismissMentionWarning}}
@icon="times"
/> />
<Chat::Message::MentionWarning @message={{@message}} />
{{#if this.mentionWarning.cannot_see}}
<p class="warning-item cannot-see">
{{this.mentionedCannotSeeText}}
</p>
{{/if}}
{{#if this.mentionWarning.without_membership}}
<p class="warning-item without-membership">
<span>{{this.mentionedWithoutMembershipText}}</span>
<a
class="invite-link"
href
onclick={{this.inviteMentioned}}
>
{{i18n "chat.mention_warning.invite"}}
</a>
</p>
{{/if}}
{{#if this.mentionWarning.group_mentions_disabled}}
<p class="warning-item">
{{this.groupsWithDisabledMentions}}
</p>
{{/if}}
{{#if this.mentionWarning.groups_with_too_many_members}}
<p class="warning-item">
{{this.groupsWithTooManyMembers}}
</p>
{{/if}}
{{/if}}
</div>
{{/if}}
</div> </div>
{{#if this.showThreadIndicator}} {{#if this.showThreadIndicator}}

View File

@ -2,7 +2,6 @@ import { action } from "@ember/object";
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import I18n from "I18n"; import I18n from "I18n";
import optionalService from "discourse/lib/optional-service"; import optionalService from "discourse/lib/optional-service";
import { ajax } from "discourse/lib/ajax";
import { cancel, schedule } from "@ember/runloop"; import { cancel, schedule } from "@ember/runloop";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import discourseLater from "discourse-common/lib/later"; import discourseLater from "discourse-common/lib/later";
@ -101,6 +100,10 @@ export default class ChatMessage extends Component {
); );
} }
get shouldRenderOpenEmojiPickerButton() {
return this.chat.userCanInteractWithChat && this.site.desktopView;
}
@action @action
expand() { expand() {
const recursiveExpand = (message) => { const recursiveExpand = (message) => {
@ -386,82 +389,6 @@ export default class ChatMessage extends Component {
); );
} }
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?.cannot_see?.[0]?.username,
count: this.mentionWarning?.cannot_see?.length,
}
);
}
get mentionedWithoutMembershipText() {
return this._findTranslatedWarning(
"chat.mention_warning.without_membership",
"chat.mention_warning.without_membership_multiple",
{
username: this.mentionWarning?.without_membership?.[0]?.username,
count: this.mentionWarning?.without_membership?.length,
}
);
}
get groupsWithDisabledMentions() {
return this._findTranslatedWarning(
"chat.mention_warning.group_mentions_disabled",
"chat.mention_warning.group_mentions_disabled_multiple",
{
group_name: this.mentionWarning?.group_mentions_disabled?.[0],
count: this.mentionWarning?.group_mentions_disabled?.length,
}
);
}
get groupsWithTooManyMembers() {
return this._findTranslatedWarning(
"chat.mention_warning.too_many_members",
"chat.mention_warning.too_many_members_multiple",
{
group_name: this.mentionWarning.groups_with_too_many_members?.[0],
count: this.mentionWarning.groups_with_too_many_members?.length,
}
);
}
_findTranslatedWarning(oneKey, multipleKey, args) {
const translationKey = args.count === 1 ? oneKey : multipleKey;
args.count--;
return I18n.t(translationKey, args);
}
@action
inviteMentioned() {
const userIds = this.mentionWarning.without_membership.mapBy("id");
ajax(`/chat/${this.args.message.channel.id}/invite`, {
method: "PUT",
data: { user_ids: userIds, chat_message_id: this.args.message.id },
}).then(() => {
this.args.message.mentionWarning.set("invitationSent", true);
this._invitationSentTimer = discourseLater(() => {
this.dismissMentionWarning();
}, 3000);
});
return false;
}
@action
dismissMentionWarning() {
this.args.message.mentionWarning = null;
}
#teardownMentionedUsers() { #teardownMentionedUsers() {
this.args.message.mentionedUsers.forEach((user) => { this.args.message.mentionedUsers.forEach((user) => {
user.stopTrackingStatus(); user.stopTrackingStatus();

View File

@ -27,8 +27,6 @@
<ChatMessage <ChatMessage
@message={{message}} @message={{message}}
@resendStagedMessage={{this.resendStagedMessage}} @resendStagedMessage={{this.resendStagedMessage}}
@messageDidEnterViewport={{this.messageDidEnterViewport}}
@messageDidLeaveViewport={{this.messageDidLeaveViewport}}
@context="thread" @context="thread"
/> />
{{/each}} {{/each}}

View File

@ -348,16 +348,6 @@ export default class ChatThreadPanel extends Component {
@action @action
resendStagedMessage() {} resendStagedMessage() {}
@action
messageDidEnterViewport(message) {
message.visible = true;
}
@action
messageDidLeaveViewport(message) {
message.visible = false;
}
#handleErrors(error) { #handleErrors(error) {
switch (error?.jqXHR?.status) { switch (error?.jqXHR?.status) {
case 429: case 429:

View File

@ -0,0 +1,20 @@
{{#if @message.error}}
<div class="chat-message-error">
{{#if (eq @message.error "network_error")}}
<DButton
class="chat-message-error__retry-btn"
@action={{fn @onRetry @message}}
@icon="exclamation-circle"
>
<span class="chat-message-error__retry-btn-title">
{{i18n "chat.retry_staged_message.title"}}
</span>
<span class="chat-message-error__retry-btn-action">
{{i18n "chat.retry_staged_message.action"}}
</span>
</DButton>
{{else}}
{{@message.error}}
{{/if}}
</div>
{{/if}}

View File

@ -0,0 +1,63 @@
{{#if @show}}
<div
class="chat-message-info"
{{did-insert this.trackStatus}}
{{will-destroy this.stopTrackingStatus}}
>
{{#if @message.chatWebhookEvent}}
{{#if @message.chatWebhookEvent.username}}
<span
class={{concat-class
"chat-message-info__username"
this.usernameClasses
}}
>
{{@message.chatWebhookEvent.username}}
</span>
{{/if}}
<span class="chat-message-info__bot-indicator">
{{i18n "chat.bot"}}
</span>
{{else}}
<span
role="button"
class={{concat-class
"chat-message-info__username"
this.usernameClasses
"clickable"
}}
data-user-card={{@message.user.username}}
>
<span class="chat-message-info__username__name">{{this.name}}</span>
{{#if this.showStatus}}
<div class="chat-message-info__status">
<UserStatusMessage @status={{@message.user.status}} />
</div>
{{/if}}
</span>
{{/if}}
<span class="chat-message-info__date">
{{format-chat-date @message}}
</span>
{{#if @message.bookmark}}
<span class="chat-message-info__bookmark">
<BookmarkIcon @bookmark={{@message.bookmark}} />
</span>
{{/if}}
{{#if this.isFlagged}}
<span class="chat-message-info__flag">
{{#if @message.reviewableId}}
<LinkTo @route="review.show" @model={{@message.reviewableId}}>
{{d-icon "flag" title="chat.flagged"}}
</LinkTo>
{{else}}
{{d-icon "flag" title="chat.you_flagged"}}
{{/if}}
</span>
{{/if}}
</div>
{{/if}}

View File

@ -0,0 +1,56 @@
{{#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}}
</div>
{{/if}}

View File

@ -0,0 +1,98 @@
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)
);
}
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,35 @@
<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

@ -0,0 +1,75 @@
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

@ -1,11 +1,6 @@
<StyleguideExample @title="<ChatMessage>"> <StyleguideExample @title="<ChatMessage>">
<Styleguide::Component> <Styleguide::Component>
<ChatMessage <ChatMessage @message={{this.message}} @context="channel" />
@message={{this.message}}
@context="channel"
@messageDidEnterViewport={{(noop)}}
@messageDidLeaveViewport={{(noop)}}
/>
</Styleguide::Component> </Styleguide::Component>
<Styleguide::Controls> <Styleguide::Controls>

View File

@ -3,3 +3,4 @@
<Styleguide::ChatThreadListItem /> <Styleguide::ChatThreadListItem />
<Styleguide::ChatComposerMessageDetails /> <Styleguide::ChatComposerMessageDetails />
<Styleguide::ChatHeaderIcon /> <Styleguide::ChatHeaderIcon />
<Styleguide::ChatMessageMentionWarning />

View File

@ -13,10 +13,12 @@ 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";
import Category from "discourse/models/category"; import Category from "discourse/models/category";
import Group from "discourse/models/group";
let sequence = 0; let sequence = 0;
@ -145,6 +147,16 @@ function reactionFabricator(args = {}) {
}); });
} }
function groupFabricator(args = {}) {
return Group.create({
name: args.name || "Engineers",
});
}
function messageMentionWarningFabricator(message, args = {}) {
return ChatMessageMentionWarning.create(message, args);
}
function uploadFabricator() { function uploadFabricator() {
return { return {
extension: "jpeg", extension: "jpeg",
@ -175,4 +187,6 @@ export default {
upload: uploadFabricator, upload: uploadFabricator,
category: categoryFabricator, category: categoryFabricator,
directMessage: directMessageFabricator, directMessage: directMessageFabricator,
messageMentionWarning: messageMentionWarningFabricator,
group: groupFabricator,
}; };

View File

@ -0,0 +1,21 @@
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;
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;
}
}

View File

@ -0,0 +1,21 @@
import Modifier from "ember-modifier";
import { registerDestructor } from "@ember/destroyable";
import { cancel } from "@ember/runloop";
import discourseLater from "discourse-common/lib/later";
export default class ChatLaterFn extends Modifier {
constructor(owner, args) {
super(owner, args);
registerDestructor(this, (instance) => instance.cleanup());
}
modify(element, [fn, delay]) {
this.handler = discourseLater(() => {
fn?.(element);
}, delay);
}
cleanup() {
cancel(this.handler);
}
}

View File

@ -11,9 +11,9 @@ export default class ChatTrackMessage extends Modifier {
registerDestructor(this, (instance) => instance.cleanup()); registerDestructor(this, (instance) => instance.cleanup());
} }
modify(element, [callbacks = {}]) { modify(element, [didEnterViewport, didLeaveViewport]) {
this.didEnterViewport = callbacks.didEnterViewport; this.didEnterViewport = didEnterViewport;
this.didLeaveViewport = callbacks.didLeaveViewport; this.didLeaveViewport = didLeaveViewport;
this.intersectionObserver = new IntersectionObserver( this.intersectionObserver = new IntersectionObserver(
this._intersectionObserverCallback, this._intersectionObserverCallback,

View File

@ -460,6 +460,20 @@ export default class ChatApi extends Service {
}); });
} }
/**
* Invite users to a channel.
*
* @param {number} channelId - The ID of the channel.
* @param {Array<number>} userIds - The IDs of the users to invite.
* @param {Array<number>} [messageId] - The ID of a message to highlight when opening the notification.
*/
invite(channelId, userIds, options = {}) {
return ajax(`/chat/${channelId}/invite`, {
type: "put",
data: { user_ids: userIds, chat_message_id: options.messageId },
});
}
get #basePath() { get #basePath() {
return "/chat/api"; return "/chat/api";
} }

View File

@ -1,6 +1,6 @@
import Service, { inject as service } from "@ember/service"; import Service, { inject as service } from "@ember/service";
import EmberObject from "@ember/object";
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";
@ -200,7 +200,7 @@ export default class ChatPaneBaseSubscriptionsManager extends Service {
handleMentionWarning(data) { handleMentionWarning(data) {
const message = this.messagesManager.findMessage(data.chat_message_id); const message = this.messagesManager.findMessage(data.chat_message_id);
if (message) { if (message) {
message.mentionWarning = EmberObject.create(data); message.mentionWarning = ChatMessageMentionWarning.create(message, data);
} }
} }

View File

@ -0,0 +1,40 @@
.chat-message-error {
color: var(--danger-medium);
&__retry-btn {
padding: 0.5em 0;
background: none;
&:hover,
&:focus,
.-active & {
background: none !important;
}
&:focus .retry-staged-message-btn__action {
text-decoration: underline;
}
.d-icon,
&-title,
&:hover .d-icon {
color: var(--danger) !important;
font-size: var(--font-down-1);
}
.d-icon {
margin-right: 0.25em !important;
}
&-action {
color: var(--tertiary);
font-size: var(--font-down-1);
margin-left: 0.25em;
&:hover {
color: var(--tertiary-high);
text-decoration: underline;
}
}
}
}

View File

@ -0,0 +1,19 @@
.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

@ -1,5 +1,5 @@
.chat-message-deleted, .chat-message-text.-deleted,
.chat-message-hidden { .chat-message-text.-hidden {
margin-left: calc(var(--message-left-width) + 0.75em); margin-left: calc(var(--message-left-width) + 0.75em);
padding: 0; padding: 0;
@ -18,6 +18,12 @@
} }
} }
.chat-message-reaction {
> * {
pointer-events: none;
}
}
.chat-message { .chat-message {
align-items: flex-start; align-items: flex-start;
padding: 0.25em 0.5em 0.25em 0.75em; padding: 0.25em 0.5em 0.25em 0.75em;
@ -26,10 +32,11 @@
.chat-message-reaction { .chat-message-reaction {
@include chat-reaction; @include chat-reaction;
} will-change: scale;
.not-mobile-device &.deleted:hover { &:active {
background-color: var(--danger-hover); transform: scale(0.93);
}
} }
.chat-message-content { .chat-message-content {
@ -87,23 +94,16 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
.reaction-users-list {
position: absolute;
top: -2px;
transform: translateY(-100%);
border: 1px solid var(--primary-low);
border-radius: 6px;
padding: 0.5em;
background: var(--primary-very-low);
max-width: 300px;
z-index: 3;
}
.chat-message-react-btn { .chat-message-react-btn {
vertical-align: top; vertical-align: top;
padding: 0em 0.25em; padding: 0em 0.25em;
background: none; background: none;
border: none; border: none;
will-change: scale;
&:active {
transform: scale(0.93);
}
> * { > * {
pointer-events: none; pointer-events: none;
@ -121,32 +121,6 @@
} }
} }
.chat-send-error {
color: var(--danger-medium);
}
.chat-message-mention-warning {
position: relative;
margin-top: 0.25em;
font-size: var(--font-down-1);
.dismiss-mention-warning {
position: absolute;
top: 15px;
right: 5px;
cursor: pointer;
}
.warning-item {
margin: 0.25em 0;
}
.invite-link {
color: var(--tertiary);
cursor: pointer;
}
}
.chat-message-avatar .chat-user-avatar .chat-user-avatar-container .avatar, .chat-message-avatar .chat-user-avatar .chat-user-avatar-container .avatar,
.chat-emoji-avatar .chat-emoji-avatar-container { .chat-emoji-avatar .chat-emoji-avatar-container {
width: 28px; width: 28px;
@ -256,94 +230,3 @@
display: none; display: none;
} }
} }
.has-full-page-chat .chat-message .onebox:not(img),
.chat-drawer-container .chat-message .onebox {
margin: 0.5em 0;
border-width: 2px;
header {
margin-bottom: 0.5em;
}
h3 a,
h4 a {
font-size: 14px;
}
pre {
display: flex;
max-height: 150px;
}
p {
overflow: hidden;
}
}
.chat-drawer-container .chat-message .onebox {
width: 85%;
border: 2px solid var(--primary-low);
header {
margin-bottom: 0.5em;
}
.onebox-body {
grid-template-rows: auto auto auto;
overflow: auto;
}
h3 {
@include line-clamp(2);
font-weight: 500;
font-size: var(--font-down-1);
}
p {
display: none;
}
}
.chat-message-reaction {
> * {
pointer-events: none;
}
}
.retry-staged-message-btn {
padding: 0.5em 0;
background: none;
&:hover,
&:focus,
.-active & {
background: none !important;
}
&:focus .retry-staged-message-btn__action {
text-decoration: underline;
}
.d-icon,
&__title,
&:hover .d-icon {
color: var(--danger) !important;
font-size: var(--font-down-1);
}
.d-icon {
margin-right: 0.25em !important;
}
&__action {
color: var(--tertiary);
font-size: var(--font-down-1);
margin-left: 0.25em;
&:hover {
color: var(--tertiary-high);
text-decoration: underline;
}
}
}

View File

@ -28,8 +28,50 @@
} }
} }
.chat-transcript { .chat-drawer-container .chat-message .onebox {
.chat-transcript-user-avatar .avatar { width: 85%;
aspect-ratio: 20 / 20; border: 2px solid var(--primary-low);
header {
margin-bottom: 0.5em;
}
.onebox-body {
grid-template-rows: auto auto auto;
overflow: auto;
}
h3 {
@include line-clamp(2);
font-weight: 500;
font-size: var(--font-down-1);
}
p {
display: none;
}
}
.has-full-page-chat .chat-message .onebox:not(img),
.chat-drawer-container .chat-message .onebox {
margin: 0.5em 0;
border-width: 2px;
header {
margin-bottom: 0.5em;
}
h3 a,
h4 a {
font-size: 14px;
}
pre {
display: flex;
max-height: 150px;
}
p {
overflow: hidden;
} }
} }

View File

@ -53,6 +53,10 @@
} }
} }
.chat-transcript-user-avatar .avatar {
aspect-ratio: 20 / 20;
}
.chat-transcript-user { .chat-transcript-user {
display: flex; display: flex;
flex-wrap: wrap-reverse; flex-wrap: wrap-reverse;

View File

@ -58,3 +58,5 @@
@import "chat-thread-unread-indicator"; @import "chat-thread-unread-indicator";
@import "chat-thread-participants"; @import "chat-thread-participants";
@import "channel-summary-modal"; @import "channel-summary-modal";
@import "chat-message-mention-warning";
@import "chat-message-error";

View File

@ -24,7 +24,7 @@ RSpec.describe "Deleted message", type: :system do
last_message = find(".chat-message-container:last-child") last_message = find(".chat-message-container:last-child")
channel_page.delete_message(OpenStruct.new(id: last_message["data-id"])) channel_page.delete_message(OpenStruct.new(id: last_message["data-id"]))
expect(channel_page).to have_deleted_message( expect(channel_page.messages).to have_deleted_message(
OpenStruct.new(id: last_message["data-id"]), OpenStruct.new(id: last_message["data-id"]),
count: 1, count: 1,
) )
@ -40,12 +40,12 @@ RSpec.describe "Deleted message", type: :system do
.update!(last_read_message_id: message.id) .update!(last_read_message_id: message.id)
chat_page.visit_channel(channel_1) chat_page.visit_channel(channel_1)
channel_page.delete_message(message) channel_page.delete_message(message)
expect(channel_page).to have_deleted_message(message, count: 1) expect(channel_page.messages).to have_deleted_message(message, count: 1)
sidebar_component.click_link(channel_2.name) sidebar_component.click_link(channel_2.name)
expect(channel_page).to have_no_loading_skeleton expect(channel_page).to have_no_loading_skeleton
sidebar_component.click_link(channel_1.name) sidebar_component.click_link(channel_1.name)
expect(channel_page).to have_deleted_message(message, count: 1) expect(channel_page.messages).to have_deleted_message(message, count: 1)
end end
context "when the current user is not admin" do context "when the current user is not admin" do
@ -72,7 +72,7 @@ RSpec.describe "Deleted message", type: :system do
end end
sidebar_component.click_link(channel_1.name) sidebar_component.click_link(channel_1.name)
expect(channel_page).to have_no_message(id: message.id) expect(channel_page.messages).to have_no_message(id: message.id)
end end
end end
end end
@ -93,10 +93,9 @@ RSpec.describe "Deleted message", type: :system do
channel_page.delete_message(message_4) channel_page.delete_message(message_4)
channel_page.delete_message(message_6) channel_page.delete_message(message_6)
expect(channel_page).to have_deleted_message(message_1) expect(channel_page.messages).to have_deleted_messages(message_1, message_6)
expect(channel_page).to have_deleted_message(message_4, count: 2) expect(channel_page.messages).to have_deleted_message(message_4, count: 2)
expect(channel_page).to have_deleted_message(message_6) expect(channel_page.messages).to have_no_message(id: message_3.id)
expect(channel_page).to have_no_message(id: message_3.id)
end end
end end
@ -129,20 +128,20 @@ RSpec.describe "Deleted message", type: :system do
channel_page.message_thread_indicator(thread.original_message).click channel_page.message_thread_indicator(thread.original_message).click
expect(side_panel).to have_open_thread(thread) expect(side_panel).to have_open_thread(thread)
expect(channel_page).to have_message(id: message_1.id) expect(channel_page.messages).to have_message(id: message_2.id)
expect(channel_page).to have_message(id: message_2.id) expect(channel_page.messages).to have_message(id: message_1.id)
expect(open_thread).to have_message(thread_id: thread.id, id: message_4.id) expect(open_thread.messages).to have_message(thread_id: thread.id, id: message_4.id)
expect(open_thread).to have_message(thread_id: thread.id, id: message_5.id) expect(open_thread.messages).to have_message(thread_id: thread.id, id: message_5.id)
Chat::Publisher.publish_bulk_delete!( Chat::Publisher.publish_bulk_delete!(
channel_1, channel_1,
[message_1.id, message_2.id, message_4.id, message_5.id].flatten, [message_1.id, message_2.id, message_4.id, message_5.id].flatten,
) )
expect(channel_page).to have_no_message(id: message_1.id) expect(channel_page.messages).to have_no_message(id: message_1.id)
expect(channel_page).to have_deleted_message(message_2, count: 2) expect(channel_page.messages).to have_deleted_message(message_2, count: 2)
expect(open_thread).to have_no_message(thread_id: thread.id, id: message_4.id) expect(open_thread.messages).to have_no_message(thread_id: thread.id, id: message_4.id)
expect(open_thread).to have_deleted_message(message_5, count: 2) expect(open_thread.messages).to have_deleted_message(message_5, count: 2)
end end
end end
end end

View File

@ -84,11 +84,11 @@ RSpec.describe "Move message to channel", type: :system do
click_button(I18n.t("js.chat.move_to_channel.confirm_move")) click_button(I18n.t("js.chat.move_to_channel.confirm_move"))
expect(page).to have_current_path(chat.channel_path(channel_2.slug, channel_2.id)) expect(page).to have_current_path(chat.channel_path(channel_2.slug, channel_2.id))
expect(channel_page).to have_message(text: message_1.message) expect(channel_page.messages).to have_message(text: message_1.message)
chat_page.visit_channel(channel_1) chat_page.visit_channel(channel_1)
expect(channel_page).to have_deleted_message(message_1) expect(channel_page.messages).to have_deleted_message(message_1)
end end
end end
end end

View File

@ -188,13 +188,6 @@ module PageObjects
check_message_presence(exists: false, text: text, id: id) check_message_presence(exists: false, text: text, id: id)
end end
def has_deleted_message?(message, count: 1)
has_css?(
".chat-channel .chat-message-container[data-id=\"#{message.id}\"] .chat-message-deleted",
text: I18n.t("js.chat.deleted", count: count),
)
end
def check_message_presence(exists: true, text: nil, id: nil) def check_message_presence(exists: true, text: nil, id: nil)
css_method = exists ? :has_css? : :has_no_css? css_method = exists ? :has_css? : :has_no_css?
if text if text

View File

@ -144,13 +144,6 @@ module PageObjects
".chat-thread .chat-messages-container .chat-message-container[data-id=\"#{id}\"]" ".chat-thread .chat-messages-container .chat-message-container[data-id=\"#{id}\"]"
end end
def has_deleted_message?(message, count: 1)
has_css?(
".chat-thread .chat-message-container[data-id=\"#{message.id}\"] .chat-message-deleted",
text: I18n.t("js.chat.deleted", count: count),
)
end
def open_edit_message(message) def open_edit_message(message)
hover_message(message) hover_message(message)
click_more_button click_more_button

View File

@ -44,6 +44,14 @@ module PageObjects
messages.all? { |message| has_message?(id: message.id, selected: true) } messages.all? { |message| has_message?(id: message.id, selected: true) }
end end
def has_deleted_messages?(*messages)
messages.all? { |message| has_message?(id: message.id, deleted: 1) }
end
def has_deleted_message?(message, count: 1)
has_message?(id: message.id, deleted: count)
end
private private
def message def message

View File

@ -226,7 +226,7 @@ acceptance("Chat | User status on mentions", function (needs) {
await visit(`/chat/c/-/${channelId}`); await visit(`/chat/c/-/${channelId}`);
await deleteMessage(".chat-message-content"); await deleteMessage(".chat-message-content");
await restoreMessage(".chat-message-deleted"); await restoreMessage(".chat-message-text.-deleted");
assertStatusIsRendered( assertStatusIsRendered(
assert, assert,
@ -239,7 +239,7 @@ acceptance("Chat | User status on mentions", function (needs) {
await visit(`/chat/c/-/${channelId}`); await visit(`/chat/c/-/${channelId}`);
await deleteMessage(".chat-message-content"); await deleteMessage(".chat-message-content");
await restoreMessage(".chat-message-deleted"); await restoreMessage(".chat-message-text.-deleted");
loggedInUser().appEvents.trigger("user-status:changed", { loggedInUser().appEvents.trigger("user-status:changed", {
[mentionedUser1.id]: newStatus, [mentionedUser1.id]: newStatus,
@ -254,7 +254,7 @@ acceptance("Chat | User status on mentions", function (needs) {
await visit(`/chat/c/-/${channelId}`); await visit(`/chat/c/-/${channelId}`);
await deleteMessage(".chat-message-content"); await deleteMessage(".chat-message-content");
await restoreMessage(".chat-message-deleted"); await restoreMessage(".chat-message-text.-deleted");
loggedInUser().appEvents.trigger("user-status:changed", { loggedInUser().appEvents.trigger("user-status:changed", {
[mentionedUser1.id]: null, [mentionedUser1.id]: null,

View File

@ -14,7 +14,7 @@ module("Discourse Chat | Component | chat-message-avatar", function (hooks) {
chat_webhook_event: { emoji: ":heart:" }, chat_webhook_event: { emoji: ":heart:" },
}); });
await render(hbs`<ChatMessageAvatar @message={{this.message}} />`); await render(hbs`<Chat::Message::Avatar @message={{this.message}} />`);
assert.strictEqual(query(".chat-emoji-avatar .emoji").title, "heart"); assert.strictEqual(query(".chat-emoji-avatar .emoji").title, "heart");
}); });
@ -24,7 +24,7 @@ module("Discourse Chat | Component | chat-message-avatar", function (hooks) {
user: { username: "discobot" }, user: { username: "discobot" },
}); });
await render(hbs`<ChatMessageAvatar @message={{this.message}} />`); await render(hbs`<Chat::Message::Avatar @message={{this.message}} />`);
assert.true(exists('.chat-user-avatar [data-user-card="discobot"]')); assert.true(exists('.chat-user-avatar [data-user-card="discobot"]'));
}); });

View File

@ -11,12 +11,16 @@ import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
module("Discourse Chat | Component | chat-message-info", function (hooks) { module("Discourse Chat | Component | chat-message-info", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
const template = hbs`
<Chat::Message::Info @message={{this.message}} @show={{true}} />
`;
test("chat_webhook_event", async function (assert) { test("chat_webhook_event", async function (assert) {
this.message = ChatMessage.create(fabricators.channel(), { this.message = fabricators.message({
chat_webhook_event: { username: "discobot" }, chat_webhook_event: { username: "discobot" },
}); });
await render(hbs`<ChatMessageInfo @message={{this.message}} />`); await render(template);
assert.strictEqual( assert.strictEqual(
query(".chat-message-info__username").innerText.trim(), query(".chat-message-info__username").innerText.trim(),
@ -29,11 +33,11 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
}); });
test("user", async function (assert) { test("user", async function (assert) {
this.message = ChatMessage.create(fabricators.channel(), { this.message = fabricators.message({
user: { username: "discobot" }, user: { username: "discobot" },
}); });
await render(hbs`<ChatMessageInfo @message={{this.message}} />`); await render(template);
assert.strictEqual( assert.strictEqual(
query(".chat-message-info__username").innerText.trim(), query(".chat-message-info__username").innerText.trim(),
@ -42,18 +46,18 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
}); });
test("date", async function (assert) { test("date", async function (assert) {
this.message = ChatMessage.create(fabricators.channel(), { this.message = fabricators.message({
user: { username: "discobot" }, user: { username: "discobot" },
created_at: moment(), created_at: moment(),
}); });
await render(hbs`<ChatMessageInfo @message={{this.message}} />`); await render(template);
assert.true(exists(".chat-message-info__date")); assert.true(exists(".chat-message-info__date"));
}); });
test("bookmark (with reminder)", async function (assert) { test("bookmark (with reminder)", async function (assert) {
this.message = ChatMessage.create(fabricators.channel(), { this.message = fabricators.message({
user: { username: "discobot" }, user: { username: "discobot" },
bookmark: Bookmark.create({ bookmark: Bookmark.create({
reminder_at: moment(), reminder_at: moment(),
@ -61,7 +65,7 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
}), }),
}); });
await render(hbs`<ChatMessageInfo @message={{this.message}} />`); await render(template);
assert.true( assert.true(
exists(".chat-message-info__bookmark .d-icon-discourse-bookmark-clock") exists(".chat-message-info__bookmark .d-icon-discourse-bookmark-clock")
@ -76,50 +80,48 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
}), }),
}); });
await render(hbs`<ChatMessageInfo @message={{this.message}} />`); await render(template);
assert.true(exists(".chat-message-info__bookmark .d-icon-bookmark")); assert.true(exists(".chat-message-info__bookmark .d-icon-bookmark"));
}); });
test("user status", async function (assert) { test("user status", async function (assert) {
const status = { description: "off to dentist", emoji: "tooth" }; const status = { description: "off to dentist", emoji: "tooth" };
this.message = ChatMessage.create(fabricators.channel(), { this.message = fabricators.message({ user: { status } });
user: { status },
});
await render(hbs`<ChatMessageInfo @message={{this.message}} />`); await render(template);
assert.true(exists(".chat-message-info__status .user-status-message")); assert.true(exists(".chat-message-info__status .user-status-message"));
}); });
test("reviewable", async function (assert) { test("flag status", async function (assert) {
this.message = ChatMessage.create(fabricators.channel(), { this.message = fabricators.message({
user: { username: "discobot" }, user: { username: "discobot" },
user_flag_status: 0, user_flag_status: 0,
}); });
await render(hbs`<ChatMessageInfo @message={{this.message}} />`); await render(template);
assert.strictEqual( assert
query(".chat-message-info__flag > .svg-icon-title").title, .dom(".chat-message-info__flag > .svg-icon-title")
I18n.t("chat.you_flagged") .hasAttribute("title", I18n.t("chat.you_flagged"));
);
this.message = ChatMessage.create(fabricators.channel(), {
user: { username: "discobot" },
reviewable_id: 1,
}); });
await render(hbs`<ChatMessageInfo @message={{this.message}} />`); test("reviewable", async function (assert) {
this.message = fabricators.message({
user: { username: "discobot" },
user_flag_status: 0,
});
assert.strictEqual( await render(template);
query(".chat-message-info__flag a .svg-icon-title").title,
I18n.t("chat.flagged") assert
); .dom(".chat-message-info__flag > .svg-icon-title")
.hasAttribute("title", I18n.t("chat.you_flagged"));
}); });
test("with username classes", async function (assert) { test("with username classes", async function (assert) {
this.message = ChatMessage.create(fabricators.channel(), { this.message = fabricators.message({
user: { user: {
username: "discobot", username: "discobot",
admin: true, admin: true,
@ -129,7 +131,7 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
}, },
}); });
await render(hbs`<ChatMessageInfo @message={{this.message}} />`); await render(template);
assert.dom(".chat-message-info__username.is-staff").exists(); assert.dom(".chat-message-info__username.is-staff").exists();
assert.dom(".chat-message-info__username.is-admin").exists(); assert.dom(".chat-message-info__username.is-admin").exists();
@ -139,11 +141,11 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
}); });
test("without username classes", async function (assert) { test("without username classes", async function (assert) {
this.message = ChatMessage.create(fabricators.channel(), { this.message = fabricators.message({
user: { username: "discobot" }, user: { username: "discobot" },
}); });
await render(hbs`<ChatMessageInfo @message={{this.message}} />`); await render(template);
assert.dom(".chat-message-info__username.is-staff").doesNotExist(); assert.dom(".chat-message-info__username.is-staff").doesNotExist();
assert.dom(".chat-message-info__username.is-admin").doesNotExist(); assert.dom(".chat-message-info__username.is-admin").doesNotExist();

View File

@ -0,0 +1,54 @@
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";
import I18n from "I18n";
module(
"Discourse Chat | Component | Chat::Message::LeftGutter",
function (hooks) {
setupRenderingTest(hooks);
const template = hbs`
<Chat::Message::LeftGutter @message={{this.message}} />
`;
test("default", async function (assert) {
this.message = fabricators.message();
await render(template);
assert.dom(".chat-message-left-gutter__date").exists();
});
test("with reviewable", async function (assert) {
this.message = fabricators.message({ reviewable_id: 1 });
await render(template);
assert
.dom(".chat-message-left-gutter__flag .svg-icon-title")
.hasAttribute("title", I18n.t("chat.flagged"));
});
test("with flag status", async function (assert) {
this.message = fabricators.message({ user_flag_status: 0 });
await render(template);
assert
.dom(".chat-message-left-gutter__flag .svg-icon-title")
.hasAttribute("title", I18n.t("chat.you_flagged"));
});
test("bookmark", async function (assert) {
this.message = fabricators.message({ bookmark: fabricators.bookmark() });
await render(template);
assert.dom(".chat-message-left-gutter__date").exists();
assert.dom(".chat-message-left-gutter__bookmark").exists();
});
}
);

View File

@ -0,0 +1,102 @@
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();
});
}
);

View File

@ -9,11 +9,7 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
const template = hbs` const template = hbs`
<ChatMessage <ChatMessage @message={{this.message}} />
@message={{this.message}}
@messageDidEnterViewport={{fn (noop)}}
@messageDidLeaveViewport={{fn (noop)}}
/>
`; `;
test("Message with edits", async function (assert) { test("Message with edits", async function (assert) {
@ -31,7 +27,7 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
await render(template); await render(template);
assert.true( assert.true(
exists(".chat-message-deleted .chat-message-expand"), exists(".chat-message-text.-deleted .chat-message-expand"),
"has the correct css class and expand button within" "has the correct css class and expand button within"
); );
}); });
@ -41,7 +37,7 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
await render(template); await render(template);
assert.true( assert.true(
exists(".chat-message-hidden .chat-message-expand"), exists(".chat-message-text.-hidden .chat-message-expand"),
"has the correct css class and expand button within" "has the correct css class and expand button within"
); );
}); });