From a51a54bc5979a2491f152abc47ad54e6b63f27c8 Mon Sep 17 00:00:00 2001 From: Dylam De La Torre Date: Tue, 23 Nov 2021 05:21:56 +0100 Subject: Restructure source and resource files (#46) importantly, res is now res/res and css is now res/css --- components/cellrendererpixbufanimation.cpp | 94 --- components/cellrendererpixbufanimation.hpp | 41 - components/channels.cpp | 1248 ---------------------------- components/channels.hpp | 250 ------ components/chatinput.cpp | 66 -- components/chatinput.hpp | 28 - components/chatinputindicator.cpp | 121 --- components/chatinputindicator.hpp | 28 - components/chatlist.cpp | 368 -------- components/chatlist.hpp | 115 --- components/chatmessage.cpp | 1245 --------------------------- components/chatmessage.hpp | 125 --- components/chatwindow.cpp | 239 ------ components/chatwindow.hpp | 90 -- components/completer.cpp | 392 --------- components/completer.hpp | 68 -- components/draglistbox.cpp | 141 ---- components/draglistbox.hpp | 45 - components/friendslist.cpp | 354 -------- components/friendslist.hpp | 92 -- components/lazyimage.cpp | 48 -- components/lazyimage.hpp | 21 - components/memberlist.cpp | 228 ----- components/memberlist.hpp | 44 - components/ratelimitindicator.cpp | 137 --- components/ratelimitindicator.hpp | 31 - components/statusindicator.cpp | 130 --- components/statusindicator.hpp | 30 - 28 files changed, 5819 deletions(-) delete mode 100644 components/cellrendererpixbufanimation.cpp delete mode 100644 components/cellrendererpixbufanimation.hpp delete mode 100644 components/channels.cpp delete mode 100644 components/channels.hpp delete mode 100644 components/chatinput.cpp delete mode 100644 components/chatinput.hpp delete mode 100644 components/chatinputindicator.cpp delete mode 100644 components/chatinputindicator.hpp delete mode 100644 components/chatlist.cpp delete mode 100644 components/chatlist.hpp delete mode 100644 components/chatmessage.cpp delete mode 100644 components/chatmessage.hpp delete mode 100644 components/chatwindow.cpp delete mode 100644 components/chatwindow.hpp delete mode 100644 components/completer.cpp delete mode 100644 components/completer.hpp delete mode 100644 components/draglistbox.cpp delete mode 100644 components/draglistbox.hpp delete mode 100644 components/friendslist.cpp delete mode 100644 components/friendslist.hpp delete mode 100644 components/lazyimage.cpp delete mode 100644 components/lazyimage.hpp delete mode 100644 components/memberlist.cpp delete mode 100644 components/memberlist.hpp delete mode 100644 components/ratelimitindicator.cpp delete mode 100644 components/ratelimitindicator.hpp delete mode 100644 components/statusindicator.cpp delete mode 100644 components/statusindicator.hpp (limited to 'components') diff --git a/components/cellrendererpixbufanimation.cpp b/components/cellrendererpixbufanimation.cpp deleted file mode 100644 index 2658967..0000000 --- a/components/cellrendererpixbufanimation.cpp +++ /dev/null @@ -1,94 +0,0 @@ -#include "cellrendererpixbufanimation.hpp" - -CellRendererPixbufAnimation::CellRendererPixbufAnimation() - : Glib::ObjectBase(typeid(CellRendererPixbufAnimation)) - , Gtk::CellRenderer() - , m_property_pixbuf(*this, "pixbuf") - , m_property_pixbuf_animation(*this, "pixbuf-animation") { - property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE; - property_xpad() = 2; - property_ypad() = 2; -} - -CellRendererPixbufAnimation::~CellRendererPixbufAnimation() {} - -Glib::PropertyProxy> CellRendererPixbufAnimation::property_pixbuf() { - return m_property_pixbuf.get_proxy(); -} - -Glib::PropertyProxy> CellRendererPixbufAnimation::property_pixbuf_animation() { - return m_property_pixbuf_animation.get_proxy(); -} - -void CellRendererPixbufAnimation::get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { - int width = 0; - - if (auto pixbuf = m_property_pixbuf_animation.get_value()) - width = pixbuf->get_width(); - else if (auto pixbuf = m_property_pixbuf.get_value()) - width = pixbuf->get_width(); - - int xpad, ypad; - get_padding(xpad, ypad); - minimum_width = natural_width = xpad * 2 + width; -} - -void CellRendererPixbufAnimation::get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const { - get_preferred_width_vfunc(widget, minimum_width, natural_width); -} - -void CellRendererPixbufAnimation::get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const { - int height = 0; - - if (auto pixbuf = m_property_pixbuf_animation.get_value()) - height = pixbuf->get_height(); - else if (auto pixbuf = m_property_pixbuf.get_value()) - height = pixbuf->get_height(); - - int xpad, ypad; - get_padding(xpad, ypad); - minimum_height = natural_height = ypad * 2 + height; -} - -void CellRendererPixbufAnimation::get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const { - get_preferred_height_vfunc(widget, minimum_height, natural_height); -} - -void CellRendererPixbufAnimation::render_vfunc(const Cairo::RefPtr &cr, - Gtk::Widget &widget, - const Gdk::Rectangle &background_area, - const Gdk::Rectangle &cell_area, - Gtk::CellRendererState flags) { - Gtk::Requisition minimum, natural; - get_preferred_size(widget, minimum, natural); - int xpad, ypad; - get_padding(xpad, ypad); - int pix_x = cell_area.get_x() + xpad; - int pix_y = cell_area.get_y() + ypad; - natural.width -= xpad * 2; - natural.height -= ypad * 2; - - Gdk::Rectangle pix_rect(pix_x, pix_y, natural.width, natural.height); - if (!cell_area.intersects(pix_rect)) - return; - - if (auto anim = m_property_pixbuf_animation.get_value()) { - auto map_iter = m_pixbuf_animation_iters.find(anim); - if (map_iter == m_pixbuf_animation_iters.end()) - m_pixbuf_animation_iters[anim] = anim->get_iter(nullptr); - auto pb_iter = m_pixbuf_animation_iters.at(anim); - - const auto cb = [this, &widget, anim] { - if (m_pixbuf_animation_iters.at(anim)->advance()) - widget.queue_draw(); - }; - Glib::signal_timeout().connect_once(sigc::track_obj(cb, widget), pb_iter->get_delay_time()); - Gdk::Cairo::set_source_pixbuf(cr, pb_iter->get_pixbuf(), pix_x, pix_y); - cr->rectangle(pix_x, pix_y, natural.width, natural.height); - cr->fill(); - } else if (auto pixbuf = m_property_pixbuf.get_value()) { - Gdk::Cairo::set_source_pixbuf(cr, pixbuf, pix_x, pix_y); - cr->rectangle(pix_x, pix_y, natural.width, natural.height); - cr->fill(); - } -} diff --git a/components/cellrendererpixbufanimation.hpp b/components/cellrendererpixbufanimation.hpp deleted file mode 100644 index f47e928..0000000 --- a/components/cellrendererpixbufanimation.hpp +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once -#include -#include - -// handles both static and animated -class CellRendererPixbufAnimation : public Gtk::CellRenderer { -public: - CellRendererPixbufAnimation(); - virtual ~CellRendererPixbufAnimation(); - - Glib::PropertyProxy> property_pixbuf(); - Glib::PropertyProxy> property_pixbuf_animation(); - -protected: - void get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const override; - - void get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const override; - - void get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const override; - - void get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const override; - - void render_vfunc(const Cairo::RefPtr &cr, - Gtk::Widget &widget, - const Gdk::Rectangle &background_area, - const Gdk::Rectangle &cell_area, - Gtk::CellRendererState flag) override; - -private: - Glib::Property> m_property_pixbuf; - Glib::Property> m_property_pixbuf_animation; - /* one cellrenderer is used for every animation and i dont know how to - store data per-pixbuf (in this case the iter) so this little map thing will have to do - i would try set_data on the pixbuf but i dont know if that will cause memory leaks - this would mean if a row's pixbuf animation is changed more than once then it wont be released immediately - but thats not a problem for me in this case - - unordered_map doesnt compile cuz theres no hash overload, i guess - */ - std::map, Glib::RefPtr> m_pixbuf_animation_iters; -}; diff --git a/components/channels.cpp b/components/channels.cpp deleted file mode 100644 index da31de0..0000000 --- a/components/channels.cpp +++ /dev/null @@ -1,1248 +0,0 @@ -#include "channels.hpp" -#include -#include -#include -#include "abaddon.hpp" -#include "imgmanager.hpp" -#include "util.hpp" -#include "statusindicator.hpp" - -ChannelList::ChannelList() - : Glib::ObjectBase(typeid(ChannelList)) - , Gtk::ScrolledWindow() - , m_model(Gtk::TreeStore::create(m_columns)) - , m_menu_guild_copy_id("_Copy ID", true) - , m_menu_guild_settings("View _Settings", true) - , m_menu_guild_leave("_Leave", true) - , m_menu_category_copy_id("_Copy ID", true) - , m_menu_channel_copy_id("_Copy ID", true) - , m_menu_dm_copy_id("_Copy ID", true) - , m_menu_dm_close("") // changes depending on if group or not - , m_menu_thread_copy_id("_Copy ID", true) - , m_menu_thread_leave("_Leave", true) - , m_menu_thread_archive("_Archive", true) - , m_menu_thread_unarchive("_Unarchive", true) { - get_style_context()->add_class("channel-list"); - - const auto cb = [this](const Gtk::TreeModel::Path &path, Gtk::TreeViewColumn *column) { - auto row = *m_model->get_iter(path); - const auto type = row[m_columns.m_type]; - // text channels should not be allowed to be collapsed - // maybe they should be but it seems a little difficult to handle expansion to permit this - if (type != RenderType::TextChannel) { - if (row[m_columns.m_expanded]) { - m_view.collapse_row(path); - row[m_columns.m_expanded] = false; - } else { - m_view.expand_row(path, false); - row[m_columns.m_expanded] = true; - } - } - - if (type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread) { - m_signal_action_channel_item_select.emit(static_cast(row[m_columns.m_id])); - } - }; - m_view.signal_row_activated().connect(cb, false); - m_view.signal_row_collapsed().connect(sigc::mem_fun(*this, &ChannelList::OnRowCollapsed), false); - m_view.signal_row_expanded().connect(sigc::mem_fun(*this, &ChannelList::OnRowExpanded), false); - m_view.set_activate_on_single_click(true); - m_view.get_selection()->set_mode(Gtk::SELECTION_SINGLE); - m_view.get_selection()->set_select_function(sigc::mem_fun(*this, &ChannelList::SelectionFunc)); - m_view.signal_button_press_event().connect(sigc::mem_fun(*this, &ChannelList::OnButtonPressEvent), false); - - m_view.set_hexpand(true); - m_view.set_vexpand(true); - - m_view.set_show_expanders(false); - m_view.set_enable_search(false); - m_view.set_headers_visible(false); - m_view.set_model(m_model); - m_model->set_sort_column(m_columns.m_sort, Gtk::SORT_ASCENDING); - - m_model->signal_row_inserted().connect([this](const Gtk::TreeModel::Path &path, const Gtk::TreeModel::iterator &iter) { - if (m_updating_listing) return; - if (auto parent = iter->parent(); parent && (*parent)[m_columns.m_expanded]) - m_view.expand_row(m_model->get_path(parent), false); - }); - - m_view.show(); - - add(m_view); - - auto *column = Gtk::manage(new Gtk::TreeView::Column("display")); - auto *renderer = Gtk::manage(new CellRendererChannels); - column->pack_start(*renderer); - column->add_attribute(renderer->property_type(), m_columns.m_type); - column->add_attribute(renderer->property_icon(), m_columns.m_icon); - column->add_attribute(renderer->property_icon_animation(), m_columns.m_icon_anim); - column->add_attribute(renderer->property_name(), m_columns.m_name); - column->add_attribute(renderer->property_expanded(), m_columns.m_expanded); - column->add_attribute(renderer->property_nsfw(), m_columns.m_nsfw); - m_view.append_column(*column); - - m_menu_guild_copy_id.signal_activate().connect([this] { - Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id])); - }); - m_menu_guild_settings.signal_activate().connect([this] { - m_signal_action_guild_settings.emit(static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id])); - }); - m_menu_guild_leave.signal_activate().connect([this] { - m_signal_action_guild_leave.emit(static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id])); - }); - m_menu_guild.append(m_menu_guild_copy_id); - m_menu_guild.append(m_menu_guild_settings); - m_menu_guild.append(m_menu_guild_leave); - m_menu_guild.show_all(); - - m_menu_category_copy_id.signal_activate().connect([this] { - Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id])); - }); - m_menu_category.append(m_menu_category_copy_id); - m_menu_category.show_all(); - - m_menu_channel_copy_id.signal_activate().connect([this] { - Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id])); - }); - m_menu_channel.append(m_menu_channel_copy_id); - m_menu_channel.show_all(); - - m_menu_dm_copy_id.signal_activate().connect([this] { - Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id])); - }); - m_menu_dm_close.signal_activate().connect([this] { - const auto id = static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); - auto &discord = Abaddon::Get().GetDiscordClient(); - const auto channel = discord.GetChannel(id); - if (!channel.has_value()) return; - - if (channel->Type == ChannelType::DM) - discord.CloseDM(id); - else if (Abaddon::Get().ShowConfirm("Are you sure you want to leave this group DM?")) - Abaddon::Get().GetDiscordClient().CloseDM(id); - }); - m_menu_dm.append(m_menu_dm_copy_id); - m_menu_dm.append(m_menu_dm_close); - m_menu_dm.show_all(); - - m_menu_thread_copy_id.signal_activate().connect([this] { - Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id])); - }); - m_menu_thread_leave.signal_activate().connect([this] { - if (Abaddon::Get().ShowConfirm("Are you sure you want to leave this thread?")) - Abaddon::Get().GetDiscordClient().LeaveThread(static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), "Context%20Menu", [](...) {}); - }); - m_menu_thread_archive.signal_activate().connect([this] { - Abaddon::Get().GetDiscordClient().ArchiveThread(static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {}); - }); - m_menu_thread_unarchive.signal_activate().connect([this] { - Abaddon::Get().GetDiscordClient().UnArchiveThread(static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {}); - }); - m_menu_thread.append(m_menu_thread_copy_id); - m_menu_thread.append(m_menu_thread_leave); - m_menu_thread.append(m_menu_thread_archive); - m_menu_thread.append(m_menu_thread_unarchive); - m_menu_thread.show_all(); - - m_menu_thread.signal_popped_up().connect(sigc::mem_fun(*this, &ChannelList::OnThreadSubmenuPopup)); - - auto &discord = Abaddon::Get().GetDiscordClient(); - discord.signal_message_create().connect(sigc::mem_fun(*this, &ChannelList::OnMessageCreate)); - discord.signal_guild_create().connect(sigc::mem_fun(*this, &ChannelList::UpdateNewGuild)); - discord.signal_guild_delete().connect(sigc::mem_fun(*this, &ChannelList::UpdateRemoveGuild)); - discord.signal_channel_delete().connect(sigc::mem_fun(*this, &ChannelList::UpdateRemoveChannel)); - discord.signal_channel_update().connect(sigc::mem_fun(*this, &ChannelList::UpdateChannel)); - discord.signal_channel_create().connect(sigc::mem_fun(*this, &ChannelList::UpdateCreateChannel)); - discord.signal_thread_delete().connect(sigc::mem_fun(*this, &ChannelList::OnThreadDelete)); - discord.signal_thread_update().connect(sigc::mem_fun(*this, &ChannelList::OnThreadUpdate)); - discord.signal_thread_list_sync().connect(sigc::mem_fun(*this, &ChannelList::OnThreadListSync)); - discord.signal_added_to_thread().connect(sigc::mem_fun(*this, &ChannelList::OnThreadJoined)); - discord.signal_removed_from_thread().connect(sigc::mem_fun(*this, &ChannelList::OnThreadRemoved)); - discord.signal_guild_update().connect(sigc::mem_fun(*this, &ChannelList::UpdateGuild)); -} - -void ChannelList::UpdateListing() { - m_updating_listing = true; - - m_model->clear(); - - auto &discord = Abaddon::Get().GetDiscordClient(); - - const auto guild_ids = discord.GetUserSortedGuilds(); - int sortnum = 0; - for (const auto &guild_id : guild_ids) { - const auto guild = discord.GetGuild(guild_id); - if (!guild.has_value()) continue; - - auto iter = AddGuild(*guild); - (*iter)[m_columns.m_sort] = sortnum++; - } - - m_updating_listing = false; - - AddPrivateChannels(); -} - -void ChannelList::UpdateNewGuild(const GuildData &guild) { - AddGuild(guild); - // update sort order - int sortnum = 0; - for (const auto guild_id : Abaddon::Get().GetDiscordClient().GetUserSortedGuilds()) { - auto iter = GetIteratorForGuildFromID(guild_id); - if (iter) - (*iter)[m_columns.m_sort] = ++sortnum; - } -} - -void ChannelList::UpdateRemoveGuild(Snowflake id) { - auto iter = GetIteratorForGuildFromID(id); - if (!iter) return; - m_model->erase(iter); -} - -void ChannelList::UpdateRemoveChannel(Snowflake id) { - auto iter = GetIteratorForChannelFromID(id); - if (!iter) return; - m_model->erase(iter); -} - -void ChannelList::UpdateChannel(Snowflake id) { - auto iter = GetIteratorForChannelFromID(id); - auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); - if (!iter || !channel.has_value()) return; - if (channel->Type == ChannelType::GUILD_CATEGORY) return UpdateChannelCategory(*channel); - if (!IsTextChannel(channel->Type)) return; - - // refresh stuff that might have changed - const bool is_orphan_TMP = !channel->ParentID.has_value(); - (*iter)[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel->Name); - (*iter)[m_columns.m_nsfw] = channel->NSFW(); - (*iter)[m_columns.m_sort] = *channel->Position + (is_orphan_TMP ? OrphanChannelSortOffset : 0); - - // check if the parent has changed - Gtk::TreeModel::iterator new_parent; - if (channel->ParentID.has_value()) - new_parent = GetIteratorForChannelFromID(*channel->ParentID); - else if (channel->GuildID.has_value()) - new_parent = GetIteratorForGuildFromID(*channel->GuildID); - - if (new_parent && iter->parent() != new_parent) - MoveRow(iter, new_parent); -} - -void ChannelList::UpdateCreateChannel(const ChannelData &channel) { - ; - if (channel.Type == ChannelType::GUILD_CATEGORY) return (void)UpdateCreateChannelCategory(channel); - if (channel.Type == ChannelType::DM || channel.Type == ChannelType::GROUP_DM) return UpdateCreateDMChannel(channel); - if (channel.Type != ChannelType::GUILD_TEXT && channel.Type != ChannelType::GUILD_NEWS) return; - - Gtk::TreeRow channel_row; - bool orphan; - if (channel.ParentID.has_value()) { - orphan = false; - auto iter = GetIteratorForChannelFromID(*channel.ParentID); - channel_row = *m_model->append(iter->children()); - } else { - orphan = true; - auto iter = GetIteratorForGuildFromID(*channel.GuildID); - channel_row = *m_model->append(iter->children()); - } - channel_row[m_columns.m_type] = RenderType::TextChannel; - channel_row[m_columns.m_id] = channel.ID; - channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); - channel_row[m_columns.m_nsfw] = channel.NSFW(); - if (orphan) - channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset; - else - channel_row[m_columns.m_sort] = *channel.Position; -} - -void ChannelList::UpdateGuild(Snowflake id) { - auto iter = GetIteratorForGuildFromID(id); - auto &img = Abaddon::Get().GetImageManager(); - const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(id); - if (!iter || !guild.has_value()) return; - - static const bool show_animations = Abaddon::Get().GetSettings().GetShowAnimations(); - - (*iter)[m_columns.m_name] = "" + Glib::Markup::escape_text(guild->Name) + ""; - (*iter)[m_columns.m_icon] = img.GetPlaceholder(GuildIconSize); - if (show_animations && guild->HasAnimatedIcon()) { - const auto cb = [this, id](const Glib::RefPtr &pb) { - auto iter = GetIteratorForGuildFromID(id); - if (iter) (*iter)[m_columns.m_icon_anim] = pb; - }; - img.LoadAnimationFromURL(guild->GetIconURL("gif", "32"), GuildIconSize, GuildIconSize, sigc::track_obj(cb, *this)); - } else if (guild->HasIcon()) { - const auto cb = [this, id](const Glib::RefPtr &pb) { - // iter might be invalid - auto iter = GetIteratorForGuildFromID(id); - if (iter) (*iter)[m_columns.m_icon] = pb->scale_simple(GuildIconSize, GuildIconSize, Gdk::INTERP_BILINEAR); - }; - img.LoadFromURL(guild->GetIconURL("png", "32"), sigc::track_obj(cb, *this)); - } -} - -void ChannelList::OnThreadJoined(Snowflake id) { - if (GetIteratorForChannelFromID(id)) return; - const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); - if (!channel.has_value()) return; - const auto parent = GetIteratorForChannelFromID(*channel->ParentID); - if (parent) - CreateThreadRow(parent->children(), *channel); -} - -void ChannelList::OnThreadRemoved(Snowflake id) { - DeleteThreadRow(id); -} - -void ChannelList::OnThreadDelete(const ThreadDeleteData &data) { - DeleteThreadRow(data.ID); -} - -// todo probably make the row stick around if its selected until the selection changes -void ChannelList::OnThreadUpdate(const ThreadUpdateData &data) { - auto iter = GetIteratorForChannelFromID(data.Thread.ID); - if (iter) - (*iter)[m_columns.m_name] = "- " + Glib::Markup::escape_text(*data.Thread.Name); - - if (data.Thread.ThreadMetadata->IsArchived) - DeleteThreadRow(data.Thread.ID); -} - -void ChannelList::OnThreadListSync(const ThreadListSyncData &data) { - // get the threads in the guild - std::vector threads; - auto guild_iter = GetIteratorForGuildFromID(data.GuildID); - std::queue queue; - queue.push(guild_iter); - - while (!queue.empty()) { - auto item = queue.front(); - queue.pop(); - if ((*item)[m_columns.m_type] == RenderType::Thread) - threads.push_back(static_cast((*item)[m_columns.m_id])); - for (auto child : item->children()) - queue.push(child); - } - - // delete all threads not present in the synced data - for (auto thread_id : threads) { - if (std::find_if(data.Threads.begin(), data.Threads.end(), [thread_id](const auto &x) { return x.ID == thread_id; }) == data.Threads.end()) { - auto iter = GetIteratorForChannelFromID(thread_id); - m_model->erase(iter); - } - } - - // delete all archived threads - for (auto thread : data.Threads) { - if (thread.ThreadMetadata->IsArchived) { - if (auto iter = GetIteratorForChannelFromID(thread.ID)) - m_model->erase(iter); - } - } -} - -void ChannelList::DeleteThreadRow(Snowflake id) { - auto iter = GetIteratorForChannelFromID(id); - if (iter) - m_model->erase(iter); -} - -// create a temporary channel row for non-joined threads -// and delete them when the active channel switches off of them if still not joined -void ChannelList::SetActiveChannel(Snowflake id) { - if (m_temporary_thread_row) { - const auto thread_id = static_cast((*m_temporary_thread_row)[m_columns.m_id]); - const auto thread = Abaddon::Get().GetDiscordClient().GetChannel(thread_id); - if (thread.has_value() && (!thread->IsJoinedThread() || thread->ThreadMetadata->IsArchived)) - m_model->erase(m_temporary_thread_row); - m_temporary_thread_row = {}; - } - - const auto channel_iter = GetIteratorForChannelFromID(id); - if (channel_iter) { - m_view.expand_to_path(m_model->get_path(channel_iter)); - m_view.get_selection()->select(channel_iter); - } else { - m_view.get_selection()->unselect_all(); - // SetActiveChannel should probably just take the channel object - const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); - if (!channel.has_value() || !channel->IsThread()) return; - auto parent_iter = GetIteratorForChannelFromID(*channel->ParentID); - if (!parent_iter) return; - m_temporary_thread_row = CreateThreadRow(parent_iter->children(), *channel); - m_view.get_selection()->select(m_temporary_thread_row); - } -} - -void ChannelList::UseExpansionState(const ExpansionStateRoot &root) { - auto recurse = [this](auto &self, const ExpansionStateRoot &root) -> void { - // and these are only channels - for (const auto &[id, state] : root.Children) { - if (const auto iter = GetIteratorForChannelFromID(id)) { - if (state.IsExpanded) - m_view.expand_row(m_model->get_path(iter), false); - else - m_view.collapse_row(m_model->get_path(iter)); - } - - self(self, state.Children); - } - }; - - // top level is guild - for (const auto &[id, state] : root.Children) { - if (const auto iter = GetIteratorForGuildFromID(id)) { - if (state.IsExpanded) - m_view.expand_row(m_model->get_path(iter), false); - else - m_view.collapse_row(m_model->get_path(iter)); - } - - recurse(recurse, state.Children); - } -} - -ExpansionStateRoot ChannelList::GetExpansionState() const { - ExpansionStateRoot r; - - auto recurse = [this](auto &self, const Gtk::TreeRow &row) -> ExpansionState { - ExpansionState r; - - r.IsExpanded = row[m_columns.m_expanded]; - for (const auto &child : row.children()) - r.Children.Children[static_cast(child[m_columns.m_id])] = self(self, child); - - return r; - }; - - for (const auto &child : m_model->children()) { - const auto id = static_cast(child[m_columns.m_id]); - if (static_cast(id) == 0ULL) continue; // dont save DM header - r.Children[id] = recurse(recurse, child); - } - - return r; -} - -Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) { - auto &discord = Abaddon::Get().GetDiscordClient(); - auto &img = Abaddon::Get().GetImageManager(); - - auto guild_row = *m_model->append(); - guild_row[m_columns.m_type] = RenderType::Guild; - guild_row[m_columns.m_id] = guild.ID; - guild_row[m_columns.m_name] = "" + Glib::Markup::escape_text(guild.Name) + ""; - guild_row[m_columns.m_icon] = img.GetPlaceholder(GuildIconSize); - - static const bool show_animations = Abaddon::Get().GetSettings().GetShowAnimations(); - - if (show_animations && guild.HasAnimatedIcon()) { - const auto cb = [this, id = guild.ID](const Glib::RefPtr &pb) { - auto iter = GetIteratorForGuildFromID(id); - if (iter) (*iter)[m_columns.m_icon_anim] = pb; - }; - img.LoadAnimationFromURL(guild.GetIconURL("gif", "32"), GuildIconSize, GuildIconSize, sigc::track_obj(cb, *this)); - } else if (guild.HasIcon()) { - const auto cb = [this, id = guild.ID](const Glib::RefPtr &pb) { - auto iter = GetIteratorForGuildFromID(id); - if (iter) (*iter)[m_columns.m_icon] = pb->scale_simple(GuildIconSize, GuildIconSize, Gdk::INTERP_BILINEAR); - }; - img.LoadFromURL(guild.GetIconURL("png", "32"), sigc::track_obj(cb, *this)); - } - - if (!guild.Channels.has_value()) return guild_row; - - // separate out the channels - std::vector orphan_channels; - std::map> categories; - - for (const auto &channel_ : *guild.Channels) { - const auto channel = discord.GetChannel(channel_.ID); - if (!channel.has_value()) continue; - if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS) { - if (channel->ParentID.has_value()) - categories[*channel->ParentID].push_back(*channel); - else - orphan_channels.push_back(*channel); - } else if (channel->Type == ChannelType::GUILD_CATEGORY) { - categories[channel->ID]; - } - } - - std::map> threads; - for (const auto &tmp : *guild.Threads) { - const auto thread = discord.GetChannel(tmp.ID); - if (thread.has_value()) - threads[*thread->ParentID].push_back(*thread); - } - const auto add_threads = [&](const ChannelData &channel, Gtk::TreeRow row) { - row[m_columns.m_expanded] = true; - - const auto it = threads.find(channel.ID); - if (it == threads.end()) return; - - for (const auto &thread : it->second) - CreateThreadRow(row.children(), thread); - }; - - for (const auto &channel : orphan_channels) { - auto channel_row = *m_model->append(guild_row.children()); - channel_row[m_columns.m_type] = RenderType::TextChannel; - channel_row[m_columns.m_id] = channel.ID; - channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); - channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset; - channel_row[m_columns.m_nsfw] = channel.NSFW(); - add_threads(channel, channel_row); - } - - for (const auto &[category_id, channels] : categories) { - const auto category = discord.GetChannel(category_id); - if (!category.has_value()) continue; - auto cat_row = *m_model->append(guild_row.children()); - cat_row[m_columns.m_type] = RenderType::Category; - cat_row[m_columns.m_id] = category_id; - cat_row[m_columns.m_name] = Glib::Markup::escape_text(*category->Name); - cat_row[m_columns.m_sort] = *category->Position; - cat_row[m_columns.m_expanded] = true; - // m_view.expand_row wont work because it might not have channels - - for (const auto &channel : channels) { - auto channel_row = *m_model->append(cat_row.children()); - channel_row[m_columns.m_type] = RenderType::TextChannel; - channel_row[m_columns.m_id] = channel.ID; - channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); - channel_row[m_columns.m_sort] = *channel.Position; - channel_row[m_columns.m_nsfw] = channel.NSFW(); - add_threads(channel, channel_row); - } - } - - return guild_row; -} - -Gtk::TreeModel::iterator ChannelList::UpdateCreateChannelCategory(const ChannelData &channel) { - const auto iter = GetIteratorForGuildFromID(*channel.GuildID); - if (!iter) return {}; - - auto cat_row = *m_model->append(iter->children()); - cat_row[m_columns.m_type] = RenderType::Category; - cat_row[m_columns.m_id] = channel.ID; - cat_row[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name); - cat_row[m_columns.m_sort] = *channel.Position; - cat_row[m_columns.m_expanded] = true; - - return cat_row; -} - -Gtk::TreeModel::iterator ChannelList::CreateThreadRow(const Gtk::TreeNodeChildren &children, const ChannelData &channel) { - auto thread_iter = m_model->append(children); - auto thread_row = *thread_iter; - thread_row[m_columns.m_type] = RenderType::Thread; - thread_row[m_columns.m_id] = channel.ID; - thread_row[m_columns.m_name] = "- " + Glib::Markup::escape_text(*channel.Name); - thread_row[m_columns.m_sort] = channel.ID; - thread_row[m_columns.m_nsfw] = false; - - return thread_iter; -} - -void ChannelList::UpdateChannelCategory(const ChannelData &channel) { - auto iter = GetIteratorForChannelFromID(channel.ID); - if (!iter) return; - - (*iter)[m_columns.m_sort] = *channel.Position; - (*iter)[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name); -} - -Gtk::TreeModel::iterator ChannelList::GetIteratorForGuildFromID(Snowflake id) { - for (const auto &child : m_model->children()) { - if (child[m_columns.m_id] == id) - return child; - } - return {}; -} - -Gtk::TreeModel::iterator ChannelList::GetIteratorForChannelFromID(Snowflake id) { - std::queue queue; - for (const auto &child : m_model->children()) - for (const auto &child2 : child.children()) - queue.push(child2); - - while (!queue.empty()) { - auto item = queue.front(); - if ((*item)[m_columns.m_id] == id) return item; - for (const auto &child : item->children()) - queue.push(child); - queue.pop(); - } - - return {}; -} - -bool ChannelList::IsTextChannel(ChannelType type) { - return type == ChannelType::GUILD_TEXT || type == ChannelType::GUILD_NEWS; -} - -// this should be unncessary but something is behaving strange so its just in case -void ChannelList::OnRowCollapsed(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) { - (*iter)[m_columns.m_expanded] = false; -} - -void ChannelList::OnRowExpanded(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) { - // restore previous expansion - for (auto it = iter->children().begin(); it != iter->children().end(); it++) { - if ((*it)[m_columns.m_expanded]) - m_view.expand_row(m_model->get_path(it), false); - } - - // try and restore selection if previous collapsed - if (auto selection = m_view.get_selection(); selection && !selection->get_selected()) { - selection->select(m_last_selected); - } - - (*iter)[m_columns.m_expanded] = true; -} - -bool ChannelList::SelectionFunc(const Glib::RefPtr &model, const Gtk::TreeModel::Path &path, bool is_currently_selected) { - if (auto selection = m_view.get_selection()) - if (auto row = selection->get_selected()) - m_last_selected = m_model->get_path(row); - - auto type = (*m_model->get_iter(path))[m_columns.m_type]; - return type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread; -} - -void ChannelList::AddPrivateChannels() { - auto header_row = *m_model->append(); - header_row[m_columns.m_type] = RenderType::DMHeader; - header_row[m_columns.m_sort] = -1; - header_row[m_columns.m_name] = "Direct Messages"; - m_dm_header = m_model->get_path(header_row); - - auto &discord = Abaddon::Get().GetDiscordClient(); - auto &img = Abaddon::Get().GetImageManager(); - - const auto dm_ids = discord.GetPrivateChannels(); - for (const auto dm_id : dm_ids) { - const auto dm = discord.GetChannel(dm_id); - if (!dm.has_value()) continue; - - std::optional top_recipient; - const auto recipients = dm->GetDMRecipients(); - if (recipients.size() > 0) - top_recipient = recipients[0]; - - auto iter = m_model->append(header_row->children()); - auto row = *iter; - row[m_columns.m_type] = RenderType::DM; - row[m_columns.m_id] = dm_id; - row[m_columns.m_sort] = -(dm->LastMessageID.has_value() ? *dm->LastMessageID : dm_id); - row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize); - - if (dm->Type == ChannelType::DM && top_recipient.has_value()) - row[m_columns.m_name] = Glib::Markup::escape_text(top_recipient->Username); - else if (dm->Type == ChannelType::GROUP_DM) - row[m_columns.m_name] = std::to_string(recipients.size()) + " members"; - - if (top_recipient.has_value()) { - const auto cb = [this, iter](const Glib::RefPtr &pb) { - if (iter) - (*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR); - }; - img.LoadFromURL(top_recipient->GetAvatarURL("png", "32"), sigc::track_obj(cb, *this)); - } - } -} - -void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) { - auto header_row = m_model->get_iter(m_dm_header); - auto &img = Abaddon::Get().GetImageManager(); - - std::optional top_recipient; - const auto recipients = dm.GetDMRecipients(); - if (recipients.size() > 0) - top_recipient = recipients[0]; - - auto iter = m_model->append(header_row->children()); - auto row = *iter; - row[m_columns.m_type] = RenderType::DM; - row[m_columns.m_id] = dm.ID; - row[m_columns.m_sort] = -(dm.LastMessageID.has_value() ? *dm.LastMessageID : dm.ID); - row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize); - - if (dm.Type == ChannelType::DM && top_recipient.has_value()) - row[m_columns.m_name] = Glib::Markup::escape_text(top_recipient->Username); - else if (dm.Type == ChannelType::GROUP_DM) - row[m_columns.m_name] = std::to_string(recipients.size()) + " members"; - - if (top_recipient.has_value()) { - const auto cb = [this, iter](const Glib::RefPtr &pb) { - if (iter) - (*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR); - }; - img.LoadFromURL(top_recipient->GetAvatarURL("png", "32"), sigc::track_obj(cb, *this)); - } -} - -void ChannelList::OnMessageCreate(const Message &msg) { - const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(msg.ChannelID); - if (!channel.has_value()) return; - if (channel->Type != ChannelType::DM && channel->Type != ChannelType::GROUP_DM) return; - auto iter = GetIteratorForChannelFromID(msg.ChannelID); - if (iter) - (*iter)[m_columns.m_sort] = -msg.ID; -} - -bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) { - if (ev->button == GDK_BUTTON_SECONDARY && ev->type == GDK_BUTTON_PRESS) { - if (m_view.get_path_at_pos(ev->x, ev->y, m_path_for_menu)) { - auto row = (*m_model->get_iter(m_path_for_menu)); - switch (static_cast(row[m_columns.m_type])) { - case RenderType::Guild: - m_menu_guild.popup_at_pointer(reinterpret_cast(ev)); - break; - case RenderType::Category: - m_menu_category.popup_at_pointer(reinterpret_cast(ev)); - break; - case RenderType::TextChannel: - m_menu_channel.popup_at_pointer(reinterpret_cast(ev)); - break; - case RenderType::DM: { - const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(static_cast(row[m_columns.m_id])); - if (channel.has_value()) { - m_menu_dm_close.set_label(channel->Type == ChannelType::DM ? "Close" : "Leave"); - m_menu_dm_close.show(); - } else - m_menu_dm_close.hide(); - m_menu_dm.popup_at_pointer(reinterpret_cast(ev)); - } break; - case RenderType::Thread: { - m_menu_thread.popup_at_pointer(reinterpret_cast(ev)); - break; - } break; - default: - break; - } - } - return true; - } - return false; -} - -void ChannelList::MoveRow(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::iterator &new_parent) { - // duplicate the row data under the new parent and then delete the old row - auto row = *m_model->append(new_parent->children()); - // would be nice to be able to get all columns out at runtime so i dont need this -#define M(name) \ - row[m_columns.name] = static_cast((*iter)[m_columns.name]); - M(m_type); - M(m_id); - M(m_name); - M(m_icon); - M(m_icon_anim); - M(m_sort); - M(m_nsfw); - M(m_expanded); -#undef M - - // recursively move children - // weird construct to work around iterator invalidation (at least i think thats what the problem was) - const auto tmp = iter->children(); - const auto children = std::vector(tmp.begin(), tmp.end()); - for (size_t i = 0; i < children.size(); i++) - MoveRow(children[i], row); - - // delete original - m_model->erase(iter); -} - -void ChannelList::OnThreadSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y) { - m_menu_thread_archive.set_visible(false); - m_menu_thread_unarchive.set_visible(false); - - auto &discord = Abaddon::Get().GetDiscordClient(); - auto iter = m_model->get_iter(m_path_for_menu); - if (!iter) return; - auto channel = discord.GetChannel(static_cast((*iter)[m_columns.m_id])); - if (!channel.has_value() || !channel->ThreadMetadata.has_value()) return; - if (!discord.HasGuildPermission(discord.GetUserData().ID, *channel->GuildID, Permission::MANAGE_THREADS)) return; - - m_menu_thread_archive.set_visible(!channel->ThreadMetadata->IsArchived); - m_menu_thread_unarchive.set_visible(channel->ThreadMetadata->IsArchived); -} - -ChannelList::type_signal_action_channel_item_select ChannelList::signal_action_channel_item_select() { - return m_signal_action_channel_item_select; -} - -ChannelList::type_signal_action_guild_leave ChannelList::signal_action_guild_leave() { - return m_signal_action_guild_leave; -} - -ChannelList::type_signal_action_guild_settings ChannelList::signal_action_guild_settings() { - return m_signal_action_guild_settings; -} - -ChannelList::ModelColumns::ModelColumns() { - add(m_type); - add(m_id); - add(m_name); - add(m_icon); - add(m_icon_anim); - add(m_sort); - add(m_nsfw); - add(m_expanded); -} - -CellRendererChannels::CellRendererChannels() - : Glib::ObjectBase(typeid(CellRendererChannels)) - , Gtk::CellRenderer() - , m_property_type(*this, "render-type") - , m_property_name(*this, "name") - , m_property_pixbuf(*this, "pixbuf") - , m_property_pixbuf_animation(*this, "pixbuf-animation") - , m_property_expanded(*this, "expanded") - , m_property_nsfw(*this, "nsfw") { - property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE; - property_xpad() = 2; - property_ypad() = 2; - m_property_name.get_proxy().signal_changed().connect([this] { - m_renderer_text.property_markup() = m_property_name; - }); -} - -CellRendererChannels::~CellRendererChannels() { -} - -Glib::PropertyProxy CellRendererChannels::property_type() { - return m_property_type.get_proxy(); -} - -Glib::PropertyProxy CellRendererChannels::property_name() { - return m_property_name.get_proxy(); -} - -Glib::PropertyProxy> CellRendererChannels::property_icon() { - return m_property_pixbuf.get_proxy(); -} - -Glib::PropertyProxy> CellRendererChannels::property_icon_animation() { - return m_property_pixbuf_animation.get_proxy(); -} - -Glib::PropertyProxy CellRendererChannels::property_expanded() { - return m_property_expanded.get_proxy(); -} - -Glib::PropertyProxy CellRendererChannels::property_nsfw() { - return m_property_nsfw.get_proxy(); -} - -void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { - switch (m_property_type.get_value()) { - case RenderType::Guild: - return get_preferred_width_vfunc_guild(widget, minimum_width, natural_width); - case RenderType::Category: - return get_preferred_width_vfunc_category(widget, minimum_width, natural_width); - case RenderType::TextChannel: - return get_preferred_width_vfunc_channel(widget, minimum_width, natural_width); - case RenderType::Thread: - return get_preferred_width_vfunc_thread(widget, minimum_width, natural_width); - case RenderType::DMHeader: - return get_preferred_width_vfunc_dmheader(widget, minimum_width, natural_width); - case RenderType::DM: - return get_preferred_width_vfunc_dm(widget, minimum_width, natural_width); - } -} - -void CellRendererChannels::get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const { - switch (m_property_type.get_value()) { - case RenderType::Guild: - return get_preferred_width_for_height_vfunc_guild(widget, height, minimum_width, natural_width); - case RenderType::Category: - return get_preferred_width_for_height_vfunc_category(widget, height, minimum_width, natural_width); - case RenderType::TextChannel: - return get_preferred_width_for_height_vfunc_channel(widget, height, minimum_width, natural_width); - case RenderType::Thread: - return get_preferred_width_for_height_vfunc_thread(widget, height, minimum_width, natural_width); - case RenderType::DMHeader: - return get_preferred_width_for_height_vfunc_dmheader(widget, height, minimum_width, natural_width); - case RenderType::DM: - return get_preferred_width_for_height_vfunc_dm(widget, height, minimum_width, natural_width); - } -} - -void CellRendererChannels::get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const { - switch (m_property_type.get_value()) { - case RenderType::Guild: - return get_preferred_height_vfunc_guild(widget, minimum_height, natural_height); - case RenderType::Category: - return get_preferred_height_vfunc_category(widget, minimum_height, natural_height); - case RenderType::TextChannel: - return get_preferred_height_vfunc_channel(widget, minimum_height, natural_height); - case RenderType::Thread: - return get_preferred_height_vfunc_thread(widget, minimum_height, natural_height); - case RenderType::DMHeader: - return get_preferred_height_vfunc_dmheader(widget, minimum_height, natural_height); - case RenderType::DM: - return get_preferred_height_vfunc_dm(widget, minimum_height, natural_height); - } -} - -void CellRendererChannels::get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const { - switch (m_property_type.get_value()) { - case RenderType::Guild: - return get_preferred_height_for_width_vfunc_guild(widget, width, minimum_height, natural_height); - case RenderType::Category: - return get_preferred_height_for_width_vfunc_category(widget, width, minimum_height, natural_height); - case RenderType::TextChannel: - return get_preferred_height_for_width_vfunc_channel(widget, width, minimum_height, natural_height); - case RenderType::Thread: - return get_preferred_height_for_width_vfunc_thread(widget, width, minimum_height, natural_height); - case RenderType::DMHeader: - return get_preferred_height_for_width_vfunc_dmheader(widget, width, minimum_height, natural_height); - case RenderType::DM: - return get_preferred_height_for_width_vfunc_dm(widget, width, minimum_height, natural_height); - } -} - -void CellRendererChannels::render_vfunc(const Cairo::RefPtr &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { - switch (m_property_type.get_value()) { - case RenderType::Guild: - return render_vfunc_guild(cr, widget, background_area, cell_area, flags); - case RenderType::Category: - return render_vfunc_category(cr, widget, background_area, cell_area, flags); - case RenderType::TextChannel: - return render_vfunc_channel(cr, widget, background_area, cell_area, flags); - case RenderType::Thread: - return render_vfunc_thread(cr, widget, background_area, cell_area, flags); - case RenderType::DMHeader: - return render_vfunc_dmheader(cr, widget, background_area, cell_area, flags); - case RenderType::DM: - return render_vfunc_dm(cr, widget, background_area, cell_area, flags); - } -} - -// guild functions - -void CellRendererChannels::get_preferred_width_vfunc_guild(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { - int pixbuf_width = 0; - - if (auto pixbuf = m_property_pixbuf_animation.get_value()) - pixbuf_width = pixbuf->get_width(); - else if (auto pixbuf = m_property_pixbuf.get_value()) - pixbuf_width = pixbuf->get_width(); - - int text_min, text_nat; - m_renderer_text.get_preferred_width(widget, text_min, text_nat); - - int xpad, ypad; - get_padding(xpad, ypad); - minimum_width = std::max(text_min, pixbuf_width) + xpad * 2; - natural_width = std::max(text_nat, pixbuf_width) + xpad * 2; -} - -void CellRendererChannels::get_preferred_width_for_height_vfunc_guild(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const { - get_preferred_width_vfunc_guild(widget, minimum_width, natural_width); -} - -void CellRendererChannels::get_preferred_height_vfunc_guild(Gtk::Widget &widget, int &minimum_height, int &natural_height) const { - int pixbuf_height = 0; - if (auto pixbuf = m_property_pixbuf_animation.get_value()) - pixbuf_height = pixbuf->get_height(); - else if (auto pixbuf = m_property_pixbuf.get_value()) - pixbuf_height = pixbuf->get_height(); - - int text_min, text_nat; - m_renderer_text.get_preferred_height(widget, text_min, text_nat); - - int xpad, ypad; - get_padding(xpad, ypad); - minimum_height = std::max(text_min, pixbuf_height) + ypad * 2; - natural_height = std::max(text_nat, pixbuf_height) + ypad * 2; -} - -void CellRendererChannels::get_preferred_height_for_width_vfunc_guild(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const { - get_preferred_height_vfunc_guild(widget, minimum_height, natural_height); -} - -void CellRendererChannels::render_vfunc_guild(const Cairo::RefPtr &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { - Gtk::Requisition text_minimum, text_natural; - m_renderer_text.get_preferred_size(widget, text_minimum, text_natural); - - Gtk::Requisition minimum, natural; - get_preferred_size(widget, minimum, natural); - - int pixbuf_w, pixbuf_h = 0; - if (auto pixbuf = m_property_pixbuf_animation.get_value()) { - pixbuf_w = pixbuf->get_width(); - pixbuf_h = pixbuf->get_height(); - } else if (auto pixbuf = m_property_pixbuf.get_value()) { - pixbuf_w = pixbuf->get_width(); - pixbuf_h = pixbuf->get_height(); - } - - const double icon_w = pixbuf_w; - const double icon_h = pixbuf_h; - const double icon_x = background_area.get_x(); - const double icon_y = background_area.get_y() + background_area.get_height() / 2.0 - icon_h / 2.0; - - const double text_x = icon_x + icon_w + 5.0; - const double text_y = background_area.get_y() + background_area.get_height() / 2.0 - text_natural.height / 2.0; - const double text_w = text_natural.width; - const double text_h = text_natural.height; - - Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h); - - m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); - - const static bool hover_only = Abaddon::Get().GetSettings().GetAnimatedGuildHoverOnly(); - const bool is_hovered = flags & Gtk::CELL_RENDERER_PRELIT; - auto anim = m_property_pixbuf_animation.get_value(); - - // kinda gross - if (anim) { - auto map_iter = m_pixbuf_anim_iters.find(anim); - if (map_iter == m_pixbuf_anim_iters.end()) - m_pixbuf_anim_iters[anim] = anim->get_iter(nullptr); - auto pb_iter = m_pixbuf_anim_iters.at(anim); - - const auto cb = [this, &widget, anim, icon_x, icon_y, icon_w, icon_h] { - if (m_pixbuf_anim_iters.at(anim)->advance()) - widget.queue_draw_area(icon_x, icon_y, icon_w, icon_h); - }; - - if ((hover_only && is_hovered) || !hover_only) - Glib::signal_timeout().connect_once(sigc::track_obj(cb, widget), pb_iter->get_delay_time()); - if (hover_only && !is_hovered) - m_pixbuf_anim_iters[anim] = anim->get_iter(nullptr); - - Gdk::Cairo::set_source_pixbuf(cr, pb_iter->get_pixbuf(), icon_x, icon_y); - cr->rectangle(icon_x, icon_y, icon_w, icon_h); - cr->fill(); - } else if (auto pixbuf = m_property_pixbuf.get_value()) { - Gdk::Cairo::set_source_pixbuf(cr, pixbuf, icon_x, icon_y); - cr->rectangle(icon_x, icon_y, icon_w, icon_h); - cr->fill(); - } -} - -// category - -void CellRendererChannels::get_preferred_width_vfunc_category(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { - m_renderer_text.get_preferred_width(widget, minimum_width, natural_width); -} - -void CellRendererChannels::get_preferred_width_for_height_vfunc_category(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const { - m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width); -} - -void CellRendererChannels::get_preferred_height_vfunc_category(Gtk::Widget &widget, int &minimum_height, int &natural_height) const { - m_renderer_text.get_preferred_height(widget, minimum_height, natural_height); -} - -void CellRendererChannels::get_preferred_height_for_width_vfunc_category(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const { - m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height); -} - -void CellRendererChannels::render_vfunc_category(const Cairo::RefPtr &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { - // todo: figure out how Gtk::Arrow is rendered because i like it better :^) - constexpr static int len = 5; - int x1, y1, x2, y2, x3, y3; - if (property_expanded()) { - x1 = background_area.get_x() + 7; - y1 = background_area.get_y() + background_area.get_height() / 2 - len; - x2 = background_area.get_x() + 7 + len; - y2 = background_area.get_y() + background_area.get_height() / 2 + len; - x3 = background_area.get_x() + 7 + len * 2; - y3 = background_area.get_y() + background_area.get_height() / 2 - len; - } else { - x1 = background_area.get_x() + 7; - y1 = background_area.get_y() + background_area.get_height() / 2 - len; - x2 = background_area.get_x() + 7 + len * 2; - y2 = background_area.get_y() + background_area.get_height() / 2; - x3 = background_area.get_x() + 7; - y3 = background_area.get_y() + background_area.get_height() / 2 + len; - } - cr->move_to(x1, y1); - cr->line_to(x2, y2); - cr->line_to(x3, y3); - static const auto expander_color = Gdk::RGBA(Abaddon::Get().GetSettings().GetChannelsExpanderColor()); - cr->set_source_rgb(expander_color.get_red(), expander_color.get_green(), expander_color.get_blue()); - cr->stroke(); - - Gtk::Requisition text_minimum, text_natural; - m_renderer_text.get_preferred_size(widget, text_minimum, text_natural); - - const int text_x = background_area.get_x() + 22; - const int text_y = background_area.get_y() + background_area.get_height() / 2 - text_natural.height / 2; - const int text_w = text_natural.width; - const int text_h = text_natural.height; - - Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h); - - m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); -} - -// text channel - -void CellRendererChannels::get_preferred_width_vfunc_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { - m_renderer_text.get_preferred_width(widget, minimum_width, natural_width); -} - -void CellRendererChannels::get_preferred_width_for_height_vfunc_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const { - m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width); -} - -void CellRendererChannels::get_preferred_height_vfunc_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const { - m_renderer_text.get_preferred_height(widget, minimum_height, natural_height); -} - -void CellRendererChannels::get_preferred_height_for_width_vfunc_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const { - m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height); -} - -void CellRendererChannels::render_vfunc_channel(const Cairo::RefPtr &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { - Gtk::Requisition minimum_size, natural_size; - m_renderer_text.get_preferred_size(widget, minimum_size, natural_size); - - const int text_x = background_area.get_x() + 21; - const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2; - const int text_w = natural_size.width; - const int text_h = natural_size.height; - - Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h); - - const static auto nsfw_color = Gdk::RGBA(Abaddon::Get().GetSettings().GetNSFWChannelColor()); - if (m_property_nsfw.get_value()) - m_renderer_text.property_foreground_rgba() = nsfw_color; - m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); - // setting property_foreground_rgba() sets this to true which makes non-nsfw cells use the property too which is bad - // so unset it - m_renderer_text.property_foreground_set() = false; -} - -// thread - -void CellRendererChannels::get_preferred_width_vfunc_thread(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { - m_renderer_text.get_preferred_width(widget, minimum_width, natural_width); -} - -void CellRendererChannels::get_preferred_width_for_height_vfunc_thread(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const { - get_preferred_width_vfunc_thread(widget, minimum_width, natural_width); -} - -void CellRendererChannels::get_preferred_height_vfunc_thread(Gtk::Widget &widget, int &minimum_height, int &natural_height) const { - m_renderer_text.get_preferred_height(widget, minimum_height, natural_height); -} - -void CellRendererChannels::get_preferred_height_for_width_vfunc_thread(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const { - get_preferred_height_vfunc_thread(widget, minimum_height, natural_height); -} - -void CellRendererChannels::render_vfunc_thread(const Cairo::RefPtr &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { - Gtk::Requisition minimum_size, natural_size; - m_renderer_text.get_preferred_size(widget, minimum_size, natural_size); - - const int text_x = background_area.get_x() + 26; - const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2; - const int text_w = natural_size.width; - const int text_h = natural_size.height; - - Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h); - m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); -} - -// dm header - -void CellRendererChannels::get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { - m_renderer_text.get_preferred_width(widget, minimum_width, natural_width); -} - -void CellRendererChannels::get_preferred_width_for_height_vfunc_dmheader(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const { - m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width); -} - -void CellRendererChannels::get_preferred_height_vfunc_dmheader(Gtk::Widget &widget, int &minimum_height, int &natural_height) const { - m_renderer_text.get_preferred_height(widget, minimum_height, natural_height); -} - -void CellRendererChannels::get_preferred_height_for_width_vfunc_dmheader(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const { - m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height); -} - -void CellRendererChannels::render_vfunc_dmheader(const Cairo::RefPtr &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { - // gdk::rectangle more like gdk::stupid - Gdk::Rectangle text_cell_area( - cell_area.get_x() + 9, cell_area.get_y(), // maybe theres a better way to align this ? - cell_area.get_width(), cell_area.get_height()); - m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); -} - -// dm (basically the same thing as guild) - -void CellRendererChannels::get_preferred_width_vfunc_dm(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { - int pixbuf_width = 0; - if (auto pixbuf = m_property_pixbuf.get_value()) - pixbuf_width = pixbuf->get_width(); - - int text_min, text_nat; - m_renderer_text.get_preferred_width(widget, text_min, text_nat); - - int xpad, ypad; - get_padding(xpad, ypad); - minimum_width = std::max(text_min, pixbuf_width) + xpad * 2; - natural_width = std::max(text_nat, pixbuf_width) + xpad * 2; -} - -void CellRendererChannels::get_preferred_width_for_height_vfunc_dm(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const { - get_preferred_width_vfunc_guild(widget, minimum_width, natural_width); -} - -void CellRendererChannels::get_preferred_height_vfunc_dm(Gtk::Widget &widget, int &minimum_height, int &natural_height) const { - int pixbuf_height = 0; - if (auto pixbuf = m_property_pixbuf.get_value()) - pixbuf_height = pixbuf->get_height(); - - int text_min, text_nat; - m_renderer_text.get_preferred_height(widget, text_min, text_nat); - - int xpad, ypad; - get_padding(xpad, ypad); - minimum_height = std::max(text_min, pixbuf_height) + ypad * 2; - natural_height = std::max(text_nat, pixbuf_height) + ypad * 2; -} - -void CellRendererChannels::get_preferred_height_for_width_vfunc_dm(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const { - get_preferred_height_vfunc_guild(widget, minimum_height, natural_height); -} - -void CellRendererChannels::render_vfunc_dm(const Cairo::RefPtr &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { - Gtk::Requisition text_minimum, text_natural; - m_renderer_text.get_preferred_size(widget, text_minimum, text_natural); - - Gtk::Requisition minimum, natural; - get_preferred_size(widget, minimum, natural); - - auto pixbuf = m_property_pixbuf.get_value(); - - const double icon_w = pixbuf->get_width(); - const double icon_h = pixbuf->get_height(); - const double icon_x = background_area.get_x() + 2; - const double icon_y = background_area.get_y() + background_area.get_height() / 2.0 - icon_h / 2.0; - - const double text_x = icon_x + icon_w + 5.0; - const double text_y = background_area.get_y() + background_area.get_height() / 2.0 - text_natural.height / 2.0; - const double text_w = text_natural.width; - const double text_h = text_natural.height; - - Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h); - - m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); - - Gdk::Cairo::set_source_pixbuf(cr, m_property_pixbuf.get_value(), icon_x, icon_y); - cr->rectangle(icon_x, icon_y, icon_w, icon_h); - cr->fill(); -} diff --git a/components/channels.hpp b/components/channels.hpp deleted file mode 100644 index 1faf367..0000000 --- a/components/channels.hpp +++ /dev/null @@ -1,250 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -#include "discord/discord.hpp" -#include "state.hpp" - -constexpr static int GuildIconSize = 24; -constexpr static int DMIconSize = 20; -constexpr static int OrphanChannelSortOffset = -100; // forces orphan channels to the top of the list - -enum class RenderType : uint8_t { - Guild, - Category, - TextChannel, - Thread, - - DMHeader, - DM, -}; - -class CellRendererChannels : public Gtk::CellRenderer { -public: - CellRendererChannels(); - virtual ~CellRendererChannels(); - - Glib::PropertyProxy property_type(); - Glib::PropertyProxy property_name(); - Glib::PropertyProxy> property_icon(); - Glib::PropertyProxy> property_icon_animation(); - Glib::PropertyProxy property_expanded(); - Glib::PropertyProxy property_nsfw(); - -protected: - void get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const override; - void get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const override; - void get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const override; - void get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const override; - void render_vfunc(const Cairo::RefPtr &cr, - Gtk::Widget &widget, - const Gdk::Rectangle &background_area, - const Gdk::Rectangle &cell_area, - Gtk::CellRendererState flags) override; - - // guild functions - void get_preferred_width_vfunc_guild(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; - void get_preferred_width_for_height_vfunc_guild(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; - void get_preferred_height_vfunc_guild(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; - void get_preferred_height_for_width_vfunc_guild(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; - void render_vfunc_guild(const Cairo::RefPtr &cr, - Gtk::Widget &widget, - const Gdk::Rectangle &background_area, - const Gdk::Rectangle &cell_area, - Gtk::CellRendererState flags); - - // category - void get_preferred_width_vfunc_category(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; - void get_preferred_width_for_height_vfunc_category(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; - void get_preferred_height_vfunc_category(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; - void get_preferred_height_for_width_vfunc_category(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; - void render_vfunc_category(const Cairo::RefPtr &cr, - Gtk::Widget &widget, - const Gdk::Rectangle &background_area, - const Gdk::Rectangle &cell_area, - Gtk::CellRendererState flags); - - // text channel - void get_preferred_width_vfunc_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; - void get_preferred_width_for_height_vfunc_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; - void get_preferred_height_vfunc_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; - void get_preferred_height_for_width_vfunc_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; - void render_vfunc_channel(const Cairo::RefPtr &cr, - Gtk::Widget &widget, - const Gdk::Rectangle &background_area, - const Gdk::Rectangle &cell_area, - Gtk::CellRendererState flags); - - // thread - void get_preferred_width_vfunc_thread(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; - void get_preferred_width_for_height_vfunc_thread(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; - void get_preferred_height_vfunc_thread(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; - void get_preferred_height_for_width_vfunc_thread(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; - void render_vfunc_thread(const Cairo::RefPtr &cr, - Gtk::Widget &widget, - const Gdk::Rectangle &background_area, - const Gdk::Rectangle &cell_area, - Gtk::CellRendererState flags); - - // dm header - void get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; - void get_preferred_width_for_height_vfunc_dmheader(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; - void get_preferred_height_vfunc_dmheader(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; - void get_preferred_height_for_width_vfunc_dmheader(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; - void render_vfunc_dmheader(const Cairo::RefPtr &cr, - Gtk::Widget &widget, - const Gdk::Rectangle &background_area, - const Gdk::Rectangle &cell_area, - Gtk::CellRendererState flags); - - // dm - void get_preferred_width_vfunc_dm(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; - void get_preferred_width_for_height_vfunc_dm(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; - void get_preferred_height_vfunc_dm(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; - void get_preferred_height_for_width_vfunc_dm(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; - void render_vfunc_dm(const Cairo::RefPtr &cr, - Gtk::Widget &widget, - const Gdk::Rectangle &background_area, - const Gdk::Rectangle &cell_area, - Gtk::CellRendererState flags); - -private: - Gtk::CellRendererText m_renderer_text; - - Glib::Property m_property_type; // all - Glib::Property m_property_name; // all - Glib::Property> m_property_pixbuf; // guild, dm - Glib::Property> m_property_pixbuf_animation; // guild - Glib::Property m_property_expanded; // category - Glib::Property m_property_nsfw; // channel - - // same pitfalls as in https://github.com/uowuo/abaddon/blob/60404783bd4ce9be26233fe66fc3a74475d9eaa3/components/cellrendererpixbufanimation.hpp#L32-L39 - // this will manifest though since guild icons can change - // an animation or two wont be the end of the world though - std::map, Glib::RefPtr> m_pixbuf_anim_iters; -}; - -class ChannelList : public Gtk::ScrolledWindow { -public: - ChannelList(); - - void UpdateListing(); - void SetActiveChannel(Snowflake id); - - // channel list should be populated when this is called - void UseExpansionState(const ExpansionStateRoot &state); - ExpansionStateRoot GetExpansionState() const; - -protected: - void UpdateNewGuild(const GuildData &guild); - void UpdateRemoveGuild(Snowflake id); - void UpdateRemoveChannel(Snowflake id); - void UpdateChannel(Snowflake id); - void UpdateCreateChannel(const ChannelData &channel); - void UpdateGuild(Snowflake id); - void DeleteThreadRow(Snowflake id); - - void OnThreadJoined(Snowflake id); - void OnThreadRemoved(Snowflake id); - void OnThreadDelete(const ThreadDeleteData &data); - void OnThreadUpdate(const ThreadUpdateData &data); - void OnThreadListSync(const ThreadListSyncData &data); - - Gtk::TreeView m_view; - - class ModelColumns : public Gtk::TreeModel::ColumnRecord { - public: - ModelColumns(); - - Gtk::TreeModelColumn m_type; - Gtk::TreeModelColumn m_id; - Gtk::TreeModelColumn m_name; - Gtk::TreeModelColumn> m_icon; - Gtk::TreeModelColumn> m_icon_anim; - Gtk::TreeModelColumn m_sort; - Gtk::TreeModelColumn m_nsfw; - // Gtk::CellRenderer's property_is_expanded only works how i want it to if it has children - // because otherwise it doesnt count as an "expander" (property_is_expander) - // so this solution will have to do which i hate but the alternative is adding invisible children - // to all categories without children and having a filter model but that sounds worse - // of course its a lot better than the absolute travesty i had before - Gtk::TreeModelColumn m_expanded; - }; - - ModelColumns m_columns; - Glib::RefPtr m_model; - - Gtk::TreeModel::iterator AddGuild(const GuildData &guild); - Gtk::TreeModel::iterator UpdateCreateChannelCategory(const ChannelData &channel); - Gtk::TreeModel::iterator CreateThreadRow(const Gtk::TreeNodeChildren &children, const ChannelData &channel); - - void UpdateChannelCategory(const ChannelData &channel); - - // separation necessary because a channel and guild can share the same id - Gtk::TreeModel::iterator GetIteratorForGuildFromID(Snowflake id); - Gtk::TreeModel::iterator GetIteratorForChannelFromID(Snowflake id); - - bool IsTextChannel(ChannelType type); - - void OnRowCollapsed(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path); - void OnRowExpanded(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path); - bool SelectionFunc(const Glib::RefPtr &model, const Gtk::TreeModel::Path &path, bool is_currently_selected); - bool OnButtonPressEvent(GdkEventButton *ev); - - void MoveRow(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::iterator &new_parent); - - Gtk::TreeModel::Path m_last_selected; - Gtk::TreeModel::Path m_dm_header; - - void AddPrivateChannels(); - void UpdateCreateDMChannel(const ChannelData &channel); - - void OnMessageCreate(const Message &msg); - Gtk::TreeModel::Path m_path_for_menu; - - // cant be recovered through selection - Gtk::TreeModel::iterator m_temporary_thread_row; - - Gtk::Menu m_menu_guild; - Gtk::MenuItem m_menu_guild_copy_id; - Gtk::MenuItem m_menu_guild_settings; - Gtk::MenuItem m_menu_guild_leave; - - Gtk::Menu m_menu_category; - Gtk::MenuItem m_menu_category_copy_id; - - Gtk::Menu m_menu_channel; - Gtk::MenuItem m_menu_channel_copy_id; - - Gtk::Menu m_menu_dm; - Gtk::MenuItem m_menu_dm_copy_id; - Gtk::MenuItem m_menu_dm_close; - - Gtk::Menu m_menu_thread; - Gtk::MenuItem m_menu_thread_copy_id; - Gtk::MenuItem m_menu_thread_leave; - Gtk::MenuItem m_menu_thread_archive; - Gtk::MenuItem m_menu_thread_unarchive; - - void OnThreadSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y); - - bool m_updating_listing = false; - -public: - typedef sigc::signal type_signal_action_channel_item_select; - typedef sigc::signal type_signal_action_guild_leave; - typedef sigc::signal type_signal_action_guild_settings; - - type_signal_action_channel_item_select signal_action_channel_item_select(); - type_signal_action_guild_leave signal_action_guild_leave(); - type_signal_action_guild_settings signal_action_guild_settings(); - -protected: - type_signal_action_channel_item_select m_signal_action_channel_item_select; - type_signal_action_guild_leave m_signal_action_guild_leave; - type_signal_action_guild_settings m_signal_action_guild_settings; -}; diff --git a/components/chatinput.cpp b/components/chatinput.cpp deleted file mode 100644 index c3eca32..0000000 --- a/components/chatinput.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "chatinput.hpp" - -ChatInput::ChatInput() { - get_style_context()->add_class("message-input"); - set_propagate_natural_height(true); - set_min_content_height(20); - set_max_content_height(250); - set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); - - // hack - auto cb = [this](GdkEventKey *e) -> bool { - return event(reinterpret_cast(e)); - }; - m_textview.signal_key_press_event().connect(cb, false); - m_textview.set_hexpand(false); - m_textview.set_halign(Gtk::ALIGN_FILL); - m_textview.set_valign(Gtk::ALIGN_CENTER); - m_textview.set_wrap_mode(Gtk::WRAP_WORD_CHAR); - m_textview.show(); - add(m_textview); -} - -void ChatInput::InsertText(const Glib::ustring &text) { - GetBuffer()->insert_at_cursor(text); - m_textview.grab_focus(); -} - -Glib::RefPtr ChatInput::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) { - if (event->keyval == GDK_KEY_Escape) { - m_signal_escape.emit(); - return true; - } - - if (event->keyval == GDK_KEY_Return) { - if (event->state & GDK_SHIFT_MASK) - return false; - - auto buf = GetBuffer(); - auto text = buf->get_text(); - - const bool accepted = m_signal_submit.emit(text); - if (accepted) - buf->set_text(""); - - return true; - } - - return false; -} - -void ChatInput::on_grab_focus() { - m_textview.grab_focus(); -} - -ChatInput::type_signal_submit ChatInput::signal_submit() { - return m_signal_submit; -} - -ChatInput::type_signal_escape ChatInput::signal_escape() { - return m_signal_escape; -} diff --git a/components/chatinput.hpp b/components/chatinput.hpp deleted file mode 100644 index ad7f0b1..0000000 --- a/components/chatinput.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once -#include - -class ChatInput : public Gtk::ScrolledWindow { -public: - ChatInput(); - - void InsertText(const Glib::ustring &text); - Glib::RefPtr GetBuffer(); - bool ProcessKeyPress(GdkEventKey *event); - -protected: - void on_grab_focus() override; - -private: - Gtk::TextView m_textview; - -public: - typedef sigc::signal type_signal_submit; - typedef sigc::signal type_signal_escape; - - type_signal_submit signal_submit(); - type_signal_escape signal_escape(); - -private: - type_signal_submit m_signal_submit; - type_signal_escape m_signal_escape; -}; diff --git a/components/chatinputindicator.cpp b/components/chatinputindicator.cpp deleted file mode 100644 index 9b063b2..0000000 --- a/components/chatinputindicator.cpp +++ /dev/null @@ -1,121 +0,0 @@ -#include -#include "chatinputindicator.hpp" -#include "abaddon.hpp" -#include "util.hpp" - -constexpr static const int MaxUsersInIndicator = 4; - -ChatInputIndicator::ChatInputIndicator() - : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) { - m_label.set_text(""); - m_label.set_ellipsize(Pango::ELLIPSIZE_END); - m_label.set_valign(Gtk::ALIGN_END); - m_img.set_margin_right(5); - get_style_context()->add_class("typing-indicator"); - - Abaddon::Get().GetDiscordClient().signal_typing_start().connect(sigc::mem_fun(*this, &ChatInputIndicator::OnUserTypingStart)); - Abaddon::Get().GetDiscordClient().signal_message_create().connect(sigc::mem_fun(*this, &ChatInputIndicator::OnMessageCreate)); - - add(m_img); - add(m_label); - m_label.show(); - - // try loading gif - const static auto path = Abaddon::GetResPath("/typing_indicator.gif"); - if (!std::filesystem::exists(path)) return; - auto gif_data = ReadWholeFile(path); - auto loader = Gdk::PixbufLoader::create(); - loader->signal_size_prepared().connect([&](int inw, int inh) { - int w, h; - GetImageDimensions(inw, inh, w, h, 20, 10); - loader->set_size(w, h); - }); - loader->write(gif_data.data(), gif_data.size()); - try { - loader->close(); - m_img.property_pixbuf_animation() = loader->get_animation(); - } catch (const std::exception &) {} -} - -void ChatInputIndicator::AddUser(Snowflake channel_id, const UserData &user, int timeout) { - auto current_connection_it = m_typers[channel_id].find(user.ID); - if (current_connection_it != m_typers.at(channel_id).end()) { - current_connection_it->second.disconnect(); - m_typers.at(channel_id).erase(current_connection_it); - } - - Snowflake user_id = user.ID; - auto cb = [this, user_id, channel_id]() -> bool { - m_typers.at(channel_id).erase(user_id); - ComputeTypingString(); - return false; - }; - m_typers[channel_id][user.ID] = Glib::signal_timeout().connect_seconds(cb, timeout); - ComputeTypingString(); -} - -void ChatInputIndicator::SetActiveChannel(Snowflake id) { - m_active_channel = id; - ComputeTypingString(); -} - -void ChatInputIndicator::SetCustomMarkup(const Glib::ustring &str) { - m_custom_markup = str; - ComputeTypingString(); -} - -void ChatInputIndicator::ClearCustom() { - m_custom_markup = ""; - ComputeTypingString(); -} - -void ChatInputIndicator::OnUserTypingStart(Snowflake user_id, Snowflake channel_id) { - const auto &discord = Abaddon::Get().GetDiscordClient(); - const auto user = discord.GetUser(user_id); - if (!user.has_value()) return; - - AddUser(channel_id, *user, 10); -} - -void ChatInputIndicator::OnMessageCreate(const Message &message) { - m_typers[message.ChannelID].erase(message.Author.ID); - ComputeTypingString(); -} - -void ChatInputIndicator::SetTypingString(const Glib::ustring &str) { - m_label.set_text(str); - if (str == "") - m_img.hide(); - else if (m_img.property_pixbuf_animation().get_value()) - m_img.show(); -} - -void ChatInputIndicator::ComputeTypingString() { - if (m_custom_markup != "") { - m_label.set_markup(m_custom_markup); - m_img.hide(); - return; - } - - const auto &discord = Abaddon::Get().GetDiscordClient(); - std::vector typers; - for (const auto &[id, conn] : m_typers[m_active_channel]) { - const auto user = discord.GetUser(id); - if (user.has_value()) - typers.push_back(*user); - } - if (typers.size() == 0) { - SetTypingString(""); - } else if (typers.size() == 1) { - SetTypingString(typers[0].Username + " is typing..."); - } else if (typers.size() == 2) { - SetTypingString(typers[0].Username + " and " + typers[1].Username + " are typing..."); - } else if (typers.size() > 2 && typers.size() <= MaxUsersInIndicator) { - Glib::ustring str; - for (size_t i = 0; i < typers.size() - 1; i++) - str += typers[i].Username + ", "; - SetTypingString(str + "and " + typers[typers.size() - 1].Username + " are typing..."); - } else { // size() > MaxUsersInIndicator - SetTypingString("Several people are typing..."); - } -} diff --git a/components/chatinputindicator.hpp b/components/chatinputindicator.hpp deleted file mode 100644 index ec70dfb..0000000 --- a/components/chatinputindicator.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once -#include -#include -#include "discord/message.hpp" -#include "discord/user.hpp" - -class ChatInputIndicator : public Gtk::Box { -public: - ChatInputIndicator(); - void SetActiveChannel(Snowflake id); - void SetCustomMarkup(const Glib::ustring &str); - void ClearCustom(); - -private: - void AddUser(Snowflake channel_id, const UserData &user, int timeout); - void OnUserTypingStart(Snowflake user_id, Snowflake channel_id); - void OnMessageCreate(const Message &message); - void SetTypingString(const Glib::ustring &str); - void ComputeTypingString(); - - Gtk::Image m_img; - Gtk::Label m_label; - - Glib::ustring m_custom_markup; - - Snowflake m_active_channel; - std::unordered_map> m_typers; // channel id -> [user id -> connection] -}; diff --git a/components/chatlist.cpp b/components/chatlist.cpp deleted file mode 100644 index 5b3f357..0000000 --- a/components/chatlist.cpp +++ /dev/null @@ -1,368 +0,0 @@ -#include "chatmessage.hpp" -#include "chatlist.hpp" -#include "abaddon.hpp" -#include "constants.hpp" - -ChatList::ChatList() { - m_list.get_style_context()->add_class("messages"); - - set_can_focus(false); - set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS); - signal_edge_reached().connect(sigc::mem_fun(*this, &ChatList::OnScrollEdgeOvershot)); - - auto v = get_vadjustment(); - v->signal_value_changed().connect([this, v] { - m_should_scroll_to_bottom = v->get_upper() - v->get_page_size() <= v->get_value(); - }); - - m_list.signal_size_allocate().connect([this](Gtk::Allocation &) { - if (m_should_scroll_to_bottom) - ScrollToBottom(); - }); - - m_list.set_focus_hadjustment(get_hadjustment()); - m_list.set_focus_vadjustment(get_vadjustment()); - m_list.set_selection_mode(Gtk::SELECTION_NONE); - m_list.set_hexpand(true); - m_list.set_vexpand(true); - - add(m_list); - - m_list.show(); - - m_menu_copy_id = Gtk::manage(new Gtk::MenuItem("Copy ID")); - m_menu_copy_id->signal_activate().connect([this] { - Gtk::Clipboard::get()->set_text(std::to_string(m_menu_selected_message)); - }); - m_menu_copy_id->show(); - m_menu.append(*m_menu_copy_id); - - m_menu_delete_message = Gtk::manage(new Gtk::MenuItem("Delete Message")); - m_menu_delete_message->signal_activate().connect([this] { - Abaddon::Get().GetDiscordClient().DeleteMessage(m_active_channel, m_menu_selected_message); - }); - m_menu_delete_message->show(); - m_menu.append(*m_menu_delete_message); - - m_menu_edit_message = Gtk::manage(new Gtk::MenuItem("Edit Message")); - m_menu_edit_message->signal_activate().connect([this] { - m_signal_action_message_edit.emit(m_active_channel, m_menu_selected_message); - }); - m_menu_edit_message->show(); - m_menu.append(*m_menu_edit_message); - - m_menu_copy_content = Gtk::manage(new Gtk::MenuItem("Copy Content")); - m_menu_copy_content->signal_activate().connect([this] { - const auto msg = Abaddon::Get().GetDiscordClient().GetMessage(m_menu_selected_message); - if (msg.has_value()) - Gtk::Clipboard::get()->set_text(msg->Content); - }); - m_menu_copy_content->show(); - m_menu.append(*m_menu_copy_content); - - m_menu_reply_to = Gtk::manage(new Gtk::MenuItem("Reply To")); - m_menu_reply_to->signal_activate().connect([this] { - m_signal_action_reply_to.emit(m_menu_selected_message); - }); - m_menu_reply_to->show(); - m_menu.append(*m_menu_reply_to); - - m_menu_unpin = Gtk::manage(new Gtk::MenuItem("Unpin")); - m_menu_unpin->signal_activate().connect([this] { - Abaddon::Get().GetDiscordClient().Unpin(m_active_channel, m_menu_selected_message, [](...) {}); - }); - m_menu.append(*m_menu_unpin); - - m_menu_pin = Gtk::manage(new Gtk::MenuItem("Pin")); - m_menu_pin->signal_activate().connect([this] { - Abaddon::Get().GetDiscordClient().Pin(m_active_channel, m_menu_selected_message, [](...) {}); - }); - m_menu.append(*m_menu_pin); - - m_menu.show(); -} - -void ChatList::Clear() { - auto children = m_list.get_children(); - auto it = children.begin(); - while (it != children.end()) { - delete *it; - it++; - } -} - -void ChatList::SetActiveChannel(Snowflake id) { - m_active_channel = id; -} - -void ChatList::ProcessNewMessage(const Message &data, bool prepend) { - auto &discord = Abaddon::Get().GetDiscordClient(); - if (!discord.IsStarted()) return; - - // delete preview message when gateway sends it back - if (!data.IsPending && data.Nonce.has_value() && data.Author.ID == discord.GetUserData().ID) { - for (auto [id, widget] : m_id_to_widget) { - if (dynamic_cast(widget)->Nonce == *data.Nonce) { - RemoveMessageAndHeader(widget); - m_id_to_widget.erase(id); - break; - } - } - } - - ChatMessageHeader *last_row = nullptr; - bool should_attach = false; - if (!m_separate_all && m_num_rows > 0) { - if (prepend) - last_row = dynamic_cast(m_list.get_row_at_index(0)); - else - last_row = dynamic_cast(m_list.get_row_at_index(m_num_rows - 1)); - - if (last_row != nullptr) { - const uint64_t diff = std::max(data.ID, last_row->NewestID) - std::min(data.ID, last_row->NewestID); - if (last_row->UserID == data.Author.ID && (prepend || (diff < SnowflakeSplitDifference * Snowflake::SecondsInterval))) - should_attach = true; - } - } - - m_num_messages++; - - if (m_should_scroll_to_bottom && !prepend) { - while (m_num_messages > MaxMessagesForChatCull) { - auto first_it = m_id_to_widget.begin(); - RemoveMessageAndHeader(first_it->second); - m_id_to_widget.erase(first_it); - } - } - - ChatMessageHeader *header; - if (should_attach) { - header = last_row; - } else { - const auto chan = discord.GetChannel(m_active_channel); - Snowflake guild_id; - if (chan.has_value() && chan->GuildID.has_value()) - guild_id = *chan->GuildID; - const auto user_id = data.Author.ID; - const auto user = discord.GetUser(user_id); - if (!user.has_value()) return; - - header = Gtk::manage(new ChatMessageHeader(data)); - header->signal_action_insert_mention().connect([this, user_id]() { - m_signal_action_insert_mention.emit(user_id); - }); - - header->signal_action_open_user_menu().connect([this, user_id, guild_id](const GdkEvent *event) { - m_signal_action_open_user_menu.emit(event, user_id, guild_id); - }); - - m_num_rows++; - } - - auto *content = ChatMessageItemContainer::FromMessage(data); - if (content != nullptr) { - header->AddContent(content, prepend); - m_id_to_widget[data.ID] = content; - - const auto cb = [this, id = data.ID](GdkEventButton *ev) -> bool { - if (ev->type == GDK_BUTTON_PRESS && ev->button == GDK_BUTTON_SECONDARY) { - m_menu_selected_message = id; - - const auto &client = Abaddon::Get().GetDiscordClient(); - const auto data = client.GetMessage(id); - if (!data.has_value()) return false; - const auto channel = client.GetChannel(m_active_channel); - - bool has_manage = channel.has_value() && (channel->Type == ChannelType::DM || channel->Type == ChannelType::GROUP_DM); - if (!has_manage) - has_manage = client.HasChannelPermission(client.GetUserData().ID, m_active_channel, Permission::MANAGE_MESSAGES); - - m_menu_edit_message->set_visible(!m_use_pinned_menu); - m_menu_reply_to->set_visible(!m_use_pinned_menu); - m_menu_unpin->set_visible(has_manage && data->IsPinned); - m_menu_pin->set_visible(has_manage && !data->IsPinned); - - if (data->IsDeleted()) { - m_menu_delete_message->set_sensitive(false); - m_menu_edit_message->set_sensitive(false); - } else { - const bool can_edit = client.GetUserData().ID == data->Author.ID; - const bool can_delete = can_edit || has_manage; - m_menu_delete_message->set_sensitive(can_delete); - m_menu_edit_message->set_sensitive(can_edit); - } - - m_menu.popup_at_pointer(reinterpret_cast(ev)); - } - return false; - }; - content->signal_button_press_event().connect(cb); - - if (!data.IsPending) { - content->signal_action_reaction_add().connect([this, id = data.ID](const Glib::ustring ¶m) { - m_signal_action_reaction_add.emit(id, param); - }); - content->signal_action_reaction_remove().connect([this, id = data.ID](const Glib::ustring ¶m) { - m_signal_action_reaction_remove.emit(id, param); - }); - content->signal_action_channel_click().connect([this](const Snowflake &id) { - m_signal_action_channel_click.emit(id); - }); - } - } - - header->set_margin_left(5); - header->show_all(); - - if (!should_attach) { - if (prepend) - m_list.prepend(*header); - else - m_list.add(*header); - } -} - -void ChatList::DeleteMessage(Snowflake id) { - auto widget = m_id_to_widget.find(id); - if (widget == m_id_to_widget.end()) return; - - auto *x = dynamic_cast(widget->second); - if (x != nullptr) - x->UpdateAttributes(); -} - -void ChatList::RefetchMessage(Snowflake id) { - auto widget = m_id_to_widget.find(id); - if (widget == m_id_to_widget.end()) return; - - auto *x = dynamic_cast(widget->second); - if (x != nullptr) { - x->UpdateContent(); - x->UpdateAttributes(); - } -} - -Snowflake ChatList::GetOldestListedMessage() { - if (m_id_to_widget.size() > 0) - return m_id_to_widget.begin()->first; - else - return Snowflake::Invalid; -} - -void ChatList::UpdateMessageReactions(Snowflake id) { - auto it = m_id_to_widget.find(id); - if (it == m_id_to_widget.end()) return; - auto *widget = dynamic_cast(it->second); - if (widget == nullptr) return; - widget->UpdateReactions(); -} - -void ChatList::SetFailedByNonce(const std::string &nonce) { - for (auto [id, widget] : m_id_to_widget) { - if (auto *container = dynamic_cast(widget); container->Nonce == nonce) { - container->SetFailed(); - break; - } - } -} - -std::vector ChatList::GetRecentAuthors() { - const auto &discord = Abaddon::Get().GetDiscordClient(); - std::vector ret; - - std::map ordered(m_id_to_widget.begin(), m_id_to_widget.end()); - - for (auto it = ordered.crbegin(); it != ordered.crend(); it++) { - const auto *widget = dynamic_cast(it->second); - if (widget == nullptr) continue; - const auto msg = discord.GetMessage(widget->ID); - if (!msg.has_value()) continue; - if (std::find(ret.begin(), ret.end(), msg->Author.ID) == ret.end()) - ret.push_back(msg->Author.ID); - } - - const auto chan = discord.GetChannel(m_active_channel); - if (chan->GuildID.has_value()) { - const auto others = discord.GetUsersInGuild(*chan->GuildID); - for (const auto id : others) - if (std::find(ret.begin(), ret.end(), id) == ret.end()) - ret.push_back(id); - } - - return ret; -} - -void ChatList::SetSeparateAll(bool separate) { - m_separate_all = true; -} - -void ChatList::SetUsePinnedMenu() { - m_use_pinned_menu = true; -} - -void ChatList::ActuallyRemoveMessage(Snowflake id) { - auto it = m_id_to_widget.find(id); - if (it != m_id_to_widget.end()) - RemoveMessageAndHeader(it->second); -} - -void ChatList::OnScrollEdgeOvershot(Gtk::PositionType pos) { - if (pos == Gtk::POS_TOP) - m_signal_action_chat_load_history.emit(m_active_channel); -} - -void ChatList::ScrollToBottom() { - auto x = get_vadjustment(); - x->set_value(x->get_upper()); -} - -void ChatList::RemoveMessageAndHeader(Gtk::Widget *widget) { - auto *header = dynamic_cast(widget->get_ancestor(Gtk::ListBoxRow::get_type())); - if (header != nullptr) { - if (header->GetChildContent().size() == 1) { - m_num_rows--; - delete header; - } else { - delete widget; - } - } else { - delete widget; - } - m_num_messages--; -} - -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; -} - -ChatList::type_signal_action_channel_click ChatList::signal_action_channel_click() { - return m_signal_action_channel_click; -} - -ChatList::type_signal_action_insert_mention ChatList::signal_action_insert_mention() { - return m_signal_action_insert_mention; -} - -ChatList::type_signal_action_open_user_menu ChatList::signal_action_open_user_menu() { - return m_signal_action_open_user_menu; -} - -ChatList::type_signal_action_reaction_add ChatList::signal_action_reaction_add() { - return m_signal_action_reaction_add; -} - -ChatList::type_signal_action_reaction_remove ChatList::signal_action_reaction_remove() { - return m_signal_action_reaction_remove; -} - -ChatList::type_signal_action_reply_to ChatList::signal_action_reply_to() { - return m_signal_action_reply_to; -} diff --git a/components/chatlist.hpp b/components/chatlist.hpp deleted file mode 100644 index e5afb80..0000000 --- a/components/chatlist.hpp +++ /dev/null @@ -1,115 +0,0 @@ -#pragma once -#include -#include -#include -#include "discord/snowflake.hpp" - -class ChatList : public Gtk::ScrolledWindow { -public: - ChatList(); - void Clear(); - void SetActiveChannel(Snowflake id); - template - void SetMessages(Iter begin, Iter end); - template - void PrependMessages(Iter begin, Iter end); - void ProcessNewMessage(const Message &data, bool prepend); - void DeleteMessage(Snowflake id); - void RefetchMessage(Snowflake id); - Snowflake GetOldestListedMessage(); - void UpdateMessageReactions(Snowflake id); - void SetFailedByNonce(const std::string &nonce); - std::vector GetRecentAuthors(); - void SetSeparateAll(bool separate); - void SetUsePinnedMenu(); // i think i need a better way to do menus - void ActuallyRemoveMessage(Snowflake id); // perhaps not the best method name - -private: - void OnScrollEdgeOvershot(Gtk::PositionType pos); - void ScrollToBottom(); - void RemoveMessageAndHeader(Gtk::Widget *widget); - - bool m_use_pinned_menu = false; - - Gtk::Menu m_menu; - Gtk::MenuItem *m_menu_copy_id; - Gtk::MenuItem *m_menu_copy_content; - Gtk::MenuItem *m_menu_delete_message; - Gtk::MenuItem *m_menu_edit_message; - Gtk::MenuItem *m_menu_reply_to; - Gtk::MenuItem *m_menu_unpin; - Gtk::MenuItem *m_menu_pin; - Snowflake m_menu_selected_message; - - Snowflake m_active_channel; - - int m_num_messages = 0; - int m_num_rows = 0; - std::map m_id_to_widget; - - bool m_should_scroll_to_bottom = true; - Gtk::ListBox m_list; - - bool m_separate_all = false; - -public: - // these are all forwarded by the parent - using type_signal_action_message_edit = sigc::signal; - using type_signal_action_chat_submit = sigc::signal; - using type_signal_action_chat_load_history = sigc::signal; - using type_signal_action_channel_click = sigc::signal; - using type_signal_action_insert_mention = sigc::signal; - using type_signal_action_open_user_menu = sigc::signal; - using type_signal_action_reaction_add = sigc::signal; - using type_signal_action_reaction_remove = sigc::signal; - using type_signal_action_reply_to = sigc::signal; - - 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(); - type_signal_action_open_user_menu signal_action_open_user_menu(); - type_signal_action_reaction_add signal_action_reaction_add(); - type_signal_action_reaction_remove signal_action_reaction_remove(); - type_signal_action_reply_to signal_action_reply_to(); - -private: - type_signal_action_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; - type_signal_action_open_user_menu m_signal_action_open_user_menu; - type_signal_action_reaction_add m_signal_action_reaction_add; - type_signal_action_reaction_remove m_signal_action_reaction_remove; - type_signal_action_reply_to m_signal_action_reply_to; -}; - -template -inline void ChatList::SetMessages(Iter begin, Iter end) { - Clear(); - m_num_rows = 0; - m_num_messages = 0; - m_id_to_widget.clear(); - - for (Iter it = begin; it != end; it++) - ProcessNewMessage(*it, false); - - ScrollToBottom(); -} - -template -inline void ChatList::PrependMessages(Iter begin, Iter end) { - const auto old_upper = get_vadjustment()->get_upper(); - const auto old_value = get_vadjustment()->get_value(); - for (Iter it = begin; it != end; it++) - ProcessNewMessage(*it, true); - // force everything to process before getting new values - while (Gtk::Main::events_pending()) - Gtk::Main::iteration(); - const auto new_upper = get_vadjustment()->get_upper(); - if (old_value == 0.0 && (new_upper - old_upper) > 0.0) - get_vadjustment()->set_value(new_upper - old_upper); - // this isn't ideal -} diff --git a/components/chatmessage.cpp b/components/chatmessage.cpp deleted file mode 100644 index aa4fc2e..0000000 --- a/components/chatmessage.cpp +++ /dev/null @@ -1,1245 +0,0 @@ -#include "chatmessage.hpp" -#include "abaddon.hpp" -#include "util.hpp" -#include "lazyimage.hpp" -#include - -constexpr static int EmojiSize = 24; // settings eventually -constexpr static int AvatarSize = 32; -constexpr static int EmbedImageWidth = 400; -constexpr static int EmbedImageHeight = 300; -constexpr static int ThumbnailSize = 100; -constexpr static int StickerComponentSize = 160; - -ChatMessageItemContainer::ChatMessageItemContainer() - : m_main(Gtk::ORIENTATION_VERTICAL) { - add(m_main); - - m_link_menu_copy = Gtk::manage(new Gtk::MenuItem("Copy Link")); - m_link_menu_copy->signal_activate().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::on_link_menu_copy)); - m_link_menu.append(*m_link_menu_copy); - - m_link_menu.show_all(); -} - -ChatMessageItemContainer *ChatMessageItemContainer::FromMessage(const Message &data) { - auto *container = Gtk::manage(new ChatMessageItemContainer); - container->ID = data.ID; - container->ChannelID = data.ChannelID; - - if (data.Nonce.has_value()) - container->Nonce = *data.Nonce; - - if (data.Content.size() > 0 || data.Type != MessageType::DEFAULT) { - container->m_text_component = container->CreateTextComponent(data); - container->AttachEventHandlers(*container->m_text_component); - container->m_main.add(*container->m_text_component); - } - - if ((data.MessageReference.has_value() || data.Interaction.has_value()) && data.Type != MessageType::CHANNEL_FOLLOW_ADD) { - auto *widget = container->CreateReplyComponent(data); - if (widget != nullptr) { - container->m_main.add(*widget); - container->m_main.child_property_position(*widget) = 0; // eek - } - } - - // there should only ever be 1 embed (i think?) - if (data.Embeds.size() == 1) { - const auto &embed = data.Embeds[0]; - if (IsEmbedImageOnly(embed)) { - auto *widget = container->CreateImageComponent(*embed.Thumbnail->ProxyURL, *embed.Thumbnail->URL, *embed.Thumbnail->Width, *embed.Thumbnail->Height); - container->AttachEventHandlers(*widget); - container->m_main.add(*widget); - } else { - container->m_embed_component = container->CreateEmbedComponent(embed); - container->AttachEventHandlers(*container->m_embed_component); - container->m_main.add(*container->m_embed_component); - } - } - - // i dont think attachments can be edited - // also this can definitely be done much better holy shit - for (const auto &a : data.Attachments) { - if (IsURLViewableImage(a.ProxyURL) && a.Width.has_value() && a.Height.has_value()) { - auto *widget = container->CreateImageComponent(a.ProxyURL, a.URL, *a.Width, *a.Height); - container->m_main.add(*widget); - } else { - auto *widget = container->CreateAttachmentComponent(a); - container->m_main.add(*widget); - } - } - - // only 1? - /* - DEPRECATED - if (data.Stickers.has_value()) { - const auto &sticker = data.Stickers.value()[0]; - // todo: lottie, proper apng - if (sticker.FormatType == StickerFormatType::PNG || sticker.FormatType == StickerFormatType::APNG) { - auto *widget = container->CreateStickerComponent(sticker); - container->m_main->add(*widget); - } - }*/ - - if (data.StickerItems.has_value()) { - auto *widget = container->CreateStickersComponent(*data.StickerItems); - container->m_main.add(*widget); - } - - if (data.Reactions.has_value() && data.Reactions->size() > 0) { - container->m_reactions_component = container->CreateReactionsComponent(data); - container->m_main.add(*container->m_reactions_component); - } - - container->UpdateAttributes(); - - return container; -} - -// this doesnt rly make sense -void ChatMessageItemContainer::UpdateContent() { - const auto data = Abaddon::Get().GetDiscordClient().GetMessage(ID); - if (m_text_component != nullptr) - UpdateTextComponent(m_text_component); - - if (m_embed_component != nullptr) { - delete m_embed_component; - m_embed_component = nullptr; - } - - if (data->Embeds.size() == 1) { - m_embed_component = CreateEmbedComponent(data->Embeds[0]); - AttachEventHandlers(*m_embed_component); - m_main.add(*m_embed_component); - } -} - -void ChatMessageItemContainer::UpdateReactions() { - if (m_reactions_component != nullptr) { - delete m_reactions_component; - m_reactions_component = nullptr; - } - - const auto data = Abaddon::Get().GetDiscordClient().GetMessage(ID); - if (data->Reactions.has_value() && data->Reactions->size() > 0) { - m_reactions_component = CreateReactionsComponent(*data); - m_reactions_component->show_all(); - m_main.add(*m_reactions_component); - } -} - -void ChatMessageItemContainer::SetFailed() { - if (m_text_component != nullptr) { - m_text_component->get_style_context()->remove_class("pending"); - m_text_component->get_style_context()->add_class("failed"); - } -} - -void ChatMessageItemContainer::UpdateAttributes() { - const auto data = Abaddon::Get().GetDiscordClient().GetMessage(ID); - if (!data.has_value()) return; - - const bool deleted = data->IsDeleted(); - const bool edited = data->IsEdited(); - - if (!deleted && !edited) return; - - if (m_attrib_label == nullptr) { - m_attrib_label = Gtk::manage(new Gtk::Label); - m_attrib_label->set_halign(Gtk::ALIGN_START); - m_attrib_label->show(); - m_main.add(*m_attrib_label); // todo: maybe insert markup into existing text widget's buffer if the circumstances are right (or pack horizontally) - } - - if (deleted) - m_attrib_label->set_markup("[deleted]"); - else if (edited) - m_attrib_label->set_markup("[edited]"); -} - -void ChatMessageItemContainer::AddClickHandler(Gtk::Widget *widget, std::string url) { - // clang-format off - widget->signal_button_press_event().connect([url](GdkEventButton *event) -> bool { - if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) { - LaunchBrowser(url); - return false; - } - return true; - }, false); - // clang-format on -} - -Gtk::TextView *ChatMessageItemContainer::CreateTextComponent(const Message &data) { - auto *tv = Gtk::manage(new Gtk::TextView); - - if (data.IsPending) - tv->get_style_context()->add_class("pending"); - tv->get_style_context()->add_class("message-text"); - tv->set_can_focus(false); - tv->set_editable(false); - tv->set_wrap_mode(Gtk::WRAP_WORD_CHAR); - tv->set_halign(Gtk::ALIGN_FILL); - tv->set_hexpand(true); - - UpdateTextComponent(tv); - - return tv; -} - -void ChatMessageItemContainer::UpdateTextComponent(Gtk::TextView *tv) { - const auto data = Abaddon::Get().GetDiscordClient().GetMessage(ID); - if (!data.has_value()) return; - - auto b = tv->get_buffer(); - b->set_text(""); - Gtk::TextBuffer::iterator s, e; - b->get_bounds(s, e); - switch (data->Type) { - case MessageType::DEFAULT: - case MessageType::INLINE_REPLY: - b->insert(s, data->Content); - HandleUserMentions(b); - HandleLinks(*tv); - HandleChannelMentions(tv); - HandleEmojis(*tv); - break; - case MessageType::USER_PREMIUM_GUILD_SUBSCRIPTION: - b->insert_markup(s, "[boosted server]"); - break; - case MessageType::GUILD_MEMBER_JOIN: - b->insert_markup(s, "[user joined]"); - break; - case MessageType::CHANNEL_PINNED_MESSAGE: - b->insert_markup(s, "[message pinned]"); - break; - case MessageType::APPLICATION_COMMAND: { - if (data->Application.has_value()) { - static const auto regex = Glib::Regex::create(R"()"); - Glib::MatchInfo match; - if (regex->match(data->Content, match)) { - const auto cmd = match.fetch(1); - const auto app = data->Application->Name; - b->insert_markup(s, "used " + cmd + " with " + app + ""); - } - } else { - b->insert(s, data->Content); - HandleUserMentions(b); - HandleLinks(*tv); - HandleChannelMentions(tv); - HandleEmojis(*tv); - } - } break; - case MessageType::RECIPIENT_ADD: { - if (data->Mentions.size() == 0) break; - const auto &adder = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID); - const auto &added = data->Mentions[0]; - b->insert_markup(s, "" + adder->Username + " added " + added.Username + ""); - } break; - case MessageType::RECIPIENT_REMOVE: { - if (data->Mentions.size() == 0) break; - const auto &adder = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID); - const auto &added = data->Mentions[0]; - if (adder->ID == added.ID) - b->insert_markup(s, "" + adder->Username + " left"); - else - b->insert_markup(s, "" + adder->Username + " removed " + added.Username + ""); - } break; - case MessageType::CHANNEL_NAME_CHANGE: { - const auto author = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID); - b->insert_markup(s, "" + author->GetEscapedBoldName() + " changed the name to " + Glib::Markup::escape_text(data->Content) + ""); - } break; - case MessageType::CHANNEL_ICON_CHANGE: { - const auto author = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID); - b->insert_markup(s, "" + author->GetEscapedBoldName() + " changed the channel icon"); - } break; - case MessageType::USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1: - case MessageType::USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2: - case MessageType::USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3: { - const auto author = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID); - const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(*data->GuildID); - b->insert_markup(s, "" + author->GetEscapedBoldName() + " just boosted the server " + Glib::Markup::escape_text(data->Content) + " times! " + - Glib::Markup::escape_text(guild->Name) + " has achieved Level " + std::to_string(static_cast(data->Type) - 8) + "!"); // oo cheeky me !!! - } break; - case MessageType::CHANNEL_FOLLOW_ADD: { - const auto author = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID); - b->insert_markup(s, "" + author->GetEscapedBoldName() + " has added " + Glib::Markup::escape_text(data->Content) + " to this channel. Its most important updates will show up here."); - } break; - case MessageType::CALL: { - b->insert_markup(s, "[started a call]"); - } break; - case MessageType::GUILD_DISCOVERY_DISQUALIFIED: { - b->insert_markup(s, "This server has been removed from Server Discovery because it no longer passes all the requirements."); - } break; - case MessageType::GUILD_DISCOVERY_REQUALIFIED: { - b->insert_markup(s, "This server is eligible for Server Discovery again and has been automatically relisted!"); - } break; - case MessageType::GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING: { - b->insert_markup(s, "This server has failed Discovery activity requirements for 1 week. If this server fails for 4 weeks in a row, it will be automatically removed from Discovery."); - } break; - case MessageType::GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING: { - b->insert_markup(s, "This server has failed Discovery activity requirements for 3 weeks in a row. If this server fails for 1 more week, it will be removed from Discovery."); - } break; - case MessageType::THREAD_CREATED: { - const auto author = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID); - if (data->MessageReference.has_value() && data->MessageReference->ChannelID.has_value()) { - auto iter = b->insert_markup(s, "" + author->GetEscapedBoldName() + " started a thread: "); - auto tag = b->create_tag(); - tag->property_weight() = Pango::WEIGHT_BOLD; - m_channel_tagmap[tag] = *data->MessageReference->ChannelID; - b->insert_with_tag(iter, data->Content, tag); - - tv->signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::OnClickChannel), false); - } else { - b->insert_markup(s, "" + author->GetEscapedBoldName() + " started a thread: " + Glib::Markup::escape_text(data->Content) + ""); - } - } break; - default: break; - } -} - -Gtk::Widget *ChatMessageItemContainer::CreateEmbedComponent(const EmbedData &embed) { - Gtk::EventBox *ev = Gtk::manage(new Gtk::EventBox); - ev->set_can_focus(true); - Gtk::Box *main = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); - Gtk::Box *content = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); - - if (embed.Author.has_value() && (embed.Author->Name.has_value() || embed.Author->ProxyIconURL.has_value())) { - auto *author_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); - content->pack_start(*author_box); - - constexpr static int AuthorIconSize = 20; - if (embed.Author->ProxyIconURL.has_value()) { - auto *author_img = Gtk::manage(new LazyImage(*embed.Author->ProxyIconURL, AuthorIconSize, AuthorIconSize)); - author_img->set_halign(Gtk::ALIGN_START); - author_img->set_valign(Gtk::ALIGN_START); - author_img->set_margin_start(6); - author_img->set_margin_end(6); - author_img->get_style_context()->add_class("embed-author-icon"); - author_box->add(*author_img); - } - - if (embed.Author->Name.has_value()) { - auto *author_lbl = Gtk::manage(new Gtk::Label); - author_lbl->set_halign(Gtk::ALIGN_START); - author_lbl->set_valign(Gtk::ALIGN_CENTER); - author_lbl->set_line_wrap(true); - author_lbl->set_line_wrap_mode(Pango::WRAP_WORD_CHAR); - author_lbl->set_hexpand(false); - author_lbl->set_text(*embed.Author->Name); - author_lbl->get_style_context()->add_class("embed-author"); - author_box->add(*author_lbl); - } - } - - if (embed.Title.has_value()) { - auto *title_ev = Gtk::manage(new Gtk::EventBox); - auto *title_label = Gtk::manage(new Gtk::Label); - title_label->set_use_markup(true); - title_label->set_markup("" + Glib::Markup::escape_text(*embed.Title) + ""); - title_label->set_halign(Gtk::ALIGN_CENTER); - title_label->set_hexpand(false); - title_label->get_style_context()->add_class("embed-title"); - title_label->set_single_line_mode(false); - title_label->set_line_wrap(true); - title_label->set_line_wrap_mode(Pango::WRAP_WORD_CHAR); - title_label->set_max_width_chars(50); - title_ev->add(*title_label); - content->pack_start(*title_ev); - - if (embed.URL.has_value()) { - AddPointerCursor(*title_ev); - auto url = *embed.URL; - title_ev->signal_button_press_event().connect([this, url = std::move(url)](GdkEventButton *event) -> bool { - if (event->button == GDK_BUTTON_PRIMARY) { - LaunchBrowser(url); - return true; - } - return false; - }); - static auto color = Abaddon::Get().GetSettings().GetLinkColor(); - title_label->override_color(Gdk::RGBA(color)); - title_label->set_markup("" + Glib::Markup::escape_text(*embed.Title) + ""); - } - } - - if (!embed.Provider.has_value() || embed.Provider->Name != "YouTube") { // youtube link = no description - if (embed.Description.has_value()) { - auto *desc_label = Gtk::manage(new Gtk::Label); - desc_label->set_text(*embed.Description); - desc_label->set_line_wrap(true); - desc_label->set_line_wrap_mode(Pango::WRAP_WORD_CHAR); - desc_label->set_max_width_chars(50); - desc_label->set_halign(Gtk::ALIGN_START); - desc_label->set_hexpand(false); - desc_label->get_style_context()->add_class("embed-description"); - content->pack_start(*desc_label); - } - } - - // todo: handle inline fields - if (embed.Fields.has_value() && embed.Fields->size() > 0) { - auto *flow = Gtk::manage(new Gtk::FlowBox); - flow->set_orientation(Gtk::ORIENTATION_HORIZONTAL); - flow->set_min_children_per_line(3); - flow->set_max_children_per_line(3); - flow->set_halign(Gtk::ALIGN_START); - flow->set_hexpand(false); - flow->set_column_spacing(10); - flow->set_selection_mode(Gtk::SELECTION_NONE); - content->pack_start(*flow); - - for (const auto &field : *embed.Fields) { - auto *field_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); - auto *field_lbl = Gtk::manage(new Gtk::Label); - auto *field_val = Gtk::manage(new Gtk::Label); - field_box->set_hexpand(false); - field_box->set_halign(Gtk::ALIGN_START); - field_box->set_valign(Gtk::ALIGN_START); - field_lbl->set_hexpand(false); - field_lbl->set_halign(Gtk::ALIGN_START); - field_lbl->set_valign(Gtk::ALIGN_START); - field_val->set_hexpand(false); - field_val->set_halign(Gtk::ALIGN_START); - field_val->set_valign(Gtk::ALIGN_START); - field_lbl->set_use_markup(true); - field_lbl->set_markup("" + Glib::Markup::escape_text(field.Name) + ""); - field_lbl->set_max_width_chars(20); - field_lbl->set_line_wrap(true); - field_lbl->set_line_wrap_mode(Pango::WRAP_WORD_CHAR); - field_val->set_text(field.Value); - field_val->set_max_width_chars(20); - field_val->set_line_wrap(true); - field_val->set_line_wrap_mode(Pango::WRAP_WORD_CHAR); - field_box->pack_start(*field_lbl); - field_box->pack_start(*field_val); - field_lbl->get_style_context()->add_class("embed-field-title"); - field_val->get_style_context()->add_class("embed-field-value"); - flow->insert(*field_box, -1); - } - } - - if (embed.Image.has_value() && embed.Image->ProxyURL.has_value()) { - int w = 0, h = 0; - GetImageDimensions(*embed.Image->Width, *embed.Image->Height, w, h, EmbedImageWidth, EmbedImageHeight); - - auto *img = Gtk::manage(new LazyImage(*embed.Image->ProxyURL, w, h, false)); - img->set_halign(Gtk::ALIGN_CENTER); - img->set_margin_top(5); - img->set_size_request(w, h); - content->pack_start(*img); - } - - if (embed.Footer.has_value()) { - auto *footer_lbl = Gtk::manage(new Gtk::Label); - footer_lbl->set_halign(Gtk::ALIGN_START); - footer_lbl->set_line_wrap(true); - footer_lbl->set_line_wrap_mode(Pango::WRAP_WORD_CHAR); - footer_lbl->set_hexpand(false); - footer_lbl->set_text(embed.Footer->Text); - footer_lbl->get_style_context()->add_class("embed-footer"); - content->pack_start(*footer_lbl); - } - - if (embed.Thumbnail.has_value() && embed.Thumbnail->ProxyURL.has_value()) { - int w, h; - GetImageDimensions(*embed.Thumbnail->Width, *embed.Thumbnail->Height, w, h, ThumbnailSize, ThumbnailSize); - - auto *thumbnail = Gtk::manage(new LazyImage(*embed.Thumbnail->ProxyURL, w, h, false)); - thumbnail->set_size_request(w, h); - thumbnail->set_margin_start(8); - main->pack_end(*thumbnail); - } - - auto style = main->get_style_context(); - - if (embed.Color.has_value()) { - auto provider = Gtk::CssProvider::create(); // this seems wrong - std::string css = ".embed { border-left: 2px solid #" + IntToCSSColor(*embed.Color) + "; }"; - provider->load_from_data(css); - style->add_provider(provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); - } - - style->add_class("embed"); - - main->set_margin_bottom(8); - main->set_hexpand(false); - main->set_hexpand(false); - main->set_halign(Gtk::ALIGN_START); - main->set_halign(Gtk::ALIGN_START); - main->pack_start(*content); - - ev->add(*main); - ev->show_all(); - - return ev; -} - -Gtk::Widget *ChatMessageItemContainer::CreateImageComponent(const std::string &proxy_url, const std::string &url, int inw, int inh) { - int w, h; - GetImageDimensions(inw, inh, w, h); - - Gtk::EventBox *ev = Gtk::manage(new Gtk::EventBox); - Gtk::Image *widget = Gtk::manage(new LazyImage(proxy_url, w, h, false)); - ev->add(*widget); - widget->set_halign(Gtk::ALIGN_START); - widget->set_size_request(w, h); - - AttachEventHandlers(*ev); - AddClickHandler(ev, url); - - return ev; -} - -Gtk::Widget *ChatMessageItemContainer::CreateAttachmentComponent(const AttachmentData &data) { - auto *ev = Gtk::manage(new Gtk::EventBox); - auto *btn = Gtk::manage(new Gtk::Label(data.Filename + " " + HumanReadableBytes(data.Bytes))); // Gtk::LinkButton flat out doesn't work :D - ev->set_hexpand(false); - ev->set_halign(Gtk::ALIGN_START); - ev->get_style_context()->add_class("message-attachment-box"); - ev->add(*btn); - - AttachEventHandlers(*ev); - AddClickHandler(ev, data.URL); - - return ev; -} - -Gtk::Widget *ChatMessageItemContainer::CreateStickerComponentDeprecated(const StickerData &data) { - auto *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); - auto *imgw = Gtk::manage(new Gtk::Image); - box->add(*imgw); - auto &img = Abaddon::Get().GetImageManager(); - - if (data.FormatType == StickerFormatType::PNG || data.FormatType == StickerFormatType::APNG) { - auto cb = [this, imgw](const Glib::RefPtr &pixbuf) { - imgw->property_pixbuf() = pixbuf; - }; - img.LoadFromURL(data.GetURL(), sigc::track_obj(cb, *imgw)); - } - - AttachEventHandlers(*box); - return box; -} - -Gtk::Widget *ChatMessageItemContainer::CreateStickersComponent(const std::vector &data) { - auto *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); - - for (const auto &sticker : data) { - // no lottie - if (sticker.FormatType != StickerFormatType::PNG && sticker.FormatType != StickerFormatType::APNG) continue; - auto *ev = Gtk::manage(new Gtk::EventBox); - auto *img = Gtk::manage(new LazyImage(sticker.GetURL(), StickerComponentSize, StickerComponentSize, false)); - img->set_size_request(StickerComponentSize, StickerComponentSize); // should this go in LazyImage ? - img->show(); - ev->show(); - ev->add(*img); - box->add(*ev); - } - - box->show(); - - AttachEventHandlers(*box); - return box; -} - -Gtk::Widget *ChatMessageItemContainer::CreateReactionsComponent(const Message &data) { - auto *flow = Gtk::manage(new Gtk::FlowBox); - flow->set_orientation(Gtk::ORIENTATION_HORIZONTAL); - flow->set_min_children_per_line(5); - flow->set_max_children_per_line(20); - flow->set_halign(Gtk::ALIGN_START); - flow->set_hexpand(false); - flow->set_column_spacing(2); - flow->set_selection_mode(Gtk::SELECTION_NONE); - - auto &imgr = Abaddon::Get().GetImageManager(); - auto &emojis = Abaddon::Get().GetEmojis(); - const auto &placeholder = imgr.GetPlaceholder(16); - - for (const auto &reaction : *data.Reactions) { - auto *ev = Gtk::manage(new Gtk::EventBox); - auto *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); - box->get_style_context()->add_class("reaction-box"); - ev->add(*box); - flow->add(*ev); - - bool is_stock = !reaction.Emoji.ID.IsValid(); - - bool has_reacted = reaction.HasReactedWith; - if (has_reacted) - box->get_style_context()->add_class("reacted"); - - ev->signal_button_press_event().connect([this, has_reacted, is_stock, reaction](GdkEventButton *event) -> bool { - if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) { - Glib::ustring param; // escaped in client - if (is_stock) - param = reaction.Emoji.Name; - else - param = std::to_string(reaction.Emoji.ID); - if (has_reacted) - m_signal_action_reaction_remove.emit(param); - else - m_signal_action_reaction_add.emit(param); - return true; - } - return false; - }); - - ev->signal_realize().connect([ev]() { - auto window = ev->get_window(); - auto display = window->get_display(); - auto cursor = Gdk::Cursor::create(display, "pointer"); - window->set_cursor(cursor); - }); - - // image - if (is_stock) { // unicode/stock - const auto shortcode = emojis.GetShortCodeForPattern(reaction.Emoji.Name); - if (shortcode != "") - ev->set_tooltip_text(shortcode); - - const auto &pb = emojis.GetPixBuf(reaction.Emoji.Name); - Gtk::Image *img; - if (pb) - img = Gtk::manage(new Gtk::Image(pb->scale_simple(16, 16, Gdk::INTERP_BILINEAR))); - else - img = Gtk::manage(new Gtk::Image(placeholder)); - img->set_can_focus(false); - box->add(*img); - } else { // custom - ev->set_tooltip_text(reaction.Emoji.Name); - - auto img = Gtk::manage(new LazyImage(reaction.Emoji.GetURL(), 16, 16)); - img->set_can_focus(false); - box->add(*img); - } - - auto *lbl = Gtk::manage(new Gtk::Label(std::to_string(reaction.Count))); - lbl->set_margin_left(5); - lbl->get_style_context()->add_class("reaction-count"); - box->add(*lbl); - } - - return flow; -} - -Gtk::Widget *ChatMessageItemContainer::CreateReplyComponent(const Message &data) { - if (data.Type == MessageType::THREAD_CREATED) return nullptr; - - auto *box = Gtk::manage(new Gtk::Box); - auto *lbl = Gtk::manage(new Gtk::Label); - lbl->set_single_line_mode(true); - lbl->set_line_wrap(false); - lbl->set_use_markup(true); - lbl->set_ellipsize(Pango::ELLIPSIZE_END); - lbl->get_style_context()->add_class("message-text"); // good idea? - lbl->get_style_context()->add_class("message-reply"); - box->add(*lbl); - - const auto &discord = Abaddon::Get().GetDiscordClient(); - - const auto get_author_markup = [&](Snowflake author_id, Snowflake guild_id = Snowflake::Invalid) -> std::string { - if (guild_id.IsValid()) { - const auto role_id = discord.GetMemberHoistedRole(guild_id, author_id, true); - if (role_id.IsValid()) { - const auto role = discord.GetRole(role_id); - if (role.has_value()) { - const auto author = discord.GetUser(author_id); - return "Color) + "\">" + author->GetEscapedString() + ""; - } - } - } - - const auto author = discord.GetUser(author_id); - return author->GetEscapedBoldString(); - }; - - // if the message wasnt fetched from store it might have an un-fetched reference - std::optional> referenced_message = data.ReferencedMessage; - if (data.MessageReference.has_value() && data.MessageReference->MessageID.has_value() && !referenced_message.has_value()) { - auto refd = discord.GetMessage(*data.MessageReference->MessageID); - if (refd.has_value()) - referenced_message = std::make_shared(std::move(*refd)); - } - - if (data.Interaction.has_value()) { - const auto user = *discord.GetUser(data.Interaction->User.ID); - - if (data.GuildID.has_value()) { - lbl->set_markup(get_author_markup(user.ID, *data.GuildID) + - " used /" + - Glib::Markup::escape_text(data.Interaction->Name) + - ""); - } else { - lbl->set_markup(user.GetEscapedBoldString()); - } - } else if (referenced_message.has_value()) { - if (referenced_message.value() == nullptr) { - lbl->set_markup("deleted message"); - } else { - const auto &referenced = *referenced_message.value(); - Glib::ustring text; - if (referenced.Content.empty()) { - if (!referenced.Attachments.empty()) { - text = "attachment"; - } else if (!referenced.Embeds.empty()) { - text = "embed"; - } - } else { - auto buf = Gtk::TextBuffer::create(); - Gtk::TextBuffer::iterator start, end; - buf->get_bounds(start, end); - buf->set_text(referenced.Content); - CleanupEmojis(buf); - HandleUserMentions(buf); - HandleChannelMentions(buf); - text = Glib::Markup::escape_text(buf->get_text()); - } - // getting markup out of a textbuffer seems like something that to me should be really simple - // but actually is horribly annoying. replies won't have mention colors because you can't do this - // also no emojis because idk how to make a textview act like a label - // which of course would not be an issue if i could figure out how to get fonts to work on this god-forsaken framework - // oh well - // but ill manually get colors for the user who is being replied to - if (referenced.GuildID.has_value()) - lbl->set_markup(get_author_markup(referenced.Author.ID, *referenced.GuildID) + ": " + text); - else - lbl->set_markup(get_author_markup(referenced.Author.ID) + ": " + text); - } - } else { - lbl->set_markup("reply unavailable"); - } - - return box; -} - -Glib::ustring ChatMessageItemContainer::GetText(const Glib::RefPtr &buf) { - Gtk::TextBuffer::iterator a, b; - buf->get_bounds(a, b); - auto slice = buf->get_slice(a, b, true); - return slice; -} - -bool ChatMessageItemContainer::IsEmbedImageOnly(const EmbedData &data) { - if (!data.Thumbnail.has_value()) return false; - if (data.Author.has_value()) return false; - if (data.Description.has_value()) return false; - if (data.Fields.has_value()) return false; - if (data.Footer.has_value()) return false; - if (data.Image.has_value()) return false; - if (data.Timestamp.has_value()) return false; - return data.Thumbnail->ProxyURL.has_value() && data.Thumbnail->URL.has_value() && data.Thumbnail->Width.has_value() && data.Thumbnail->Height.has_value(); -} - -void ChatMessageItemContainer::HandleUserMentions(Glib::RefPtr buf) { - constexpr static const auto mentions_regex = R"(<@!?(\d+)>)"; - - static auto rgx = Glib::Regex::create(mentions_regex); - - Glib::ustring text = GetText(buf); - const auto &discord = Abaddon::Get().GetDiscordClient(); - - int startpos = 0; - Glib::MatchInfo match; - while (rgx->match(text, startpos, match)) { - int mstart, mend; - if (!match.fetch_pos(0, mstart, mend)) break; - const Glib::ustring user_id = match.fetch(1); - const auto user = discord.GetUser(user_id); - const auto channel = discord.GetChannel(ChannelID); - if (!user.has_value() || !channel.has_value()) { - startpos = mend; - continue; - } - - Glib::ustring replacement; - - if (channel->Type == ChannelType::DM || channel->Type == ChannelType::GROUP_DM) - replacement = user->GetEscapedBoldString(); - else { - const auto role_id = user->GetHoistedRole(*channel->GuildID, true); - const auto role = discord.GetRole(role_id); - if (!role.has_value()) - replacement = user->GetEscapedBoldString(); - else - replacement = "Color) + "\">" + user->GetEscapedBoldString() + ""; - } - - // regex returns byte positions and theres no straightforward way in the c++ bindings to deal with that :( - const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart); - const auto chars_end = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mend); - const auto start_it = buf->get_iter_at_offset(chars_start); - const auto end_it = buf->get_iter_at_offset(chars_end); - - auto it = buf->erase(start_it, end_it); - buf->insert_markup(it, replacement); - - text = GetText(buf); - startpos = 0; - } -} - -void ChatMessageItemContainer::HandleStockEmojis(Gtk::TextView &tv) { - Abaddon::Get().GetEmojis().ReplaceEmojis(tv.get_buffer(), EmojiSize); -} - -void ChatMessageItemContainer::HandleCustomEmojis(Gtk::TextView &tv) { - static auto rgx = Glib::Regex::create(R"()"); - - auto &img = Abaddon::Get().GetImageManager(); - - auto buf = tv.get_buffer(); - auto text = GetText(buf); - - Glib::MatchInfo match; - int startpos = 0; - while (rgx->match(text, startpos, match)) { - int mstart, mend; - if (!match.fetch_pos(0, mstart, mend)) break; - const bool is_animated = match.fetch(0)[1] == 'a'; - const bool show_animations = Abaddon::Get().GetSettings().GetShowAnimations(); - - const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart); - const auto chars_end = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mend); - auto start_it = buf->get_iter_at_offset(chars_start); - auto end_it = buf->get_iter_at_offset(chars_end); - - startpos = mend; - if (is_animated && show_animations) { - const auto mark_start = buf->create_mark(start_it, false); - end_it.backward_char(); - const auto mark_end = buf->create_mark(end_it, false); - const auto cb = [this, &tv, buf, mark_start, mark_end](const Glib::RefPtr &pixbuf) { - auto start_it = mark_start->get_iter(); - auto end_it = mark_end->get_iter(); - end_it.forward_char(); - buf->delete_mark(mark_start); - buf->delete_mark(mark_end); - auto it = buf->erase(start_it, end_it); - const auto anchor = buf->create_child_anchor(it); - auto img = Gtk::manage(new Gtk::Image(pixbuf)); - img->show(); - tv.add_child_at_anchor(*img, anchor); - }; - img.LoadAnimationFromURL(EmojiData::URLFromID(match.fetch(2), "gif"), EmojiSize, EmojiSize, sigc::track_obj(cb, tv)); - } else { - // can't erase before pixbuf is ready or else marks that are in the same pos get mixed up - const auto mark_start = buf->create_mark(start_it, false); - end_it.backward_char(); - const auto mark_end = buf->create_mark(end_it, false); - const auto cb = [this, buf, mark_start, mark_end](const Glib::RefPtr &pixbuf) { - auto start_it = mark_start->get_iter(); - auto end_it = mark_end->get_iter(); - end_it.forward_char(); - buf->delete_mark(mark_start); - buf->delete_mark(mark_end); - auto it = buf->erase(start_it, end_it); - buf->insert_pixbuf(it, pixbuf->scale_simple(EmojiSize, EmojiSize, Gdk::INTERP_BILINEAR)); - }; - img.LoadFromURL(EmojiData::URLFromID(match.fetch(2)), sigc::track_obj(cb, tv)); - } - - text = GetText(buf); - } -} - -void ChatMessageItemContainer::HandleEmojis(Gtk::TextView &tv) { - static const bool stock_emojis = Abaddon::Get().GetSettings().GetShowStockEmojis(); - static const bool custom_emojis = Abaddon::Get().GetSettings().GetShowCustomEmojis(); - - if (stock_emojis) HandleStockEmojis(tv); - if (custom_emojis) HandleCustomEmojis(tv); -} - -void ChatMessageItemContainer::CleanupEmojis(Glib::RefPtr buf) { - static auto rgx = Glib::Regex::create(R"()"); - - auto text = GetText(buf); - - Glib::MatchInfo match; - int startpos = 0; - while (rgx->match(text, startpos, match)) { - int mstart, mend; - if (!match.fetch_pos(0, mstart, mend)) break; - - const auto new_term = ":" + match.fetch(1) + ":"; - - const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart); - const auto chars_end = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mend); - auto start_it = buf->get_iter_at_offset(chars_start); - auto end_it = buf->get_iter_at_offset(chars_end); - - startpos = mend; - const auto it = buf->erase(start_it, end_it); - const int alen = text.size(); - text = GetText(buf); - const int blen = text.size(); - startpos -= (alen - blen); - - buf->insert(it, new_term); - - text = GetText(buf); - } -} - -void ChatMessageItemContainer::HandleChannelMentions(Glib::RefPtr buf) { - static auto rgx = Glib::Regex::create(R"(<#(\d+)>)"); - - Glib::ustring text = GetText(buf); - - const auto &discord = Abaddon::Get().GetDiscordClient(); - - int startpos = 0; - Glib::MatchInfo match; - while (rgx->match(text, startpos, match)) { - int mstart, mend; - match.fetch_pos(0, mstart, mend); - std::string channel_id = match.fetch(1); - const auto chan = discord.GetChannel(channel_id); - if (!chan.has_value()) { - startpos = mend; - continue; - } - - auto tag = buf->create_tag(); - if (chan->Type == ChannelType::GUILD_TEXT) { - m_channel_tagmap[tag] = channel_id; - tag->property_weight() = Pango::WEIGHT_BOLD; - } - - const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart); - const auto chars_end = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mend); - const auto erase_from = buf->get_iter_at_offset(chars_start); - const auto erase_to = buf->get_iter_at_offset(chars_end); - auto it = buf->erase(erase_from, erase_to); - const std::string replacement = "#" + *chan->Name; - it = buf->insert_with_tag(it, "#" + *chan->Name, tag); - - // rescan the whole thing so i dont have to deal with fixing match positions - text = GetText(buf); - startpos = 0; - } -} - -void ChatMessageItemContainer::HandleChannelMentions(Gtk::TextView *tv) { - tv->signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::OnClickChannel), false); - HandleChannelMentions(tv->get_buffer()); -} - -// a lot of repetition here so there should probably just be one slot for textview's button-press -bool ChatMessageItemContainer::OnClickChannel(GdkEventButton *ev) { - if (m_text_component == nullptr) return false; - if (ev->type != GDK_BUTTON_PRESS) return false; - if (ev->button != GDK_BUTTON_PRIMARY) return false; - - auto buf = m_text_component->get_buffer(); - Gtk::TextBuffer::iterator start, end; - buf->get_selection_bounds(start, end); // no open if selection - if (start.get_offset() != end.get_offset()) - return false; - - int x, y; - m_text_component->window_to_buffer_coords(Gtk::TEXT_WINDOW_WIDGET, ev->x, ev->y, x, y); - Gtk::TextBuffer::iterator iter; - m_text_component->get_iter_at_location(iter, x, y); - - const auto tags = iter.get_tags(); - for (auto tag : tags) { - const auto it = m_channel_tagmap.find(tag); - if (it != m_channel_tagmap.end()) { - m_signal_action_channel_click.emit(it->second); - - return true; - } - } - - return false; -} - -void ChatMessageItemContainer::on_link_menu_copy() { - Gtk::Clipboard::get()->set_text(m_selected_link); -} - -void ChatMessageItemContainer::HandleLinks(Gtk::TextView &tv) { - const auto rgx = Glib::Regex::create(R"(\bhttps?:\/\/[^\s]+\.[^\s]+\b)"); - - tv.signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::OnLinkClick), false); - - auto buf = tv.get_buffer(); - Glib::ustring text = GetText(buf); - - // i'd like to let this be done thru css like .message-link { color: #bitch; } but idk how - static auto link_color = Abaddon::Get().GetSettings().GetLinkColor(); - - int startpos = 0; - Glib::MatchInfo match; - while (rgx->match(text, startpos, match)) { - int mstart, mend; - match.fetch_pos(0, mstart, mend); - std::string link = match.fetch(0); - auto tag = buf->create_tag(); - m_link_tagmap[tag] = link; - tag->property_foreground_rgba() = Gdk::RGBA(link_color); - tag->set_property("underline", 1); // stupid workaround for vcpkg bug (i think) - - const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart); - const auto chars_end = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mend); - const auto erase_from = buf->get_iter_at_offset(chars_start); - const auto erase_to = buf->get_iter_at_offset(chars_end); - auto it = buf->erase(erase_from, erase_to); - it = buf->insert_with_tag(it, link, tag); - - startpos = mend; - } -} - -bool ChatMessageItemContainer::OnLinkClick(GdkEventButton *ev) { - if (m_text_component == nullptr) return false; - if (ev->type != GDK_BUTTON_PRESS) return false; - if (ev->button != GDK_BUTTON_PRIMARY && ev->button != GDK_BUTTON_SECONDARY) return false; - - auto buf = m_text_component->get_buffer(); - Gtk::TextBuffer::iterator start, end; - buf->get_selection_bounds(start, end); // no open if selection - if (start.get_offset() != end.get_offset()) - return false; - - int x, y; - m_text_component->window_to_buffer_coords(Gtk::TEXT_WINDOW_WIDGET, ev->x, ev->y, x, y); - Gtk::TextBuffer::iterator iter; - m_text_component->get_iter_at_location(iter, x, y); - - const auto tags = iter.get_tags(); - for (auto tag : tags) { - const auto it = m_link_tagmap.find(tag); - if (it != m_link_tagmap.end()) { - if (ev->button == GDK_BUTTON_PRIMARY) { - LaunchBrowser(it->second); - return true; - } else if (ev->button == GDK_BUTTON_SECONDARY) { - m_selected_link = it->second; - m_link_menu.popup_at_pointer(reinterpret_cast(ev)); - return true; - } - } - } - - return false; -} - -ChatMessageItemContainer::type_signal_channel_click ChatMessageItemContainer::signal_action_channel_click() { - return m_signal_action_channel_click; -} - -ChatMessageItemContainer::type_signal_action_reaction_add ChatMessageItemContainer::signal_action_reaction_add() { - return m_signal_action_reaction_add; -} - -ChatMessageItemContainer::type_signal_action_reaction_remove ChatMessageItemContainer::signal_action_reaction_remove() { - return m_signal_action_reaction_remove; -} - -void ChatMessageItemContainer::AttachEventHandlers(Gtk::Widget &widget) { - const auto on_button_press_event = [this](GdkEventButton *e) -> bool { - if (e->type == GDK_BUTTON_PRESS && e->button == GDK_BUTTON_SECONDARY) { - event(reinterpret_cast(e)); // illegal ooooooh - return true; - } - - return false; - }; - widget.signal_button_press_event().connect(on_button_press_event, false); -} - -ChatMessageHeader::ChatMessageHeader(const Message &data) - : m_main_box(Gtk::ORIENTATION_HORIZONTAL) - , m_content_box(Gtk::ORIENTATION_VERTICAL) - , m_meta_box(Gtk::ORIENTATION_HORIZONTAL) - , m_avatar(Abaddon::Get().GetImageManager().GetPlaceholder(AvatarSize)) { - UserID = data.Author.ID; - ChannelID = data.ChannelID; - - const auto author = Abaddon::Get().GetDiscordClient().GetUser(UserID); - auto &img = Abaddon::Get().GetImageManager(); - - auto cb = [this](const Glib::RefPtr &pb) { - m_static_avatar = pb->scale_simple(AvatarSize, AvatarSize, Gdk::INTERP_BILINEAR); - m_avatar.property_pixbuf() = m_static_avatar; - }; - img.LoadFromURL(author->GetAvatarURL(data.GuildID), sigc::track_obj(cb, *this)); - - if (author->HasAnimatedAvatar()) { - auto cb = [this](const Glib::RefPtr &pb) { - m_anim_avatar = pb; - }; - img.LoadAnimationFromURL(author->GetAvatarURL("gif"), AvatarSize, AvatarSize, sigc::track_obj(cb, *this)); - } - - get_style_context()->add_class("message-container"); - m_author.get_style_context()->add_class("message-container-author"); - m_timestamp.get_style_context()->add_class("message-container-timestamp"); - m_avatar.get_style_context()->add_class("message-container-avatar"); - - m_avatar.set_valign(Gtk::ALIGN_START); - m_avatar.set_margin_right(10); - - m_author.set_markup(data.Author.GetEscapedBoldName()); - m_author.set_single_line_mode(true); - m_author.set_line_wrap(false); - m_author.set_ellipsize(Pango::ELLIPSIZE_END); - m_author.set_xalign(0.f); - m_author.set_can_focus(false); - - m_meta_ev.signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageHeader::on_author_button_press)); - - if (author->IsBot || 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); - m_extra->set_margin_start(12); - m_extra->set_can_focus(false); - m_extra->set_use_markup(true); - } - if (author->IsBot) - m_extra->set_markup("BOT"); - else if (data.WebhookID.has_value()) - m_extra->set_markup("Webhook"); - - m_timestamp.set_text(data.ID.GetLocalTimestamp()); - m_timestamp.set_hexpand(true); - m_timestamp.set_halign(Gtk::ALIGN_END); - m_timestamp.set_ellipsize(Pango::ELLIPSIZE_END); - m_timestamp.set_opacity(0.5); - m_timestamp.set_single_line_mode(true); - m_timestamp.set_margin_start(12); - m_timestamp.set_can_focus(false); - - m_main_box.set_hexpand(true); - m_main_box.set_vexpand(true); - m_main_box.set_can_focus(true); - - m_meta_box.set_hexpand(true); - m_meta_box.set_can_focus(false); - - m_content_box.set_can_focus(false); - - const auto on_enter_cb = [this](const GdkEventCrossing *event) -> bool { - if (m_anim_avatar) - m_avatar.property_pixbuf_animation() = m_anim_avatar; - return false; - }; - const auto on_leave_cb = [this](const GdkEventCrossing *event) -> bool { - if (m_anim_avatar) - m_avatar.property_pixbuf() = m_static_avatar; - return false; - }; - - m_content_box_ev.add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK); - m_meta_ev.add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK); - m_avatar_ev.add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK); - if (Abaddon::Get().GetSettings().GetShowAnimations()) { - m_content_box_ev.signal_enter_notify_event().connect(on_enter_cb); - m_content_box_ev.signal_leave_notify_event().connect(on_leave_cb); - m_meta_ev.signal_enter_notify_event().connect(on_enter_cb); - m_meta_ev.signal_leave_notify_event().connect(on_leave_cb); - m_avatar_ev.signal_enter_notify_event().connect(on_enter_cb); - m_avatar_ev.signal_leave_notify_event().connect(on_leave_cb); - } - - m_meta_box.add(m_author); - if (m_extra != nullptr) - m_meta_box.add(*m_extra); - - m_meta_box.add(m_timestamp); - m_meta_ev.add(m_meta_box); - m_content_box.add(m_meta_ev); - m_avatar_ev.add(m_avatar); - m_main_box.add(m_avatar_ev); - m_content_box_ev.add(m_content_box); - m_main_box.add(m_content_box_ev); - add(m_main_box); - - set_margin_bottom(8); - - show_all(); - - auto &discord = Abaddon::Get().GetDiscordClient(); - auto role_update_cb = [this](...) { UpdateNameColor(); }; - discord.signal_role_update().connect(sigc::track_obj(role_update_cb, *this)); - auto guild_member_update_cb = [this](const auto &, const auto &) { UpdateNameColor(); }; - discord.signal_guild_member_update().connect(sigc::track_obj(guild_member_update_cb, *this)); - UpdateNameColor(); - AttachUserMenuHandler(m_meta_ev); - AttachUserMenuHandler(m_avatar_ev); -} - -void ChatMessageHeader::UpdateNameColor() { - const auto &discord = Abaddon::Get().GetDiscordClient(); - const auto user = discord.GetUser(UserID); - if (!user.has_value()) return; - const auto chan = discord.GetChannel(ChannelID); - bool is_guild = chan.has_value() && chan->GuildID.has_value(); - if (is_guild) { - const auto role_id = discord.GetMemberHoistedRole(*chan->GuildID, UserID, true); - const auto role = discord.GetRole(role_id); - - std::string md; - if (role.has_value()) - m_author.set_markup("" + user->GetEscapedName() + ""); - else - m_author.set_markup("" + user->GetEscapedName() + ""); - } else - m_author.set_markup("" + user->GetEscapedName() + ""); -} - -std::vector ChatMessageHeader::GetChildContent() { - return m_content_widgets; -} - -void ChatMessageHeader::AttachUserMenuHandler(Gtk::Widget &widget) { - widget.signal_button_press_event().connect([this](GdkEventButton *ev) -> bool { - if (ev->type == GDK_BUTTON_PRESS && ev->button == GDK_BUTTON_SECONDARY) { - auto info = Abaddon::Get().GetDiscordClient().GetChannel(ChannelID); - Snowflake guild_id; - if (info.has_value() && info->GuildID.has_value()) - guild_id = *info->GuildID; - Abaddon::Get().ShowUserMenu(reinterpret_cast(ev), UserID, guild_id); - return true; - } - - return false; - }); -} - -bool ChatMessageHeader::on_author_button_press(GdkEventButton *ev) { - if (ev->button == GDK_BUTTON_PRIMARY && (ev->state & GDK_SHIFT_MASK)) { - m_signal_action_insert_mention.emit(); - return true; - } - - return false; -} - -ChatMessageHeader::type_signal_action_insert_mention ChatMessageHeader::signal_action_insert_mention() { - return m_signal_action_insert_mention; -} - -ChatMessageHeader::type_signal_action_open_user_menu ChatMessageHeader::signal_action_open_user_menu() { - return m_signal_action_open_user_menu; -} - -void ChatMessageHeader::AddContent(Gtk::Widget *widget, bool prepend) { - m_content_widgets.push_back(widget); - const auto cb = [this, widget]() { - m_content_widgets.erase(std::remove(m_content_widgets.begin(), m_content_widgets.end(), widget), m_content_widgets.end()); - }; - widget->signal_unmap().connect(sigc::track_obj(cb, *this, *widget), false); - m_content_box.add(*widget); - if (prepend) - m_content_box.reorder_child(*widget, 1); - if (auto *x = dynamic_cast(widget)) { - if (x->ID > NewestID) - NewestID = x->ID; - } -} diff --git a/components/chatmessage.hpp b/components/chatmessage.hpp deleted file mode 100644 index 8b69117..0000000 --- a/components/chatmessage.hpp +++ /dev/null @@ -1,125 +0,0 @@ -#pragma once -#include -#include "discord/discord.hpp" - -class ChatMessageItemContainer : public Gtk::Box { -public: - Snowflake ID; - Snowflake ChannelID; - - std::string Nonce; - - ChatMessageItemContainer(); - static ChatMessageItemContainer *FromMessage(const Message &data); - - // attributes = edited, deleted - void UpdateAttributes(); - void UpdateContent(); - void UpdateReactions(); - void SetFailed(); - -protected: - void AddClickHandler(Gtk::Widget *widget, std::string); - Gtk::TextView *CreateTextComponent(const Message &data); // Message.Content - void UpdateTextComponent(Gtk::TextView *tv); - Gtk::Widget *CreateEmbedComponent(const EmbedData &data); // Message.Embeds[0] - Gtk::Widget *CreateImageComponent(const std::string &proxy_url, const std::string &url, int inw, int inh); - Gtk::Widget *CreateAttachmentComponent(const AttachmentData &data); // non-image attachments - Gtk::Widget *CreateStickerComponentDeprecated(const StickerData &data); - Gtk::Widget *CreateStickersComponent(const std::vector &data); - Gtk::Widget *CreateReactionsComponent(const Message &data); - Gtk::Widget *CreateReplyComponent(const Message &data); - - static Glib::ustring GetText(const Glib::RefPtr &buf); - - static bool IsEmbedImageOnly(const EmbedData &data); - - void HandleUserMentions(Glib::RefPtr buf); - void HandleStockEmojis(Gtk::TextView &tv); - void HandleCustomEmojis(Gtk::TextView &tv); - void HandleEmojis(Gtk::TextView &tv); - void CleanupEmojis(Glib::RefPtr buf); - - void HandleChannelMentions(Glib::RefPtr buf); - void HandleChannelMentions(Gtk::TextView *tv); - bool OnClickChannel(GdkEventButton *ev); - - // reused for images and links - Gtk::Menu m_link_menu; - Gtk::MenuItem *m_link_menu_copy; - - void on_link_menu_copy(); - Glib::ustring m_selected_link; - - void HandleLinks(Gtk::TextView &tv); - bool OnLinkClick(GdkEventButton *ev); - std::map, std::string> m_link_tagmap; - std::map, Snowflake> m_channel_tagmap; - - void AttachEventHandlers(Gtk::Widget &widget); - - Gtk::EventBox *_ev; - Gtk::Box m_main; - Gtk::Label *m_attrib_label = nullptr; - - Gtk::TextView *m_text_component = nullptr; - Gtk::Widget *m_embed_component = nullptr; - Gtk::Widget *m_reactions_component = nullptr; - -public: - typedef sigc::signal type_signal_channel_click; - typedef sigc::signal type_signal_action_reaction_add; - typedef sigc::signal type_signal_action_reaction_remove; - - type_signal_channel_click signal_action_channel_click(); - type_signal_action_reaction_add signal_action_reaction_add(); - type_signal_action_reaction_remove signal_action_reaction_remove(); - -private: - type_signal_channel_click m_signal_action_channel_click; - type_signal_action_reaction_add m_signal_action_reaction_add; - type_signal_action_reaction_remove m_signal_action_reaction_remove; -}; - -class ChatMessageHeader : public Gtk::ListBoxRow { -public: - Snowflake UserID; - Snowflake ChannelID; - Snowflake NewestID = 0; - - ChatMessageHeader(const Message &data); - void AddContent(Gtk::Widget *widget, bool prepend); - void UpdateNameColor(); - std::vector GetChildContent(); - -protected: - void AttachUserMenuHandler(Gtk::Widget &widget); - - bool on_author_button_press(GdkEventButton *ev); - - std::vector m_content_widgets; - - Gtk::Box m_main_box; - Gtk::Box m_content_box; - Gtk::EventBox m_content_box_ev; - Gtk::Box m_meta_box; - Gtk::EventBox m_meta_ev; - Gtk::Label m_author; - Gtk::Label m_timestamp; - Gtk::Label *m_extra = nullptr; - Gtk::Image m_avatar; - Gtk::EventBox m_avatar_ev; - - Glib::RefPtr m_static_avatar; - Glib::RefPtr m_anim_avatar; - - typedef sigc::signal type_signal_action_insert_mention; - typedef sigc::signal type_signal_action_open_user_menu; - - type_signal_action_insert_mention m_signal_action_insert_mention; - type_signal_action_open_user_menu m_signal_action_open_user_menu; - -public: - type_signal_action_insert_mention signal_action_insert_mention(); - type_signal_action_open_user_menu signal_action_open_user_menu(); -}; diff --git a/components/chatwindow.cpp b/components/chatwindow.cpp deleted file mode 100644 index 9b34dfd..0000000 --- a/components/chatwindow.cpp +++ /dev/null @@ -1,239 +0,0 @@ -#include "chatwindow.hpp" -#include "chatmessage.hpp" -#include "abaddon.hpp" -#include "chatinputindicator.hpp" -#include "ratelimitindicator.hpp" -#include "chatinput.hpp" -#include "chatlist.hpp" - -ChatWindow::ChatWindow() { - Abaddon::Get().GetDiscordClient().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); - m_input = Gtk::manage(new ChatInput); - m_input_indicator = Gtk::manage(new ChatInputIndicator); - m_rate_limit_indicator = Gtk::manage(new RateLimitIndicator); - m_meta = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); - - m_rate_limit_indicator->set_margin_end(5); - m_rate_limit_indicator->set_hexpand(true); - m_rate_limit_indicator->set_halign(Gtk::ALIGN_END); - m_rate_limit_indicator->set_valign(Gtk::ALIGN_END); - m_rate_limit_indicator->show(); - - m_input_indicator->set_halign(Gtk::ALIGN_START); - m_input_indicator->set_valign(Gtk::ALIGN_END); - m_input_indicator->show(); - - m_main->get_style_context()->add_class("messages"); - - m_main->set_hexpand(true); - m_main->set_vexpand(true); - - m_topic.get_style_context()->add_class("channel-topic"); - m_topic.add(m_topic_text); - m_topic_text.set_halign(Gtk::ALIGN_START); - m_topic_text.show(); - - m_input->signal_submit().connect(sigc::mem_fun(*this, &ChatWindow::OnInputSubmit)); - m_input->signal_escape().connect([this]() { - if (m_is_replying) - StopReplying(); - }); - m_input->signal_key_press_event().connect(sigc::mem_fun(*this, &ChatWindow::OnKeyPressEvent), false); - m_input->show(); - - m_completer.SetBuffer(m_input->GetBuffer()); - m_completer.SetGetChannelID([this]() -> auto { - return m_active_channel; - }); - - m_completer.SetGetRecentAuthors([this]() -> auto { - return m_chat->GetRecentAuthors(); - }); - - m_completer.show(); - - m_chat->signal_action_channel_click().connect([this](Snowflake id) { - m_signal_action_channel_click.emit(id); - }); - 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); - }); - m_chat->signal_action_message_edit().connect([this](Snowflake channel_id, Snowflake message_id) { - m_signal_action_message_edit.emit(channel_id, message_id); - }); - m_chat->signal_action_reaction_add().connect([this](Snowflake id, const Glib::ustring ¶m) { - m_signal_action_reaction_add.emit(id, param); - }); - m_chat->signal_action_reaction_remove().connect([this](Snowflake id, const Glib::ustring ¶m) { - m_signal_action_reaction_remove.emit(id, param); - }); - m_chat->signal_action_reply_to().connect([this](Snowflake id) { - StartReplying(id); - }); - m_chat->show(); - - m_meta->set_hexpand(true); - m_meta->set_halign(Gtk::ALIGN_FILL); - m_meta->show(); - - m_meta->add(*m_input_indicator); - m_meta->add(*m_rate_limit_indicator); - //m_scroll->add(*m_list); - m_main->add(m_topic); - m_main->add(*m_chat); - m_main->add(m_completer); - m_main->add(*m_input); - m_main->add(*m_meta); - m_main->show(); -} - -Gtk::Widget *ChatWindow::GetRoot() const { - return m_main; -} - -void ChatWindow::Clear() { - m_chat->Clear(); -} - -void ChatWindow::SetMessages(const std::vector &msgs) { - m_chat->SetMessages(msgs.begin(), msgs.end()); -} - -void ChatWindow::SetActiveChannel(Snowflake id) { - m_active_channel = id; - m_chat->SetActiveChannel(id); - m_input_indicator->SetActiveChannel(id); - m_rate_limit_indicator->SetActiveChannel(id); - if (m_is_replying) - StopReplying(); -} - -void ChatWindow::AddNewMessage(const Message &data) { - m_chat->ProcessNewMessage(data, false); -} - -void ChatWindow::DeleteMessage(Snowflake id) { - m_chat->DeleteMessage(id); -} - -void ChatWindow::UpdateMessage(Snowflake id) { - m_chat->RefetchMessage(id); -} - -void ChatWindow::AddNewHistory(const std::vector &msgs) { - m_chat->PrependMessages(msgs.crbegin(), msgs.crend()); -} - -void ChatWindow::InsertChatInput(std::string text) { - m_input->InsertText(text); -} - -Snowflake ChatWindow::GetOldestListedMessage() { - return m_chat->GetOldestListedMessage(); -} - -void ChatWindow::UpdateReactions(Snowflake id) { - m_chat->UpdateMessageReactions(id); -} - -void ChatWindow::SetTopic(const std::string &text) { - m_topic_text.set_text(text); - m_topic.set_visible(text.length() > 0); -} - -Snowflake ChatWindow::GetActiveChannel() const { - return m_active_channel; -} - -bool ChatWindow::OnInputSubmit(const Glib::ustring &text) { - if (!m_rate_limit_indicator->CanSpeak()) - return false; - - if (text.size() == 0) - return false; - - if (m_active_channel.IsValid()) - m_signal_action_chat_submit.emit(text, m_active_channel, m_replying_to); // m_replying_to is checked for invalid in the handler - if (m_is_replying) - StopReplying(); - - return true; -} - -bool ChatWindow::OnKeyPressEvent(GdkEventKey *e) { - if (m_completer.ProcessKeyPress(e)) - return true; - - if (m_input->ProcessKeyPress(e)) - return true; - - return false; -} - -void ChatWindow::StartReplying(Snowflake message_id) { - const auto &discord = Abaddon::Get().GetDiscordClient(); - const auto message = *discord.GetMessage(message_id); - const auto author = discord.GetUser(message.Author.ID); - m_replying_to = message_id; - m_is_replying = true; - m_input->grab_focus(); - m_input->get_style_context()->add_class("replying"); - if (author.has_value()) - m_input_indicator->SetCustomMarkup("Replying to " + author->GetEscapedBoldString()); - else - m_input_indicator->SetCustomMarkup("Replying..."); -} - -void ChatWindow::StopReplying() { - m_is_replying = false; - m_replying_to = Snowflake::Invalid; - m_input->get_style_context()->remove_class("replying"); - m_input_indicator->ClearCustom(); -} - -void ChatWindow::OnScrollEdgeOvershot(Gtk::PositionType pos) { - if (pos == Gtk::POS_TOP) - m_signal_action_chat_load_history.emit(m_active_channel); -} - -void ChatWindow::OnMessageSendFail(const std::string &nonce, float retry_after) { - m_chat->SetFailedByNonce(nonce); -} - -ChatWindow::type_signal_action_message_edit ChatWindow::signal_action_message_edit() { - return m_signal_action_message_edit; -} - -ChatWindow::type_signal_action_chat_submit ChatWindow::signal_action_chat_submit() { - return m_signal_action_chat_submit; -} - -ChatWindow::type_signal_action_chat_load_history ChatWindow::signal_action_chat_load_history() { - return m_signal_action_chat_load_history; -} - -ChatWindow::type_signal_action_channel_click ChatWindow::signal_action_channel_click() { - return m_signal_action_channel_click; -} - -ChatWindow::type_signal_action_insert_mention ChatWindow::signal_action_insert_mention() { - return m_signal_action_insert_mention; -} - -ChatWindow::type_signal_action_reaction_add ChatWindow::signal_action_reaction_add() { - return m_signal_action_reaction_add; -} - -ChatWindow::type_signal_action_reaction_remove ChatWindow::signal_action_reaction_remove() { - return m_signal_action_reaction_remove; -} diff --git a/components/chatwindow.hpp b/components/chatwindow.hpp deleted file mode 100644 index de55b0a..0000000 --- a/components/chatwindow.hpp +++ /dev/null @@ -1,90 +0,0 @@ -#pragma once -#include -#include -#include -#include "discord/discord.hpp" -#include "completer.hpp" - -class ChatMessageHeader; -class ChatMessageItemContainer; -class ChatInput; -class ChatInputIndicator; -class RateLimitIndicator; -class ChatList; -class ChatWindow { -public: - ChatWindow(); - - Gtk::Widget *GetRoot() const; - Snowflake GetActiveChannel() const; - - void Clear(); - void SetMessages(const std::vector &msgs); // clear contents and replace with given set - void SetActiveChannel(Snowflake id); - void AddNewMessage(const Message &data); // append new message to bottom - void DeleteMessage(Snowflake id); // add [deleted] indicator - void UpdateMessage(Snowflake id); // add [edited] indicator - void AddNewHistory(const std::vector &msgs); // prepend messages - void InsertChatInput(std::string text); - Snowflake GetOldestListedMessage(); // oldest message that is currently in the ListBox - void UpdateReactions(Snowflake id); - void SetTopic(const std::string &text); - -protected: - bool m_is_replying = false; - Snowflake m_replying_to; - - void StartReplying(Snowflake message_id); - void StopReplying(); - - Snowflake m_active_channel; - - bool OnInputSubmit(const Glib::ustring &text); - - bool OnKeyPressEvent(GdkEventKey *e); - void OnScrollEdgeOvershot(Gtk::PositionType pos); - - void OnMessageSendFail(const std::string &nonce, float retry_after); - - Gtk::Box *m_main; - //Gtk::ListBox *m_list; - //Gtk::ScrolledWindow *m_scroll; - - Gtk::EventBox m_topic; // todo probably make everything else go on the stack - Gtk::Label m_topic_text; - - ChatList *m_chat; - - ChatInput *m_input; - - Completer m_completer; - ChatInputIndicator *m_input_indicator; - RateLimitIndicator *m_rate_limit_indicator; - Gtk::Box *m_meta; - -public: - typedef sigc::signal type_signal_action_message_edit; - typedef sigc::signal type_signal_action_chat_submit; - typedef sigc::signal type_signal_action_chat_load_history; - typedef sigc::signal type_signal_action_channel_click; - typedef sigc::signal type_signal_action_insert_mention; - typedef sigc::signal type_signal_action_reaction_add; - typedef sigc::signal type_signal_action_reaction_remove; - - 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(); - type_signal_action_reaction_add signal_action_reaction_add(); - type_signal_action_reaction_remove signal_action_reaction_remove(); - -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; - type_signal_action_reaction_add m_signal_action_reaction_add; - type_signal_action_reaction_remove m_signal_action_reaction_remove; -}; diff --git a/components/completer.cpp b/components/completer.cpp deleted file mode 100644 index 327ef95..0000000 --- a/components/completer.cpp +++ /dev/null @@ -1,392 +0,0 @@ -#include -#include "completer.hpp" -#include "abaddon.hpp" -#include "util.hpp" - -constexpr const int CompleterHeight = 150; -constexpr const int MaxCompleterEntries = 30; - -Completer::Completer() { - set_reveal_child(false); - set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_NONE); // only SLIDE_UP and NONE work decently - - m_scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); - m_scroll.set_max_content_height(CompleterHeight); - m_scroll.set_size_request(-1, CompleterHeight); - m_scroll.set_placement(Gtk::CORNER_BOTTOM_LEFT); - - m_list.set_adjustment(m_scroll.get_vadjustment()); - m_list.set_focus_vadjustment(m_scroll.get_vadjustment()); - m_list.get_style_context()->add_class("completer"); - m_list.set_activate_on_single_click(true); - - m_list.set_focus_on_click(false); - set_can_focus(false); - - m_list.signal_row_activated().connect(sigc::mem_fun(*this, &Completer::OnRowActivate)); - - m_scroll.add(m_list); - add(m_scroll); - show_all(); -} - -Completer::Completer(const Glib::RefPtr &buf) - : Completer() { - SetBuffer(buf); -} - -void Completer::SetBuffer(const Glib::RefPtr &buf) { - m_buf = buf; - m_buf->signal_changed().connect(sigc::mem_fun(*this, &Completer::OnTextBufferChanged)); -} - -bool Completer::ProcessKeyPress(GdkEventKey *e) { - if (!IsShown()) return false; - if (e->type != GDK_KEY_PRESS) return false; - - switch (e->keyval) { - case GDK_KEY_Down: { - if (m_entries.size() == 0) return true; - const auto index = static_cast(m_list.get_selected_row()->get_index()); - if (index >= m_entries.size() - 1) return true; - m_list.select_row(*m_entries[index + 1]); - ScrollListBoxToSelected(m_list); - } - return true; - case GDK_KEY_Up: { - if (m_entries.size() == 0) return true; - const auto index = static_cast(m_list.get_selected_row()->get_index()); - if (index == 0) return true; - m_list.select_row(*m_entries[index - 1]); - ScrollListBoxToSelected(m_list); - } - return true; - case GDK_KEY_Return: { - if (m_entries.size() == 0) return true; - DoCompletion(m_list.get_selected_row()); - } - return true; - default: - break; - } - - return false; -} - -void Completer::SetGetRecentAuthors(get_recent_authors_cb cb) { - m_recent_authors_cb = cb; -} - -void Completer::SetGetChannelID(get_channel_id_cb cb) { - m_channel_id_cb = cb; -} - -bool Completer::IsShown() const { - return get_child_revealed(); -} - -CompleterEntry *Completer::CreateEntry(const Glib::ustring &completion) { - auto entry = Gtk::manage(new CompleterEntry(completion, m_entries.size())); - m_entries.push_back(entry); - entry->show_all(); - m_list.add(*entry); - return entry; -} - -void Completer::CompleteMentions(const Glib::ustring &term) { - if (!m_recent_authors_cb) - return; - - const auto &discord = Abaddon::Get().GetDiscordClient(); - - Snowflake channel_id; - if (m_channel_id_cb) - channel_id = m_channel_id_cb(); - auto author_ids = m_recent_authors_cb(); - if (channel_id.IsValid()) { - const auto chan = discord.GetChannel(channel_id); - if (chan->GuildID.has_value()) { - const auto members = discord.GetUsersInGuild(*chan->GuildID); - for (const auto x : members) - if (std::find(author_ids.begin(), author_ids.end(), x) == author_ids.end()) - author_ids.push_back(x); - } - } - const auto me = discord.GetUserData().ID; - int i = 0; - for (const auto id : author_ids) { - if (id == me) continue; - const auto author = discord.GetUser(id); - if (!author.has_value()) continue; - if (!StringContainsCaseless(author->Username, term)) continue; - if (i++ > 15) break; - - auto entry = CreateEntry(author->GetMention()); - - entry->SetText(author->Username + "#" + author->Discriminator); - - if (channel_id.IsValid()) { - const auto chan = discord.GetChannel(channel_id); - if (chan.has_value() && chan->GuildID.has_value()) { - const auto role_id = discord.GetMemberHoistedRole(*chan->GuildID, id, true); - if (role_id.IsValid()) { - const auto role = discord.GetRole(role_id); - if (role.has_value()) - entry->SetTextColor(role->Color); - } - } - } - - entry->SetImage(author->GetAvatarURL()); - } -} - -void Completer::CompleteEmojis(const Glib::ustring &term) { - if (!m_channel_id_cb) - return; - - const auto &discord = Abaddon::Get().GetDiscordClient(); - const auto channel_id = m_channel_id_cb(); - const auto channel = discord.GetChannel(channel_id); - - const auto make_entry = [&](const Glib::ustring &name, const Glib::ustring &completion, const Glib::ustring &url = "", bool animated = false) -> CompleterEntry * { - const auto entry = CreateEntry(completion); - entry->SetText(name); - if (url == "") return entry; - if (animated) - entry->SetAnimation(url); - else - entry->SetImage(url); - return entry; - }; - - const auto self_id = discord.GetUserData().ID; - const bool can_use_external = discord.GetSelfPremiumType() != EPremiumType::None && discord.HasChannelPermission(self_id, channel_id, Permission::USE_EXTERNAL_EMOJIS); - - int i = 0; - if (!can_use_external) { - if (channel->GuildID.has_value()) { - const auto guild = discord.GetGuild(*channel->GuildID); - - if (guild.has_value() && guild->Emojis.has_value()) - for (const auto &tmp : *guild->Emojis) { - const auto emoji = *discord.GetEmoji(tmp.ID); - if (emoji.IsAnimated.has_value() && *emoji.IsAnimated) continue; - if (emoji.IsAvailable.has_value() && !*emoji.IsAvailable) continue; - if (emoji.Roles.has_value() && emoji.Roles->size() > 0) continue; - if (term.size() > 0) - if (!StringContainsCaseless(emoji.Name, term)) continue; - - if (i++ > MaxCompleterEntries) break; - - make_entry(emoji.Name, "<:" + emoji.Name + ":" + std::to_string(emoji.ID) + ">", emoji.GetURL()); - } - } - } else { - for (const auto guild_id : discord.GetGuilds()) { - const auto guild = discord.GetGuild(guild_id); - if (!guild.has_value()) continue; - for (const auto &tmp : *guild->Emojis) { - const auto emoji = *discord.GetEmoji(tmp.ID); - const bool is_animated = emoji.IsAnimated.has_value() && *emoji.IsAnimated; - if (emoji.IsAvailable.has_value() && !*emoji.IsAvailable) continue; - if (emoji.Roles.has_value() && emoji.Roles->size() > 0) continue; - if (term.size() > 0) - if (!StringContainsCaseless(emoji.Name, term)) continue; - - if (i++ > MaxCompleterEntries) goto done; - - if (is_animated) - make_entry(emoji.Name, "", emoji.GetURL("gif"), true); - else - make_entry(emoji.Name, "<:" + emoji.Name + ":" + std::to_string(emoji.ID) + ">", emoji.GetURL()); - } - } - } -done: - - // if <15 guild emojis match then load up stock - if (i < 15) { - std::unordered_set added_patterns; - auto &emojis = Abaddon::Get().GetEmojis(); - const auto &shortcodes = emojis.GetShortCodes(); - for (const auto &[shortcode, pattern] : shortcodes) { - if (added_patterns.find(pattern) != added_patterns.end()) continue; - if (!StringContainsCaseless(shortcode, term)) continue; - if (i++ > 15) break; - const auto &pb = emojis.GetPixBuf(pattern); - if (!pb) continue; - added_patterns.insert(pattern); - const auto entry = make_entry(shortcode, pattern); - entry->SetImage(pb->scale_simple(CompleterImageSize, CompleterImageSize, Gdk::INTERP_BILINEAR)); - } - } -} - -void Completer::CompleteChannels(const Glib::ustring &term) { - if (!m_channel_id_cb) - return; - - const auto &discord = Abaddon::Get().GetDiscordClient(); - const auto channel_id = m_channel_id_cb(); - const auto channel = discord.GetChannel(channel_id); - if (!channel->GuildID.has_value()) return; - const auto channels = discord.GetChannelsInGuild(*channel->GuildID); - int i = 0; - for (const auto chan_id : channels) { - const auto chan = discord.GetChannel(chan_id); - if (chan->Type == ChannelType::GUILD_VOICE || chan->Type == ChannelType::GUILD_CATEGORY) continue; - if (!StringContainsCaseless(*chan->Name, term)) continue; - if (i++ > MaxCompleterEntries) break; - const auto entry = CreateEntry("<#" + std::to_string(chan_id) + ">"); - entry->SetText("#" + *chan->Name); - } -} - -void Completer::DoCompletion(Gtk::ListBoxRow *row) { - const int index = row->get_index(); - const auto completion = m_entries[index]->GetCompletion(); - const auto it = m_buf->erase(m_start, m_end); // entry is deleted here - m_buf->insert(it, completion + " "); -} - -void Completer::OnRowActivate(Gtk::ListBoxRow *row) { - DoCompletion(row); -} - -void Completer::OnTextBufferChanged() { - const auto term = GetTerm(); - - for (auto it = m_entries.begin(); it != m_entries.end();) { - delete *it; - it = m_entries.erase(it); - } - - switch (term[0]) { - case '@': - CompleteMentions(term.substr(1)); - break; - case ':': - CompleteEmojis(term.substr(1)); - break; - case '#': - CompleteChannels(term.substr(1)); - break; - default: - break; - } - if (m_entries.size() > 0) { - m_list.select_row(*m_entries[0]); - set_reveal_child(true); - } else { - set_reveal_child(false); - } -} - -bool MultiBackwardSearch(const Gtk::TextIter &iter, const Glib::ustring &chars, Gtk::TextSearchFlags flags, Gtk::TextBuffer::iterator &out) { - bool any = false; - for (const auto c : chars) { - Glib::ustring tmp(1, c); - Gtk::TextBuffer::iterator tstart, tend; - if (!iter.backward_search(tmp, flags, tstart, tend)) continue; - // if previous found, compare to see if closer to out iter - if (any) { - if (tstart.get_offset() > out.get_offset()) - out = tstart; - } else - out = tstart; - any = true; - } - return any; -} - -bool MultiForwardSearch(const Gtk::TextIter &iter, const Glib::ustring &chars, Gtk::TextSearchFlags flags, Gtk::TextBuffer::iterator &out) { - bool any = false; - for (const auto c : chars) { - Glib::ustring tmp(1, c); - Gtk::TextBuffer::iterator tstart, tend; - if (!iter.forward_search(tmp, flags, tstart, tend)) continue; - // if previous found, compare to see if closer to out iter - if (any) { - if (tstart.get_offset() < out.get_offset()) - out = tstart; - } else - out = tstart; - any = true; - } - return any; -} - -Glib::ustring Completer::GetTerm() { - const auto iter = m_buf->get_insert()->get_iter(); - Gtk::TextBuffer::iterator dummy; - if (!MultiBackwardSearch(iter, " \n", Gtk::TEXT_SEARCH_TEXT_ONLY, m_start)) - m_buf->get_bounds(m_start, dummy); - else - m_start.forward_char(); // 1 behind - if (!MultiForwardSearch(iter, " \n", Gtk::TEXT_SEARCH_TEXT_ONLY, m_end)) - m_buf->get_bounds(dummy, m_end); - return m_start.get_text(m_end); -} - -CompleterEntry::CompleterEntry(const Glib::ustring &completion, int index) - : m_completion(completion) - , m_index(index) - , m_box(Gtk::ORIENTATION_HORIZONTAL) { - set_halign(Gtk::ALIGN_START); - get_style_context()->add_class("completer-entry"); - set_can_focus(false); - set_focus_on_click(false); - m_box.show(); - add(m_box); -} - -void CompleterEntry::SetTextColor(int color) { - if (m_text == nullptr) return; - const auto cur = m_text->get_text(); - m_text->set_markup("" + Glib::Markup::escape_text(cur) + ""); -} - -void CompleterEntry::SetText(const Glib::ustring &text) { - if (m_text == nullptr) { - m_text = Gtk::manage(new Gtk::Label); - m_text->get_style_context()->add_class("completer-entry-label"); - m_text->show(); - m_box.pack_end(*m_text); - } - m_text->set_label(text); -} - -void CompleterEntry::SetImage(const Glib::RefPtr &pb) { - CheckImage(); - m_img->property_pixbuf() = pb; -} - -void CompleterEntry::SetImage(const std::string &url) { - CheckImage(); - m_img->SetAnimated(false); - m_img->SetURL(url); -} - -void CompleterEntry::SetAnimation(const std::string &url) { - CheckImage(); - m_img->SetAnimated(true); - m_img->SetURL(url); -} - -void CompleterEntry::CheckImage() { - if (m_img == nullptr) { - m_img = Gtk::manage(new LazyImage(CompleterImageSize, CompleterImageSize)); - m_img->get_style_context()->add_class("completer-entry-image"); - m_img->show(); - m_box.pack_start(*m_img); - } -} - -int CompleterEntry::GetIndex() const { - return m_index; -} - -Glib::ustring CompleterEntry::GetCompletion() const { - return m_completion; -} diff --git a/components/completer.hpp b/components/completer.hpp deleted file mode 100644 index 6bd8be9..0000000 --- a/components/completer.hpp +++ /dev/null @@ -1,68 +0,0 @@ -#pragma once -#include -#include -#include "lazyimage.hpp" -#include "discord/snowflake.hpp" - -constexpr static int CompleterImageSize = 24; - -class CompleterEntry : public Gtk::ListBoxRow { -public: - CompleterEntry(const Glib::ustring &completion, int index); - void SetTextColor(int color); // SetText will reset - void SetText(const Glib::ustring &text); - void SetImage(const Glib::RefPtr &pb); - void SetImage(const std::string &url); - void SetAnimation(const std::string &url); - - int GetIndex() const; - Glib::ustring GetCompletion() const; - -private: - void CheckImage(); - - Glib::ustring m_completion; - int m_index; - Gtk::Box m_box; - Gtk::Label *m_text = nullptr; - LazyImage *m_img = nullptr; -}; - -class Completer : public Gtk::Revealer { -public: - Completer(); - Completer(const Glib::RefPtr &buf); - - void SetBuffer(const Glib::RefPtr &buf); - bool ProcessKeyPress(GdkEventKey *e); - - using get_recent_authors_cb = std::function()>; - void SetGetRecentAuthors(get_recent_authors_cb cb); // maybe a better way idk - using get_channel_id_cb = std::function; - void SetGetChannelID(get_channel_id_cb cb); - - bool IsShown() const; - -private: - CompleterEntry *CreateEntry(const Glib::ustring &completion); - void CompleteMentions(const Glib::ustring &term); - void CompleteEmojis(const Glib::ustring &term); - void CompleteChannels(const Glib::ustring &term); - void DoCompletion(Gtk::ListBoxRow *row); - - std::vector m_entries; - - void OnRowActivate(Gtk::ListBoxRow *row); - void OnTextBufferChanged(); - Glib::ustring GetTerm(); - - Gtk::TextBuffer::iterator m_start; - Gtk::TextBuffer::iterator m_end; - - Gtk::ScrolledWindow m_scroll; - Gtk::ListBox m_list; - Glib::RefPtr m_buf; - - get_recent_authors_cb m_recent_authors_cb; - get_channel_id_cb m_channel_id_cb; -}; diff --git a/components/draglistbox.cpp b/components/draglistbox.cpp deleted file mode 100644 index 492abc3..0000000 --- a/components/draglistbox.cpp +++ /dev/null @@ -1,141 +0,0 @@ -#include "draglistbox.hpp" - -DragListBox::DragListBox() { - drag_dest_set(m_entries, Gtk::DEST_DEFAULT_MOTION | Gtk::DEST_DEFAULT_DROP, Gdk::ACTION_MOVE); -} - -void DragListBox::row_drag_begin(Gtk::Widget *widget, const Glib::RefPtr &context) { - m_drag_row = dynamic_cast(widget->get_ancestor(GTK_TYPE_LIST_BOX_ROW)); - - auto alloc = m_drag_row->get_allocation(); - auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, alloc.get_width(), alloc.get_height()); - auto cr = Cairo::Context::create(surface); - - m_drag_row->get_style_context()->add_class("drag-icon"); - gtk_widget_draw(reinterpret_cast(m_drag_row->gobj()), cr->cobj()); - m_drag_row->get_style_context()->remove_class("drag-icon"); - - int x, y; - widget->translate_coordinates(*m_drag_row, 0, 0, x, y); - surface->set_device_offset(-x, -y); - context->set_icon(surface); -} - -bool DragListBox::on_drag_motion(const Glib::RefPtr &context, gint x, gint y, guint time) { - if (y > m_hover_top || y < m_hover_bottom) { - auto *row = get_row_at_y(y); - if (row == nullptr) return true; - const bool old_top = m_top; - const auto alloc = row->get_allocation(); - - const int hover_row_y = alloc.get_y(); - const int hover_row_height = alloc.get_height(); - if (row != m_drag_row) { - if (y < hover_row_y + hover_row_height / 2) { - m_hover_top = hover_row_y; - m_hover_bottom = m_hover_top + hover_row_height / 2; - row->get_style_context()->add_class("drag-hover-top"); - row->get_style_context()->remove_class("drag-hover-bottom"); - m_top = true; - } else { - m_hover_top = hover_row_y + hover_row_height / 2; - m_hover_bottom = hover_row_y + hover_row_height; - row->get_style_context()->add_class("drag-hover-bottom"); - row->get_style_context()->remove_class("drag-hover-top"); - m_top = false; - } - } - - if (m_hover_row != nullptr && m_hover_row != row) { - if (old_top) - m_hover_row->get_style_context()->remove_class("drag-hover-top"); - else - m_hover_row->get_style_context()->remove_class("drag-hover-bottom"); - } - - m_hover_row = row; - } - - check_scroll(y); - if (m_should_scroll && !m_scrolling) { - m_scrolling = true; - Glib::signal_timeout().connect(sigc::mem_fun(*this, &DragListBox::scroll), SCROLL_DELAY); - } - - return true; -} - -void DragListBox::on_drag_leave(const Glib::RefPtr &context, guint time) { - m_should_scroll = false; -} - -void DragListBox::check_scroll(gint y) { - if (!get_focus_vadjustment()) - return; - - const double vadjustment_min = get_focus_vadjustment()->get_value(); - const double vadjustment_max = get_focus_vadjustment()->get_page_size() + vadjustment_min; - const double show_min = std::max(0, y - SCROLL_DISTANCE); - const double show_max = std::min(get_focus_vadjustment()->get_upper(), static_cast(y) + SCROLL_DISTANCE); - if (vadjustment_min > show_min) { - m_should_scroll = true; - m_scroll_up = true; - } else if (vadjustment_max < show_max) { - m_should_scroll = true; - m_scroll_up = false; - } else { - m_should_scroll = false; - } -} - -bool DragListBox::scroll() { - if (m_should_scroll) { - if (m_scroll_up) { - get_focus_vadjustment()->set_value(get_focus_vadjustment()->get_value() - SCROLL_STEP_SIZE); - } else { - get_focus_vadjustment()->set_value(get_focus_vadjustment()->get_value() + SCROLL_STEP_SIZE); - } - } else { - m_scrolling = false; - } - return m_should_scroll; -} - -void DragListBox::on_drag_data_received(const Glib::RefPtr &context, int x, int y, const Gtk::SelectionData &selection_data, guint info, guint time) { - int index = 0; - if (m_hover_row != nullptr) { - if (m_top) { - index = m_hover_row->get_index() - 1; - m_hover_row->get_style_context()->remove_class("drag-hover-top"); - } else { - index = m_hover_row->get_index(); - m_hover_row->get_style_context()->remove_class("drag-hover-bottom"); - } - - Gtk::Widget *handle = *reinterpret_cast(selection_data.get_data()); - auto *row = dynamic_cast(handle->get_ancestor(GTK_TYPE_LIST_BOX_ROW)); - - if (row != nullptr && row != m_hover_row) { - if (row->get_index() > index) - index += 1; - if (m_signal_on_drop.emit(row, index)) return; - row->get_parent()->remove(*row); - insert(*row, index); - } - } - - m_drag_row = nullptr; -} - -void DragListBox::add_draggable(Gtk::ListBoxRow *widget) { - widget->drag_source_set(m_entries, Gdk::BUTTON1_MASK, Gdk::ACTION_MOVE); - widget->signal_drag_begin().connect(sigc::bind<0>(sigc::mem_fun(*this, &DragListBox::row_drag_begin), widget)); - widget->signal_drag_data_get().connect([this, widget](const Glib::RefPtr &context, Gtk::SelectionData &selection_data, guint info, guint time) { - selection_data.set("GTK_LIST_BOX_ROW", 32, reinterpret_cast(&widget), sizeof(&widget)); - }); - add(*widget); -} - -DragListBox::type_signal_on_drop DragListBox::signal_on_drop() { - return m_signal_on_drop; -} diff --git a/components/draglistbox.hpp b/components/draglistbox.hpp deleted file mode 100644 index 9f204be..0000000 --- a/components/draglistbox.hpp +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once -#include - -class DragListBox : public Gtk::ListBox { -public: - DragListBox(); - - void row_drag_begin(Gtk::Widget *widget, const Glib::RefPtr &context); - - bool on_drag_motion(const Glib::RefPtr &context, gint x, gint y, guint time) override; - - void on_drag_leave(const Glib::RefPtr &context, guint time) override; - - void check_scroll(gint y); - - bool scroll(); - - void on_drag_data_received(const Glib::RefPtr &context, int x, int y, const Gtk::SelectionData &selection_data, guint info, guint time) override; - - void add_draggable(Gtk::ListBoxRow *widget); - -private: - Gtk::ListBoxRow *m_hover_row = nullptr; - Gtk::ListBoxRow *m_drag_row = nullptr; - bool m_top = false; - int m_hover_top = 0; - int m_hover_bottom = 0; - bool m_should_scroll = false; - bool m_scrolling = false; - bool m_scroll_up = false; - - constexpr static int SCROLL_STEP_SIZE = 8; - constexpr static int SCROLL_DISTANCE = 30; - constexpr static int SCROLL_DELAY = 50; - - const std::vector m_entries = { - Gtk::TargetEntry("GTK_LIST_BOX_ROW", Gtk::TARGET_SAME_APP, 0), - }; - - using type_signal_on_drop = sigc::signal; - type_signal_on_drop m_signal_on_drop; - -public: - type_signal_on_drop signal_on_drop(); // return true to prevent drop -}; diff --git a/components/friendslist.cpp b/components/friendslist.cpp deleted file mode 100644 index 3896f02..0000000 --- a/components/friendslist.cpp +++ /dev/null @@ -1,354 +0,0 @@ -#include "friendslist.hpp" -#include "abaddon.hpp" -#include "lazyimage.hpp" - -using namespace std::string_literals; - -FriendsList::FriendsList() - : Gtk::Box(Gtk::ORIENTATION_VERTICAL) - , m_filter_mode(FILTER_FRIENDS) { - get_style_context()->add_class("friends-list"); - - auto &discord = Abaddon::Get().GetDiscordClient(); - - discord.signal_relationship_add().connect(sigc::mem_fun(*this, &FriendsList::OnRelationshipAdd)); - discord.signal_relationship_remove().connect(sigc::mem_fun(*this, &FriendsList::OnRelationshipRemove)); - - PopulateRelationships(); - signal_map().connect(sigc::mem_fun(*this, &FriendsList::PopulateRelationships)); - - constexpr static std::array strs = { - "Friends", - "Online", - "Pending", - "Blocked", - }; - for (const auto &x : strs) { - auto *btn = Gtk::manage(new Gtk::RadioButton(m_group, x)); - m_buttons.add(*btn); - btn->show(); - btn->signal_toggled().connect([this, btn, str = x] { - if (!btn->get_active()) return; - switch (str[0]) { // hehe - case 'F': - m_filter_mode = FILTER_FRIENDS; - break; - case 'O': - m_filter_mode = FILTER_ONLINE; - break; - case 'P': - m_filter_mode = FILTER_PENDING; - break; - case 'B': - m_filter_mode = FILTER_BLOCKED; - break; - } - m_list.invalidate_filter(); - }); - } - m_buttons.set_homogeneous(true); - m_buttons.set_halign(Gtk::ALIGN_CENTER); - - m_add.set_halign(Gtk::ALIGN_CENTER); - m_add.set_margin_top(5); - m_add.set_margin_bottom(5); - - m_scroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); - - m_list.set_sort_func(sigc::mem_fun(*this, &FriendsList::ListSortFunc)); - m_list.set_filter_func(sigc::mem_fun(*this, &FriendsList::ListFilterFunc)); - m_list.set_selection_mode(Gtk::SELECTION_NONE); - m_list.set_hexpand(true); - m_list.set_vexpand(true); - m_scroll.add(m_list); - add(m_add); - add(m_buttons); - add(m_scroll); - - m_add.show(); - m_scroll.show(); - m_buttons.show(); - m_list.show(); -} - -FriendsListFriendRow *FriendsList::MakeRow(const UserData &user, RelationshipType type) { - auto *row = Gtk::manage(new FriendsListFriendRow(type, user)); - row->signal_action_remove().connect(sigc::bind(sigc::mem_fun(*this, &FriendsList::OnActionRemove), user.ID)); - row->signal_action_accept().connect(sigc::bind(sigc::mem_fun(*this, &FriendsList::OnActionAccept), user.ID)); - return row; -} - -void FriendsList::OnRelationshipAdd(const RelationshipAddData &data) { - for (auto *row_ : m_list.get_children()) { - auto *row = dynamic_cast(row_); - if (row == nullptr || row->ID != data.ID) continue; - delete row; - break; - } - - auto *row = MakeRow(data.User, data.Type); - m_list.add(*row); - row->show(); -} - -void FriendsList::OnRelationshipRemove(Snowflake id, RelationshipType type) { - for (auto *row_ : m_list.get_children()) { - auto *row = dynamic_cast(row_); - if (row == nullptr || row->ID != id) continue; - delete row; - return; - } -} - -void FriendsList::OnActionAccept(Snowflake id) { - const auto cb = [this](DiscordError code) { - if (code != DiscordError::NONE) { - Gtk::MessageDialog dlg(*dynamic_cast(get_toplevel()), "Failed to accept", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); - dlg.set_position(Gtk::WIN_POS_CENTER); - dlg.run(); - } - }; - Abaddon::Get().GetDiscordClient().PutRelationship(id, sigc::track_obj(cb, *this)); -} - -void FriendsList::OnActionRemove(Snowflake id) { - auto &discord = Abaddon::Get().GetDiscordClient(); - const auto user = discord.GetUser(id); - if (auto *window = dynamic_cast(get_toplevel())) { - Glib::ustring str; - switch (*discord.GetRelationship(id)) { - case RelationshipType::Blocked: - str = "Are you sure you want to unblock " + user->Username + "#" + user->Discriminator + "?"; - break; - case RelationshipType::Friend: - str = "Are you sure you want to remove " + user->Username + "#" + user->Discriminator + "?"; - break; - case RelationshipType::PendingIncoming: - str = "Are you sure you want to ignore " + user->Username + "#" + user->Discriminator + "?"; - break; - case RelationshipType::PendingOutgoing: - str = "Are you sure you want to cancel your request to " + user->Username + "#" + user->Discriminator + "?"; - break; - default: - break; - } - if (Abaddon::Get().ShowConfirm(str, window)) { - const auto cb = [this, window](DiscordError code) { - if (code == DiscordError::NONE) return; - Gtk::MessageDialog dlg(*window, "Failed to remove user", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); - dlg.set_position(Gtk::WIN_POS_CENTER); - dlg.run(); - }; - discord.RemoveRelationship(id, sigc::track_obj(cb, *this)); - } - } -} - -void FriendsList::PopulateRelationships() { - for (auto child : m_list.get_children()) - delete child; - - auto &discord = Abaddon::Get().GetDiscordClient(); - for (const auto &[id, type] : discord.GetRelationships()) { - const auto user = discord.GetUser(id); - if (!user.has_value()) continue; - auto *row = MakeRow(*user, type); - m_list.add(*row); - row->show(); - } -} - -int FriendsList::ListSortFunc(Gtk::ListBoxRow *a_, Gtk::ListBoxRow *b_) { - auto *a = dynamic_cast(a_); - auto *b = dynamic_cast(b_); - if (a == nullptr || b == nullptr) return 0; - return a->Name.compare(b->Name); -} - -bool FriendsList::ListFilterFunc(Gtk::ListBoxRow *row_) { - auto *row = dynamic_cast(row_); - if (row == nullptr) return false; - switch (m_filter_mode) { - case FILTER_FRIENDS: - return row->Type == RelationshipType::Friend; - case FILTER_ONLINE: - return row->Type == RelationshipType::Friend && row->Status != PresenceStatus::Offline; - case FILTER_PENDING: - return row->Type == RelationshipType::PendingIncoming || row->Type == RelationshipType::PendingOutgoing; - case FILTER_BLOCKED: - return row->Type == RelationshipType::Blocked; - default: - return false; - } -} - -FriendsListAddComponent::FriendsListAddComponent() - : Gtk::Box(Gtk::ORIENTATION_VERTICAL) - , m_label("Add a Friend", Gtk::ALIGN_START) - , m_status("", Gtk::ALIGN_START) - , m_add("Add") - , m_box(Gtk::ORIENTATION_HORIZONTAL) { - m_box.add(m_entry); - m_box.add(m_add); - m_box.add(m_status); - - m_add.signal_clicked().connect(sigc::mem_fun(*this, &FriendsListAddComponent::Submit)); - - m_label.set_halign(Gtk::ALIGN_CENTER); - - m_entry.set_placeholder_text("Enter a Username#1234"); - m_entry.signal_key_press_event().connect(sigc::mem_fun(*this, &FriendsListAddComponent::OnKeyPress), false); - - add(m_label); - add(m_box); - - show_all_children(); -} - -void FriendsListAddComponent::Submit() { - if (m_requesting) return; - - auto text = m_entry.get_text(); - m_label.set_text("Invalid input"); // cheeky !! - m_entry.set_text(""); - const auto hashpos = text.find("#"); - if (hashpos == Glib::ustring::npos) return; - const auto username = text.substr(0, hashpos); - const auto discriminator = text.substr(hashpos + 1); - if (username.size() == 0 || discriminator.size() != 4) return; - if (discriminator.find_first_not_of("0123456789") != Glib::ustring::npos) return; - - m_requesting = true; - m_label.set_text("Hang on..."); - - const auto cb = [this](DiscordError code) { - m_requesting = false; - if (code == DiscordError::NONE) { - m_label.set_text("Success!"); - } else { - m_label.set_text("Failed: "s + GetDiscordErrorDisplayString(code)); - } - }; - Abaddon::Get().GetDiscordClient().SendFriendRequest(username, std::stoul(discriminator), sigc::track_obj(cb, *this)); -} - -bool FriendsListAddComponent::OnKeyPress(GdkEventKey *e) { - if (e->keyval == GDK_KEY_Return) { - Submit(); - return true; - } - return false; -} - -FriendsListFriendRow::FriendsListFriendRow(RelationshipType type, const UserData &data) - : ID(data.ID) - , Type(type) - , Name(data.Username + "#" + data.Discriminator) - , Status(Abaddon::Get().GetDiscordClient().GetUserStatus(data.ID)) - , m_accept("Accept") { - auto *ev = Gtk::manage(new Gtk::EventBox); - auto *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); - auto *img = Gtk::manage(new LazyImage(32, 32, true)); - auto *namebox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); - auto *namelbl = Gtk::manage(new Gtk::Label("", Gtk::ALIGN_START)); - m_status_lbl = Gtk::manage(new Gtk::Label("", Gtk::ALIGN_START)); - auto *lblbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); - - auto &discord = Abaddon::Get().GetDiscordClient(); - discord.signal_presence_update().connect(sigc::mem_fun(*this, &FriendsListFriendRow::OnPresenceUpdate)); - - static bool show_animations = Abaddon::Get().GetSettings().GetShowAnimations(); - if (data.HasAnimatedAvatar() && show_animations) { - img->SetAnimated(true); - img->SetURL(data.GetAvatarURL("gif", "32")); - } else { - img->SetURL(data.GetAvatarURL("png", "32")); - } - - namelbl->set_markup(data.GetEscapedBoldName()); - - UpdatePresenceLabel(); - - AddWidgetMenuHandler(ev, m_menu, [this] { - m_accept.set_visible(Type == RelationshipType::PendingIncoming); - - switch (Type) { - case RelationshipType::Blocked: - case RelationshipType::Friend: - m_remove.set_label("Remove"); - break; - case RelationshipType::PendingIncoming: - m_remove.set_label("Ignore"); - break; - case RelationshipType::PendingOutgoing: - m_remove.set_label("Cancel"); - break; - default: - break; - } - }); - - m_remove.signal_activate().connect([this] { - m_signal_remove.emit(); - }); - - m_accept.signal_activate().connect([this] { - m_signal_accept.emit(); - }); - - m_menu.append(m_accept); - m_menu.append(m_remove); - m_menu.show_all(); - - lblbox->set_valign(Gtk::ALIGN_CENTER); - - img->set_margin_end(5); - - namebox->add(*namelbl); - lblbox->add(*namebox); - lblbox->add(*m_status_lbl); - - box->add(*img); - box->add(*lblbox); - - ev->add(*box); - add(*ev); - show_all_children(); -} - -void FriendsListFriendRow::UpdatePresenceLabel() { - switch (Type) { - case RelationshipType::PendingIncoming: - m_status_lbl->set_text("Incoming Friend Request"); - break; - case RelationshipType::PendingOutgoing: - m_status_lbl->set_text("Outgoing Friend Request"); - break; - default: - m_status_lbl->set_text(GetPresenceDisplayString(Status)); - break; - } -} - -void FriendsListFriendRow::OnPresenceUpdate(const UserData &user, PresenceStatus status) { - if (user.ID != ID) return; - Status = status; - UpdatePresenceLabel(); - changed(); -} - -FriendsListFriendRow::type_signal_remove FriendsListFriendRow::signal_action_remove() { - return m_signal_remove; -} - -FriendsListFriendRow::type_signal_accept FriendsListFriendRow::signal_action_accept() { - return m_signal_accept; -} - -FriendsListWindow::FriendsListWindow() { - add(m_friends); - set_default_size(500, 500); - get_style_context()->add_class("app-window"); - get_style_context()->add_class("app-popup"); - m_friends.show(); -} diff --git a/components/friendslist.hpp b/components/friendslist.hpp deleted file mode 100644 index 460ad32..0000000 --- a/components/friendslist.hpp +++ /dev/null @@ -1,92 +0,0 @@ -#pragma once -#include -#include "discord/objects.hpp" - -class FriendsListAddComponent : public Gtk::Box { -public: - FriendsListAddComponent(); - -private: - void Submit(); - bool OnKeyPress(GdkEventKey *e); - - Gtk::Label m_label; - Gtk::Label m_status; - Gtk::Entry m_entry; - Gtk::Button m_add; - Gtk::Box m_box; - - bool m_requesting = false; -}; - -class FriendsListFriendRow; -class FriendsList : public Gtk::Box { -public: - FriendsList(); - -private: - FriendsListFriendRow *MakeRow(const UserData &user, RelationshipType type); - - void OnRelationshipAdd(const RelationshipAddData &data); - void OnRelationshipRemove(Snowflake id, RelationshipType type); - - void OnActionAccept(Snowflake id); - void OnActionRemove(Snowflake id); - - void PopulateRelationships(); - - enum FilterMode { - FILTER_FRIENDS, - FILTER_ONLINE, - FILTER_PENDING, - FILTER_BLOCKED, - }; - - FilterMode m_filter_mode; - - int ListSortFunc(Gtk::ListBoxRow *a, Gtk::ListBoxRow *b); - bool ListFilterFunc(Gtk::ListBoxRow *row); - - FriendsListAddComponent m_add; - Gtk::RadioButtonGroup m_group; - Gtk::ButtonBox m_buttons; - Gtk::ScrolledWindow m_scroll; - Gtk::ListBox m_list; -}; - -class FriendsListFriendRow : public Gtk::ListBoxRow { -public: - FriendsListFriendRow(RelationshipType type, const UserData &str); - - Snowflake ID; - RelationshipType Type; - Glib::ustring Name; - PresenceStatus Status; - -private: - void UpdatePresenceLabel(); - void OnPresenceUpdate(const UserData &user, PresenceStatus status); - - Gtk::Label *m_status_lbl; - - Gtk::Menu m_menu; - Gtk::MenuItem m_remove; // or cancel or ignore - Gtk::MenuItem m_accept; // incoming - - using type_signal_remove = sigc::signal; - using type_signal_accept = sigc::signal; - type_signal_remove m_signal_remove; - type_signal_accept m_signal_accept; - -public: - type_signal_remove signal_action_remove(); - type_signal_accept signal_action_accept(); -}; - -class FriendsListWindow : public Gtk::Window { -public: - FriendsListWindow(); - -private: - FriendsList m_friends; -}; diff --git a/components/lazyimage.cpp b/components/lazyimage.cpp deleted file mode 100644 index 49bbdeb..0000000 --- a/components/lazyimage.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include "lazyimage.hpp" -#include "abaddon.hpp" - -LazyImage::LazyImage(int w, int h, bool use_placeholder) - : m_width(w) - , m_height(h) { - if (use_placeholder) - property_pixbuf() = Abaddon::Get().GetImageManager().GetPlaceholder(w)->scale_simple(w, h, Gdk::INTERP_BILINEAR); - signal_draw().connect(sigc::mem_fun(*this, &LazyImage::OnDraw)); -} - -LazyImage::LazyImage(const std::string &url, int w, int h, bool use_placeholder) - : m_url(url) - , m_width(w) - , m_height(h) { - if (use_placeholder) - property_pixbuf() = Abaddon::Get().GetImageManager().GetPlaceholder(w)->scale_simple(w, h, Gdk::INTERP_BILINEAR); - signal_draw().connect(sigc::mem_fun(*this, &LazyImage::OnDraw)); -} - -void LazyImage::SetAnimated(bool is_animated) { - m_animated = is_animated; -} - -void LazyImage::SetURL(const std::string &url) { - m_url = url; -} - -bool LazyImage::OnDraw(const Cairo::RefPtr &context) { - if (!m_needs_request || m_url == "") return false; - m_needs_request = false; - - if (m_animated) { - auto cb = [this](const Glib::RefPtr &pb) { - property_pixbuf_animation() = pb; - }; - - Abaddon::Get().GetImageManager().LoadAnimationFromURL(m_url, m_width, m_height, sigc::track_obj(cb, *this)); - } else { - auto cb = [this](const Glib::RefPtr &pb) { - property_pixbuf() = pb->scale_simple(m_width, m_height, Gdk::INTERP_BILINEAR); - }; - - Abaddon::Get().GetImageManager().LoadFromURL(m_url, sigc::track_obj(cb, *this)); - } - - return false; -} diff --git a/components/lazyimage.hpp b/components/lazyimage.hpp deleted file mode 100644 index fae69df..0000000 --- a/components/lazyimage.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once -#include - -// loads an image only when the widget is drawn for the first time -class LazyImage : public Gtk::Image { -public: - LazyImage(int w, int h, bool use_placeholder = true); - LazyImage(const std::string &url, int w, int h, bool use_placeholder = true); - - void SetAnimated(bool is_animated); - void SetURL(const std::string &url); - -private: - bool OnDraw(const Cairo::RefPtr &context); - - bool m_animated = false; - bool m_needs_request = true; - std::string m_url; - int m_width; - int m_height; -}; diff --git a/components/memberlist.cpp b/components/memberlist.cpp deleted file mode 100644 index 0c4d9bc..0000000 --- a/components/memberlist.cpp +++ /dev/null @@ -1,228 +0,0 @@ -#include "memberlist.hpp" -#include "abaddon.hpp" -#include "util.hpp" -#include "lazyimage.hpp" -#include "statusindicator.hpp" - -constexpr static const int MaxMemberListRows = 200; - -MemberListUserRow::MemberListUserRow(const std::optional &guild, const UserData &data) { - ID = data.ID; - m_ev = Gtk::manage(new Gtk::EventBox); - m_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); - m_label = Gtk::manage(new Gtk::Label); - m_avatar = Gtk::manage(new LazyImage(16, 16)); - m_status_indicator = Gtk::manage(new StatusIndicator(ID)); - - static bool crown = Abaddon::Get().GetSettings().GetShowOwnerCrown(); - if (crown && guild.has_value() && guild->OwnerID == data.ID) { - try { - const static auto crown_path = Abaddon::GetResPath("/crown.png"); - auto pixbuf = Gdk::Pixbuf::create_from_file(crown_path, 12, 12); - m_crown = Gtk::manage(new Gtk::Image(pixbuf)); - m_crown->set_valign(Gtk::ALIGN_CENTER); - m_crown->set_margin_end(8); - } catch (...) {} - } - - m_status_indicator->set_margin_start(3); - - if (guild.has_value()) - m_avatar->SetURL(data.GetAvatarURL(guild->ID, "png")); - else - m_avatar->SetURL(data.GetAvatarURL("png")); - - get_style_context()->add_class("members-row"); - get_style_context()->add_class("members-row-member"); - m_label->get_style_context()->add_class("members-row-label"); - m_avatar->get_style_context()->add_class("members-row-avatar"); - - m_label->set_single_line_mode(true); - m_label->set_ellipsize(Pango::ELLIPSIZE_END); - - static bool show_discriminator = Abaddon::Get().GetSettings().GetShowMemberListDiscriminators(); - std::string display = data.Username; - if (show_discriminator) - display += "#" + data.Discriminator; - if (guild.has_value()) { - if (const auto col_id = data.GetHoistedRole(guild->ID, true); col_id.IsValid()) { - auto color = Abaddon::Get().GetDiscordClient().GetRole(col_id)->Color; - m_label->set_use_markup(true); - m_label->set_markup("" + Glib::Markup::escape_text(display) + ""); - } else { - m_label->set_text(display); - } - } else { - m_label->set_text(display); - } - - m_label->set_halign(Gtk::ALIGN_START); - m_box->add(*m_avatar); - m_box->add(*m_status_indicator); - m_box->add(*m_label); - if (m_crown != nullptr) - m_box->add(*m_crown); - m_ev->add(*m_box); - add(*m_ev); - show_all(); -} - -MemberList::MemberList() { - m_main = Gtk::manage(new Gtk::ScrolledWindow); - m_listbox = Gtk::manage(new Gtk::ListBox); - - m_listbox->get_style_context()->add_class("members"); - - m_listbox->set_selection_mode(Gtk::SELECTION_NONE); - - m_main->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); - m_main->add(*m_listbox); - m_main->show_all(); -} - -Gtk::Widget *MemberList::GetRoot() const { - return m_main; -} - -void MemberList::Clear() { - SetActiveChannel(Snowflake::Invalid); - UpdateMemberList(); -} - -void MemberList::SetActiveChannel(Snowflake id) { - m_chan_id = id; - m_guild_id = Snowflake::Invalid; - if (m_chan_id.IsValid()) { - const auto chan = Abaddon::Get().GetDiscordClient().GetChannel(id); - if (chan.has_value() && chan->GuildID.has_value()) m_guild_id = *chan->GuildID; - } -} - -void MemberList::UpdateMemberList() { - m_id_to_row.clear(); - - auto children = m_listbox->get_children(); - auto it = children.begin(); - while (it != children.end()) { - delete *it; - it++; - } - - if (!Abaddon::Get().GetDiscordClient().IsStarted()) return; - if (!m_chan_id.IsValid()) return; - - auto &discord = Abaddon::Get().GetDiscordClient(); - const auto chan = discord.GetChannel(m_chan_id); - if (!chan.has_value()) return; - if (chan->Type == ChannelType::DM || chan->Type == ChannelType::GROUP_DM) { - int num_rows = 0; - for (const auto &user : chan->GetDMRecipients()) { - if (num_rows++ > MaxMemberListRows) break; - auto *row = Gtk::manage(new MemberListUserRow(std::nullopt, user)); - m_id_to_row[user.ID] = row; - AttachUserMenuHandler(row, user.ID); - m_listbox->add(*row); - } - - return; - } - - std::set ids; - if (chan->IsThread()) { - const auto x = discord.GetUsersInThread(m_chan_id); - ids = { x.begin(), x.end() }; - } else - ids = discord.GetUsersInGuild(m_guild_id); - - // process all the shit first so its in proper order - std::map pos_to_role; - std::map> pos_to_users; - std::unordered_map user_to_color; - std::vector roleless_users; - - for (const auto &id : ids) { - auto user = discord.GetUser(id); - if (!user.has_value() || user->IsDeleted()) - continue; - - auto pos_role_id = discord.GetMemberHoistedRole(m_guild_id, id); // role for positioning - auto col_role_id = discord.GetMemberHoistedRole(m_guild_id, id, true); // role for color - auto pos_role = discord.GetRole(pos_role_id); - auto col_role = discord.GetRole(col_role_id); - - if (!pos_role.has_value()) { - roleless_users.push_back(id); - continue; - }; - - pos_to_role[pos_role->Position] = *pos_role; - pos_to_users[pos_role->Position].push_back(std::move(*user)); - if (col_role.has_value()) - user_to_color[id] = col_role->Color; - } - - int num_rows = 0; - const auto guild = *discord.GetGuild(m_guild_id); - auto add_user = [this, &user_to_color, &num_rows, guild](const UserData &data) -> bool { - if (num_rows++ > MaxMemberListRows) return false; - auto *row = Gtk::manage(new MemberListUserRow(guild, data)); - m_id_to_row[data.ID] = row; - AttachUserMenuHandler(row, data.ID); - m_listbox->add(*row); - return true; - }; - - auto add_role = [this](std::string name) { - auto *role_row = Gtk::manage(new Gtk::ListBoxRow); - auto *role_lbl = Gtk::manage(new Gtk::Label); - - role_row->get_style_context()->add_class("members-row"); - role_row->get_style_context()->add_class("members-row-role"); - role_lbl->get_style_context()->add_class("members-row-label"); - - role_lbl->set_single_line_mode(true); - role_lbl->set_ellipsize(Pango::ELLIPSIZE_END); - role_lbl->set_use_markup(true); - role_lbl->set_markup("" + Glib::Markup::escape_text(name) + ""); - role_lbl->set_halign(Gtk::ALIGN_START); - role_row->add(*role_lbl); - role_row->show_all(); - m_listbox->add(*role_row); - }; - - for (auto it = pos_to_role.crbegin(); it != pos_to_role.crend(); it++) { - auto pos = it->first; - const auto &role = it->second; - - add_role(role.Name); - - if (pos_to_users.find(pos) == pos_to_users.end()) continue; - - auto &users = pos_to_users.at(pos); - AlphabeticalSort(users.begin(), users.end(), [](const auto &e) { return e.Username; }); - - for (const auto &data : users) - if (!add_user(data)) return; - } - - if (chan->Type == ChannelType::DM || chan->Type == ChannelType::GROUP_DM) - add_role("Users"); - else - add_role("@everyone"); - for (const auto &id : roleless_users) { - const auto user = discord.GetUser(id); - if (user.has_value()) - if (!add_user(*user)) return; - } -} - -void MemberList::AttachUserMenuHandler(Gtk::ListBoxRow *row, Snowflake id) { - row->signal_button_press_event().connect([this, row, id](GdkEventButton *e) -> bool { - if (e->type == GDK_BUTTON_PRESS && e->button == GDK_BUTTON_SECONDARY) { - Abaddon::Get().ShowUserMenu(reinterpret_cast(e), id, m_guild_id); - return true; - } - - return false; - }); -} diff --git a/components/memberlist.hpp b/components/memberlist.hpp deleted file mode 100644 index 60a25bc..0000000 --- a/components/memberlist.hpp +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include "discord/discord.hpp" - -class LazyImage; -class StatusIndicator; -class MemberListUserRow : public Gtk::ListBoxRow { -public: - MemberListUserRow(const std::optional &guild, const UserData &data); - - Snowflake ID; - -private: - Gtk::EventBox *m_ev; - Gtk::Box *m_box; - LazyImage *m_avatar; - StatusIndicator *m_status_indicator; - Gtk::Label *m_label; - Gtk::Image *m_crown = nullptr; -}; - -class MemberList { -public: - MemberList(); - Gtk::Widget *GetRoot() const; - - void UpdateMemberList(); - void Clear(); - void SetActiveChannel(Snowflake id); - -private: - void AttachUserMenuHandler(Gtk::ListBoxRow *row, Snowflake id); - - Gtk::ScrolledWindow *m_main; - Gtk::ListBox *m_listbox; - - Snowflake m_guild_id; - Snowflake m_chan_id; - - std::unordered_map m_id_to_row; -}; diff --git a/components/ratelimitindicator.cpp b/components/ratelimitindicator.cpp deleted file mode 100644 index ac4ef4b..0000000 --- a/components/ratelimitindicator.cpp +++ /dev/null @@ -1,137 +0,0 @@ -#include "ratelimitindicator.hpp" -#include "abaddon.hpp" -#include - -RateLimitIndicator::RateLimitIndicator() - : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) { - m_label.set_text(""); - m_label.set_ellipsize(Pango::ELLIPSIZE_START); - m_label.set_valign(Gtk::ALIGN_END); - get_style_context()->add_class("ratelimit-indicator"); - - m_img.set_margin_start(7); - - add(m_label); - add(m_img); - m_label.show(); - - const static auto clock_path = Abaddon::GetResPath("/clock.png"); - if (std::filesystem::exists(clock_path)) { - try { - const auto pixbuf = Gdk::Pixbuf::create_from_file(clock_path); - int w, h; - GetImageDimensions(pixbuf->get_width(), pixbuf->get_height(), w, h, 20, 10); - m_img.property_pixbuf() = pixbuf->scale_simple(w, h, Gdk::INTERP_BILINEAR); - } catch (...) {} - } - - Abaddon::Get().GetDiscordClient().signal_message_create().connect(sigc::mem_fun(*this, &RateLimitIndicator::OnMessageCreate)); - Abaddon::Get().GetDiscordClient().signal_message_send_fail().connect(sigc::mem_fun(*this, &RateLimitIndicator::OnMessageSendFail)); - Abaddon::Get().GetDiscordClient().signal_channel_update().connect(sigc::mem_fun(*this, &RateLimitIndicator::OnChannelUpdate)); -} - -void RateLimitIndicator::SetActiveChannel(Snowflake id) { - m_active_channel = id; - const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(m_active_channel); - if (channel.has_value() && channel->RateLimitPerUser.has_value()) - m_rate_limit = *channel->RateLimitPerUser; - else - m_rate_limit = 0; - - UpdateIndicator(); -} - -bool RateLimitIndicator::CanSpeak() const { - const auto rate_limit = GetRateLimit(); - if (rate_limit == 0) return true; - - const auto it = m_times.find(m_active_channel); - if (it == m_times.end()) - return true; - - const auto now = std::chrono::steady_clock::now(); - const auto sec_diff = std::chrono::duration_cast(it->second - now).count(); - return sec_diff <= 0; -} - -int RateLimitIndicator::GetTimeLeft() const { - if (CanSpeak()) return 0; - - auto it = m_times.find(m_active_channel); - if (it == m_times.end()) return 0; - - const auto now = std::chrono::steady_clock::now(); - const auto sec_diff = std::chrono::duration_cast(it->second - now).count(); - - if (sec_diff <= 0) - return 0; - else - return sec_diff; -} - -int RateLimitIndicator::GetRateLimit() const { - return m_rate_limit; -} - -bool RateLimitIndicator::UpdateIndicator() { - if (const auto rate_limit = GetRateLimit(); rate_limit != 0) { - m_img.show(); - - auto &discord = Abaddon::Get().GetDiscordClient(); - if (discord.HasAnyChannelPermission(discord.GetUserData().ID, m_active_channel, Permission::MANAGE_MESSAGES | Permission::MANAGE_CHANNELS)) { - m_label.set_text("You may bypass slowmode."); - set_has_tooltip(false); - } else { - const auto time_left = GetTimeLeft(); - if (time_left > 0) - m_label.set_text(std::to_string(time_left) + "s"); - else - m_label.set_text(""); - set_tooltip_text("Slowmode is enabled. Members can send one message every " + std::to_string(rate_limit) + " seconds."); - } - } else { - m_img.hide(); - - m_label.set_text(""); - set_has_tooltip(false); - } - - if (m_connection) - m_connection.disconnect(); - m_connection = Glib::signal_timeout().connect_seconds(sigc::mem_fun(*this, &RateLimitIndicator::UpdateIndicator), 1); - - return false; -} - -void RateLimitIndicator::OnMessageCreate(const Message &message) { - auto &discord = Abaddon::Get().GetDiscordClient(); - if (message.Author.ID != discord.GetUserData().ID) return; - if (!message.GuildID.has_value()) return; - const bool can_bypass = discord.HasAnyChannelPermission(discord.GetUserData().ID, m_active_channel, Permission::MANAGE_MESSAGES | Permission::MANAGE_CHANNELS); - const auto rate_limit = GetRateLimit(); - if (rate_limit > 0 && !can_bypass) { - m_times[message.ChannelID] = std::chrono::steady_clock::now() + std::chrono::duration(rate_limit + 1); - UpdateIndicator(); - } -} - -void RateLimitIndicator::OnMessageSendFail(const std::string &nonce, float retry_after) { - if (retry_after != 0) { // failed to rate limit - const auto msg = Abaddon::Get().GetDiscordClient().GetMessage(nonce); - const auto channel_id = msg->ChannelID; - m_times[channel_id] = std::chrono::steady_clock::now() + std::chrono::duration(std::lroundf(retry_after + 0.5f) + 1); // + 0.5 will ceil it - UpdateIndicator(); - } -} - -void RateLimitIndicator::OnChannelUpdate(Snowflake channel_id) { - if (channel_id != m_active_channel) return; - const auto chan = Abaddon::Get().GetDiscordClient().GetChannel(m_active_channel); - if (!chan.has_value()) return; - const auto r = chan->RateLimitPerUser; - if (r.has_value()) - m_rate_limit = *r; - else - m_rate_limit = 0; - UpdateIndicator(); -} diff --git a/components/ratelimitindicator.hpp b/components/ratelimitindicator.hpp deleted file mode 100644 index b4dbb69..0000000 --- a/components/ratelimitindicator.hpp +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once -#include -#include -#include -#include "discord/message.hpp" - -class RateLimitIndicator : public Gtk::Box { -public: - RateLimitIndicator(); - void SetActiveChannel(Snowflake id); - - // even tho this probably isnt the right place for this im gonna do it anyway to reduce coad - bool CanSpeak() const; - -private: - int GetTimeLeft() const; - int GetRateLimit() const; - bool UpdateIndicator(); - void OnMessageCreate(const Message &message); - void OnMessageSendFail(const std::string &nonce, float rate_limit); - void OnChannelUpdate(Snowflake channel_id); - - Gtk::Image m_img; - Gtk::Label m_label; - - sigc::connection m_connection; - - int m_rate_limit; - Snowflake m_active_channel; - std::unordered_map> m_times; // time point of when next message can be sent -}; diff --git a/components/statusindicator.cpp b/components/statusindicator.cpp deleted file mode 100644 index 42eb170..0000000 --- a/components/statusindicator.cpp +++ /dev/null @@ -1,130 +0,0 @@ -#include "statusindicator.hpp" -#include "abaddon.hpp" - -static const constexpr int Diameter = 8; -static const auto OnlineColor = Gdk::RGBA("#43B581"); -static const auto IdleColor = Gdk::RGBA("#FAA61A"); -static const auto DNDColor = Gdk::RGBA("#982929"); -static const auto OfflineColor = Gdk::RGBA("#808080"); - -StatusIndicator::StatusIndicator(Snowflake user_id) - : Glib::ObjectBase("statusindicator") - , Gtk::Widget() - , m_id(user_id) - , m_status(static_cast(-1)) { - set_has_window(true); - set_name("status-indicator"); - - get_style_context()->add_class("status-indicator"); - - Abaddon::Get().GetDiscordClient().signal_guild_member_list_update().connect(sigc::hide(sigc::mem_fun(*this, &StatusIndicator::CheckStatus))); - auto cb = [this](const UserData &user, PresenceStatus status) { - if (user.ID == m_id) CheckStatus(); - }; - Abaddon::Get().GetDiscordClient().signal_presence_update().connect(sigc::track_obj(cb, *this)); - - CheckStatus(); -} - -StatusIndicator::~StatusIndicator() { -} - -void StatusIndicator::CheckStatus() { - const auto status = Abaddon::Get().GetDiscordClient().GetUserStatus(m_id); - const auto last_status = m_status; - get_style_context()->remove_class("online"); - get_style_context()->remove_class("dnd"); - get_style_context()->remove_class("idle"); - get_style_context()->remove_class("offline"); - get_style_context()->add_class(GetPresenceString(status)); - m_status = status; - - if (last_status != m_status) - queue_draw(); -} - -Gtk::SizeRequestMode StatusIndicator::get_request_mode_vfunc() const { - return Gtk::Widget::get_request_mode_vfunc(); -} - -void StatusIndicator::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const { - minimum_width = 0; - natural_width = Diameter; -} - -void StatusIndicator::get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const { - minimum_height = 0; - natural_height = Diameter; -} - -void StatusIndicator::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const { - minimum_height = 0; - natural_height = Diameter; -} - -void StatusIndicator::get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const { - minimum_width = 0; - natural_width = Diameter; -} - -void StatusIndicator::on_size_allocate(Gtk::Allocation &allocation) { - set_allocation(allocation); - - if (m_window) - m_window->move_resize(allocation.get_x(), allocation.get_y(), allocation.get_width(), allocation.get_height()); -} - -void StatusIndicator::on_map() { - Gtk::Widget::on_map(); -} - -void StatusIndicator::on_unmap() { - Gtk::Widget::on_unmap(); -} - -void StatusIndicator::on_realize() { - set_realized(true); - - if (!m_window) { - GdkWindowAttr attributes; - std::memset(&attributes, 0, sizeof(attributes)); - - auto allocation = get_allocation(); - - attributes.x = allocation.get_x(); - attributes.y = allocation.get_y(); - attributes.width = allocation.get_width(); - attributes.height = allocation.get_height(); - - attributes.event_mask = get_events() | Gdk::EXPOSURE_MASK; - attributes.window_type = GDK_WINDOW_CHILD; - attributes.wclass = GDK_INPUT_OUTPUT; - - m_window = Gdk::Window::create(get_parent_window(), &attributes, GDK_WA_X | GDK_WA_Y); - set_window(m_window); - - m_window->set_user_data(gobj()); - } -} - -void StatusIndicator::on_unrealize() { - m_window.reset(); - - Gtk::Widget::on_unrealize(); -} - -bool StatusIndicator::on_draw(const Cairo::RefPtr &cr) { - const auto allocation = get_allocation(); - const auto width = allocation.get_width(); - const auto height = allocation.get_height(); - - const auto color = get_style_context()->get_color(Gtk::STATE_FLAG_NORMAL); - - cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue()); - cr->arc(width / 2, height / 2, width / 3, 0.0, 2 * (4 * std::atan(1))); - cr->close_path(); - cr->fill_preserve(); - cr->stroke(); - - return true; -} diff --git a/components/statusindicator.hpp b/components/statusindicator.hpp deleted file mode 100644 index b2cf0bd..0000000 --- a/components/statusindicator.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once -#include -#include "discord/snowflake.hpp" -#include "discord/activity.hpp" - -class StatusIndicator : public Gtk::Widget { -public: - StatusIndicator(Snowflake user_id); - virtual ~StatusIndicator(); - -protected: - Gtk::SizeRequestMode get_request_mode_vfunc() const override; - void get_preferred_width_vfunc(int &minimum_width, int &natural_width) const override; - void get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const override; - void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override; - void get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const override; - void on_size_allocate(Gtk::Allocation &allocation) override; - void on_map() override; - void on_unmap() override; - void on_realize() override; - void on_unrealize() override; - bool on_draw(const Cairo::RefPtr &cr) override; - - Glib::RefPtr m_window; - - void CheckStatus(); - - Snowflake m_id; - PresenceStatus m_status; -}; -- cgit v1.2.3