diff --git a/README.md b/README.md index 33fd59f8..c7e6111b 100644 --- a/README.md +++ b/README.md @@ -199,12 +199,7 @@ spam filter's wrath: | `.embed-field-title` | The title of an embed field | | `.embed-field-value` | The value of an embed field | | `.embed-footer` | The footer of an embed | -| `.members` | Container of the member list | -| `.members-row` | All rows within the members container | -| `.members-row-label` | All labels in the members container | -| `.members-row-member` | Rows containing a member | -| `.members-row-role` | Rows containing a role | -| `.members-row-avatar` | Contains the avatar for a row in the member list | +| `.member-list` | Container of the member list | | `.status-indicator` | The status indicator | | `.online` | Applied to status indicators when the associated user is online | | `.idle` | Applied to status indicators when the associated user is away | diff --git a/src/components/cellrenderermemberlist.cpp b/src/components/cellrenderermemberlist.cpp new file mode 100644 index 00000000..66b223e2 --- /dev/null +++ b/src/components/cellrenderermemberlist.cpp @@ -0,0 +1,140 @@ +#include "cellrenderermemberlist.hpp" + +CellRendererMemberList::CellRendererMemberList() + : Glib::ObjectBase(typeid(CellRendererMemberList)) + , m_property_type(*this, "render-type") + , m_property_id(*this, "id") + , m_property_name(*this, "name") + , m_property_pixbuf(*this, "pixbuf") + , m_property_color(*this, "color") { + 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 CellRendererMemberList::property_type() { + return m_property_type.get_proxy(); +} + +Glib::PropertyProxy CellRendererMemberList::property_id() { + return m_property_id.get_proxy(); +} + +Glib::PropertyProxy CellRendererMemberList::property_name() { + return m_property_name.get_proxy(); +} + +Glib::PropertyProxy> CellRendererMemberList::property_pixbuf() { + return m_property_pixbuf.get_proxy(); +} + +Glib::PropertyProxy CellRendererMemberList::property_color() { + return m_property_color.get_proxy(); +} + +void CellRendererMemberList::get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { + switch (m_property_type.get_value()) { + case MemberListRenderType::Role: + return get_preferred_width_vfunc_role(widget, minimum_width, natural_width); + case MemberListRenderType::Member: + return get_preferred_width_vfunc_member(widget, minimum_width, natural_width); + } +} + +void CellRendererMemberList::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 MemberListRenderType::Role: + return get_preferred_width_for_height_vfunc_role(widget, height, minimum_width, natural_width); + case MemberListRenderType::Member: + return get_preferred_width_for_height_vfunc_member(widget, height, minimum_width, natural_width); + } +} + +void CellRendererMemberList::get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { + switch (m_property_type.get_value()) { + case MemberListRenderType::Role: + return get_preferred_height_vfunc_role(widget, minimum_width, natural_width); + case MemberListRenderType::Member: + return get_preferred_height_vfunc_member(widget, minimum_width, natural_width); + } +} + +void CellRendererMemberList::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 MemberListRenderType::Role: + return get_preferred_height_for_width_vfunc_role(widget, width, minimum_height, natural_height); + case MemberListRenderType::Member: + return get_preferred_height_for_width_vfunc_member(widget, width, minimum_height, natural_height); + } +} + +void CellRendererMemberList::render_vfunc(const Cairo::RefPtr &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { + switch (m_property_type.get_value()) { + case MemberListRenderType::Role: + return render_vfunc_role(cr, widget, background_area, cell_area, flags); + case MemberListRenderType::Member: + return render_vfunc_member(cr, widget, background_area, cell_area, flags); + } +} + +void CellRendererMemberList::get_preferred_width_vfunc_role(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { + m_renderer_text.get_preferred_width(widget, minimum_width, natural_width); +} + +void CellRendererMemberList::get_preferred_width_for_height_vfunc_role(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 CellRendererMemberList::get_preferred_height_vfunc_role(Gtk::Widget &widget, int &minimum_height, int &natural_height) const { + m_renderer_text.get_preferred_height(widget, minimum_height, natural_height); +} + +void CellRendererMemberList::get_preferred_height_for_width_vfunc_role(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 CellRendererMemberList::render_vfunc_role(const Cairo::RefPtr &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { + m_renderer_text.render(cr, widget, background_area, cell_area, flags); +} + +void CellRendererMemberList::get_preferred_width_vfunc_member(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { + m_renderer_text.get_preferred_width(widget, minimum_width, natural_width); +} + +void CellRendererMemberList::get_preferred_width_for_height_vfunc_member(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 CellRendererMemberList::get_preferred_height_vfunc_member(Gtk::Widget &widget, int &minimum_height, int &natural_height) const { + m_renderer_text.get_preferred_height(widget, minimum_height, natural_height); +} + +void CellRendererMemberList::get_preferred_height_for_width_vfunc_member(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 CellRendererMemberList::render_vfunc_member(const Cairo::RefPtr &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { + Gdk::Rectangle text_cell_area = cell_area; + text_cell_area.set_x(22); + const auto color = m_property_color.get_value(); + if (color.get_alpha_u() > 0) { + m_renderer_text.property_foreground_rgba().set_value(color); + } + m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); + m_renderer_text.property_foreground_set().set_value(false); + + const double icon_x = background_area.get_x() + 6.0; + const double icon_y = background_area.get_y() + background_area.get_height() / 2.0 - 8.0; + Gdk::Cairo::set_source_pixbuf(cr, m_property_pixbuf.get_value(), icon_x, icon_y); + cr->rectangle(icon_x, icon_y, 16.0, 16.0); + cr->fill(); + + m_signal_render.emit(m_property_id.get_value()); +} + +CellRendererMemberList::type_signal_render CellRendererMemberList::signal_render() { + return m_signal_render; +} diff --git a/src/components/cellrenderermemberlist.hpp b/src/components/cellrenderermemberlist.hpp new file mode 100644 index 00000000..7a49ccf0 --- /dev/null +++ b/src/components/cellrenderermemberlist.hpp @@ -0,0 +1,65 @@ +#pragma once +#include + +enum class MemberListRenderType : uint8_t { + Role, + Member, +}; + +class CellRendererMemberList : public Gtk::CellRenderer { +public: + CellRendererMemberList(); + ~CellRendererMemberList() override = default; + + Glib::PropertyProxy property_type(); + Glib::PropertyProxy property_id(); + Glib::PropertyProxy property_name(); + Glib::PropertyProxy> property_pixbuf(); + Glib::PropertyProxy property_color(); + +protected: + void get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const override; + void get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const override; + void get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const override; + void get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const override; + void render_vfunc(const Cairo::RefPtr &cr, + Gtk::Widget &widget, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags) override; + + void get_preferred_width_vfunc_role(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; + void get_preferred_width_for_height_vfunc_role(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; + void get_preferred_height_vfunc_role(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; + void get_preferred_height_for_width_vfunc_role(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; + void render_vfunc_role(const Cairo::RefPtr &cr, + Gtk::Widget &widget, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags); + + void get_preferred_width_vfunc_member(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; + void get_preferred_width_for_height_vfunc_member(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; + void get_preferred_height_vfunc_member(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; + void get_preferred_height_for_width_vfunc_member(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; + void render_vfunc_member(const Cairo::RefPtr &cr, + Gtk::Widget &widget, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags); + +private: + Gtk::CellRendererText m_renderer_text; + + Glib::Property m_property_type; + Glib::Property m_property_id; + Glib::Property m_property_name; + Glib::Property> m_property_pixbuf; + Glib::Property m_property_color; + + using type_signal_render = sigc::signal; + type_signal_render m_signal_render; + +public: + type_signal_render signal_render(); +}; diff --git a/src/components/memberlist.cpp b/src/components/memberlist.cpp index 975b5277..b082daa2 100644 --- a/src/components/memberlist.cpp +++ b/src/components/memberlist.cpp @@ -1,233 +1,252 @@ #include "memberlist.hpp" -#include "lazyimage.hpp" -#include "statusindicator.hpp" - -constexpr static const int MaxMemberListRows = 200; - -MemberListUserRow::MemberListUserRow(const std::optional &guild, const UserData &data) { - ID = data.ID; - m_ev = Gtk::manage(new Gtk::EventBox); - m_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); - m_label = Gtk::manage(new Gtk::Label); - m_avatar = Gtk::manage(new LazyImage(16, 16)); - m_status_indicator = Gtk::manage(new StatusIndicator(ID)); - - if (Abaddon::Get().GetSettings().ShowOwnerCrown && guild.has_value() && guild->OwnerID == data.ID) { - try { - const static auto crown_path = Abaddon::GetResPath("/crown.png"); - auto pixbuf = Gdk::Pixbuf::create_from_file(crown_path, 12, 12); - m_crown = Gtk::manage(new Gtk::Image(pixbuf)); - m_crown->set_valign(Gtk::ALIGN_CENTER); - m_crown->set_margin_end(8); - } catch (...) {} - } - - m_status_indicator->set_margin_start(3); - if (guild.has_value()) - m_avatar->SetURL(data.GetAvatarURL(guild->ID, "png")); - else - m_avatar->SetURL(data.GetAvatarURL("png")); +constexpr static int MemberListUserLimit = 200; - get_style_context()->add_class("members-row"); - get_style_context()->add_class("members-row-member"); - m_label->get_style_context()->add_class("members-row-label"); - m_avatar->get_style_context()->add_class("members-row-avatar"); +MemberList::MemberList() + : m_model(Gtk::TreeStore::create(m_columns)) + , m_menu_role_copy_id("_Copy ID", true) { + m_main.get_style_context()->add_class("member-list"); - m_label->set_single_line_mode(true); - m_label->set_ellipsize(Pango::ELLIPSIZE_END); + m_view.set_hexpand(true); + m_view.set_vexpand(true); - // todo remove after migration complete - std::string display; - if (data.IsPomelo()) { - display = data.GetDisplayName(guild.has_value() ? guild->ID : Snowflake::Invalid); - } else { - display = data.Username; - if (Abaddon::Get().GetSettings().ShowMemberListDiscriminators) { - display += "#" + data.Discriminator; - } - } - - if (guild.has_value()) { - if (const auto col_id = data.GetHoistedRole(guild->ID, true); col_id.IsValid()) { - auto color = Abaddon::Get().GetDiscordClient().GetRole(col_id)->Color; - m_label->set_use_markup(true); - m_label->set_markup("" + Glib::Markup::escape_text(display) + ""); - } else { - m_label->set_text(display); - } - } else { - m_label->set_text(display); - } + m_view.set_show_expanders(false); + m_view.set_enable_search(false); + m_view.set_headers_visible(false); + m_view.get_selection()->set_mode(Gtk::SELECTION_NONE); + m_view.set_model(m_model); + m_view.signal_button_press_event().connect(sigc::mem_fun(*this, &MemberList::OnButtonPressEvent), false); - m_label->set_halign(Gtk::ALIGN_START); - m_box->add(*m_avatar); - m_box->add(*m_status_indicator); - m_box->add(*m_label); - if (m_crown != nullptr) - m_box->add(*m_crown); - m_ev->add(*m_box); - add(*m_ev); - show_all(); -} + m_main.add(m_view); + m_main.show_all_children(); -MemberList::MemberList() { - m_main = Gtk::manage(new Gtk::ScrolledWindow); - m_listbox = Gtk::manage(new Gtk::ListBox); + auto *column = Gtk::make_managed("display"); + auto *renderer = Gtk::make_managed(); + column->pack_start(*renderer); + column->add_attribute(renderer->property_type(), m_columns.m_type); + column->add_attribute(renderer->property_id(), m_columns.m_id); + column->add_attribute(renderer->property_name(), m_columns.m_name); + column->add_attribute(renderer->property_pixbuf(), m_columns.m_pixbuf); + column->add_attribute(renderer->property_color(), m_columns.m_color); + m_view.append_column(*column); - m_listbox->get_style_context()->add_class("members"); + m_model->set_sort_column(m_columns.m_sort, Gtk::SORT_ASCENDING); + m_model->set_default_sort_func([](const Gtk::TreeModel::iterator &, const Gtk::TreeModel::iterator &) -> int { return 0; }); + m_model->set_sort_func(m_columns.m_sort, sigc::mem_fun(*this, &MemberList::SortFunc)); - m_listbox->set_selection_mode(Gtk::SELECTION_NONE); + renderer->signal_render().connect(sigc::mem_fun(*this, &MemberList::OnCellRender)); - m_main->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); - m_main->add(*m_listbox); - m_main->show_all(); -} + // Menu stuff -Gtk::Widget *MemberList::GetRoot() const { - return m_main; -} + m_menu_role.append(m_menu_role_copy_id); + m_menu_role.show_all(); -void MemberList::Clear() { - SetActiveChannel(Snowflake::Invalid); - UpdateMemberList(); + m_menu_role_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])); + }); } -void MemberList::SetActiveChannel(Snowflake id) { - m_chan_id = id; - m_guild_id = Snowflake::Invalid; - if (m_chan_id.IsValid()) { - const auto chan = Abaddon::Get().GetDiscordClient().GetChannel(id); - if (chan.has_value() && chan->GuildID.has_value()) m_guild_id = *chan->GuildID; - } +Gtk::Widget *MemberList::GetRoot() { + return &m_main; } void MemberList::UpdateMemberList() { - m_id_to_row.clear(); - - auto children = m_listbox->get_children(); - auto it = children.begin(); - while (it != children.end()) { - delete *it; - it++; - } - - if (!Abaddon::Get().GetDiscordClient().IsStarted()) return; - if (!m_chan_id.IsValid()) return; + Clear(); + if (!m_active_channel.IsValid()) return; auto &discord = Abaddon::Get().GetDiscordClient(); - const auto chan = discord.GetChannel(m_chan_id); - if (!chan.has_value()) return; - if (chan->Type == ChannelType::DM || chan->Type == ChannelType::GROUP_DM) { - int num_rows = 0; - for (const auto &user : chan->GetDMRecipients()) { - if (num_rows++ > MaxMemberListRows) break; - auto *row = Gtk::manage(new MemberListUserRow(std::nullopt, user)); - m_id_to_row[user.ID] = row; - AttachUserMenuHandler(row, user.ID); - m_listbox->add(*row); - } + const auto channel = discord.GetChannel(m_active_channel); + if (!channel.has_value()) { return; } + const static auto color_transparent = Gdk::RGBA("rgba(0,0,0,0)"); + + if (channel->IsDM()) { + for (const auto &user : channel->GetDMRecipients()) { + auto row_iter = m_model->append(); + auto row = *row_iter; + row[m_columns.m_type] = MemberListRenderType::Member; + row[m_columns.m_id] = user.ID; + row[m_columns.m_name] = user.GetDisplayNameEscaped(); + row[m_columns.m_color] = color_transparent; + row[m_columns.m_av_requested] = false; + row[m_columns.m_pixbuf] = Abaddon::Get().GetImageManager().GetPlaceholder(16); + m_pending_avatars[user.ID] = row_iter; + } + } + + const auto guild = discord.GetGuild(m_active_guild); + if (!guild.has_value()) return; + std::set ids; - if (chan->IsThread()) { - const auto x = discord.GetUsersInThread(m_chan_id); + if (channel->IsThread()) { + const auto x = discord.GetUsersInThread(m_active_channel); ids = { x.begin(), x.end() }; - } else - ids = discord.GetUsersInGuild(m_guild_id); + } else { + ids = discord.GetUsersInGuild(m_active_guild); + } - // process all the shit first so its in proper order - std::map pos_to_role; - std::map> pos_to_users; + std::unordered_map> role_to_users; std::unordered_map user_to_color; - std::vector roleless_users; + std::vector roleless_users; - for (const auto &id : ids) { - auto user = discord.GetUser(id); - if (!user.has_value() || user->IsDeleted()) - continue; + const auto users = discord.GetUsersBulk(ids.begin(), ids.end()); - auto pos_role_id = discord.GetMemberHoistedRole(m_guild_id, id); // role for positioning - auto col_role_id = discord.GetMemberHoistedRole(m_guild_id, id, true); // role for color - auto pos_role = discord.GetRole(pos_role_id); - auto col_role = discord.GetRole(col_role_id); + std::unordered_map role_cache; + if (guild->Roles.has_value()) { + for (const auto &role : *guild->Roles) { + role_cache[role.ID] = role; + } + } + for (const auto &user : users) { + if (user.IsDeleted()) continue; + const auto member = discord.GetMember(user.ID, m_active_guild); + if (!member.has_value()) continue; + + const auto pos_role = discord.GetMemberHoistedRoleCached(*member, role_cache); + const auto col_role = discord.GetMemberHoistedRoleCached(*member, role_cache, true); if (!pos_role.has_value()) { - roleless_users.push_back(id); + roleless_users.push_back(user); continue; } - pos_to_role[pos_role->Position] = *pos_role; - pos_to_users[pos_role->Position].push_back(std::move(*user)); - if (col_role.has_value()) - user_to_color[id] = col_role->Color; + role_to_users[pos_role->ID].push_back(user); + if (col_role.has_value()) user_to_color[user.ID] = col_role->Color; } - int num_rows = 0; - const auto guild = discord.GetGuild(m_guild_id); - if (!guild.has_value()) return; - auto add_user = [this, &num_rows, guild](const UserData &data) -> bool { - if (num_rows++ > MaxMemberListRows) return false; - auto *row = Gtk::manage(new MemberListUserRow(*guild, data)); - m_id_to_row[data.ID] = row; - AttachUserMenuHandler(row, data.ID); - m_listbox->add(*row); + int count = 0; + const auto add_user = [this, &count, &guild, &user_to_color](const UserData &user, const Gtk::TreeRow &parent) -> bool { + if (count++ > MemberListUserLimit) return false; + auto row_iter = m_model->append(parent->children()); + auto row = *row_iter; + row[m_columns.m_type] = MemberListRenderType::Member; + row[m_columns.m_id] = user.ID; + row[m_columns.m_name] = user.GetDisplayNameEscaped(); + row[m_columns.m_pixbuf] = Abaddon::Get().GetImageManager().GetPlaceholder(16); + row[m_columns.m_av_requested] = false; + if (const auto iter = user_to_color.find(user.ID); iter != user_to_color.end()) { + row[m_columns.m_color] = IntToRGBA(iter->second); + } else { + row[m_columns.m_color] = color_transparent; + } + m_pending_avatars[user.ID] = row_iter; return true; }; - auto add_role = [this](const std::string &name) { - auto *role_row = Gtk::manage(new Gtk::ListBoxRow); - auto *role_lbl = Gtk::manage(new Gtk::Label); - - role_row->get_style_context()->add_class("members-row"); - role_row->get_style_context()->add_class("members-row-role"); - role_lbl->get_style_context()->add_class("members-row-label"); - - role_lbl->set_single_line_mode(true); - role_lbl->set_ellipsize(Pango::ELLIPSIZE_END); - role_lbl->set_use_markup(true); - role_lbl->set_markup("" + Glib::Markup::escape_text(name) + ""); - role_lbl->set_halign(Gtk::ALIGN_START); - role_row->add(*role_lbl); - role_row->show_all(); - m_listbox->add(*role_row); + const auto add_role = [this](const RoleData &role) { + auto row = *m_model->append(); + row[m_columns.m_type] = MemberListRenderType::Role; + row[m_columns.m_id] = role.ID; + row[m_columns.m_name] = "" + role.GetEscapedName() + ""; + row[m_columns.m_sort] = role.Position; + return row; }; - for (auto it = pos_to_role.crbegin(); it != pos_to_role.crend(); it++) { - auto pos = it->first; - const auto &role = it->second; + // Kill sorting + m_view.freeze_child_notify(); + m_model->set_sort_column(Gtk::TreeSortable::DEFAULT_SORT_COLUMN_ID, Gtk::SORT_ASCENDING); - add_role(role.Name); + for (const auto &[role_id, users] : role_to_users) { + if (const auto iter = role_cache.find(role_id); iter != role_cache.end()) { + auto role_row = add_role(iter->second); + for (const auto &user : users) add_user(user, role_row); + } + } - if (pos_to_users.find(pos) == pos_to_users.end()) continue; + auto everyone_role = *m_model->append(); + everyone_role[m_columns.m_type] = MemberListRenderType::Role; + everyone_role[m_columns.m_id] = m_active_guild; // yes thats how the role works + everyone_role[m_columns.m_name] = "@everyone"; + everyone_role[m_columns.m_sort] = 0; - auto &users = pos_to_users.at(pos); - AlphabeticalSort(users.begin(), users.end(), [](const auto &e) { return e.Username; }); + for (const auto &user : roleless_users) { + add_user(user, everyone_role); + } - for (const auto &data : users) - if (!add_user(data)) return; + // Restore sorting + m_model->set_sort_column(m_columns.m_sort, Gtk::SORT_ASCENDING); + m_view.expand_all(); + m_view.thaw_child_notify(); +} + +void MemberList::Clear() { + m_model->clear(); + m_pending_avatars.clear(); +} + +void MemberList::SetActiveChannel(Snowflake id) { + m_active_channel = id; + m_active_guild = Snowflake::Invalid; + if (m_active_channel.IsValid()) { + const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(m_active_channel); + if (channel.has_value() && channel->GuildID.has_value()) m_active_guild = *channel->GuildID; } +} - if (chan->Type == ChannelType::DM || chan->Type == ChannelType::GROUP_DM) - add_role("Users"); - else - add_role("@everyone"); - for (const auto &id : roleless_users) { - const auto user = discord.GetUser(id); - if (user.has_value()) - if (!add_user(*user)) return; +void MemberList::OnCellRender(uint64_t id) { + Snowflake real_id = id; + if (const auto iter = m_pending_avatars.find(real_id); iter != m_pending_avatars.end()) { + auto row = iter->second; + m_pending_avatars.erase(iter); + if (!row) return; + if ((*row)[m_columns.m_av_requested]) return; + (*row)[m_columns.m_av_requested] = true; + const auto user = Abaddon::Get().GetDiscordClient().GetUser(real_id); + if (!user.has_value()) return; + const auto cb = [this, row](const Glib::RefPtr &pb) { + // for some reason row::operator bool() returns true when m_model->iter_is_valid returns false + // idk why since other code already does essentially the same thing im doing here + // iter_is_valid is "slow" according to gtk but the only other workaround i can think of would be worse + if (row && m_model->iter_is_valid(row)) { + (*row)[m_columns.m_pixbuf] = pb->scale_simple(16, 16, Gdk::INTERP_BILINEAR); + } + }; + Abaddon::Get().GetImageManager().LoadFromURL(user->GetAvatarURL("png", "16"), cb); } } -void MemberList::AttachUserMenuHandler(Gtk::ListBoxRow *row, Snowflake id) { - row->signal_button_press_event().connect([this, id](GdkEventButton *e) -> bool { - if (e->type == GDK_BUTTON_PRESS && e->button == GDK_BUTTON_SECONDARY) { - Abaddon::Get().ShowUserMenu(reinterpret_cast(e), id, m_guild_id); - return true; +bool MemberList::OnButtonPressEvent(GdkEventButton *ev) { + if (ev->button == GDK_BUTTON_SECONDARY && ev->type == GDK_BUTTON_PRESS) { + if (m_view.get_path_at_pos(static_cast(ev->x), static_cast(ev->y), m_path_for_menu)) { + switch ((*m_model->get_iter(m_path_for_menu))[m_columns.m_type]) { + case MemberListRenderType::Role: + OnRoleSubmenuPopup(); + m_menu_role.popup_at_pointer(reinterpret_cast(ev)); + break; + case MemberListRenderType::Member: + Abaddon::Get().ShowUserMenu( + reinterpret_cast(ev), + static_cast((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), + m_active_guild); + break; + } } + return true; + } + return false; +} - return false; - }); +void MemberList::OnRoleSubmenuPopup() { +} + +int MemberList::SortFunc(const Gtk::TreeModel::iterator &a, const Gtk::TreeModel::iterator &b) { + if ((*a)[m_columns.m_type] == MemberListRenderType::Role) { + return (*b)[m_columns.m_sort] - (*a)[m_columns.m_sort]; + } else if ((*a)[m_columns.m_type] == MemberListRenderType::Member) { + return static_cast((*a)[m_columns.m_name]).compare((*b)[m_columns.m_name]); + } + return 0; +} + +MemberList::ModelColumns::ModelColumns() { + add(m_type); + add(m_id); + add(m_name); + add(m_pixbuf); + add(m_av_requested); + add(m_color); + add(m_sort); } diff --git a/src/components/memberlist.hpp b/src/components/memberlist.hpp index 7d5da101..cb6c1916 100644 --- a/src/components/memberlist.hpp +++ b/src/components/memberlist.hpp @@ -1,43 +1,58 @@ #pragma once -#include -#include -#include -#include "discord/discord.hpp" +#include +#include +#include +#include -class LazyImage; -class StatusIndicator; -class MemberListUserRow : public Gtk::ListBoxRow { -public: - MemberListUserRow(const std::optional &guild, const UserData &data); +#include - Snowflake ID; - -private: - Gtk::EventBox *m_ev; - Gtk::Box *m_box; - LazyImage *m_avatar; - StatusIndicator *m_status_indicator; - Gtk::Label *m_label; - Gtk::Image *m_crown = nullptr; -}; +#include "cellrenderermemberlist.hpp" +#include "discord/snowflake.hpp" class MemberList { public: MemberList(); - Gtk::Widget *GetRoot() const; + Gtk::Widget *GetRoot(); void UpdateMemberList(); void Clear(); void SetActiveChannel(Snowflake id); private: - void AttachUserMenuHandler(Gtk::ListBoxRow *row, Snowflake id); + void OnCellRender(uint64_t id); + bool OnButtonPressEvent(GdkEventButton *ev); + + void OnRoleSubmenuPopup(); + + int SortFunc(const Gtk::TreeModel::iterator &a, const Gtk::TreeModel::iterator &b); + + class ModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + ModelColumns(); + + Gtk::TreeModelColumn m_type; + Gtk::TreeModelColumn m_id; + Gtk::TreeModelColumn m_name; + Gtk::TreeModelColumn> m_pixbuf; + Gtk::TreeModelColumn m_color; + Gtk::TreeModelColumn m_sort; + + Gtk::TreeModelColumn m_av_requested; + }; + + ModelColumns m_columns; + Glib::RefPtr m_model; + Gtk::TreeView m_view; + + Gtk::TreePath m_path_for_menu; + + Gtk::ScrolledWindow m_main; - Gtk::ScrolledWindow *m_main; - Gtk::ListBox *m_listbox; + Snowflake m_active_channel; + Snowflake m_active_guild; - Snowflake m_guild_id; - Snowflake m_chan_id; + Gtk::Menu m_menu_role; + Gtk::MenuItem m_menu_role_copy_id; - std::unordered_map m_id_to_row; + std::unordered_map m_pending_avatars; }; diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index ccc61b46..0618e720 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -261,6 +261,21 @@ Snowflake DiscordClient::GetMemberHoistedRole(Snowflake guild_id, Snowflake user return top_role.has_value() ? top_role->ID : Snowflake::Invalid; } +std::optional DiscordClient::GetMemberHoistedRoleCached(const GuildMember &member, const std::unordered_map &roles, bool with_color) const { + std::optional top_role; + for (const auto id : member.Roles) { + if (const auto iter = roles.find(id); iter != roles.end()) { + const auto &role = iter->second; + if ((with_color && role.Color != 0x000000) || (!with_color && role.IsHoisted)) { + if (!top_role.has_value() || top_role->Position < role.Position) { + top_role = role; + } + } + } + } + return top_role; +} + std::optional DiscordClient::GetMemberHighestRole(Snowflake guild_id, Snowflake user_id) const { const auto data = GetMember(user_id, guild_id); if (!data.has_value()) return std::nullopt; diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index ebbf5f9c..cb14a52b 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -18,7 +18,7 @@ #include #ifdef GetMessage - #undef GetMessage +#undef GetMessage #endif class Abaddon; @@ -55,6 +55,7 @@ class DiscordClient { std::optional GetGuild(Snowflake id) const; std::optional GetMember(Snowflake user_id, Snowflake guild_id) const; Snowflake GetMemberHoistedRole(Snowflake guild_id, Snowflake user_id, bool with_color = false) const; + std::optional GetMemberHoistedRoleCached(const GuildMember &member, const std::unordered_map &roles, bool with_color = false) const; std::optional GetMemberHighestRole(Snowflake guild_id, Snowflake user_id) const; std::set GetUsersInGuild(Snowflake id) const; std::set GetChannelsInGuild(Snowflake id) const; @@ -162,6 +163,11 @@ class DiscordClient { }); } + template + std::vector GetUsersBulk(Iter begin, Iter end) { + return m_store.GetUsersBulk(begin, end); + } + // FetchGuildBans fetches all bans+reasons via api, this func fetches stored bans (so usually just GUILD_BAN_ADD data) std::vector GetBansInGuild(Snowflake guild_id); void FetchGuildBan(Snowflake guild_id, Snowflake user_id, const sigc::slot &callback); diff --git a/src/discord/store.cpp b/src/discord/store.cpp index d8994c42..dfeb7d1a 100644 --- a/src/discord/store.cpp +++ b/src/discord/store.cpp @@ -27,18 +27,7 @@ Store::Store(bool mem_store) m_ok &= CreateStatements(); } -Store::~Store() { - m_db.Close(); - if (!m_db.OK()) { - fprintf(stderr, "error closing database: %s\n", m_db.ErrStr()); - return; - } - - if (m_db_path != ":memory:") { - std::error_code ec; - std::filesystem::remove(m_db_path, ec); - } -} +Store::~Store() {} bool Store::IsValid() const { return m_db.OK() && m_ok; @@ -519,7 +508,6 @@ std::optional Store::GetWebhookMessage(Snowflake message_id) return data; } - Snowflake Store::GetGuildOwner(Snowflake guild_id) const { auto &s = m_stmt_get_guild_owner; @@ -961,6 +949,21 @@ std::optional Store::GetMessage(Snowflake id) const { return top; } +UserData Store::GetUserBound(Statement *stmt) const { + UserData u; + stmt->Get(0, u.ID); + stmt->Get(1, u.Username); + stmt->Get(2, u.Discriminator); + stmt->Get(3, u.Avatar); + stmt->Get(4, u.IsBot); + stmt->Get(5, u.IsSystem); + stmt->Get(6, u.IsMFAEnabled); + stmt->Get(7, u.PremiumType); + stmt->Get(8, u.PublicFlags); + stmt->Get(9, u.GlobalName); + return u; +} + Message Store::GetMessageBound(std::unique_ptr &s) const { Message r; @@ -1137,18 +1140,7 @@ std::optional Store::GetUser(Snowflake id) const { return {}; } - UserData r; - - r.ID = id; - s->Get(1, r.Username); - s->Get(2, r.Discriminator); - s->Get(3, r.Avatar); - s->Get(4, r.IsBot); - s->Get(5, r.IsSystem); - s->Get(6, r.IsMFAEnabled); - s->Get(7, r.PremiumType); - s->Get(8, r.PublicFlags); - s->Get(9, r.GlobalName); + auto r = GetUserBound(s.get()); s->Reset(); @@ -2360,7 +2352,8 @@ bool Store::CreateStatements() { return true; } -Store::Database::Database(const char *path) { +Store::Database::Database(const char *path) + : m_db_path(path) { if (path != ":memory:"s) { std::error_code ec; if (std::filesystem::exists(path, ec) && !std::filesystem::remove(path, ec)) { @@ -2377,9 +2370,18 @@ Store::Database::~Database() { int Store::Database::Close() { if (m_db == nullptr) return m_err; - m_signal_close.emit(); m_err = sqlite3_close(m_db); m_db = nullptr; + + if (!OK()) { + fprintf(stderr, "error closing database: %s\n", ErrStr()); + } else { + if (m_db_path != ":memory:") { + std::error_code ec; + std::filesystem::remove(m_db_path, ec); + } + } + return m_err; } @@ -2420,17 +2422,9 @@ sqlite3 *Store::Database::obj() { return m_db; } -Store::Database::type_signal_close Store::Database::signal_close() { - return m_signal_close; -} - Store::Statement::Statement(Database &db, const char *command) : m_db(&db) { if (m_db->SetError(sqlite3_prepare_v2(m_db->obj(), command, -1, &m_stmt, nullptr)) != SQLITE_OK) return; - m_db->signal_close().connect([this] { - sqlite3_finalize(m_stmt); - m_stmt = nullptr; - }); } Store::Statement::~Statement() { diff --git a/src/discord/store.hpp b/src/discord/store.hpp index b6979d0b..6157f09f 100644 --- a/src/discord/store.hpp +++ b/src/discord/store.hpp @@ -11,6 +11,9 @@ #endif class Store { +private: + class Statement; + public: Store(bool mem_store = false); ~Store(); @@ -51,6 +54,36 @@ class Store { std::unordered_set GetMembersInGuild(Snowflake guild_id) const; // ^ not the same as GetUsersInGuild since users in a guild may include users who do not have retrieved member data + template + std::vector GetUsersBulk(Iter begin, Iter end) { + const int size = std::distance(begin, end); + if (size == 0) return {}; + + std::string query = "SELECT * FROM USERS WHERE id IN ("; + for (int i = 0; i < size; i++) { + query += "?, "; + } + query.resize(query.size() - 2); // chop off extra ", " + query += ")"; + + Statement s(m_db, query.c_str()); + if (!s.OK()) { + printf("failed to prepare bulk users: %s\n", m_db.ErrStr()); + return {}; + } + + for (int i = 0; begin != end; i++, begin++) { + s.Bind(i, *begin); + } + + std::vector r; + r.reserve(size); + while (s.FetchOne()) { + r.push_back(GetUserBound(&s)); + } + return r; + } + void AddReaction(const MessageReactionAddObject &data, bool byself); void RemoveReaction(const MessageReactionRemoveObject &data, bool byself); @@ -69,7 +102,6 @@ class Store { void EndTransaction(); private: - class Statement; class Database { public: Database(const char *path); @@ -89,13 +121,7 @@ class Store { sqlite3 *m_db; int m_err = SQLITE_OK; mutable char m_err_scratch[256] { 0 }; - - // stupid shit i dont like to allow closing properly - using type_signal_close = sigc::signal; - type_signal_close m_signal_close; - - public: - type_signal_close signal_close(); + std::filesystem::path m_db_path; }; class Statement { @@ -242,6 +268,7 @@ class Store { sqlite3_stmt *m_stmt; }; + UserData GetUserBound(Statement *stmt) const; Message GetMessageBound(std::unique_ptr &stmt) const; static RoleData GetRoleBound(std::unique_ptr &stmt);