mirror of
https://github.com/discourse/discourse.git
synced 2025-06-19 01:45:05 +08:00
FIX: improves draft for channels (#21724)
This commit attempts to correctly change draft when the channel changes. It moves responsibility to the composer instead of the channel. A new service `chatDraftsManager` is being introduced here to allow finer control and pave the way for future thread draft support. These changes also now allow an editing message to be stored as a draft.
This commit is contained in:
@ -43,6 +43,7 @@ export default class ChatLivePane extends Component {
|
|||||||
@service appEvents;
|
@service appEvents;
|
||||||
@service messageBus;
|
@service messageBus;
|
||||||
@service site;
|
@service site;
|
||||||
|
@service chatDraftsManager;
|
||||||
|
|
||||||
@tracked loading = false;
|
@tracked loading = false;
|
||||||
@tracked loadingMorePast = false;
|
@tracked loadingMorePast = false;
|
||||||
@ -116,11 +117,6 @@ 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;
|
||||||
|
|
||||||
if (this.args.channel.draft) {
|
|
||||||
this.chatChannelComposer.message = this.args.channel.draft;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._loadedChannelId = this.args.channel?.id;
|
this._loadedChannelId = this.args.channel?.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -642,6 +638,7 @@ export default class ChatLivePane extends Component {
|
|||||||
.editMessage(this.args.channel.id, message.id, data)
|
.editMessage(this.args.channel.id, message.id, data)
|
||||||
.catch(popupAjaxError)
|
.catch(popupAjaxError)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
this.chatDraftsManager.remove({ channelId: this.args.channel.id });
|
||||||
this.chatChannelPane.sending = false;
|
this.chatChannelPane.sending = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -704,7 +701,7 @@ export default class ChatLivePane extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.args.channel.draft = null;
|
this.chatDraftsManager.remove({ channelId: this.args.channel.id });
|
||||||
this.chatChannelPane.sending = false;
|
this.chatChannelPane.sending = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
<div class="chat-composer-message-details" data-id={{@message.id}}>
|
<div
|
||||||
|
class="chat-composer-message-details"
|
||||||
|
data-id={{@message.id}}
|
||||||
|
data-action={{if @message.editing "edit" "reply"}}
|
||||||
|
>
|
||||||
<div class="chat-reply">
|
<div class="chat-reply">
|
||||||
{{d-icon (if @message.editing "pencil-alt" "reply")}}
|
{{d-icon (if @message.editing "pencil-alt" "reply")}}
|
||||||
<ChatUserAvatar @user={{@message.user}} />
|
<ChatUserAvatar @user={{@message.user}} />
|
||||||
|
@ -26,6 +26,8 @@
|
|||||||
{{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.setup}}
|
{{did-insert this.setup}}
|
||||||
|
{{did-insert this.didUpdateChannel}}
|
||||||
|
{{did-update this.didUpdateChannel @channel.id}}
|
||||||
{{will-destroy this.teardown}}
|
{{will-destroy this.teardown}}
|
||||||
{{will-destroy this.cancelPersistDraft}}
|
{{will-destroy this.cancelPersistDraft}}
|
||||||
>
|
>
|
||||||
|
@ -17,8 +17,8 @@ import I18n from "I18n";
|
|||||||
import { translations } from "pretty-text/emoji/data";
|
import { translations } from "pretty-text/emoji/data";
|
||||||
import { setupHashtagAutocomplete } from "discourse/lib/hashtag-autocomplete";
|
import { setupHashtagAutocomplete } from "discourse/lib/hashtag-autocomplete";
|
||||||
import { isEmpty, isPresent } from "@ember/utils";
|
import { isEmpty, isPresent } from "@ember/utils";
|
||||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
|
||||||
import { Promise } from "rsvp";
|
import { Promise } from "rsvp";
|
||||||
|
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||||
import User from "discourse/models/user";
|
import User from "discourse/models/user";
|
||||||
|
|
||||||
export default class ChatComposer extends Component {
|
export default class ChatComposer extends Component {
|
||||||
@ -33,6 +33,7 @@ export default class ChatComposer extends Component {
|
|||||||
@service chatEmojiPickerManager;
|
@service chatEmojiPickerManager;
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
@service chatApi;
|
@service chatApi;
|
||||||
|
@service chatDraftsManager;
|
||||||
|
|
||||||
@tracked isFocused = false;
|
@tracked isFocused = false;
|
||||||
@tracked inProgressUploadsCount = 0;
|
@tracked inProgressUploadsCount = 0;
|
||||||
@ -78,15 +79,8 @@ export default class ChatComposer extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
sendMessage(raw) {
|
sendMessage() {
|
||||||
const message = ChatMessage.createDraftMessage(this.args.channel, {
|
this.args.onSendMessage(this.currentMessage);
|
||||||
user: this.currentUser,
|
|
||||||
message: raw,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.args.onSendMessage(message);
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -107,13 +101,15 @@ export default class ChatComposer extends Component {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
didUpdateMessage() {
|
didUpdateMessage() {
|
||||||
cancel(this._persistHandler);
|
this.cancelPersistDraft();
|
||||||
this.textareaInteractor.value = this.currentMessage.message || "";
|
this.textareaInteractor.value = this.currentMessage.message || "";
|
||||||
this.textareaInteractor.focus({ refreshHeight: true });
|
this.textareaInteractor.focus({ refreshHeight: true });
|
||||||
|
this.persistDraft();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
didUpdateInReplyTo() {
|
didUpdateInReplyTo() {
|
||||||
|
this.cancelPersistDraft();
|
||||||
this.textareaInteractor.focus({ ensureAtEnd: true, refreshHeight: true });
|
this.textareaInteractor.focus({ ensureAtEnd: true, refreshHeight: true });
|
||||||
this.persistDraft();
|
this.persistDraft();
|
||||||
}
|
}
|
||||||
@ -149,10 +145,20 @@ export default class ChatComposer extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setup() {
|
didUpdateChannel() {
|
||||||
this.composer.message = ChatMessage.createDraftMessage(this.args.channel, {
|
if (!this.args.channel) {
|
||||||
|
this.composer.message = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.composer.message =
|
||||||
|
this.chatDraftsManager.get({ channelId: this.args.channel.id }) ||
|
||||||
|
ChatMessage.createDraftMessage(this.args.channel, {
|
||||||
user: this.currentUser,
|
user: this.currentUser,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setup() {
|
||||||
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",
|
||||||
|
@ -7,6 +7,7 @@ import { action } from "@ember/object";
|
|||||||
export default class ChatComposerChannel extends ChatComposer {
|
export default class ChatComposerChannel extends ChatComposer {
|
||||||
@service("chat-channel-composer") composer;
|
@service("chat-channel-composer") composer;
|
||||||
@service("chat-channel-pane") pane;
|
@service("chat-channel-pane") pane;
|
||||||
|
@service chatDraftsManager;
|
||||||
|
|
||||||
context = "channel";
|
context = "channel";
|
||||||
|
|
||||||
@ -23,6 +24,8 @@ export default class ChatComposerChannel extends ChatComposer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.chatDraftsManager.add(this.currentMessage);
|
||||||
|
|
||||||
this._persistHandler = discourseDebounce(
|
this._persistHandler = discourseDebounce(
|
||||||
this,
|
this,
|
||||||
this._debouncedPersistDraft,
|
this._debouncedPersistDraft,
|
||||||
|
@ -81,7 +81,6 @@ export default class ChatChannel {
|
|||||||
@tracked canFlag;
|
@tracked canFlag;
|
||||||
@tracked canModerate;
|
@tracked canModerate;
|
||||||
@tracked userSilenced;
|
@tracked userSilenced;
|
||||||
@tracked draft = null;
|
|
||||||
@tracked meta;
|
@tracked meta;
|
||||||
@tracked chatableType;
|
@tracked chatableType;
|
||||||
@tracked chatableUrl;
|
@tracked chatableUrl;
|
||||||
|
@ -138,9 +138,13 @@ export default class ChatMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cook() {
|
cook() {
|
||||||
next(() => {
|
|
||||||
const site = getOwner(this).lookup("service:site");
|
const site = getOwner(this).lookup("service:site");
|
||||||
|
|
||||||
|
next(() => {
|
||||||
|
if (this.isDestroyed || this.isDestroying) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const markdownOptions = {
|
const markdownOptions = {
|
||||||
featuresOverride:
|
featuresOverride:
|
||||||
site.markdown_additional_options?.chat?.limited_pretty_text_features,
|
site.markdown_additional_options?.chat?.limited_pretty_text_features,
|
||||||
@ -248,6 +252,12 @@ export default class ChatMessage {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.editing) {
|
||||||
|
data.editing = true;
|
||||||
|
data.id = this.id;
|
||||||
|
data.excerpt = this.excerpt;
|
||||||
|
}
|
||||||
|
|
||||||
return JSON.stringify(data);
|
return JSON.stringify(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,19 @@
|
|||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import ChatComposer from "./chat-composer";
|
import ChatComposer from "./chat-composer";
|
||||||
|
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||||
|
|
||||||
export default class ChatChannelComposer extends ChatComposer {
|
export default class ChatChannelComposer extends ChatComposer {
|
||||||
@service chat;
|
@service chat;
|
||||||
@service router;
|
@service router;
|
||||||
|
|
||||||
|
@action
|
||||||
|
reset(channel) {
|
||||||
|
this.message = ChatMessage.createDraftMessage(channel, {
|
||||||
|
user: this.currentUser,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
replyTo(message) {
|
replyTo(message) {
|
||||||
this.chat.activeMessage = null;
|
this.chat.activeMessage = null;
|
||||||
|
@ -7,7 +7,7 @@ export default class ChatChannelThreadComposer extends ChatComposer {
|
|||||||
reset(channel, thread) {
|
reset(channel, thread) {
|
||||||
this.message = ChatMessage.createDraftMessage(channel, {
|
this.message = ChatMessage.createDraftMessage(channel, {
|
||||||
user: this.currentUser,
|
user: this.currentUser,
|
||||||
|
thread,
|
||||||
});
|
});
|
||||||
this.message.thread = thread;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import Service, { inject as service } from "@ember/service";
|
import Service, { inject as service } from "@ember/service";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
|
||||||
|
|
||||||
export default class ChatComposer extends Service {
|
export default class ChatComposer extends Service {
|
||||||
@service chat;
|
@service chat;
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
|
|
||||||
@tracked _message;
|
@tracked message;
|
||||||
|
|
||||||
@action
|
@action
|
||||||
cancel() {
|
cancel() {
|
||||||
@ -18,13 +17,6 @@ export default class ChatComposer extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
reset(channel) {
|
|
||||||
this.message = ChatMessage.createDraftMessage(channel, {
|
|
||||||
user: this.currentUser,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
clear() {
|
clear() {
|
||||||
this.message.message = "";
|
this.message.message = "";
|
||||||
@ -41,12 +33,4 @@ export default class ChatComposer extends Service {
|
|||||||
onCancelEditing() {
|
onCancelEditing() {
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
get message() {
|
|
||||||
return this._message;
|
|
||||||
}
|
|
||||||
|
|
||||||
set message(message) {
|
|
||||||
this._message = message;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
import Service from "@ember/service";
|
||||||
|
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||||
|
export default class ChatDraftsManager extends Service {
|
||||||
|
drafts = {};
|
||||||
|
|
||||||
|
add(message) {
|
||||||
|
if (message instanceof ChatMessage) {
|
||||||
|
this.drafts[message.channel.id] = message;
|
||||||
|
} else {
|
||||||
|
throw new Error("message must be an instance of ChatMessage");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get({ channelId }) {
|
||||||
|
return this.drafts[channelId];
|
||||||
|
}
|
||||||
|
|
||||||
|
remove({ channelId }) {
|
||||||
|
delete this.drafts[channelId];
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.drafts = {};
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,7 @@ export default class Chat extends Service {
|
|||||||
@service chatNotificationManager;
|
@service chatNotificationManager;
|
||||||
@service chatSubscriptionsManager;
|
@service chatSubscriptionsManager;
|
||||||
@service chatStateManager;
|
@service chatStateManager;
|
||||||
|
@service chatDraftsManager;
|
||||||
@service presence;
|
@service presence;
|
||||||
@service router;
|
@service router;
|
||||||
@service site;
|
@service site;
|
||||||
@ -167,19 +168,18 @@ export default class Chat extends Service {
|
|||||||
[...channels.public_channels, ...channels.direct_message_channels].forEach(
|
[...channels.public_channels, ...channels.direct_message_channels].forEach(
|
||||||
(channelObject) => {
|
(channelObject) => {
|
||||||
const channel = this.chatChannelsManager.store(channelObject);
|
const channel = this.chatChannelsManager.store(channelObject);
|
||||||
|
const storedDraft = (this.currentUser?.chat_drafts || []).find(
|
||||||
if (this.currentUser.chat_drafts) {
|
|
||||||
const storedDraft = this.currentUser.chat_drafts.find(
|
|
||||||
(draft) => draft.channel_id === channel.id
|
(draft) => draft.channel_id === channel.id
|
||||||
);
|
);
|
||||||
|
|
||||||
channel.draft = ChatMessage.createDraftMessage(
|
if (storedDraft) {
|
||||||
|
this.chatDraftsManager.add(
|
||||||
|
ChatMessage.createDraftMessage(
|
||||||
channel,
|
channel,
|
||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{ user: this.currentUser },
|
||||||
user: this.currentUser,
|
JSON.parse(storedDraft.data)
|
||||||
},
|
)
|
||||||
storedDraft ? JSON.parse(storedDraft.data) : {}
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
135
plugins/chat/spec/system/chat_composer_draft_spec.rb
Normal file
135
plugins/chat/spec/system/chat_composer_draft_spec.rb
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe "Chat composer draft", type: :system, js: true do
|
||||||
|
fab!(:current_user) { Fabricate(:user) }
|
||||||
|
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||||
|
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1) }
|
||||||
|
|
||||||
|
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||||
|
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||||
|
|
||||||
|
before { chat_system_bootstrap }
|
||||||
|
|
||||||
|
context "when loading a channel with a draft" do
|
||||||
|
fab!(:draft_1) do
|
||||||
|
Chat::Draft.create!(
|
||||||
|
chat_channel: channel_1,
|
||||||
|
user: current_user,
|
||||||
|
data: { message: "draft" }.to_json,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
channel_1.add(current_user)
|
||||||
|
sign_in(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "loads the draft" do
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
|
||||||
|
expect(channel_page.composer.value).to eq("draft")
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when loading another channel and back" do
|
||||||
|
fab!(:channel_2) { Fabricate(:chat_channel) }
|
||||||
|
|
||||||
|
fab!(:draft_2) do
|
||||||
|
Chat::Draft.create!(
|
||||||
|
chat_channel: channel_2,
|
||||||
|
user: current_user,
|
||||||
|
data: { message: "draft2" }.to_json,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
before { channel_2.add(current_user) }
|
||||||
|
|
||||||
|
it "loads the correct drafts" do
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
|
||||||
|
expect(channel_page.composer.value).to eq("draft")
|
||||||
|
|
||||||
|
chat_page.visit_channel(channel_2)
|
||||||
|
|
||||||
|
expect(channel_page.composer.value).to eq("draft2")
|
||||||
|
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
|
||||||
|
expect(channel_page.composer.value).to eq("draft")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with editing" do
|
||||||
|
fab!(:draft_1) do
|
||||||
|
Chat::Draft.create!(
|
||||||
|
chat_channel: channel_1,
|
||||||
|
user: current_user,
|
||||||
|
data: { message: message_1.message, id: message_1.id, editing: true }.to_json,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "loads the draft with the editing state" do
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
|
||||||
|
expect(channel_page.composer).to be_editing_message(message_1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with uploads" do
|
||||||
|
fab!(:upload_1) do
|
||||||
|
Fabricate(
|
||||||
|
:upload,
|
||||||
|
url: "/images/logo-dark.png",
|
||||||
|
original_filename: "logo_dark.png",
|
||||||
|
width: 400,
|
||||||
|
height: 300,
|
||||||
|
extension: "png",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
fab!(:draft_1) do
|
||||||
|
Chat::Draft.create!(
|
||||||
|
chat_channel: channel_1,
|
||||||
|
user: current_user,
|
||||||
|
data: { message: "draft", uploads: [upload_1] }.to_json,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "loads the draft with the upload" do
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
|
||||||
|
expect(channel_page.composer.value).to eq("draft")
|
||||||
|
expect(page).to have_selector(".chat-composer-upload--image", count: 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when replying" do
|
||||||
|
fab!(:draft_1) do
|
||||||
|
Chat::Draft.create!(
|
||||||
|
chat_channel: channel_1,
|
||||||
|
user: current_user,
|
||||||
|
data: {
|
||||||
|
message: "draft",
|
||||||
|
replyToMsg: {
|
||||||
|
id: message_1.id,
|
||||||
|
excerpt: message_1.excerpt,
|
||||||
|
user: {
|
||||||
|
id: message_1.user.id,
|
||||||
|
name: nil,
|
||||||
|
avatar_template: message_1.user.avatar_template,
|
||||||
|
username: message_1.user.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}.to_json,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "loads the draft with replied to mesage" do
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
|
||||||
|
expect(channel_page.composer.value).to eq("draft")
|
||||||
|
expect(page).to have_selector(".chat-reply__username", text: message_1.user.username)
|
||||||
|
expect(page).to have_selector(".chat-reply__excerpt", text: message_1.excerpt)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -5,90 +5,11 @@ RSpec.describe "Chat composer", type: :system, js: true do
|
|||||||
fab!(:channel_1) { Fabricate(:chat_channel) }
|
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||||
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1) }
|
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1) }
|
||||||
|
|
||||||
let(:chat) { PageObjects::Pages::Chat.new }
|
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||||
let(:channel) { PageObjects::Pages::ChatChannel.new }
|
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||||
|
|
||||||
before { chat_system_bootstrap }
|
before { chat_system_bootstrap }
|
||||||
|
|
||||||
context "when loading a channel with a draft" do
|
|
||||||
fab!(:draft_1) do
|
|
||||||
Chat::Draft.create!(
|
|
||||||
chat_channel: channel_1,
|
|
||||||
user: current_user,
|
|
||||||
data: { message: "draft" }.to_json,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
channel_1.add(current_user)
|
|
||||||
sign_in(current_user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "loads the draft" do
|
|
||||||
chat.visit_channel(channel_1)
|
|
||||||
|
|
||||||
expect(find(".chat-composer__input").value).to eq("draft")
|
|
||||||
end
|
|
||||||
|
|
||||||
context "with uploads" do
|
|
||||||
fab!(:upload_1) do
|
|
||||||
Fabricate(
|
|
||||||
:upload,
|
|
||||||
url: "/images/logo-dark.png",
|
|
||||||
original_filename: "logo_dark.png",
|
|
||||||
width: 400,
|
|
||||||
height: 300,
|
|
||||||
extension: "png",
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
fab!(:draft_1) do
|
|
||||||
Chat::Draft.create!(
|
|
||||||
chat_channel: channel_1,
|
|
||||||
user: current_user,
|
|
||||||
data: { message: "draft", uploads: [upload_1] }.to_json,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "loads the draft with the upload" do
|
|
||||||
chat.visit_channel(channel_1)
|
|
||||||
|
|
||||||
expect(find(".chat-composer__input").value).to eq("draft")
|
|
||||||
expect(page).to have_selector(".chat-composer-upload--image", count: 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when replying" do
|
|
||||||
fab!(:draft_1) do
|
|
||||||
Chat::Draft.create!(
|
|
||||||
chat_channel: channel_1,
|
|
||||||
user: current_user,
|
|
||||||
data: {
|
|
||||||
message: "draft",
|
|
||||||
replyToMsg: {
|
|
||||||
id: message_1.id,
|
|
||||||
excerpt: message_1.excerpt,
|
|
||||||
user: {
|
|
||||||
id: message_1.user.id,
|
|
||||||
name: nil,
|
|
||||||
avatar_template: message_1.user.avatar_template,
|
|
||||||
username: message_1.user.username,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}.to_json,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "loads the draft with replied to mesage" do
|
|
||||||
chat.visit_channel(channel_1)
|
|
||||||
|
|
||||||
expect(find(".chat-composer__input").value).to eq("draft")
|
|
||||||
expect(page).to have_selector(".chat-reply__username", text: message_1.user.username)
|
|
||||||
expect(page).to have_selector(".chat-reply__excerpt", text: message_1.excerpt)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when replying to a message" do
|
context "when replying to a message" do
|
||||||
before do
|
before do
|
||||||
channel_1.add(current_user)
|
channel_1.add(current_user)
|
||||||
@ -96,8 +17,8 @@ RSpec.describe "Chat composer", type: :system, js: true do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "adds the reply indicator to the composer" do
|
it "adds the reply indicator to the composer" do
|
||||||
chat.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
channel.reply_to(message_1)
|
channel_page.reply_to(message_1)
|
||||||
|
|
||||||
expect(page).to have_selector(
|
expect(page).to have_selector(
|
||||||
".chat-composer-message-details .chat-reply__username",
|
".chat-composer-message-details .chat-reply__username",
|
||||||
@ -109,8 +30,8 @@ RSpec.describe "Chat composer", type: :system, js: true do
|
|||||||
before { message_1.update!(message: "<mark>not marked</mark>") }
|
before { message_1.update!(message: "<mark>not marked</mark>") }
|
||||||
|
|
||||||
it "renders text in the details" do
|
it "renders text in the details" do
|
||||||
chat.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
channel.reply_to(message_1)
|
channel_page.reply_to(message_1)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
find(".chat-composer-message-details .chat-reply__excerpt")["innerHTML"].strip,
|
find(".chat-composer-message-details .chat-reply__excerpt")["innerHTML"].strip,
|
||||||
@ -128,47 +49,47 @@ RSpec.describe "Chat composer", type: :system, js: true do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "adds the edit indicator" do
|
it "adds the edit indicator" do
|
||||||
chat.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
channel.edit_message(message_2)
|
channel_page.edit_message(message_2)
|
||||||
|
|
||||||
expect(page).to have_selector(
|
expect(page).to have_selector(
|
||||||
".chat-composer-message-details .chat-reply__username",
|
".chat-composer-message-details .chat-reply__username",
|
||||||
text: current_user.username,
|
text: current_user.username,
|
||||||
)
|
)
|
||||||
expect(find(".chat-composer__input").value).to eq(message_2.message)
|
expect(channel_page.composer.value).to eq(message_2.message)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "updates the message instantly" do
|
it "updates the message instantly" do
|
||||||
chat.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
page.driver.browser.network_conditions = { offline: true }
|
page.driver.browser.network_conditions = { offline: true }
|
||||||
|
|
||||||
channel.edit_message(message_2)
|
channel_page.edit_message(message_2)
|
||||||
find(".chat-composer__input").send_keys("instant")
|
find(".chat-composer__input").send_keys("instant")
|
||||||
channel.click_send_message
|
channel_page.click_send_message
|
||||||
|
|
||||||
expect(channel).to have_message(text: message_2.message + "instant")
|
expect(channel_page).to have_message(text: message_2.message + "instant")
|
||||||
page.driver.browser.network_conditions = { offline: false }
|
page.driver.browser.network_conditions = { offline: false }
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when pressing escape" do
|
context "when pressing escape" do
|
||||||
it "cancels editing" do
|
it "cancels editing" do
|
||||||
chat.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
channel.edit_message(message_2)
|
channel_page.edit_message(message_2)
|
||||||
find(".chat-composer__input").send_keys(:escape)
|
find(".chat-composer__input").send_keys(:escape)
|
||||||
|
|
||||||
expect(page).to have_no_selector(".chat-composer-message-details .chat-reply__username")
|
expect(page).to have_no_selector(".chat-composer-message-details .chat-reply__username")
|
||||||
expect(find(".chat-composer__input").value).to eq("")
|
expect(channel_page.composer.value).to eq("")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when closing edited message" do
|
context "when closing edited message" do
|
||||||
it "cancels editing" do
|
it "cancels editing" do
|
||||||
chat.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
channel.edit_message(message_2)
|
channel_page.edit_message(message_2)
|
||||||
find(".cancel-message-action").click
|
find(".cancel-message-action").click
|
||||||
|
|
||||||
expect(page).to have_no_selector(".chat-composer-message-details .chat-reply__username")
|
expect(page).to have_no_selector(".chat-composer-message-details .chat-reply__username")
|
||||||
expect(find(".chat-composer__input").value).to eq("")
|
expect(channel_page.composer.value).to eq("")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -180,19 +101,19 @@ RSpec.describe "Chat composer", type: :system, js: true do
|
|||||||
end
|
end
|
||||||
|
|
||||||
xit "adds the emoji to the composer" do
|
xit "adds the emoji to the composer" do
|
||||||
chat.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
channel.open_action_menu
|
channel_page.open_action_menu
|
||||||
channel.click_action_button("emoji")
|
channel_page.click_action_button("emoji")
|
||||||
find("[data-emoji='grimacing']").click(wait: 0.5)
|
find("[data-emoji='grimacing']").click(wait: 0.5)
|
||||||
|
|
||||||
expect(find(".chat-composer__input").value).to eq(":grimacing:")
|
expect(channel_page.composer.value).to eq(":grimacing:")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "removes denied emojis from insert emoji picker" do
|
it "removes denied emojis from insert emoji picker" do
|
||||||
SiteSetting.emoji_deny_list = "monkey|peach"
|
SiteSetting.emoji_deny_list = "monkey|peach"
|
||||||
|
|
||||||
chat.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
channel.composer.open_emoji_picker
|
channel_page.composer.open_emoji_picker
|
||||||
|
|
||||||
expect(page).to have_no_selector("[data-emoji='monkey']")
|
expect(page).to have_no_selector("[data-emoji='monkey']")
|
||||||
expect(page).to have_no_selector("[data-emoji='peach']")
|
expect(page).to have_no_selector("[data-emoji='peach']")
|
||||||
@ -206,16 +127,16 @@ RSpec.describe "Chat composer", type: :system, js: true do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "adds the emoji to the composer" do
|
it "adds the emoji to the composer" do
|
||||||
chat.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
find(".chat-composer__input").fill_in(with: ":gri")
|
find(".chat-composer__input").fill_in(with: ":gri")
|
||||||
find(".emoji-shortname", text: "grimacing").click
|
find(".emoji-shortname", text: "grimacing").click
|
||||||
|
|
||||||
expect(find(".chat-composer__input").value).to eq(":grimacing: ")
|
expect(channel_page.composer.value).to eq(":grimacing: ")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn't suggest denied emojis and aliases" do
|
it "doesn't suggest denied emojis and aliases" do
|
||||||
SiteSetting.emoji_deny_list = "peach|poop"
|
SiteSetting.emoji_deny_list = "peach|poop"
|
||||||
chat.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
|
|
||||||
find(".chat-composer__input").fill_in(with: ":peac")
|
find(".chat-composer__input").fill_in(with: ":peac")
|
||||||
expect(page).to have_no_selector(".emoji-shortname", text: "peach")
|
expect(page).to have_no_selector(".emoji-shortname", text: "peach")
|
||||||
@ -232,7 +153,7 @@ RSpec.describe "Chat composer", type: :system, js: true do
|
|||||||
end
|
end
|
||||||
|
|
||||||
xit "prefills the emoji picker filter input" do
|
xit "prefills the emoji picker filter input" do
|
||||||
chat.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
find(".chat-composer__input").fill_in(with: ":gri")
|
find(".chat-composer__input").fill_in(with: ":gri")
|
||||||
|
|
||||||
click_link(I18n.t("js.composer.more_emoji"))
|
click_link(I18n.t("js.composer.more_emoji"))
|
||||||
@ -241,7 +162,7 @@ RSpec.describe "Chat composer", type: :system, js: true do
|
|||||||
end
|
end
|
||||||
|
|
||||||
xit "filters with the prefilled input" do
|
xit "filters with the prefilled input" do
|
||||||
chat.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
find(".chat-composer__input").fill_in(with: ":fr")
|
find(".chat-composer__input").fill_in(with: ":fr")
|
||||||
|
|
||||||
click_link(I18n.t("js.composer.more_emoji"))
|
click_link(I18n.t("js.composer.more_emoji"))
|
||||||
@ -258,19 +179,19 @@ RSpec.describe "Chat composer", type: :system, js: true do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "propagates keys to composer" do
|
it "propagates keys to composer" do
|
||||||
chat.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
|
|
||||||
find("body").send_keys("b")
|
find("body").send_keys("b")
|
||||||
|
|
||||||
expect(find(".chat-composer__input").value).to eq("b")
|
expect(channel_page.composer.value).to eq("b")
|
||||||
|
|
||||||
find("body").send_keys("b")
|
find("body").send_keys("b")
|
||||||
|
|
||||||
expect(find(".chat-composer__input").value).to eq("bb")
|
expect(channel_page.composer.value).to eq("bb")
|
||||||
|
|
||||||
find("body").send_keys(:enter) # special case
|
find("body").send_keys(:enter) # special case
|
||||||
|
|
||||||
expect(find(".chat-composer__input").value).to eq("bb")
|
expect(channel_page.composer.value).to eq("bb")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -288,7 +209,7 @@ RSpec.describe "Chat composer", type: :system, js: true do
|
|||||||
element.setSelectionRange(0, element.value.length)
|
element.setSelectionRange(0, element.value.length)
|
||||||
JS
|
JS
|
||||||
|
|
||||||
chat.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
|
|
||||||
find("body").send_keys("https://www.discourse.org")
|
find("body").send_keys("https://www.discourse.org")
|
||||||
page.execute_script(select_text, ".chat-composer__input")
|
page.execute_script(select_text, ".chat-composer__input")
|
||||||
@ -301,7 +222,7 @@ RSpec.describe "Chat composer", type: :system, js: true do
|
|||||||
|
|
||||||
page.send_keys [modifier, "v"]
|
page.send_keys [modifier, "v"]
|
||||||
|
|
||||||
expect(find(".chat-composer__input").value).to eq("[discourse](https://www.discourse.org)")
|
expect(channel_page.composer.value).to eq("[discourse](https://www.discourse.org)")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -313,11 +234,11 @@ RSpec.describe "Chat composer", type: :system, js: true do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "works" do
|
it "works" do
|
||||||
chat.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
find("body").send_keys("1")
|
find("body").send_keys("1")
|
||||||
channel.click_send_message
|
channel_page.click_send_message
|
||||||
|
|
||||||
expect(channel).to have_message(text: "1")
|
expect(channel_page).to have_message(text: "1")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -329,7 +250,7 @@ RSpec.describe "Chat composer", type: :system, js: true do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "doesn’t allow to send" do
|
it "doesn’t allow to send" do
|
||||||
chat.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
find("body").send_keys("1")
|
find("body").send_keys("1")
|
||||||
|
|
||||||
expect(page).to have_css(".chat-composer.is-send-disabled")
|
expect(page).to have_css(".chat-composer.is-send-disabled")
|
||||||
@ -343,14 +264,14 @@ RSpec.describe "Chat composer", type: :system, js: true do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "doesn’t allow to send" do
|
it "doesn’t allow to send" do
|
||||||
chat.visit_channel(channel_1)
|
chat_page.visit_channel(channel_1)
|
||||||
|
|
||||||
page.driver.browser.network_conditions = { latency: 20_000 }
|
page.driver.browser.network_conditions = { latency: 20_000 }
|
||||||
|
|
||||||
file_path = file_from_fixtures("logo.png", "images").path
|
file_path = file_from_fixtures("logo.png", "images").path
|
||||||
attach_file(file_path) do
|
attach_file(file_path) do
|
||||||
channel.open_action_menu
|
channel_page.open_action_menu
|
||||||
channel.click_action_button("chat-upload-btn")
|
channel_page.click_action_button("chat-upload-btn")
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(page).to have_css(".chat-composer-upload--in-progress")
|
expect(page).to have_css(".chat-composer-upload--in-progress")
|
||||||
|
@ -12,6 +12,10 @@ module PageObjects
|
|||||||
@context = context
|
@context = context
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def message_details
|
||||||
|
@message_details ||= PageObjects::Components::Chat::ComposerMessageDetails.new(context)
|
||||||
|
end
|
||||||
|
|
||||||
def input
|
def input
|
||||||
find(context).find(SELECTOR).find(".chat-composer__input")
|
find(context).find(SELECTOR).find(".chat-composer__input")
|
||||||
end
|
end
|
||||||
@ -31,6 +35,10 @@ module PageObjects
|
|||||||
def open_emoji_picker
|
def open_emoji_picker
|
||||||
find(context).find(SELECTOR).find(".chat-composer-button__btn.emoji").click
|
find(context).find(SELECTOR).find(".chat-composer-button__btn.emoji").click
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def editing_message?(message)
|
||||||
|
value == message.message && message_details.editing_message?(message)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -12,13 +12,19 @@ module PageObjects
|
|||||||
@context = context
|
@context = context
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_message?(message)
|
def has_message?(message, action: nil)
|
||||||
find(context).find(SELECTOR + "[data-id=\"#{message.id}\"]")
|
data_attributes = "[data-id=\"#{message.id}\"]"
|
||||||
|
data_attributes << "[data-action=\"#{action}\"]" if action
|
||||||
|
find(context).find(SELECTOR + data_attributes)
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_no_message?
|
def has_no_message?
|
||||||
find(context).has_no_css?(SELECTOR)
|
find(context).has_no_css?(SELECTOR)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def editing_message?(message)
|
||||||
|
has_message?(message, action: :edit)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
import { module, test } from "qunit";
|
||||||
|
import { setupTest } from "ember-qunit";
|
||||||
|
import { getOwner } from "discourse-common/lib/get-owner";
|
||||||
|
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
|
||||||
|
|
||||||
|
module(
|
||||||
|
"Discourse Chat | Unit | Service | chat-drafts-manager",
|
||||||
|
function (hooks) {
|
||||||
|
setupTest(hooks);
|
||||||
|
|
||||||
|
hooks.beforeEach(function () {
|
||||||
|
this.subject = getOwner(this).lookup("service:chat-drafts-manager");
|
||||||
|
});
|
||||||
|
|
||||||
|
hooks.afterEach(function () {
|
||||||
|
this.subject.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("storing and retrieving message", function (assert) {
|
||||||
|
const message1 = fabricators.message();
|
||||||
|
this.subject.add(message1);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
this.subject.get({ channelId: message1.channel.id }),
|
||||||
|
message1
|
||||||
|
);
|
||||||
|
|
||||||
|
const message2 = fabricators.message();
|
||||||
|
this.subject.add(message2);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
this.subject.get({ channelId: message2.channel.id }),
|
||||||
|
message2
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("stores only chat messages", function (assert) {
|
||||||
|
assert.throws(function () {
|
||||||
|
this.subject.add({ foo: "bar" });
|
||||||
|
}, /instance of ChatMessage/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("#reset", function (assert) {
|
||||||
|
this.subject.add(fabricators.message());
|
||||||
|
|
||||||
|
assert.strictEqual(Object.keys(this.subject.drafts).length, 1);
|
||||||
|
|
||||||
|
this.subject.reset();
|
||||||
|
|
||||||
|
assert.strictEqual(Object.keys(this.subject.drafts).length, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
Reference in New Issue
Block a user