mirror of
https://github.com/discourse/discourse.git
synced 2025-06-07 09:57:38 +08:00
FEATURE: Type reactions in chat (#31439)
This change allows you to add a reaction to the most recent message, by sending a reaction message. A reaction message can be formatted as `+{emoji}` (eg, `+❤️`), or as `+{emoji_code}` (eg, `+❤️`).
This commit is contained in:
@ -18,7 +18,7 @@ import Site from "discourse/models/site";
|
||||
export const SKIP = "skip";
|
||||
export const CANCELLED_STATUS = "__CANCELLED";
|
||||
|
||||
const ALLOWED_LETTERS_REGEXP = /[\s[{(/]/;
|
||||
const ALLOWED_LETTERS_REGEXP = /[\s[{(/+]/;
|
||||
let _autoCompletePopper, _inputTimeout;
|
||||
|
||||
const keys = {
|
||||
|
@ -184,6 +184,14 @@ export function emojiExists(code) {
|
||||
return extendedEmojiMap.has(code) || emojiMap.has(code) || aliasMap.has(code);
|
||||
}
|
||||
|
||||
export function normalizeEmoji(code) {
|
||||
code = code.toLowerCase();
|
||||
if (extendedEmojiMap.get(code) || emojiMap.get(code)) {
|
||||
return code;
|
||||
}
|
||||
return aliasMap.get(code);
|
||||
}
|
||||
|
||||
let toSearch;
|
||||
export function emojiSearch(term, options) {
|
||||
const maxResults = options?.maxResults;
|
||||
|
@ -8018,6 +8018,7 @@ export const replacements = {
|
||||
"🖌": "paintbrush",
|
||||
"🔍": "mag",
|
||||
"🔎": "mag_right",
|
||||
"❤️": "heart",
|
||||
"❤": "heart",
|
||||
"💛": "yellow_heart",
|
||||
"💚": "green_heart",
|
||||
|
@ -3656,6 +3656,10 @@
|
||||
"code": "1f50e",
|
||||
"name": "mag_right"
|
||||
},
|
||||
{
|
||||
"code": "2764-fe0f",
|
||||
"name": "heart"
|
||||
},
|
||||
{
|
||||
"code": "2764",
|
||||
"name": "heart"
|
||||
|
@ -6,8 +6,12 @@ import { cancel, next } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import { isPresent } from "@ember/utils";
|
||||
import $ from "jquery";
|
||||
import { emojiSearch, isSkinTonableEmoji } from "pretty-text/emoji";
|
||||
import { translations } from "pretty-text/emoji/data";
|
||||
import {
|
||||
emojiSearch,
|
||||
isSkinTonableEmoji,
|
||||
normalizeEmoji,
|
||||
} from "pretty-text/emoji";
|
||||
import { replacements, translations } from "pretty-text/emoji/data";
|
||||
import { Promise } from "rsvp";
|
||||
import EmojiPickerDetached from "discourse/components/emoji-picker/detached";
|
||||
import InsertHyperlink from "discourse/components/modal/insert-hyperlink";
|
||||
@ -248,10 +252,45 @@ export default class ChatComposer extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this.reactingToLastMessage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.args.onSendMessage(this.draft);
|
||||
this.composer.textarea.refreshHeight();
|
||||
}
|
||||
|
||||
async reactingToLastMessage() {
|
||||
// Check if the message is a reaction to the latest message in the channel.
|
||||
const message = this.draft.message.trim();
|
||||
let reactionCode = "";
|
||||
if (message.startsWith("+")) {
|
||||
const reaction = message.substring(1);
|
||||
// First check if the message is +{emoji}
|
||||
if (replacements[reaction]) {
|
||||
reactionCode = replacements[reaction];
|
||||
} else {
|
||||
// Then check if the message is +:{emoji_code}:
|
||||
const emojiCode = reaction.substring(1, reaction.length - 1);
|
||||
reactionCode = normalizeEmoji(emojiCode);
|
||||
}
|
||||
}
|
||||
|
||||
if (reactionCode && this.lastMessage?.id) {
|
||||
const interactor = new ChatMessageInteractor(
|
||||
getOwner(this),
|
||||
this.lastMessage,
|
||||
this.context
|
||||
);
|
||||
|
||||
await interactor.react(reactionCode, "add");
|
||||
this.resetDraft();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
reportReplyingPresence() {
|
||||
if (!this.args.channel || !this.draft) {
|
||||
return;
|
||||
@ -478,7 +517,7 @@ export default class ChatComposer extends Component {
|
||||
treatAsTextarea: true,
|
||||
onKeyUp: (text, cp) => {
|
||||
const matches =
|
||||
/(?:^|[\s.\?,@\/#!%&*;:\[\]{}=\-_()])(:(?!:).?[\w-]*:?(?!:)(?:t\d?)?:?) ?$/gi.exec(
|
||||
/(?:^|[\s.\?,@\/#!%&*;:\[\]{}=\-_()+])(:(?!:).?[\w-]*:?(?!:)(?:t\d?)?:?) ?$/gi.exec(
|
||||
text.substring(0, cp)
|
||||
);
|
||||
|
||||
|
@ -46,7 +46,7 @@ export default class ChatComposerChannel extends ChatComposer {
|
||||
}
|
||||
|
||||
get lastMessage() {
|
||||
return this.args.channel.lastMessage;
|
||||
return this.args.channel.messagesManager.findLastMessage();
|
||||
}
|
||||
|
||||
lastUserMessage(user) {
|
||||
|
@ -58,6 +58,10 @@ export default class ChatComposerThread extends ChatComposer {
|
||||
return i18n("chat.placeholder_thread");
|
||||
}
|
||||
|
||||
get lastMessage() {
|
||||
return this.args.thread.messagesManager.findLastMessage();
|
||||
}
|
||||
|
||||
lastUserMessage(user) {
|
||||
return this.args.thread.messagesManager.findLastUserMessage(user);
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ RSpec.describe "Chat composer", type: :system do
|
||||
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||
let(:cdp) { PageObjects::CDP.new }
|
||||
let(:side_panel) { PageObjects::Pages::ChatSidePanel.new }
|
||||
let(:open_thread) { PageObjects::Pages::ChatThread.new }
|
||||
|
||||
before do
|
||||
chat_system_bootstrap
|
||||
@ -244,4 +246,75 @@ RSpec.describe "Chat composer", type: :system do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when sending a react message" do
|
||||
fab!(:react_message) do
|
||||
Fabricate(:chat_message, user: current_user, chat_channel: channel_1, message: "HI!")
|
||||
end
|
||||
|
||||
context "in a channel" do
|
||||
it "adds a reaction to the message" do
|
||||
chat_page.visit_channel(channel_1)
|
||||
|
||||
channel_page.send_message("+:+1:")
|
||||
|
||||
expect(channel_page).to have_reaction(react_message, "+1")
|
||||
end
|
||||
|
||||
it "works with literal emoji" do
|
||||
chat_page.visit_channel(channel_1)
|
||||
|
||||
channel_page.send_message("+👍")
|
||||
|
||||
expect(channel_page).to have_reaction(react_message, "+1")
|
||||
end
|
||||
end
|
||||
|
||||
context "in a thread" do
|
||||
fab!(:original_message) do
|
||||
Fabricate(:chat_message, chat_channel: channel_1, user: current_user)
|
||||
end
|
||||
fab!(:thread) do
|
||||
Fabricate(:chat_thread, channel: channel_1, original_message: original_message)
|
||||
end
|
||||
|
||||
fab!(:thread_message) do
|
||||
Fabricate(
|
||||
:chat_message,
|
||||
in_reply_to_id: original_message.id,
|
||||
chat_channel: channel_1,
|
||||
thread_id: thread.id,
|
||||
user: current_user,
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
channel_1.update!(threading_enabled: true)
|
||||
Chat::Thread.update_counts
|
||||
thread.add(current_user)
|
||||
end
|
||||
|
||||
it "adds a reaction to the message" do
|
||||
chat_page.visit_channel(channel_1)
|
||||
channel_page.message_thread_indicator(original_message).click
|
||||
|
||||
expect(side_panel).to have_open_thread(original_message.thread)
|
||||
|
||||
open_thread.send_message("+:+1:")
|
||||
|
||||
expect(open_thread).to have_reaction(thread_message, "+1")
|
||||
end
|
||||
|
||||
it "works with literal emoji" do
|
||||
chat_page.visit_channel(channel_1)
|
||||
channel_page.message_thread_indicator(original_message).click
|
||||
|
||||
expect(side_panel).to have_open_thread(original_message.thread)
|
||||
|
||||
open_thread.send_message("+👍")
|
||||
|
||||
expect(open_thread).to have_reaction(thread_message, "+1")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -140,6 +140,16 @@ module PageObjects
|
||||
end
|
||||
end
|
||||
|
||||
def has_reaction?(message, emoji, text = nil)
|
||||
within(message_reactions_list(message)) do
|
||||
has_css?("[data-emoji-name=\"#{emoji}\"]", text: text)
|
||||
end
|
||||
end
|
||||
|
||||
def message_reactions_list(message)
|
||||
within(message_by_id(message.id)) { find(".chat-message-reaction-list") }
|
||||
end
|
||||
|
||||
def message_by_id(id)
|
||||
find(message_by_id_selector(id))
|
||||
end
|
||||
|
Reference in New Issue
Block a user