add ability to create replies

This commit is contained in:
ouwou 2021-03-14 17:59:52 -05:00
parent ba6b8b2773
commit 927acfb9fe
16 changed files with 146 additions and 33 deletions

View File

@ -94,6 +94,7 @@ Or, do steps 1 and 2, and open CMakeLists.txt in Visual Studio if `vcpkg integra
.message-attachment-box - Contains attachment info
.message-reply - Container for the replied-to message in a reply (these elements will also have .message-text set)
.message-input - Applied to the chat input container
.replying - Extra class for chat input container when a reply is currently being created
.reaction-box - Contains a reaction image and the count
.reacted - Additional class for reaction-box when the user has reacted with a particular reaction
.reaction-count - Contains the count for reaction
@ -124,7 +125,7 @@ Or, do steps 1 and 2, and open CMakeLists.txt in Visual Studio if `vcpkg integra
.dnd - Applied to status indicators when the associated user is on do not disturb
.offline - Applied to status indicators when the associated user is offline
.typing-indicator - The typing indicator
.typing-indicator - The typing indicator (also used for replies)
Used in reorderable list implementation:
.drag-icon

View File

@ -464,10 +464,13 @@ void Abaddon::ActionChatLoadHistory(Snowflake id) {
});
}
void Abaddon::ActionChatInputSubmit(std::string msg, Snowflake channel) {
void Abaddon::ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflake referenced_message) {
if (msg.substr(0, 7) == "/shrug " || msg == "/shrug")
msg = msg.substr(6) + "\xC2\xAF\x5C\x5F\x28\xE3\x83\x84\x29\x5F\x2F\xC2\xAF"; // this is important
m_discord.SendChatMessage(msg, channel);
if (referenced_message.IsValid())
m_discord.SendChatMessage(msg, channel, referenced_message);
else
m_discord.SendChatMessage(msg, channel);
}
void Abaddon::ActionChatDeleteMessage(Snowflake channel_id, Snowflake id) {

View File

@ -34,7 +34,7 @@ public:
void ActionSetToken();
void ActionJoinGuildDialog();
void ActionChannelOpened(Snowflake id);
void ActionChatInputSubmit(std::string msg, Snowflake channel);
void ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflake referenced_message);
void ActionChatLoadHistory(Snowflake id);
void ActionChatDeleteMessage(Snowflake channel_id, Snowflake id);
void ActionChatEditMessage(Snowflake channel_id, Snowflake id);

View File

@ -31,6 +31,11 @@ Glib::RefPtr<Gtk::TextBuffer> ChatInput::GetBuffer() {
// this isnt connected directly so that the chat window can handle stuff like the completer first
bool ChatInput::ProcessKeyPress(GdkEventKey *event) {
if (event->keyval == GDK_KEY_Escape) {
m_signal_escape.emit();
return true;
}
if (event->keyval == GDK_KEY_Return) {
if (event->state & GDK_SHIFT_MASK)
return false;
@ -48,6 +53,14 @@ bool ChatInput::ProcessKeyPress(GdkEventKey *event) {
return false;
}
void ChatInput::on_grab_focus() {
m_textview.grab_focus();
}
ChatInput::type_signal_submit ChatInput::signal_submit() {
return m_signal_submit;
}
ChatInput::type_signal_escape ChatInput::signal_escape() {
return m_signal_escape;
}

View File

@ -9,15 +9,20 @@ public:
Glib::RefPtr<Gtk::TextBuffer> GetBuffer();
bool ProcessKeyPress(GdkEventKey *event);
private:
protected:
void on_grab_focus() override;
private:
Gtk::TextView m_textview;
public:
typedef sigc::signal<void, Glib::ustring> type_signal_submit;
typedef sigc::signal<void> type_signal_escape;
type_signal_submit signal_submit();
type_signal_escape signal_escape();
private:
type_signal_submit m_signal_submit;
type_signal_escape m_signal_escape;
};

View File

@ -1,11 +1,11 @@
#include <filesystem>
#include "typingindicator.hpp"
#include "chatinputindicator.hpp"
#include "../abaddon.hpp"
#include "../util.hpp"
constexpr static const int MaxUsersInIndicator = 4;
TypingIndicator::TypingIndicator()
ChatInputIndicator::ChatInputIndicator()
: Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) {
m_label.set_text("");
m_label.set_ellipsize(Pango::ELLIPSIZE_END);
@ -13,8 +13,8 @@ TypingIndicator::TypingIndicator()
m_img.set_margin_right(5);
get_style_context()->add_class("typing-indicator");
Abaddon::Get().GetDiscordClient().signal_typing_start().connect(sigc::mem_fun(*this, &TypingIndicator::OnUserTypingStart));
Abaddon::Get().GetDiscordClient().signal_message_create().connect(sigc::mem_fun(*this, &TypingIndicator::OnMessageCreate));
Abaddon::Get().GetDiscordClient().signal_typing_start().connect(sigc::mem_fun(*this, &ChatInputIndicator::OnUserTypingStart));
Abaddon::Get().GetDiscordClient().signal_message_create().connect(sigc::mem_fun(*this, &ChatInputIndicator::OnMessageCreate));
add(m_img);
add(m_label);
@ -36,7 +36,7 @@ TypingIndicator::TypingIndicator()
} catch (const std::exception &) {}
}
void TypingIndicator::AddUser(Snowflake channel_id, const UserData &user, int timeout) {
void ChatInputIndicator::AddUser(Snowflake channel_id, const UserData &user, int timeout) {
auto current_connection_it = m_typers[channel_id].find(user.ID);
if (current_connection_it != m_typers.at(channel_id).end()) {
current_connection_it->second.disconnect();
@ -53,12 +53,22 @@ void TypingIndicator::AddUser(Snowflake channel_id, const UserData &user, int ti
ComputeTypingString();
}
void TypingIndicator::SetActiveChannel(Snowflake id) {
void ChatInputIndicator::SetActiveChannel(Snowflake id) {
m_active_channel = id;
ComputeTypingString();
}
void TypingIndicator::OnUserTypingStart(Snowflake user_id, Snowflake channel_id) {
void ChatInputIndicator::SetCustomMarkup(const Glib::ustring &str) {
m_custom_markup = str;
ComputeTypingString();
}
void ChatInputIndicator::ClearCustom() {
m_custom_markup = "";
ComputeTypingString();
}
void ChatInputIndicator::OnUserTypingStart(Snowflake user_id, Snowflake channel_id) {
const auto &discord = Abaddon::Get().GetDiscordClient();
const auto user = discord.GetUser(user_id);
if (!user.has_value()) return;
@ -66,14 +76,14 @@ void TypingIndicator::OnUserTypingStart(Snowflake user_id, Snowflake channel_id)
AddUser(channel_id, *user, 10);
}
void TypingIndicator::OnMessageCreate(Snowflake message_id) {
void ChatInputIndicator::OnMessageCreate(Snowflake message_id) {
const auto msg = Abaddon::Get().GetDiscordClient().GetMessage(message_id);
if (!msg.has_value()) return;
m_typers[msg->ChannelID].erase(msg->Author.ID);
ComputeTypingString();
}
void TypingIndicator::SetTypingString(const Glib::ustring &str) {
void ChatInputIndicator::SetTypingString(const Glib::ustring &str) {
m_label.set_text(str);
if (str == "")
m_img.hide();
@ -81,7 +91,13 @@ void TypingIndicator::SetTypingString(const Glib::ustring &str) {
m_img.show();
}
void TypingIndicator::ComputeTypingString() {
void ChatInputIndicator::ComputeTypingString() {
if (m_custom_markup != "") {
m_label.set_markup(m_custom_markup);
m_img.hide();
return;
}
const auto &discord = Abaddon::Get().GetDiscordClient();
std::vector<UserData> typers;
for (const auto &[id, conn] : m_typers[m_active_channel]) {

View File

@ -4,10 +4,12 @@
#include "../discord/snowflake.hpp"
#include "../discord/user.hpp"
class TypingIndicator : public Gtk::Box {
class ChatInputIndicator : public Gtk::Box {
public:
TypingIndicator();
ChatInputIndicator();
void SetActiveChannel(Snowflake id);
void SetCustomMarkup(const Glib::ustring &str);
void ClearCustom();
private:
void AddUser(Snowflake channel_id, const UserData &user, int timeout);
@ -19,6 +21,8 @@ private:
Gtk::Image m_img;
Gtk::Label m_label;
Glib::ustring m_custom_markup;
Snowflake m_active_channel;
std::unordered_map<Snowflake, std::unordered_map<Snowflake, sigc::connection>> m_typers; // channel id -> [user id -> connection]
};

View File

@ -29,6 +29,10 @@ ChatMessageItemContainer::ChatMessageItemContainer() {
m_menu_copy_content->signal_activate().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::on_menu_copy_content));
m_menu.append(*m_menu_copy_content);
m_menu_reply_to = Gtk::manage(new Gtk::MenuItem("Reply To"));
m_menu_reply_to->signal_activate().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::on_menu_reply_to));
m_menu.append(*m_menu_reply_to);
m_menu.show_all();
m_link_menu_copy = Gtk::manage(new Gtk::MenuItem("Copy Link"));
@ -919,6 +923,10 @@ void ChatMessageItemContainer::on_menu_copy_content() {
Gtk::Clipboard::get()->set_text(msg->Content);
}
void ChatMessageItemContainer::on_menu_reply_to() {
m_signal_action_reply_to.emit(ID);
}
ChatMessageItemContainer::type_signal_action_delete ChatMessageItemContainer::signal_action_delete() {
return m_signal_action_delete;
}
@ -939,6 +947,10 @@ ChatMessageItemContainer::type_signal_action_reaction_remove ChatMessageItemCont
return m_signal_action_reaction_remove;
}
ChatMessageItemContainer::type_signal_action_reply_to ChatMessageItemContainer::signal_action_reply_to() {
return m_signal_action_reply_to;
}
void ChatMessageItemContainer::AttachEventHandlers(Gtk::Widget &widget) {
const auto on_button_press_event = [this](GdkEventButton *event) -> bool {
if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_SECONDARY) {

View File

@ -60,11 +60,13 @@ protected:
Gtk::MenuItem *m_menu_copy_content;
Gtk::MenuItem *m_menu_delete_message;
Gtk::MenuItem *m_menu_edit_message;
Gtk::MenuItem *m_menu_reply_to;
void on_menu_copy_id();
void on_menu_delete_message();
void on_menu_edit_message();
void on_menu_copy_content();
void on_menu_reply_to();
Gtk::EventBox *m_ev;
Gtk::Box *m_main;
@ -80,6 +82,7 @@ public:
typedef sigc::signal<void, Snowflake> type_signal_channel_click;
typedef sigc::signal<void, Glib::ustring> type_signal_action_reaction_add;
typedef sigc::signal<void, Glib::ustring> type_signal_action_reaction_remove;
typedef sigc::signal<void, Snowflake> type_signal_action_reply_to;
typedef sigc::signal<void> type_signal_enter;
typedef sigc::signal<void> type_signal_leave;
@ -88,12 +91,15 @@ public:
type_signal_channel_click signal_action_channel_click();
type_signal_action_reaction_add signal_action_reaction_add();
type_signal_action_reaction_remove signal_action_reaction_remove();
type_signal_action_reply_to signal_action_reply_to();
private:
type_signal_action_delete m_signal_action_delete;
type_signal_action_edit m_signal_action_edit;
type_signal_channel_click m_signal_action_channel_click;
type_signal_action_reaction_add m_signal_action_reaction_add;
type_signal_action_reaction_remove m_signal_action_reaction_remove;
type_signal_action_reply_to m_signal_action_reply_to;
};
class ChatMessageHeader : public Gtk::ListBoxRow {

View File

@ -1,7 +1,7 @@
#include "chatwindow.hpp"
#include "chatmessage.hpp"
#include "../abaddon.hpp"
#include "typingindicator.hpp"
#include "chatinputindicator.hpp"
#include "chatinput.hpp"
ChatWindow::ChatWindow() {
@ -9,10 +9,10 @@ ChatWindow::ChatWindow() {
m_list = Gtk::manage(new Gtk::ListBox);
m_scroll = Gtk::manage(new Gtk::ScrolledWindow);
m_input = Gtk::manage(new ChatInput);
m_typing_indicator = Gtk::manage(new TypingIndicator);
m_input_indicator = Gtk::manage(new ChatInputIndicator);
m_typing_indicator->set_valign(Gtk::ALIGN_END);
m_typing_indicator->show();
m_input_indicator->set_valign(Gtk::ALIGN_END);
m_input_indicator->show();
m_main->get_style_context()->add_class("messages");
m_list->get_style_context()->add_class("messages");
@ -43,9 +43,10 @@ ChatWindow::ChatWindow() {
m_list->set_focus_vadjustment(m_scroll->get_vadjustment());
m_list->show();
m_input->signal_submit().connect([this](const Glib::ustring &text) {
if (m_active_channel.IsValid())
m_signal_action_chat_submit.emit(text, m_active_channel);
m_input->signal_submit().connect(sigc::mem_fun(*this, &ChatWindow::OnInputSubmit));
m_input->signal_escape().connect([this]() {
if (m_is_replying)
StopReplying();
});
m_input->signal_key_press_event().connect(sigc::mem_fun(*this, &ChatWindow::OnKeyPressEvent), false);
m_input->show();
@ -87,7 +88,7 @@ ChatWindow::ChatWindow() {
m_main->add(*m_scroll);
m_main->add(m_completer);
m_main->add(*m_input);
m_main->add(*m_typing_indicator);
m_main->add(*m_input_indicator);
m_main->show();
}
@ -118,7 +119,9 @@ void ChatWindow::SetMessages(const std::set<Snowflake> &msgs) {
void ChatWindow::SetActiveChannel(Snowflake id) {
m_active_channel = id;
m_typing_indicator->SetActiveChannel(id);
m_input_indicator->SetActiveChannel(id);
if (m_is_replying)
StopReplying();
}
void ChatWindow::AddNewMessage(Snowflake id) {
@ -178,6 +181,13 @@ Snowflake ChatWindow::GetActiveChannel() const {
return m_active_channel;
}
void ChatWindow::OnInputSubmit(const Glib::ustring &text) {
if (m_active_channel.IsValid())
m_signal_action_chat_submit.emit(text, m_active_channel, m_replying_to); // m_replying_to is checked for invalid in the handler
if (m_is_replying)
StopReplying();
}
bool ChatWindow::OnKeyPressEvent(GdkEventKey *e) {
if (m_completer.ProcessKeyPress(e))
return true;
@ -253,6 +263,7 @@ void ChatWindow::ProcessNewMessage(Snowflake id, bool prepend) {
content->signal_action_channel_click().connect([this](const Snowflake &id) {
m_signal_action_channel_click.emit(id);
});
content->signal_action_reply_to().connect(sigc::mem_fun(*this, &ChatWindow::StartReplying));
}
header->set_margin_left(5);
@ -266,6 +277,27 @@ void ChatWindow::ProcessNewMessage(Snowflake id, bool prepend) {
}
}
void ChatWindow::StartReplying(Snowflake message_id) {
const auto &discord = Abaddon::Get().GetDiscordClient();
const auto message = *discord.GetMessage(message_id);
const auto author = discord.GetUser(message.Author.ID);
m_replying_to = message_id;
m_is_replying = true;
m_input->grab_focus();
m_input->get_style_context()->add_class("replying");
if (author.has_value())
m_input_indicator->SetCustomMarkup("Replying to " + author->GetEscapedBoldString<false>());
else
m_input_indicator->SetCustomMarkup("Replying...");
}
void ChatWindow::StopReplying() {
m_is_replying = false;
m_replying_to = Snowflake::Invalid;
m_input->get_style_context()->remove_class("replying");
m_input_indicator->ClearCustom();
}
void ChatWindow::OnScrollEdgeOvershot(Gtk::PositionType pos) {
if (pos == Gtk::POS_TOP)
m_signal_action_chat_load_history.emit(m_active_channel);

View File

@ -7,7 +7,7 @@
#include "completer.hpp"
class ChatInput;
class TypingIndicator;
class ChatInputIndicator;
class ChatWindow {
public:
ChatWindow();
@ -30,11 +30,19 @@ protected:
ChatMessageItemContainer *CreateMessageComponent(Snowflake id); // to be inserted into header's content box
void ProcessNewMessage(Snowflake id, bool prepend); // creates and adds components
bool m_is_replying = false;
Snowflake m_replying_to;
void StartReplying(Snowflake message_id);
void StopReplying();
int m_num_rows = 0;
std::unordered_map<Snowflake, Gtk::Widget *> m_id_to_widget;
Snowflake m_active_channel;
void OnInputSubmit(const Glib::ustring &text);
bool OnKeyPressEvent(GdkEventKey *e);
void OnScrollEdgeOvershot(Gtk::PositionType pos);
@ -48,12 +56,12 @@ protected:
ChatInput *m_input;
Completer m_completer;
TypingIndicator *m_typing_indicator;
ChatInputIndicator *m_input_indicator;
public:
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_action_message_delete;
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_action_message_edit;
typedef sigc::signal<void, std::string, Snowflake> type_signal_action_chat_submit;
typedef sigc::signal<void, std::string, Snowflake, Snowflake> type_signal_action_chat_submit;
typedef sigc::signal<void, Snowflake> type_signal_action_chat_load_history;
typedef sigc::signal<void, Snowflake> type_signal_action_channel_click;
typedef sigc::signal<void, Snowflake> type_signal_action_insert_mention;

View File

@ -101,6 +101,10 @@
border-radius: 15px;
}
.message-input.replying {
border: 1px solid #026FB9;
}
.message-input {
padding: 0px 0px 0px 5px;
}

View File

@ -345,12 +345,18 @@ bool DiscordClient::CanManageMember(Snowflake guild_id, Snowflake actor, Snowfla
return actor_highest->Position > target_highest->Position;
}
void DiscordClient::SendChatMessage(std::string content, Snowflake channel) {
void DiscordClient::SendChatMessage(const std::string &content, Snowflake channel) {
// @([^@#]{1,32})#(\\d{4})
CreateMessageObject obj;
obj.Content = content;
nlohmann::json j = obj;
m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", j.dump(), [](auto) {});
m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), [](auto &) {});
}
void DiscordClient::SendChatMessage(const std::string &content, Snowflake channel, Snowflake referenced_message) {
CreateMessageObject obj;
obj.Content = content;
obj.MessageReference.emplace().MessageID = referenced_message;
m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), [](auto &) {});
}
void DiscordClient::DeleteMessage(Snowflake channel_id, Snowflake id) {

View File

@ -97,7 +97,8 @@ public:
Permission ComputeOverwrites(Permission base, Snowflake member_id, Snowflake channel_id) const;
bool CanManageMember(Snowflake guild_id, Snowflake actor, Snowflake target) const; // kick, ban, edit nickname (cant think of a better name)
void SendChatMessage(std::string content, Snowflake channel);
void SendChatMessage(const std::string &content, Snowflake channel);
void SendChatMessage(const std::string &content, Snowflake channel, Snowflake referenced_message);
void DeleteMessage(Snowflake channel_id, Snowflake id);
void EditMessage(Snowflake channel_id, Snowflake id, std::string content);
void SendLazyLoad(Snowflake id);

View File

@ -189,6 +189,7 @@ void to_json(nlohmann::json &j, const HeartbeatMessage &m) {
void to_json(nlohmann::json &j, const CreateMessageObject &m) {
j["content"] = m.Content;
JS_IF("message_reference", m.MessageReference);
}
void to_json(nlohmann::json &j, const MessageEditObject &m) {

View File

@ -285,6 +285,7 @@ struct HeartbeatMessage : GatewayMessage {
struct CreateMessageObject {
std::string Content;
std::optional<MessageReferenceData> MessageReference;
friend void to_json(nlohmann::json &j, const CreateMessageObject &m);
};