DEV: rework the chat-live-pane

This PR is introducing glimmer usage in the chat-live-pane, for components but also for models. RestModel usage has been dropped in favor of native classes.

Other changes/additions in this PR:

- sticky dates, scrolling will now keep the date separator of the current section at the top of the screen
- better unread management, marking a channel as unread will correctly mark the correct message and not mark the whole channel as read. Tracking state will also now correctly return unread count and unread mentions.
- adds an animation on bottom arrow
- better scrolling behavior, we should now always correctly keep the scroll position while loading more
- reactions are now more reactive, and will update their tooltip without needed to close/reopen it
- skeleton has been improved with placeholder images and reactions
- when making a reaction on the desktop message actions, the menu won't move anymore
- simplify logic and stop maintaining a list of unloaded messages
This commit is contained in:
Joffrey JAFFEUX
2023-03-02 16:34:25 +01:00
committed by GitHub
parent e206bd8907
commit 67c0498f64
118 changed files with 2550 additions and 2289 deletions

View File

@ -9,16 +9,17 @@ module("Discourse Chat | Component | chat-channel-metadata", function (hooks) {
setupRenderingTest(hooks);
test("displays last message sent at", async function (assert) {
let lastMessageSentAt = moment().subtract(1, "day");
let lastMessageSentAt = moment().subtract(1, "day").format();
this.channel = fabricators.directMessageChatChannel({
last_message_sent_at: lastMessageSentAt,
});
await render(hbs`<ChatChannelMetadata @channel={{this.channel}} />`);
assert.dom(".chat-channel-metadata__date").hasText("Yesterday");
lastMessageSentAt = moment();
this.channel.set("last_message_sent_at", lastMessageSentAt);
this.channel.lastMessageSentAt = lastMessageSentAt;
await render(hbs`<ChatChannelMetadata @channel={{this.channel}} />`);
assert

View File

@ -51,9 +51,7 @@ module("Discourse Chat | Component | chat-channel-row", function (hooks) {
assert
.dom(".chat-channel-metadata")
.hasText(
moment(this.categoryChatChannel.last_message_sent_at).format("l")
);
.hasText(moment(this.categoryChatChannel.lastMessageSentAt).format("l"));
});
test("renders membership toggling button when necessary", async function (assert) {

View File

@ -8,7 +8,6 @@ import {
import hbs from "htmlbars-inline-precompile";
import { click, render, settled, waitFor } from "@ember/test-helpers";
import { module, test } from "qunit";
import { run } from "@ember/runloop";
const fakeUpload = {
type: ".png",
@ -47,12 +46,11 @@ module("Discourse Chat | Component | chat-composer-uploads", function (hooks) {
setupRenderingTest(hooks);
test("loading uploads from an outside source (e.g. draft or editing message)", async function (assert) {
await render(hbs`
<ChatComposerUploads @fileUploadElementId="chat-widget-uploader" />
`);
this.existingUploads = [fakeUpload];
this.appEvents = this.container.lookup("service:appEvents");
this.appEvents.trigger("chat-composer:load-uploads", [fakeUpload]);
await render(hbs`
<ChatComposerUploads @existingUploads={{this.existingUploads}} @fileUploadElementId="chat-widget-uploader" />
`);
await settled();
assert.strictEqual(count(".chat-composer-upload"), 1);
@ -61,10 +59,7 @@ module("Discourse Chat | Component | chat-composer-uploads", function (hooks) {
test("upload starts and completes", async function (assert) {
setupUploadPretender();
this.set("changedUploads", null);
this.set("onUploadChanged", (uploads) => {
this.set("changedUploads", uploads);
});
this.set("onUploadChanged", () => {});
await render(hbs`
<ChatComposerUploads @fileUploadElementId="chat-widget-uploader" @onUploadChanged={{this.onUploadChanged}} />
@ -80,34 +75,31 @@ module("Discourse Chat | Component | chat-composer-uploads", function (hooks) {
done();
}
);
this.appEvents.trigger(
"upload-mixin:chat-composer-uploader:add-files",
createFile("avatar.png")
);
await waitFor(".chat-composer-upload");
assert.strictEqual(count(".chat-composer-upload"), 1);
assert.dom(".chat-composer-upload").exists({ count: 1 });
});
test("removing a completed upload", async function (assert) {
this.set("changedUploads", null);
this.set("onUploadChanged", (uploads) => {
this.set("changedUploads", uploads);
});
this.set("onUploadChanged", () => {});
this.existingUploads = [fakeUpload];
await render(hbs`
<ChatComposerUploads @fileUploadElementId="chat-widget-uploader" @onUploadChanged={{this.onUploadChanged}} />
<ChatComposerUploads @existingUploads={{this.existingUploads}} @fileUploadElementId="chat-widget-uploader" @onUploadChanged={{this.onUploadChanged}} />
`);
this.appEvents = this.container.lookup("service:appEvents");
run(() =>
this.appEvents.trigger("chat-composer:load-uploads", [fakeUpload])
);
assert.strictEqual(count(".chat-composer-upload"), 1);
assert.dom(".chat-composer-upload").exists({ count: 1 });
await click(".remove-upload");
assert.strictEqual(count(".chat-composer-upload"), 0);
assert.dom(".chat-composer-upload").exists({ count: 0 });
});
test("cancelling in progress upload", async function (assert) {

View File

@ -1,44 +0,0 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { exists } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile";
import { module, test } from "qunit";
import fabricators from "../helpers/fabricators";
import { render } from "@ember/test-helpers";
import pretender, { response } from "discourse/tests/helpers/create-pretender";
import MockPresenceChannel from "../helpers/mock-presence-channel";
function mockChat(context) {
const mock = context.container.lookup("service:chat");
mock.draftStore = {};
mock.currentUser = context.currentUser;
mock.presenceChannel = MockPresenceChannel.create();
return mock;
}
module("Discourse Chat | Component | chat-live-pane", function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.set("chat", mockChat(this));
this.set("channel", fabricators.chatChannel());
});
test("Shows skeleton when loading", async function (assert) {
pretender.get(`/chat/chat_channels.json`, () => response(this.channel));
pretender.get(`/chat/:id/messages.json`, () =>
response({ chat_messages: [], meta: { can_delete_self: true } })
);
await render(
hbs`<ChatLivePane @loadingMorePast={{true}} @chat={{this.chat}} @chatChannel={{this.channel}} />`
);
assert.true(exists(".chat-skeleton"));
await render(
hbs`<ChatLivePane @loadingMoreFuture={{true}} @chat={{this.chat}} @chatChannel={{this.channel}} />`
);
assert.true(exists(".chat-skeleton"));
});
});

View File

@ -3,12 +3,16 @@ import hbs from "htmlbars-inline-precompile";
import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import { module, test } from "qunit";
import { render } from "@ember/test-helpers";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import fabricators from "../helpers/fabricators";
module("Discourse Chat | Component | chat-message-avatar", function (hooks) {
setupRenderingTest(hooks);
test("chat_webhook_event", async function (assert) {
this.set("message", { chat_webhook_event: { emoji: ":heart:" } });
this.message = ChatMessage.create(fabricators.chatChannel(), {
chat_webhook_event: { emoji: ":heart:" },
});
await render(hbs`<ChatMessageAvatar @message={{this.message}} />`);
@ -16,7 +20,9 @@ module("Discourse Chat | Component | chat-message-avatar", function (hooks) {
});
test("user", async function (assert) {
this.set("message", { user: { username: "discobot" } });
this.message = ChatMessage.create(fabricators.chatChannel(), {
user: { username: "discobot" },
});
await render(hbs`<ChatMessageAvatar @message={{this.message}} />`);

View File

@ -6,21 +6,21 @@ import I18n from "I18n";
import { module, test } from "qunit";
import { render } from "@ember/test-helpers";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import fabricators from "../helpers/fabricators";
module("Discourse Chat | Component | chat-message-info", function (hooks) {
setupRenderingTest(hooks);
test("chat_webhook_event", async function (assert) {
this.set(
"message",
ChatMessage.create({ chat_webhook_event: { username: "discobot" } })
);
this.message = ChatMessage.create(fabricators.chatChannel(), {
chat_webhook_event: { username: "discobot" },
});
await render(hbs`<ChatMessageInfo @message={{this.message}} />`);
assert.strictEqual(
query(".chat-message-info__username").innerText.trim(),
this.message.chat_webhook_event.username
this.message.chatWebhookEvent.username
);
assert.strictEqual(
query(".chat-message-info__bot-indicator").textContent.trim(),
@ -29,7 +29,9 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("user", async function (assert) {
this.set("message", ChatMessage.create({ user: { username: "discobot" } }));
this.message = ChatMessage.create(fabricators.chatChannel(), {
user: { username: "discobot" },
});
await render(hbs`<ChatMessageInfo @message={{this.message}} />`);
@ -40,13 +42,10 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("date", async function (assert) {
this.set(
"message",
ChatMessage.create({
user: { username: "discobot" },
created_at: moment(),
})
);
this.message = ChatMessage.create(fabricators.chatChannel(), {
user: { username: "discobot" },
created_at: moment(),
});
await render(hbs`<ChatMessageInfo @message={{this.message}} />`);
@ -54,16 +53,13 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("bookmark (with reminder)", async function (assert) {
this.set(
"message",
ChatMessage.create({
user: { username: "discobot" },
bookmark: Bookmark.create({
reminder_at: moment(),
name: "some name",
}),
})
);
this.message = ChatMessage.create(fabricators.chatChannel(), {
user: { username: "discobot" },
bookmark: Bookmark.create({
reminder_at: moment(),
name: "some name",
}),
});
await render(hbs`<ChatMessageInfo @message={{this.message}} />`);
@ -73,15 +69,12 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("bookmark (no reminder)", async function (assert) {
this.set(
"message",
ChatMessage.create({
user: { username: "discobot" },
bookmark: Bookmark.create({
name: "some name",
}),
})
);
this.message = ChatMessage.create(fabricators.chatChannel(), {
user: { username: "discobot" },
bookmark: Bookmark.create({
name: "some name",
}),
});
await render(hbs`<ChatMessageInfo @message={{this.message}} />`);
@ -90,7 +83,9 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
test("user status", async function (assert) {
const status = { description: "off to dentist", emoji: "tooth" };
this.set("message", ChatMessage.create({ user: { status } }));
this.message = ChatMessage.create(fabricators.chatChannel(), {
user: { status },
});
await render(hbs`<ChatMessageInfo @message={{this.message}} />`);
@ -98,13 +93,10 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("reviewable", async function (assert) {
this.set(
"message",
ChatMessage.create({
user: { username: "discobot" },
user_flag_status: 0,
})
);
this.message = ChatMessage.create(fabricators.chatChannel(), {
user: { username: "discobot" },
user_flag_status: 0,
});
await render(hbs`<ChatMessageInfo @message={{this.message}} />`);
@ -113,13 +105,12 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
I18n.t("chat.you_flagged")
);
this.set(
"message",
ChatMessage.create({
user: { username: "discobot" },
reviewable_id: 1,
})
);
this.message = ChatMessage.create(fabricators.chatChannel(), {
user: { username: "discobot" },
reviewable_id: 1,
});
await render(hbs`<ChatMessageInfo @message={{this.message}} />`);
assert.strictEqual(
query(".chat-message-info__flag a .svg-icon-title").title,
@ -128,18 +119,15 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("with username classes", async function (assert) {
this.set(
"message",
ChatMessage.create({
user: {
username: "discobot",
admin: true,
moderator: true,
new_user: true,
primary_group_name: "foo",
},
})
);
this.message = ChatMessage.create(fabricators.chatChannel(), {
user: {
username: "discobot",
admin: true,
moderator: true,
new_user: true,
primary_group_name: "foo",
},
});
await render(hbs`<ChatMessageInfo @message={{this.message}} />`);
@ -151,7 +139,9 @@ module("Discourse Chat | Component | chat-message-info", function (hooks) {
});
test("without username classes", async function (assert) {
this.set("message", ChatMessage.create({ user: { username: "discobot" } }));
this.message = ChatMessage.create(fabricators.chatChannel(), {
user: { username: "discobot" },
});
await render(hbs`<ChatMessageInfo @message={{this.message}} />`);

View File

@ -7,14 +7,6 @@ import { module, test } from "qunit";
module("Discourse Chat | Component | chat-message-reaction", function (hooks) {
setupRenderingTest(hooks);
test("accepts arbitrary class property", async function (assert) {
await render(hbs`
<ChatMessageReaction @reaction={{hash emoji="heart"}} @class="foo" />
`);
assert.true(exists(".chat-message-reaction.foo"));
});
test("adds reacted class when user reacted", async function (assert) {
await render(hbs`
<ChatMessageReaction @reaction={{hash emoji="heart" reacted=true}} />
@ -29,19 +21,6 @@ module("Discourse Chat | Component | chat-message-reaction", function (hooks) {
assert.true(exists(`.chat-message-reaction[data-emoji-name="heart"]`));
});
test("adds show class when count is positive", async function (assert) {
this.set("count", 0);
await render(hbs`
<ChatMessageReaction @reaction={{hash emoji="heart" count=this.count}} />
`);
assert.false(exists(".chat-message-reaction.show"));
this.set("count", 1);
assert.true(exists(".chat-message-reaction.show"));
});
test("title/alt attributes", async function (assert) {
await render(hbs`<ChatMessageReaction @reaction={{hash emoji="heart"}} />`);

View File

@ -0,0 +1,24 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { query } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile";
import { module, test } from "qunit";
import { render } from "@ember/test-helpers";
module(
"Discourse Chat | Component | chat-message-separator-date",
function (hooks) {
setupRenderingTest(hooks);
test("first message of the day", async function (assert) {
this.set("date", moment().format("LLL"));
this.set("message", { firstMessageOfTheDayAt: this.date });
await render(hbs`<ChatMessageSeparatorDate @message={{this.message}} />`);
assert.strictEqual(
query(".chat-message-separator-date").innerText.trim(),
this.date
);
});
}
);

View File

@ -0,0 +1,24 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { query } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile";
import I18n from "I18n";
import { module, test } from "qunit";
import { render } from "@ember/test-helpers";
module(
"Discourse Chat | Component | chat-message-separator-new",
function (hooks) {
setupRenderingTest(hooks);
test("newest message", async function (assert) {
this.set("message", { newest: true });
await render(hbs`<ChatMessageSeparatorNew @message={{this.message}} />`);
assert.strictEqual(
query(".chat-message-separator-new").innerText.trim(),
I18n.t("chat.last_visit")
);
});
}
);

View File

@ -1,35 +0,0 @@
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { query } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile";
import I18n from "I18n";
import { module, test } from "qunit";
import { render } from "@ember/test-helpers";
module("Discourse Chat | Component | chat-message-separator", function (hooks) {
setupRenderingTest(hooks);
test("newest message", async function (assert) {
this.set("message", { newestMessage: true });
await render(hbs`<ChatMessageSeparator @message={{this.message}} />`);
assert.strictEqual(
query(".chat-message-separator.new-message .text").innerText.trim(),
I18n.t("chat.new_messages")
);
});
test("first message of the day", async function (assert) {
this.set("date", moment().format("LLL"));
this.set("message", { firstMessageOfTheDayAt: this.date });
await render(hbs`<ChatMessageSeparator @message={{this.message}} />`);
assert.strictEqual(
query(
".chat-message-separator.first-daily-message .text"
).innerText.trim(),
this.date
);
});
});

View File

@ -1,5 +1,5 @@
import User from "discourse/models/user";
import { render, waitFor } from "@ember/test-helpers";
import { render } from "@ember/test-helpers";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import { exists } from "discourse/tests/helpers/qunit-helpers";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
@ -21,9 +21,16 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
unread_count: 0,
muted: false,
},
canInteractWithChat: true,
canDeleteSelf: true,
canDeleteOthers: true,
canFlag: true,
userSilenced: false,
canModerate: true,
});
return {
message: ChatMessage.create(
chatChannel,
Object.assign(
{
id: 178,
@ -38,14 +45,6 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
messageData
)
),
canInteractWithChat: true,
details: {
can_delete_self: true,
can_delete_others: true,
can_flag: true,
user_silenced: false,
can_moderate: true,
},
chatChannel,
setReplyTo: () => {},
replyMessageClicked: () => {},
@ -55,8 +54,9 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
onStartSelectingMessages: () => {},
onSelectMessage: () => {},
bulkSelectMessages: () => {},
afterReactionAdded: () => {},
onHoverMessage: () => {},
didShowMessage: () => {},
didHideMessage: () => {},
};
}
@ -64,8 +64,7 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
<ChatMessage
@message={{this.message}}
@canInteractWithChat={{this.canInteractWithChat}}
@details={{this.details}}
@chatChannel={{this.chatChannel}}
@channel={{this.chatChannel}}
@setReplyTo={{this.setReplyTo}}
@replyMessageClicked={{this.replyMessageClicked}}
@editButtonClicked={{this.editButtonClicked}}
@ -74,7 +73,8 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
@onSelectMessage={{this.onSelectMessage}}
@bulkSelectMessages={{this.bulkSelectMessages}}
@onHoverMessage={{this.onHoverMessage}}
@afterReactionAdded={{this.reStickScrollIfNeeded}}
@didShowMessage={{this.didShowMessage}}
@didHideMessage={{this.didHideMessage}}
/>
`;
@ -90,6 +90,7 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
test("Deleted message", async function (assert) {
this.setProperties(generateMessageProps({ deleted_at: moment() }));
await render(template);
assert.true(
exists(".chat-message-deleted .chat-message-expand"),
"has the correct deleted css class and expand button within"
@ -104,16 +105,4 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
"has the correct hidden css class and expand button within"
);
});
test("Message marked as visible", async function (assert) {
this.setProperties(generateMessageProps());
await render(template);
await waitFor("div[data-visible=true]");
assert.true(
exists(".chat-message-container[data-visible=true]"),
"message is marked as visible"
);
});
});

View File

@ -14,9 +14,7 @@ module(
this.channel = ChatChannel.create({ chatable_type: "Category" });
this.currentUser.set("needs_channel_retention_reminder", true);
await render(
hbs`<ChatRetentionReminder @chatChannel={{this.channel}} />`
);
await render(hbs`<ChatRetentionReminder @channel={{this.channel}} />`);
assert.dom(".chat-retention-reminder").includesText(
I18n.t("chat.retention_reminders.public", {