summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/abaddon.cpp24
-rw-r--r--src/abaddon.hpp3
-rw-r--r--src/components/chatinput.cpp521
-rw-r--r--src/components/chatinput.hpp140
-rw-r--r--src/components/chatlist.cpp7
-rw-r--r--src/components/chatlist.hpp3
-rw-r--r--src/components/chatmessage.cpp4
-rw-r--r--src/components/chatwindow.cpp82
-rw-r--r--src/components/chatwindow.hpp8
-rw-r--r--src/components/progressbar.cpp24
-rw-r--r--src/components/progressbar.hpp11
-rw-r--r--src/constants.hpp8
-rw-r--r--src/dialogs/textinput.cpp26
-rw-r--r--src/dialogs/textinput.hpp14
-rw-r--r--src/discord/chatsubmitparams.hpp24
-rw-r--r--src/discord/discord.cpp98
-rw-r--r--src/discord/discord.hpp14
-rw-r--r--src/discord/guild.hpp9
-rw-r--r--src/discord/httpclient.cpp19
-rw-r--r--src/discord/httpclient.hpp3
-rw-r--r--src/discord/store.cpp1
-rw-r--r--src/discord/user.cpp4
-rw-r--r--src/discord/user.hpp1
-rw-r--r--src/http.cpp81
-rw-r--r--src/http.hpp26
-rw-r--r--src/windows/guildsettings/memberspane.cpp2
-rw-r--r--src/windows/mainwindow.cpp17
-rw-r--r--src/windows/mainwindow.hpp3
28 files changed, 1108 insertions, 69 deletions
diff --git a/src/abaddon.cpp b/src/abaddon.cpp
index 08e6857..e296aa4 100644
--- a/src/abaddon.cpp
+++ b/src/abaddon.cpp
@@ -11,6 +11,7 @@
#include "dialogs/setstatus.hpp"
#include "dialogs/friendpicker.hpp"
#include "dialogs/verificationgate.hpp"
+#include "dialogs/textinput.hpp"
#include "abaddon.hpp"
#include "windows/guildsettingswindow.hpp"
#include "windows/profilewindow.hpp"
@@ -741,17 +742,13 @@ void Abaddon::ActionChatLoadHistory(Snowflake id) {
});
}
-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
+void Abaddon::ActionChatInputSubmit(ChatSubmitParams data) {
+ if (data.Message.substr(0, 7) == "/shrug " || data.Message == "/shrug")
+ data.Message = data.Message.substr(6) + "\xC2\xAF\x5C\x5F\x28\xE3\x83\x84\x29\x5F\x2F\xC2\xAF"; // this is important
- if (!channel.IsValid()) return;
- if (!m_discord.HasChannelPermission(m_discord.GetUserData().ID, channel, Permission::VIEW_CHANNEL)) return;
+ if (!m_discord.HasChannelPermission(m_discord.GetUserData().ID, data.ChannelID, Permission::VIEW_CHANNEL)) return;
- if (referenced_message.IsValid())
- m_discord.SendChatMessage(msg, channel, referenced_message);
- else
- m_discord.SendChatMessage(msg, channel);
+ m_discord.SendChatMessage(data, NOOP_CALLBACK);
}
void Abaddon::ActionChatEditMessage(Snowflake channel_id, Snowflake id) {
@@ -860,6 +857,15 @@ void Abaddon::ActionViewThreads(Snowflake channel_id) {
window->show();
}
+std::optional<Glib::ustring> Abaddon::ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder, Gtk::Window *window) {
+ TextInputDialog dlg(prompt, title, placeholder, window != nullptr ? *window : *m_main_window);
+ const auto code = dlg.run();
+ if (code == Gtk::RESPONSE_OK)
+ return dlg.GetInput();
+ else
+ return {};
+}
+
bool Abaddon::ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window) {
ConfirmDialog dlg(window != nullptr ? *window : *m_main_window);
dlg.SetConfirmText(prompt);
diff --git a/src/abaddon.hpp b/src/abaddon.hpp
index 3296c45..c267269 100644
--- a/src/abaddon.hpp
+++ b/src/abaddon.hpp
@@ -36,7 +36,7 @@ public:
void ActionSetToken();
void ActionJoinGuildDialog();
void ActionChannelOpened(Snowflake id, bool expand_to = true);
- void ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflake referenced_message);
+ void ActionChatInputSubmit(ChatSubmitParams data);
void ActionChatLoadHistory(Snowflake id);
void ActionChatEditMessage(Snowflake channel_id, Snowflake id);
void ActionInsertMention(Snowflake id);
@@ -51,6 +51,7 @@ public:
void ActionViewPins(Snowflake channel_id);
void ActionViewThreads(Snowflake channel_id);
+ std::optional<Glib::ustring> ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder = "", Gtk::Window *window = nullptr);
bool ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window = nullptr);
void ActionReloadCSS();
diff --git a/src/components/chatinput.cpp b/src/components/chatinput.cpp
index 3e1db15..2466965 100644
--- a/src/components/chatinput.cpp
+++ b/src/components/chatinput.cpp
@@ -1,6 +1,9 @@
#include "chatinput.hpp"
+#include "abaddon.hpp"
+#include "constants.hpp"
+#include <filesystem>
-ChatInput::ChatInput() {
+ChatInputText::ChatInputText() {
get_style_context()->add_class("message-input");
set_propagate_natural_height(true);
set_min_content_height(20);
@@ -20,22 +23,26 @@ ChatInput::ChatInput() {
add(m_textview);
}
-void ChatInput::InsertText(const Glib::ustring &text) {
+void ChatInputText::InsertText(const Glib::ustring &text) {
GetBuffer()->insert_at_cursor(text);
m_textview.grab_focus();
}
-Glib::RefPtr<Gtk::TextBuffer> ChatInput::GetBuffer() {
+Glib::RefPtr<Gtk::TextBuffer> ChatInputText::GetBuffer() {
return m_textview.get_buffer();
}
// this isnt connected directly so that the chat window can handle stuff like the completer first
-bool ChatInput::ProcessKeyPress(GdkEventKey *event) {
+bool ChatInputText::ProcessKeyPress(GdkEventKey *event) {
if (event->keyval == GDK_KEY_Escape) {
m_signal_escape.emit();
return true;
}
+ if ((event->state & GDK_CONTROL_MASK) && event->keyval == GDK_KEY_v) {
+ return CheckHandleClipboardPaste();
+ }
+
if (event->keyval == GDK_KEY_Return) {
if (event->state & GDK_SHIFT_MASK)
return false;
@@ -53,10 +60,514 @@ bool ChatInput::ProcessKeyPress(GdkEventKey *event) {
return false;
}
-void ChatInput::on_grab_focus() {
+void ChatInputText::on_grab_focus() {
m_textview.grab_focus();
}
+bool ChatInputText::CheckHandleClipboardPaste() {
+ auto clip = Gtk::Clipboard::get();
+
+ if (!clip->wait_is_image_available()) return false;
+
+ const auto pb = clip->wait_for_image();
+ if (pb) {
+ m_signal_image_paste.emit(pb);
+
+ return true;
+ } else {
+ return false;
+ }
+}
+
+ChatInputText::type_signal_submit ChatInputText::signal_submit() {
+ return m_signal_submit;
+}
+
+ChatInputText::type_signal_escape ChatInputText::signal_escape() {
+ return m_signal_escape;
+}
+
+ChatInputText::type_signal_image_paste ChatInputText::signal_image_paste() {
+ return m_signal_image_paste;
+}
+
+ChatInputTextContainer::ChatInputTextContainer() {
+ // triple hack !!!
+ auto cb = [this](GdkEventKey *e) -> bool {
+ return event(reinterpret_cast<GdkEvent *>(e));
+ };
+ m_input.signal_key_press_event().connect(cb, false);
+
+ m_upload_img.property_icon_name() = "document-send-symbolic";
+ m_upload_img.property_icon_size() = Gtk::ICON_SIZE_LARGE_TOOLBAR;
+ m_upload_img.get_style_context()->add_class("message-input-browse-icon");
+
+ AddPointerCursor(m_upload_ev);
+
+ m_upload_ev.signal_button_press_event().connect([this](GdkEventButton *ev) -> bool {
+ if (ev->button == GDK_BUTTON_PRIMARY) {
+ ShowFileChooser();
+ // return focus
+ m_input.grab_focus();
+ return true;
+ }
+ return false;
+ });
+
+ m_upload_ev.add(m_upload_img);
+ add_overlay(m_upload_ev);
+ add(m_input);
+
+ show_all_children();
+
+ // stop the overlay from using (start) padding
+ signal_get_child_position().connect(sigc::mem_fun(*this, &ChatInputTextContainer::GetChildPosition), false);
+}
+
+void ChatInputTextContainer::ShowFileChooser() {
+ auto dlg = Gtk::FileChooserNative::create("Choose file", Gtk::FILE_CHOOSER_ACTION_OPEN);
+ dlg->set_select_multiple(true);
+ dlg->set_modal(true);
+
+ dlg->signal_response().connect([this, dlg](int response) {
+ if (response == Gtk::RESPONSE_ACCEPT) {
+ for (const auto &file : dlg->get_files()) {
+ m_signal_add_attachment.emit(file);
+ }
+ }
+ });
+
+ auto filter_all = Gtk::FileFilter::create();
+ filter_all->set_name("All files (*.*)");
+ filter_all->add_pattern("*.*");
+ dlg->add_filter(filter_all);
+
+ dlg->run();
+}
+
+ChatInputText &ChatInputTextContainer::Get() {
+ return m_input;
+}
+
+void ChatInputTextContainer::ShowChooserIcon() {
+ m_upload_ev.show();
+}
+
+void ChatInputTextContainer::HideChooserIcon() {
+ m_upload_ev.hide();
+}
+
+bool ChatInputTextContainer::GetChildPosition(Gtk::Widget *child, Gdk::Rectangle &pos) {
+ Gtk::Allocation main_alloc;
+ {
+ auto *grandchild = m_input.get_child();
+ int x, y;
+ if (grandchild->translate_coordinates(m_input, 0, 0, x, y)) {
+ main_alloc.set_x(x);
+ main_alloc.set_y(y);
+ } else {
+ main_alloc.set_x(0);
+ main_alloc.set_y(0);
+ }
+ main_alloc.set_width(grandchild->get_allocated_width());
+ main_alloc.set_height(grandchild->get_allocated_height());
+ }
+
+ Gtk::Requisition min, req;
+ child->get_preferred_size(min, req);
+
+ // let css move it around
+ pos.set_x(0);
+ pos.set_y(0);
+ pos.set_width(std::max(min.width, std::min(main_alloc.get_width(), req.width)));
+ pos.set_height(std::max(min.height, std::min(main_alloc.get_height(), req.height)));
+
+ return true;
+}
+
+ChatInputTextContainer::type_signal_add_attachment ChatInputTextContainer::signal_add_attachment() {
+ return m_signal_add_attachment;
+}
+
+ChatInputAttachmentContainer::ChatInputAttachmentContainer()
+ : m_box(Gtk::ORIENTATION_HORIZONTAL) {
+ get_style_context()->add_class("attachment-container");
+
+ m_box.set_halign(Gtk::ALIGN_START);
+
+ add(m_box);
+ m_box.show();
+
+ set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_NEVER);
+ set_vexpand(true);
+ set_size_request(-1, AttachmentItemSize + 10);
+}
+
+void ChatInputAttachmentContainer::Clear() {
+ for (auto *item : m_attachments) {
+ item->RemoveIfTemp();
+ delete item;
+ }
+ m_attachments.clear();
+}
+
+void ChatInputAttachmentContainer::ClearNoPurge() {
+ for (auto *item : m_attachments) {
+ delete item;
+ }
+ m_attachments.clear();
+}
+
+bool ChatInputAttachmentContainer::AddImage(const Glib::RefPtr<Gdk::Pixbuf> &pb) {
+ if (m_attachments.size() == 10) return false;
+
+ static unsigned go_up = 0;
+ std::string dest_name = "pasted-image-" + std::to_string(go_up++);
+ const auto path = (std::filesystem::temp_directory_path() / "abaddon-cache" / dest_name).string();
+
+ try {
+ pb->save(path, "png");
+ } catch (...) {
+ fprintf(stderr, "pasted image save error\n");
+ return false;
+ }
+
+ auto *item = Gtk::make_managed<ChatInputAttachmentItem>(Gio::File::create_for_path(path), pb);
+ item->set_valign(Gtk::ALIGN_FILL);
+ item->set_vexpand(true);
+ item->set_margin_bottom(5);
+ item->show();
+ m_box.add(*item);
+
+ m_attachments.push_back(item);
+
+ item->signal_item_removed().connect([this, item] {
+ item->RemoveIfTemp();
+ if (auto it = std::find(m_attachments.begin(), m_attachments.end(), item); it != m_attachments.end())
+ m_attachments.erase(it);
+ delete item;
+ if (m_attachments.empty())
+ m_signal_emptied.emit();
+ });
+
+ return true;
+}
+
+bool ChatInputAttachmentContainer::AddFile(const Glib::RefPtr<Gio::File> &file, Glib::RefPtr<Gdk::Pixbuf> pb) {
+ if (m_attachments.size() == 10) return false;
+
+ ChatInputAttachmentItem *item;
+ if (pb)
+ item = Gtk::make_managed<ChatInputAttachmentItem>(file, pb, true);
+ else
+ item = Gtk::make_managed<ChatInputAttachmentItem>(file);
+ item->set_valign(Gtk::ALIGN_FILL);
+ item->set_vexpand(true);
+ item->set_margin_bottom(5);
+ item->show();
+ m_box.add(*item);
+
+ m_attachments.push_back(item);
+
+ item->signal_item_removed().connect([this, item] {
+ if (auto it = std::find(m_attachments.begin(), m_attachments.end(), item); it != m_attachments.end())
+ m_attachments.erase(it);
+ delete item;
+ if (m_attachments.empty())
+ m_signal_emptied.emit();
+ });
+
+ return true;
+}
+
+std::vector<ChatSubmitParams::Attachment> ChatInputAttachmentContainer::GetAttachments() const {
+ std::vector<ChatSubmitParams::Attachment> ret;
+ for (auto *x : m_attachments) {
+ if (!x->GetFile()->query_exists())
+ puts("bad!");
+ ret.push_back({ x->GetFile(), x->GetType(), x->GetFilename() });
+ }
+ return ret;
+}
+
+ChatInputAttachmentContainer::type_signal_emptied ChatInputAttachmentContainer::signal_emptied() {
+ return m_signal_emptied;
+}
+
+ChatInputAttachmentItem::ChatInputAttachmentItem(const Glib::RefPtr<Gio::File> &file)
+ : m_file(file)
+ , m_img(Gtk::make_managed<Gtk::Image>())
+ , m_type(ChatSubmitParams::ExtantFile)
+ , m_box(Gtk::ORIENTATION_VERTICAL) {
+ get_style_context()->add_class("attachment-item");
+
+ set_size_request(AttachmentItemSize, AttachmentItemSize);
+ set_halign(Gtk::ALIGN_START);
+ m_box.set_hexpand(true);
+ m_box.set_vexpand(true);
+ m_box.set_halign(Gtk::ALIGN_FILL);
+ m_box.set_valign(Gtk::ALIGN_FILL);
+ m_box.add(*m_img);
+ m_box.add(m_label);
+ add(m_box);
+ show_all_children();
+
+ m_label.set_valign(Gtk::ALIGN_END);
+ m_label.set_max_width_chars(0); // will constrain to given size
+ m_label.set_ellipsize(Pango::ELLIPSIZE_MIDDLE);
+ m_label.set_margin_start(7);
+ m_label.set_margin_end(7);
+
+ m_img->set_vexpand(true);
+ m_img->property_icon_name() = "document-send-symbolic";
+ m_img->property_icon_size() = Gtk::ICON_SIZE_DIALOG; // todo figure out how to not use this weird property??? i dont know how icons work (screw your theme)
+
+ SetFilenameFromFile();
+
+ SetupMenu();
+ UpdateTooltip();
+}
+
+ChatInputAttachmentItem::ChatInputAttachmentItem(const Glib::RefPtr<Gio::File> &file, const Glib::RefPtr<Gdk::Pixbuf> &pb, bool is_extant)
+ : m_file(file)
+ , m_img(Gtk::make_managed<Gtk::Image>())
+ , m_type(is_extant ? ChatSubmitParams::ExtantFile : ChatSubmitParams::PastedImage)
+ , m_filename("unknown.png")
+ , m_label("unknown.png")
+ , m_box(Gtk::ORIENTATION_VERTICAL) {
+ get_style_context()->add_class("attachment-item");
+
+ int outw, outh;
+ GetImageDimensions(pb->get_width(), pb->get_height(), outw, outh, AttachmentItemSize, AttachmentItemSize);
+ m_img->property_pixbuf() = pb->scale_simple(outw, outh, Gdk::INTERP_BILINEAR);
+
+ set_size_request(AttachmentItemSize, AttachmentItemSize);
+ set_halign(Gtk::ALIGN_START);
+ m_box.set_hexpand(true);
+ m_box.set_vexpand(true);
+ m_box.set_halign(Gtk::ALIGN_FILL);
+ m_box.set_valign(Gtk::ALIGN_FILL);
+ m_box.add(*m_img);
+ m_box.add(m_label);
+ add(m_box);
+ show_all_children();
+
+ m_label.set_valign(Gtk::ALIGN_END);
+ m_label.set_max_width_chars(0); // will constrain to given size
+ m_label.set_ellipsize(Pango::ELLIPSIZE_MIDDLE);
+ m_label.set_margin_start(7);
+ m_label.set_margin_end(7);
+
+ m_img->set_vexpand(true);
+
+ if (is_extant)
+ SetFilenameFromFile();
+
+ SetupMenu();
+ UpdateTooltip();
+}
+
+Glib::RefPtr<Gio::File> ChatInputAttachmentItem::GetFile() const {
+ return m_file;
+}
+
+ChatSubmitParams::AttachmentType ChatInputAttachmentItem::GetType() const {
+ return m_type;
+}
+
+std::string ChatInputAttachmentItem::GetFilename() const {
+ return m_filename;
+}
+
+bool ChatInputAttachmentItem::IsTemp() const noexcept {
+ return m_type == ChatSubmitParams::PastedImage;
+}
+
+void ChatInputAttachmentItem::RemoveIfTemp() {
+ if (IsTemp())
+ m_file->remove();
+}
+
+void ChatInputAttachmentItem::SetFilenameFromFile() {
+ auto info = m_file->query_info(G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME);
+ m_filename = info->get_attribute_string(G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME);
+ m_label.set_text(m_filename);
+}
+
+void ChatInputAttachmentItem::SetupMenu() {
+ m_menu_remove.set_label("Remove");
+ m_menu_remove.signal_activate().connect([this] {
+ m_signal_item_removed.emit();
+ });
+
+ m_menu_set_filename.set_label("Change Filename");
+ m_menu_set_filename.signal_activate().connect([this] {
+ const auto name = Abaddon::Get().ShowTextPrompt("Enter new filename for attachment", "Enter filename", m_filename);
+ if (name.has_value()) {
+ m_filename = *name;
+ m_label.set_text(m_filename);
+ UpdateTooltip();
+ }
+ });
+
+ m_menu.add(m_menu_set_filename);
+ m_menu.add(m_menu_remove);
+ m_menu.show_all();
+
+ signal_button_press_event().connect([this](GdkEventButton *ev) -> bool {
+ if (ev->button == GDK_BUTTON_SECONDARY) {
+ m_menu.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
+ return true;
+ }
+
+ return false;
+ });
+}
+
+void ChatInputAttachmentItem::UpdateTooltip() {
+ set_tooltip_text(m_filename);
+}
+
+ChatInputAttachmentItem::type_signal_item_removed ChatInputAttachmentItem::signal_item_removed() {
+ return m_signal_item_removed;
+}
+
+ChatInput::ChatInput()
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL) {
+ m_input.signal_add_attachment().connect(sigc::mem_fun(*this, &ChatInput::AddAttachment));
+
+ m_input.Get().signal_escape().connect([this] {
+ m_attachments.Clear();
+ m_attachments_revealer.set_reveal_child(false);
+ m_signal_escape.emit();
+ });
+
+ m_input.Get().signal_submit().connect([this](const Glib::ustring &input) -> bool {
+ ChatSubmitParams data;
+ data.Message = input;
+ data.Attachments = m_attachments.GetAttachments();
+
+ bool b = m_signal_submit.emit(data);
+ if (b) {
+ m_attachments_revealer.set_reveal_child(false);
+ m_attachments.ClearNoPurge();
+ }
+ return b;
+ });
+
+ m_attachments.set_vexpand(false);
+
+ m_attachments_revealer.set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_SLIDE_UP);
+ m_attachments_revealer.add(m_attachments);
+ add(m_attachments_revealer);
+ add(m_input);
+ show_all_children();
+
+ m_input.Get().signal_image_paste().connect([this](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
+ if (CanAttachFiles() && m_attachments.AddImage(pb))
+ m_attachments_revealer.set_reveal_child(true);
+ });
+
+ // double hack !
+ auto cb = [this](GdkEventKey *e) -> bool {
+ return event(reinterpret_cast<GdkEvent *>(e));
+ };
+ m_input.signal_key_press_event().connect(cb, false);
+
+ m_attachments.signal_emptied().connect([this] {
+ m_attachments_revealer.set_reveal_child(false);
+ });
+
+ SetActiveChannel(Snowflake::Invalid);
+}
+
+void ChatInput::InsertText(const Glib::ustring &text) {
+ m_input.Get().InsertText(text);
+}
+
+Glib::RefPtr<Gtk::TextBuffer> ChatInput::GetBuffer() {
+ return m_input.Get().GetBuffer();
+}
+
+bool ChatInput::ProcessKeyPress(GdkEventKey *event) {
+ return m_input.Get().ProcessKeyPress(event);
+}
+
+void ChatInput::AddAttachment(const Glib::RefPtr<Gio::File> &file) {
+ if (!CanAttachFiles()) return;
+
+ std::string content_type;
+
+ try {
+ const auto info = file->query_info(G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE);
+ content_type = info->get_attribute_string(G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE);
+ } catch (const Gio::Error &err) {
+ printf("io error: %s\n", err.what().c_str());
+ return;
+ } catch (...) {
+ puts("attachment query exception");
+ return;
+ }
+
+ static const std::unordered_set<std::string> image_exts {
+ ".png",
+ ".jpg",
+ };
+
+ if (image_exts.find(content_type) != image_exts.end()) {
+ if (AddFileAsImageAttachment(file)) {
+ m_attachments_revealer.set_reveal_child(true);
+ m_input.Get().grab_focus();
+ }
+ } else if (m_attachments.AddFile(file)) {
+ m_attachments_revealer.set_reveal_child(true);
+ m_input.Get().grab_focus();
+ }
+}
+
+void ChatInput::IndicateTooLarge() {
+ m_input.Get().get_style_context()->add_class("bad-input");
+ const auto cb = [this] {
+ m_input.Get().get_style_context()->remove_class("bad-input");
+ };
+ Glib::signal_timeout().connect_seconds_once(sigc::track_obj(cb, *this), 2);
+}
+
+void ChatInput::SetActiveChannel(Snowflake id) {
+ m_active_channel = id;
+ if (CanAttachFiles()) {
+ m_input.Get().get_style_context()->add_class("with-browse-icon");
+ m_input.ShowChooserIcon();
+ } else {
+ m_input.Get().get_style_context()->remove_class("with-browse-icon");
+ m_input.HideChooserIcon();
+ }
+}
+
+void ChatInput::StartReplying() {
+ m_input.Get().grab_focus();
+ m_input.Get().get_style_context()->add_class("replying");
+}
+
+void ChatInput::StopReplying() {
+ m_input.Get().get_style_context()->remove_class("replying");
+}
+
+bool ChatInput::AddFileAsImageAttachment(const Glib::RefPtr<Gio::File> &file) {
+ try {
+ const auto read_stream = file->read();
+ if (!read_stream) return false;
+ const auto pb = Gdk::Pixbuf::create_from_stream(read_stream);
+ return m_attachments.AddFile(file, pb);
+ } catch (...) {
+ return m_attachments.AddFile(file);
+ }
+}
+
+bool ChatInput::CanAttachFiles() {
+ return Abaddon::Get().GetDiscordClient().HasSelfChannelPermission(m_active_channel, Permission::ATTACH_FILES | Permission::SEND_MESSAGES);
+}
+
ChatInput::type_signal_submit ChatInput::signal_submit() {
return m_signal_submit;
}
diff --git a/src/components/chatinput.hpp b/src/components/chatinput.hpp
index ad7f0b1..807f958 100644
--- a/src/components/chatinput.hpp
+++ b/src/components/chatinput.hpp
@@ -1,9 +1,72 @@
#pragma once
#include <gtkmm.h>
+#include "discord/chatsubmitparams.hpp"
+#include "discord/permissions.hpp"
-class ChatInput : public Gtk::ScrolledWindow {
+class ChatInputAttachmentItem : public Gtk::EventBox {
public:
- ChatInput();
+ ChatInputAttachmentItem(const Glib::RefPtr<Gio::File> &file);
+ ChatInputAttachmentItem(const Glib::RefPtr<Gio::File> &file, const Glib::RefPtr<Gdk::Pixbuf> &pb, bool is_extant = false);
+
+ [[nodiscard]] Glib::RefPtr<Gio::File> GetFile() const;
+ [[nodiscard]] ChatSubmitParams::AttachmentType GetType() const;
+ [[nodiscard]] std::string GetFilename() const;
+ [[nodiscard]] bool IsTemp() const noexcept;
+ void RemoveIfTemp();
+
+private:
+ void SetFilenameFromFile();
+ void SetupMenu();
+ void UpdateTooltip();
+
+ Gtk::Menu m_menu;
+ Gtk::MenuItem m_menu_remove;
+ Gtk::MenuItem m_menu_set_filename;
+
+ Gtk::Box m_box;
+ Gtk::Label m_label;
+ Gtk::Image *m_img = nullptr;
+
+ Glib::RefPtr<Gio::File> m_file;
+ ChatSubmitParams::AttachmentType m_type;
+ std::string m_filename;
+
+private:
+ using type_signal_item_removed = sigc::signal<void>;
+
+ type_signal_item_removed m_signal_item_removed;
+
+public:
+ type_signal_item_removed signal_item_removed();
+};
+
+class ChatInputAttachmentContainer : public Gtk::ScrolledWindow {
+public:
+ ChatInputAttachmentContainer();
+
+ void Clear();
+ void ClearNoPurge();
+ bool AddImage(const Glib::RefPtr<Gdk::Pixbuf> &pb);
+ bool AddFile(const Glib::RefPtr<Gio::File> &file, Glib::RefPtr<Gdk::Pixbuf> pb = {});
+ [[nodiscard]] std::vector<ChatSubmitParams::Attachment> GetAttachments() const;
+
+private:
+ std::vector<ChatInputAttachmentItem *> m_attachments;
+
+ Gtk::Box m_box;
+
+private:
+ using type_signal_emptied = sigc::signal<void>;
+
+ type_signal_emptied m_signal_emptied;
+
+public:
+ type_signal_emptied signal_emptied();
+};
+
+class ChatInputText : public Gtk::ScrolledWindow {
+public:
+ ChatInputText();
void InsertText(const Glib::ustring &text);
Glib::RefPtr<Gtk::TextBuffer> GetBuffer();
@@ -15,9 +78,78 @@ protected:
private:
Gtk::TextView m_textview;
+ bool CheckHandleClipboardPaste();
+
+public:
+ using type_signal_submit = sigc::signal<bool, Glib::ustring>;
+ using type_signal_escape = sigc::signal<void>;
+ using type_signal_image_paste = sigc::signal<void, Glib::RefPtr<Gdk::Pixbuf>>;
+
+ type_signal_submit signal_submit();
+ type_signal_escape signal_escape();
+ type_signal_image_paste signal_image_paste();
+
+private:
+ type_signal_submit m_signal_submit;
+ type_signal_escape m_signal_escape;
+ type_signal_image_paste m_signal_image_paste;
+};
+
+// file upload, text
+class ChatInputTextContainer : public Gtk::Overlay {
+public:
+ ChatInputTextContainer();
+
+ // not proxying everythign lol!!
+ ChatInputText &Get();
+
+ void ShowChooserIcon();
+ void HideChooserIcon();
+
+private:
+ void ShowFileChooser();
+ bool GetChildPosition(Gtk::Widget *child, Gdk::Rectangle &pos);
+
+ Gtk::EventBox m_upload_ev;
+ Gtk::Image m_upload_img;
+ ChatInputText m_input;
+
+public:
+ using type_signal_add_attachment = sigc::signal<void, Glib::RefPtr<Gio::File>>;
+ type_signal_add_attachment signal_add_attachment();
+
+private:
+ type_signal_add_attachment m_signal_add_attachment;
+};
+
+class ChatInput : public Gtk::Box {
+public:
+ ChatInput();
+
+ void InsertText(const Glib::ustring &text);
+ Glib::RefPtr<Gtk::TextBuffer> GetBuffer();
+ bool ProcessKeyPress(GdkEventKey *event);
+ void AddAttachment(const Glib::RefPtr<Gio::File> &file);
+ void IndicateTooLarge();
+
+ void SetActiveChannel(Snowflake id);
+
+ void StartReplying();
+ void StopReplying();
+
+private:
+ bool AddFileAsImageAttachment(const Glib::RefPtr<Gio::File> &file);
+ bool CanAttachFiles();
+
+ Gtk::Revealer m_attachments_revealer;
+ ChatInputAttachmentContainer m_attachments;
+ ChatInputTextContainer m_input;
+
+ Snowflake m_active_channel;
+
public:
- typedef sigc::signal<bool, Glib::ustring> type_signal_submit;
- typedef sigc::signal<void> type_signal_escape;
+ using type_signal_submit = sigc::signal<bool, ChatSubmitParams>;
+ using type_signal_escape = sigc::signal<void>;
type_signal_submit signal_submit();
type_signal_escape signal_escape();
diff --git a/src/components/chatlist.cpp b/src/components/chatlist.cpp
index 5b923b5..d2995ee 100644
--- a/src/components/chatlist.cpp
+++ b/src/components/chatlist.cpp
@@ -34,6 +34,9 @@ void ChatList::Clear() {
delete *it;
it++;
}
+ m_id_to_widget.clear();
+ m_num_messages = 0;
+ m_num_rows = 0;
}
void ChatList::SetActiveChannel(Snowflake id) {
@@ -352,10 +355,6 @@ ChatList::type_signal_action_message_edit ChatList::signal_action_message_edit()
return m_signal_action_message_edit;
}
-ChatList::type_signal_action_chat_submit ChatList::signal_action_chat_submit() {
- return m_signal_action_chat_submit;
-}
-
ChatList::type_signal_action_chat_load_history ChatList::signal_action_chat_load_history() {
return m_signal_action_chat_load_history;
}
diff --git a/src/components/chatlist.hpp b/src/components/chatlist.hpp
index f77bbd6..9cc6992 100644
--- a/src/components/chatlist.hpp
+++ b/src/components/chatlist.hpp
@@ -63,7 +63,6 @@ private:
public:
// these are all forwarded by the parent
using type_signal_action_message_edit = sigc::signal<void, Snowflake, Snowflake>;
- using type_signal_action_chat_submit = sigc::signal<void, std::string, Snowflake, Snowflake>;
using type_signal_action_chat_load_history = sigc::signal<void, Snowflake>;
using type_signal_action_channel_click = sigc::signal<void, Snowflake>;
using type_signal_action_insert_mention = sigc::signal<void, Snowflake>;
@@ -73,7 +72,6 @@ public:
using type_signal_action_reply_to = sigc::signal<void, Snowflake>;
type_signal_action_message_edit signal_action_message_edit();
- type_signal_action_chat_submit signal_action_chat_submit();
type_signal_action_chat_load_history signal_action_chat_load_history();
type_signal_action_channel_click signal_action_channel_click();
type_signal_action_insert_mention signal_action_insert_mention();
@@ -84,7 +82,6 @@ public:
private:
type_signal_action_message_edit m_signal_action_message_edit;
- type_signal_action_chat_submit m_signal_action_chat_submit;
type_signal_action_chat_load_history m_signal_action_chat_load_history;
type_signal_action_channel_click m_signal_action_channel_click;
type_signal_action_insert_mention m_signal_action_insert_mention;
diff --git a/src/components/chatmessage.cpp b/src/components/chatmessage.cpp
index fd71fce..1aca81d 100644
--- a/src/components/chatmessage.cpp
+++ b/src/components/chatmessage.cpp
@@ -1135,7 +1135,7 @@ ChatMessageHeader::ChatMessageHeader(const Message &data)
m_meta_ev.signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageHeader::on_author_button_press));
- if (author->IsBot || data.WebhookID.has_value()) {
+ if (author->IsABot() || data.WebhookID.has_value()) {
m_extra = Gtk::manage(new Gtk::Label);
m_extra->get_style_context()->add_class("message-container-extra");
m_extra->set_single_line_mode(true);
@@ -1143,7 +1143,7 @@ ChatMessageHeader::ChatMessageHeader(const Message &data)
m_extra->set_can_focus(false);
m_extra->set_use_markup(true);
}
- if (author->IsBot)
+ if (author->IsABot())
m_extra->set_markup("<b>BOT</b>");
else if (data.WebhookID.has_value())
m_extra->set_markup("<b>Webhook</b>");
diff --git a/src/components/chatwindow.cpp b/src/components/chatwindow.cpp
index 5aab4e6..c5b4d14 100644
--- a/src/components/chatwindow.cpp
+++ b/src/components/chatwindow.cpp
@@ -4,12 +4,14 @@
#include "ratelimitindicator.hpp"
#include "chatinput.hpp"
#include "chatlist.hpp"
+#include "constants.hpp"
#ifdef WITH_LIBHANDY
#include "channeltabswitcherhandy.hpp"
#endif
ChatWindow::ChatWindow() {
- Abaddon::Get().GetDiscordClient().signal_message_send_fail().connect(sigc::mem_fun(*this, &ChatWindow::OnMessageSendFail));
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ discord.signal_message_send_fail().connect(sigc::mem_fun(*this, &ChatWindow::OnMessageSendFail));
m_main = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
m_chat = Gtk::manage(new ChatList);
@@ -45,6 +47,8 @@ ChatWindow::ChatWindow() {
m_topic_text.set_halign(Gtk::ALIGN_START);
m_topic_text.show();
+ m_input->set_valign(Gtk::ALIGN_END);
+
m_input->signal_submit().connect(sigc::mem_fun(*this, &ChatWindow::OnInputSubmit));
m_input->signal_escape().connect([this]() {
if (m_is_replying)
@@ -54,11 +58,11 @@ ChatWindow::ChatWindow() {
m_input->show();
m_completer.SetBuffer(m_input->GetBuffer());
- m_completer.SetGetChannelID([this]() -> auto {
+ m_completer.SetGetChannelID([this]() {
return m_active_channel;
});
- m_completer.SetGetRecentAuthors([this]() -> auto {
+ m_completer.SetGetRecentAuthors([this]() {
return m_chat->GetRecentAuthors();
});
@@ -70,9 +74,6 @@ ChatWindow::ChatWindow() {
m_chat->signal_action_chat_load_history().connect([this](Snowflake id) {
m_signal_action_chat_load_history.emit(id);
});
- m_chat->signal_action_chat_submit().connect([this](const std::string &str, Snowflake channel_id, Snowflake referenced_id) {
- m_signal_action_chat_submit.emit(str, channel_id, referenced_id);
- });
m_chat->signal_action_insert_mention().connect([this](Snowflake id) {
// lowkey gross
m_signal_action_insert_mention.emit(id);
@@ -107,6 +108,10 @@ ChatWindow::ChatWindow() {
m_main->add(m_completer);
m_main->add(*m_input);
m_main->add(*m_meta);
+ m_main->add(m_progress);
+
+ m_progress.show();
+
m_main->show();
}
@@ -125,6 +130,7 @@ void ChatWindow::SetMessages(const std::vector<Message> &msgs) {
void ChatWindow::SetActiveChannel(Snowflake id) {
m_active_channel = id;
m_chat->SetActiveChannel(id);
+ m_input->SetActiveChannel(id);
m_input_indicator->SetActiveChannel(id);
m_rate_limit_indicator->SetActiveChannel(id);
if (m_is_replying)
@@ -168,6 +174,10 @@ void ChatWindow::SetTopic(const std::string &text) {
m_topic.set_visible(text.length() > 0);
}
+void ChatWindow::AddAttachment(const Glib::RefPtr<Gio::File> &file) {
+ m_input->AddAttachment(file);
+}
+
#ifdef WITH_LIBHANDY
void ChatWindow::OpenNewTab(Snowflake id) {
// open if its the first tab (in which case it really isnt a tab but whatever)
@@ -210,15 +220,64 @@ Snowflake ChatWindow::GetActiveChannel() const {
return m_active_channel;
}
-bool ChatWindow::OnInputSubmit(const Glib::ustring &text) {
+bool ChatWindow::OnInputSubmit(ChatSubmitParams data) {
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ if (!discord.HasSelfChannelPermission(m_active_channel, Permission::SEND_MESSAGES)) return false;
+ if (!data.Attachments.empty() && !discord.HasSelfChannelPermission(m_active_channel, Permission::ATTACH_FILES)) return false;
+
+ int nitro_restriction = BaseAttachmentSizeLimit;
+ const auto nitro = discord.GetUserData().PremiumType;
+ if (!nitro.has_value() || nitro == EPremiumType::None) {
+ nitro_restriction = BaseAttachmentSizeLimit;
+ } else if (nitro == EPremiumType::NitroClassic) {
+ nitro_restriction = NitroClassicAttachmentSizeLimit;
+ } else if (nitro == EPremiumType::Nitro) {
+ nitro_restriction = NitroAttachmentSizeLimit;
+ }
+
+ int guild_restriction = BaseAttachmentSizeLimit;
+ if (const auto channel = discord.GetChannel(m_active_channel); channel.has_value() && channel->GuildID.has_value()) {
+ if (const auto guild = discord.GetGuild(*channel->GuildID); guild.has_value()) {
+ if (!guild->PremiumTier.has_value() || guild->PremiumTier == GuildPremiumTier::NONE || guild->PremiumTier == GuildPremiumTier::TIER_1) {
+ guild_restriction = BaseAttachmentSizeLimit;
+ } else if (guild->PremiumTier == GuildPremiumTier::TIER_2) {
+ guild_restriction = BoostLevel2AttachmentSizeLimit;
+ } else if (guild->PremiumTier == GuildPremiumTier::TIER_3) {
+ guild_restriction = BoostLevel3AttachmentSizeLimit;
+ }
+ }
+ }
+
+ int restriction = std::max(nitro_restriction, guild_restriction);
+
+ goffset total_size = 0;
+ for (const auto &attachment : data.Attachments) {
+ const auto info = attachment.File->query_info();
+ if (info) {
+ const auto size = info->get_size();
+ if (size > restriction) {
+ m_input->IndicateTooLarge();
+ return false;
+ }
+ total_size += size;
+ if (total_size > MaxMessagePayloadSize) {
+ m_input->IndicateTooLarge();
+ return false;
+ }
+ }
+ }
+
if (!m_rate_limit_indicator->CanSpeak())
return false;
- if (text.empty())
+ if (data.Message.empty() && data.Attachments.empty())
return false;
+ data.ChannelID = m_active_channel;
+ data.InReplyToID = m_replying_to;
+
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
+ m_signal_action_chat_submit.emit(data); // m_replying_to is checked for invalid in the handler
if (m_is_replying)
StopReplying();
@@ -241,8 +300,7 @@ void ChatWindow::StartReplying(Snowflake 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");
+ m_input->StartReplying();
if (author.has_value())
m_input_indicator->SetCustomMarkup("Replying to " + author->GetEscapedBoldString<false>());
else
@@ -252,7 +310,7 @@ void ChatWindow::StartReplying(Snowflake message_id) {
void ChatWindow::StopReplying() {
m_is_replying = false;
m_replying_to = Snowflake::Invalid;
- m_input->get_style_context()->remove_class("replying");
+ m_input->StopReplying();
m_input_indicator->ClearCustom();
}
diff --git a/src/components/chatwindow.hpp b/src/components/chatwindow.hpp
index 9b27ff1..802826b 100644
--- a/src/components/chatwindow.hpp
+++ b/src/components/chatwindow.hpp
@@ -3,8 +3,10 @@
#include <string>
#include <set>
#include "discord/discord.hpp"
+#include "discord/chatsubmitparams.hpp"
#include "completer.hpp"
#include "state.hpp"
+#include "progressbar.hpp"
#ifdef WITH_LIBHANDY
class ChannelTabSwitcherHandy;
@@ -34,6 +36,7 @@ public:
Snowflake GetOldestListedMessage(); // oldest message that is currently in the ListBox
void UpdateReactions(Snowflake id);
void SetTopic(const std::string &text);
+ void AddAttachment(const Glib::RefPtr<Gio::File> &file);
#ifdef WITH_LIBHANDY
void OpenNewTab(Snowflake id);
@@ -55,7 +58,7 @@ protected:
Snowflake m_active_channel;
- bool OnInputSubmit(const Glib::ustring &text);
+ bool OnInputSubmit(ChatSubmitParams data);
bool OnKeyPressEvent(GdkEventKey *e);
void OnScrollEdgeOvershot(Gtk::PositionType pos);
@@ -77,6 +80,7 @@ protected:
ChatInputIndicator *m_input_indicator;
RateLimitIndicator *m_rate_limit_indicator;
Gtk::Box *m_meta;
+ MessageUploadProgressBar m_progress;
#ifdef WITH_LIBHANDY
ChannelTabSwitcherHandy *m_tab_switcher;
@@ -84,7 +88,7 @@ protected:
public:
using type_signal_action_message_edit = sigc::signal<void, Snowflake, Snowflake>;
- using type_signal_action_chat_submit = sigc::signal<void, std::string, Snowflake, Snowflake>;
+ using type_signal_action_chat_submit = sigc::signal<void, ChatSubmitParams>;
using type_signal_action_chat_load_history = sigc::signal<void, Snowflake>;
using type_signal_action_channel_click = sigc::signal<void, Snowflake, bool>;
using type_signal_action_insert_mention = sigc::signal<void, Snowflake>;
diff --git a/src/components/progressbar.cpp b/src/components/progressbar.cpp
new file mode 100644
index 0000000..aa5d748
--- /dev/null
+++ b/src/components/progressbar.cpp
@@ -0,0 +1,24 @@
+#include "progressbar.hpp"
+#include "abaddon.hpp"
+
+MessageUploadProgressBar::MessageUploadProgressBar() {
+ get_style_context()->add_class("message-progress");
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ discord.signal_message_progress().connect([this](const std::string &nonce, float percent) {
+ if (nonce == m_last_nonce) {
+ set_fraction(percent);
+ }
+ });
+ discord.signal_message_send_fail().connect([this](const std::string &nonce, float) {
+ if (nonce == m_last_nonce)
+ set_fraction(0.0);
+ });
+ discord.signal_message_create().connect([this](const Message &msg) {
+ if (msg.IsPending) {
+ m_last_nonce = *msg.Nonce;
+ } else if (msg.Nonce.has_value() && (*msg.Nonce == m_last_nonce)) {
+ m_last_nonce = "";
+ set_fraction(0.0);
+ }
+ });
+}
diff --git a/src/components/progressbar.hpp b/src/components/progressbar.hpp
new file mode 100644
index 0000000..b73521b
--- /dev/null
+++ b/src/components/progressbar.hpp
@@ -0,0 +1,11 @@
+#pragma once
+#include <gtkmm/progressbar.h>
+#include <string>
+
+class MessageUploadProgressBar : public Gtk::ProgressBar {
+public:
+ MessageUploadProgressBar();
+
+private:
+ std::string m_last_nonce;
+};
diff --git a/src/constants.hpp b/src/constants.hpp
index 6c6276f..b1ef82b 100644
--- a/src/constants.hpp
+++ b/src/constants.hpp
@@ -1,4 +1,12 @@
+#pragma once
#include <cstdint>
constexpr static uint64_t SnowflakeSplitDifference = 600;
constexpr static int MaxMessagesForChatCull = 50; // this has to be 50 (for now) cuz that magic number is used in a couple other places and i dont feel like replacing them
+constexpr static int AttachmentItemSize = 120;
+constexpr static int BaseAttachmentSizeLimit = 8 * 1024 * 1024;
+constexpr static int NitroClassicAttachmentSizeLimit = 50 * 1024 * 1024;
+constexpr static int NitroAttachmentSizeLimit = 100 * 1024 * 1024;
+constexpr static int BoostLevel2AttachmentSizeLimit = 50 * 1024 * 1024;
+constexpr static int BoostLevel3AttachmentSizeLimit = 100 * 1024 * 1024;
+constexpr static int MaxMessagePayloadSize = 199 * 1024 * 1024;
diff --git a/src/dialogs/textinput.cpp b/src/dialogs/textinput.cpp
new file mode 100644
index 0000000..ae75f70
--- /dev/null
+++ b/src/dialogs/textinput.cpp
@@ -0,0 +1,26 @@
+#include "textinput.hpp"
+
+TextInputDialog::TextInputDialog(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder, Gtk::Window &parent)
+ : Gtk::Dialog(title, parent, true)
+ , m_label(prompt) {
+ get_style_context()->add_class("app-window");
+ get_style_context()->add_class("app-popup");
+
+ auto ok = add_button("OK", Gtk::RESPONSE_OK);
+ auto cancel = add_button("Cancel", Gtk::RESPONSE_CANCEL);
+
+ get_content_area()->add(m_label);
+ get_content_area()->add(m_entry);
+
+ m_entry.set_text(placeholder);
+
+ m_entry.set_activates_default(true);
+ ok->set_can_default(true);
+ ok->grab_default();
+
+ show_all_children();
+}
+
+Glib::ustring TextInputDialog::GetInput() const {
+ return m_entry.get_text();
+}
diff --git a/src/dialogs/textinput.hpp b/src/dialogs/textinput.hpp
new file mode 100644
index 0000000..fd2d2b8
--- /dev/null
+++ b/src/dialogs/textinput.hpp
@@ -0,0 +1,14 @@
+#pragma once
+#include <gtkmm/dialog.h>
+#include <gtkmm/entry.h>
+
+class TextInputDialog : public Gtk::Dialog {
+public:
+ TextInputDialog(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder, Gtk::Window &parent);
+
+ Glib::ustring GetInput() const;
+
+private:
+ Gtk::Label m_label;
+ Gtk::Entry m_entry;
+};
diff --git a/src/discord/chatsubmitparams.hpp b/src/discord/chatsubmitparams.hpp
new file mode 100644
index 0000000..6199634
--- /dev/null
+++ b/src/discord/chatsubmitparams.hpp
@@ -0,0 +1,24 @@
+#pragma once
+#include <vector>
+#include <string>
+#include <glibmm/ustring.h>
+#include <giomm/file.h>
+#include "discord/snowflake.hpp"
+
+struct ChatSubmitParams {
+ enum AttachmentType {
+ PastedImage,
+ ExtantFile,
+ };
+
+ struct Attachment {
+ Glib::RefPtr<Gio::File> File;
+ AttachmentType Type;
+ std::string Filename;
+ };
+
+ Snowflake ChannelID;
+ Snowflake InReplyToID;
+ Glib::ustring Message;
+ std::vector<Attachment> Attachments;
+};
diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp
index 4b6f6dd..48f08d6 100644
--- a/src/discord/discord.cpp
+++ b/src/discord/discord.cpp
@@ -319,6 +319,10 @@ bool DiscordClient::HasGuildPermission(Snowflake user_id, Snowflake guild_id, Pe
return (base & perm) == perm;
}
+bool DiscordClient::HasSelfChannelPermission(Snowflake channel_id, Permission perm) const {
+ return HasChannelPermission(m_user_data.ID, channel_id, perm);
+}
+
bool DiscordClient::HasAnyChannelPermission(Snowflake user_id, Snowflake channel_id, Permission perm) const {
const auto channel = m_store.GetChannel(channel_id);
if (!channel.has_value() || !channel->GuildID.has_value()) return false;
@@ -410,7 +414,7 @@ bool DiscordClient::CanManageMember(Snowflake guild_id, Snowflake actor, Snowfla
return actor_highest->Position > target_highest->Position;
}
-void DiscordClient::ChatMessageCallback(const std::string &nonce, const http::response_type &response) {
+void DiscordClient::ChatMessageCallback(const std::string &nonce, const http::response_type &response, const sigc::slot<void(DiscordError)> &callback) {
if (!CheckCode(response)) {
if (response.status_code == http::TooManyRequests) {
try { // not sure if this body is guaranteed
@@ -422,54 +426,108 @@ void DiscordClient::ChatMessageCallback(const std::string &nonce, const http::re
} else {
m_signal_message_send_fail.emit(nonce, 0);
}
+
+ // todo actually callback with correct error code (not necessary rn)
+ callback(DiscordError::GENERIC);
+ } else {
+ callback(DiscordError::NONE);
}
}
-void DiscordClient::SendChatMessage(const std::string &content, Snowflake channel) {
- // @([^@#]{1,32})#(\\d{4})
+void DiscordClient::SendChatMessageNoAttachments(const ChatSubmitParams &params, const sigc::slot<void(DiscordError)> &callback) {
const auto nonce = std::to_string(Snowflake::FromNow());
+
CreateMessageObject obj;
- obj.Content = content;
+ obj.Content = params.Message;
obj.Nonce = nonce;
- m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), sigc::bind<0>(sigc::mem_fun(*this, &DiscordClient::ChatMessageCallback), nonce));
- // dummy data so the content can be shown while waiting for MESSAGE_CREATE
+ if (params.InReplyToID.IsValid())
+ obj.MessageReference.emplace().MessageID = params.InReplyToID;
+
+ m_http.MakePOST("/channels/" + std::to_string(params.ChannelID) + "/messages",
+ nlohmann::json(obj).dump(),
+ [this, nonce, callback](const http::response_type &r) {
+ ChatMessageCallback(nonce, r, callback);
+ });
+
+ // dummy preview data
Message tmp;
- tmp.Content = content;
+ tmp.Content = params.Message;
tmp.ID = nonce;
- tmp.ChannelID = channel;
+ tmp.ChannelID = params.ChannelID;
tmp.Author = GetUserData();
tmp.IsTTS = false;
tmp.DoesMentionEveryone = false;
tmp.Type = MessageType::DEFAULT;
tmp.IsPinned = false;
tmp.Timestamp = "2000-01-01T00:00:00.000000+00:00";
- tmp.Nonce = obj.Nonce;
+ tmp.Nonce = nonce;
tmp.IsPending = true;
+
m_store.SetMessage(tmp.ID, tmp);
- m_signal_message_sent.emit(tmp);
+ m_signal_message_create.emit(tmp);
}
-void DiscordClient::SendChatMessage(const std::string &content, Snowflake channel, Snowflake referenced_message) {
+void DiscordClient::SendChatMessageAttachments(const ChatSubmitParams &params, const sigc::slot<void(DiscordError)> &callback) {
const auto nonce = std::to_string(Snowflake::FromNow());
+
CreateMessageObject obj;
- obj.Content = content;
+ obj.Content = params.Message;
obj.Nonce = nonce;
- obj.MessageReference.emplace().MessageID = referenced_message;
- m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), sigc::bind<0>(sigc::mem_fun(*this, &DiscordClient::ChatMessageCallback), nonce));
+ if (params.InReplyToID.IsValid())
+ obj.MessageReference.emplace().MessageID = params.InReplyToID;
+
+ auto req = m_http.CreateRequest(http::REQUEST_POST, "/channels/" + std::to_string(params.ChannelID) + "/messages");
+ m_progress_cb_timer.start();
+ req.set_progress_callback([this, nonce](curl_off_t ultotal, curl_off_t ulnow) {
+ if (m_progress_cb_timer.elapsed() < 0.0417) return; // try to prevent it from blocking ui
+ m_progress_cb_timer.start();
+ m_generic_mutex.lock();
+ m_generic_queue.push([this, nonce, ultotal, ulnow] {
+ m_signal_message_progress.emit(
+ nonce,
+ static_cast<float>(ulnow) / static_cast<float>(ultotal));
+ });
+ m_generic_mutex.unlock();
+ m_generic_dispatch.emit();
+ });
+ req.make_form();
+ req.add_field("payload_json", nlohmann::json(obj).dump().c_str(), CURL_ZERO_TERMINATED);
+ for (size_t i = 0; i < params.Attachments.size(); i++) {
+ const auto field_name = "files[" + std::to_string(i) + "]";
+ req.add_file(field_name, params.Attachments.at(i).File, params.Attachments.at(i).Filename);
+ }
+ m_http.Execute(std::move(req), [this, params, nonce, callback](const http::response_type &res) {
+ for (const auto &attachment : params.Attachments) {
+ if (attachment.Type == ChatSubmitParams::AttachmentType::PastedImage) {
+ attachment.File->remove();
+ }
+ }
+ ChatMessageCallback(nonce, res, callback);
+ });
+
+ // dummy preview data
Message tmp;
- tmp.Content = content;
+ tmp.Content = params.Message;
tmp.ID = nonce;
- tmp.ChannelID = channel;
+ tmp.ChannelID = params.ChannelID;
tmp.Author = GetUserData();
tmp.IsTTS = false;
tmp.DoesMentionEveryone = false;
tmp.Type = MessageType::DEFAULT;
tmp.IsPinned = false;
tmp.Timestamp = "2000-01-01T00:00:00.000000+00:00";
- tmp.Nonce = obj.Nonce;
+ tmp.Nonce = nonce;
tmp.IsPending = true;
+
m_store.SetMessage(tmp.ID, tmp);
- m_signal_message_sent.emit(tmp);
+ m_signal_message_create.emit(tmp);
+}
+
+void DiscordClient::SendChatMessage(const ChatSubmitParams &params, const sigc::slot<void(DiscordError)> &callback) {
+ if (params.Attachments.empty())
+ SendChatMessageNoAttachments(params, callback);
+ else
+ SendChatMessageAttachments(params, callback);
}
void DiscordClient::DeleteMessage(Snowflake channel_id, Snowflake id) {
@@ -2577,6 +2635,10 @@ DiscordClient::type_signal_connected DiscordClient::signal_connected() {
return m_signal_connected;
}
+DiscordClient::type_signal_message_progress DiscordClient::signal_message_progress() {
+ return m_signal_message_progress;
+}
+
DiscordClient::type_signal_role_update DiscordClient::signal_role_update() {
return m_signal_role_update;
}
diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp
index 75b681a..b1b623b 100644
--- a/src/discord/discord.hpp
+++ b/src/discord/discord.hpp
@@ -3,6 +3,7 @@
#include "httpclient.hpp"
#include "objects.hpp"
#include "store.hpp"
+#include "chatsubmitparams.hpp"
#include <sigc++/sigc++.h>
#include <nlohmann/json.hpp>
#include <thread>
@@ -96,16 +97,18 @@ public:
bool IsThreadJoined(Snowflake thread_id) const;
bool HasGuildPermission(Snowflake user_id, Snowflake guild_id, Permission perm) const;
+ bool HasSelfChannelPermission(Snowflake channel_id, Permission perm) const;
bool HasAnyChannelPermission(Snowflake user_id, Snowflake channel_id, Permission perm) const;
bool HasChannelPermission(Snowflake user_id, Snowflake channel_id, Permission perm) const;
Permission ComputePermissions(Snowflake member_id, Snowflake guild_id) const;
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 ChatMessageCallback(const std::string &nonce, const http::response_type &response);
+ void ChatMessageCallback(const std::string &nonce, const http::response_type &response, const sigc::slot<void(DiscordError code)> &callback);
+ void SendChatMessageNoAttachments(const ChatSubmitParams &params, const sigc::slot<void(DiscordError code)> &callback);
+ void SendChatMessageAttachments(const ChatSubmitParams &params, const sigc::slot<void(DiscordError code)> &callback);
- void SendChatMessage(const std::string &content, Snowflake channel);
- void SendChatMessage(const std::string &content, Snowflake channel, Snowflake referenced_message);
+ void SendChatMessage(const ChatSubmitParams &params, const sigc::slot<void(DiscordError code)> &callback);
void DeleteMessage(Snowflake channel_id, Snowflake id);
void EditMessage(Snowflake channel_id, Snowflake id, std::string content);
void SendLazyLoad(Snowflake id);
@@ -346,6 +349,8 @@ private:
Glib::Dispatcher m_generic_dispatch;
std::queue<std::function<void()>> m_generic_queue;
+ Glib::Timer m_progress_cb_timer;
+
std::set<Snowflake> m_channels_pinned_requested;
std::set<Snowflake> m_channels_lazy_loaded;
@@ -405,6 +410,7 @@ public:
typedef sigc::signal<void, std::string /* nonce */, float /* retry_after */> type_signal_message_send_fail; // retry after param will be 0 if it failed for a reason that isnt slowmode
typedef sigc::signal<void, bool, GatewayCloseCode> type_signal_disconnected; // bool true if reconnecting
typedef sigc::signal<void> type_signal_connected;
+ typedef sigc::signal<void, std::string, float> type_signal_message_progress;
type_signal_gateway_ready signal_gateway_ready();
type_signal_message_create signal_message_create();
@@ -458,6 +464,7 @@ public:
type_signal_message_send_fail signal_message_send_fail();
type_signal_disconnected signal_disconnected();
type_signal_connected signal_connected();
+ type_signal_message_progress signal_message_progress();
protected:
type_signal_gateway_ready m_signal_gateway_ready;
@@ -512,4 +519,5 @@ protected:
type_signal_message_send_fail m_signal_message_send_fail;
type_signal_disconnected m_signal_disconnected;
type_signal_connected m_signal_connected;
+ type_signal_message_progress m_signal_message_progress;
};
diff --git a/src/discord/guild.hpp b/src/discord/guild.hpp
index 152e250..0428928 100644
--- a/src/discord/guild.hpp
+++ b/src/discord/guild.hpp
@@ -16,6 +16,13 @@ enum class GuildApplicationStatus {
UNKNOWN,
};
+enum class GuildPremiumTier {
+ NONE = 0,
+ TIER_1 = 1,
+ TIER_2 = 2,
+ TIER_3 = 3,
+};
+
struct GuildApplicationData {
Snowflake UserID;
Snowflake GuildID;
@@ -73,7 +80,7 @@ struct GuildData {
std::optional<std::string> VanityURL; // null
std::optional<std::string> Description; // null
std::optional<std::string> BannerHash; // null
- std::optional<int> PremiumTier;
+ std::optional<GuildPremiumTier> PremiumTier;
std::optional<int> PremiumSubscriptionCount;
std::optional<std::string> PreferredLocale;
std::optional<Snowflake> PublicUpdatesChannelID; // null
diff --git a/src/discord/httpclient.cpp b/src/discord/httpclient.cpp
index cd699f0..d13246d 100644
--- a/src/discord/httpclient.cpp
+++ b/src/discord/httpclient.cpp
@@ -124,6 +124,25 @@ void HTTPClient::MakeGET(const std::string &path, const std::function<void(http:
}));
}
+http::request HTTPClient::CreateRequest(http::EMethod method, std::string path) {
+ http::request req(method, m_api_base + path);
+ req.set_header("Authorization", m_authorization);
+ req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon");
+#ifdef USE_LOCAL_PROXY
+ req.set_proxy("http://127.0.0.1:8888");
+ req.set_verify_ssl(false);
+#endif
+ return req;
+}
+
+void HTTPClient::Execute(http::request &&req, const std::function<void(http::response_type r)> &cb) {
+ printf("%s %s\n", req.get_method(), req.get_url().c_str());
+ m_futures.push_back(std::async(std::launch::async, [this, cb, req = std::move(req)]() mutable {
+ auto res = req.execute();
+ OnResponse(res, cb);
+ }));
+}
+
void HTTPClient::CleanupFutures() {
for (auto it = m_futures.begin(); it != m_futures.end();) {
if (it->wait_for(std::chrono::seconds(0)) == std::future_status::ready)
diff --git a/src/discord/httpclient.hpp b/src/discord/httpclient.hpp
index 841ce11..83b1f5a 100644
--- a/src/discord/httpclient.hpp
+++ b/src/discord/httpclient.hpp
@@ -25,6 +25,9 @@ public:
void MakePOST(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb);
void MakePUT(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb);
+ [[nodiscard]] http::request CreateRequest(http::EMethod method, std::string path);
+ void Execute(http::request &&req, const std::function<void(http::response_type r)> &cb);
+
private:
void AddHeaders(http::request &r);
diff --git a/src/discord/store.cpp b/src/discord/store.cpp
index f08f0c8..892f4aa 100644
--- a/src/discord/store.cpp
+++ b/src/discord/store.cpp
@@ -746,6 +746,7 @@ std::optional<GuildData> Store::GetGuild(Snowflake id) const {
s->Get(2, r.Icon);
s->Get(5, r.OwnerID);
s->Get(20, r.IsUnavailable);
+ s->Get(27, r.PremiumTier);
s->Reset();
diff --git a/src/discord/user.cpp b/src/discord/user.cpp
index 4393992..0ab2af5 100644
--- a/src/discord/user.cpp
+++ b/src/discord/user.cpp
@@ -1,6 +1,10 @@
#include "user.hpp"
#include "abaddon.hpp"
+bool UserData::IsABot() const noexcept {
+ return IsBot.has_value() && *IsBot;
+}
+
bool UserData::IsDeleted() const {
return Discriminator == "0000";
}
diff --git a/src/discord/user.hpp b/src/discord/user.hpp
index 083f5c4..1b9d517 100644
--- a/src/discord/user.hpp
+++ b/src/discord/user.hpp
@@ -60,6 +60,7 @@ struct UserData {
friend void to_json(nlohmann::json &j, const UserData &m);
void update_from_json(const nlohmann::json &j);
+ [[nodiscard]] bool IsABot() const noexcept;
[[nodiscard]] bool IsDeleted() const;
[[nodiscard]] bool HasAvatar() const;
[[nodiscard]] bool HasAnimatedAvatar() const noexcept;
diff --git a/src/http.cpp b/src/http.cpp
index 80fb829..0fae39f 100644
--- a/src/http.cpp
+++ b/src/http.cpp
@@ -29,12 +29,53 @@ request::request(EMethod method, std::string url)
prepare();
}
+request::request(request &&other) noexcept
+ : m_curl(std::exchange(other.m_curl, nullptr))
+ , m_url(std::exchange(other.m_url, ""))
+ , m_method(std::exchange(other.m_method, nullptr))
+ , m_header_list(std::exchange(other.m_header_list, nullptr))
+ , m_error_buf(other.m_error_buf)
+ , m_form(std::exchange(other.m_form, nullptr))
+ , m_read_streams(std::move(other.m_read_streams))
+ , m_progress_callback(std::move(other.m_progress_callback)) {
+ if (m_progress_callback) {
+ curl_easy_setopt(m_curl, CURLOPT_XFERINFODATA, this);
+ }
+}
+
request::~request() {
if (m_curl != nullptr)
curl_easy_cleanup(m_curl);
if (m_header_list != nullptr)
curl_slist_free_all(m_header_list);
+
+ if (m_form != nullptr)
+ curl_mime_free(m_form);
+}
+
+const std::string &request::get_url() const {
+ return m_url;
+}
+
+const char *request::get_method() const {
+ return m_method;
+}
+
+size_t http_req_xferinfofunc(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) {
+ if (ultotal > 0) {
+ auto *req = reinterpret_cast<request *>(clientp);
+ req->m_progress_callback(ultotal, ulnow);
+ }
+
+ return 0;
+}
+
+void request::set_progress_callback(std::function<void(curl_off_t, curl_off_t)> func) {
+ m_progress_callback = std::move(func);
+ curl_easy_setopt(m_curl, CURLOPT_NOPROGRESS, 0L);
+ curl_easy_setopt(m_curl, CURLOPT_XFERINFOFUNCTION, http_req_xferinfofunc);
+ curl_easy_setopt(m_curl, CURLOPT_XFERINFODATA, this);
}
void request::set_verify_ssl(bool verify) {
@@ -61,6 +102,42 @@ CURL *request::get_curl() {
return m_curl;
}
+void request::make_form() {
+ m_form = curl_mime_init(m_curl);
+}
+
+static size_t http_readfunc(char *buffer, size_t size, size_t nitems, void *arg) {
+ auto stream = Glib::wrap(G_FILE_INPUT_STREAM(arg), true);
+ int r = stream->read(buffer, size * nitems);
+ if (r == -1) {
+ // https://github.com/curl/curl/blob/ad9bc5976d6661cd5b03ebc379313bf657701c14/lib/mime.c#L724
+ return size_t(-1);
+ }
+ return r;
+}
+
+// file must exist until request completes
+void request::add_file(std::string_view name, const Glib::RefPtr<Gio::File> &file, std::string_view filename) {
+ if (!file->query_exists()) return;
+
+ auto *field = curl_mime_addpart(m_form);
+ curl_mime_name(field, name.data());
+ auto info = file->query_info();
+ auto stream = file->read();
+ curl_mime_data_cb(field, info->get_size(), http_readfunc, nullptr, nullptr, stream->gobj());
+ curl_mime_filename(field, filename.data());
+
+ // hold ref
+ m_read_streams.insert(stream);
+}
+
+// copied
+void request::add_field(std::string_view name, const char *data, size_t size) {
+ auto *field = curl_mime_addpart(m_form);
+ curl_mime_name(field, name.data());
+ curl_mime_data(field, data, size);
+}
+
response request::execute() {
if (m_curl == nullptr) {
auto response = detail::make_response(m_url, EStatusCode::ClientErrorCURLInit);
@@ -80,12 +157,14 @@ response request::execute() {
m_error_buf[0] = '\0';
if (m_header_list != nullptr)
curl_easy_setopt(m_curl, CURLOPT_HTTPHEADER, m_header_list);
+ if (m_form != nullptr)
+ curl_easy_setopt(m_curl, CURLOPT_MIMEPOST, m_form);
CURLcode result = curl_easy_perform(m_curl);
if (result != CURLE_OK) {
auto response = detail::make_response(m_url, EStatusCode::ClientErrorCURLPerform);
response.error_string = curl_easy_strerror(result);
- response.error_string += " " + std::string(m_error_buf);
+ response.error_string += " " + std::string(m_error_buf.data());
return response;
}
diff --git a/src/http.hpp b/src/http.hpp
index c44bce3..5bf3c69 100644
--- a/src/http.hpp
+++ b/src/http.hpp
@@ -1,8 +1,10 @@
#pragma once
+#include <array>
+#include <functional>
+#include <set>
#include <string>
#include <curl/curl.h>
-
-// i regret not using snake case for everything oh well
+#include <giomm/file.h>
namespace http {
enum EStatusCode : int {
@@ -98,13 +100,25 @@ struct response {
struct request {
request(EMethod method, std::string url);
+ request(request &&other) noexcept;
~request();
+ request(const request &) = delete;
+ request &operator=(const request &) = delete;
+ request &operator=(request &&) noexcept = delete;
+
+ const std::string &get_url() const;
+ const char *get_method() const;
+
+ void set_progress_callback(std::function<void(curl_off_t, curl_off_t)> func);
void set_verify_ssl(bool verify);
void set_proxy(const std::string &proxy);
void set_header(const std::string &name, const std::string &value);
void set_body(const std::string &data);
void set_user_agent(const std::string &data);
+ void make_form();
+ void add_file(std::string_view name, const Glib::RefPtr<Gio::File> &file, std::string_view filename);
+ void add_field(std::string_view name, const char *data, size_t size);
response execute();
@@ -117,7 +131,13 @@ private:
std::string m_url;
const char *m_method;
curl_slist *m_header_list = nullptr;
- char m_error_buf[CURL_ERROR_SIZE] = { 0 };
+ std::array<char, CURL_ERROR_SIZE> m_error_buf = { 0 };
+ curl_mime *m_form = nullptr;
+ std::function<void(curl_off_t, curl_off_t)> m_progress_callback;
+
+ std::set<Glib::RefPtr<Gio::FileInputStream>> m_read_streams;
+
+ friend size_t http_req_xferinfofunc(void *, curl_off_t, curl_off_t, curl_off_t, curl_off_t);
};
using response_type = response;
diff --git a/src/windows/guildsettings/memberspane.cpp b/src/windows/guildsettings/memberspane.cpp
index 34650ad..973e380 100644
--- a/src/windows/guildsettings/memberspane.cpp
+++ b/src/windows/guildsettings/memberspane.cpp
@@ -204,7 +204,7 @@ void GuildSettingsMembersPaneInfo::SetUser(Snowflake user_id) {
auto member = *discord.GetMember(user_id, GuildID);
member.User = discord.GetUser(user_id);
- m_bot.set_visible(member.User->IsBot.has_value() && *member.User->IsBot);
+ m_bot.set_visible(member.User->IsABot());
m_id.set_text("User ID: " + std::to_string(user_id));
m_created.set_text("Account created: " + user_id.GetLocalTimestamp());
diff --git a/src/windows/mainwindow.cpp b/src/windows/mainwindow.cpp
index a6a17c5..17edfa3 100644
--- a/src/windows/mainwindow.cpp
+++ b/src/windows/mainwindow.cpp
@@ -76,6 +76,7 @@ MainWindow::MainWindow()
add(m_main_box);
SetupMenu();
+ SetupDND();
}
void MainWindow::UpdateComponents() {
@@ -350,6 +351,22 @@ void MainWindow::SetupMenu() {
#endif
}
+void MainWindow::SetupDND() {
+ std::vector<Gtk::TargetEntry> targets;
+ targets.emplace_back("text/uri-list", Gtk::TargetFlags(0), 0);
+ drag_dest_set(targets, Gtk::DEST_DEFAULT_DROP | Gtk::DEST_DEFAULT_MOTION | Gtk::DEST_DEFAULT_HIGHLIGHT, Gdk::DragAction::ACTION_COPY);
+ signal_drag_data_received().connect([this](const Glib::RefPtr<Gdk::DragContext> &ctx, int x, int y, const Gtk::SelectionData &selection, guint info, guint time) {
+ HandleDroppedURIs(selection);
+ });
+}
+
+void MainWindow::HandleDroppedURIs(const Gtk::SelectionData &selection) {
+ for (const auto &uri : selection.get_uris()) {
+ // not using Glib::get_filename_for_uri or whatever because the conversion is BAD (on windows at least)
+ m_chat.AddAttachment(Gio::File::create_for_uri(uri));
+ }
+}
+
MainWindow::type_signal_action_connect MainWindow::signal_action_connect() {
return m_signal_action_connect;
}
diff --git a/src/windows/mainwindow.hpp b/src/windows/mainwindow.hpp
index e67f6d2..b5b6fc1 100644
--- a/src/windows/mainwindow.hpp
+++ b/src/windows/mainwindow.hpp
@@ -39,6 +39,9 @@ public:
private:
void SetupMenu();
+ void SetupDND();
+
+ void HandleDroppedURIs(const Gtk::SelectionData &selection);
Gtk::Box m_main_box;
Gtk::Box m_content_box;