start attachments (image paste and upload)
This commit is contained in:
@@ -743,7 +743,7 @@ void Abaddon::ActionChatLoadHistory(Snowflake id) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Abaddon::ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflake referenced_message) {
|
void Abaddon::ActionChatInputSubmit(std::string msg, const std::vector<std::string> &attachment_paths, Snowflake channel, Snowflake referenced_message) {
|
||||||
if (msg.substr(0, 7) == "/shrug " || msg == "/shrug")
|
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
|
msg = msg.substr(6) + "\xC2\xAF\x5C\x5F\x28\xE3\x83\x84\x29\x5F\x2F\xC2\xAF"; // this is important
|
||||||
|
|
||||||
@@ -751,9 +751,9 @@ void Abaddon::ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflak
|
|||||||
if (!m_discord.HasChannelPermission(m_discord.GetUserData().ID, channel, Permission::VIEW_CHANNEL)) return;
|
if (!m_discord.HasChannelPermission(m_discord.GetUserData().ID, channel, Permission::VIEW_CHANNEL)) return;
|
||||||
|
|
||||||
if (referenced_message.IsValid())
|
if (referenced_message.IsValid())
|
||||||
m_discord.SendChatMessage(msg, channel, referenced_message);
|
m_discord.SendChatMessage(msg, attachment_paths, channel, referenced_message);
|
||||||
else
|
else
|
||||||
m_discord.SendChatMessage(msg, channel);
|
m_discord.SendChatMessage(msg, attachment_paths, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Abaddon::ActionChatEditMessage(Snowflake channel_id, Snowflake id) {
|
void Abaddon::ActionChatEditMessage(Snowflake channel_id, Snowflake id) {
|
||||||
|
@@ -36,7 +36,7 @@ public:
|
|||||||
void ActionSetToken();
|
void ActionSetToken();
|
||||||
void ActionJoinGuildDialog();
|
void ActionJoinGuildDialog();
|
||||||
void ActionChannelOpened(Snowflake id, bool expand_to = true);
|
void ActionChannelOpened(Snowflake id, bool expand_to = true);
|
||||||
void ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflake referenced_message);
|
void ActionChatInputSubmit(std::string msg, const std::vector<std::string> &attachment_paths, Snowflake channel, Snowflake referenced_message);
|
||||||
void ActionChatLoadHistory(Snowflake id);
|
void ActionChatLoadHistory(Snowflake id);
|
||||||
void ActionChatEditMessage(Snowflake channel_id, Snowflake id);
|
void ActionChatEditMessage(Snowflake channel_id, Snowflake id);
|
||||||
void ActionInsertMention(Snowflake id);
|
void ActionInsertMention(Snowflake id);
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
#include "chatinput.hpp"
|
#include "chatinput.hpp"
|
||||||
|
#include "abaddon.hpp"
|
||||||
|
#include "constants.hpp"
|
||||||
|
#include <filesystem>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
ChatInput::ChatInput() {
|
ChatInputText::ChatInputText() {
|
||||||
get_style_context()->add_class("message-input");
|
get_style_context()->add_class("message-input");
|
||||||
set_propagate_natural_height(true);
|
set_propagate_natural_height(true);
|
||||||
set_min_content_height(20);
|
set_min_content_height(20);
|
||||||
@@ -20,22 +24,26 @@ ChatInput::ChatInput() {
|
|||||||
add(m_textview);
|
add(m_textview);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatInput::InsertText(const Glib::ustring &text) {
|
void ChatInputText::InsertText(const Glib::ustring &text) {
|
||||||
GetBuffer()->insert_at_cursor(text);
|
GetBuffer()->insert_at_cursor(text);
|
||||||
m_textview.grab_focus();
|
m_textview.grab_focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
Glib::RefPtr<Gtk::TextBuffer> ChatInput::GetBuffer() {
|
Glib::RefPtr<Gtk::TextBuffer> ChatInputText::GetBuffer() {
|
||||||
return m_textview.get_buffer();
|
return m_textview.get_buffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// this isnt connected directly so that the chat window can handle stuff like the completer first
|
// 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) {
|
if (event->keyval == GDK_KEY_Escape) {
|
||||||
m_signal_escape.emit();
|
m_signal_escape.emit();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((event->state & GDK_CONTROL_MASK) && event->keyval == GDK_KEY_v) {
|
||||||
|
return CheckHandleClipboardPaste();
|
||||||
|
}
|
||||||
|
|
||||||
if (event->keyval == GDK_KEY_Return) {
|
if (event->keyval == GDK_KEY_Return) {
|
||||||
if (event->state & GDK_SHIFT_MASK)
|
if (event->state & GDK_SHIFT_MASK)
|
||||||
return false;
|
return false;
|
||||||
@@ -53,10 +61,196 @@ bool ChatInput::ProcessKeyPress(GdkEventKey *event) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatInput::on_grab_focus() {
|
void ChatInputText::on_grab_focus() {
|
||||||
m_textview.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();
|
||||||
|
std::array<char, L_tmpnam> dest_name {};
|
||||||
|
if (std::tmpnam(dest_name.data()) == nullptr) {
|
||||||
|
fprintf(stderr, "failed to get temporary path\n");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// stinky
|
||||||
|
std::filesystem::path part1(std::filesystem::temp_directory_path() / "abaddon-cache");
|
||||||
|
std::filesystem::path part2(dest_name.data());
|
||||||
|
const auto dest_path = (part1 / part2.relative_path()).string();
|
||||||
|
|
||||||
|
try {
|
||||||
|
pb->save(dest_path, "png");
|
||||||
|
} catch (...) {
|
||||||
|
fprintf(stderr, "pasted image save error\n");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_signal_image_paste.emit(pb, dest_path);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatInputAttachmentContainer::ChatInputAttachmentContainer()
|
||||||
|
: m_box(Gtk::ORIENTATION_HORIZONTAL) {
|
||||||
|
get_style_context()->add_class("attachment-container");
|
||||||
|
|
||||||
|
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 *x : m_attachments)
|
||||||
|
delete x;
|
||||||
|
m_attachments.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatInputAttachmentContainer::AddImage(const Glib::RefPtr<Gdk::Pixbuf> &pb, const std::string &path) {
|
||||||
|
if (m_attachments.size() == 10) return false;
|
||||||
|
|
||||||
|
auto *item = Gtk::make_managed<ChatInputAttachmentItem>(path, pb);
|
||||||
|
item->show();
|
||||||
|
item->set_valign(Gtk::ALIGN_CENTER);
|
||||||
|
m_box.add(*item);
|
||||||
|
|
||||||
|
m_attachments.insert(item);
|
||||||
|
|
||||||
|
item->signal_remove().connect([this, item] {
|
||||||
|
m_attachments.erase(item);
|
||||||
|
delete item;
|
||||||
|
if (m_attachments.empty())
|
||||||
|
m_signal_emptied.emit();
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> ChatInputAttachmentContainer::GetFilePaths() const {
|
||||||
|
std::vector<std::string> ret;
|
||||||
|
for (auto *x : m_attachments)
|
||||||
|
ret.push_back(x->GetPath());
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatInputAttachmentContainer::type_signal_emptied ChatInputAttachmentContainer::signal_emptied() {
|
||||||
|
return m_signal_emptied;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatInputAttachmentItem::ChatInputAttachmentItem(std::string path, const Glib::RefPtr<Gdk::Pixbuf> &pb)
|
||||||
|
: m_path(std::move(path))
|
||||||
|
, m_img(Gtk::make_managed<Gtk::Image>()) {
|
||||||
|
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);
|
||||||
|
m_box.add(*m_img);
|
||||||
|
add(m_box);
|
||||||
|
show_all_children();
|
||||||
|
|
||||||
|
SetupMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ChatInputAttachmentItem::GetPath() const {
|
||||||
|
return m_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatInputAttachmentItem::SetupMenu() {
|
||||||
|
m_menu_remove.set_label("Remove");
|
||||||
|
m_menu_remove.signal_activate().connect([this] {
|
||||||
|
m_signal_remove.emit();
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatInputAttachmentItem::type_signal_remove ChatInputAttachmentItem::signal_remove() {
|
||||||
|
return m_signal_remove;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatInput::ChatInput()
|
||||||
|
: Gtk::Box(Gtk::ORIENTATION_VERTICAL) {
|
||||||
|
m_input.signal_escape().connect([this] {
|
||||||
|
m_attachments.Clear();
|
||||||
|
m_attachments_revealer.set_reveal_child(false);
|
||||||
|
m_signal_escape.emit();
|
||||||
|
});
|
||||||
|
m_input.signal_submit().connect([this](const Glib::ustring &input) -> bool {
|
||||||
|
const auto attachments = m_attachments.GetFilePaths();
|
||||||
|
bool b = m_signal_submit.emit(input, attachments);
|
||||||
|
if (b) {
|
||||||
|
m_attachments.Clear();
|
||||||
|
m_attachments_revealer.set_reveal_child(false);
|
||||||
|
}
|
||||||
|
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.signal_image_paste().connect([this](const Glib::RefPtr<Gdk::Pixbuf> &pb, const std::string &path) {
|
||||||
|
if (m_attachments.AddImage(pb, path))
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatInput::InsertText(const Glib::ustring &text) {
|
||||||
|
m_input.InsertText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
Glib::RefPtr<Gtk::TextBuffer> ChatInput::GetBuffer() {
|
||||||
|
return m_input.GetBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatInput::ProcessKeyPress(GdkEventKey *event) {
|
||||||
|
return m_input.ProcessKeyPress(event);
|
||||||
|
}
|
||||||
|
|
||||||
ChatInput::type_signal_submit ChatInput::signal_submit() {
|
ChatInput::type_signal_submit ChatInput::signal_submit() {
|
||||||
return m_signal_submit;
|
return m_signal_submit;
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,57 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <gtkmm.h>
|
#include <gtkmm.h>
|
||||||
|
|
||||||
class ChatInput : public Gtk::ScrolledWindow {
|
class ChatInputAttachmentItem : public Gtk::EventBox {
|
||||||
public:
|
public:
|
||||||
ChatInput();
|
ChatInputAttachmentItem(std::string path, const Glib::RefPtr<Gdk::Pixbuf> &pb);
|
||||||
|
|
||||||
|
[[nodiscard]] std::string GetPath() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void SetupMenu();
|
||||||
|
|
||||||
|
Gtk::Menu m_menu;
|
||||||
|
Gtk::MenuItem m_menu_remove;
|
||||||
|
|
||||||
|
Gtk::Box m_box;
|
||||||
|
Gtk::Image *m_img = nullptr;
|
||||||
|
|
||||||
|
std::string m_path;
|
||||||
|
|
||||||
|
private:
|
||||||
|
using type_signal_remove = sigc::signal<void>;
|
||||||
|
|
||||||
|
type_signal_remove m_signal_remove;
|
||||||
|
|
||||||
|
public:
|
||||||
|
type_signal_remove signal_remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
class ChatInputAttachmentContainer : public Gtk::ScrolledWindow {
|
||||||
|
public:
|
||||||
|
ChatInputAttachmentContainer();
|
||||||
|
|
||||||
|
void Clear();
|
||||||
|
bool AddImage(const Glib::RefPtr<Gdk::Pixbuf> &pb, const std::string &path);
|
||||||
|
[[nodiscard]] std::vector<std::string> GetFilePaths() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::set<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);
|
void InsertText(const Glib::ustring &text);
|
||||||
Glib::RefPtr<Gtk::TextBuffer> GetBuffer();
|
Glib::RefPtr<Gtk::TextBuffer> GetBuffer();
|
||||||
@@ -15,9 +63,42 @@ protected:
|
|||||||
private:
|
private:
|
||||||
Gtk::TextView m_textview;
|
Gtk::TextView m_textview;
|
||||||
|
|
||||||
|
bool CheckHandleClipboardPaste();
|
||||||
|
void HandleNewPastedImage(const Glib::RefPtr<Gdk::Pixbuf> &pb, const std::string &filename);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
typedef sigc::signal<bool, Glib::ustring> type_signal_submit;
|
using type_signal_submit = sigc::signal<bool, Glib::ustring>;
|
||||||
typedef sigc::signal<void> type_signal_escape;
|
using type_signal_escape = sigc::signal<void>;
|
||||||
|
using type_signal_image_paste = sigc::signal<void, Glib::RefPtr<Gdk::Pixbuf>, std::string>;
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ChatInput : public Gtk::Box {
|
||||||
|
public:
|
||||||
|
ChatInput();
|
||||||
|
|
||||||
|
void InsertText(const Glib::ustring &text);
|
||||||
|
Glib::RefPtr<Gtk::TextBuffer> GetBuffer();
|
||||||
|
bool ProcessKeyPress(GdkEventKey *event);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Gtk::Revealer m_attachments_revealer;
|
||||||
|
ChatInputAttachmentContainer m_attachments;
|
||||||
|
ChatInputText m_input;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// text, attachments -> request sent
|
||||||
|
// maybe this should be reduced to a single struct, its bound to get more complicated (application commands?)
|
||||||
|
using type_signal_submit = sigc::signal<bool, Glib::ustring, std::vector<std::string>>;
|
||||||
|
using type_signal_escape = sigc::signal<void>;
|
||||||
|
|
||||||
type_signal_submit signal_submit();
|
type_signal_submit signal_submit();
|
||||||
type_signal_escape signal_escape();
|
type_signal_escape signal_escape();
|
||||||
|
@@ -352,10 +352,6 @@ ChatList::type_signal_action_message_edit ChatList::signal_action_message_edit()
|
|||||||
return m_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() {
|
ChatList::type_signal_action_chat_load_history ChatList::signal_action_chat_load_history() {
|
||||||
return m_signal_action_chat_load_history;
|
return m_signal_action_chat_load_history;
|
||||||
}
|
}
|
||||||
|
@@ -63,7 +63,6 @@ private:
|
|||||||
public:
|
public:
|
||||||
// these are all forwarded by the parent
|
// these are all forwarded by the parent
|
||||||
using type_signal_action_message_edit = sigc::signal<void, Snowflake, Snowflake>;
|
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_chat_load_history = sigc::signal<void, Snowflake>;
|
||||||
using type_signal_action_channel_click = sigc::signal<void, Snowflake>;
|
using type_signal_action_channel_click = sigc::signal<void, Snowflake>;
|
||||||
using type_signal_action_insert_mention = 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>;
|
using type_signal_action_reply_to = sigc::signal<void, Snowflake>;
|
||||||
|
|
||||||
type_signal_action_message_edit signal_action_message_edit();
|
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_chat_load_history signal_action_chat_load_history();
|
||||||
type_signal_action_channel_click signal_action_channel_click();
|
type_signal_action_channel_click signal_action_channel_click();
|
||||||
type_signal_action_insert_mention signal_action_insert_mention();
|
type_signal_action_insert_mention signal_action_insert_mention();
|
||||||
@@ -84,7 +82,6 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
type_signal_action_message_edit m_signal_action_message_edit;
|
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_chat_load_history m_signal_action_chat_load_history;
|
||||||
type_signal_action_channel_click m_signal_action_channel_click;
|
type_signal_action_channel_click m_signal_action_channel_click;
|
||||||
type_signal_action_insert_mention m_signal_action_insert_mention;
|
type_signal_action_insert_mention m_signal_action_insert_mention;
|
||||||
|
@@ -45,6 +45,8 @@ ChatWindow::ChatWindow() {
|
|||||||
m_topic_text.set_halign(Gtk::ALIGN_START);
|
m_topic_text.set_halign(Gtk::ALIGN_START);
|
||||||
m_topic_text.show();
|
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_submit().connect(sigc::mem_fun(*this, &ChatWindow::OnInputSubmit));
|
||||||
m_input->signal_escape().connect([this]() {
|
m_input->signal_escape().connect([this]() {
|
||||||
if (m_is_replying)
|
if (m_is_replying)
|
||||||
@@ -70,9 +72,6 @@ ChatWindow::ChatWindow() {
|
|||||||
m_chat->signal_action_chat_load_history().connect([this](Snowflake id) {
|
m_chat->signal_action_chat_load_history().connect([this](Snowflake id) {
|
||||||
m_signal_action_chat_load_history.emit(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) {
|
m_chat->signal_action_insert_mention().connect([this](Snowflake id) {
|
||||||
// lowkey gross
|
// lowkey gross
|
||||||
m_signal_action_insert_mention.emit(id);
|
m_signal_action_insert_mention.emit(id);
|
||||||
@@ -210,15 +209,15 @@ Snowflake ChatWindow::GetActiveChannel() const {
|
|||||||
return m_active_channel;
|
return m_active_channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatWindow::OnInputSubmit(const Glib::ustring &text) {
|
bool ChatWindow::OnInputSubmit(const Glib::ustring &text, const std::vector<std::string> &attachment_paths) {
|
||||||
if (!m_rate_limit_indicator->CanSpeak())
|
if (!m_rate_limit_indicator->CanSpeak())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (text.empty())
|
if (text.empty() && attachment_paths.empty())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (m_active_channel.IsValid())
|
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(text, attachment_paths, m_active_channel, m_replying_to); // m_replying_to is checked for invalid in the handler
|
||||||
if (m_is_replying)
|
if (m_is_replying)
|
||||||
StopReplying();
|
StopReplying();
|
||||||
|
|
||||||
|
@@ -55,7 +55,7 @@ protected:
|
|||||||
|
|
||||||
Snowflake m_active_channel;
|
Snowflake m_active_channel;
|
||||||
|
|
||||||
bool OnInputSubmit(const Glib::ustring &text);
|
bool OnInputSubmit(const Glib::ustring &text, const std::vector<std::string> &attachment_paths);
|
||||||
|
|
||||||
bool OnKeyPressEvent(GdkEventKey *e);
|
bool OnKeyPressEvent(GdkEventKey *e);
|
||||||
void OnScrollEdgeOvershot(Gtk::PositionType pos);
|
void OnScrollEdgeOvershot(Gtk::PositionType pos);
|
||||||
@@ -84,7 +84,7 @@ protected:
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
using type_signal_action_message_edit = sigc::signal<void, Snowflake, Snowflake>;
|
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, std::string, std::vector<std::string>, Snowflake, Snowflake>;
|
||||||
using type_signal_action_chat_load_history = sigc::signal<void, Snowflake>;
|
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_channel_click = sigc::signal<void, Snowflake, bool>;
|
||||||
using type_signal_action_insert_mention = sigc::signal<void, Snowflake>;
|
using type_signal_action_insert_mention = sigc::signal<void, Snowflake>;
|
||||||
|
@@ -2,3 +2,4 @@
|
|||||||
|
|
||||||
constexpr static uint64_t SnowflakeSplitDifference = 600;
|
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 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 = 128;
|
||||||
|
@@ -424,12 +424,32 @@ void DiscordClient::ChatMessageCallback(const std::string &nonce, const http::re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DiscordClient::SendChatMessage(const std::string &content, Snowflake channel) {
|
void DiscordClient::SendChatMessageAttachments(const std::string &content, const std::vector<std::string> &attachment_paths, Snowflake channel, Snowflake referenced_message) {
|
||||||
|
const auto nonce = std::to_string(Snowflake::FromNow());
|
||||||
|
CreateMessageObject obj;
|
||||||
|
obj.Content = content;
|
||||||
|
obj.Nonce = nonce;
|
||||||
|
if (referenced_message.IsValid())
|
||||||
|
obj.MessageReference.emplace().MessageID = referenced_message;
|
||||||
|
|
||||||
|
auto req = m_http.CreateRequest(http::REQUEST_POST, "/channels/" + std::to_string(channel) + "/messages");
|
||||||
|
req.make_form();
|
||||||
|
req.add_field("payload_json", nlohmann::json(obj).dump().c_str(), CURL_ZERO_TERMINATED);
|
||||||
|
for (size_t i = 0; i < attachment_paths.size(); i++) {
|
||||||
|
const auto field_name = "files[" + std::to_string(i) + "]";
|
||||||
|
req.add_file(field_name, attachment_paths.at(i), "unknown.png");
|
||||||
|
}
|
||||||
|
m_http.Execute(std::move(req), sigc::bind<0>(sigc::mem_fun(*this, &DiscordClient::ChatMessageCallback), nonce));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DiscordClient::SendChatMessageText(const std::string &content, Snowflake channel, Snowflake referenced_message) {
|
||||||
// @([^@#]{1,32})#(\\d{4})
|
// @([^@#]{1,32})#(\\d{4})
|
||||||
const auto nonce = std::to_string(Snowflake::FromNow());
|
const auto nonce = std::to_string(Snowflake::FromNow());
|
||||||
CreateMessageObject obj;
|
CreateMessageObject obj;
|
||||||
obj.Content = content;
|
obj.Content = content;
|
||||||
obj.Nonce = nonce;
|
obj.Nonce = nonce;
|
||||||
|
if (referenced_message.IsValid())
|
||||||
|
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));
|
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
|
// dummy data so the content can be shown while waiting for MESSAGE_CREATE
|
||||||
Message tmp;
|
Message tmp;
|
||||||
@@ -448,27 +468,22 @@ void DiscordClient::SendChatMessage(const std::string &content, Snowflake channe
|
|||||||
m_signal_message_sent.emit(tmp);
|
m_signal_message_sent.emit(tmp);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DiscordClient::SendChatMessage(const std::string &content, Snowflake channel, Snowflake referenced_message) {
|
void DiscordClient::SendChatMessage(const std::string &content, const std::vector<std::string> &attachment_paths, Snowflake channel) {
|
||||||
const auto nonce = std::to_string(Snowflake::FromNow());
|
if (attachment_paths.empty())
|
||||||
CreateMessageObject obj;
|
SendChatMessageText(content, channel);
|
||||||
obj.Content = content;
|
else {
|
||||||
obj.Nonce = nonce;
|
puts("attach");
|
||||||
obj.MessageReference.emplace().MessageID = referenced_message;
|
SendChatMessageAttachments(content, attachment_paths, channel, Snowflake::Invalid);
|
||||||
m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), sigc::bind<0>(sigc::mem_fun(*this, &DiscordClient::ChatMessageCallback), nonce));
|
}
|
||||||
Message tmp;
|
}
|
||||||
tmp.Content = content;
|
|
||||||
tmp.ID = nonce;
|
void DiscordClient::SendChatMessage(const std::string &content, const std::vector<std::string> &attachment_paths, Snowflake channel, Snowflake referenced_message) {
|
||||||
tmp.ChannelID = channel;
|
if (attachment_paths.empty())
|
||||||
tmp.Author = GetUserData();
|
SendChatMessageText(content, channel, referenced_message);
|
||||||
tmp.IsTTS = false;
|
else {
|
||||||
tmp.DoesMentionEveryone = false;
|
puts("attach");
|
||||||
tmp.Type = MessageType::DEFAULT;
|
SendChatMessageAttachments(content, attachment_paths, channel, referenced_message);
|
||||||
tmp.IsPinned = false;
|
}
|
||||||
tmp.Timestamp = "2000-01-01T00:00:00.000000+00:00";
|
|
||||||
tmp.Nonce = obj.Nonce;
|
|
||||||
tmp.IsPending = true;
|
|
||||||
m_store.SetMessage(tmp.ID, tmp);
|
|
||||||
m_signal_message_sent.emit(tmp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DiscordClient::DeleteMessage(Snowflake channel_id, Snowflake id) {
|
void DiscordClient::DeleteMessage(Snowflake channel_id, Snowflake id) {
|
||||||
|
@@ -104,8 +104,8 @@ public:
|
|||||||
|
|
||||||
void ChatMessageCallback(const std::string &nonce, const http::response_type &response);
|
void ChatMessageCallback(const std::string &nonce, const http::response_type &response);
|
||||||
|
|
||||||
void SendChatMessage(const std::string &content, Snowflake channel);
|
void SendChatMessage(const std::string &content, const std::vector<std::string> &attachment_paths, Snowflake channel);
|
||||||
void SendChatMessage(const std::string &content, Snowflake channel, Snowflake referenced_message);
|
void SendChatMessage(const std::string &content, const std::vector<std::string> &attachment_paths, Snowflake channel, Snowflake referenced_message);
|
||||||
void DeleteMessage(Snowflake channel_id, Snowflake id);
|
void DeleteMessage(Snowflake channel_id, Snowflake id);
|
||||||
void EditMessage(Snowflake channel_id, Snowflake id, std::string content);
|
void EditMessage(Snowflake channel_id, Snowflake id, std::string content);
|
||||||
void SendLazyLoad(Snowflake id);
|
void SendLazyLoad(Snowflake id);
|
||||||
@@ -223,6 +223,9 @@ private:
|
|||||||
std::vector<uint8_t> m_decompress_buf;
|
std::vector<uint8_t> m_decompress_buf;
|
||||||
z_stream m_zstream;
|
z_stream m_zstream;
|
||||||
|
|
||||||
|
void SendChatMessageAttachments(const std::string &content, const std::vector<std::string> &attachment_paths, Snowflake channel, Snowflake referenced_message = Snowflake::Invalid);
|
||||||
|
void SendChatMessageText(const std::string &content, Snowflake channel, Snowflake referenced_message = Snowflake::Invalid);
|
||||||
|
|
||||||
static std::string GetAPIURL();
|
static std::string GetAPIURL();
|
||||||
static std::string GetGatewayURL();
|
static std::string GetGatewayURL();
|
||||||
|
|
||||||
|
@@ -112,6 +112,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() {
|
void HTTPClient::CleanupFutures() {
|
||||||
for (auto it = m_futures.begin(); it != m_futures.end();) {
|
for (auto it = m_futures.begin(); it != m_futures.end();) {
|
||||||
if (it->wait_for(std::chrono::seconds(0)) == std::future_status::ready)
|
if (it->wait_for(std::chrono::seconds(0)) == std::future_status::ready)
|
||||||
|
@@ -23,6 +23,9 @@ public:
|
|||||||
void MakePOST(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb);
|
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);
|
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:
|
private:
|
||||||
void OnResponse(const http::response_type &r, const std::function<void(http::response_type r)> &cb);
|
void OnResponse(const http::response_type &r, const std::function<void(http::response_type r)> &cb);
|
||||||
void CleanupFutures();
|
void CleanupFutures();
|
||||||
|
45
src/http.cpp
45
src/http.cpp
@@ -29,12 +29,33 @@ request::request(EMethod method, std::string url)
|
|||||||
prepare();
|
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)) {
|
||||||
|
// i think this is correct???
|
||||||
|
}
|
||||||
|
|
||||||
request::~request() {
|
request::~request() {
|
||||||
if (m_curl != nullptr)
|
if (m_curl != nullptr)
|
||||||
curl_easy_cleanup(m_curl);
|
curl_easy_cleanup(m_curl);
|
||||||
|
|
||||||
if (m_header_list != nullptr)
|
if (m_header_list != nullptr)
|
||||||
curl_slist_free_all(m_header_list);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
void request::set_verify_ssl(bool verify) {
|
void request::set_verify_ssl(bool verify) {
|
||||||
@@ -57,6 +78,26 @@ void request::set_user_agent(const std::string &data) {
|
|||||||
curl_easy_setopt(m_curl, CURLOPT_USERAGENT, data.c_str());
|
curl_easy_setopt(m_curl, CURLOPT_USERAGENT, data.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void request::make_form() {
|
||||||
|
m_form = curl_mime_init(m_curl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// file must exist until request completes
|
||||||
|
void request::add_file(std::string_view name, std::string_view file_path, std::string_view filename) {
|
||||||
|
auto *field = curl_mime_addpart(m_form);
|
||||||
|
curl_mime_name(field, name.data());
|
||||||
|
curl_mime_filedata(field, file_path.data());
|
||||||
|
curl_mime_filename(field, filename.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
// copied
|
||||||
|
void request::add_field(std::string_view name, const char *data, size_t size) {
|
||||||
|
puts(name.data());
|
||||||
|
auto *field = curl_mime_addpart(m_form);
|
||||||
|
curl_mime_name(field, name.data());
|
||||||
|
curl_mime_data(field, data, size);
|
||||||
|
}
|
||||||
|
|
||||||
response request::execute() {
|
response request::execute() {
|
||||||
if (m_curl == nullptr) {
|
if (m_curl == nullptr) {
|
||||||
auto response = detail::make_response(m_url, EStatusCode::ClientErrorCURLInit);
|
auto response = detail::make_response(m_url, EStatusCode::ClientErrorCURLInit);
|
||||||
@@ -76,12 +117,14 @@ response request::execute() {
|
|||||||
m_error_buf[0] = '\0';
|
m_error_buf[0] = '\0';
|
||||||
if (m_header_list != nullptr)
|
if (m_header_list != nullptr)
|
||||||
curl_easy_setopt(m_curl, CURLOPT_HTTPHEADER, m_header_list);
|
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);
|
CURLcode result = curl_easy_perform(m_curl);
|
||||||
if (result != CURLE_OK) {
|
if (result != CURLE_OK) {
|
||||||
auto response = detail::make_response(m_url, EStatusCode::ClientErrorCURLPerform);
|
auto response = detail::make_response(m_url, EStatusCode::ClientErrorCURLPerform);
|
||||||
response.error_string = curl_easy_strerror(result);
|
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;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
15
src/http.hpp
15
src/http.hpp
@@ -1,4 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <array>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
|
|
||||||
@@ -98,13 +99,24 @@ struct response {
|
|||||||
|
|
||||||
struct request {
|
struct request {
|
||||||
request(EMethod method, std::string url);
|
request(EMethod method, std::string url);
|
||||||
|
request(request &&other) noexcept;
|
||||||
~request();
|
~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_verify_ssl(bool verify);
|
void set_verify_ssl(bool verify);
|
||||||
void set_proxy(const std::string &proxy);
|
void set_proxy(const std::string &proxy);
|
||||||
void set_header(const std::string &name, const std::string &value);
|
void set_header(const std::string &name, const std::string &value);
|
||||||
void set_body(const std::string &data);
|
void set_body(const std::string &data);
|
||||||
void set_user_agent(const std::string &data);
|
void set_user_agent(const std::string &data);
|
||||||
|
void make_form();
|
||||||
|
void add_file(std::string_view name, std::string_view file_path, std::string_view filename);
|
||||||
|
void add_field(std::string_view name, const char *data, size_t size);
|
||||||
|
|
||||||
response execute();
|
response execute();
|
||||||
|
|
||||||
@@ -115,7 +127,8 @@ private:
|
|||||||
std::string m_url;
|
std::string m_url;
|
||||||
const char *m_method;
|
const char *m_method;
|
||||||
curl_slist *m_header_list = nullptr;
|
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
using response_type = response;
|
using response_type = response;
|
||||||
|
Reference in New Issue
Block a user