Merge branch 'attachments'

This commit is contained in:
ouwou 2022-08-12 18:35:58 -04:00
commit 6a5ecb4d95
31 changed files with 1159 additions and 71 deletions

View File

@ -73,6 +73,17 @@ jobs:
cp -r res/css res/res res/fonts build/artifactdir/bin
cp /mingw64/share/glib-2.0/schemas/gschemas.compiled build/artifactdir/share/glib-2.0/schemas
cat "ci/msys-deps.txt" | sed 's/\r$//' | xargs -I % cp /mingw64% build/artifactdir/bin || :
mkdir -p build/artifactdir/share/icons/Adwaita
cd build/artifactdir/share/icons/Adwaita
mkdir -p 16x16/actions 24x24/actions 32x32/actions 48x48/actions 64x64/actions 96x96/actions scalable/actions
cd ../../../../../
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/16x16/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/16x16/actions || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/24x24/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/24x24/actions || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/32x32/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/32x32/actions || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/48x48/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/48x48/actions || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/64x64/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/64x64/actions || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/96x96/actions/%.symbolic.png build/artifactdir/share/icons/Adwaita/96x96/actions || :
cat "ci/used-icons.txt" | sed 's/\r$//' | xargs -I % cp ci/gtk-for-windows/gtk-nsis-pack/share/icons/Adwaita/scalable/actions/%.svg build/artifactdir/share/icons/Adwaita/scalable/actions || :
- name: Upload build (1)
uses: haya14busa/action-cond@v1

1
ci/used-icons.txt Normal file
View File

@ -0,0 +1 @@
document-send-symbolic

View File

@ -101,17 +101,38 @@
.message-input, .message-input textview, .message-input textview text {
background-color: #242424;
color: #adadad;
border-radius: 15px;
border-radius: 3px;
border: 1px solid transparent;
}
.message-input {
border: 1px solid #444444;
margin-right: 15px;
}
.message-input.replying {
border: 1px solid #026FB9;
}
.message-input {
.message-input.bad-input {
border: 1px solid #dd3300;
}
.message-input-browse-icon {
color: #b9bbbe;
margin-left: 5px;
margin-top: 11px;
}
/* i dont think theres a way to circumvent having to do this to adjust around the browse icon */
.message-input:not(.with-browser-icon) {
padding: 0px 0px 0px 5px;
}
.message-input.with-browse-icon {
padding: 0px 0px 0px 30px;
}
.members {
background-color: @background_color;
}
@ -323,3 +344,19 @@
.channel-tab-switcher tab > button:hover {
background-color: alpha(#ff0000, 0.5);
}
.message-progress {
border: none;
margin-bottom: -8px;
}
.message-progress trough {
border: none;
background-color: transparent;
}
.message-progress progress {
border: none;
background-color: #dd3300;
margin-left: 1px;
}

View File

@ -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);

View File

@ -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();

View File

@ -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;
}

View File

@ -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:
typedef sigc::signal<bool, Glib::ustring> type_signal_submit;
typedef sigc::signal<void> type_signal_escape;
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:
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();

View File

@ -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;
}

View File

@ -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;

View File

@ -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>");

View File

@ -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();
}

View File

@ -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>;

View File

@ -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);
}
});
}

View File

@ -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;
};

View File

@ -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;

26
src/dialogs/textinput.cpp Normal file
View File

@ -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();
}

14
src/dialogs/textinput.hpp Normal file
View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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;
};

View File

@ -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

View File

@ -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)

View File

@ -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);

View File

@ -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();

View File

@ -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";
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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());

View File

@ -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;
}

View File

@ -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;