summaryrefslogtreecommitdiff
path: root/src/components/channellist
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/channellist')
-rw-r--r--src/components/channellist/cellrendererchannels.cpp873
-rw-r--r--src/components/channellist/cellrendererchannels.hpp177
-rw-r--r--src/components/channellist/channellist.cpp131
-rw-r--r--src/components/channellist/channellist.hpp71
-rw-r--r--src/components/channellist/channellisttree.cpp1527
-rw-r--r--src/components/channellist/channellisttree.hpp248
-rw-r--r--src/components/channellist/classic/guildlist.cpp177
-rw-r--r--src/components/channellist/classic/guildlist.hpp48
-rw-r--r--src/components/channellist/classic/guildlistfolderitem.cpp129
-rw-r--r--src/components/channellist/classic/guildlistfolderitem.hpp44
-rw-r--r--src/components/channellist/classic/guildlistguilditem.cpp52
-rw-r--r--src/components/channellist/classic/guildlistguilditem.hpp22
12 files changed, 3499 insertions, 0 deletions
diff --git a/src/components/channellist/cellrendererchannels.cpp b/src/components/channellist/cellrendererchannels.cpp
new file mode 100644
index 0000000..b049252
--- /dev/null
+++ b/src/components/channellist/cellrendererchannels.cpp
@@ -0,0 +1,873 @@
+#include "cellrendererchannels.hpp"
+#include <gdkmm/general.h>
+#include "abaddon.hpp"
+
+constexpr static int MentionsRightPad = 7;
+#ifndef M_PI
+constexpr static double M_PI = 3.14159265358979;
+#endif
+constexpr static double M_PI_H = M_PI / 2.0;
+constexpr static double M_PI_3_2 = M_PI * 3.0 / 2.0;
+
+void AddUnreadIndicator(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area) {
+ static const auto color_setting = Gdk::RGBA(Abaddon::Get().GetSettings().UnreadIndicatorColor);
+
+ const auto color = color_setting.get_alpha_u() > 0 ? color_setting : widget.get_style_context()->get_background_color(Gtk::STATE_FLAG_SELECTED);
+
+ cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue());
+ const auto x = background_area.get_x();
+ const auto y = background_area.get_y();
+ const auto w = background_area.get_width();
+ const auto h = background_area.get_height();
+ cr->rectangle(x, y, 3, h);
+ cr->fill();
+}
+
+void RenderExpander(int x_offset, const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, bool is_expanded) {
+ constexpr static int len = 5;
+ int x1, y1, x2, y2, x3, y3;
+ if (is_expanded) {
+ x1 = background_area.get_x() + x_offset;
+ y1 = background_area.get_y() + background_area.get_height() / 2 - len;
+ x2 = background_area.get_x() + x_offset + len;
+ y2 = background_area.get_y() + background_area.get_height() / 2 + len;
+ x3 = background_area.get_x() + x_offset + len * 2;
+ y3 = background_area.get_y() + background_area.get_height() / 2 - len;
+ } else {
+ x1 = background_area.get_x() + x_offset;
+ y1 = background_area.get_y() + background_area.get_height() / 2 - len;
+ x2 = background_area.get_x() + x_offset + len * 2;
+ y2 = background_area.get_y() + background_area.get_height() / 2;
+ x3 = background_area.get_x() + x_offset;
+ 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);
+ auto expander_color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelsExpanderColor);
+ if (expander_color.get_alpha_u() == 0) {
+ expander_color = widget.get_style_context()->get_background_color(Gtk::STATE_FLAG_SELECTED);
+ }
+ cr->set_source_rgb(expander_color.get_red(), expander_color.get_green(), expander_color.get_blue());
+ cr->stroke();
+}
+
+CellRendererChannels::CellRendererChannels()
+ : Glib::ObjectBase(typeid(CellRendererChannels))
+ , Gtk::CellRenderer()
+ , m_property_type(*this, "render-type")
+ , m_property_id(*this, "id")
+ , 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")
+ , m_property_color(*this, "color")
+ , m_property_voice_state(*this, "voice-state") {
+ 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;
+ });
+}
+
+Glib::PropertyProxy<RenderType> CellRendererChannels::property_type() {
+ return m_property_type.get_proxy();
+}
+
+Glib::PropertyProxy<uint64_t> CellRendererChannels::property_id() {
+ return m_property_id.get_proxy();
+}
+
+Glib::PropertyProxy<Glib::ustring> CellRendererChannels::property_name() {
+ return m_property_name.get_proxy();
+}
+
+Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> CellRendererChannels::property_icon() {
+ return m_property_pixbuf.get_proxy();
+}
+
+Glib::PropertyProxy<Glib::RefPtr<Gdk::PixbufAnimation>> CellRendererChannels::property_icon_animation() {
+ return m_property_pixbuf_animation.get_proxy();
+}
+
+Glib::PropertyProxy<bool> CellRendererChannels::property_expanded() {
+ return m_property_expanded.get_proxy();
+}
+
+Glib::PropertyProxy<bool> CellRendererChannels::property_nsfw() {
+ return m_property_nsfw.get_proxy();
+}
+
+Glib::PropertyProxy<std::optional<Gdk::RGBA>> CellRendererChannels::property_color() {
+ return m_property_color.get_proxy();
+}
+
+Glib::PropertyProxy<VoiceStateFlags> CellRendererChannels::property_voice_state() {
+ return m_property_voice_state.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::Folder:
+ return get_preferred_width_vfunc_folder(widget, minimum_width, natural_width);
+ 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);
+#ifdef WITH_VOICE
+ case RenderType::VoiceChannel:
+ return get_preferred_width_vfunc_voice_channel(widget, minimum_width, natural_width);
+ case RenderType::VoiceParticipant:
+ return get_preferred_width_vfunc_voice_participant(widget, minimum_width, natural_width);
+#endif
+ 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::Folder:
+ return get_preferred_width_for_height_vfunc_folder(widget, height, minimum_width, natural_width);
+ 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);
+#ifdef WITH_VOICE
+ case RenderType::VoiceChannel:
+ return get_preferred_width_for_height_vfunc_voice_channel(widget, height, minimum_width, natural_width);
+ case RenderType::VoiceParticipant:
+ return get_preferred_width_for_height_vfunc_voice_participant(widget, height, minimum_width, natural_width);
+#endif
+ 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::Folder:
+ return get_preferred_height_vfunc_folder(widget, minimum_height, natural_height);
+ 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);
+#ifdef WITH_VOICE
+ case RenderType::VoiceChannel:
+ return get_preferred_height_vfunc_voice_channel(widget, minimum_height, natural_height);
+ case RenderType::VoiceParticipant:
+ return get_preferred_height_vfunc_voice_participant(widget, minimum_height, natural_height);
+#endif
+ 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::Folder:
+ return get_preferred_height_for_width_vfunc_folder(widget, width, minimum_height, natural_height);
+ 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);
+#ifdef WITH_VOICE
+ case RenderType::VoiceChannel:
+ return get_preferred_height_for_width_vfunc_voice_channel(widget, width, minimum_height, natural_height);
+ case RenderType::VoiceParticipant:
+ return get_preferred_height_for_width_vfunc_voice_participant(widget, width, minimum_height, natural_height);
+#endif
+ 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<Cairo::Context> &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::Folder:
+ return render_vfunc_folder(cr, widget, background_area, cell_area, flags);
+ 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);
+#ifdef WITH_VOICE
+ case RenderType::VoiceChannel:
+ return render_vfunc_voice_channel(cr, widget, background_area, cell_area, flags);
+ case RenderType::VoiceParticipant:
+ return render_vfunc_voice_participant(cr, widget, background_area, cell_area, flags);
+#endif
+ 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);
+ }
+}
+
+// folder functions
+
+void CellRendererChannels::get_preferred_width_vfunc_folder(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_folder(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_folder(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_folder(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_folder(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
+ RenderExpander(7, cr, widget, background_area, property_expanded());
+ 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);
+
+ if (m_property_color.get_value().has_value()) {
+ m_renderer_text.property_foreground_rgba() = *m_property_color.get_value();
+ }
+ m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
+ m_renderer_text.property_foreground_set() = false;
+}
+
+// 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<Cairo::Context> &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() + 3;
+ 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(static_cast<int>(text_x),
+ static_cast<int>(text_y),
+ static_cast<int>(text_w),
+ static_cast<int>(text_h));
+
+ m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
+
+ const bool hover_only = Abaddon::Get().GetSettings().AnimatedGuildHoverOnly;
+ 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(
+ static_cast<int>(icon_x),
+ static_cast<int>(icon_y),
+ static_cast<int>(icon_w),
+ static_cast<int>(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();
+ }
+
+ // unread
+ if (!Abaddon::Get().GetSettings().Unreads) return;
+
+ const auto id = m_property_id.get_value();
+
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ int total_mentions;
+ const auto has_unread = discord.GetUnreadStateForGuild(id, total_mentions);
+
+ if (has_unread && !discord.IsGuildMuted(id)) {
+ auto area = background_area;
+ area.set_y(area.get_y() + area.get_height() / 2.0 - 24.0 / 2.0);
+ AddUnreadIndicator(cr, widget, area);
+ }
+
+ if (total_mentions < 1) return;
+ auto *paned = dynamic_cast<Gtk::Paned *>(widget.get_ancestor(Gtk::Paned::get_type()));
+ if (paned != nullptr) {
+ const auto edge = std::min(paned->get_position(), background_area.get_width());
+
+ unread_render_mentions(cr, widget, total_mentions, edge, background_area);
+ }
+}
+
+// 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<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
+ RenderExpander(7, cr, widget, background_area, property_expanded());
+
+ 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);
+
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ const auto id = m_property_id.get_value();
+ if (!discord.IsChannelMuted(m_property_id.get_value())) {
+ if (discord.GetUnreadChannelsCountForCategory(id) > 0) {
+ AddUnreadIndicator(cr, widget, background_area);
+ }
+ }
+ 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<Cairo::Context> &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);
+
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ const auto id = m_property_id.get_value();
+ const bool is_muted = discord.IsChannelMuted(id);
+
+ static const auto nsfw_color = Gdk::RGBA(Abaddon::Get().GetSettings().NSFWChannelColor);
+
+ auto color = widget.get_style_context()->get_color(Gtk::STATE_FLAG_NORMAL);
+ if (property_nsfw()) color = nsfw_color;
+ if (is_muted) color.set_alpha(0.6);
+
+ m_renderer_text.property_foreground_rgba() = color;
+ m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
+ m_renderer_text.property_foreground_set() = false;
+
+ // unread
+ if (!Abaddon::Get().GetSettings().Unreads) return;
+
+ const auto unread_state = discord.GetUnreadStateForChannel(id);
+ if (unread_state < 0) return;
+
+ if (!is_muted) {
+ AddUnreadIndicator(cr, widget, background_area);
+ }
+
+ if (unread_state < 1) return;
+ auto *paned = dynamic_cast<Gtk::Paned *>(widget.get_ancestor(Gtk::Paned::get_type()));
+ if (paned != nullptr) {
+ const auto edge = std::min(paned->get_position(), cell_area.get_width());
+
+ unread_render_mentions(cr, widget, unread_state, edge, cell_area);
+ }
+}
+
+// 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<Cairo::Context> &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);
+
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ const auto id = m_property_id.get_value();
+ const bool is_muted = discord.IsChannelMuted(id);
+
+ m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
+
+ // unread
+ if (!Abaddon::Get().GetSettings().Unreads) return;
+
+ const auto unread_state = discord.GetUnreadStateForChannel(id);
+ if (unread_state < 0) return;
+
+ if (!is_muted) {
+ AddUnreadIndicator(cr, widget, background_area);
+ }
+
+ if (unread_state < 1) return;
+ auto *paned = dynamic_cast<Gtk::Paned *>(widget.get_ancestor(Gtk::Paned::get_type()));
+ if (paned != nullptr) {
+ const auto edge = std::min(paned->get_position(), cell_area.get_width());
+
+ unread_render_mentions(cr, widget, unread_state, edge, cell_area);
+ }
+}
+
+#ifdef WITH_VOICE
+
+// voice channel
+
+void CellRendererChannels::get_preferred_width_vfunc_voice_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_voice_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_voice_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_voice_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_voice_channel(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
+ // channel name text
+ 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() + 35;
+ 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);
+
+ // speaker character
+ Pango::FontDescription font;
+ font.set_family("sans 14");
+
+ auto layout = widget.create_pango_layout("\U0001F50A");
+ layout->set_font_description(font);
+ layout->set_alignment(Pango::ALIGN_LEFT);
+ cr->set_source_rgba(1.0, 1.0, 1.0, 1.0);
+ int width, height;
+ layout->get_pixel_size(width, height);
+ cr->move_to(
+ background_area.get_x() + 1,
+ cell_area.get_y() + cell_area.get_height() / 2.0 - height / 2.0);
+ layout->show_in_cairo_context(cr);
+
+ RenderExpander(24, cr, widget, background_area, property_expanded());
+}
+
+// voice participant
+
+void CellRendererChannels::get_preferred_width_vfunc_voice_participant(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_voice_participant(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_voice_participant(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_voice_participant(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_voice_participant(const Cairo::RefPtr<Cairo::Context> &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 = 0;
+ int pixbuf_h = 0;
+
+ 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() + 28;
+ 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.property_scale() = Pango::SCALE_SMALL;
+ m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
+ m_renderer_text.property_scale_set() = false;
+
+ 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();
+ }
+
+ auto *paned = dynamic_cast<Gtk::Paned *>(widget.get_ancestor(Gtk::Paned::get_type()));
+ if (paned != nullptr) {
+ const auto edge = std::min(paned->get_position(), background_area.get_width());
+
+ const static std::array<std::pair<VoiceStateFlags, Glib::ustring>, 3> icon_order = { {
+ { VoiceStateFlags::SelfMute | VoiceStateFlags::Mute, "microphone-disabled-symbolic" },
+ { VoiceStateFlags::SelfDeaf | VoiceStateFlags::Deaf, "audio-volume-muted-symbolic" },
+ { VoiceStateFlags::SelfVideo, "camera-web-symbolic" },
+ } };
+
+ constexpr static int IconSize = 18;
+ constexpr static int IconPad = 2;
+
+ const VoiceStateFlags voice_flags = m_property_voice_state.get_value();
+
+ int offset = 0;
+ for (auto iter = icon_order.rbegin(); iter != icon_order.rend(); iter++) {
+ const auto &[flag, icon] = *iter;
+ if ((voice_flags & flag) == VoiceStateFlags::Clear) continue;
+
+ const double icon_w = 18;
+ const double icon_h = 18;
+ const double icon_x = background_area.get_x() + edge - icon_w + offset;
+ const double icon_y = background_area.get_y() + background_area.get_height() / 2 - icon_h / 2;
+ Gdk::Rectangle icon_cell_area(icon_x, icon_y, icon_w, icon_h);
+
+ offset -= (IconSize + IconPad);
+
+ const bool is_server_mute = (voice_flags & VoiceStateFlags::Mute) == VoiceStateFlags::Mute;
+ const bool is_server_deaf = (voice_flags & VoiceStateFlags::Deaf) == VoiceStateFlags::Deaf;
+ auto context = widget.get_style_context();
+ if (is_server_mute || is_server_deaf) {
+ context->context_save();
+ context->add_class("voice-state-server");
+ }
+
+ m_renderer_pixbuf.property_icon_name() = icon;
+ m_renderer_pixbuf.render(cr, widget, background_area, icon_cell_area, flags);
+
+ if (is_server_mute || is_server_deaf) {
+ context->context_restore();
+ }
+ }
+ }
+}
+
+#endif
+
+// 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<Cairo::Context> &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);
+
+ if (!Abaddon::Get().GetSettings().Unreads) return;
+
+ auto *paned = dynamic_cast<Gtk::Paned *>(widget.get_ancestor(Gtk::Paned::get_type()));
+ if (paned != nullptr) {
+ const auto edge = std::min(paned->get_position(), background_area.get_width());
+ if (const auto unread = Abaddon::Get().GetDiscordClient().GetUnreadDMsCount(); unread > 0)
+ unread_render_mentions(cr, widget, unread, edge, background_area);
+ }
+}
+
+// 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<Cairo::Context> &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() + 3;
+ 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 + 6.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(static_cast<int>(text_x),
+ static_cast<int>(text_y),
+ static_cast<int>(text_w),
+ static_cast<int>(text_h));
+
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ const auto id = m_property_id.get_value();
+ const bool is_muted = discord.IsChannelMuted(id);
+
+ 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();
+
+ // unread
+ if (!Abaddon::Get().GetSettings().Unreads) return;
+
+ const auto unread_state = discord.GetUnreadStateForChannel(id);
+ if (unread_state < 0) return;
+
+ if (!is_muted) {
+ AddUnreadIndicator(cr, widget, background_area);
+ }
+}
+
+void CellRendererChannels::cairo_path_rounded_rect(const Cairo::RefPtr<Cairo::Context> &cr, double x, double y, double w, double h, double r) {
+ const double degrees = M_PI / 180.0;
+
+ cr->begin_new_sub_path();
+ cr->arc(x + w - r, y + r, r, -M_PI_H, 0);
+ cr->arc(x + w - r, y + h - r, r, 0, M_PI_H);
+ cr->arc(x + r, y + h - r, r, M_PI_H, M_PI);
+ cr->arc(x + r, y + r, r, M_PI, M_PI_3_2);
+ cr->close_path();
+}
+
+void CellRendererChannels::unread_render_mentions(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, int mentions, int edge, const Gdk::Rectangle &cell_area) {
+ Pango::FontDescription font;
+ font.set_family("sans 14");
+ // font.set_weight(Pango::WEIGHT_BOLD);
+
+ auto layout = widget.create_pango_layout(std::to_string(mentions));
+ layout->set_font_description(font);
+ layout->set_alignment(Pango::ALIGN_RIGHT);
+
+ int width, height;
+ layout->get_pixel_size(width, height);
+ {
+ static const auto badge_setting = Gdk::RGBA(Abaddon::Get().GetSettings().MentionBadgeColor);
+ static const auto text_setting = Gdk::RGBA(Abaddon::Get().GetSettings().MentionBadgeTextColor);
+
+ auto bg = badge_setting.get_alpha_u() > 0 ? badge_setting : widget.get_style_context()->get_background_color(Gtk::STATE_FLAG_SELECTED);
+ auto text = text_setting.get_alpha_u() > 0 ? text_setting : widget.get_style_context()->get_color(Gtk::STATE_FLAG_SELECTED);
+
+ const auto x = cell_area.get_x() + edge - width - MentionsRightPad;
+ const auto y = cell_area.get_y() + cell_area.get_height() / 2.0 - height / 2.0 - 1;
+ cairo_path_rounded_rect(cr, x - 4, y + 2, width + 8, height, 5);
+ cr->set_source_rgb(bg.get_red(), bg.get_green(), bg.get_blue());
+ cr->fill();
+ cr->set_source_rgb(text.get_red(), text.get_green(), text.get_blue());
+ cr->move_to(x, y);
+ layout->show_in_cairo_context(cr);
+ }
+}
diff --git a/src/components/channellist/cellrendererchannels.hpp b/src/components/channellist/cellrendererchannels.hpp
new file mode 100644
index 0000000..e142b2a
--- /dev/null
+++ b/src/components/channellist/cellrendererchannels.hpp
@@ -0,0 +1,177 @@
+#pragma once
+#include <map>
+#include <optional>
+#include <gdkmm/pixbufanimation.h>
+#include <glibmm/property.h>
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/cellrenderertext.h>
+#include "discord/snowflake.hpp"
+#include "discord/voicestateflags.hpp"
+#include "misc/bitwise.hpp"
+
+enum class RenderType : uint8_t {
+ Folder,
+ Guild,
+ Category,
+ TextChannel,
+ Thread,
+
+// TODO: maybe enable anyways but without ability to join if no voice support
+#ifdef WITH_VOICE
+ VoiceChannel,
+ VoiceParticipant,
+#endif
+
+ DMHeader,
+ DM,
+};
+
+class CellRendererChannels : public Gtk::CellRenderer {
+public:
+ CellRendererChannels();
+ ~CellRendererChannels() override = default;
+
+ Glib::PropertyProxy<RenderType> property_type();
+ Glib::PropertyProxy<uint64_t> property_id();
+ Glib::PropertyProxy<Glib::ustring> property_name();
+ Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> property_icon();
+ Glib::PropertyProxy<Glib::RefPtr<Gdk::PixbufAnimation>> property_icon_animation();
+ Glib::PropertyProxy<bool> property_expanded();
+ Glib::PropertyProxy<bool> property_nsfw();
+ Glib::PropertyProxy<std::optional<Gdk::RGBA>> property_color();
+ Glib::PropertyProxy<VoiceStateFlags> property_voice_state();
+
+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<Cairo::Context> &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_folder(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
+ void get_preferred_width_for_height_vfunc_folder(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
+ void get_preferred_height_vfunc_folder(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
+ void get_preferred_height_for_width_vfunc_folder(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
+ void render_vfunc_folder(const Cairo::RefPtr<Cairo::Context> &cr,
+ Gtk::Widget &widget,
+ const Gdk::Rectangle &background_area,
+ const Gdk::Rectangle &cell_area,
+ Gtk::CellRendererState flags);
+
+ // 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<Cairo::Context> &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<Cairo::Context> &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<Cairo::Context> &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<Cairo::Context> &cr,
+ Gtk::Widget &widget,
+ const Gdk::Rectangle &background_area,
+ const Gdk::Rectangle &cell_area,
+ Gtk::CellRendererState flags);
+
+#ifdef WITH_VOICE
+ // voice channel
+ void get_preferred_width_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
+ void get_preferred_width_for_height_vfunc_voice_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
+ void get_preferred_height_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
+ void get_preferred_height_for_width_vfunc_voice_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
+ void render_vfunc_voice_channel(const Cairo::RefPtr<Cairo::Context> &cr,
+ Gtk::Widget &widget,
+ const Gdk::Rectangle &background_area,
+ const Gdk::Rectangle &cell_area,
+ Gtk::CellRendererState flags);
+
+ // voice participant
+ void get_preferred_width_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
+ void get_preferred_width_for_height_vfunc_voice_participant(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
+ void get_preferred_height_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
+ void get_preferred_height_for_width_vfunc_voice_participant(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
+ void render_vfunc_voice_participant(const Cairo::RefPtr<Cairo::Context> &cr,
+ Gtk::Widget &widget,
+ const Gdk::Rectangle &background_area,
+ const Gdk::Rectangle &cell_area,
+ Gtk::CellRendererState flags);
+#endif
+
+ // 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<Cairo::Context> &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<Cairo::Context> &cr,
+ Gtk::Widget &widget,
+ const Gdk::Rectangle &background_area,
+ const Gdk::Rectangle &cell_area,
+ Gtk::CellRendererState flags);
+
+ static void cairo_path_rounded_rect(const Cairo::RefPtr<Cairo::Context> &cr, double x, double y, double w, double h, double r);
+ static void unread_render_mentions(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, int mentions, int edge, const Gdk::Rectangle &cell_area);
+
+private:
+ Gtk::CellRendererText m_renderer_text;
+ Gtk::CellRendererPixbuf m_renderer_pixbuf;
+
+ Glib::Property<RenderType> m_property_type; // all
+ Glib::Property<Glib::ustring> m_property_name; // all
+ Glib::Property<uint64_t> m_property_id;
+ Glib::Property<Glib::RefPtr<Gdk::Pixbuf>> m_property_pixbuf; // guild, dm
+ Glib::Property<Glib::RefPtr<Gdk::PixbufAnimation>> m_property_pixbuf_animation; // guild
+ Glib::Property<bool> m_property_expanded; // category
+ Glib::Property<bool> m_property_nsfw; // channel
+ Glib::Property<std::optional<Gdk::RGBA>> m_property_color; // folder
+ Glib::Property<VoiceStateFlags> m_property_voice_state;
+
+ // 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<Gdk::PixbufAnimation>, Glib::RefPtr<Gdk::PixbufAnimationIter>> m_pixbuf_anim_iters;
+};
diff --git a/src/components/channellist/channellist.cpp b/src/components/channellist/channellist.cpp
new file mode 100644
index 0000000..f409592
--- /dev/null
+++ b/src/components/channellist/channellist.cpp
@@ -0,0 +1,131 @@
+#include "channellist.hpp"
+
+#include "abaddon.hpp"
+
+ChannelList::ChannelList() {
+ get_style_context()->add_class("channel-browser-pane");
+
+ ConnectSignals();
+
+ m_guilds.set_halign(Gtk::ALIGN_START);
+
+ m_guilds_scroll.get_style_context()->add_class("guild-list-scroll");
+ m_guilds_scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+
+ m_guilds.signal_guild_selected().connect([this](Snowflake guild_id) {
+ m_tree.SetSelectedGuild(guild_id);
+ });
+
+ m_guilds.signal_dms_selected().connect([this]() {
+ m_tree.SetSelectedDMs();
+ });
+
+ m_guilds.show();
+ m_tree.show();
+ m_guilds_scroll.add(m_guilds);
+ pack_start(m_guilds_scroll, false, false); // only take the space it needs
+ pack_start(m_tree, true, true); // use all the remaining space
+}
+
+void ChannelList::UpdateListing() {
+ m_tree.UpdateListing();
+ if (m_is_classic) m_guilds.UpdateListing();
+}
+
+void ChannelList::SetActiveChannel(Snowflake id, bool expand_to) {
+ if (Abaddon::Get().GetSettings().ClassicChangeGuildOnOpen) {
+ if (const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); channel.has_value() && channel->GuildID.has_value()) {
+ m_tree.SetSelectedGuild(*channel->GuildID);
+ } else {
+ m_tree.SetSelectedDMs();
+ }
+ }
+
+ m_tree.SetActiveChannel(id, expand_to);
+}
+
+void ChannelList::UseExpansionState(const ExpansionStateRoot &state) {
+ m_tree.UseExpansionState(state);
+}
+
+ExpansionStateRoot ChannelList::GetExpansionState() const {
+ return m_tree.GetExpansionState();
+}
+
+void ChannelList::UsePanedHack(Gtk::Paned &paned) {
+ m_tree.UsePanedHack(paned);
+}
+
+void ChannelList::SetClassic(bool value) {
+ m_is_classic = value;
+ m_tree.SetClassic(value);
+ m_guilds_scroll.set_visible(value);
+}
+
+void ChannelList::ConnectSignals() {
+ // TODO: if these all just travel upwards to the singleton then get rid of them but mayeb they dont
+
+#ifdef WITH_LIBHANDY
+ m_tree.signal_action_open_new_tab().connect([this](Snowflake id) {
+ m_signal_action_open_new_tab.emit(id);
+ });
+#endif
+
+#ifdef WITH_VOICE
+ m_tree.signal_action_join_voice_channel().connect([this](Snowflake id) {
+ m_signal_action_join_voice_channel.emit(id);
+ });
+
+ m_tree.signal_action_disconnect_voice().connect([this]() {
+ m_signal_action_disconnect_voice.emit();
+ });
+#endif
+
+ m_tree.signal_action_channel_item_select().connect([this](Snowflake id) {
+ m_signal_action_channel_item_select.emit(id);
+ });
+
+ m_tree.signal_action_guild_leave().connect([this](Snowflake id) {
+ m_signal_action_guild_leave.emit(id);
+ });
+
+ m_tree.signal_action_guild_settings().connect([this](Snowflake id) {
+ m_signal_action_guild_settings.emit(id);
+ });
+
+ m_guilds.signal_action_guild_leave().connect([this](Snowflake id) {
+ m_signal_action_guild_leave.emit(id);
+ });
+
+ m_guilds.signal_action_guild_settings().connect([this](Snowflake id) {
+ m_signal_action_guild_settings.emit(id);
+ });
+}
+
+#ifdef WITH_LIBHANDY
+ChannelList::type_signal_action_open_new_tab ChannelList::signal_action_open_new_tab() {
+ return m_signal_action_open_new_tab;
+}
+#endif
+
+#ifdef WITH_VOICE
+ChannelList::type_signal_action_join_voice_channel ChannelList::signal_action_join_voice_channel() {
+ return m_signal_action_join_voice_channel;
+}
+
+ChannelList::type_signal_action_disconnect_voice ChannelList::signal_action_disconnect_voice() {
+ return m_signal_action_disconnect_voice;
+}
+#endif
+
+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;
+}
diff --git a/src/components/channellist/channellist.hpp b/src/components/channellist/channellist.hpp
new file mode 100644
index 0000000..78d6372
--- /dev/null
+++ b/src/components/channellist/channellist.hpp
@@ -0,0 +1,71 @@
+#pragma once
+#include <gtkmm/box.h>
+#include <gtkmm/paned.h>
+#include "channellisttree.hpp"
+#include "classic/guildlist.hpp"
+#include "discord/snowflake.hpp"
+#include "state.hpp"
+
+// Contains the actual ChannelListTree and the classic listing if enabled
+class ChannelList : public Gtk::HBox {
+ // have to proxy public and signals to underlying tree... ew!!!
+public:
+ ChannelList();
+
+ void UpdateListing();
+ void SetActiveChannel(Snowflake id, bool expand_to);
+
+ // channel list should be populated when this is called
+ void UseExpansionState(const ExpansionStateRoot &state);
+ ExpansionStateRoot GetExpansionState() const;
+
+ void UsePanedHack(Gtk::Paned &paned);
+
+ void SetClassic(bool value);
+
+private:
+ void ConnectSignals();
+
+ ChannelListTree m_tree;
+
+ Gtk::ScrolledWindow m_guilds_scroll;
+ GuildList m_guilds;
+
+ bool m_is_classic = false;
+
+public:
+ using type_signal_action_channel_item_select = sigc::signal<void, Snowflake>;
+ using type_signal_action_guild_leave = sigc::signal<void, Snowflake>;
+ using type_signal_action_guild_settings = sigc::signal<void, Snowflake>;
+
+#ifdef WITH_LIBHANDY
+ using type_signal_action_open_new_tab = sigc::signal<void, Snowflake>;
+ type_signal_action_open_new_tab signal_action_open_new_tab();
+#endif
+
+#ifdef WITH_VOICE
+ using type_signal_action_join_voice_channel = sigc::signal<void, Snowflake>;
+ using type_signal_action_disconnect_voice = sigc::signal<void>;
+
+ type_signal_action_join_voice_channel signal_action_join_voice_channel();
+ type_signal_action_disconnect_voice signal_action_disconnect_voice();
+#endif
+
+ 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();
+
+private:
+ 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;
+
+#ifdef WITH_LIBHANDY
+ type_signal_action_open_new_tab m_signal_action_open_new_tab;
+#endif
+
+#ifdef WITH_VOICE
+ type_signal_action_join_voice_channel m_signal_action_join_voice_channel;
+ type_signal_action_disconnect_voice m_signal_action_disconnect_voice;
+#endif
+};
diff --git a/src/components/channellist/channellisttree.cpp b/src/components/channellist/channellisttree.cpp
new file mode 100644
index 0000000..4816b42
--- /dev/null
+++ b/src/components/channellist/channellisttree.cpp
@@ -0,0 +1,1527 @@
+#include "channellisttree.hpp"
+
+#include <algorithm>
+#include <map>
+#include <unordered_map>
+
+#include <gtkmm/main.h>
+
+#include "abaddon.hpp"
+#include "imgmanager.hpp"
+#include "util.hpp"
+
+ChannelListTree::ChannelListTree()
+ : Glib::ObjectBase(typeid(ChannelListTree))
+ , m_model(Gtk::TreeStore::create(m_columns))
+ , m_filter_model(Gtk::TreeModelFilter::create(m_model))
+ , m_sort_model(Gtk::TreeModelSort::create(m_filter_model))
+ , m_menu_guild_copy_id("_Copy ID", true)
+ , m_menu_guild_settings("View _Settings", true)
+ , m_menu_guild_leave("_Leave", true)
+ , m_menu_guild_mark_as_read("Mark as _Read", true)
+ , m_menu_category_copy_id("_Copy ID", true)
+ , m_menu_channel_copy_id("_Copy ID", true)
+ , m_menu_channel_mark_as_read("Mark as _Read", true)
+#ifdef WITH_LIBHANDY
+ , m_menu_channel_open_tab("Open in New _Tab", true)
+ , m_menu_dm_open_tab("Open in New _Tab", true)
+#endif
+#ifdef WITH_VOICE
+ , m_menu_voice_channel_join("_Join", true)
+ , m_menu_voice_channel_disconnect("_Disconnect", true)
+#endif
+ , m_menu_dm_copy_id("_Copy ID", true)
+ , m_menu_dm_close("") // changes depending on if group or not
+#ifdef WITH_VOICE
+ , m_menu_dm_join_voice("Join _Voice", true)
+ , m_menu_dm_disconnect_voice("_Disconnect Voice", true)
+#endif
+ , 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)
+ , m_menu_thread_mark_as_read("Mark as _Read", true) {
+ get_style_context()->add_class("channel-list");
+
+ // Filter iters
+ const auto cb = [this](const Gtk::TreeModel::Path &path, Gtk::TreeViewColumn *column) {
+ auto view_path = ConvertViewPathToModel(path);
+ if (!view_path) return;
+ auto row = *m_model->get_iter(view_path);
+ if (!row) return;
+ 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 && type != RenderType::DM) {
+ 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) {
+ const auto id = static_cast<Snowflake>(row[m_columns.m_id]);
+ m_signal_action_channel_item_select.emit(id);
+ Abaddon::Get().GetDiscordClient().MarkChannelAsRead(id, [](...) {});
+ }
+ };
+ m_view.signal_row_activated().connect(cb, false);
+ m_view.signal_row_collapsed().connect(sigc::mem_fun(*this, &ChannelListTree::OnRowCollapsed), false);
+ m_view.signal_row_expanded().connect(sigc::mem_fun(*this, &ChannelListTree::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, &ChannelListTree::SelectionFunc));
+ m_view.signal_button_press_event().connect(sigc::mem_fun(*this, &ChannelListTree::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_sort_model);
+ m_sort_model->set_sort_column(m_columns.m_sort, Gtk::SORT_ASCENDING);
+ m_sort_model->set_sort_func(m_columns.m_sort, sigc::mem_fun(*this, &ChannelListTree::SortFunc));
+
+ 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]) {
+ if (const auto view_path = ConvertModelPathToView(m_model->get_path(parent))) {
+ m_view.expand_row(view_path, false);
+ }
+ }
+ });
+
+ m_filter_model->set_visible_func([this](const Gtk::TreeModel::const_iterator &iter) -> bool {
+ if (!m_classic || m_updating_listing) return true;
+
+ const RenderType type = (*iter)[m_columns.m_type];
+
+ if (m_classic_selected_dms) {
+ if (iter->parent()) return true;
+ return type == RenderType::DMHeader;
+ }
+
+ if (type == RenderType::Guild) {
+ return (*iter)[m_columns.m_id] == m_classic_selected_guild;
+ }
+ return type != RenderType::DMHeader;
+ });
+
+ 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_id(), m_columns.m_id);
+ column->add_attribute(renderer->property_expanded(), m_columns.m_expanded);
+ column->add_attribute(renderer->property_nsfw(), m_columns.m_nsfw);
+ column->add_attribute(renderer->property_color(), m_columns.m_color);
+ column->add_attribute(renderer->property_voice_state(), m_columns.m_voice_flags);
+ 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<Snowflake>((*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<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
+ });
+ m_menu_guild_mark_as_read.signal_activate().connect([this] {
+ Abaddon::Get().GetDiscordClient().MarkGuildAsRead(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {});
+ });
+ m_menu_guild_toggle_mute.signal_activate().connect([this] {
+ const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ if (discord.IsGuildMuted(id))
+ discord.UnmuteGuild(id, NOOP_CALLBACK);
+ else
+ discord.MuteGuild(id, NOOP_CALLBACK);
+ });
+ m_menu_guild.append(m_menu_guild_mark_as_read);
+ m_menu_guild.append(m_menu_guild_settings);
+ m_menu_guild.append(m_menu_guild_leave);
+ m_menu_guild.append(m_menu_guild_toggle_mute);
+ m_menu_guild.append(m_menu_guild_copy_id);
+ 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_toggle_mute.signal_activate().connect([this] {
+ const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ if (discord.IsChannelMuted(id))
+ discord.UnmuteChannel(id, NOOP_CALLBACK);
+ else
+ discord.MuteChannel(id, NOOP_CALLBACK);
+ });
+ m_menu_category.append(m_menu_category_toggle_mute);
+ 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_mark_as_read.signal_activate().connect([this] {
+ Abaddon::Get().GetDiscordClient().MarkChannelAsRead(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {});
+ });
+ m_menu_channel_toggle_mute.signal_activate().connect([this] {
+ const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ if (discord.IsChannelMuted(id))
+ discord.UnmuteChannel(id, NOOP_CALLBACK);
+ else
+ discord.MuteChannel(id, NOOP_CALLBACK);
+ });
+
+#ifdef WITH_LIBHANDY
+ m_menu_channel_open_tab.signal_activate().connect([this] {
+ const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
+ m_signal_action_open_new_tab.emit(id);
+ });
+ m_menu_channel.append(m_menu_channel_open_tab);
+#endif
+
+ m_menu_channel.append(m_menu_channel_mark_as_read);
+ m_menu_channel.append(m_menu_channel_toggle_mute);
+ m_menu_channel.append(m_menu_channel_copy_id);
+ m_menu_channel.show_all();
+
+#ifdef WITH_VOICE
+ m_menu_voice_channel_join.signal_activate().connect([this]() {
+ const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
+ m_signal_action_join_voice_channel.emit(id);
+ });
+
+ m_menu_voice_channel_disconnect.signal_activate().connect([this]() {
+ m_signal_action_disconnect_voice.emit();
+ });
+
+ m_menu_voice_channel.append(m_menu_voice_channel_join);
+ m_menu_voice_channel.append(m_menu_voice_channel_disconnect);
+ m_menu_voice_channel.show_all();
+#endif
+
+ 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<Snowflake>((*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_toggle_mute.signal_activate().connect([this] {
+ const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ if (discord.IsChannelMuted(id))
+ discord.UnmuteChannel(id, NOOP_CALLBACK);
+ else
+ discord.MuteChannel(id, NOOP_CALLBACK);
+ });
+#ifdef WITH_LIBHANDY
+ m_menu_dm_open_tab.signal_activate().connect([this] {
+ const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
+ m_signal_action_open_new_tab.emit(id);
+ });
+ m_menu_dm.append(m_menu_dm_open_tab);
+#endif
+ m_menu_dm.append(m_menu_dm_toggle_mute);
+ m_menu_dm.append(m_menu_dm_close);
+#ifdef WITH_VOICE
+ m_menu_dm_join_voice.signal_activate().connect([this]() {
+ const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
+ m_signal_action_join_voice_channel.emit(id);
+ });
+ m_menu_dm_disconnect_voice.signal_activate().connect([this]() {
+ m_signal_action_disconnect_voice.emit();
+ });
+ m_menu_dm.append(m_menu_dm_join_voice);
+ m_menu_dm.append(m_menu_dm_disconnect_voice);
+#endif
+ m_menu_dm.append(m_menu_dm_copy_id);
+ 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<Snowflake>((*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<Snowflake>((*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<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {});
+ });
+ m_menu_thread_mark_as_read.signal_activate().connect([this] {
+ Abaddon::Get().GetDiscordClient().MarkChannelAsRead(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), NOOP_CALLBACK);
+ });
+ m_menu_thread_toggle_mute.signal_activate().connect([this] {
+ const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ if (discord.IsChannelMuted(id))
+ discord.UnmuteThread(id, NOOP_CALLBACK);
+ else
+ discord.MuteThread(id, NOOP_CALLBACK);
+ });
+ m_menu_thread.append(m_menu_thread_mark_as_read);
+ m_menu_thread.append(m_menu_thread_toggle_mute);
+ 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.append(m_menu_thread_copy_id);
+ m_menu_thread.show_all();
+
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ discord.signal_message_create().connect(sigc::mem_fun(*this, &ChannelListTree::OnMessageCreate));
+ discord.signal_guild_create().connect(sigc::mem_fun(*this, &ChannelListTree::UpdateNewGuild));
+ discord.signal_guild_delete().connect(sigc::mem_fun(*this, &ChannelListTree::UpdateRemoveGuild));
+ discord.signal_channel_delete().connect(sigc::mem_fun(*this, &ChannelListTree::UpdateRemoveChannel));
+ discord.signal_channel_update().connect(sigc::mem_fun(*this, &ChannelListTree::UpdateChannel));
+ discord.signal_channel_create().connect(sigc::mem_fun(*this, &ChannelListTree::UpdateCreateChannel));
+ discord.signal_thread_delete().connect(sigc::mem_fun(*this, &ChannelListTree::OnThreadDelete));
+ discord.signal_thread_update().connect(sigc::mem_fun(*this, &ChannelListTree::OnThreadUpdate));
+ discord.signal_thread_list_sync().connect(sigc::mem_fun(*this, &ChannelListTree::OnThreadListSync));
+ discord.signal_added_to_thread().connect(sigc::mem_fun(*this, &ChannelListTree::OnThreadJoined));
+ discord.signal_removed_from_thread().connect(sigc::mem_fun(*this, &ChannelListTree::OnThreadRemoved));
+ discord.signal_guild_update().connect(sigc::mem_fun(*this, &ChannelListTree::UpdateGuild));
+ discord.signal_message_ack().connect(sigc::mem_fun(*this, &ChannelListTree::OnMessageAck));
+ discord.signal_channel_muted().connect(sigc::mem_fun(*this, &ChannelListTree::OnChannelMute));
+ discord.signal_channel_unmuted().connect(sigc::mem_fun(*this, &ChannelListTree::OnChannelUnmute));
+ discord.signal_guild_muted().connect(sigc::mem_fun(*this, &ChannelListTree::OnGuildMute));
+ discord.signal_guild_unmuted().connect(sigc::mem_fun(*this, &ChannelListTree::OnGuildUnmute));
+
+#if WITH_VOICE
+ discord.signal_voice_user_connect().connect(sigc::mem_fun(*this, &ChannelListTree::OnVoiceUserConnect));
+ discord.signal_voice_user_disconnect().connect(sigc::mem_fun(*this, &ChannelListTree::OnVoiceUserDisconnect));
+ discord.signal_voice_state_set().connect(sigc::mem_fun(*this, &ChannelListTree::OnVoiceStateSet));
+#endif
+}
+
+void ChannelListTree::UsePanedHack(Gtk::Paned &paned) {
+ paned.property_position().signal_changed().connect(sigc::mem_fun(*this, &ChannelListTree::OnPanedPositionChanged));
+}
+
+void ChannelListTree::SetClassic(bool value) {
+ m_classic = value;
+ m_filter_model->refilter();
+}
+
+void ChannelListTree::SetSelectedGuild(Snowflake guild_id) {
+ m_classic_selected_guild = guild_id;
+ m_classic_selected_dms = false;
+ m_filter_model->refilter();
+ auto guild_iter = GetIteratorForGuildFromID(guild_id);
+ if (guild_iter) {
+ if (auto view_iter = ConvertModelIterToView(guild_iter)) {
+ m_view.expand_row(GetViewPathFromViewIter(view_iter), false);
+ }
+ }
+}
+
+void ChannelListTree::SetSelectedDMs() {
+ m_classic_selected_dms = true;
+ m_filter_model->refilter();
+ if (m_dm_header) {
+ if (auto view_path = ConvertModelPathToView(m_dm_header)) {
+ m_view.expand_row(view_path, false);
+ }
+ }
+}
+
+int ChannelListTree::SortFunc(const Gtk::TreeModel::iterator &a, const Gtk::TreeModel::iterator &b) {
+ const RenderType a_type = (*a)[m_columns.m_type];
+ const RenderType b_type = (*b)[m_columns.m_type];
+ const int64_t a_sort = (*a)[m_columns.m_sort];
+ const int64_t b_sort = (*b)[m_columns.m_sort];
+ if (a_type == RenderType::DMHeader) return -1;
+ if (b_type == RenderType::DMHeader) return 1;
+#ifdef WITH_VOICE
+ if (a_type == RenderType::TextChannel && b_type == RenderType::VoiceChannel) return -1;
+ if (b_type == RenderType::TextChannel && a_type == RenderType::VoiceChannel) return 1;
+#endif
+ return static_cast<int>(std::clamp(a_sort - b_sort, int64_t(-1), int64_t(1)));
+}
+
+void ChannelListTree::OnPanedPositionChanged() {
+ m_view.queue_draw();
+}
+
+void ChannelListTree::UpdateListingClassic() {
+ m_updating_listing = true;
+
+ // refilter so every row is visible
+ // otherwise clear() causes a CRITICAL assert in a slot for the filter model
+ m_filter_model->refilter();
+ m_model->clear();
+
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ const auto guild_ids = discord.GetUserSortedGuilds();
+ for (const auto guild_id : guild_ids) {
+ if (const auto guild = discord.GetGuild(guild_id); guild.has_value()) {
+ AddGuild(*guild, m_model->children());
+ }
+ }
+
+ m_updating_listing = false;
+
+ AddPrivateChannels();
+}
+
+void ChannelListTree::UpdateListing() {
+ if (m_classic) {
+ UpdateListingClassic();
+ return;
+ }
+
+ m_updating_listing = true;
+
+ m_model->clear();
+
+ auto &discord = Abaddon::Get().GetDiscordClient();
+
+ /*
+ guild_folders looks something like this
+ "guild_folders": [
+ {
+ "color": null,
+ "guild_ids": [
+ "8009060___________"
+ ],
+ "id": null,
+ "name": null
+ },
+ {
+ "color": null,
+ "guild_ids": [
+ "99615594__________",
+ "86132141__________",
+ "35450138__________",
+ "83714048__________"
+ ],
+ "id": 2853066769,
+ "name": null
+ }
+ ]
+
+ so if id != null then its a folder (they can have single entries)
+ */
+
+ int sort_value = 0;
+
+ const auto folders = discord.GetUserSettings().GuildFolders;
+ const auto guild_ids = discord.GetUserSortedGuilds();
+
+ // user_settings.guild_folders may not contain every guild the user is in
+ // this seems to be the case if you organize your guilds and join a server without further organization
+ // so add guilds not present in guild_folders by descending id order first
+
+ std::set<Snowflake> foldered_guilds;
+ for (const auto &group : folders) {
+ foldered_guilds.insert(group.GuildIDs.begin(), group.GuildIDs.end());
+ }
+
+ for (auto iter = guild_ids.rbegin(); iter != guild_ids.rend(); iter++) {
+ if (foldered_guilds.find(*iter) == foldered_guilds.end()) {
+ const auto guild = discord.GetGuild(*iter);
+ if (!guild.has_value()) continue;
+ auto tree_iter = AddGuild(*guild, m_model->children());
+ if (tree_iter) (*tree_iter)[m_columns.m_sort] = sort_value++;
+ }
+ }
+
+ // then whatever is in folders
+
+ for (const auto &group : folders) {
+ auto iter = AddFolder(group);
+ if (iter) (*iter)[m_columns.m_sort] = sort_value++;
+ }
+
+ m_updating_listing = false;
+
+ AddPrivateChannels();
+}
+
+// TODO update for folders
+void ChannelListTree::UpdateNewGuild(const GuildData &guild) {
+ AddGuild(guild, m_model->children());
+ // 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 ChannelListTree::UpdateRemoveGuild(Snowflake id) {
+ auto iter = GetIteratorForGuildFromID(id);
+ if (!iter) return;
+ m_model->erase(iter);
+}
+
+void ChannelListTree::UpdateRemoveChannel(Snowflake id) {
+ auto iter = GetIteratorForRowFromID(id);
+ if (!iter) return;
+ m_model->erase(iter);
+}
+
+void ChannelListTree::UpdateChannel(Snowflake id) {
+ auto iter = GetIteratorForRowFromID(id);
+ auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id);
+ if (!iter || !channel.has_value()) return;
+ if (channel->Type == ChannelType::GUILD_CATEGORY) return UpdateChannelCategory(*channel);
+ if (!channel->IsText()) 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 = GetIteratorForRowFromID(*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 ChannelListTree::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 = GetIteratorForRowFromID(*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 ChannelListTree::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;
+
+ (*iter)[m_columns.m_name] = "<b>" + Glib::Markup::escape_text(guild->Name) + "</b>";
+ (*iter)[m_columns.m_icon] = img.GetPlaceholder(GuildIconSize);
+ if (Abaddon::Get().GetSettings().ShowAnimations && guild->HasAnimatedIcon()) {
+ const auto cb = [this, id](const Glib::RefPtr<Gdk::PixbufAnimation> &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<Gdk::Pixbuf> &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 ChannelListTree::OnThreadJoined(Snowflake id) {
+ if (GetIteratorForRowFromID(id)) return;
+ const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id);
+ if (!channel.has_value()) return;
+ const auto parent = GetIteratorForRowFromID(*channel->ParentID);
+ if (parent)
+ CreateThreadRow(parent->children(), *channel);
+}
+
+void ChannelListTree::OnThreadRemoved(Snowflake id) {
+ DeleteThreadRow(id);
+}
+
+void ChannelListTree::OnThreadDelete(const ThreadDeleteData &data) {
+ DeleteThreadRow(data.ID);
+}
+
+// todo probably make the row stick around if its selected until the selection changes
+void ChannelListTree::OnThreadUpdate(const ThreadUpdateData &data) {
+ auto iter = GetIteratorForRowFromID(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 ChannelListTree::OnThreadListSync(const ThreadListSyncData &data) {
+ // get the threads in the guild
+ std::vector<Snowflake> threads;
+ auto guild_iter = GetIteratorForGuildFromID(data.GuildID);
+ if (!guild_iter) return;
+
+ std::queue<Gtk::TreeModel::iterator> 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<Snowflake>((*item)[m_columns.m_id]));
+ for (const 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 = GetIteratorForRowFromID(thread_id);
+ m_model->erase(iter);
+ }
+ }
+
+ // delete all archived threads
+ for (auto thread : data.Threads) {
+ if (thread.ThreadMetadata->IsArchived) {
+ if (auto iter = GetIteratorForRowFromID(thread.ID))
+ m_model->erase(iter);
+ }
+ }
+}
+
+#ifdef WITH_VOICE
+void ChannelListTree::OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id) {
+ auto parent_iter = GetIteratorForRowFromIDOfType(channel_id, RenderType::VoiceChannel);
+ if (!parent_iter) parent_iter = GetIteratorForRowFromIDOfType(channel_id, RenderType::DM);
+ if (!parent_iter) return;
+ const auto user = Abaddon::Get().GetDiscordClient().GetUser(user_id);
+ if (!user.has_value()) return;
+
+ CreateVoiceParticipantRow(*user, parent_iter->children());
+}
+
+void ChannelListTree::OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id) {
+ if (auto iter = GetIteratorForRowFromIDOfType(user_id, RenderType::VoiceParticipant)) {
+ m_model->erase(iter);
+ }
+}
+
+void ChannelListTree::OnVoiceStateSet(Snowflake user_id, Snowflake channel_id, VoiceStateFlags flags) {
+ if (auto iter = GetIteratorForRowFromIDOfType(user_id, RenderType::VoiceParticipant)) {
+ (*iter)[m_columns.m_voice_flags] = flags;
+ }
+}
+#endif
+
+void ChannelListTree::DeleteThreadRow(Snowflake id) {
+ auto iter = GetIteratorForRowFromID(id);
+ if (iter)
+ m_model->erase(iter);
+}
+
+void ChannelListTree::OnChannelMute(Snowflake id) {
+ if (auto iter = GetIteratorForRowFromID(id))
+ m_model->row_changed(m_model->get_path(iter), iter);
+}
+
+void ChannelListTree::OnChannelUnmute(Snowflake id) {
+ if (auto iter = GetIteratorForRowFromID(id))
+ m_model->row_changed(m_model->get_path(iter), iter);
+}
+
+void ChannelListTree::OnGuildMute(Snowflake id) {
+ if (auto iter = GetIteratorForGuildFromID(id))
+ m_model->row_changed(m_model->get_path(iter), iter);
+}
+
+void ChannelListTree::OnGuildUnmute(Snowflake id) {
+ if (auto iter = GetIteratorForGuildFromID(id))
+ m_model->row_changed(m_model->get_path(iter), 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 ChannelListTree::SetActiveChannel(Snowflake id, bool expand_to) {
+ while (Gtk::Main::events_pending()) Gtk::Main::iteration();
+
+ // mark channel as read when switching off
+ if (m_active_channel.IsValid())
+ Abaddon::Get().GetDiscordClient().MarkChannelAsRead(m_active_channel, [](...) {});
+
+ m_active_channel = id;
+
+ if (m_temporary_thread_row) {
+ const auto thread_id = static_cast<Snowflake>((*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 = GetIteratorForRowFromID(id);
+ if (channel_iter) {
+ m_view.get_selection()->unselect_all();
+ const auto view_iter = ConvertModelIterToView(channel_iter);
+ if (view_iter) {
+ if (expand_to) {
+ m_view.expand_to_path(GetViewPathFromViewIter(view_iter));
+ }
+ m_view.get_selection()->select(view_iter);
+ }
+ } else {
+ m_view.get_selection()->unselect_all();
+ const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id);
+ if (!channel.has_value() || !channel->IsThread()) return;
+ auto parent_iter = GetIteratorForRowFromID(*channel->ParentID);
+ if (!parent_iter) return;
+ m_temporary_thread_row = CreateThreadRow(parent_iter->children(), *channel);
+ const auto view_iter = ConvertModelIterToView(m_temporary_thread_row);
+ if (view_iter) {
+ m_view.get_selection()->select(view_iter);
+ }
+ }
+}
+
+void ChannelListTree::UseExpansionState(const ExpansionStateRoot &root) {
+ m_updating_listing = true;
+ m_filter_model->refilter();
+
+ auto recurse = [this](auto &self, const ExpansionStateRoot &root) -> void {
+ for (const auto &[id, state] : root.Children) {
+ Gtk::TreeModel::iterator row_iter;
+ if (const auto map_iter = m_tmp_row_map.find(id); map_iter != m_tmp_row_map.end()) {
+ row_iter = map_iter->second;
+ } else if (const auto map_iter = m_tmp_guild_row_map.find(id); map_iter != m_tmp_guild_row_map.end()) {
+ row_iter = map_iter->second;
+ }
+
+ if (row_iter) {
+ (*row_iter)[m_columns.m_expanded] = state.IsExpanded;
+ auto view_iter = ConvertModelIterToView(row_iter);
+ if (view_iter) {
+ if (state.IsExpanded) {
+ m_view.expand_row(GetViewPathFromViewIter(view_iter), false);
+ } else {
+ m_view.collapse_row(GetViewPathFromViewIter(view_iter));
+ }
+ }
+ }
+
+ self(self, state.Children);
+ }
+ };
+
+ for (const auto &[id, state] : root.Children) {
+ if (const auto iter = GetIteratorForTopLevelFromID(id)) {
+ (*iter)[m_columns.m_expanded] = state.IsExpanded;
+ auto view_iter = ConvertModelIterToView(iter);
+ if (view_iter) {
+ if (state.IsExpanded) {
+ m_view.expand_row(GetViewPathFromViewIter(view_iter), false);
+ } else {
+ m_view.collapse_row(GetViewPathFromViewIter(view_iter));
+ }
+ }
+ }
+
+ recurse(recurse, state.Children);
+ }
+
+ m_updating_listing = false;
+ m_filter_model->refilter();
+
+ m_tmp_row_map.clear();
+ m_tmp_guild_row_map.clear();
+}
+
+ExpansionStateRoot ChannelListTree::GetExpansionState() const {
+ ExpansionStateRoot r;
+
+ auto recurse = [this](auto &self, const Gtk::TreeRow &row) -> ExpansionState {
+ ExpansionState r;
+
+ r.IsExpanded = row[m_columns.m_expanded];
+ for (auto child : row.children()) {
+ r.Children.Children[static_cast<Snowflake>(child[m_columns.m_id])] = self(self, child);
+ }
+
+ return r;
+ };
+
+ for (auto child : m_model->children()) {
+ const auto id = static_cast<Snowflake>(child[m_columns.m_id]);
+ if (static_cast<uint64_t>(id) == 0ULL) continue; // dont save DM header
+ r.Children[id] = recurse(recurse, child);
+ }
+
+ return r;
+}
+
+Gtk::TreePath ChannelListTree::ConvertModelPathToView(const Gtk::TreePath &path) {
+ if (const auto filter_path = m_filter_model->convert_child_path_to_path(path)) {
+ if (const auto sort_path = m_sort_model->convert_child_path_to_path(filter_path)) {
+ return sort_path;
+ }
+ }
+
+ return {};
+}
+
+Gtk::TreeIter ChannelListTree::ConvertModelIterToView(const Gtk::TreeIter &iter) {
+ if (const auto filter_iter = m_filter_model->convert_child_iter_to_iter(iter)) {
+ if (const auto sort_iter = m_sort_model->convert_child_iter_to_iter(filter_iter)) {
+ return sort_iter;
+ }
+ }
+
+ return {};
+}
+
+Gtk::TreePath ChannelListTree::ConvertViewPathToModel(const Gtk::TreePath &path) {
+ if (const auto filter_path = m_sort_model->convert_path_to_child_path(path)) {
+ if (const auto model_path = m_filter_model->convert_path_to_child_path(filter_path)) {
+ return model_path;
+ }
+ }
+
+ return {};
+}
+
+Gtk::TreeIter ChannelListTree::ConvertViewIterToModel(const Gtk::TreeIter &iter) {
+ if (const auto filter_iter = m_sort_model->convert_iter_to_child_iter(iter)) {
+ if (const auto model_iter = m_filter_model->convert_iter_to_child_iter(filter_iter)) {
+ return model_iter;
+ }
+ }
+
+ return {};
+}
+
+Gtk::TreePath ChannelListTree::GetViewPathFromViewIter(const Gtk::TreeIter &iter) {
+ return m_sort_model->get_path(iter);
+}
+
+Gtk::TreeModel::iterator ChannelListTree::AddFolder(const UserSettingsGuildFoldersEntry &folder) {
+ if (!folder.ID.has_value()) {
+ // just a guild
+ if (!folder.GuildIDs.empty()) {
+ const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(folder.GuildIDs[0]);
+ if (guild.has_value()) {
+ return AddGuild(*guild, m_model->children());
+ }
+ }
+ } else {
+ auto folder_row = *m_model->append();
+ folder_row[m_columns.m_type] = RenderType::Folder;
+ folder_row[m_columns.m_id] = *folder.ID;
+ m_tmp_row_map[*folder.ID] = folder_row;
+ if (folder.Name.has_value()) {
+ folder_row[m_columns.m_name] = Glib::Markup::escape_text(*folder.Name);
+ } else {
+ folder_row[m_columns.m_name] = "Folder";
+ }
+ if (folder.Color.has_value()) {
+ folder_row[m_columns.m_color] = IntToRGBA(*folder.Color);
+ }
+
+ int sort_value = 0;
+ for (const auto &guild_id : folder.GuildIDs) {
+ const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(guild_id);
+ if (guild.has_value()) {
+ auto guild_row = AddGuild(*guild, folder_row->children());
+ (*guild_row)[m_columns.m_sort] = sort_value++;
+ }
+ }
+
+ return folder_row;
+ }
+
+ return {};
+}
+
+Gtk::TreeModel::iterator ChannelListTree::AddGuild(const GuildData &guild, const Gtk::TreeNodeChildren &root) {
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ auto &img = Abaddon::Get().GetImageManager();
+
+ auto guild_row = *m_model->append(root);
+ guild_row[m_columns.m_type] = RenderType::Guild;
+ guild_row[m_columns.m_id] = guild.ID;
+ guild_row[m_columns.m_name] = "<b>" + Glib::Markup::escape_text(guild.Name) + "</b>";
+ guild_row[m_columns.m_icon] = img.GetPlaceholder(GuildIconSize);
+ m_tmp_guild_row_map[guild.ID] = guild_row;
+
+ if (Abaddon::Get().GetSettings().ShowAnimations && guild.HasAnimatedIcon()) {
+ const auto cb = [this, id = guild.ID](const Glib::RefPtr<Gdk::PixbufAnimation> &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<Gdk::Pixbuf> &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<ChannelData> orphan_channels;
+ std::map<Snowflake, std::vector<ChannelData>> categories;
+
+ for (const auto &channel_ : *guild.Channels) {
+ const auto channel = discord.GetChannel(channel_.ID);
+ if (!channel.has_value()) continue;
+#ifdef WITH_VOICE
+ if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS || channel->Type == ChannelType::GUILD_VOICE) {
+#else
+ if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS) {
+#endif
+ 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<Snowflake, std::vector<ChannelData>> 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, const 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);
+ }
+ };
+
+#ifdef WITH_VOICE
+ auto add_voice_participants = [this, &discord](const ChannelData &channel, const Gtk::TreeNodeChildren &root) {
+ for (auto user_id : discord.GetUsersInVoiceChannel(channel.ID)) {
+ if (const auto user = discord.GetUser(user_id); user.has_value()) {
+ CreateVoiceParticipantRow(*user, root);
+ }
+ }
+ };
+#endif
+
+ for (const auto &channel : orphan_channels) {
+ auto channel_row = *m_model->append(guild_row.children());
+ if (IsTextChannel(channel.Type)) {
+ channel_row[m_columns.m_type] = RenderType::TextChannel;
+ channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name);
+ }
+#ifdef WITH_VOICE
+ else {
+ channel_row[m_columns.m_type] = RenderType::VoiceChannel;
+ channel_row[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name);
+ add_voice_participants(channel, channel_row->children());
+ }
+#endif
+ channel_row[m_columns.m_id] = channel.ID;
+ channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset;
+ channel_row[m_columns.m_nsfw] = channel.NSFW();
+ add_threads(channel, channel_row);
+ m_tmp_row_map[channel.ID] = 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_tmp_row_map[category_id] = cat_row;
+ // 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());
+ if (IsTextChannel(channel.Type)) {
+ channel_row[m_columns.m_type] = RenderType::TextChannel;
+ channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name);
+ }
+#ifdef WITH_VOICE
+ else {
+ channel_row[m_columns.m_type] = RenderType::VoiceChannel;
+ channel_row[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name);
+ add_voice_participants(channel, channel_row->children());
+ }
+#endif
+ channel_row[m_columns.m_id] = channel.ID;
+ channel_row[m_columns.m_sort] = *channel.Position;
+ channel_row[m_columns.m_nsfw] = channel.NSFW();
+ add_threads(channel, channel_row);
+ m_tmp_row_map[channel.ID] = channel_row;
+ }
+ }
+
+ return guild_row;
+}
+
+Gtk::TreeModel::iterator ChannelListTree::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 ChannelListTree::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] = static_cast<int64_t>(channel.ID);
+ thread_row[m_columns.m_nsfw] = false;
+
+ return thread_iter;
+}
+
+#ifdef WITH_VOICE
+Gtk::TreeModel::iterator ChannelListTree::CreateVoiceParticipantRow(const UserData &user, const Gtk::TreeNodeChildren &parent) {
+ auto row = *m_model->append(parent);
+ row[m_columns.m_type] = RenderType::VoiceParticipant;
+ row[m_columns.m_id] = user.ID;
+ row[m_columns.m_name] = user.GetDisplayNameEscaped();
+
+ const auto voice_state = Abaddon::Get().GetDiscordClient().GetVoiceState(user.ID);
+ if (voice_state.has_value()) {
+ row[m_columns.m_voice_flags] = voice_state->second;
+ }
+
+ auto &img = Abaddon::Get().GetImageManager();
+ row[m_columns.m_icon] = img.GetPlaceholder(VoiceParticipantIconSize);
+ const auto cb = [this, user_id = user.ID](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
+ auto iter = GetIteratorForRowFromIDOfType(user_id, RenderType::VoiceParticipant);
+ if (iter) (*iter)[m_columns.m_icon] = pb->scale_simple(VoiceParticipantIconSize, VoiceParticipantIconSize, Gdk::INTERP_BILINEAR);
+ };
+ img.LoadFromURL(user.GetAvatarURL("png", "32"), sigc::track_obj(cb, *this));
+
+ return row;
+}
+#endif
+
+void ChannelListTree::UpdateChannelCategory(const ChannelData &channel) {
+ auto iter = GetIteratorForRowFromID(channel.ID);
+ if (!iter) return;
+
+ (*iter)[m_columns.m_sort] = *channel.Position;
+ (*iter)[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name);
+}
+
+// todo this all needs refactoring for shooore
+Gtk::TreeModel::iterator ChannelListTree::GetIteratorForTopLevelFromID(Snowflake id) {
+ for (const auto &child : m_model->children()) {
+ if ((child[m_columns.m_type] == RenderType::Guild || child[m_columns.m_type] == RenderType::Folder) && child[m_columns.m_id] == id) {
+ return child;
+ } else if (child[m_columns.m_type] == RenderType::Folder) {
+ for (const auto &folder_child : child->children()) {
+ if (folder_child[m_columns.m_id] == id) {
+ return folder_child;
+ }
+ }
+ }
+ }
+ return {};
+}
+
+Gtk::TreeModel::iterator ChannelListTree::GetIteratorForGuildFromID(Snowflake id) {
+ for (const auto &child : m_model->children()) {
+ if (child[m_columns.m_type] == RenderType::Guild && child[m_columns.m_id] == id) {
+ return child;
+ } else if (child[m_columns.m_type] == RenderType::Folder) {
+ for (const auto &folder_child : child->children()) {
+ if (folder_child[m_columns.m_id] == id) {
+ return folder_child;
+ }
+ }
+ }
+ }
+ return {};
+}
+
+Gtk::TreeModel::iterator ChannelListTree::GetIteratorForRowFromID(Snowflake id) {
+ std::queue<Gtk::TreeModel::iterator> 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 && (*item)[m_columns.m_type] != RenderType::Guild) return item;
+ for (const auto &child : item->children())
+ queue.push(child);
+ queue.pop();
+ }
+
+ return {};
+}
+
+Gtk::TreeModel::iterator ChannelListTree::GetIteratorForRowFromIDOfType(Snowflake id, RenderType type) {
+ std::queue<Gtk::TreeModel::iterator> 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_type] == type && (*item)[m_columns.m_id] == id) return item;
+ for (const auto &child : item->children())
+ queue.push(child);
+ queue.pop();
+ }
+
+ return {};
+}
+
+bool ChannelListTree::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 ChannelListTree::OnRowCollapsed(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) const {
+ (*iter)[m_columns.m_expanded] = false;
+}
+
+void ChannelListTree::OnRowExpanded(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) {
+ // restore previous expansion
+ auto model_iter = ConvertViewIterToModel(iter);
+ for (auto it = model_iter->children().begin(); it != model_iter->children().end(); it++) {
+ if ((*it)[m_columns.m_expanded]) {
+ m_view.expand_row(GetViewPathFromViewIter(ConvertModelIterToView(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);
+ }
+
+ (*model_iter)[m_columns.m_expanded] = true;
+}
+
+bool ChannelListTree::SelectionFunc(const Glib::RefPtr<Gtk::TreeModel> &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 = GetViewPathFromViewIter(row);
+ }
+ }
+
+ auto type = (*model->get_iter(path))[m_columns.m_type];
+ return type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread;
+}
+
+void ChannelListTree::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] = "<b>Direct Messages</b>";
+ 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<UserData> top_recipient;
+ const auto recipients = dm->GetDMRecipients();
+ if (!recipients.empty())
+ 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_name] = Glib::Markup::escape_text(dm->GetDisplayName());
+ row[m_columns.m_sort] = static_cast<int64_t>(-(dm->LastMessageID.has_value() ? *dm->LastMessageID : dm_id));
+ row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize);
+ row[m_columns.m_expanded] = true;
+
+#ifdef WITH_VOICE
+ for (auto user_id : discord.GetUsersInVoiceChannel(dm_id)) {
+ if (const auto user = discord.GetUser(user_id); user.has_value()) {
+ CreateVoiceParticipantRow(*user, row->children());
+ }
+ }
+#endif
+
+ SetDMChannelIcon(iter, *dm);
+ }
+}
+
+void ChannelListTree::UpdateCreateDMChannel(const ChannelData &dm) {
+ auto header_row = m_model->get_iter(m_dm_header);
+ auto &img = Abaddon::Get().GetImageManager();
+
+ 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_name] = Glib::Markup::escape_text(dm.GetDisplayName());
+ row[m_columns.m_sort] = static_cast<int64_t>(-(dm.LastMessageID.has_value() ? *dm.LastMessageID : dm.ID));
+ row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize);
+
+ SetDMChannelIcon(iter, dm);
+}
+
+void ChannelListTree::SetDMChannelIcon(Gtk::TreeIter iter, const ChannelData &dm) {
+ auto &img = Abaddon::Get().GetImageManager();
+
+ std::optional<UserData> top_recipient;
+ const auto recipients = dm.GetDMRecipients();
+ if (!recipients.empty())
+ top_recipient = recipients[0];
+
+ if (dm.HasIcon()) {
+ const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
+ if (iter)
+ (*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR);
+ };
+ img.LoadFromURL(dm.GetIconURL(), sigc::track_obj(cb, *this));
+ } else if (dm.Type == ChannelType::DM && top_recipient.has_value()) {
+ const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &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));
+ } else { // GROUP_DM
+ std::string hash;
+ switch (dm.ID.GetUnixMilliseconds() % 8) {
+ case 0:
+ hash = "ee9275c5a437f7dc7f9430ba95f12ebd";
+ break;
+ case 1:
+ hash = "9baf45aac2a0ec2e2dab288333acb9d9";
+ break;
+ case 2:
+ hash = "7ba11ffb1900fa2b088cb31324242047";
+ break;
+ case 3:
+ hash = "f90fca70610c4898bc57b58bce92f587";
+ break;
+ case 4:
+ hash = "e2779af34b8d9126b77420e5f09213ce";
+ break;
+ case 5:
+ hash = "c6851bd0b03f1cca5a8c1e720ea6ea17";
+ break;
+ case 6:
+ hash = "f7e38ac976a2a696161c923502a8345b";
+ break;
+ case 7:
+ default:
+ hash = "3cb840d03313467838d658bbec801fcd";
+ break;
+ }
+ const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
+ if (iter)
+ (*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR);
+ };
+ img.LoadFromURL("https://discord.com/assets/" + hash + ".png", sigc::track_obj(cb, *this));
+ }
+}
+
+void ChannelListTree::RedrawUnreadIndicatorsForChannel(const ChannelData &channel) {
+ if (channel.GuildID.has_value()) {
+ auto iter = GetIteratorForGuildFromID(*channel.GuildID);
+ if (iter) m_model->row_changed(m_model->get_path(iter), iter);
+ }
+ if (channel.ParentID.has_value()) {
+ auto iter = GetIteratorForRowFromIDOfType(*channel.ParentID, RenderType::Category);
+ if (iter) m_model->row_changed(m_model->get_path(iter), iter);
+ }
+}
+
+void ChannelListTree::OnMessageAck(const MessageAckData &data) {
+ // trick renderer into redrawing
+ m_model->row_changed(Gtk::TreeModel::Path("0"), m_model->get_iter("0")); // 0 is always path for dm header
+ auto iter = GetIteratorForRowFromID(data.ChannelID);
+ if (iter) m_model->row_changed(m_model->get_path(iter), iter);
+ auto channel = Abaddon::Get().GetDiscordClient().GetChannel(data.ChannelID);
+ if (channel.has_value()) {
+ RedrawUnreadIndicatorsForChannel(*channel);
+ }
+}
+
+void ChannelListTree::OnMessageCreate(const Message &msg) {
+ auto iter = GetIteratorForRowFromID(msg.ChannelID);
+ if (iter) m_model->row_changed(m_model->get_path(iter), iter); // redraw
+ const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(msg.ChannelID);
+ if (!channel.has_value()) return;
+ if (channel->Type == ChannelType::DM || channel->Type == ChannelType::GROUP_DM) {
+ if (iter)
+ (*iter)[m_columns.m_sort] = static_cast<int64_t>(-msg.ID);
+ }
+ RedrawUnreadIndicatorsForChannel(*channel);
+}
+
+bool ChannelListTree::OnButtonPressEvent(GdkEventButton *ev) {
+ if (ev->button == GDK_BUTTON_SECONDARY && ev->type == GDK_BUTTON_PRESS) {
+ if (m_view.get_path_at_pos(static_cast<int>(ev->x), static_cast<int>(ev->y), m_path_for_menu)) {
+ m_path_for_menu = m_filter_model->convert_path_to_child_path(m_sort_model->convert_path_to_child_path(m_path_for_menu));
+ if (!m_path_for_menu) return true;
+ auto row = (*m_model->get_iter(m_path_for_menu));
+ switch (static_cast<RenderType>(row[m_columns.m_type])) {
+ case RenderType::Guild:
+ OnGuildSubmenuPopup();
+ m_menu_guild.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
+ break;
+ case RenderType::Category:
+ OnCategorySubmenuPopup();
+ m_menu_category.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
+ break;
+ case RenderType::TextChannel:
+ OnChannelSubmenuPopup();
+ m_menu_channel.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
+ break;
+#ifdef WITH_VOICE
+ case RenderType::VoiceChannel:
+ OnVoiceChannelSubmenuPopup();
+ m_menu_voice_channel.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
+ break;
+#endif
+ case RenderType::DM: {
+ OnDMSubmenuPopup();
+ const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(static_cast<Snowflake>(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<GdkEvent *>(ev));
+ } break;
+ case RenderType::Thread: {
+ OnThreadSubmenuPopup();
+ m_menu_thread.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
+ break;
+ } break;
+ default:
+ break;
+ }
+ }
+ return true;
+ }
+ return false;
+}
+
+void ChannelListTree::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<decltype(m_columns.name)::ElementType>((*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);
+ M(m_color);
+#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<Gtk::TreeRow>(tmp.begin(), tmp.end());
+ for (size_t i = 0; i < children.size(); i++)
+ MoveRow(children[i], row);
+
+ // delete original
+ m_model->erase(iter);
+}
+
+void ChannelListTree::OnGuildSubmenuPopup() {
+ const auto iter = m_model->get_iter(m_path_for_menu);
+ if (!iter) return;
+ const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ if (discord.IsGuildMuted(id))
+ m_menu_guild_toggle_mute.set_label("Unmute");
+ else
+ m_menu_guild_toggle_mute.set_label("Mute");
+
+ const auto guild = discord.GetGuild(id);
+ const auto self_id = discord.GetUserData().ID;
+ m_menu_guild_leave.set_sensitive(!(guild.has_value() && guild->OwnerID == self_id));
+}
+
+void ChannelListTree::OnCategorySubmenuPopup() {
+ const auto iter = m_model->get_iter(m_path_for_menu);
+ if (!iter) return;
+ const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
+ if (Abaddon::Get().GetDiscordClient().IsChannelMuted(id))
+ m_menu_category_toggle_mute.set_label("Unmute");
+ else
+ m_menu_category_toggle_mute.set_label("Mute");
+}
+
+void ChannelListTree::OnChannelSubmenuPopup() {
+ const auto iter = m_model->get_iter(m_path_for_menu);
+ if (!iter) return;
+ const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
+ auto &discord = Abaddon::Get().GetDiscordClient();
+#ifdef WITH_LIBHANDY
+ const auto perms = discord.HasChannelPermission(discord.GetUserData().ID, id, Permission::VIEW_CHANNEL);
+ m_menu_channel_open_tab.set_sensitive(perms);
+#endif
+ if (discord.IsChannelMuted(id))
+ m_menu_channel_toggle_mute.set_label("Unmute");
+ else
+ m_menu_channel_toggle_mute.set_label("Mute");
+}
+
+#ifdef WITH_VOICE
+void ChannelListTree::OnVoiceChannelSubmenuPopup() {
+ const auto iter = m_model->get_iter(m_path_for_menu);
+ if (!iter) return;
+ const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ if (discord.IsVoiceConnected() || discord.IsVoiceConnecting()) {
+ m_menu_voice_channel_join.set_sensitive(false);
+ m_menu_voice_channel_disconnect.set_sensitive(discord.GetVoiceChannelID() == id);
+ } else {
+ m_menu_voice_channel_join.set_sensitive(true);
+ m_menu_voice_channel_disconnect.set_sensitive(false);
+ }
+}
+#endif
+
+void ChannelListTree::OnDMSubmenuPopup() {
+ auto iter = m_model->get_iter(m_path_for_menu);
+ if (!iter) return;
+ const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ if (discord.IsChannelMuted(id))
+ m_menu_dm_toggle_mute.set_label("Unmute");
+ else
+ m_menu_dm_toggle_mute.set_label("Mute");
+
+#ifdef WITH_VOICE
+ if (discord.IsVoiceConnected() || discord.IsVoiceConnecting()) {
+ m_menu_dm_join_voice.set_sensitive(false);
+ m_menu_dm_disconnect_voice.set_sensitive(discord.GetVoiceChannelID() == id);
+ } else {
+ m_menu_dm_join_voice.set_sensitive(true);
+ m_menu_dm_disconnect_voice.set_sensitive(false);
+ }
+#endif
+}
+
+void ChannelListTree::OnThreadSubmenuPopup() {
+ 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;
+ const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
+
+ if (discord.IsChannelMuted(id))
+ m_menu_thread_toggle_mute.set_label("Unmute");
+ else
+ m_menu_thread_toggle_mute.set_label("Mute");
+
+ auto channel = discord.GetChannel(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);
+}
+
+ChannelListTree::type_signal_action_channel_item_select ChannelListTree::signal_action_channel_item_select() {
+ return m_signal_action_channel_item_select;
+}
+
+ChannelListTree::type_signal_action_guild_leave ChannelListTree::signal_action_guild_leave() {
+ return m_signal_action_guild_leave;
+}
+
+ChannelListTree::type_signal_action_guild_settings ChannelListTree::signal_action_guild_settings() {
+ return m_signal_action_guild_settings;
+}
+
+#ifdef WITH_LIBHANDY
+ChannelListTree::type_signal_action_open_new_tab ChannelListTree::signal_action_open_new_tab() {
+ return m_signal_action_open_new_tab;
+}
+#endif
+
+#ifdef WITH_VOICE
+ChannelListTree::type_signal_action_join_voice_channel ChannelListTree::signal_action_join_voice_channel() {
+ return m_signal_action_join_voice_channel;
+}
+
+ChannelListTree::type_signal_action_disconnect_voice ChannelListTree::signal_action_disconnect_voice() {
+ return m_signal_action_disconnect_voice;
+}
+#endif
+
+ChannelListTree::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);
+ add(m_color);
+ add(m_voice_flags);
+}
diff --git a/src/components/channellist/channellisttree.hpp b/src/components/channellist/channellisttree.hpp
new file mode 100644
index 0000000..136522b
--- /dev/null
+++ b/src/components/channellist/channellisttree.hpp
@@ -0,0 +1,248 @@
+#pragma once
+#include <string>
+#include <queue>
+#include <mutex>
+#include <unordered_set>
+#include <unordered_map>
+#include <gtkmm/paned.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/treemodel.h>
+#include <gtkmm/treestore.h>
+#include <gtkmm/treemodelfilter.h>
+#include <gtkmm/treeview.h>
+#include <sigc++/sigc++.h>
+#include "discord/discord.hpp"
+#include "state.hpp"
+#include "cellrendererchannels.hpp"
+
+constexpr static int GuildIconSize = 24;
+constexpr static int DMIconSize = 20;
+constexpr static int VoiceParticipantIconSize = 18;
+constexpr static int OrphanChannelSortOffset = -100; // forces orphan channels to the top of the list
+
+class ChannelListTree : public Gtk::ScrolledWindow {
+public:
+ ChannelListTree();
+
+ void UpdateListing();
+ void SetActiveChannel(Snowflake id, bool expand_to);
+
+ // channel list should be populated when this is called
+ void UseExpansionState(const ExpansionStateRoot &state);
+ ExpansionStateRoot GetExpansionState() const;
+
+ void UsePanedHack(Gtk::Paned &paned);
+
+ void SetClassic(bool value);
+ void SetSelectedGuild(Snowflake guild_id);
+ void SetSelectedDMs();
+
+protected:
+ int SortFunc(const Gtk::TreeModel::iterator &a, const Gtk::TreeModel::iterator &b);
+
+ void OnPanedPositionChanged();
+
+ void UpdateListingClassic();
+
+ 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 OnChannelMute(Snowflake id);
+ void OnChannelUnmute(Snowflake id);
+ void OnGuildMute(Snowflake id);
+ void OnGuildUnmute(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);
+
+ void OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id);
+ void OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id);
+ void OnVoiceStateSet(Snowflake user_id, Snowflake channel_id, VoiceStateFlags flags);
+
+ Gtk::TreeView m_view;
+
+ class ModelColumns : public Gtk::TreeModel::ColumnRecord {
+ public:
+ ModelColumns();
+
+ Gtk::TreeModelColumn<RenderType> m_type;
+ Gtk::TreeModelColumn<uint64_t> m_id;
+ Gtk::TreeModelColumn<Glib::ustring> m_name;
+ Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> m_icon;
+ Gtk::TreeModelColumn<Glib::RefPtr<Gdk::PixbufAnimation>> m_icon_anim;
+ Gtk::TreeModelColumn<int64_t> m_sort;
+ Gtk::TreeModelColumn<bool> m_nsfw;
+ Gtk::TreeModelColumn<std::optional<Gdk::RGBA>> m_color; // for folders right now
+ Gtk::TreeModelColumn<VoiceStateFlags> m_voice_flags;
+ // 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<bool> m_expanded;
+ };
+
+ ModelColumns m_columns;
+ Glib::RefPtr<Gtk::TreeStore> m_model;
+ Glib::RefPtr<Gtk::TreeModelFilter> m_filter_model;
+ Glib::RefPtr<Gtk::TreeModelSort> m_sort_model;
+
+ Gtk::TreePath ConvertModelPathToView(const Gtk::TreePath &path);
+ Gtk::TreeIter ConvertModelIterToView(const Gtk::TreeIter &iter);
+ Gtk::TreePath ConvertViewPathToModel(const Gtk::TreePath &path);
+ Gtk::TreeIter ConvertViewIterToModel(const Gtk::TreeIter &iter);
+ Gtk::TreePath GetViewPathFromViewIter(const Gtk::TreeIter &iter);
+ Gtk::TreeModel::iterator AddFolder(const UserSettingsGuildFoldersEntry &folder);
+ Gtk::TreeModel::iterator AddGuild(const GuildData &guild, const Gtk::TreeNodeChildren &root);
+ Gtk::TreeModel::iterator UpdateCreateChannelCategory(const ChannelData &channel);
+ Gtk::TreeModel::iterator CreateThreadRow(const Gtk::TreeNodeChildren &children, const ChannelData &channel);
+
+#ifdef WITH_VOICE
+ Gtk::TreeModel::iterator CreateVoiceParticipantRow(const UserData &user, const Gtk::TreeNodeChildren &parent);
+#endif
+
+ void UpdateChannelCategory(const ChannelData &channel);
+
+ // separation necessary because a channel and guild can share the same id
+ Gtk::TreeModel::iterator GetIteratorForTopLevelFromID(Snowflake id);
+ Gtk::TreeModel::iterator GetIteratorForGuildFromID(Snowflake id);
+ Gtk::TreeModel::iterator GetIteratorForRowFromID(Snowflake id);
+ Gtk::TreeModel::iterator GetIteratorForRowFromIDOfType(Snowflake id, RenderType type);
+
+ bool IsTextChannel(ChannelType type);
+
+ void OnRowCollapsed(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) const;
+ void OnRowExpanded(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path);
+ bool SelectionFunc(const Glib::RefPtr<Gtk::TreeModel> &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 SetDMChannelIcon(Gtk::TreeIter iter, const ChannelData &dm);
+
+ void RedrawUnreadIndicatorsForChannel(const ChannelData &channel);
+ void OnMessageAck(const MessageAckData &data);
+ 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::MenuItem m_menu_guild_mark_as_read;
+ Gtk::MenuItem m_menu_guild_toggle_mute;
+
+ Gtk::Menu m_menu_category;
+ Gtk::MenuItem m_menu_category_copy_id;
+ Gtk::MenuItem m_menu_category_toggle_mute;
+
+ Gtk::Menu m_menu_channel;
+ Gtk::MenuItem m_menu_channel_copy_id;
+ Gtk::MenuItem m_menu_channel_mark_as_read;
+ Gtk::MenuItem m_menu_channel_toggle_mute;
+
+#ifdef WITH_LIBHANDY
+ Gtk::MenuItem m_menu_channel_open_tab;
+#endif
+
+#ifdef WITH_VOICE
+ Gtk::Menu m_menu_voice_channel;
+ Gtk::MenuItem m_menu_voice_channel_join;
+ Gtk::MenuItem m_menu_voice_channel_disconnect;
+#endif
+
+ Gtk::Menu m_menu_dm;
+ Gtk::MenuItem m_menu_dm_copy_id;
+ Gtk::MenuItem m_menu_dm_close;
+ Gtk::MenuItem m_menu_dm_toggle_mute;
+#ifdef WITH_VOICE
+ Gtk::MenuItem m_menu_dm_join_voice;
+ Gtk::MenuItem m_menu_dm_disconnect_voice;
+#endif
+
+#ifdef WITH_LIBHANDY
+ Gtk::MenuItem m_menu_dm_open_tab;
+#endif
+
+ 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;
+ Gtk::MenuItem m_menu_thread_mark_as_read;
+ Gtk::MenuItem m_menu_thread_toggle_mute;
+
+ void OnGuildSubmenuPopup();
+ void OnCategorySubmenuPopup();
+ void OnChannelSubmenuPopup();
+ void OnDMSubmenuPopup();
+ void OnThreadSubmenuPopup();
+
+#ifdef WITH_VOICE
+ void OnVoiceChannelSubmenuPopup();
+#endif
+
+ bool m_updating_listing = false;
+
+ bool m_classic = false;
+ Snowflake m_classic_selected_guild;
+ bool m_classic_selected_dms = false;
+
+ Snowflake m_active_channel;
+
+ // hashtable for the billion lookups done in UseExpansionState
+ std::unordered_map<Snowflake, Gtk::TreeModel::iterator> m_tmp_row_map;
+ std::unordered_map<Snowflake, Gtk::TreeModel::iterator> m_tmp_guild_row_map;
+
+public:
+ using type_signal_action_channel_item_select = sigc::signal<void, Snowflake>;
+ using type_signal_action_guild_leave = sigc::signal<void, Snowflake>;
+ using type_signal_action_guild_settings = sigc::signal<void, Snowflake>;
+
+#ifdef WITH_LIBHANDY
+ using type_signal_action_open_new_tab = sigc::signal<void, Snowflake>;
+ type_signal_action_open_new_tab signal_action_open_new_tab();
+#endif
+
+#ifdef WITH_VOICE
+ using type_signal_action_join_voice_channel = sigc::signal<void, Snowflake>;
+ using type_signal_action_disconnect_voice = sigc::signal<void>;
+
+ type_signal_action_join_voice_channel signal_action_join_voice_channel();
+ type_signal_action_disconnect_voice signal_action_disconnect_voice();
+#endif
+
+ 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();
+
+private:
+ 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;
+
+#ifdef WITH_LIBHANDY
+ type_signal_action_open_new_tab m_signal_action_open_new_tab;
+#endif
+
+#ifdef WITH_VOICE
+ type_signal_action_join_voice_channel m_signal_action_join_voice_channel;
+ type_signal_action_disconnect_voice m_signal_action_disconnect_voice;
+#endif
+};
diff --git a/src/components/channellist/classic/guildlist.cpp b/src/components/channellist/classic/guildlist.cpp
new file mode 100644
index 0000000..d756c6f
--- /dev/null
+++ b/src/components/channellist/classic/guildlist.cpp
@@ -0,0 +1,177 @@
+#include "guildlist.hpp"
+
+#include "abaddon.hpp"
+#include "guildlistfolderitem.hpp"
+
+class GuildListDMsButton : public Gtk::EventBox {
+public:
+ GuildListDMsButton() {
+ set_size_request(48, 48);
+
+ m_img.property_icon_name() = "user-available-symbolic"; // meh
+ m_img.property_icon_size() = Gtk::ICON_SIZE_DND;
+ add(m_img);
+ show_all_children();
+ }
+
+private:
+ Gtk::Image m_img;
+};
+
+GuildList::GuildList()
+ : m_menu_guild_copy_id("_Copy ID", true)
+ , m_menu_guild_settings("View _Settings", true)
+ , m_menu_guild_leave("_Leave", true)
+ , m_menu_guild_mark_as_read("Mark as _Read", true) {
+ get_style_context()->add_class("classic-guild-list");
+ set_selection_mode(Gtk::SELECTION_NONE);
+ show_all_children();
+
+ m_menu_guild_copy_id.signal_activate().connect([this] {
+ Gtk::Clipboard::get()->set_text(std::to_string(m_menu_guild_target));
+ });
+ m_menu_guild_settings.signal_activate().connect([this] {
+ m_signal_action_guild_settings.emit(m_menu_guild_target);
+ });
+ m_menu_guild_leave.signal_activate().connect([this] {
+ m_signal_action_guild_leave.emit(m_menu_guild_target);
+ });
+ m_menu_guild_mark_as_read.signal_activate().connect([this] {
+ Abaddon::Get().GetDiscordClient().MarkGuildAsRead(m_menu_guild_target, [](...) {});
+ });
+ m_menu_guild_toggle_mute.signal_activate().connect([this] {
+ const auto id = m_menu_guild_target;
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ if (discord.IsGuildMuted(id))
+ discord.UnmuteGuild(id, NOOP_CALLBACK);
+ else
+ discord.MuteGuild(id, NOOP_CALLBACK);
+ });
+ m_menu_guild.append(m_menu_guild_mark_as_read);
+ m_menu_guild.append(m_menu_guild_settings);
+ m_menu_guild.append(m_menu_guild_leave);
+ m_menu_guild.append(m_menu_guild_toggle_mute);
+ m_menu_guild.append(m_menu_guild_copy_id);
+ m_menu_guild.show_all();
+}
+
+void GuildList::UpdateListing() {
+ auto &discord = Abaddon::Get().GetDiscordClient();
+
+ Clear();
+
+ auto *dms = Gtk::make_managed<GuildListDMsButton>();
+ dms->show();
+ dms->signal_button_press_event().connect([this](GdkEventButton *ev) -> bool {
+ if (ev->type == GDK_BUTTON_PRESS && ev->button == GDK_BUTTON_PRIMARY) {
+ m_signal_dms_selected.emit();
+ }
+ return false;
+ });
+ add(*dms);
+
+ // does this function still even work ??lol
+ const auto folders = discord.GetUserSettings().GuildFolders;
+ const auto guild_ids = discord.GetUserSortedGuilds();
+
+ // same logic from ChannelListTree
+
+ std::set<Snowflake> foldered_guilds;
+ for (const auto &group : folders) {
+ foldered_guilds.insert(group.GuildIDs.begin(), group.GuildIDs.end());
+ }
+
+ for (auto iter = guild_ids.crbegin(); iter != guild_ids.crend(); iter++) {
+ if (foldered_guilds.find(*iter) == foldered_guilds.end()) {
+ AddGuild(*iter);
+ }
+ }
+
+ for (const auto &group : folders) {
+ AddFolder(group);
+ }
+}
+
+void GuildList::AddGuild(Snowflake id) {
+ if (auto item = CreateGuildWidget(id)) {
+ item->show();
+ add(*item);
+ }
+}
+
+GuildListGuildItem *GuildList::CreateGuildWidget(Snowflake id) {
+ const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(id);
+ if (!guild.has_value()) return nullptr;
+
+ auto *item = Gtk::make_managed<GuildListGuildItem>(*guild);
+ item->signal_button_press_event().connect([this, id](GdkEventButton *event) -> bool {
+ if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) {
+ m_signal_guild_selected.emit(id);
+ } else if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_SECONDARY) {
+ m_menu_guild_target = id;
+ OnGuildSubmenuPopup();
+ m_menu_guild.popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+ }
+ return true;
+ });
+
+ return item;
+}
+
+void GuildList::AddFolder(const UserSettingsGuildFoldersEntry &folder) {
+ // groups with no ID arent actually folders
+ if (!folder.ID.has_value()) {
+ if (!folder.GuildIDs.empty()) {
+ AddGuild(folder.GuildIDs[0]);
+ }
+ return;
+ }
+
+ auto *folder_widget = Gtk::make_managed<GuildListFolderItem>(folder);
+ for (const auto guild_id : folder.GuildIDs) {
+ if (auto *guild_widget = CreateGuildWidget(guild_id)) {
+ guild_widget->show();
+ folder_widget->AddGuildWidget(guild_widget);
+ }
+ }
+
+ folder_widget->show();
+ add(*folder_widget);
+}
+
+void GuildList::Clear() {
+ const auto children = get_children();
+ for (auto *child : children) {
+ delete child;
+ }
+}
+
+void GuildList::OnGuildSubmenuPopup() {
+ const auto id = m_menu_guild_target;
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ if (discord.IsGuildMuted(id)) {
+ m_menu_guild_toggle_mute.set_label("Unmute");
+ } else {
+ m_menu_guild_toggle_mute.set_label("Mute");
+ }
+
+ const auto guild = discord.GetGuild(id);
+ const auto self_id = discord.GetUserData().ID;
+ m_menu_guild_leave.set_sensitive(!(guild.has_value() && guild->OwnerID == self_id));
+}
+
+GuildList::type_signal_guild_selected GuildList::signal_guild_selected() {
+ return m_signal_guild_selected;
+}
+
+GuildList::type_signal_dms_selected GuildList::signal_dms_selected() {
+ return m_signal_dms_selected;
+}
+
+GuildList::type_signal_action_guild_leave GuildList::signal_action_guild_leave() {
+ return m_signal_action_guild_leave;
+}
+
+GuildList::type_signal_action_guild_settings GuildList::signal_action_guild_settings() {
+ return m_signal_action_guild_settings;
+}
diff --git a/src/components/channellist/classic/guildlist.hpp b/src/components/channellist/classic/guildlist.hpp
new file mode 100644
index 0000000..72e88e8
--- /dev/null
+++ b/src/components/channellist/classic/guildlist.hpp
@@ -0,0 +1,48 @@
+#pragma once
+#include <gtkmm/listbox.h>
+#include "discord/snowflake.hpp"
+#include "discord/usersettings.hpp"
+
+class GuildListGuildItem;
+
+class GuildList : public Gtk::ListBox {
+public:
+ GuildList();
+
+ void UpdateListing();
+
+private:
+ void AddGuild(Snowflake id);
+ void AddFolder(const UserSettingsGuildFoldersEntry &folder);
+ void Clear();
+
+ GuildListGuildItem *CreateGuildWidget(Snowflake id);
+
+ // todo code duplication not good no sir
+ 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::MenuItem m_menu_guild_mark_as_read;
+ Gtk::MenuItem m_menu_guild_toggle_mute;
+ Snowflake m_menu_guild_target;
+
+ void OnGuildSubmenuPopup();
+
+public:
+ using type_signal_guild_selected = sigc::signal<void, Snowflake>;
+ using type_signal_dms_selected = sigc::signal<void>;
+ using type_signal_action_guild_leave = sigc::signal<void, Snowflake>;
+ using type_signal_action_guild_settings = sigc::signal<void, Snowflake>;
+
+ type_signal_guild_selected signal_guild_selected();
+ type_signal_dms_selected signal_dms_selected();
+ type_signal_action_guild_leave signal_action_guild_leave();
+ type_signal_action_guild_settings signal_action_guild_settings();
+
+private:
+ type_signal_guild_selected m_signal_guild_selected;
+ type_signal_dms_selected m_signal_dms_selected;
+ type_signal_action_guild_leave m_signal_action_guild_leave;
+ type_signal_action_guild_settings m_signal_action_guild_settings;
+};
diff --git a/src/components/channellist/classic/guildlistfolderitem.cpp b/src/components/channellist/classic/guildlistfolderitem.cpp
new file mode 100644
index 0000000..e062d42
--- /dev/null
+++ b/src/components/channellist/classic/guildlistfolderitem.cpp
@@ -0,0 +1,129 @@
+#include "guildlistfolderitem.hpp"
+
+#include "abaddon.hpp"
+#include "guildlistguilditem.hpp"
+#include "util.hpp"
+
+// doing my best to copy discord here
+
+const int FolderGridButtonSize = 48;
+const int FolderGridImageSize = 24;
+
+GuildListFolderButton::GuildListFolderButton() {
+ set_size_request(FolderGridButtonSize, FolderGridButtonSize);
+}
+
+void GuildListFolderButton::SetGuilds(const std::vector<Snowflake> &guild_ids) {
+ for (int y = 0; y < 2; y++) {
+ for (int x = 0; x < 2; x++) {
+ const size_t i = y * 2 + x;
+ auto &widget = m_images[x][y];
+ widget.property_pixbuf() = Abaddon::Get().GetImageManager().GetPlaceholder(FolderGridImageSize);
+ attach(widget, x, y, 1, 1);
+
+ if (i < guild_ids.size()) {
+ widget.show();
+
+ if (const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(guild_ids[i]); guild.has_value() && guild->HasIcon()) {
+ const auto cb = [&widget](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
+ widget.property_pixbuf() = pb->scale_simple(FolderGridImageSize, FolderGridImageSize, Gdk::INTERP_BILINEAR);
+ };
+ Abaddon::Get().GetImageManager().LoadFromURL(guild->GetIconURL("png", "32"), sigc::track_obj(cb, *this));
+ }
+ }
+ }
+ }
+}
+
+GuildListFolderItem::GuildListFolderItem(const UserSettingsGuildFoldersEntry &folder) {
+ m_guild_ids = folder.GuildIDs;
+
+ get_style_context()->add_class("classic-guild-list-folder");
+
+ if (folder.Name.has_value()) {
+ set_tooltip_text(*folder.Name);
+ }
+
+ m_revealer.add(m_box);
+ m_revealer.set_reveal_child(false);
+
+ m_image.property_pixbuf() = Abaddon::Get().GetImageManager().GetPlaceholder(48);
+
+ m_ev.signal_button_press_event().connect([this](GdkEventButton *event) -> bool {
+ if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) {
+ m_revealer.set_reveal_child(!m_revealer.get_reveal_child());
+ if (!Abaddon::Get().GetSettings().FolderIconOnly) {
+ if (m_revealer.get_reveal_child()) {
+ m_stack.set_visible_child("icon", Gtk::STACK_TRANSITION_TYPE_SLIDE_DOWN);
+ } else {
+ m_stack.set_visible_child("grid", Gtk::STACK_TRANSITION_TYPE_SLIDE_UP);
+ }
+ }
+ }
+
+ return false;
+ });
+
+ m_grid.SetGuilds(folder.GuildIDs);
+ m_grid.show();
+
+ m_icon.property_icon_name() = "folder-symbolic";
+ m_icon.property_icon_size() = Gtk::ICON_SIZE_DND;
+ if (folder.Color.has_value()) {
+ m_icon.override_color(IntToRGBA(*folder.Color));
+ }
+ m_icon.show();
+
+ m_stack.add(m_grid, "grid");
+ m_stack.add(m_icon, "icon");
+ m_stack.set_visible_child(Abaddon::Get().GetSettings().FolderIconOnly ? "icon" : "grid");
+ m_stack.show();
+
+ m_ev.add(m_stack);
+ add(m_ev);
+ add(m_revealer);
+
+ m_ev.show();
+ m_revealer.show();
+ m_box.show();
+ m_image.show();
+ show();
+
+ Abaddon::Get().GetDiscordClient().signal_message_create().connect(sigc::mem_fun(*this, &GuildListFolderItem::OnMessageCreate));
+ Abaddon::Get().GetDiscordClient().signal_message_ack().connect(sigc::mem_fun(*this, &GuildListFolderItem::OnMessageAck));
+
+ CheckUnreadStatus();
+}
+
+void GuildListFolderItem::AddGuildWidget(GuildListGuildItem *widget) {
+ m_box.add(*widget);
+}
+
+void GuildListFolderItem::OnMessageCreate(const Message &msg) {
+ if (msg.GuildID.has_value() && std::find(m_guild_ids.begin(), m_guild_ids.end(), *msg.GuildID) != m_guild_ids.end()) CheckUnreadStatus();
+}
+
+void GuildListFolderItem::OnMessageAck(const MessageAckData &data) {
+ CheckUnreadStatus();
+}
+
+void GuildListFolderItem::CheckUnreadStatus() {
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ if (!Abaddon::Get().GetSettings().Unreads) return;
+
+ bool has_any_unreads = false;
+
+ for (auto guild_id : m_guild_ids) {
+ int mentions;
+ if (!discord.IsGuildMuted(guild_id) && discord.GetUnreadStateForGuild(guild_id, mentions)) {
+ has_any_unreads = true;
+ break;
+ }
+ }
+
+ if (has_any_unreads) {
+ get_style_context()->add_class("has-unread");
+ } else {
+ get_style_context()->remove_class("has-unread");
+ }
+}
diff --git a/src/components/channellist/classic/guildlistfolderitem.hpp b/src/components/channellist/classic/guildlistfolderitem.hpp
new file mode 100644
index 0000000..e5772c0
--- /dev/null
+++ b/src/components/channellist/classic/guildlistfolderitem.hpp
@@ -0,0 +1,44 @@
+#pragma once
+#include <gtkmm/box.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/image.h>
+#include <gtkmm/revealer.h>
+#include <gtkmm/stack.h>
+
+#include "guildlistguilditem.hpp"
+#include "discord/usersettings.hpp"
+
+class GuildListGuildItem;
+
+class GuildListFolderButton : public Gtk::Grid {
+public:
+ GuildListFolderButton();
+ void SetGuilds(const std::vector<Snowflake> &guild_ids);
+
+private:
+ Gtk::Image m_images[2][2];
+};
+
+class GuildListFolderItem : public Gtk::VBox {
+public:
+ GuildListFolderItem(const UserSettingsGuildFoldersEntry &folder);
+
+ void AddGuildWidget(GuildListGuildItem *widget);
+
+private:
+ void OnMessageCreate(const Message &msg);
+ void OnMessageAck(const MessageAckData &data);
+ void CheckUnreadStatus();
+
+ std::vector<Snowflake> m_guild_ids;
+
+ Gtk::Stack m_stack;
+ GuildListFolderButton m_grid;
+ Gtk::Image m_icon;
+
+ Gtk::EventBox m_ev;
+ Gtk::Image m_image;
+ Gtk::Revealer m_revealer;
+ Gtk::VBox m_box;
+};
diff --git a/src/components/channellist/classic/guildlistguilditem.cpp b/src/components/channellist/classic/guildlistguilditem.cpp
new file mode 100644
index 0000000..5b578be
--- /dev/null
+++ b/src/components/channellist/classic/guildlistguilditem.cpp
@@ -0,0 +1,52 @@
+#include "guildlistguilditem.hpp"
+
+#include "abaddon.hpp"
+
+GuildListGuildItem::GuildListGuildItem(const GuildData &guild)
+ : ID(guild.ID) {
+ get_style_context()->add_class("classic-guild-list-guild");
+
+ m_image.property_pixbuf() = Abaddon::Get().GetImageManager().GetPlaceholder(48);
+
+ add(m_box);
+ m_box.pack_start(m_image);
+ show_all_children();
+
+ set_tooltip_text(guild.Name);
+
+ UpdateIcon();
+
+ Abaddon::Get().GetDiscordClient().signal_message_create().connect(sigc::mem_fun(*this, &GuildListGuildItem::OnMessageCreate));
+ Abaddon::Get().GetDiscordClient().signal_message_ack().connect(sigc::mem_fun(*this, &GuildListGuildItem::OnMessageAck));
+
+ CheckUnreadStatus();
+}
+
+void GuildListGuildItem::UpdateIcon() {
+ const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(ID);
+ if (!guild.has_value() || !guild->HasIcon()) return;
+ Abaddon::Get().GetImageManager().LoadFromURL(guild->GetIconURL("png", "64"), sigc::mem_fun(*this, &GuildListGuildItem::OnIconFetched));
+}
+
+void GuildListGuildItem::OnIconFetched(const Glib::RefPtr<Gdk::Pixbuf> &pb) {
+ m_image.property_pixbuf() = pb->scale_simple(48, 48, Gdk::INTERP_BILINEAR);
+}
+
+void GuildListGuildItem::OnMessageCreate(const Message &msg) {
+ if (msg.GuildID.has_value() && *msg.GuildID == ID) CheckUnreadStatus();
+}
+
+void GuildListGuildItem::OnMessageAck(const MessageAckData &data) {
+ CheckUnreadStatus();
+}
+
+void GuildListGuildItem::CheckUnreadStatus() {
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ if (!Abaddon::Get().GetSettings().Unreads) return;
+ int mentions;
+ if (!discord.IsGuildMuted(ID) && discord.GetUnreadStateForGuild(ID, mentions)) {
+ get_style_context()->add_class("has-unread");
+ } else {
+ get_style_context()->remove_class("has-unread");
+ }
+}
diff --git a/src/components/channellist/classic/guildlistguilditem.hpp b/src/components/channellist/classic/guildlistguilditem.hpp
new file mode 100644
index 0000000..6e2b241
--- /dev/null
+++ b/src/components/channellist/classic/guildlistguilditem.hpp
@@ -0,0 +1,22 @@
+#pragma once
+#include <gtkmm/box.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/image.h>
+#include "discord/guild.hpp"
+
+class GuildListGuildItem : public Gtk::EventBox {
+public:
+ GuildListGuildItem(const GuildData &guild);
+
+ Snowflake ID;
+
+private:
+ void UpdateIcon();
+ void OnIconFetched(const Glib::RefPtr<Gdk::Pixbuf> &pb);
+ void OnMessageCreate(const Message &msg);
+ void OnMessageAck(const MessageAckData &data);
+ void CheckUnreadStatus();
+
+ Gtk::Box m_box;
+ Gtk::Image m_image;
+};