DEV: various improvements to devex on chat (#21612)

- Improves styleguide support
- Adds toggle color scheme to styleguide
- Adds properties mutators to styleguide
- Attempts to quit a session as soon as done with it in system specs, this should at least free resources faster
- Refactors fabricators to simplify them
- Adds more fabricators (uploads for example)
- Starts implementing components pattern in system specs
- Uses Chat::Message creator to create messages in system specs, this should help to have more real specs as the side effects should now happen
This commit is contained in:
Joffrey JAFFEUX 2023-05-17 17:49:52 +02:00 committed by GitHub
parent 4d0b997559
commit 60c67afba4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 1002 additions and 260 deletions

View File

@ -41,7 +41,6 @@
{{#each @channel.messages key="id" as |message|}} {{#each @channel.messages key="id" as |message|}}
<ChatMessage <ChatMessage
@message={{message}} @message={{message}}
@channel={{@channel}}
@resendStagedMessage={{this.resendStagedMessage}} @resendStagedMessage={{this.resendStagedMessage}}
@messageDidEnterViewport={{this.messageDidEnterViewport}} @messageDidEnterViewport={{this.messageDidEnterViewport}}
@messageDidLeaveViewport={{this.messageDidLeaveViewport}} @messageDidLeaveViewport={{this.messageDidLeaveViewport}}

View File

@ -113,11 +113,10 @@ export default class ChatLivePane extends Component {
if (this._loadedChannelId !== this.args.channel?.id) { if (this._loadedChannelId !== this.args.channel?.id) {
this.unsubscribeToUpdates(this._loadedChannelId); this.unsubscribeToUpdates(this._loadedChannelId);
this.chatChannelPane.selectingMessages = false; this.chatChannelPane.selectingMessages = false;
this.chatChannelComposer.message =
this.args.channel.draft || if (this.args.channel.draft) {
ChatMessage.createDraftMessage(this.args.channel, { this.chatChannelComposer.message = this.args.channel.draft;
user: this.currentUser, }
});
this._loadedChannelId = this.args.channel?.id; this._loadedChannelId = this.args.channel?.id;
} }

View File

@ -1,6 +1,6 @@
<div class="chat-composer-message-details"> <div class="chat-composer-message-details" data-id={{@message.id}}>
<div class="chat-reply"> <div class="chat-reply">
{{d-icon @icon}} {{d-icon (if @message.editing "pencil-alt" "reply")}}
<ChatUserAvatar @user={{@message.user}} /> <ChatUserAvatar @user={{@message.user}} />
<span class="chat-reply__username">{{@message.user.username}}</span> <span class="chat-reply__username">{{@message.user.username}}</span>
<span class="chat-reply__excerpt"> <span class="chat-reply__excerpt">

View File

@ -9,7 +9,6 @@
this.currentMessage this.currentMessage
this.currentMessage.inReplyTo this.currentMessage.inReplyTo
}} }}
@icon={{if this.currentMessage.editing "pencil-alt" "reply"}}
@cancelAction={{this.onCancel}} @cancelAction={{this.onCancel}}
/> />
{{/if}} {{/if}}
@ -26,7 +25,7 @@
}} }}
{{did-update this.didUpdateMessage this.currentMessage}} {{did-update this.didUpdateMessage this.currentMessage}}
{{did-update this.didUpdateInReplyTo this.currentMessage.inReplyTo}} {{did-update this.didUpdateInReplyTo this.currentMessage.inReplyTo}}
{{did-insert this.setupAppEvents}} {{did-insert this.setup}}
{{will-destroy this.teardown}} {{will-destroy this.teardown}}
{{will-destroy this.cancelPersistDraft}} {{will-destroy this.cancelPersistDraft}}
> >
@ -71,7 +70,7 @@
{{on "click" this.onSend}} {{on "click" this.onSend}}
@icon="paper-plane" @icon="paper-plane"
class="chat-composer__send-btn" class="chat-composer__send-btn"
title="chat.composer.send" title={{i18n "chat.composer.send"}}
disabled={{or this.disabled (not this.sendEnabled)}} disabled={{or this.disabled (not this.sendEnabled)}}
tabindex={{if this.sendEnabled 0 -1}} tabindex={{if this.sendEnabled 0 -1}}
{{on "focus" (fn this.computeIsFocused true)}} {{on "focus" (fn this.computeIsFocused true)}}

View File

@ -141,7 +141,10 @@ export default class ChatComposer extends Component {
} }
@action @action
setupAppEvents() { setup() {
this.composer.message = ChatMessage.createDraftMessage(this.args.channel, {
user: this.currentUser,
});
this.appEvents.on("chat:modify-selection", this, "modifySelection"); this.appEvents.on("chat:modify-selection", this, "modifySelection");
this.appEvents.on( this.appEvents.on(
"chat:open-insert-link-modal", "chat:open-insert-link-modal",

View File

@ -54,6 +54,10 @@ export default class ChatMessageActionsDesktop extends Component {
this.context this.context
); );
if (!messageContainer) {
return;
}
const viewport = messageContainer.closest(".popper-viewport"); const viewport = messageContainer.closest(".popper-viewport");
this.size = this.size =
viewport.clientWidth < REDUCED_WIDTH_THRESHOLD ? REDUCED : FULL; viewport.clientWidth < REDUCED_WIDTH_THRESHOLD ? REDUCED : FULL;

View File

@ -130,7 +130,7 @@ export default class ChatMessage extends Component {
} }
_chatMessageDecorators.forEach((decorator) => { _chatMessageDecorators.forEach((decorator) => {
decorator.call(this, this.messageContainer, this.args.channel); decorator.call(this, this.messageContainer, this.args.message.channel);
}); });
}); });
} }
@ -147,7 +147,7 @@ export default class ChatMessage extends Component {
!this.args.message?.deletedAt || !this.args.message?.deletedAt ||
this.currentUser.id === this.args.message?.user?.id || this.currentUser.id === this.args.message?.user?.id ||
this.currentUser.staff || this.currentUser.staff ||
this.args.channel?.canModerate this.args.message?.channel?.canModerate
); );
} }
@ -316,7 +316,10 @@ export default class ChatMessage extends Component {
} }
get threadingEnabled() { get threadingEnabled() {
return this.args.channel?.threadingEnabled && !!this.args.message?.thread; return (
this.args.message?.channel?.threadingEnabled &&
!!this.args.message?.thread
);
} }
get showThreadIndicator() { get showThreadIndicator() {

View File

@ -34,7 +34,6 @@
{{#each this.thread.messages key="id" as |message|}} {{#each this.thread.messages key="id" as |message|}}
<ChatMessage <ChatMessage
@message={{message}} @message={{message}}
@channel={{this.channel}}
@resendStagedMessage={{this.resendStagedMessage}} @resendStagedMessage={{this.resendStagedMessage}}
@messageDidEnterViewport={{this.messageDidEnterViewport}} @messageDidEnterViewport={{this.messageDidEnterViewport}}
@messageDidLeaveViewport={{this.messageDidLeaveViewport}} @messageDidLeaveViewport={{this.messageDidLeaveViewport}}

View File

@ -20,10 +20,7 @@
/> />
</div> </div>
</div> </div>
<Chat::Thread::OriginalMessage <Chat::Thread::OriginalMessage @message={{@thread.originalMessage}} />
@thread={{@thread}}
@message={{@thread.originalMessage}}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,15 @@
<StyleguideExample @title="<ChatComposerMessageDetails>">
<Styleguide::Component>
<ChatComposerMessageDetails @message={{this.message}} />
</Styleguide::Component>
<Styleguide::Controls>
<Styleguide::Controls::Row @name="Mode">
{{#if this.message.editing}}
<DButton @action={{this.toggleMode}} @translatedLabel="Reply" />
{{else}}
<DButton @action={{this.toggleMode}} @translatedLabel="Editing" />
{{/if}}
</Styleguide::Controls::Row>
</Styleguide::Controls>
</StyleguideExample>

View File

@ -0,0 +1,27 @@
import Component from "@glimmer/component";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { action } from "@ember/object";
import { cached } from "@glimmer/tracking";
import { inject as service } from "@ember/service";
export default class ChatStyleguideChatComposerMessageDetails extends Component {
@service site;
@service session;
@service keyValueStore;
@cached
get message() {
return fabricators.message();
}
@action
toggleMode() {
if (this.message.editing) {
this.message.editing = false;
this.message.inReplyTo = fabricators.message();
} else {
this.message.editing = true;
this.message.inReplyTo = null;
}
}
}

View File

@ -0,0 +1,23 @@
<StyleguideExample @title="<ChatComposer>">
<Styleguide::Component>
<Chat::Composer::Channel
@channel={{this.channel}}
@onSendMessage={{this.onSendMessage}}
/>
</Styleguide::Component>
<Styleguide::Controls>
<Styleguide::Controls::Row @name="Disabled">
<DToggleSwitch
@state={{this.channel.isReadOnly}}
{{on "click" this.toggleDisabled}}
/>
</Styleguide::Controls::Row>
<Styleguide::Controls::Row @name="Sending">
<DToggleSwitch
@state={{this.chatChannelPane.sending}}
{{on "click" this.toggleSending}}
/>
</Styleguide::Controls::Row>
</Styleguide::Controls>
</StyleguideExample>

View File

@ -0,0 +1,30 @@
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";
import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel";
export default class ChatStyleguideChatComposer extends Component {
@service chatChannelComposer;
@service chatChannelPane;
channel = fabricators.channel();
@action
toggleDisabled() {
if (this.channel.status === CHANNEL_STATUSES.open) {
this.channel.status = CHANNEL_STATUSES.readOnly;
} else {
this.channel.status = CHANNEL_STATUSES.open;
}
}
@action
toggleSending() {
this.chatChannelPane.sending = !this.chatChannelPane.sending;
}
@action
onSendMessage() {
this.chatChannelComposer.reset();
}
}

View File

@ -0,0 +1,54 @@
<StyleguideExample @title="<ChatMessage>">
<Styleguide::Component>
<ChatMessage
@message={{this.message}}
@context="channel"
@messageDidEnterViewport={{(noop)}}
@messageDidLeaveViewport={{(noop)}}
/>
</Styleguide::Component>
<Styleguide::Controls>
<Styleguide::Controls::Row @name="Last Visit">
<DToggleSwitch
@state={{this.message.newest}}
{{on "click" this.toggleLastVisit}}
/>
</Styleguide::Controls::Row>
<Styleguide::Controls::Row @name="Deleted">
<DToggleSwitch
@state={{not (not this.message.deletedAt)}}
{{on "click" this.toggleDeleted}}
/>
</Styleguide::Controls::Row>
<Styleguide::Controls::Row @name="Bookmark">
<DToggleSwitch
@state={{not (not this.message.bookmark)}}
{{on "click" this.toggleBookmarked}}
/>
</Styleguide::Controls::Row>
<Styleguide::Controls::Row @name="Thread">
<DToggleSwitch
@state={{not (not this.message.thread)}}
{{on "click" this.toggleThread}}
/>
</Styleguide::Controls::Row>
<Styleguide::Controls::Row @name="Reactions">
<DToggleSwitch
@state={{not (not this.message.reactions)}}
{{on "click" this.toggleReaction}}
/>
</Styleguide::Controls::Row>
<Styleguide::Controls::Row @name="Upload">
<DToggleSwitch
@state={{not (not this.message.uploads)}}
{{on "click" this.toggleUpload}}
/>
</Styleguide::Controls::Row>
<Styleguide::Controls::Row @name="Message">
<textarea
{{on "input" this.updateMessage}}
>{{this.message.message}}</textarea>
</Styleguide::Controls::Row>
</Styleguide::Controls>
</StyleguideExample>

View File

@ -0,0 +1,86 @@
import Component from "@glimmer/component";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { action } from "@ember/object";
import ChatMessagesManager from "discourse/plugins/chat/discourse/lib/chat-messages-manager";
import { getOwner } from "discourse-common/lib/get-owner";
export default class ChatStyleguideChatMessage extends Component {
manager = new ChatMessagesManager(getOwner(this));
message = fabricators.message();
@action
toggleDeleted() {
if (this.message.deletedAt) {
this.message.deletedAt = null;
} else {
this.message.deletedAt = moment();
}
}
@action
toggleBookmarked() {
if (this.message.bookmark) {
this.message.bookmark = null;
} else {
this.message.bookmark = fabricators.bookmark();
}
}
@action
toggleHighlighted() {
this.message.highlighted = !this.message.highlighted;
}
@action
toggleEdited() {
this.message.edited = !this.message.edited;
}
@action
toggleLastVisit() {
this.message.newest = !this.message.newest;
}
@action
toggleThread() {
if (this.message.thread) {
this.message.channel.threadingEnabled = false;
this.message.thread = null;
this.message.threadReplyCount = 0;
} else {
this.message.thread = fabricators.thread({
channel: this.message.channel,
});
this.message.threadReplyCount = 1;
this.message.channel.threadingEnabled = true;
}
}
@action
updateMessage(event) {
this.message.message = event.target.value;
this.message.cook();
}
@action
toggleReaction() {
if (this.message.reactions?.length) {
this.message.reactions = [];
} else {
this.message.reactions = [
fabricators.reaction({ emoji: "heart" }),
fabricators.reaction({ emoji: "rocket", reacted: true }),
];
}
}
@action
toggleUpload() {
if (this.message.uploads?.length) {
this.message.uploads = [];
} else {
this.message.uploads = [fabricators.upload(), fabricators.upload()];
}
}
}

View File

@ -0,0 +1,5 @@
<StyleguideExample @title="<ChatThreadOriginalMessage>">
<Styleguide::Component>
<Chat::Thread::OriginalMessage @message={{this.message}} />
</Styleguide::Component>
</StyleguideExample>

View File

@ -0,0 +1,6 @@
import Component from "@glimmer/component";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
export default class ChatStyleguideChatThreadOriginalMessage extends Component {
message = fabricators.message();
}

View File

@ -0,0 +1,170 @@
/*
Fabricators are used to create fake data for testing purposes.
The following fabricators are available in lib folder to allow
styleguide to use them, and eventually to generate dummy data
in a placeholder component. It should not be used for any other case.
*/
import ChatChannel, {
CHANNEL_STATUSES,
CHATABLE_TYPES,
} from "discourse/plugins/chat/discourse/models/chat-channel";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread";
import ChatDirectMessage from "discourse/plugins/chat/discourse/models/chat-direct-message";
import ChatMessageReaction from "discourse/plugins/chat/discourse/models/chat-message-reaction";
import User from "discourse/models/user";
import Bookmark from "discourse/models/bookmark";
import Category from "discourse/models/category";
let sequence = 0;
function messageFabricator(args = {}) {
const channel = args.channel || channelFabricator();
const message = ChatMessage.create(
channel,
Object.assign(
{
id: args.id || sequence++,
user: args.user || userFabricator(),
message:
args.message ||
"@discobot **abc**defghijklmnopqrstuvwxyz [discourse](discourse.org) :rocket: ",
created_at: args.created_at || moment(),
},
args
)
);
const excerptLength = 50;
const text = message.message.toString();
if (text.length <= excerptLength) {
message.excerpt = text;
} else {
message.excerpt = text.slice(0, excerptLength) + "...";
}
message.cook();
return message;
}
function channelFabricator(args = {}) {
const id = args.id || sequence++;
return ChatChannel.create(
Object.assign(
{
id,
chatable_type:
args.chatable?.type ||
args.chatable_type ||
CHATABLE_TYPES.categoryChannel,
last_message_sent_at: args.last_message_sent_at,
chatable_id: args.chatable?.id || args.chatable_id,
title: args.title || "General",
description: args.description,
chatable: args.chatable || categoryFabricator(),
status: CHANNEL_STATUSES.open,
},
args
)
);
}
function categoryFabricator(args = {}) {
return Category.create({
id: args.id || sequence++,
color: args.color || "D56353",
read_restricted: false,
name: args.name || "General",
slug: args.slug || "general",
});
}
function directMessageFabricator(args = {}) {
return ChatDirectMessage.create({
id: args.id || sequence++,
users: args.users || [userFabricator(), userFabricator()],
});
}
function directMessageChannelFabricator(args = {}) {
const directMessage =
args.chatable ||
directMessageFabricator({
id: args.chatable_id || sequence++,
});
return channelFabricator(
Object.assign(args, {
chatable_type: CHATABLE_TYPES.directMessageChannel,
chatable_id: directMessage.id,
chatable: directMessage,
})
);
}
function userFabricator(args = {}) {
return User.create({
id: args.id || sequence++,
username: args.username || "hawk",
name: args.name,
avatar_template: "/letter_avatar_proxy/v3/letter/t/41988e/{size}.png",
});
}
function bookmarkFabricator(args = {}) {
return Bookmark.create({
id: args.id || sequence++,
});
}
function threadFabricator(args = {}) {
const channel = args.channel || channelFabricator();
return ChatThread.create(channel, {
id: args.id || sequence++,
original_message: args.original_message || messageFabricator({ channel }),
});
}
function reactionFabricator(args = {}) {
return ChatMessageReaction.create({
count: args.count || 1,
users: args.users || [userFabricator()],
emoji: args.emoji || "heart",
reacted: args.reacted || false,
});
}
function uploadFabricator() {
return {
extension: "jpeg",
filesize: 126177,
height: 800,
human_filesize: "123 KB",
id: 202,
original_filename: "avatar.PNG.jpg",
retain_hours: null,
short_path: "/images/avatar.png",
short_url: "upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg",
thumbnail_height: 320,
thumbnail_width: 690,
url: "/images/avatar.png",
width: 1920,
};
}
export default {
bookmark: bookmarkFabricator,
user: userFabricator,
channel: channelFabricator,
directMessageChannel: directMessageChannelFabricator,
message: messageFabricator,
thread: threadFabricator,
reaction: reactionFabricator,
upload: uploadFabricator,
category: categoryFabricator,
directMessage: directMessageFabricator,
};

View File

@ -89,6 +89,7 @@ export default class ChatChannel {
@tracked membershipsCount = 0; @tracked membershipsCount = 0;
@tracked archive; @tracked archive;
@tracked tracking; @tracked tracking;
@tracked threadingEnabled = false;
threadsManager = new ChatThreadsManager(getOwner(this)); threadsManager = new ChatThreadsManager(getOwner(this));
messagesManager = new ChatMessagesManager(getOwner(this)); messagesManager = new ChatMessagesManager(getOwner(this));
@ -114,7 +115,10 @@ export default class ChatChannel {
this.autoJoinUsers = args.auto_join_users; this.autoJoinUsers = args.auto_join_users;
this.allowChannelWideMentions = args.allow_channel_wide_mentions; this.allowChannelWideMentions = args.allow_channel_wide_mentions;
this.chatable = this.isDirectMessageChannel this.chatable = this.isDirectMessageChannel
? ChatDirectMessage.create(args) ? ChatDirectMessage.create({
id: args.chatable?.id,
users: args.chatable?.users,
})
: Category.create(args.chatable); : Category.create(args.chatable);
this.currentUserMembership = UserChatChannelMembership.create( this.currentUserMembership = UserChatChannelMembership.create(
args.current_user_membership args.current_user_membership

View File

@ -1,5 +1,6 @@
import User from "discourse/models/user"; import User from "discourse/models/user";
import { tracked } from "@glimmer/tracking"; import { tracked } from "@glimmer/tracking";
import { CHATABLE_TYPES } from "discourse/plugins/chat/discourse/models/chat-channel";
export default class ChatDirectMessage { export default class ChatDirectMessage {
static create(args = {}) { static create(args = {}) {
@ -9,9 +10,11 @@ export default class ChatDirectMessage {
@tracked id; @tracked id;
@tracked users = null; @tracked users = null;
type = CHATABLE_TYPES.drectMessageChannel;
constructor(args = {}) { constructor(args = {}) {
this.id = args.chatable.id; this.id = args.id;
this.users = this.#initUsers(args.chatable.users || []); this.users = this.#initUsers(args.users || []);
} }
#initUsers(users) { #initUsers(users) {

View File

@ -12,9 +12,9 @@ export default class ChatMessageReaction {
@tracked count = 0; @tracked count = 0;
@tracked reacted = false; @tracked reacted = false;
@tracked users = []; @tracked users = [];
@tracked emoji;
constructor(args = {}) { constructor(args = {}) {
this.messageId = args.messageId;
this.count = args.count; this.count = args.count;
this.emoji = args.emoji; this.emoji = args.emoji;
this.users = this.#initUsersModels(args.users); this.users = this.#initUsersModels(args.users);

View File

@ -7,7 +7,7 @@ import I18n from "I18n";
import { generateCookFunction } from "discourse/lib/text"; import { generateCookFunction } from "discourse/lib/text";
import simpleCategoryHashMentionTransform from "discourse/plugins/chat/discourse/lib/simple-category-hash-mention-transform"; import simpleCategoryHashMentionTransform from "discourse/plugins/chat/discourse/lib/simple-category-hash-mention-transform";
import { getOwner } from "discourse-common/lib/get-owner"; import { getOwner } from "discourse-common/lib/get-owner";
import { next } from "@ember/runloop";
export default class ChatMessage { export default class ChatMessage {
static cookFunction = null; static cookFunction = null;
@ -38,9 +38,10 @@ export default class ChatMessage {
@tracked expanded; @tracked expanded;
@tracked bookmark; @tracked bookmark;
@tracked userFlagStatus; @tracked userFlagStatus;
@tracked hidden; @tracked hidden = false;
@tracked version = 0; @tracked version = 0;
@tracked edited; @tracked edited = false;
@tracked editing = false;
@tracked chatWebhookEvent = new TrackedObject(); @tracked chatWebhookEvent = new TrackedObject();
@tracked mentionWarning; @tracked mentionWarning;
@tracked availableFlags; @tracked availableFlags;
@ -62,6 +63,7 @@ export default class ChatMessage {
this.firstOfResults = args.firstOfResults; this.firstOfResults = args.firstOfResults;
this.staged = args.staged; this.staged = args.staged;
this.edited = args.edited; this.edited = args.edited;
this.editing = args.editing;
this.availableFlags = args.availableFlags || args.available_flags; this.availableFlags = args.availableFlags || args.available_flags;
this.hidden = args.hidden; this.hidden = args.hidden;
this.threadReplyCount = args.threadReplyCount || args.thread_reply_count; this.threadReplyCount = args.threadReplyCount || args.thread_reply_count;
@ -82,10 +84,7 @@ export default class ChatMessage {
? ChatMessage.create(channel, args.in_reply_to || args.replyToMsg) ? ChatMessage.create(channel, args.in_reply_to || args.replyToMsg)
: null); : null);
this.channel = channel; this.channel = channel;
this.reactions = this.#initChatMessageReactionModel( this.reactions = this.#initChatMessageReactionModel(args.reactions);
args.id,
args.reactions
);
this.uploads = new TrackedArray(args.uploads || []); this.uploads = new TrackedArray(args.uploads || []);
this.user = this.#initUserModel(args.user); this.user = this.#initUserModel(args.user);
this.bookmark = args.bookmark ? Bookmark.create(args.bookmark) : null; this.bookmark = args.bookmark ? Bookmark.create(args.bookmark) : null;
@ -138,33 +137,35 @@ export default class ChatMessage {
} }
cook() { cook() {
const site = getOwner(this).lookup("service:site"); next(() => {
const site = getOwner(this).lookup("service:site");
const markdownOptions = { const markdownOptions = {
featuresOverride: featuresOverride:
site.markdown_additional_options?.chat?.limited_pretty_text_features, site.markdown_additional_options?.chat?.limited_pretty_text_features,
markdownItRules: markdownItRules:
site.markdown_additional_options?.chat site.markdown_additional_options?.chat
?.limited_pretty_text_markdown_rules, ?.limited_pretty_text_markdown_rules,
hashtagTypesInPriorityOrder: hashtagTypesInPriorityOrder:
site.hashtag_configurations?.["chat-composer"], site.hashtag_configurations?.["chat-composer"],
hashtagIcons: site.hashtag_icons, hashtagIcons: site.hashtag_icons,
}; };
if (ChatMessage.cookFunction) {
this.cooked = ChatMessage.cookFunction(this.message);
} else {
generateCookFunction(markdownOptions).then((cookFunction) => {
ChatMessage.cookFunction = (raw) => {
return simpleCategoryHashMentionTransform(
cookFunction(raw),
site.categories
);
};
if (ChatMessage.cookFunction) {
this.cooked = ChatMessage.cookFunction(this.message); this.cooked = ChatMessage.cookFunction(this.message);
}); } else {
} generateCookFunction(markdownOptions).then((cookFunction) => {
ChatMessage.cookFunction = (raw) => {
return simpleCategoryHashMentionTransform(
cookFunction(raw),
site.categories
);
};
this.cooked = ChatMessage.cookFunction(this.message);
});
}
});
} }
get read() { get read() {
@ -306,10 +307,8 @@ export default class ChatMessage {
} }
} }
#initChatMessageReactionModel(messageId, reactions = []) { #initChatMessageReactionModel(reactions = []) {
return reactions.map((reaction) => return reactions.map((reaction) => ChatMessageReaction.create(reaction));
ChatMessageReaction.create(Object.assign({ messageId }, reaction))
);
} }
#initUserModel(user) { #initUserModel(user) {

View File

@ -13,6 +13,10 @@ export const THREAD_STATUSES = {
}; };
export default class ChatThread { export default class ChatThread {
static create(channel, args = {}) {
return new ChatThread(channel, args);
}
@tracked id; @tracked id;
@tracked title; @tracked title;
@tracked status; @tracked status;

View File

@ -4,7 +4,6 @@ import ChatComposer from "./chat-composer";
export default class ChatChannelComposer extends ChatComposer { export default class ChatChannelComposer extends ChatComposer {
@service chat; @service chat;
@service chatChannelThreadComposer;
@service router; @service router;
@action @action

View File

@ -0,0 +1,4 @@
<Styleguide::ChatMessage />
<Styleguide::ChatComposer />
<Styleguide::ChatThreadOriginalMessage />
<Styleguide::ChatComposerMessageDetails />

View File

@ -641,3 +641,8 @@ en:
chat_notifications_with_unread: chat_notifications_with_unread:
one: "Chat notifications - %{count} unread notification" one: "Chat notifications - %{count} unread notification"
other: "Chat notifications - %{count} unread notifications" other: "Chat notifications - %{count} unread notifications"
styleguide:
sections:
chat:
title: Chat

View File

@ -49,13 +49,29 @@ Fabricator(:direct_message_channel, from: :chat_channel) do
end end
end end
Fabricator(:chat_message, class_name: "Chat::Message") do Fabricator(:chat_message, class_name: "Chat::MessageCreator") do
chat_channel transient :chat_channel
user transient :user
message "Beep boop" transient :message
cooked { |attrs| Chat::Message.cook(attrs[:message]) } transient :in_reply_to
cooked_version Chat::Message::BAKED_VERSION transient :thread
in_reply_to nil transient :upload_ids
initialize_with do |transients|
user = transients[:user] || Fabricate(:user)
channel =
transients[:chat_channel] || transients[:thread]&.channel ||
transients[:in_reply_to]&.chat_channel || Fabricate(:chat_channel)
resolved_class.create(
chat_channel: channel,
user: user,
content: transients[:message] || Faker::Lorem.paragraph,
thread_id: transients[:thread]&.id,
in_reply_to_id: transients[:in_reply_to]&.id,
upload_ids: transients[:upload_ids],
).chat_message
end
end end
Fabricator(:chat_mention, class_name: "Chat::Mention") do Fabricator(:chat_mention, class_name: "Chat::Mention") do

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
RSpec.describe "Chat | composer | shortcuts | thread", type: :system, js: true do
fab!(:channel_1) { Fabricate(:chat_channel, threading_enabled: true) }
fab!(:current_user) { Fabricate(:user) }
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1) }
let(:chat_page) { PageObjects::Pages::Chat.new }
let(:thread_page) { PageObjects::Pages::ChatThread.new }
before do
SiteSetting.enable_experimental_chat_threaded_discussions = true
chat_system_bootstrap
channel_1.add(current_user)
sign_in(current_user)
end
describe "ArrowUp" do
let(:thread_1) { message_1.reload.thread }
context "when there are editable messages" do
let(:last_thread_message) { thread_1.replies.last }
before do
thread_message_1 = Fabricate(:chat_message, user: current_user, in_reply_to: message_1)
Fabricate(:chat_message, user: current_user, thread: thread_message_1.reload.thread)
end
it "starts editing the last editable message" do
chat_page.visit_thread(thread_1)
thread_page.composer.edit_last_message_shortcut
expect(thread_page.composer_message_details).to have_message(last_thread_message)
expect(thread_page.composer.value).to eq(last_thread_message.message)
end
end
context "when there are no editable messages" do
before { Fabricate(:chat_message, in_reply_to: message_1) }
it "does nothing" do
chat_page.visit_thread(thread_1)
thread_page.composer.edit_last_message_shortcut
expect(thread_page.composer_message_details).to have_no_message
expect(thread_page.composer.value).to be_blank
end
end
end
end

View File

@ -45,9 +45,15 @@ RSpec.describe "Chat channel", type: :system, js: true do
chat.visit_channel(channel_1) chat.visit_channel(channel_1)
end end
using_session(:tab_1) { channel.send_message("test_message") } using_session(:tab_1) do |session|
channel.send_message("test_message")
session.quit
end
using_session(:tab_2) { expect(channel).to have_message(text: "test_message") } using_session(:tab_2) do |session|
expect(channel).to have_message(text: "test_message")
session.quit
end
end end
end end

View File

@ -23,11 +23,12 @@ RSpec.describe "Edited message", type: :system, js: true do
it "shows as edited for all users" do it "shows as edited for all users" do
chat_page.visit_channel(channel_1) chat_page.visit_channel(channel_1)
using_session(:user_1) do using_session(:user_1) do |session|
sign_in(editing_user) sign_in(editing_user)
chat_page.visit_channel(channel_1) chat_page.visit_channel(channel_1)
channel_page.edit_message(message_1, "a different message") channel_page.edit_message(message_1, "a different message")
expect(page).to have_content(I18n.t("js.chat.edited")) expect(page).to have_content(I18n.t("js.chat.edited"))
session.quit
end end
expect(page).to have_content(I18n.t("js.chat.edited")) expect(page).to have_content(I18n.t("js.chat.edited"))

View File

@ -35,7 +35,10 @@ RSpec.describe "Message notifications - mobile", type: :system, js: true, mobile
Jobs.run_immediately! Jobs.run_immediately!
visit("/chat") visit("/chat")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) } using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator") expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator")
expect(page).to have_no_css( expect(page).to have_no_css(
@ -62,7 +65,10 @@ RSpec.describe "Message notifications - mobile", type: :system, js: true, mobile
Jobs.run_immediately! Jobs.run_immediately!
visit("/chat") visit("/chat")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) } using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_css(".do-not-disturb-background") expect(page).to have_css(".do-not-disturb-background")
expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator") expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator")
@ -77,7 +83,10 @@ RSpec.describe "Message notifications - mobile", type: :system, js: true, mobile
Jobs.run_immediately! Jobs.run_immediately!
visit("/chat") visit("/chat")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) } using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator") expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator")
expect(page).to have_no_css( expect(page).to have_no_css(
@ -92,7 +101,10 @@ RSpec.describe "Message notifications - mobile", type: :system, js: true, mobile
Jobs.run_immediately! Jobs.run_immediately!
visit("/chat") visit("/chat")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) } using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "") expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "")
expect(page).to have_css( expect(page).to have_css(
@ -138,14 +150,20 @@ RSpec.describe "Message notifications - mobile", type: :system, js: true, mobile
Jobs.run_immediately! Jobs.run_immediately!
visit("/chat") visit("/chat")
using_session(:user_1) { create_message(channel: dm_channel_1, creator: user_1) } using_session(:user_1) do |session|
create_message(channel: dm_channel_1, creator: user_1)
session.quit
end
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "1") expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "1")
expect(page).to have_css( expect(page).to have_css(
".chat-channel-row[data-chat-channel-id=\"#{dm_channel_1.id}\"] .chat-channel-unread-indicator", ".chat-channel-row[data-chat-channel-id=\"#{dm_channel_1.id}\"] .chat-channel-unread-indicator",
) )
using_session(:user_1) { create_message(channel: dm_channel_1, creator: user_1) } using_session(:user_1) do |session|
create_message(channel: dm_channel_1, creator: user_1)
session.quit
end
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "2") expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "2")
end end
@ -162,7 +180,10 @@ RSpec.describe "Message notifications - mobile", type: :system, js: true, mobile
".chat-channel-row:nth-child(2)[data-chat-channel-id=\"#{dm_channel_2.id}\"]", ".chat-channel-row:nth-child(2)[data-chat-channel-id=\"#{dm_channel_2.id}\"]",
) )
using_session(:user_1) { create_message(channel: dm_channel_2, creator: user_2) } using_session(:user_1) do |session|
create_message(channel: dm_channel_2, creator: user_2)
session.quit
end
expect(page).to have_css( expect(page).to have_css(
".chat-channel-row:nth-child(1)[data-chat-channel-id=\"#{dm_channel_2.id}\"]", ".chat-channel-row:nth-child(1)[data-chat-channel-id=\"#{dm_channel_2.id}\"]",
@ -190,14 +211,20 @@ RSpec.describe "Message notifications - mobile", type: :system, js: true, mobile
Jobs.run_immediately! Jobs.run_immediately!
visit("/chat") visit("/chat")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) } using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "") expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "")
expect(page).to have_css( expect(page).to have_css(
".chat-channel-row[data-chat-channel-id=\"#{channel_1.id}\"] .chat-channel-unread-indicator", ".chat-channel-row[data-chat-channel-id=\"#{channel_1.id}\"] .chat-channel-unread-indicator",
) )
using_session(:user_1) { create_message(channel: dm_channel_1, creator: user_1) } using_session(:user_1) do |session|
create_message(channel: dm_channel_1, creator: user_1)
session.quit
end
expect(page).to have_css( expect(page).to have_css(
".chat-channel-row[data-chat-channel-id=\"#{dm_channel_1.id}\"] .chat-channel-unread-indicator", ".chat-channel-row[data-chat-channel-id=\"#{dm_channel_1.id}\"] .chat-channel-unread-indicator",

View File

@ -34,7 +34,10 @@ RSpec.describe "Message notifications - with sidebar", type: :system, js: true d
context "when a message is created" do context "when a message is created" do
it "doesn't show anything" do it "doesn't show anything" do
visit("/") visit("/")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) } using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator") expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator")
expect(page).to have_no_css(".sidebar-row.channel-#{channel_1.id}") expect(page).to have_no_css(".sidebar-row.channel-#{channel_1.id}")
@ -59,7 +62,10 @@ RSpec.describe "Message notifications - with sidebar", type: :system, js: true d
Jobs.run_immediately! Jobs.run_immediately!
visit("/") visit("/")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) } using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_css(".do-not-disturb-background") expect(page).to have_css(".do-not-disturb-background")
expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator") expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator")
@ -72,7 +78,10 @@ RSpec.describe "Message notifications - with sidebar", type: :system, js: true d
context "when a message is created" do context "when a message is created" do
it "doesn't show anything" do it "doesn't show anything" do
visit("/") visit("/")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) } using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator") expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator")
expect(page).to have_no_css(".sidebar-row.channel-#{channel_1.id} .unread") expect(page).to have_no_css(".sidebar-row.channel-#{channel_1.id} .unread")
@ -91,7 +100,10 @@ RSpec.describe "Message notifications - with sidebar", type: :system, js: true d
context "when a message is created" do context "when a message is created" do
it "doesn't show any indicator on chat-header-icon" do it "doesn't show any indicator on chat-header-icon" do
visit("/") visit("/")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) } using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator") expect(page).to have_no_css(".chat-header-icon .chat-channel-unread-indicator")
end end
@ -109,7 +121,10 @@ RSpec.describe "Message notifications - with sidebar", type: :system, js: true d
context "when a message is created" do context "when a message is created" do
it "doesn't show any indicator on chat-header-icon" do it "doesn't show any indicator on chat-header-icon" do
visit("/") visit("/")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) } using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_no_css( expect(page).to have_no_css(
".chat-header-icon .chat-channel-unread-indicator.urgent", ".chat-header-icon .chat-channel-unread-indicator.urgent",
@ -137,7 +152,10 @@ RSpec.describe "Message notifications - with sidebar", type: :system, js: true d
context "when a message is created" do context "when a message is created" do
it "correctly renders notifications" do it "correctly renders notifications" do
visit("/") visit("/")
using_session(:user_1) { create_message(channel: channel_1, creator: user_1) } using_session(:user_1) do |session|
create_message(channel: channel_1, creator: user_1)
session.quit
end
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "") expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "")
expect(page).to have_css(".sidebar-row.channel-#{channel_1.id} .unread") expect(page).to have_css(".sidebar-row.channel-#{channel_1.id} .unread")
@ -178,12 +196,18 @@ RSpec.describe "Message notifications - with sidebar", type: :system, js: true d
context "when a message is created" do context "when a message is created" do
it "correctly renders notifications" do it "correctly renders notifications" do
visit("/") visit("/")
using_session(:user_1) { create_message(channel: dm_channel_1, creator: user_1) } using_session(:user_1) do |session|
create_message(channel: dm_channel_1, creator: user_1)
session.quit
end
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "1") expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "1")
expect(page).to have_css(".sidebar-row.channel-#{dm_channel_1.id} .icon.urgent") expect(page).to have_css(".sidebar-row.channel-#{dm_channel_1.id} .icon.urgent")
using_session(:user_1) { create_message(channel: dm_channel_1, creator: user_1) } using_session(:user_1) do |session|
create_message(channel: dm_channel_1, creator: user_1)
session.quit
end
expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "2") expect(page).to have_css(".chat-header-icon .chat-channel-unread-indicator", text: "2")
end end
@ -198,7 +222,10 @@ RSpec.describe "Message notifications - with sidebar", type: :system, js: true d
"#sidebar-section-content-chat-dms .sidebar-section-link-wrapper:nth-child(2) .channel-#{dm_channel_2.id}", "#sidebar-section-content-chat-dms .sidebar-section-link-wrapper:nth-child(2) .channel-#{dm_channel_2.id}",
) )
using_session(:user_1) { create_message(channel: dm_channel_2, creator: user_2) } using_session(:user_1) do |session|
create_message(channel: dm_channel_2, creator: user_2)
session.quit
end
expect(page).to have_css( expect(page).to have_css(
"#sidebar-section-content-chat-dms .sidebar-section-link-wrapper:nth-child(1) .channel-#{dm_channel_2.id}", "#sidebar-section-content-chat-dms .sidebar-section-link-wrapper:nth-child(1) .channel-#{dm_channel_2.id}",
@ -237,12 +264,13 @@ RSpec.describe "Message notifications - with sidebar", type: :system, js: true d
session.quit session.quit
end end
using_session(:current_user) do using_session(:current_user) do |session|
expect(page).to have_css(".sidebar-row.channel-#{dm_channel_1.id} .icon.urgent") expect(page).to have_css(".sidebar-row.channel-#{dm_channel_1.id} .icon.urgent")
expect(page).to have_css( expect(page).to have_css(
".chat-header-icon .chat-channel-unread-indicator", ".chat-header-icon .chat-channel-unread-indicator",
text: "1", text: "1",
) )
session.quit
end end
end end
end end

View File

@ -17,8 +17,8 @@ module PageObjects
visit("/chat") visit("/chat")
end end
def visit_channel(channel, mobile: false) def visit_channel(channel)
visit(channel.url + (mobile ? "?mobile_view=1" : "")) visit(channel.url)
has_no_css?(".chat-channel--not-loaded-once") has_no_css?(".chat-channel--not-loaded-once")
has_no_css?(".chat-skeleton") has_no_css?(".chat-skeleton")
end end

View File

@ -3,6 +3,15 @@
module PageObjects module PageObjects
module Pages module Pages
class ChatThread < PageObjects::Pages::Base class ChatThread < PageObjects::Pages::Base
def composer
@composer ||= PageObjects::Components::Chat::Composer.new(".chat-thread")
end
def composer_message_details
@composer_message_details ||=
PageObjects::Components::Chat::ComposerMessageDetails.new(".chat-thread")
end
def header def header
find(".chat-thread__header") find(".chat-thread__header")
end end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
module PageObjects
module Components
module Chat
class Composer < PageObjects::Components::Base
attr_reader :context
SELECTOR = ".chat-composer__wrapper"
def initialize(context)
@context = context
end
def input
find(context).find(SELECTOR).find(".chat-composer__input")
end
def value
input.value
end
def reply_to_last_message_shortcut
input.send_keys(%i[shift arrow_up])
end
def edit_last_message_shortcut
input.send_keys(%i[arrow_up])
end
end
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module PageObjects
module Components
module Chat
class ComposerMessageDetails < PageObjects::Components::Base
attr_reader :context
SELECTOR = ".chat-composer-message-details"
def initialize(context)
@context = context
end
def has_message?(message)
find(context).find(SELECTOR + "[data-id=\"#{message.id}\"]")
end
def has_no_message?
find(context).has_no_css?(SELECTOR)
end
end
end
end
end

View File

@ -87,7 +87,7 @@ RSpec.describe "Reply to message - channel - drawer", type: :system, js: true do
expect(page).to have_selector( expect(page).to have_selector(
".chat-channel .chat-reply__excerpt", ".chat-channel .chat-reply__excerpt",
text: original_message.message, text: original_message.excerpt,
) )
channel_page.fill_composer("reply to message") channel_page.fill_composer("reply to message")

View File

@ -91,7 +91,7 @@ RSpec.describe "Reply to message - channel - full page", type: :system, js: true
expect(page).to have_selector( expect(page).to have_selector(
".chat-channel .chat-reply__excerpt", ".chat-channel .chat-reply__excerpt",
text: original_message.message, text: original_message.excerpt,
) )
channel_page.fill_composer("reply to message") channel_page.fill_composer("reply to message")

View File

@ -97,7 +97,7 @@ RSpec.describe "Reply to message - channel - mobile", type: :system, js: true, m
expect(page).to have_selector( expect(page).to have_selector(
".chat-channel .chat-reply__excerpt", ".chat-channel .chat-reply__excerpt",
text: original_message.message, text: original_message.excerpt,
) )
channel_page.fill_composer("reply to message") channel_page.fill_composer("reply to message")

View File

@ -161,15 +161,17 @@ describe "Single thread in side panel", type: :system, js: true do
expect(thread_page).to have_message(thread_id: thread.id, text: "the other user message") expect(thread_page).to have_message(thread_id: thread.id, text: "the other user message")
end end
using_session(:tab_1) do using_session(:tab_1) do |session|
expect(side_panel).to have_open_thread(thread) expect(side_panel).to have_open_thread(thread)
expect(thread_page).to have_message(thread_id: thread.id, text: "the other user message") expect(thread_page).to have_message(thread_id: thread.id, text: "the other user message")
thread_page.send_message("this is a test message") thread_page.send_message("this is a test message")
expect(thread_page).to have_message(thread_id: thread.id, text: "this is a test message") expect(thread_page).to have_message(thread_id: thread.id, text: "this is a test message")
session.quit
end end
using_session(:tab_2) do using_session(:tab_2) do |session|
expect(thread_page).to have_message(thread_id: thread.id, text: "this is a test message") expect(thread_page).to have_message(thread_id: thread.id, text: "this is a test message")
session.quit
end end
end end

View File

@ -22,12 +22,13 @@ RSpec.describe "Unfollow dm channel", type: :system, js: true do
expect(page).to have_no_css(".channel-#{dm_channel_1.id}") expect(page).to have_no_css(".channel-#{dm_channel_1.id}")
using_session(:user_1) do using_session(:user_1) do |session|
text = "this is fine" text = "this is fine"
sign_in(other_user) sign_in(other_user)
chat_page.visit_channel(dm_channel_1) chat_page.visit_channel(dm_channel_1)
chat_channel_page.send_message(text) chat_channel_page.send_message(text)
expect(chat_channel_page).to have_message(text: text) expect(chat_channel_page).to have_message(text: text)
session.quit
end end
expect(page).to have_css(".channel-#{dm_channel_1.id} .urgent") expect(page).to have_css(".channel-#{dm_channel_1.id} .urgent")

View File

@ -198,13 +198,14 @@ RSpec.describe "User menu notifications | sidebar", type: :system, js: true do
channel.send_message("this is fine @#{other_user.username}") channel.send_message("this is fine @#{other_user.username}")
find(".invite-link", wait: 5).click find(".invite-link", wait: 5).click
using_session(:user_1) do using_session(:user_1) do |session|
sign_in(other_user) sign_in(other_user)
visit("/") visit("/")
find(".header-dropdown-toggle.current-user").click find(".header-dropdown-toggle.current-user").click
expect(find("#user-menu-button-chat-notifications")).to have_content(1) expect(find("#user-menu-button-chat-notifications")).to have_content(1)
expect(find("#quick-access-all-notifications")).to have_css(".chat-invitation.unread") expect(find("#quick-access-all-notifications")).to have_css(".chat-invitation.unread")
session.quit
end end
end end
end end

View File

@ -1,5 +1,5 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import fabricators from "../helpers/fabricators"; import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { query } from "discourse/tests/helpers/qunit-helpers"; import { query } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile"; import hbs from "htmlbars-inline-precompile";
import { render } from "@ember/test-helpers"; import { render } from "@ember/test-helpers";
@ -13,7 +13,7 @@ module(
test("channel title is escaped in instructions correctly", async function (assert) { test("channel title is escaped in instructions correctly", async function (assert) {
this.set( this.set(
"channel", "channel",
fabricators.chatChannel({ fabricators.channel({
title: `<script>someeviltitle</script>`, title: `<script>someeviltitle</script>`,
}) })
); );

View File

@ -1,7 +1,7 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { exists, query } from "discourse/tests/helpers/qunit-helpers"; import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile"; import hbs from "htmlbars-inline-precompile";
import fabricators from "../helpers/fabricators"; import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { render } from "@ember/test-helpers"; import { render } from "@ember/test-helpers";
import { module, test } from "qunit"; import { module, test } from "qunit";
import I18n from "I18n"; import I18n from "I18n";
@ -10,7 +10,7 @@ module("Discourse Chat | Component | chat-channel-card", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
hooks.beforeEach(function () { hooks.beforeEach(function () {
this.channel = fabricators.chatChannel(); this.channel = fabricators.channel();
this.channel.description = this.channel.description =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."; "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
}); });

View File

@ -1,5 +1,5 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import fabricators from "../helpers/fabricators"; import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { query } from "discourse/tests/helpers/qunit-helpers"; import { query } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile"; import hbs from "htmlbars-inline-precompile";
import { render } from "@ember/test-helpers"; import { render } from "@ember/test-helpers";
@ -13,7 +13,7 @@ module(
test("channel title is escaped in instructions correctly", async function (assert) { test("channel title is escaped in instructions correctly", async function (assert) {
this.set( this.set(
"channel", "channel",
fabricators.chatChannel({ fabricators.channel({
title: `<script>someeviltitle</script>`, title: `<script>someeviltitle</script>`,
}) })
); );

View File

@ -5,7 +5,7 @@ import hbs from "htmlbars-inline-precompile";
import pretender from "discourse/tests/helpers/create-pretender"; import pretender from "discourse/tests/helpers/create-pretender";
import I18n from "I18n"; import I18n from "I18n";
import { module, test } from "qunit"; import { module, test } from "qunit";
import fabricators from "../helpers/fabricators"; import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
module("Discourse Chat | Component | chat-channel-leave-btn", function (hooks) { module("Discourse Chat | Component | chat-channel-leave-btn", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
@ -13,7 +13,7 @@ module("Discourse Chat | Component | chat-channel-leave-btn", function (hooks) {
test("accepts an optional onLeaveChannel callback", async function (assert) { test("accepts an optional onLeaveChannel callback", async function (assert) {
this.foo = 1; this.foo = 1;
this.onLeaveChannel = () => (this.foo = 2); this.onLeaveChannel = () => (this.foo = 2);
this.channel = fabricators.directMessageChatChannel({ users: [{ id: 1 }] }); this.channel = fabricators.directMessageChannel();
await render( await render(
hbs`<ChatChannelLeaveBtn @channel={{this.channel}} @onLeaveChannel={{this.onLeaveChannel}} />` hbs`<ChatChannelLeaveBtn @channel={{this.channel}} @onLeaveChannel={{this.onLeaveChannel}} />`
@ -29,7 +29,7 @@ module("Discourse Chat | Component | chat-channel-leave-btn", function (hooks) {
}); });
test("has a specific title for direct message channel", async function (assert) { test("has a specific title for direct message channel", async function (assert) {
this.channel = fabricators.directMessageChatChannel(); this.channel = fabricators.directMessageChannel();
await render(hbs`<ChatChannelLeaveBtn @channel={{this.channel}} />`); await render(hbs`<ChatChannelLeaveBtn @channel={{this.channel}} />`);
@ -38,7 +38,7 @@ module("Discourse Chat | Component | chat-channel-leave-btn", function (hooks) {
}); });
test("has a specific title for message channel", async function (assert) { test("has a specific title for message channel", async function (assert) {
this.channel = fabricators.chatChannel(); this.channel = fabricators.channel();
await render(hbs`<ChatChannelLeaveBtn @channel={{this.channel}} />`); await render(hbs`<ChatChannelLeaveBtn @channel={{this.channel}} />`);
@ -48,7 +48,7 @@ module("Discourse Chat | Component | chat-channel-leave-btn", function (hooks) {
test("is not visible on mobile", async function (assert) { test("is not visible on mobile", async function (assert) {
this.site.mobileView = true; this.site.mobileView = true;
this.channel = fabricators.chatChannel(); this.channel = fabricators.channel();
await render(hbs`<ChatChannelLeaveBtn @channel={{this.channel}} />`); await render(hbs`<ChatChannelLeaveBtn @channel={{this.channel}} />`);

View File

@ -1,7 +1,7 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { exists } from "discourse/tests/helpers/qunit-helpers"; import { exists } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile"; import hbs from "htmlbars-inline-precompile";
import fabricators from "../helpers/fabricators"; import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { module, test } from "qunit"; import { module, test } from "qunit";
import { render } from "@ember/test-helpers"; import { render } from "@ember/test-helpers";
@ -10,7 +10,7 @@ module("Discourse Chat | Component | chat-channel-metadata", function (hooks) {
test("displays last message sent at", async function (assert) { test("displays last message sent at", async function (assert) {
let lastMessageSentAt = moment().subtract(1, "day").format(); let lastMessageSentAt = moment().subtract(1, "day").format();
this.channel = fabricators.directMessageChatChannel({ this.channel = fabricators.directMessageChannel({
last_message_sent_at: lastMessageSentAt, last_message_sent_at: lastMessageSentAt,
}); });
@ -28,7 +28,7 @@ module("Discourse Chat | Component | chat-channel-metadata", function (hooks) {
}); });
test("unreadIndicator", async function (assert) { test("unreadIndicator", async function (assert) {
this.channel = fabricators.directMessageChatChannel(); this.channel = fabricators.directMessageChannel();
this.channel.tracking.unreadCount = 1; this.channel.tracking.unreadCount = 1;
this.unreadIndicator = true; this.unreadIndicator = true;

View File

@ -3,7 +3,7 @@ import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile"; import hbs from "htmlbars-inline-precompile";
import { render } from "@ember/test-helpers"; import { render } from "@ember/test-helpers";
import { module, test } from "qunit"; import { module, test } from "qunit";
import fabricators from "../helpers/fabricators"; import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
module( module(
"Discourse Chat | Component | chat-channel-preview-card", "Discourse Chat | Component | chat-channel-preview-card",
@ -11,10 +11,7 @@ module(
setupRenderingTest(hooks); setupRenderingTest(hooks);
hooks.beforeEach(function () { hooks.beforeEach(function () {
this.set( this.set("channel", fabricators.channel({ chatable_type: "Category" }));
"channel",
fabricators.chatChannel({ chatable_type: "Category" })
);
this.channel.description = "Important stuff is announced here."; this.channel.description = "Important stuff is announced here.";
this.channel.title = "announcements"; this.channel.title = "announcements";

View File

@ -2,14 +2,14 @@ import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { render } from "@ember/test-helpers"; import { render } from "@ember/test-helpers";
import { hbs } from "ember-cli-htmlbars"; import { hbs } from "ember-cli-htmlbars";
import fabricators from "../helpers/fabricators"; import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
module("Discourse Chat | Component | chat-channel-row", function (hooks) { module("Discourse Chat | Component | chat-channel-row", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
hooks.beforeEach(function () { hooks.beforeEach(function () {
this.categoryChatChannel = fabricators.chatChannel(); this.categoryChatChannel = fabricators.channel();
this.directMessageChatChannel = fabricators.directMessageChatChannel(); this.directMessageChannel = fabricators.directMessageChannel();
}); });
test("links to correct channel", async function (assert) { test("links to correct channel", async function (assert) {
@ -47,11 +47,14 @@ module("Discourse Chat | Component | chat-channel-row", function (hooks) {
}); });
test("renders correct channel metadata", async function (assert) { test("renders correct channel metadata", async function (assert) {
this.categoryChatChannel.lastMessageSentAt = moment().toISOString();
await render(hbs`<ChatChannelRow @channel={{this.categoryChatChannel}} />`); await render(hbs`<ChatChannelRow @channel={{this.categoryChatChannel}} />`);
assert assert
.dom(".chat-channel-metadata") .dom(".chat-channel-metadata")
.hasText(moment(this.categoryChatChannel.lastMessageSentAt).format("l")); .hasText(
moment(this.categoryChatChannel.lastMessageSentAt).format("h:mm A")
);
}); });
test("renders membership toggling button when necessary", async function (assert) { test("renders membership toggling button when necessary", async function (assert) {
@ -145,11 +148,14 @@ module("Discourse Chat | Component | chat-channel-row", function (hooks) {
}); });
test("user status with direct message channel", async function (assert) { test("user status with direct message channel", async function (assert) {
this.directMessageChannel.chatable = fabricators.directMessage({
users: [fabricators.user()],
});
const status = { description: "Off to dentist", emoji: "tooth" }; const status = { description: "Off to dentist", emoji: "tooth" };
this.directMessageChatChannel.chatable.users[0].status = status; this.directMessageChannel.chatable.users[0].status = status;
await render( await render(
hbs`<ChatChannelRow @channel={{this.directMessageChatChannel}} />` hbs`<ChatChannelRow @channel={{this.directMessageChannel}} />`
); );
assert.dom(".user-status-message").exists(); assert.dom(".user-status-message").exists();
@ -157,9 +163,9 @@ module("Discourse Chat | Component | chat-channel-row", function (hooks) {
test("user status with direct message channel and multiple users", async function (assert) { test("user status with direct message channel and multiple users", async function (assert) {
const status = { description: "Off to dentist", emoji: "tooth" }; const status = { description: "Off to dentist", emoji: "tooth" };
this.directMessageChatChannel.chatable.users[0].status = status; this.directMessageChannel.chatable.users[0].status = status;
this.directMessageChatChannel.chatable.users.push({ this.directMessageChannel.chatable.users.push({
id: 2, id: 2,
username: "bill", username: "bill",
name: null, name: null,
@ -167,7 +173,7 @@ module("Discourse Chat | Component | chat-channel-row", function (hooks) {
}); });
await render( await render(
hbs`<ChatChannelRow @channel={{this.directMessageChatChannel}} />` hbs`<ChatChannelRow @channel={{this.directMessageChannel}} />`
); );
assert.dom(".user-status-message").doesNotExist(); assert.dom(".user-status-message").doesNotExist();

View File

@ -3,7 +3,7 @@ import hbs from "htmlbars-inline-precompile";
import I18n from "I18n"; import I18n from "I18n";
import { module, test } from "qunit"; import { module, test } from "qunit";
import { render } from "@ember/test-helpers"; import { render } from "@ember/test-helpers";
import fabricators from "../helpers/fabricators"; import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { import {
CHANNEL_STATUSES, CHANNEL_STATUSES,
channelStatusIcon, channelStatusIcon,
@ -13,7 +13,7 @@ module("Discourse Chat | Component | chat-channel-status", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
test("renders nothing when channel is opened", async function (assert) { test("renders nothing when channel is opened", async function (assert) {
this.channel = fabricators.chatChannel(); this.channel = fabricators.channel();
await render(hbs`<ChatChannelStatus @channel={{this.channel}} />`); await render(hbs`<ChatChannelStatus @channel={{this.channel}} />`);
@ -21,7 +21,7 @@ module("Discourse Chat | Component | chat-channel-status", function (hooks) {
}); });
test("defaults to long format", async function (assert) { test("defaults to long format", async function (assert) {
this.channel = fabricators.chatChannel({ status: CHANNEL_STATUSES.closed }); this.channel = fabricators.channel({ status: CHANNEL_STATUSES.closed });
await render(hbs`<ChatChannelStatus @channel={{this.channel}} />`); await render(hbs`<ChatChannelStatus @channel={{this.channel}} />`);
@ -31,7 +31,7 @@ module("Discourse Chat | Component | chat-channel-status", function (hooks) {
}); });
test("accepts a format argument", async function (assert) { test("accepts a format argument", async function (assert) {
this.channel = fabricators.chatChannel({ this.channel = fabricators.channel({
status: CHANNEL_STATUSES.archived, status: CHANNEL_STATUSES.archived,
}); });
@ -45,7 +45,7 @@ module("Discourse Chat | Component | chat-channel-status", function (hooks) {
}); });
test("renders the correct icon", async function (assert) { test("renders the correct icon", async function (assert) {
this.channel = fabricators.chatChannel({ this.channel = fabricators.channel({
status: CHANNEL_STATUSES.archived, status: CHANNEL_STATUSES.archived,
}); });
@ -56,7 +56,7 @@ module("Discourse Chat | Component | chat-channel-status", function (hooks) {
test("renders archive status", async function (assert) { test("renders archive status", async function (assert) {
this.currentUser.admin = true; this.currentUser.admin = true;
this.channel = fabricators.chatChannel({ this.channel = fabricators.channel({
status: CHANNEL_STATUSES.archived, status: CHANNEL_STATUSES.archived,
archive_failed: true, archive_failed: true,
}); });

View File

@ -1,7 +1,7 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { exists, query } from "discourse/tests/helpers/qunit-helpers"; import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile"; import hbs from "htmlbars-inline-precompile";
import fabricators from "../helpers/fabricators"; import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { CHATABLE_TYPES } from "discourse/plugins/chat/discourse/models/chat-channel"; import { CHATABLE_TYPES } from "discourse/plugins/chat/discourse/models/chat-channel";
import { module, test } from "qunit"; import { module, test } from "qunit";
import { render } from "@ember/test-helpers"; import { render } from "@ember/test-helpers";
@ -10,7 +10,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
test("category channel", async function (assert) { test("category channel", async function (assert) {
this.channel = fabricators.chatChannel({ this.channel = fabricators.channel({
chatable_type: CHATABLE_TYPES.categoryChannel, chatable_type: CHATABLE_TYPES.categoryChannel,
}); });
@ -27,7 +27,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
}); });
test("category channel - escapes title", async function (assert) { test("category channel - escapes title", async function (assert) {
this.channel = fabricators.chatChannel({ this.channel = fabricators.channel({
chatable_type: CHATABLE_TYPES.categoryChannel, chatable_type: CHATABLE_TYPES.categoryChannel,
title: "<div class='xss'>evil</div>", title: "<div class='xss'>evil</div>",
}); });
@ -38,7 +38,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
}); });
test("category channel - read restricted", async function (assert) { test("category channel - read restricted", async function (assert) {
this.channel = fabricators.chatChannel({ this.channel = fabricators.channel({
chatable_type: CHATABLE_TYPES.categoryChannel, chatable_type: CHATABLE_TYPES.categoryChannel,
chatable: { read_restricted: true }, chatable: { read_restricted: true },
}); });
@ -49,7 +49,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
}); });
test("category channel - not read restricted", async function (assert) { test("category channel - not read restricted", async function (assert) {
this.channel = fabricators.chatChannel({ this.channel = fabricators.channel({
chatable_type: CHATABLE_TYPES.categoryChannel, chatable_type: CHATABLE_TYPES.categoryChannel,
chatable: { read_restricted: false }, chatable: { read_restricted: false },
}); });
@ -60,7 +60,11 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
}); });
test("direct message channel - one user", async function (assert) { test("direct message channel - one user", async function (assert) {
this.channel = fabricators.directMessageChatChannel(); this.channel = fabricators.directMessageChannel({
chatable: fabricators.directMessage({
users: [fabricators.user()],
}),
});
await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`); await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`);
@ -77,7 +81,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
}); });
test("direct message channel - multiple users", async function (assert) { test("direct message channel - multiple users", async function (assert) {
const channel = fabricators.directMessageChatChannel(); const channel = fabricators.directMessageChannel();
channel.chatable.users.push({ channel.chatable.users.push({
id: 2, id: 2,

View File

@ -0,0 +1,77 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import hbs from "htmlbars-inline-precompile";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { module, test } from "qunit";
import { render } from "@ember/test-helpers";
module(
"Discourse Chat | Component | chat-composer-message-details",
function (hooks) {
setupRenderingTest(hooks);
test("data-id attribute", async function (assert) {
this.message = fabricators.message();
await render(
hbs`<ChatComposerMessageDetails @message={{this.message}} />`
);
assert
.dom(".chat-composer-message-details")
.hasAttribute("data-id", this.message.id.toString());
});
test("editing a message has the pencil icon", async function (assert) {
this.message = fabricators.message({ editing: true });
await render(
hbs`<ChatComposerMessageDetails @message={{this.message}} />`
);
assert.dom(".chat-composer-message-details .d-icon-pencil-alt").exists();
});
test("replying to a message has the reply icon", async function (assert) {
const firstMessage = fabricators.message();
this.message = fabricators.message({ inReplyTo: firstMessage });
await render(
hbs`<ChatComposerMessageDetails @message={{this.message}} />`
);
assert.dom(".chat-composer-message-details .d-icon-reply").exists();
});
test("displays user avatar", async function (assert) {
this.message = fabricators.message();
await render(
hbs`<ChatComposerMessageDetails @message={{this.message}} />`
);
assert
.dom(".chat-composer-message-details .chat-user-avatar .avatar")
.hasAttribute("title", this.message.user.username);
});
test("displays message excerpt", async function (assert) {
this.message = fabricators.message();
await render(
hbs`<ChatComposerMessageDetails @message={{this.message}} />`
);
assert.dom(".chat-reply__excerpt").hasText(this.message.excerpt);
});
test("displays users username", async function (assert) {
this.message = fabricators.message();
await render(
hbs`<ChatComposerMessageDetails @message={{this.message}} />`
);
assert.dom(".chat-reply__username").hasText(this.message.user.username);
});
}
);

View File

@ -4,13 +4,13 @@ import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import { module, test } from "qunit"; import { module, test } from "qunit";
import { render } from "@ember/test-helpers"; import { render } from "@ember/test-helpers";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message"; import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import fabricators from "../helpers/fabricators"; import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
module("Discourse Chat | Component | chat-message-avatar", function (hooks) { module("Discourse Chat | Component | chat-message-avatar", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
test("chat_webhook_event", async function (assert) { test("chat_webhook_event", async function (assert) {
this.message = ChatMessage.create(fabricators.chatChannel(), { this.message = ChatMessage.create(fabricators.channel(), {
chat_webhook_event: { emoji: ":heart:" }, chat_webhook_event: { emoji: ":heart:" },
}); });
@ -20,7 +20,7 @@ module("Discourse Chat | Component | chat-message-avatar", function (hooks) {
}); });
test("user", async function (assert) { test("user", async function (assert) {
this.message = ChatMessage.create(fabricators.chatChannel(), { this.message = ChatMessage.create(fabricators.channel(), {
user: { username: "discobot" }, user: { username: "discobot" },
}); });

View File

@ -6,13 +6,13 @@ import I18n from "I18n";
import { module, test } from "qunit"; import { module, test } from "qunit";
import { render } from "@ember/test-helpers"; import { render } from "@ember/test-helpers";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message"; import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import fabricators from "../helpers/fabricators"; 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);
test("chat_webhook_event", async function (assert) { test("chat_webhook_event", async function (assert) {
this.message = ChatMessage.create(fabricators.chatChannel(), { this.message = ChatMessage.create(fabricators.channel(), {
chat_webhook_event: { username: "discobot" }, chat_webhook_event: { username: "discobot" },
}); });
@ -29,7 +29,7 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
}); });
test("user", async function (assert) { test("user", async function (assert) {
this.message = ChatMessage.create(fabricators.chatChannel(), { this.message = ChatMessage.create(fabricators.channel(), {
user: { username: "discobot" }, user: { username: "discobot" },
}); });
@ -42,7 +42,7 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
}); });
test("date", async function (assert) { test("date", async function (assert) {
this.message = ChatMessage.create(fabricators.chatChannel(), { this.message = ChatMessage.create(fabricators.channel(), {
user: { username: "discobot" }, user: { username: "discobot" },
created_at: moment(), created_at: moment(),
}); });
@ -53,7 +53,7 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
}); });
test("bookmark (with reminder)", async function (assert) { test("bookmark (with reminder)", async function (assert) {
this.message = ChatMessage.create(fabricators.chatChannel(), { this.message = ChatMessage.create(fabricators.channel(), {
user: { username: "discobot" }, user: { username: "discobot" },
bookmark: Bookmark.create({ bookmark: Bookmark.create({
reminder_at: moment(), reminder_at: moment(),
@ -69,7 +69,7 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
}); });
test("bookmark (no reminder)", async function (assert) { test("bookmark (no reminder)", async function (assert) {
this.message = ChatMessage.create(fabricators.chatChannel(), { this.message = ChatMessage.create(fabricators.channel(), {
user: { username: "discobot" }, user: { username: "discobot" },
bookmark: Bookmark.create({ bookmark: Bookmark.create({
name: "some name", name: "some name",
@ -83,7 +83,7 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
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.chatChannel(), { this.message = ChatMessage.create(fabricators.channel(), {
user: { status }, user: { status },
}); });
@ -93,7 +93,7 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
}); });
test("reviewable", async function (assert) { test("reviewable", async function (assert) {
this.message = ChatMessage.create(fabricators.chatChannel(), { this.message = ChatMessage.create(fabricators.channel(), {
user: { username: "discobot" }, user: { username: "discobot" },
user_flag_status: 0, user_flag_status: 0,
}); });
@ -105,7 +105,7 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
I18n.t("chat.you_flagged") I18n.t("chat.you_flagged")
); );
this.message = ChatMessage.create(fabricators.chatChannel(), { this.message = ChatMessage.create(fabricators.channel(), {
user: { username: "discobot" }, user: { username: "discobot" },
reviewable_id: 1, reviewable_id: 1,
}); });
@ -119,7 +119,7 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
}); });
test("with username classes", async function (assert) { test("with username classes", async function (assert) {
this.message = ChatMessage.create(fabricators.chatChannel(), { this.message = ChatMessage.create(fabricators.channel(), {
user: { user: {
username: "discobot", username: "discobot",
admin: true, admin: true,
@ -139,7 +139,7 @@ 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.chatChannel(), { this.message = ChatMessage.create(fabricators.channel(), {
user: { username: "discobot" }, user: { username: "discobot" },
}); });

View File

@ -1,5 +1,5 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import fabricators from "../helpers/fabricators"; import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { query } from "discourse/tests/helpers/qunit-helpers"; import { query } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile"; import hbs from "htmlbars-inline-precompile";
import { render } from "@ember/test-helpers"; import { render } from "@ember/test-helpers";
@ -13,7 +13,7 @@ module(
test("channel title is escaped in instructions correctly", async function (assert) { test("channel title is escaped in instructions correctly", async function (assert) {
this.set( this.set(
"channel", "channel",
fabricators.chatChannel({ title: "<script>someeviltitle</script>" }) fabricators.channel({ title: "<script>someeviltitle</script>" })
); );
this.set("chat", { publicChannels: [this.channel] }); this.set("chat", { publicChannels: [this.channel] });
this.set("selectedMessageIds", [1]); this.set("selectedMessageIds", [1]);

View File

@ -55,7 +55,6 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
const template = hbs` const template = hbs`
<ChatMessage <ChatMessage
@message={{this.message}} @message={{this.message}}
@channel={{this.channel}}
@messageDidEnterViewport={{this.messageDidEnterViewport}} @messageDidEnterViewport={{this.messageDidEnterViewport}}
@messageDidLeaveViewport={{this.messageDidLeaveViewport}} @messageDidLeaveViewport={{this.messageDidLeaveViewport}}
/> />

View File

@ -1,7 +1,7 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { query } from "discourse/tests/helpers/qunit-helpers"; import { query } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile"; import hbs from "htmlbars-inline-precompile";
import fabricators from "../helpers/fabricators"; import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { module, test } from "qunit"; import { module, test } from "qunit";
import { render } from "@ember/test-helpers"; import { render } from "@ember/test-helpers";
import { import {
@ -87,7 +87,7 @@ module(
}); });
test("displays indicator when 2 or 3 users are replying", async function (assert) { test("displays indicator when 2 or 3 users are replying", async function (assert) {
this.channel = fabricators.chatChannel(); this.channel = fabricators.channel();
await render( await render(
hbs`<ChatReplyingIndicator @presenceChannelName="/chat-reply/1" />` hbs`<ChatReplyingIndicator @presenceChannelName="/chat-reply/1" />`
@ -102,7 +102,7 @@ module(
}); });
test("displays indicator when 3 users are replying", async function (assert) { test("displays indicator when 3 users are replying", async function (assert) {
this.channel = fabricators.chatChannel(); this.channel = fabricators.channel();
await render( await render(
hbs`<ChatReplyingIndicator @presenceChannelName="/chat-reply/1" />` hbs`<ChatReplyingIndicator @presenceChannelName="/chat-reply/1" />`
@ -118,7 +118,7 @@ module(
}); });
test("displays indicator when more than 3 users are replying", async function (assert) { test("displays indicator when more than 3 users are replying", async function (assert) {
this.channel = fabricators.chatChannel(); this.channel = fabricators.channel();
await render( await render(
hbs`<ChatReplyingIndicator @presenceChannelName="/chat-reply/1" />` hbs`<ChatReplyingIndicator @presenceChannelName="/chat-reply/1" />`
@ -135,7 +135,7 @@ module(
}); });
test("filters current user from list of repliers", async function (assert) { test("filters current user from list of repliers", async function (assert) {
this.channel = fabricators.chatChannel(); this.channel = fabricators.channel();
await render( await render(
hbs`<ChatReplyingIndicator @presenceChannelName="/chat-reply/1" />` hbs`<ChatReplyingIndicator @presenceChannelName="/chat-reply/1" />`

View File

@ -3,7 +3,7 @@ import hbs from "htmlbars-inline-precompile";
import I18n from "I18n"; import I18n from "I18n";
import { module, test } from "qunit"; import { module, test } from "qunit";
import { render } from "@ember/test-helpers"; import { render } from "@ember/test-helpers";
import fabricators from "../helpers/fabricators"; import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
module( module(
"Discourse Chat | Component | chat-retention-reminder-text", "Discourse Chat | Component | chat-retention-reminder-text",
@ -11,7 +11,7 @@ module(
setupRenderingTest(hooks); setupRenderingTest(hooks);
test("when setting is set on 0", async function (assert) { test("when setting is set on 0", async function (assert) {
this.channel = fabricators.chatChannel(); this.channel = fabricators.channel();
this.siteSettings.chat_channel_retention_days = 0; this.siteSettings.chat_channel_retention_days = 0;
await render( await render(
@ -25,7 +25,7 @@ module(
test("when channel is a public channel", async function (assert) { test("when channel is a public channel", async function (assert) {
const count = 10; const count = 10;
this.channel = fabricators.chatChannel(); this.channel = fabricators.channel();
this.siteSettings.chat_channel_retention_days = count; this.siteSettings.chat_channel_retention_days = count;
await render( await render(
@ -39,7 +39,7 @@ module(
test("when channel is a DM channel", async function (assert) { test("when channel is a DM channel", async function (assert) {
const count = 10; const count = 10;
this.channel = fabricators.directMessageChatChannel(); this.channel = fabricators.directMessageChannel();
this.siteSettings.chat_dm_retention_days = count; this.siteSettings.chat_dm_retention_days = count;
await render( await render(

View File

@ -4,7 +4,7 @@ import hbs from "htmlbars-inline-precompile";
import { exists, query } from "discourse/tests/helpers/qunit-helpers"; import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel"; import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
import { Promise } from "rsvp"; import { Promise } from "rsvp";
import fabricators from "../helpers/fabricators"; import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { module, test } from "qunit"; import { module, test } from "qunit";
function mockChat(context, options = {}) { function mockChat(context, options = {}) {
@ -15,7 +15,7 @@ function mockChat(context, options = {}) {
}); });
}; };
mock.getDmChannelForUsernames = () => { mock.getDmChannelForUsernames = () => {
return Promise.resolve({ chat_channel: fabricators.chatChannel() }); return Promise.resolve({ chat_channel: fabricators.channel() });
}; };
return mock; return mock;
} }
@ -114,7 +114,7 @@ module("Discourse Chat | Component | direct-message-creator", function (hooks) {
await fillIn(".filter-usernames", "hawk"); await fillIn(".filter-usernames", "hawk");
assert.strictEqual(query(".filter-usernames").value, "hawk"); assert.strictEqual(query(".filter-usernames").value, "hawk");
this.set("channel", fabricators.chatChannel()); this.set("channel", fabricators.channel());
this.set("channel", ChatChannel.createDirectMessageChannelDraft()); this.set("channel", ChatChannel.createDirectMessageChannelDraft());
assert.strictEqual(query(".filter-usernames").value, ""); assert.strictEqual(query(".filter-usernames").value, "");

View File

@ -1,21 +0,0 @@
import { cloneJSON } from "discourse-common/lib/object";
// heavily inspired by https://github.com/travelperk/fabricator
export function Fabricator(Model, attributes = {}) {
return (opts) => fabricate(Model, attributes, opts);
}
function fabricate(Model, attributes, opts = {}) {
if (typeof attributes === "function") {
return attributes();
}
const extendedModel = cloneJSON({ ...attributes, ...opts });
const props = {};
for (const [key, value] of Object.entries(extendedModel)) {
props[key] = typeof value === "function" ? value() : value;
}
return Model.create(props);
}

View File

@ -1,60 +0,0 @@
import ChatChannel, {
CHATABLE_TYPES,
} from "discourse/plugins/chat/discourse/models/chat-channel";
import EmberObject from "@ember/object";
import { Fabricator } from "./fabricator";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
const userFabricator = Fabricator(EmberObject, {
id: 1,
username: "hawk",
name: null,
avatar_template: "/letter_avatar_proxy/v3/letter/t/41988e/{size}.png",
});
const categoryChatableFabricator = Fabricator(EmberObject, {
id: 1,
color: "D56353",
read_restricted: false,
name: "My category",
});
const directChannelChatableFabricator = Fabricator(EmberObject, {
users: [userFabricator({ id: 1, username: "bob" })],
});
export default {
chatChannel: Fabricator(ChatChannel, {
id: 1,
chatable_type: CHATABLE_TYPES.categoryChannel,
status: "open",
title: "My category title",
name: "My category name",
chatable: categoryChatableFabricator(),
last_message_sent_at: "2021-11-08T21:26:05.710Z",
allow_channel_wide_mentions: true,
message_bus_last_ids: {
new_mentions: 0,
new_messages: 0,
},
}),
chatChannelMessage: Fabricator(ChatMessage, {
id: 1,
chat_channel_id: 1,
user_id: 1,
cooked: "This is a test message",
}),
directMessageChatChannel: Fabricator(ChatChannel, {
id: 1,
chatable_type: CHATABLE_TYPES.directMessageChannel,
status: "open",
chatable: directChannelChatableFabricator(),
last_message_sent_at: "2021-11-08T21:26:05.710Z",
message_bus_last_ids: {
new_mentions: 0,
new_messages: 0,
},
}),
};

View File

@ -3,21 +3,20 @@ import hbs from "htmlbars-inline-precompile";
import { render } from "@ember/test-helpers"; import { render } from "@ember/test-helpers";
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { query } from "discourse/tests/helpers/qunit-helpers"; import { query } from "discourse/tests/helpers/qunit-helpers";
import fabricators from "../../helpers/fabricators"; import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
module("Discourse Chat | Unit | Helpers | format-chat-date", function (hooks) { module("Discourse Chat | Unit | Helpers | format-chat-date", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
test("link to chat message", async function (assert) { test("link to chat message", async function (assert) {
const channel = fabricators.chatChannel(); const channel = fabricators.channel();
this.message = ChatMessage.create(channel, { this.message = fabricators.message({ channel });
id: 1,
chat_channel_id: channel.id,
});
await render(hbs`{{format-chat-date this.message}}`); await render(hbs`{{format-chat-date this.message}}`);
assert.equal(query(".chat-time").getAttribute("href"), "/chat/c/-/1/1"); assert.equal(
query(".chat-time").getAttribute("href"),
`/chat/c/-/${channel.id}/${this.message.id}`
);
}); });
}); });

View File

@ -1,7 +1,7 @@
import { acceptance } from "discourse/tests/helpers/qunit-helpers"; import { acceptance } from "discourse/tests/helpers/qunit-helpers";
import { test } from "qunit"; import { test } from "qunit";
import { set } from "@ember/object"; import { set } from "@ember/object";
import fabricators from "../../helpers/fabricators"; import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
acceptance("Discourse Chat | Unit | Service | chat-guardian", function (needs) { acceptance("Discourse Chat | Unit | Service | chat-guardian", function (needs) {
needs.hooks.beforeEach(function () { needs.hooks.beforeEach(function () {
@ -69,7 +69,7 @@ acceptance("Discourse Chat | Unit | Service | chat-guardian", function (needs) {
}); });
test("#canArchiveChannel", async function (assert) { test("#canArchiveChannel", async function (assert) {
const channel = fabricators.chatChannel(); const channel = fabricators.channel();
set(this.currentUser, "has_chat_enabled", true); set(this.currentUser, "has_chat_enabled", true);
set(this.currentUser, "admin", true); set(this.currentUser, "admin", true);

View File

@ -0,0 +1,3 @@
<div class="component">
{{yield}}
</div>

View File

@ -0,0 +1,3 @@
import Component from "@glimmer/component";
export default class StyleguideComponent extends Component {}

View File

@ -0,0 +1,5 @@
<table class="component-properties">
<tbody>
{{yield}}
</tbody>
</table>

View File

@ -0,0 +1,3 @@
import Component from "@glimmer/component";
export default class StyleguideControls extends Component {}

View File

@ -0,0 +1,6 @@
<tr class="component-properties__row">
<td class="component-properties__cell">{{@name}}</td>
<td class="component-properties__cell">
{{yield}}
</td>
</tr>

View File

@ -0,0 +1 @@
<DToggleSwitch @state={{@enabled}} {{on "click" @action}} />

View File

@ -0,0 +1,3 @@
import Component from "@glimmer/component";
export default class StyleguideControlsToggle extends Component {}

View File

@ -0,0 +1 @@
<DButton @action={{this.toggle}} class="toggle-color-mode">Toggle color</DButton>

View File

@ -0,0 +1,45 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { tracked } from "@glimmer/tracking";
const DARK = "dark";
const LIGHT = "light";
function colorSchemeOverride(type) {
const lightScheme = document.querySelector("link.light-scheme");
const darkScheme = document.querySelector("link.dark-scheme");
if (!lightScheme || !darkScheme) {
return;
}
switch (type) {
case DARK:
lightScheme.media = "none";
darkScheme.media = "all";
break;
case LIGHT:
lightScheme.media = "all";
darkScheme.media = "none";
break;
}
}
export default class ToggleColorMode extends Component {
@service keyValueStore;
@tracked colorSchemeOverride = this.default;
get default() {
return window.matchMedia("(prefers-color-scheme: dark)").matches
? DARK
: LIGHT;
}
@action
toggle() {
this.colorSchemeOverride = this.colorSchemeOverride === DARK ? LIGHT : DARK;
colorSchemeOverride(this.colorSchemeOverride);
}
}

View File

@ -1,5 +1,6 @@
<section class="styleguide"> <section class="styleguide">
<section class="styleguide-menu"> <section class="styleguide-menu">
<ToggleColorMode />
{{#each this.categories as |c|}} {{#each this.categories as |c|}}
<ul> <ul>
<li class="styleguide-heading">{{i18n <li class="styleguide-heading">{{i18n

View File

@ -73,6 +73,36 @@
.rendered { .rendered {
width: 100%; width: 100%;
position: relative;
.component {
padding: 2rem;
border: 2px dotted var(--primary-low);
margin-bottom: 2rem;
}
.component-properties {
width: 100%;
&__cell {
padding: 0.5rem 0;
&:first-child {
width: 30%;
}
textarea,
input {
box-sizing: border-box;
margin: 0;
width: 100%;
}
textarea {
height: 100px;
}
}
}
} }
margin-bottom: 2em; margin-bottom: 2em;

View File

@ -101,7 +101,10 @@ module SystemHelpers
ENV["TZ"] = timezone ENV["TZ"] = timezone
using_session(timezone) { freeze_time(&example) } using_session(timezone) do |session|
freeze_time(&example)
session.quit
end
ENV["TZ"] = previous_browser_timezone ENV["TZ"] = previous_browser_timezone
end end