Merge pull request #220 from uowuo/member-list

Rewrite member list (for real this time)
This commit is contained in:
ouwou 2023-10-03 02:10:20 +00:00 committed by GitHub
commit 483b547a64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 541 additions and 265 deletions

View File

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

View File

@ -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<MemberListRenderType> CellRendererMemberList::property_type() {
return m_property_type.get_proxy();
}
Glib::PropertyProxy<uint64_t> CellRendererMemberList::property_id() {
return m_property_id.get_proxy();
}
Glib::PropertyProxy<Glib::ustring> CellRendererMemberList::property_name() {
return m_property_name.get_proxy();
}
Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> CellRendererMemberList::property_pixbuf() {
return m_property_pixbuf.get_proxy();
}
Glib::PropertyProxy<Gdk::RGBA> 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<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 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<Cairo::Context> &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<Cairo::Context> &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;
}

View File

@ -0,0 +1,65 @@
#pragma once
#include <gtkmm/cellrenderer.h>
enum class MemberListRenderType : uint8_t {
Role,
Member,
};
class CellRendererMemberList : public Gtk::CellRenderer {
public:
CellRendererMemberList();
~CellRendererMemberList() override = default;
Glib::PropertyProxy<MemberListRenderType> property_type();
Glib::PropertyProxy<uint64_t> property_id();
Glib::PropertyProxy<Glib::ustring> property_name();
Glib::PropertyProxy<Glib::RefPtr<Gdk::Pixbuf>> property_pixbuf();
Glib::PropertyProxy<Gdk::RGBA> 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<Cairo::Context> &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<Cairo::Context> &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<Cairo::Context> &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<MemberListRenderType> m_property_type;
Glib::Property<uint64_t> m_property_id;
Glib::Property<Glib::ustring> m_property_name;
Glib::Property<Glib::RefPtr<Gdk::Pixbuf>> m_property_pixbuf;
Glib::Property<Gdk::RGBA> m_property_color;
using type_signal_render = sigc::signal<void(uint64_t)>;
type_signal_render m_signal_render;
public:
type_signal_render signal_render();
};

View File

@ -1,233 +1,252 @@
#include "memberlist.hpp"
#include "lazyimage.hpp"
#include "statusindicator.hpp"
constexpr static const int MaxMemberListRows = 200;
constexpr static int MemberListUserLimit = 200;
MemberListUserRow::MemberListUserRow(const std::optional<GuildData> &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));
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");
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_view.set_hexpand(true);
m_view.set_vexpand(true);
m_status_indicator->set_margin_start(3);
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);
if (guild.has_value())
m_avatar->SetURL(data.GetAvatarURL(guild->ID, "png"));
else
m_avatar->SetURL(data.GetAvatarURL("png"));
m_main.add(m_view);
m_main.show_all_children();
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");
auto *column = Gtk::make_managed<Gtk::TreeView::Column>("display");
auto *renderer = Gtk::make_managed<CellRendererMemberList>();
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_label->set_single_line_mode(true);
m_label->set_ellipsize(Pango::ELLIPSIZE_END);
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));
// 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;
}
}
renderer->signal_render().connect(sigc::mem_fun(*this, &MemberList::OnCellRender));
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("<span color='#" + IntToCSSColor(color) + "'>" + Glib::Markup::escape_text(display) + "</span>");
} else {
m_label->set_text(display);
}
} else {
m_label->set_text(display);
}
// Menu stuff
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_menu_role.append(m_menu_role_copy_id);
m_menu_role.show_all();
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]));
});
}
MemberList::MemberList() {
m_main = Gtk::manage(new Gtk::ScrolledWindow);
m_listbox = Gtk::manage(new Gtk::ListBox);
m_listbox->get_style_context()->add_class("members");
m_listbox->set_selection_mode(Gtk::SELECTION_NONE);
m_main->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
m_main->add(*m_listbox);
m_main->show_all();
}
Gtk::Widget *MemberList::GetRoot() const {
return m_main;
}
void MemberList::Clear() {
SetActiveChannel(Snowflake::Invalid);
UpdateMemberList();
}
void MemberList::SetActiveChannel(Snowflake id) {
m_chan_id = id;
m_guild_id = Snowflake::Invalid;
if (m_chan_id.IsValid()) {
const auto chan = Abaddon::Get().GetDiscordClient().GetChannel(id);
if (chan.has_value() && chan->GuildID.has_value()) m_guild_id = *chan->GuildID;
}
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<Snowflake> 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<int, RoleData> pos_to_role;
std::map<int, std::vector<UserData>> pos_to_users;
std::unordered_map<Snowflake, std::vector<UserData>> role_to_users;
std::unordered_map<Snowflake, int> user_to_color;
std::vector<Snowflake> roleless_users;
std::vector<UserData> 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<Snowflake, RoleData> 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("<b>" + Glib::Markup::escape_text(name) + "</b>");
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] = "<b>" + role.GetEscapedName() + "</b>";
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);
if (pos_to_users.find(pos) == pos_to_users.end()) continue;
auto &users = pos_to_users.at(pos);
AlphabeticalSort(users.begin(), users.end(), [](const auto &e) { return e.Username; });
for (const auto &data : users)
if (!add_user(data)) return;
}
if (chan->Type == ChannelType::DM || chan->Type == ChannelType::GROUP_DM)
add_role("Users");
else
add_role("@everyone");
for (const auto &id : roleless_users) {
const auto user = discord.GetUser(id);
if (user.has_value())
if (!add_user(*user)) return;
}
}
void MemberList::AttachUserMenuHandler(Gtk::ListBoxRow *row, Snowflake id) {
row->signal_button_press_event().connect([this, id](GdkEventButton *e) -> bool {
if (e->type == GDK_BUTTON_PRESS && e->button == GDK_BUTTON_SECONDARY) {
Abaddon::Get().ShowUserMenu(reinterpret_cast<const GdkEvent *>(e), id, m_guild_id);
return true;
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);
}
}
return false;
});
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] = "<b>@everyone</b>";
everyone_role[m_columns.m_sort] = 0;
for (const auto &user : roleless_users) {
add_user(user, everyone_role);
}
// 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;
}
}
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<Gdk::Pixbuf> &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);
}
}
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<int>(ev->x), static_cast<int>(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<GdkEvent *>(ev));
break;
case MemberListRenderType::Member:
Abaddon::Get().ShowUserMenu(
reinterpret_cast<GdkEvent *>(ev),
static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]),
m_active_guild);
break;
}
}
return true;
}
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<Glib::ustring>((*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);
}

View File

@ -1,43 +1,58 @@
#pragma once
#include <mutex>
#include <gdkmm/pixbuf.h>
#include <gtkmm/treemodel.h>
#include <gtkmm/treestore.h>
#include <gtkmm/treeview.h>
#include <unordered_map>
#include <optional>
#include "discord/discord.hpp"
class LazyImage;
class StatusIndicator;
class MemberListUserRow : public Gtk::ListBoxRow {
public:
MemberListUserRow(const std::optional<GuildData> &guild, const UserData &data);
Snowflake ID;
private:
Gtk::EventBox *m_ev;
Gtk::Box *m_box;
LazyImage *m_avatar;
StatusIndicator *m_status_indicator;
Gtk::Label *m_label;
Gtk::Image *m_crown = nullptr;
};
#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);
Gtk::ScrolledWindow *m_main;
Gtk::ListBox *m_listbox;
void OnRoleSubmenuPopup();
Snowflake m_guild_id;
Snowflake m_chan_id;
int SortFunc(const Gtk::TreeModel::iterator &a, const Gtk::TreeModel::iterator &b);
std::unordered_map<Snowflake, Gtk::ListBoxRow *> m_id_to_row;
class ModelColumns : public Gtk::TreeModel::ColumnRecord {
public:
ModelColumns();
Gtk::TreeModelColumn<MemberListRenderType> m_type;
Gtk::TreeModelColumn<uint64_t> m_id;
Gtk::TreeModelColumn<Glib::ustring> m_name;
Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> m_pixbuf;
Gtk::TreeModelColumn<Gdk::RGBA> m_color;
Gtk::TreeModelColumn<int> m_sort;
Gtk::TreeModelColumn<bool> m_av_requested;
};
ModelColumns m_columns;
Glib::RefPtr<Gtk::TreeStore> m_model;
Gtk::TreeView m_view;
Gtk::TreePath m_path_for_menu;
Gtk::ScrolledWindow m_main;
Snowflake m_active_channel;
Snowflake m_active_guild;
Gtk::Menu m_menu_role;
Gtk::MenuItem m_menu_role_copy_id;
std::unordered_map<Snowflake, Gtk::TreeIter> m_pending_avatars;
};

View File

@ -261,6 +261,21 @@ Snowflake DiscordClient::GetMemberHoistedRole(Snowflake guild_id, Snowflake user
return top_role.has_value() ? top_role->ID : Snowflake::Invalid;
}
std::optional<RoleData> DiscordClient::GetMemberHoistedRoleCached(const GuildMember &member, const std::unordered_map<Snowflake, RoleData> &roles, bool with_color) const {
std::optional<RoleData> 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<RoleData> DiscordClient::GetMemberHighestRole(Snowflake guild_id, Snowflake user_id) const {
const auto data = GetMember(user_id, guild_id);
if (!data.has_value()) return std::nullopt;

View File

@ -18,7 +18,7 @@
#include <queue>
#ifdef GetMessage
#undef GetMessage
#undef GetMessage
#endif
class Abaddon;
@ -55,6 +55,7 @@ public:
std::optional<GuildData> GetGuild(Snowflake id) const;
std::optional<GuildMember> GetMember(Snowflake user_id, Snowflake guild_id) const;
Snowflake GetMemberHoistedRole(Snowflake guild_id, Snowflake user_id, bool with_color = false) const;
std::optional<RoleData> GetMemberHoistedRoleCached(const GuildMember &member, const std::unordered_map<Snowflake, RoleData> &roles, bool with_color = false) const;
std::optional<RoleData> GetMemberHighestRole(Snowflake guild_id, Snowflake user_id) const;
std::set<Snowflake> GetUsersInGuild(Snowflake id) const;
std::set<Snowflake> GetChannelsInGuild(Snowflake id) const;
@ -162,6 +163,11 @@ public:
});
}
template<typename Iter>
std::vector<UserData> 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<BanData> GetBansInGuild(Snowflake guild_id);
void FetchGuildBan(Snowflake guild_id, Snowflake user_id, const sigc::slot<void(BanData)> &callback);

View File

@ -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<WebhookMessageData> 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<Message> 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<Statement> &s) const {
Message r;
@ -1137,18 +1140,7 @@ std::optional<UserData> 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() {

View File

@ -11,6 +11,9 @@
#endif
class Store {
private:
class Statement;
public:
Store(bool mem_store = false);
~Store();
@ -51,6 +54,36 @@ public:
std::unordered_set<Snowflake> 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<typename Iter>
std::vector<UserData> 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<UserData> 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 @@ public:
void EndTransaction();
private:
class Statement;
class Database {
public:
Database(const char *path);
@ -89,13 +121,7 @@ private:
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<void>;
type_signal_close m_signal_close;
public:
type_signal_close signal_close();
std::filesystem::path m_db_path;
};
class Statement {
@ -242,6 +268,7 @@ private:
sqlite3_stmt *m_stmt;
};
UserData GetUserBound(Statement *stmt) const;
Message GetMessageBound(std::unique_ptr<Statement> &stmt) const;
static RoleData GetRoleBound(std::unique_ptr<Statement> &stmt);