From a51a54bc5979a2491f152abc47ad54e6b63f27c8 Mon Sep 17 00:00:00 2001 From: Dylam De La Torre Date: Tue, 23 Nov 2021 05:21:56 +0100 Subject: Restructure source and resource files (#46) importantly, res is now res/res and css is now res/css --- src/windows/guildsettings/auditlogpane.cpp | 636 +++++++++++++++++++++++++++++ src/windows/guildsettings/auditlogpane.hpp | 19 + src/windows/guildsettings/banspane.cpp | 161 ++++++++ src/windows/guildsettings/banspane.hpp | 45 ++ src/windows/guildsettings/emojispane.cpp | 257 ++++++++++++ src/windows/guildsettings/emojispane.hpp | 53 +++ src/windows/guildsettings/infopane.cpp | 220 ++++++++++ src/windows/guildsettings/infopane.hpp | 26 ++ src/windows/guildsettings/invitespane.cpp | 136 ++++++ src/windows/guildsettings/invitespane.hpp | 43 ++ src/windows/guildsettings/memberspane.cpp | 410 +++++++++++++++++++ src/windows/guildsettings/memberspane.hpp | 135 ++++++ src/windows/guildsettings/rolespane.cpp | 419 +++++++++++++++++++ src/windows/guildsettings/rolespane.hpp | 102 +++++ 14 files changed, 2662 insertions(+) create mode 100644 src/windows/guildsettings/auditlogpane.cpp create mode 100644 src/windows/guildsettings/auditlogpane.hpp create mode 100644 src/windows/guildsettings/banspane.cpp create mode 100644 src/windows/guildsettings/banspane.hpp create mode 100644 src/windows/guildsettings/emojispane.cpp create mode 100644 src/windows/guildsettings/emojispane.hpp create mode 100644 src/windows/guildsettings/infopane.cpp create mode 100644 src/windows/guildsettings/infopane.hpp create mode 100644 src/windows/guildsettings/invitespane.cpp create mode 100644 src/windows/guildsettings/invitespane.hpp create mode 100644 src/windows/guildsettings/memberspane.cpp create mode 100644 src/windows/guildsettings/memberspane.hpp create mode 100644 src/windows/guildsettings/rolespane.cpp create mode 100644 src/windows/guildsettings/rolespane.hpp (limited to 'src/windows/guildsettings') diff --git a/src/windows/guildsettings/auditlogpane.cpp b/src/windows/guildsettings/auditlogpane.cpp new file mode 100644 index 0000000..08f99da --- /dev/null +++ b/src/windows/guildsettings/auditlogpane.cpp @@ -0,0 +1,636 @@ +#include "auditlogpane.hpp" +#include "abaddon.hpp" + +using namespace std::string_literals; + +GuildSettingsAuditLogPane::GuildSettingsAuditLogPane(Snowflake id) + : GuildID(id) { + signal_map().connect(sigc::mem_fun(*this, &GuildSettingsAuditLogPane::OnMap)); + set_name("guild-audit-log-pane"); + set_hexpand(true); + set_vexpand(true); + + m_list.set_selection_mode(Gtk::SELECTION_NONE); + m_list.show(); + add(m_list); +} + +void GuildSettingsAuditLogPane::OnMap() { + if (m_requested) return; + m_requested = true; + + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto self_id = discord.GetUserData().ID; + + if (discord.HasGuildPermission(self_id, GuildID, Permission::VIEW_AUDIT_LOG)) + discord.FetchAuditLog(GuildID, sigc::mem_fun(*this, &GuildSettingsAuditLogPane::OnAuditLogFetch)); +} + +void GuildSettingsAuditLogPane::OnAuditLogFetch(const AuditLogData &data) { + auto &discord = Abaddon::Get().GetDiscordClient(); + auto guild = *discord.GetGuild(GuildID); + for (const auto &entry : data.Entries) { + if (entry.TargetID == "") continue; + + auto expander = Gtk::manage(new Gtk::Expander); + auto label = Gtk::manage(new Gtk::Label); + label->set_ellipsize(Pango::ELLIPSIZE_END); + + Glib::ustring user_markup = "Unknown User"; + if (entry.UserID.has_value()) { + if (auto user = discord.GetUser(*entry.UserID); user.has_value()) + user_markup = discord.GetUser(*entry.UserID)->GetEscapedBoldString(); + } + + // spaghetti moment + Glib::ustring markup; + std::vector extra_markup; + switch (entry.Type) { + case AuditLogActionType::GUILD_UPDATE: { + markup = + user_markup + + " made changes to " + + Glib::Markup::escape_text(guild.Name) + + ""; + + if (entry.Changes.has_value()) + for (const auto &change : *entry.Changes) { + if (change.Key == "icon_hash") { + extra_markup.push_back("Set the server icon"); + } else if (change.Key == "name") { + auto new_name = change.NewValue; + if (new_name.has_value()) + extra_markup.push_back("Set the server name to " + + Glib::Markup::escape_text(new_name->get()) + + ""); + else + extra_markup.push_back("Set the server name"); + } + } + } break; + case AuditLogActionType::CHANNEL_CREATE: { + const auto type = *entry.GetNewFromKey("type"); + markup = user_markup + + " created a " + (type == ChannelType::GUILD_VOICE ? "voice" : "text") + + " channel #" + + Glib::Markup::escape_text(*entry.GetNewFromKey("name")) + + ""; + if (entry.Changes.has_value()) + for (const auto &change : *entry.Changes) { + if (change.Key == "name" && change.NewValue.has_value()) + extra_markup.push_back("Set the name to " + + Glib::Markup::escape_text(change.NewValue->get()) + + ""); + else if (change.Key == "nsfw" && change.NewValue.has_value()) + extra_markup.push_back((*change.NewValue ? "Marked" : "Unmarked") + + " the channel as NSFW"s); + } + + } break; + case AuditLogActionType::CHANNEL_UPDATE: { + const auto target_channel = discord.GetChannel(entry.TargetID); + if (target_channel.has_value()) { + markup = user_markup + + " made changes to #" + + Glib::Markup::escape_text(*target_channel->Name) + + ""; + } else { + markup = user_markup + + " made changes to <#" + + entry.TargetID + + ">"; + } + if (entry.Changes.has_value()) + for (const auto &change : *entry.Changes) { + if (change.Key == "name" && change.NewValue.has_value()) { + if (change.OldValue.has_value()) + extra_markup.push_back("Changed the name from " + + Glib::Markup::escape_text(change.OldValue->get()) + + " to " + + Glib::Markup::escape_text(change.NewValue->get()) + + ""); + else + extra_markup.push_back("Changed the name to " + + Glib::Markup::escape_text(change.NewValue->get()) + + ""); + } else if (change.Key == "topic") { + if (change.NewValue.has_value()) + extra_markup.push_back("Changed the topic to " + + Glib::Markup::escape_text(change.NewValue->get()) + + ""); + else + extra_markup.push_back("Cleared the topic"); + } else if (change.Key == "nsfw" && change.NewValue.has_value()) { + extra_markup.push_back((*change.NewValue ? "Marked" : "Unmarked") + " the channel as NSFW"s); + } else if (change.Key == "rate_limit_per_user" && change.NewValue.has_value()) { + const int secs = change.NewValue->get(); + if (secs == 0) + extra_markup.push_back("Disabled slowmode"); + else + extra_markup.push_back("Set slowmode to " + + std::to_string(secs) + " seconds"); + } + } + } break; + case AuditLogActionType::CHANNEL_DELETE: { + markup = user_markup + + " removed #" + + Glib::Markup::escape_text(*entry.GetOldFromKey("name")) + + ""; + } break; + case AuditLogActionType::CHANNEL_OVERWRITE_CREATE: { + const auto channel = discord.GetChannel(entry.TargetID); + if (channel.has_value()) { + markup = user_markup + + " created channel overrides for #" + + Glib::Markup::escape_text(*channel->Name) + ""; + } else { + markup = user_markup + + " created channel overrides for <#" + + entry.TargetID + ">"; + } + } break; + case AuditLogActionType::CHANNEL_OVERWRITE_UPDATE: { + const auto channel = discord.GetChannel(entry.TargetID); + if (channel.has_value()) { + markup = user_markup + + " updated channel overrides for #" + + Glib::Markup::escape_text(*channel->Name) + ""; + } else { + markup = user_markup + + " updated channel overrides for <#" + + entry.TargetID + ">"; + } + } break; + case AuditLogActionType::CHANNEL_OVERWRITE_DELETE: { + const auto channel = discord.GetChannel(entry.TargetID); + if (channel.has_value()) { + markup = user_markup + + " removed channel overrides for #" + + Glib::Markup::escape_text(*channel->Name) + ""; + } else { + markup = user_markup + + " removed channel overrides for <#" + + entry.TargetID + ">"; + } + } break; + case AuditLogActionType::MEMBER_KICK: { + const auto target_user = discord.GetUser(entry.TargetID); + markup = user_markup + + " kicked " + + target_user->GetEscapedString() + + ""; + } break; + case AuditLogActionType::MEMBER_PRUNE: { + markup = user_markup + + " pruned " + + *entry.Options->MembersRemoved + + " members"; + extra_markup.push_back("For " + + *entry.Options->DeleteMemberDays + + " days of inactivity"); + } break; + case AuditLogActionType::MEMBER_BAN_ADD: { + const auto target_user = discord.GetUser(entry.TargetID); + markup = user_markup + + " banned " + + target_user->GetEscapedString() + + ""; + } break; + case AuditLogActionType::MEMBER_BAN_REMOVE: { + const auto target_user = discord.GetUser(entry.TargetID); + markup = user_markup + + " removed the ban for " + + target_user->GetEscapedString() + + ""; + } break; + case AuditLogActionType::MEMBER_UPDATE: { + const auto target_user = discord.GetUser(entry.TargetID); + markup = user_markup + + " updated " + + target_user->GetEscapedString() + + ""; + if (entry.Changes.has_value()) + for (const auto &change : *entry.Changes) { + if (change.Key == "deaf" && change.NewValue.has_value()) + extra_markup.push_back( + (change.NewValue->get() ? "Deafened"s : "Undeafened"s) + + " them"); + else if (change.Key == "mute" && change.NewValue.has_value()) + extra_markup.push_back( + (change.NewValue->get() ? "Muted"s : "Unmuted"s) + + " them"); + else if (change.Key == "nick" && change.NewValue.has_value()) + extra_markup.push_back("Set their nickname to " + + Glib::Markup::escape_text(change.NewValue->get()) + + ""); + } + } break; + case AuditLogActionType::MEMBER_ROLE_UPDATE: { + const auto target_user = discord.GetUser(entry.TargetID); + markup = user_markup + + " updated roles for " + + target_user->GetEscapedString() + ""; + if (entry.Changes.has_value()) + for (const auto &change : *entry.Changes) { + if (change.Key == "$remove" && change.NewValue.has_value()) { + extra_markup.push_back("Removed a role " + + Glib::Markup::escape_text(change.NewValue.value()[0].at("name").get()) + + ""); + } else if (change.Key == "$add" && change.NewValue.has_value()) { + extra_markup.push_back("Added a role " + + Glib::Markup::escape_text(change.NewValue.value()[0].at("name").get()) + + ""); + } + } + } break; + case AuditLogActionType::MEMBER_MOVE: { + const auto channel = discord.GetChannel(*entry.Options->ChannelID); + markup = user_markup + + " moved " + + *entry.Options->Count + + " user" + + (*entry.Options->Count == "1" ? ""s : "s"s) + + " to " + + Glib::Markup::escape_text(*channel->Name) + + ""; + } break; + case AuditLogActionType::MEMBER_DISCONNECT: { + markup = user_markup + + " disconnected " + + *entry.Options->Count + + " users from voice"; + } break; + case AuditLogActionType::BOT_ADD: { + const auto target_user = discord.GetUser(entry.TargetID); + markup = user_markup + + " added " + + target_user->GetEscapedString() + + " to the server"; + } break; + case AuditLogActionType::ROLE_CREATE: { + markup = user_markup + + " created the role " + + *entry.GetNewFromKey("name") + + ""; + } break; + case AuditLogActionType::ROLE_UPDATE: { + const auto role = discord.GetRole(entry.TargetID); + markup = user_markup + + " updated the role " + + (role.has_value() ? Glib::Markup::escape_text(role->Name) : Glib::ustring(entry.TargetID)) + + ""; + if (entry.Changes.has_value()) + for (const auto &change : *entry.Changes) { + if (change.Key == "name" && change.NewValue.has_value()) { + extra_markup.push_back("Changed the name to " + + Glib::Markup::escape_text(change.NewValue->get()) + + ""); + } else if (change.Key == "color" && change.NewValue.has_value()) { + const auto col = change.NewValue->get(); + if (col == 0) + extra_markup.push_back("Removed the color"); + else + extra_markup.push_back("Set the color to " + + IntToCSSColor(col) + + ""); + } else if (change.Key == "permissions") { + extra_markup.push_back("Updated the permissions"); + } else if (change.Key == "mentionable" && change.NewValue.has_value()) { + extra_markup.push_back(change.NewValue->get() ? "Mentionable" : "Not mentionable"); + } else if (change.Key == "hoist" && change.NewValue.has_value()) { + extra_markup.push_back(change.NewValue->get() ? "Not hoisted" : "Hoisted"); + } + } + } break; + case AuditLogActionType::ROLE_DELETE: { + markup = user_markup + + " deleted the role " + + *entry.GetOldFromKey("name") + + ""; + } break; + case AuditLogActionType::INVITE_CREATE: { + const auto code = *entry.GetNewFromKey("code"); + markup = user_markup + + " created an invite " + code + ""; + if (entry.Changes.has_value()) + for (const auto &change : *entry.Changes) { + if (change.Key == "channel_id" && change.NewValue.has_value()) { + const auto channel = discord.GetChannel(change.NewValue->get()); + if (!channel.has_value()) continue; + extra_markup.push_back("For channel #" + + Glib::Markup::escape_text(*channel->Name) + + ""); + } else if (change.Key == "max_uses" && change.NewValue.has_value()) { + const auto uses = change.NewValue->get(); + if (uses == 0) + extra_markup.push_back("Which has unlimited uses"); + else + extra_markup.push_back("Which has " + std::to_string(uses) + " uses"); + } else if (change.Key == "temporary" && change.NewValue.has_value()) { + extra_markup.push_back("With temporary "s + + (change.NewValue->get() ? "on" : "off") + + ""); + } // no max_age cuz fuck time + } + } break; + case AuditLogActionType::INVITE_DELETE: { + markup = user_markup + + " deleted an invite " + + *entry.GetOldFromKey("code") + + ""; + } break; + case AuditLogActionType::WEBHOOK_CREATE: { + markup = user_markup + + " created the webhook " + + Glib::Markup::escape_text(*entry.GetNewFromKey("name")) + + ""; + for (const auto &change : *entry.Changes) { + if (change.Key == "channel_id" && change.NewValue.has_value()) { + const auto channel = discord.GetChannel(change.NewValue->get()); + if (channel.has_value()) { + extra_markup.push_back("With channel #" + + Glib::Markup::escape_text(*channel->Name) + + ""); + } + } + } + } break; + case AuditLogActionType::WEBHOOK_UPDATE: { + const WebhookData *webhookptr = nullptr; + for (const auto &webhook : data.Webhooks) { + if (webhook.ID == entry.TargetID) + webhookptr = &webhook; + } + if (webhookptr != nullptr) { + markup = user_markup + + " updated the webhook " + + Glib::Markup::escape_text(webhookptr->Name) + + ""; + } else { + markup = user_markup + + " updated a webhook"; + } + if (entry.Changes.has_value()) + for (const auto &change : *entry.Changes) { + if (change.Key == "name" && change.NewValue.has_value()) { + extra_markup.push_back("Changed the name to " + + Glib::Markup::escape_text(change.NewValue->get()) + + ""); + } else if (change.Key == "avatar_hash") { + extra_markup.push_back("Changed the avatar"); + } else if (change.Key == "channel_id" && change.NewValue.has_value()) { + const auto channel = discord.GetChannel(change.NewValue->get()); + if (channel.has_value()) { + extra_markup.push_back("Changed the channel to #" + + Glib::Markup::escape_text(*channel->Name) + + ""); + } else { + extra_markup.push_back("Changed the channel"); + } + } + } + } break; + case AuditLogActionType::WEBHOOK_DELETE: { + markup = user_markup + + " deleted the webhook " + + Glib::Markup::escape_text(*entry.GetOldFromKey("name")) + + ""; + } break; + case AuditLogActionType::EMOJI_CREATE: { + markup = user_markup + + " created the emoji " + + Glib::Markup::escape_text(*entry.GetNewFromKey("name")) + + ""; + } break; + case AuditLogActionType::EMOJI_UPDATE: { + markup = user_markup + + " updated the emoji " + + Glib::Markup::escape_text(*entry.GetOldFromKey("name")) + + ""; + extra_markup.push_back("Changed the name from " + + Glib::Markup::escape_text(*entry.GetOldFromKey("name")) + + " to " + + Glib::Markup::escape_text(*entry.GetNewFromKey("name")) + + ""); + } break; + case AuditLogActionType::EMOJI_DELETE: { + markup = user_markup + + " deleted the emoji " + + Glib::Markup::escape_text(*entry.GetOldFromKey("name")) + + ""; + } break; + case AuditLogActionType::MESSAGE_DELETE: { + const auto channel = discord.GetChannel(*entry.Options->ChannelID); + const auto count = *entry.Options->Count; + if (channel.has_value()) { + markup = user_markup + + " deleted " + count + " messages in #" + + Glib::Markup::escape_text(*channel->Name) + + ""; + } else { + markup = user_markup + + " deleted " + count + " messages"; + } + } break; + case AuditLogActionType::MESSAGE_BULK_DELETE: { + const auto channel = discord.GetChannel(entry.TargetID); + if (channel.has_value()) { + markup = user_markup + + " deleted " + + *entry.Options->Count + + " messages in #" + + Glib::Markup::escape_text(*channel->Name) + + ""; + } else { + markup = user_markup + + " deleted " + + *entry.Options->Count + + " messages"; + } + } break; + case AuditLogActionType::MESSAGE_PIN: { + const auto target_user = discord.GetUser(entry.TargetID); + markup = user_markup + + " pinned a message by " + + target_user->GetEscapedString() + + ""; + } break; + case AuditLogActionType::MESSAGE_UNPIN: { + const auto target_user = discord.GetUser(entry.TargetID); + markup = user_markup + + " unpinned a message by " + + target_user->GetEscapedString() + + ""; + } break; + case AuditLogActionType::STAGE_INSTANCE_CREATE: { + const auto channel = discord.GetChannel(*entry.Options->ChannelID); + if (channel.has_value()) { + markup = user_markup + + " started the stage for " + + Glib::Markup::escape_text(*channel->Name) + + ""; + } else { + markup = user_markup + + " started the stage for " + + std::to_string(*entry.Options->ChannelID) + + ""; + } + + if (entry.Changes.has_value()) { + for (const auto &change : *entry.Changes) { + if (change.Key == "topic" && change.NewValue.has_value()) { + extra_markup.push_back( + "Set the topic to " + + Glib::Markup::escape_text(change.NewValue->get()) + + ""); + } else if (change.Key == "privacy_level" && change.NewValue.has_value()) { + Glib::ustring str = Glib::Markup::escape_text(GetStagePrivacyDisplayString(change.NewValue->get())); + extra_markup.push_back( + "Set the privacy level to " + + str + + ""); + } + } + } + } break; + case AuditLogActionType::STAGE_INSTANCE_UPDATE: { + const auto channel = discord.GetChannel(*entry.Options->ChannelID); + if (channel.has_value()) { + markup = user_markup + + " updated the stage for " + + Glib::Markup::escape_text(*channel->Name) + + ""; + } else { + markup = user_markup + + " updated the stage for " + + std::to_string(*entry.Options->ChannelID) + + ""; + } + + if (entry.Changes.has_value()) { + for (const auto &change : *entry.Changes) { + if (change.Key == "topic" && change.NewValue.has_value()) { + extra_markup.push_back( + "Set the topic to " + + Glib::Markup::escape_text(change.NewValue->get()) + + ""); + } else if (change.Key == "privacy_level" && change.NewValue.has_value()) { + Glib::ustring str = Glib::Markup::escape_text(GetStagePrivacyDisplayString(change.NewValue->get())); + extra_markup.push_back( + "Set the privacy level to " + + str + + ""); + } + } + } + } break; + case AuditLogActionType::STAGE_INSTANCE_DELETE: { + const auto channel = discord.GetChannel(*entry.Options->ChannelID); + if (channel.has_value()) { + markup = user_markup + + " ended the stage for " + + Glib::Markup::escape_text(*channel->Name) + + ""; + } else { + markup = user_markup + + " ended the stage for " + + std::to_string(*entry.Options->ChannelID) + + ""; + } + } break; + case AuditLogActionType::THREAD_CREATE: { + const auto channel = discord.GetChannel(entry.TargetID); + markup = user_markup + + " created a thread " + + (channel.has_value() + ? Glib::Markup::escape_text(*channel->Name) + : Glib::ustring(*entry.GetNewFromKey("name"))) + + ""; + if (entry.Changes.has_value()) { + for (const auto &change : *entry.Changes) { + if (change.Key == "name") + extra_markup.push_back("Set the name to " + Glib::Markup::escape_text(change.NewValue->get()) + ""); + else if (change.Key == "archived") + extra_markup.push_back(change.NewValue->get() ? "Archived the thread" : "Unarchived the thread"); + else if (change.Key == "auto_archive_duration") + extra_markup.push_back("Set auto archive duration to "s + std::to_string(change.NewValue->get()) + " minutes"s); + else if (change.Key == "rate_limit_per_user" && change.NewValue.has_value()) { + const int secs = change.NewValue->get(); + if (secs == 0) + extra_markup.push_back("Disabled slowmode"); + else + extra_markup.push_back("Set slowmode to " + + std::to_string(secs) + " seconds"); + } else if (change.Key == "locked") + extra_markup.push_back(change.NewValue->get() ? "Locked the thread, restricting it to only be unarchived by moderators" : "Unlocked the thread, allowing it to be unarchived by non-moderators"); + } + } + } break; + case AuditLogActionType::THREAD_UPDATE: { + const auto channel = discord.GetChannel(entry.TargetID); + markup = user_markup + + " made changes to the thread " + + (channel.has_value() + ? Glib::Markup::escape_text(*channel->Name) + : Glib::ustring(entry.TargetID)) + + ""; + for (const auto &change : *entry.Changes) { + if (change.Key == "name") + extra_markup.push_back( + "Changed the name from " + + Glib::Markup::escape_text(change.OldValue->get()) + + " to " + + Glib::Markup::escape_text(change.NewValue->get()) + + ""); + else if (change.Key == "auto_archive_duration") + extra_markup.push_back("Set auto archive duration to "s + std::to_string(change.NewValue->get()) + " minutes"s); + else if (change.Key == "rate_limit_per_user" && change.NewValue.has_value()) { + const int secs = change.NewValue->get(); + if (secs == 0) + extra_markup.push_back("Disabled slowmode"); + else + extra_markup.push_back("Set slowmode to " + + std::to_string(secs) + + " seconds"); + } else if (change.Key == "locked") + extra_markup.push_back(change.NewValue->get() ? "Locked the thread, restricting it to only be unarchived by moderators" : "Unlocked the thread, allowing it to be unarchived by non-moderators"); + else if (change.Key == "archived") + extra_markup.push_back(change.NewValue->get() ? "Archived the thread" : "Unarchived the thread"); + } + } break; + case AuditLogActionType::THREAD_DELETE: { + markup = user_markup + + " deleted the thread " + Glib::Markup::escape_text(*entry.GetOldFromKey("name")) + ""; + } break; + default: + markup = "Unknown action"; + break; + } + + label->set_markup(markup); + expander->set_label_widget(*label); + + if (entry.Reason.has_value()) { + extra_markup.push_back("With reason " + + Glib::Markup::escape_text(*entry.Reason) + + ""); + } + + expander->set_expanded(true); + + auto contents = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + for (const auto &extra : extra_markup) { + auto extra_label = Gtk::manage(new Gtk::Label); + extra_label->set_markup(extra); + extra_label->set_halign(Gtk::ALIGN_START); + extra_label->set_margin_start(25); + extra_label->set_ellipsize(Pango::ELLIPSIZE_END); + contents->add(*extra_label); + } + expander->add(*contents); + expander->set_margin_bottom(5); + expander->show_all(); + m_list.add(*expander); + } +} diff --git a/src/windows/guildsettings/auditlogpane.hpp b/src/windows/guildsettings/auditlogpane.hpp new file mode 100644 index 0000000..ac12321 --- /dev/null +++ b/src/windows/guildsettings/auditlogpane.hpp @@ -0,0 +1,19 @@ +#pragma once +#include +#include "discord/objects.hpp" + +class GuildSettingsAuditLogPane : public Gtk::ScrolledWindow { +public: + GuildSettingsAuditLogPane(Snowflake id); + +private: + void OnMap(); + + bool m_requested = false; + + Gtk::ListBox m_list; + + void OnAuditLogFetch(const AuditLogData &data); + + Snowflake GuildID; +}; diff --git a/src/windows/guildsettings/banspane.cpp b/src/windows/guildsettings/banspane.cpp new file mode 100644 index 0000000..97a70c4 --- /dev/null +++ b/src/windows/guildsettings/banspane.cpp @@ -0,0 +1,161 @@ +#include "banspane.hpp" +#include "abaddon.hpp" + +// gtk_list_store_set_value: assertion 'column >= 0 && column < priv->n_columns' failed +// dont care to figure out why this happens cuz it doesnt seem to break anything + +GuildSettingsBansPane::GuildSettingsBansPane(Snowflake id) + : Gtk::Box(Gtk::ORIENTATION_VERTICAL) + , GuildID(id) + , m_model(Gtk::ListStore::create(m_columns)) + , m_menu_unban("Unban") + , m_menu_copy_id("Copy ID") { + signal_map().connect(sigc::mem_fun(*this, &GuildSettingsBansPane::OnMap)); + set_name("guild-bans-pane"); + set_hexpand(true); + set_vexpand(true); + + auto &discord = Abaddon::Get().GetDiscordClient(); + + discord.signal_guild_ban_add().connect(sigc::mem_fun(*this, &GuildSettingsBansPane::OnBanAdd)); + discord.signal_guild_ban_remove().connect(sigc::mem_fun(*this, &GuildSettingsBansPane::OnBanRemove)); + + const auto self_id = discord.GetUserData().ID; + const auto can_ban = discord.HasGuildPermission(self_id, GuildID, Permission::BAN_MEMBERS); + + if (!can_ban) { + for (const auto &ban : discord.GetBansInGuild(id)) + OnGuildBanFetch(ban); + + m_no_perms_note = Gtk::manage(new Gtk::Label("You do not have permission to see bans. However, bans made while you are connected will appear here")); + m_no_perms_note->set_single_line_mode(true); + m_no_perms_note->set_ellipsize(Pango::ELLIPSIZE_END); + m_no_perms_note->set_halign(Gtk::ALIGN_START); + add(*m_no_perms_note); + } + + m_menu_unban.signal_activate().connect(sigc::mem_fun(*this, &GuildSettingsBansPane::OnMenuUnban)); + m_menu_copy_id.signal_activate().connect(sigc::mem_fun(*this, &GuildSettingsBansPane::OnMenuCopyID)); + m_menu_unban.show(); + m_menu_copy_id.show(); + m_menu.append(m_menu_unban); + m_menu.append(m_menu_copy_id); + + m_view.signal_button_press_event().connect(sigc::mem_fun(*this, &GuildSettingsBansPane::OnTreeButtonPress), false); + m_view.show(); + + m_scroll.set_propagate_natural_height(true); + m_scroll.add(m_view); + add(m_scroll); + show_all_children(); + + m_view.set_enable_search(false); + m_view.set_model(m_model); + m_view.append_column("User", m_columns.m_col_user); + m_view.append_column("Reason", m_columns.m_col_reason); +} + +void GuildSettingsBansPane::OnMap() { + if (m_requested) return; + m_requested = true; + + auto &discord = Abaddon::Get().GetDiscordClient(); + + const auto self_id = discord.GetUserData().ID; + const auto can_ban = discord.HasGuildPermission(self_id, GuildID, Permission::BAN_MEMBERS); + + if (can_ban) + discord.FetchGuildBans(GuildID, sigc::mem_fun(*this, &GuildSettingsBansPane::OnGuildBansFetch)); +} + +void GuildSettingsBansPane::OnGuildBanFetch(const BanData &ban) { + const auto user = Abaddon::Get().GetDiscordClient().GetUser(ban.User.ID); + auto row = *m_model->append(); + row[m_columns.m_col_id] = ban.User.ID; + if (user.has_value()) + row[m_columns.m_col_user] = user->Username + "#" + user->Discriminator; + else + row[m_columns.m_col_user] = "<@" + std::to_string(ban.User.ID) + ">"; + + row[m_columns.m_col_reason] = ban.Reason; +} + +void GuildSettingsBansPane::OnGuildBansFetch(const std::vector &bans) { + for (const auto &ban : bans) { + const auto user = Abaddon::Get().GetDiscordClient().GetUser(ban.User.ID); + auto row = *m_model->append(); + row[m_columns.m_col_id] = user->ID; + row[m_columns.m_col_user] = user->Username + "#" + user->Discriminator; + row[m_columns.m_col_reason] = ban.Reason; + } +} + +void GuildSettingsBansPane::OnMenuUnban() { + auto selected_row = *m_view.get_selection()->get_selected(); + if (selected_row) { + Snowflake id = selected_row[m_columns.m_col_id]; + auto cb = [this](DiscordError code) { + if (code != DiscordError::NONE) { + Gtk::MessageDialog dlg("Failed to unban user", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); + dlg.set_position(Gtk::WIN_POS_CENTER); + dlg.run(); + } + }; + Abaddon::Get().GetDiscordClient().UnbanUser(GuildID, id, sigc::track_obj(cb, *this)); + } +} + +void GuildSettingsBansPane::OnMenuCopyID() { + auto selected_row = *m_view.get_selection()->get_selected(); + if (selected_row) + Gtk::Clipboard::get()->set_text(std::to_string(static_cast(selected_row[m_columns.m_col_id]))); +} + +bool GuildSettingsBansPane::OnTreeButtonPress(GdkEventButton *event) { + if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_SECONDARY) { + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto self_id = discord.GetUserData().ID; + const auto can_ban = discord.HasGuildPermission(self_id, GuildID, Permission::BAN_MEMBERS); + m_menu_unban.set_sensitive(can_ban); + auto selection = m_view.get_selection(); + Gtk::TreeModel::Path path; + if (m_view.get_path_at_pos(event->x, event->y, path)) { + m_view.get_selection()->select(path); + m_menu.popup_at_pointer(reinterpret_cast(event)); + } + + return true; + } + + return false; +} + +void GuildSettingsBansPane::OnBanRemove(Snowflake guild_id, Snowflake user_id) { + if (guild_id != GuildID) return; + for (auto &child : m_model->children()) { + if (static_cast(child[m_columns.m_col_id]) == user_id) { + m_model->erase(child); + break; + } + } +} + +void GuildSettingsBansPane::OnBanAdd(Snowflake guild_id, Snowflake user_id) { + if (guild_id != GuildID) return; + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.HasGuildPermission(discord.GetUserData().ID, guild_id, Permission::BAN_MEMBERS)) { + discord.FetchGuildBan(guild_id, user_id, sigc::mem_fun(*this, &GuildSettingsBansPane::OnGuildBanFetch)); + } else { + auto user = *discord.GetUser(user_id); + auto row = *m_model->append(); + row[m_columns.m_col_id] = user_id; + row[m_columns.m_col_user] = user.Username + "#" + user.Discriminator; + row[m_columns.m_col_reason] = ""; + } +} + +GuildSettingsBansPane::ModelColumns::ModelColumns() { + add(m_col_id); + add(m_col_user); + add(m_col_reason); +} diff --git a/src/windows/guildsettings/banspane.hpp b/src/windows/guildsettings/banspane.hpp new file mode 100644 index 0000000..b2420a9 --- /dev/null +++ b/src/windows/guildsettings/banspane.hpp @@ -0,0 +1,45 @@ +#pragma once +#include +#include "discord/snowflake.hpp" +#include "discord/ban.hpp" + +class GuildSettingsBansPane : public Gtk::Box { +public: + GuildSettingsBansPane(Snowflake id); + +private: + void OnMap(); + + bool m_requested = false; + + void OnGuildBanFetch(const BanData &ban); + void OnGuildBansFetch(const std::vector &bans); + void OnMenuUnban(); + void OnMenuCopyID(); + bool OnTreeButtonPress(GdkEventButton *event); + void OnBanRemove(Snowflake guild_id, Snowflake user_id); + void OnBanAdd(Snowflake guild_id, Snowflake user_id); + + Gtk::Label *m_no_perms_note = nullptr; + + Gtk::ScrolledWindow m_scroll; + Gtk::TreeView m_view; + + Snowflake GuildID; + + class ModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + ModelColumns(); + + Gtk::TreeModelColumn m_col_user; + Gtk::TreeModelColumn m_col_reason; + Gtk::TreeModelColumn m_col_id; + }; + + ModelColumns m_columns; + Glib::RefPtr m_model; + + Gtk::Menu m_menu; + Gtk::MenuItem m_menu_unban; + Gtk::MenuItem m_menu_copy_id; +}; diff --git a/src/windows/guildsettings/emojispane.cpp b/src/windows/guildsettings/emojispane.cpp new file mode 100644 index 0000000..1f4bfa9 --- /dev/null +++ b/src/windows/guildsettings/emojispane.cpp @@ -0,0 +1,257 @@ +#include "emojispane.hpp" +#include "abaddon.hpp" +#include "components/cellrendererpixbufanimation.hpp" + +GuildSettingsEmojisPane::GuildSettingsEmojisPane(Snowflake guild_id) + : Gtk::Box(Gtk::ORIENTATION_VERTICAL) + , GuildID(guild_id) + , m_model(Gtk::ListStore::create(m_columns)) + , m_filter(Gtk::TreeModelFilter::create(m_model)) + , m_menu_delete("Delete") + , m_menu_copy_id("Copy ID") + , m_menu_copy_emoji_url("Copy Emoji URL") + , m_menu_show_emoji("Open in Browser") { + signal_map().connect(sigc::mem_fun(*this, &GuildSettingsEmojisPane::OnMap)); + set_name("guild-emojis-pane"); + + m_view_scroll.set_hexpand(true); + m_view_scroll.set_vexpand(true); + + m_view.signal_button_press_event().connect(sigc::mem_fun(*this, &GuildSettingsEmojisPane::OnTreeButtonPress), false); + + m_menu_copy_id.signal_activate().connect(sigc::mem_fun(*this, &GuildSettingsEmojisPane::OnMenuCopyID)); + m_menu_delete.signal_activate().connect(sigc::mem_fun(*this, &GuildSettingsEmojisPane::OnMenuDelete)); + m_menu_copy_emoji_url.signal_activate().connect(sigc::mem_fun(*this, &GuildSettingsEmojisPane::OnMenuCopyEmojiURL)); + m_menu_show_emoji.signal_activate().connect(sigc::mem_fun(*this, &GuildSettingsEmojisPane::OnMenuShowEmoji)); + + m_menu.append(m_menu_delete); + m_menu.append(m_menu_copy_id); + m_menu.append(m_menu_copy_emoji_url); + m_menu.append(m_menu_show_emoji); + m_menu.show_all(); + + auto &discord = Abaddon::Get().GetDiscordClient(); + + discord.signal_guild_emojis_update().connect(sigc::hide<0>(sigc::mem_fun(*this, &GuildSettingsEmojisPane::OnFetchEmojis))); + + const auto self_id = discord.GetUserData().ID; + const bool can_manage = discord.HasGuildPermission(self_id, GuildID, Permission::MANAGE_EMOJIS); + m_menu_delete.set_sensitive(can_manage); + + m_search.set_placeholder_text("Filter"); + m_search.signal_changed().connect([this]() { + m_filter->refilter(); + }); + + m_view_scroll.add(m_view); + add(m_search); + add(m_view_scroll); + m_search.show(); + m_view.show(); + m_view_scroll.show(); + + m_filter->set_visible_func([this](const Gtk::TreeModel::const_iterator &iter) -> bool { + const auto text = m_search.get_text(); + if (text == "") return true; + return StringContainsCaseless((*iter)[m_columns.m_col_name], text); + }); + m_view.set_enable_search(false); + m_view.set_model(m_filter); + + auto *column = Gtk::manage(new Gtk::TreeView::Column("Emoji")); + auto *renderer = Gtk::manage(new CellRendererPixbufAnimation); + column->pack_start(*renderer); + column->add_attribute(renderer->property_pixbuf(), m_columns.m_col_pixbuf); + column->add_attribute(renderer->property_pixbuf_animation(), m_columns.m_col_pixbuf_animation); + m_view.append_column(*column); + + if (can_manage) { + auto *column = Gtk::manage(new Gtk::TreeView::Column("Name")); + auto *renderer = Gtk::manage(new Gtk::CellRendererText); + column->pack_start(*renderer); + column->add_attribute(renderer->property_text(), m_columns.m_col_name); + renderer->property_editable() = true; + renderer->signal_edited().connect([this, renderer, column](const Glib::ustring &path, const Glib::ustring &text) { + std::string new_str; + int size = 0; + for (const auto ch : text) { + if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_') + new_str += ch; + else if (ch == ' ') + new_str += '_'; + if (++size == 32) break; + } + if (auto row = *m_model->get_iter(path)) { + row[m_columns.m_col_name] = new_str; + OnEditName(row[m_columns.m_col_id], new_str); + } + }); + m_view.append_column(*column); + } else + m_view.append_column("Name", m_columns.m_col_name); + if (can_manage) + m_view.append_column("Creator", m_columns.m_col_creator); + m_view.append_column("Is Animated?", m_columns.m_col_animated); + + for (const auto column : m_view.get_columns()) + column->set_resizable(true); +} + +void GuildSettingsEmojisPane::OnMap() { + m_view.grab_focus(); + + if (m_requested) return; + m_requested = true; + + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto self_id = discord.GetUserData().ID; + const bool can_manage = discord.HasGuildPermission(self_id, GuildID, Permission::MANAGE_EMOJIS); + m_menu_delete.set_sensitive(can_manage); + + discord.FetchGuildEmojis(GuildID, sigc::mem_fun(*this, &GuildSettingsEmojisPane::OnFetchEmojis)); +} + +void GuildSettingsEmojisPane::AddEmojiRow(const EmojiData &emoji) { + auto &img = Abaddon::Get().GetImageManager(); + + auto row = *m_model->append(); + + row[m_columns.m_col_id] = emoji.ID; + row[m_columns.m_col_pixbuf] = img.GetPlaceholder(32); + row[m_columns.m_col_name] = emoji.Name; + if (emoji.Creator.has_value()) + row[m_columns.m_col_creator] = emoji.Creator->Username + "#" + emoji.Creator->Discriminator; + if (emoji.IsAnimated.has_value()) + row[m_columns.m_col_animated] = *emoji.IsAnimated ? "Yes" : "No"; + else + row[m_columns.m_col_animated] = "No"; + if (emoji.IsAvailable.has_value()) + row[m_columns.m_col_available] = *emoji.IsAvailable ? "Yes" : "No"; + else + row[m_columns.m_col_available] = "Yes"; + + static bool show_animations = Abaddon::Get().GetSettings().GetShowAnimations(); + if (show_animations && emoji.IsAnimated.has_value() && *emoji.IsAnimated) { + const auto cb = [this, id = emoji.ID](const Glib::RefPtr &pb) { + for (auto &row : m_model->children()) { + if (static_cast(row[m_columns.m_col_id]) == id) { + row[m_columns.m_col_pixbuf_animation] = pb; + return; + } + } + }; + img.LoadAnimationFromURL(emoji.GetURL("gif"), 32, 32, sigc::track_obj(cb, *this)); + } else { + const auto cb = [this, id = emoji.ID](const Glib::RefPtr &pb) { + for (auto &row : m_model->children()) { + if (static_cast(row[m_columns.m_col_id]) == id) { + row[m_columns.m_col_pixbuf] = pb->scale_simple(32, 32, Gdk::INTERP_BILINEAR); + return; + } + } + }; + img.LoadFromURL(emoji.GetURL(), sigc::track_obj(cb, *this)); + } +} + +void GuildSettingsEmojisPane::OnFetchEmojis(std::vector emojis) { + m_model->clear(); + + // put animated emojis at the end then sort alphabetically + std::sort(emojis.begin(), emojis.end(), [&](const EmojiData &a, const EmojiData &b) { + const bool a_is_animated = a.IsAnimated.has_value() && *a.IsAnimated; + const bool b_is_animated = b.IsAnimated.has_value() && *b.IsAnimated; + if (a_is_animated == b_is_animated) + return a.Name < b.Name; + else if (a_is_animated && !b_is_animated) + return false; + else if (!a_is_animated && b_is_animated) + return true; + return false; // this wont happen please be quiet compiler + }); + + for (const auto &emoji : emojis) + AddEmojiRow(emoji); +} + +void GuildSettingsEmojisPane::OnEditName(Snowflake id, const std::string &name) { + const auto cb = [this](DiscordError code) { + if (code != DiscordError::NONE) { + Gtk::MessageDialog dlg("Failed to set emoji name", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK); + dlg.set_position(Gtk::WIN_POS_CENTER); + dlg.run(); + } + }; + Abaddon::Get().GetDiscordClient().ModifyEmojiName(GuildID, id, name, cb); +} + +void GuildSettingsEmojisPane::OnMenuCopyID() { + if (auto selected_row = *m_view.get_selection()->get_selected()) { + const auto id = static_cast(selected_row[m_columns.m_col_id]); + Gtk::Clipboard::get()->set_text(std::to_string(id)); + } +} + +void GuildSettingsEmojisPane::OnMenuDelete() { + if (auto selected_row = *m_view.get_selection()->get_selected()) { + const auto name = static_cast(selected_row[m_columns.m_col_name]); + const auto id = static_cast(selected_row[m_columns.m_col_id]); + if (auto *window = dynamic_cast(get_toplevel())) + if (Abaddon::Get().ShowConfirm("Are you sure you want to delete " + name + "?", window)) { + const auto cb = [this](DiscordError code) { + if (code != DiscordError::NONE) { + Gtk::MessageDialog dlg("Failed to delete emoji", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK); + dlg.set_position(Gtk::WIN_POS_CENTER); + dlg.run(); + } + }; + Abaddon::Get().GetDiscordClient().DeleteEmoji(GuildID, id, cb); + } + } +} + +void GuildSettingsEmojisPane::OnMenuCopyEmojiURL() { + if (auto selected_row = *m_view.get_selection()->get_selected()) { + const auto id = static_cast(selected_row[m_columns.m_col_id]); + const bool is_animated = static_cast(selected_row[m_columns.m_col_animated]) == "Yes"; + Gtk::Clipboard::get()->set_text(EmojiData::URLFromID(id, is_animated ? "gif" : "png", "256")); + } +} + +void GuildSettingsEmojisPane::OnMenuShowEmoji() { + if (auto selected_row = *m_view.get_selection()->get_selected()) { + const auto id = static_cast(selected_row[m_columns.m_col_id]); + const bool is_animated = static_cast(selected_row[m_columns.m_col_animated]) == "Yes"; + LaunchBrowser(EmojiData::URLFromID(id, is_animated ? "gif" : "png", "256")); + } +} + +bool GuildSettingsEmojisPane::OnTreeButtonPress(GdkEventButton *event) { + if (event->button == GDK_BUTTON_SECONDARY) { + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto self_id = discord.GetUserData().ID; + const bool can_manage = discord.HasGuildPermission(self_id, GuildID, Permission::MANAGE_EMOJIS); + m_menu_delete.set_sensitive(can_manage); + + auto selection = m_view.get_selection(); + Gtk::TreeModel::Path path; + if (m_view.get_path_at_pos(event->x, event->y, path)) { + m_view.get_selection()->select(path); + m_menu.popup_at_pointer(reinterpret_cast(event)); + } + + return true; + } + + return false; +} + +GuildSettingsEmojisPane::ModelColumns::ModelColumns() { + add(m_col_id); + add(m_col_pixbuf); + add(m_col_pixbuf_animation); + add(m_col_name); + add(m_col_creator); + add(m_col_animated); + add(m_col_available); +} diff --git a/src/windows/guildsettings/emojispane.hpp b/src/windows/guildsettings/emojispane.hpp new file mode 100644 index 0000000..1c0edd1 --- /dev/null +++ b/src/windows/guildsettings/emojispane.hpp @@ -0,0 +1,53 @@ +#pragma once +#include +#include "discord/emoji.hpp" + +class GuildSettingsEmojisPane : public Gtk::Box { +public: + GuildSettingsEmojisPane(Snowflake guild_id); + +private: + void OnMap(); + + bool m_requested = false; + + void AddEmojiRow(const EmojiData &emoji); + + void OnFetchEmojis(std::vector emojis); + + void OnEditName(Snowflake id, const std::string &name); + void OnMenuCopyID(); + void OnMenuDelete(); + void OnMenuCopyEmojiURL(); + void OnMenuShowEmoji(); + bool OnTreeButtonPress(GdkEventButton *event); + + Snowflake GuildID; + + Gtk::Entry m_search; + Gtk::ScrolledWindow m_view_scroll; + Gtk::TreeView m_view; + + class ModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + ModelColumns(); + + Gtk::TreeModelColumn> m_col_pixbuf; + Gtk::TreeModelColumn> m_col_pixbuf_animation; + Gtk::TreeModelColumn m_col_name; + Gtk::TreeModelColumn m_col_creator; + Gtk::TreeModelColumn m_col_animated; + Gtk::TreeModelColumn m_col_available; + Gtk::TreeModelColumn m_col_id; + }; + + ModelColumns m_columns; + Glib::RefPtr m_model; + Glib::RefPtr m_filter; + + Gtk::Menu m_menu; + Gtk::MenuItem m_menu_delete; + Gtk::MenuItem m_menu_copy_id; + Gtk::MenuItem m_menu_copy_emoji_url; + Gtk::MenuItem m_menu_show_emoji; +}; diff --git a/src/windows/guildsettings/infopane.cpp b/src/windows/guildsettings/infopane.cpp new file mode 100644 index 0000000..b4f75f3 --- /dev/null +++ b/src/windows/guildsettings/infopane.cpp @@ -0,0 +1,220 @@ +#include "infopane.hpp" +#include "abaddon.hpp" +#include + +GuildSettingsInfoPane::GuildSettingsInfoPane(Snowflake id) + : m_guild_icon_label("Guild icon") + , m_guild_name_label("Guild name") + , GuildID(id) { + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto guild = *discord.GetGuild(id); + const auto self_id = discord.GetUserData().ID; + const auto can_modify = discord.HasGuildPermission(self_id, id, Permission::MANAGE_GUILD); + + set_name("guild-info-pane"); + + m_guild_name.set_sensitive(can_modify); + m_guild_name.set_text(guild.Name); + m_guild_name.signal_focus_out_event().connect([this](GdkEventFocus *e) -> bool { + UpdateGuildName(); + return false; + }); + m_guild_name.signal_key_press_event().connect([this](GdkEventKey *e) -> bool { + if (e->keyval == GDK_KEY_Return) + UpdateGuildName(); + return false; + // clang-format off + }, false); + // clang-format on + m_guild_name.set_tooltip_text("Press enter or lose focus to submit"); + m_guild_name.show(); + m_guild_name_label.show(); + + auto guild_update_cb = [this](Snowflake id) { + if (id != GuildID) return; + const auto guild = *Abaddon::Get().GetDiscordClient().GetGuild(id); + FetchGuildIcon(guild); + }; + discord.signal_guild_update().connect(sigc::track_obj(guild_update_cb, *this)); + FetchGuildIcon(guild); + + AddPointerCursor(m_guild_icon_ev); + + m_guild_icon.set_margin_bottom(10); + if (can_modify) { + m_guild_icon_ev.set_tooltip_text("Click to choose a file, right click to paste"); + + m_guild_icon_ev.signal_button_press_event().connect([this](GdkEventButton *event) -> bool { + if (event->type == GDK_BUTTON_PRESS) { + if (event->button == GDK_BUTTON_PRIMARY) + UpdateGuildIconPicker(); + else if (event->button == GDK_BUTTON_SECONDARY) + UpdateGuildIconClipboard(); + } + + return false; + }); + } else if (guild.HasIcon()) { + std::string guild_icon_url; + if (guild.HasAnimatedIcon()) + guild_icon_url = guild.GetIconURL("gif", "512"); + else + guild_icon_url = guild.GetIconURL("png", "512"); + m_guild_icon_ev.signal_button_press_event().connect([this, guild_icon_url](GdkEventButton *event) -> bool { + if (event->type == GDK_BUTTON_PRESS) + if (event->button == GDK_BUTTON_PRIMARY) + LaunchBrowser(guild_icon_url); + + return false; + }); + } + + m_guild_icon.show(); + m_guild_icon_ev.show(); + + m_guild_icon_ev.add(m_guild_icon); + attach(m_guild_icon_ev, 0, 0, 1, 1); + attach(m_guild_name_label, 0, 1, 1, 1); + attach_next_to(m_guild_name, m_guild_name_label, Gtk::POS_RIGHT, 1, 1); +} + +void GuildSettingsInfoPane::FetchGuildIcon(const GuildData &guild) { + m_guild_icon.property_pixbuf() = Abaddon::Get().GetImageManager().GetPlaceholder(32); + if (guild.HasIcon()) { + if (Abaddon::Get().GetSettings().GetShowAnimations() && guild.HasAnimatedIcon()) { + auto cb = [this](const Glib::RefPtr &pixbuf) { + m_guild_icon.property_pixbuf_animation() = pixbuf; + }; + Abaddon::Get().GetImageManager().LoadAnimationFromURL(guild.GetIconURL("gif", "64"), 64, 64, sigc::track_obj(cb, *this)); + } + + auto cb = [this](const Glib::RefPtr &pixbuf) { + m_guild_icon.property_pixbuf() = pixbuf->scale_simple(64, 64, Gdk::INTERP_BILINEAR); + }; + Abaddon::Get().GetImageManager().LoadFromURL(guild.GetIconURL("png", "64"), sigc::track_obj(cb, *this)); + } +} + +void GuildSettingsInfoPane::UpdateGuildName() { + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.GetGuild(GuildID)->Name == m_guild_name.get_text()) return; + + auto cb = [this](DiscordError code) { + if (code != DiscordError::NONE) { + m_guild_name.set_text(Abaddon::Get().GetDiscordClient().GetGuild(GuildID)->Name); + Gtk::MessageDialog dlg("Failed to set guild name", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); + dlg.set_position(Gtk::WIN_POS_CENTER); + dlg.run(); + } + }; + discord.SetGuildName(GuildID, m_guild_name.get_text(), sigc::track_obj(cb, *this)); +} + +void GuildSettingsInfoPane::UpdateGuildIconFromData(const std::vector &data, const std::string &mime) { + auto encoded = "data:" + mime + ";base64," + Glib::Base64::encode(std::string(data.begin(), data.end())); + auto &discord = Abaddon::Get().GetDiscordClient(); + + auto cb = [this](DiscordError code) { + if (code != DiscordError::NONE) { + Gtk::MessageDialog dlg("Failed to set guild icon", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); + dlg.set_position(Gtk::WIN_POS_CENTER); + dlg.run(); + } + }; + discord.SetGuildIcon(GuildID, encoded, sigc::track_obj(cb, *this)); +} + +void GuildSettingsInfoPane::UpdateGuildIconFromPixbuf(Glib::RefPtr pixbuf) { + int w = pixbuf->get_width(); + int h = pixbuf->get_height(); + if (w > 1024 || h > 1024) { + GetImageDimensions(w, h, w, h, 1024, 1024); + pixbuf = pixbuf->scale_simple(w, h, Gdk::INTERP_BILINEAR); + } + gchar *buffer; + gsize buffer_size; + pixbuf->save_to_buffer(buffer, buffer_size, "png"); + std::vector data(buffer_size); + std::memcpy(data.data(), buffer, buffer_size); + UpdateGuildIconFromData(data, "image/png"); +} + +void GuildSettingsInfoPane::UpdateGuildIconPicker() { + // this picker fucking sucks + Gtk::FileChooserDialog dlg("Choose new guild icon", Gtk::FILE_CHOOSER_ACTION_OPEN); + dlg.get_style_context()->remove_provider(Abaddon::Get().GetStyleProvider()); + dlg.set_modal(true); + dlg.signal_response().connect([this, &dlg](int response) { + if (response == Gtk::RESPONSE_OK) { + auto data = ReadWholeFile(dlg.get_filename()); + if (GetExtension(dlg.get_filename()) == ".gif") + UpdateGuildIconFromData(data, "image/gif"); + else + try { + auto loader = Gdk::PixbufLoader::create(); + loader->signal_size_prepared().connect([&loader](int inw, int inh) { + int w, h; + GetImageDimensions(inw, inh, w, h, 1024, 1024); + loader->set_size(w, h); + }); + loader->write(data.data(), data.size()); + loader->close(); + UpdateGuildIconFromPixbuf(loader->get_pixbuf()); + } catch (const std::exception &) {}; + } + }); + + dlg.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK); + dlg.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); + + auto filter_images = Gtk::FileFilter::create(); + if (Abaddon::Get().GetDiscordClient().GetGuild(GuildID)->HasFeature("ANIMATED_ICON")) { + filter_images->set_name("Supported images (*.jpg, *.jpeg, *.png, *.gif)"); + filter_images->add_pattern("*.gif"); + } else { + filter_images->set_name("Supported images (*.jpg, *.jpeg, *.png)"); + } + filter_images->add_pattern("*.jpg"); + filter_images->add_pattern("*.jpeg"); + filter_images->add_pattern("*.png"); + dlg.add_filter(filter_images); + + auto filter_all = Gtk::FileFilter::create(); + filter_all->set_name("All files (*.*)"); + filter_all->add_pattern("*.*"); + dlg.add_filter(filter_all); + + dlg.run(); +} + +void GuildSettingsInfoPane::UpdateGuildIconClipboard() { + std::vector icon_data; + + auto cb = Gtk::Clipboard::get(); + // query for file path then for actual image + if (cb->wait_is_text_available()) { + auto path = cb->wait_for_text(); + if (!std::filesystem::exists(path.c_str())) return; + auto data = ReadWholeFile(path); + try { + auto loader = Gdk::PixbufLoader::create(); + loader->signal_size_prepared().connect([&loader](int inw, int inh) { + int w, h; + GetImageDimensions(inw, inh, w, h, 1024, 1024); + loader->set_size(w, h); + }); + loader->write(data.data(), data.size()); + loader->close(); + auto pb = loader->get_pixbuf(); + UpdateGuildIconFromPixbuf(pb); + + return; + } catch (const std::exception &) {}; + } + + if (cb->wait_is_image_available()) { + auto pb = cb->wait_for_image(); + UpdateGuildIconFromPixbuf(pb); + return; + } +} diff --git a/src/windows/guildsettings/infopane.hpp b/src/windows/guildsettings/infopane.hpp new file mode 100644 index 0000000..8a7e6a2 --- /dev/null +++ b/src/windows/guildsettings/infopane.hpp @@ -0,0 +1,26 @@ +#pragma once +#include +#include "discord/guild.hpp" + +class GuildSettingsInfoPane : public Gtk::Grid { +public: + GuildSettingsInfoPane(Snowflake id); + +private: + void FetchGuildIcon(const GuildData &guild); + + void UpdateGuildName(); + void UpdateGuildIconFromData(const std::vector &data, const std::string &mime); + void UpdateGuildIconFromPixbuf(Glib::RefPtr pixbuf); + void UpdateGuildIconPicker(); + void UpdateGuildIconClipboard(); + + Gtk::Label m_guild_icon_label; + Gtk::EventBox m_guild_icon_ev; // necessary to make custom cursor behave properly + Gtk::Image m_guild_icon; + + Gtk::Label m_guild_name_label; + Gtk::Entry m_guild_name; + + Snowflake GuildID; +}; diff --git a/src/windows/guildsettings/invitespane.cpp b/src/windows/guildsettings/invitespane.cpp new file mode 100644 index 0000000..bec4784 --- /dev/null +++ b/src/windows/guildsettings/invitespane.cpp @@ -0,0 +1,136 @@ +#include "invitespane.hpp" +#include "abaddon.hpp" + +GuildSettingsInvitesPane::GuildSettingsInvitesPane(Snowflake id) + : GuildID(id) + , m_model(Gtk::ListStore::create(m_columns)) + , m_menu_delete("Delete") { + signal_map().connect(sigc::mem_fun(*this, &GuildSettingsInvitesPane::OnMap)); + set_name("guild-invites-pane"); + set_hexpand(true); + set_vexpand(true); + + m_view.signal_button_press_event().connect(sigc::mem_fun(*this, &GuildSettingsInvitesPane::OnTreeButtonPress), false); + + m_menu_delete.signal_activate().connect(sigc::mem_fun(*this, &GuildSettingsInvitesPane::OnMenuDelete)); + m_menu.append(m_menu_delete); + m_menu.show_all(); + + auto &discord = Abaddon::Get().GetDiscordClient(); + + discord.signal_invite_create().connect(sigc::mem_fun(*this, &GuildSettingsInvitesPane::OnInviteCreate)); + discord.signal_invite_delete().connect(sigc::mem_fun(*this, &GuildSettingsInvitesPane::OnInviteDelete)); + + m_view.show(); + add(m_view); + + m_view.set_enable_search(false); + m_view.set_model(m_model); + m_view.append_column("Code", m_columns.m_col_code); + m_view.append_column("Expires", m_columns.m_col_expires); + m_view.append_column("Created by", m_columns.m_col_inviter); + m_view.append_column("Uses", m_columns.m_col_uses); + m_view.append_column("Max uses", m_columns.m_col_max_uses); + m_view.append_column("Grants temporary membership", m_columns.m_col_temporary); + + for (const auto column : m_view.get_columns()) + column->set_resizable(true); +} + +void GuildSettingsInvitesPane::OnMap() { + if (m_requested) return; + m_requested = true; + + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto self_id = discord.GetUserData().ID; + + if (discord.HasGuildPermission(self_id, GuildID, Permission::MANAGE_GUILD)) + discord.FetchGuildInvites(GuildID, sigc::mem_fun(*this, &GuildSettingsInvitesPane::OnInvitesFetch)); +} + +void GuildSettingsInvitesPane::AppendInvite(const InviteData &invite) { + auto row = *m_model->append(); + row[m_columns.m_col_code] = invite.Code; + if (invite.Inviter.has_value()) + row[m_columns.m_col_inviter] = invite.Inviter->Username + "#" + invite.Inviter->Discriminator; + + if (invite.MaxAge.has_value()) { + if (*invite.MaxAge == 0) + row[m_columns.m_col_expires] = "Never"; + else + row[m_columns.m_col_expires] = FormatISO8601(*invite.CreatedAt, *invite.MaxAge); + } + + row[m_columns.m_col_uses] = *invite.Uses; + if (*invite.MaxUses == 0) + row[m_columns.m_col_max_uses] = "Unlimited"; + else + row[m_columns.m_col_max_uses] = std::to_string(*invite.MaxUses); + + row[m_columns.m_col_temporary] = *invite.IsTemporary ? "Yes" : "No"; +} + +void GuildSettingsInvitesPane::OnInviteFetch(const std::optional &invite) { + if (!invite.has_value()) return; + AppendInvite(*invite); +} + +void GuildSettingsInvitesPane::OnInvitesFetch(const std::vector &invites) { + for (const auto &invite : invites) + AppendInvite(invite); +} + +void GuildSettingsInvitesPane::OnInviteCreate(const InviteData &invite) { + if (invite.Guild->ID == GuildID) + OnInviteFetch(std::make_optional(invite)); +} + +void GuildSettingsInvitesPane::OnInviteDelete(const InviteDeleteObject &data) { + if (*data.GuildID == GuildID) + for (auto &row : m_model->children()) + if (row[m_columns.m_col_code] == data.Code) + m_model->erase(row); +} + +void GuildSettingsInvitesPane::OnMenuDelete() { + auto selected_row = *m_view.get_selection()->get_selected(); + if (selected_row) { + auto code = static_cast(selected_row[m_columns.m_col_code]); + auto cb = [this](DiscordError code) { + if (code != DiscordError::NONE) { + Gtk::MessageDialog dlg("Failed to delete invite", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); + dlg.set_position(Gtk::WIN_POS_CENTER); + dlg.run(); + } + }; + Abaddon::Get().GetDiscordClient().DeleteInvite(code, sigc::track_obj(cb, *this)); + } +} + +bool GuildSettingsInvitesPane::OnTreeButtonPress(GdkEventButton *event) { + if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_SECONDARY) { + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto self_id = discord.GetUserData().ID; + const auto can_manage = discord.HasGuildPermission(self_id, GuildID, Permission::MANAGE_GUILD); + m_menu_delete.set_sensitive(can_manage); + auto selection = m_view.get_selection(); + Gtk::TreeModel::Path path; + if (m_view.get_path_at_pos(event->x, event->y, path)) { + m_view.get_selection()->select(path); + m_menu.popup_at_pointer(reinterpret_cast(event)); + } + + return true; + } + + return false; +} + +GuildSettingsInvitesPane::ModelColumns::ModelColumns() { + add(m_col_code); + add(m_col_expires); + add(m_col_inviter); + add(m_col_temporary); + add(m_col_uses); + add(m_col_max_uses); +} diff --git a/src/windows/guildsettings/invitespane.hpp b/src/windows/guildsettings/invitespane.hpp new file mode 100644 index 0000000..5268d68 --- /dev/null +++ b/src/windows/guildsettings/invitespane.hpp @@ -0,0 +1,43 @@ +#pragma once +#include +#include "discord/objects.hpp" + +class GuildSettingsInvitesPane : public Gtk::ScrolledWindow { +public: + GuildSettingsInvitesPane(Snowflake id); + +private: + void OnMap(); + + bool m_requested = false; + + void AppendInvite(const InviteData &invite); + void OnInviteFetch(const std::optional &invite); + void OnInvitesFetch(const std::vector &invites); + void OnInviteCreate(const InviteData &invite); + void OnInviteDelete(const InviteDeleteObject &data); + void OnMenuDelete(); + bool OnTreeButtonPress(GdkEventButton *event); + + Gtk::TreeView m_view; + + Snowflake GuildID; + + class ModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + ModelColumns(); + + Gtk::TreeModelColumn m_col_code; + Gtk::TreeModelColumn m_col_expires; + Gtk::TreeModelColumn m_col_inviter; + Gtk::TreeModelColumn m_col_temporary; + Gtk::TreeModelColumn m_col_uses; + Gtk::TreeModelColumn m_col_max_uses; + }; + + ModelColumns m_columns; + Glib::RefPtr m_model; + + Gtk::Menu m_menu; + Gtk::MenuItem m_menu_delete; +}; diff --git a/src/windows/guildsettings/memberspane.cpp b/src/windows/guildsettings/memberspane.cpp new file mode 100644 index 0000000..36c5c0b --- /dev/null +++ b/src/windows/guildsettings/memberspane.cpp @@ -0,0 +1,410 @@ +#include "memberspane.hpp" +#include "abaddon.hpp" + +GuildSettingsMembersPane::GuildSettingsMembersPane(Snowflake id) + : Gtk::Box(Gtk::ORIENTATION_VERTICAL) + , GuildID(id) + , m_layout(Gtk::ORIENTATION_HORIZONTAL) + , m_member_list(id) + , m_member_info(id) { + set_name("guild-members-pane"); + set_hexpand(true); + set_vexpand(true); + + m_member_list.signal_member_select().connect(sigc::mem_fun(m_member_info, &GuildSettingsMembersPaneInfo::SetUser)); + + m_note.set_label("Some members may not be shown if the client is not aware of them"); + m_note.set_single_line_mode(true); + m_note.set_ellipsize(Pango::ELLIPSIZE_END); + + m_layout.set_homogeneous(true); + m_layout.add(m_member_list); + m_layout.add(m_member_info); + add(m_note); + add(m_layout); + + m_member_list.show(); + m_member_info.show(); + m_note.show(); + m_layout.show(); +} + +GuildSettingsMembersPaneMembers::GuildSettingsMembersPaneMembers(Snowflake id) + : Gtk::Box(Gtk::ORIENTATION_VERTICAL) + , GuildID(id) { + m_list_scroll.get_style_context()->add_class("guild-members-pane-list"); + + m_list_scroll.set_hexpand(true); + m_list_scroll.set_vexpand(true); + m_list_scroll.set_propagate_natural_height(true); + + auto &discord = Abaddon::Get().GetDiscordClient(); + auto members = discord.GetUsersInGuild(id); + const auto guild = *discord.GetGuild(GuildID); + for (const auto member_id : members) { + auto member = discord.GetMember(member_id, GuildID); + if (!member.has_value()) continue; // fixme this should not be necessary + member->User = discord.GetUser(member_id); + if (member->User->IsDeleted()) continue; + auto *row = Gtk::manage(new GuildSettingsMembersListItem(guild, *member)); + row->show(); + m_list.add(*row); + } + + m_list.set_selection_mode(Gtk::SELECTION_SINGLE); + m_list.signal_row_selected().connect([this](Gtk::ListBoxRow *selected_) { + if (auto *selected = dynamic_cast(selected_)) + m_signal_member_select.emit(selected->UserID); + }); + + m_search.set_placeholder_text("Filter"); + m_search.signal_changed().connect([this] { + m_list.invalidate_filter(); + }); + + m_list.set_filter_func([this](Gtk::ListBoxRow *row_) -> bool { + const auto search_term = m_search.get_text(); + if (search_term.size() == 0) return true; + if (auto *row = dynamic_cast(row_)) + return StringContainsCaseless(row->DisplayTerm, m_search.get_text()); + return true; + }); + + m_list_scroll.add(m_list); + add(m_search); + add(m_list_scroll); + + m_search.show(); + m_list.show(); + m_list_scroll.show(); +} + +GuildSettingsMembersPaneMembers::type_signal_member_select GuildSettingsMembersPaneMembers::signal_member_select() { + return m_signal_member_select; +} + +GuildSettingsMembersListItem::GuildSettingsMembersListItem(const GuildData &guild, const GuildMember &member) + : UserID(member.User->ID) + , GuildID(guild.ID) + , m_avatar(32, 32) { + m_avatar.SetAnimated(true); + + m_ev.signal_button_press_event().connect([this](GdkEventButton *event) -> bool { + if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_SECONDARY) { + Abaddon::Get().ShowUserMenu(reinterpret_cast(event), UserID, GuildID); + return true; + } + return false; + }); + + auto &discord = Abaddon::Get().GetDiscordClient(); + + if (member.User->HasAnimatedAvatar() && Abaddon::Get().GetSettings().GetShowAnimations()) + m_avatar.SetURL(member.User->GetAvatarURL("gif", "32")); + else + m_avatar.SetURL(member.User->GetAvatarURL("png", "32")); + + DisplayTerm = member.User->Username + "#" + member.User->Discriminator; + + const auto member_update_cb = [this](Snowflake guild_id, Snowflake user_id) { + if (user_id == UserID) + UpdateColor(); + }; + discord.signal_guild_member_update().connect(sigc::track_obj(member_update_cb, *this)); + UpdateColor(); + + static bool crown = Abaddon::Get().GetSettings().GetShowOwnerCrown(); + if (crown && guild.OwnerID == member.User->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_start(10); + m_crown->show(); + } catch (...) {} + } + + m_avatar.set_margin_end(5); + m_avatar.set_halign(Gtk::ALIGN_START); + m_avatar.set_valign(Gtk::ALIGN_CENTER); + m_name.set_halign(Gtk::ALIGN_START); + m_name.set_valign(Gtk::ALIGN_CENTER); + + m_main.set_hexpand(true); + + m_main.add(m_avatar); + m_main.add(m_name); + if (m_crown != nullptr) + m_main.add(*m_crown); + + m_ev.add(m_main); + add(m_ev); + + m_avatar.show(); + m_name.show(); + m_main.show(); + m_ev.show(); +} + +void GuildSettingsMembersListItem::UpdateColor() { + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto user = *discord.GetUser(UserID); + if (auto color_id = discord.GetMemberHoistedRole(GuildID, UserID, true); color_id.IsValid()) { + auto role = *discord.GetRole(color_id); + m_name.set_markup("" + user.GetEscapedBoldString() + ""); + } else + m_name.set_markup(user.GetEscapedBoldString()); +} + +GuildSettingsMembersPaneInfo::GuildSettingsMembersPaneInfo(Snowflake guild_id) + : GuildID(guild_id) + , m_roles(guild_id) + , m_box(Gtk::ORIENTATION_VERTICAL) { + get_style_context()->add_class("guild-members-pane-info"); + + const auto label = [](Gtk::Label &lbl) { + lbl.set_single_line_mode(true); + lbl.set_halign(Gtk::ALIGN_START); + lbl.set_valign(Gtk::ALIGN_START); + lbl.set_ellipsize(Pango::ELLIPSIZE_END); + lbl.set_margin_bottom(5); + lbl.show(); + }; + + m_bot.set_text("User is a bot"); + + label(m_bot); + label(m_id); + label(m_created); + label(m_joined); + label(m_nickname); + label(m_boosting); + + m_box.set_halign(Gtk::ALIGN_FILL); + m_box.set_valign(Gtk::ALIGN_START); + m_box.set_hexpand(true); + m_box.set_vexpand(true); + m_box.add(m_bot); + m_box.add(m_id); + m_box.add(m_created); + m_box.add(m_joined); + m_box.add(m_nickname); + m_box.add(m_boosting); + m_box.add(m_roles); + + m_bot.hide(); + m_box.show(); + + add(m_box); +} + +void GuildSettingsMembersPaneInfo::SetUser(Snowflake user_id) { + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto guild = *discord.GetGuild(GuildID); + auto member = *discord.GetMember(user_id, GuildID); + member.User = discord.GetUser(user_id); + + m_bot.set_visible(member.User->IsBot.has_value() && *member.User->IsBot); + + m_id.set_text("User ID: " + std::to_string(user_id)); + m_created.set_text("Account created: " + user_id.GetLocalTimestamp()); + if (member.JoinedAt != "") + m_joined.set_text("Joined server: " + FormatISO8601(member.JoinedAt)); + else + m_joined.set_text("Joined server: Unknown"); + m_nickname.set_text("Nickname: " + member.Nickname); + m_nickname.set_visible(member.Nickname != ""); + if (member.PremiumSince.has_value()) { + m_boosting.set_text("Boosting since " + FormatISO8601(*member.PremiumSince)); + m_boosting.show(); + } else + m_boosting.hide(); + + m_roles.show(); + m_roles.SetRoles(user_id, member.Roles, guild.OwnerID == discord.GetUserData().ID); +} + +GuildSettingsMembersPaneRoles::GuildSettingsMembersPaneRoles(Snowflake guild_id) + : GuildID(guild_id) { + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto self_id = discord.GetUserData().ID; + const bool can_modify = discord.HasGuildPermission(self_id, guild_id, Permission::MANAGE_ROLES); + const auto highest = discord.GetMemberHighestRole(GuildID, self_id); + if (highest.has_value()) + m_hoisted_position = highest->Position; + + discord.signal_role_create().connect(sigc::mem_fun(*this, &GuildSettingsMembersPaneRoles::OnRoleCreate)); + discord.signal_role_update().connect(sigc::mem_fun(*this, &GuildSettingsMembersPaneRoles::OnRoleUpdate)); + discord.signal_role_delete().connect(sigc::mem_fun(*this, &GuildSettingsMembersPaneRoles::OnRoleDelete)); + + const auto guild = *discord.GetGuild(guild_id); + const auto roles = guild.FetchRoles(); + for (const auto &role : roles) { + CreateRow(can_modify, role, guild.OwnerID == self_id); + } + + m_list.set_sort_func([this](Gtk::ListBoxRow *a, Gtk::ListBoxRow *b) -> int { + auto *rowa = dynamic_cast(a); + auto *rowb = dynamic_cast(b); + return rowb->Position - rowa->Position; + }); + + set_propagate_natural_height(true); + set_propagate_natural_width(true); + set_hexpand(true); + set_vexpand(true); + set_halign(Gtk::ALIGN_FILL); + set_valign(Gtk::ALIGN_START); + + m_list.show(); + + add(m_list); +} + +void GuildSettingsMembersPaneRoles::SetRoles(Snowflake user_id, const std::vector &roles, bool is_owner) { + UserID = user_id; + + for (auto it = m_update_connection.begin(); it != m_update_connection.end();) { + it->disconnect(); + it = m_update_connection.erase(it); + } + + m_set_role_ids = { roles.begin(), roles.end() }; + for (const auto &[role_id, row] : m_rows) { + auto role = *Abaddon::Get().GetDiscordClient().GetRole(role_id); + if (role.ID == GuildID) { + row->SetChecked(true); + row->SetToggleable(false); + } else { + row->SetToggleable(role.Position < m_hoisted_position || is_owner); + row->SetChecked(m_set_role_ids.find(role_id) != m_set_role_ids.end()); + } + } +} + +void GuildSettingsMembersPaneRoles::CreateRow(bool has_manage_roles, const RoleData &role, bool is_owner) { + auto *row = Gtk::manage(new GuildSettingsMembersPaneRolesItem(has_manage_roles, role)); + if (role.ID == GuildID) { + row->SetChecked(true); + row->SetToggleable(false); + } else + row->SetToggleable(role.Position < m_hoisted_position || is_owner); + row->signal_role_click().connect(sigc::mem_fun(*this, &GuildSettingsMembersPaneRoles::OnRoleToggle)); + row->show(); + m_rows[role.ID] = row; + m_list.add(*row); +} + +void GuildSettingsMembersPaneRoles::OnRoleToggle(Snowflake role_id, bool new_set) { + auto row = m_rows.at(role_id); + row->SetToggleable(false); + auto &discord = Abaddon::Get().GetDiscordClient(); + auto cb = [this, new_set, role_id, row](bool success) { + if (!success) { // undo + if (new_set) + m_set_role_ids.erase(role_id); + else + m_set_role_ids.insert(role_id); + } else + row->SetChecked(new_set); + + row->SetToggleable(true); + }; + + if (new_set) + m_set_role_ids.insert(role_id); + else + m_set_role_ids.erase(role_id); + + // hack to prevent cb from being called if SetRoles is called before callback completion + sigc::signal tmp; + m_update_connection.push_back(tmp.connect(std::move(cb))); + const auto tmp_cb = [this, tmp = std::move(tmp)](DiscordError code) { tmp.emit(code == DiscordError::NONE); }; + discord.SetMemberRoles(GuildID, UserID, m_set_role_ids.begin(), m_set_role_ids.end(), sigc::track_obj(tmp_cb, *this)); +} + +void GuildSettingsMembersPaneRoles::OnRoleCreate(Snowflake guild_id, Snowflake role_id) { + if (guild_id != GuildID) return; + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto self_id = discord.GetUserData().ID; + const bool can_modify = discord.HasGuildPermission(self_id, GuildID, Permission::MANAGE_ROLES); + const auto role = *discord.GetRole(role_id); + CreateRow(can_modify, role, discord.GetGuild(guild_id)->OwnerID == self_id); +} + +void GuildSettingsMembersPaneRoles::OnRoleUpdate(Snowflake guild_id, Snowflake role_id) { + if (guild_id != GuildID) return; + auto role = *Abaddon::Get().GetDiscordClient().GetRole(role_id); + m_rows.at(role_id)->UpdateRoleData(role); + m_list.invalidate_sort(); +} + +void GuildSettingsMembersPaneRoles::OnRoleDelete(Snowflake guild_id, Snowflake role_id) { + if (guild_id != GuildID) return; + delete m_rows.at(role_id); +} + +GuildSettingsMembersPaneRolesItem::GuildSettingsMembersPaneRolesItem(bool sensitive, const RoleData &role) + : RoleID(role.ID) { + UpdateRoleData(role); + + m_main.set_hexpand(true); + m_main.set_vexpand(true); + + const auto cb = [this](GdkEventButton *event) -> bool { + if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) { + m_signal_role_click.emit(RoleID, !m_check.get_active()); + return true; + } + return false; + }; + m_check.signal_button_press_event().connect(cb, false); + + m_desired_sensitivity = sensitive; + ComputeSensitivity(); + + m_check.set_margin_start(5); + m_label.set_margin_start(5); + + m_main.add(m_check); + m_main.add(m_label); + add(m_main); + m_check.show(); + m_label.show(); + m_main.show(); +} + +void GuildSettingsMembersPaneRolesItem::SetChecked(bool checked) { + m_check.set_active(checked); +} + +void GuildSettingsMembersPaneRolesItem::SetToggleable(bool toggleable) { + m_desired_sensitivity = toggleable; + ComputeSensitivity(); +} + +void GuildSettingsMembersPaneRolesItem::UpdateRoleData(const RoleData &role) { + m_role = role; + Position = role.Position; + UpdateLabel(); +} + +void GuildSettingsMembersPaneRolesItem::UpdateLabel() { + if (m_role.Color) + m_label.set_markup("" + Glib::Markup::escape_text(m_role.Name) + ""); + else + m_label.set_text(m_role.Name); +} + +void GuildSettingsMembersPaneRolesItem::ComputeSensitivity() { + if (m_role.IsManaged) { + m_check.set_sensitive(false); + return; + } + m_check.set_sensitive(m_desired_sensitivity); +} + +GuildSettingsMembersPaneRolesItem::type_signal_role_click GuildSettingsMembersPaneRolesItem::signal_role_click() { + return m_signal_role_click; +} diff --git a/src/windows/guildsettings/memberspane.hpp b/src/windows/guildsettings/memberspane.hpp new file mode 100644 index 0000000..01398da --- /dev/null +++ b/src/windows/guildsettings/memberspane.hpp @@ -0,0 +1,135 @@ +#pragma once +#include +#include +#include "discord/member.hpp" +#include "discord/guild.hpp" +#include "components/lazyimage.hpp" + +class GuildSettingsMembersPaneRolesItem : public Gtk::ListBoxRow { +public: + GuildSettingsMembersPaneRolesItem(bool sensitive, const RoleData &role); + void SetChecked(bool checked); + void SetToggleable(bool toggleable); + void UpdateRoleData(const RoleData &role); + + Snowflake RoleID; + int Position; + +private: + void UpdateLabel(); + void ComputeSensitivity(); + bool m_desired_sensitivity = true; + + RoleData m_role; + + Gtk::Box m_main; + Gtk::CheckButton m_check; + Gtk::Label m_label; + + // own thing so we can stop it from actually changing + typedef sigc::signal type_signal_role_click; + + type_signal_role_click m_signal_role_click; + +public: + type_signal_role_click signal_role_click(); +}; + +class GuildSettingsMembersPaneRoles : public Gtk::ScrolledWindow { +public: + GuildSettingsMembersPaneRoles(Snowflake guild_id); + + void SetRoles(Snowflake user_id, const std::vector &roles, bool is_owner); + +private: + void CreateRow(bool has_manage_roles, const RoleData &role, bool is_owner); + + void OnRoleToggle(Snowflake role_id, bool new_set); + + void OnRoleCreate(Snowflake guild_id, Snowflake role_id); + void OnRoleUpdate(Snowflake guild_id, Snowflake role_id); + void OnRoleDelete(Snowflake guild_id, Snowflake role_id); + + int m_hoisted_position = 0; + + std::vector m_update_connection; + + std::unordered_set m_set_role_ids; + + Snowflake GuildID; + Snowflake UserID; + + Gtk::ListBox m_list; + + std::unordered_map m_rows; +}; + +class GuildSettingsMembersPaneInfo : public Gtk::ScrolledWindow { +public: + GuildSettingsMembersPaneInfo(Snowflake guild_id); + + void SetUser(Snowflake user_id); + +private: + Snowflake GuildID; + Snowflake UserID; + + Gtk::Label m_bot; + Gtk::Label m_id; + Gtk::Label m_created; + Gtk::Label m_joined; + Gtk::Label m_nickname; + Gtk::Label m_boosting; + GuildSettingsMembersPaneRoles m_roles; + Gtk::Box m_box; +}; + +class GuildSettingsMembersPaneMembers : public Gtk::Box { +public: + GuildSettingsMembersPaneMembers(Snowflake id); + +private: + Snowflake GuildID; + + Gtk::Entry m_search; + Gtk::ScrolledWindow m_list_scroll; + Gtk::ListBox m_list; + + typedef sigc::signal type_signal_member_select; + type_signal_member_select m_signal_member_select; + +public: + type_signal_member_select signal_member_select(); +}; + +class GuildSettingsMembersListItem : public Gtk::ListBoxRow { +public: + GuildSettingsMembersListItem(const GuildData &guild, const GuildMember &member); + + Glib::ustring DisplayTerm; + + Snowflake UserID; + Snowflake GuildID; + +private: + void UpdateColor(); + + Gtk::EventBox m_ev; + LazyImage m_avatar; + Gtk::Label m_name; + Gtk::Box m_main; + Gtk::Image *m_crown = nullptr; +}; + +class GuildSettingsMembersPane : public Gtk::Box { +public: + GuildSettingsMembersPane(Snowflake id); + +private: + Snowflake GuildID; + + Gtk::Box m_layout; + Gtk::Label m_note; + GuildSettingsMembersPaneMembers m_member_list; + GuildSettingsMembersPaneInfo m_member_info; +}; diff --git a/src/windows/guildsettings/rolespane.cpp b/src/windows/guildsettings/rolespane.cpp new file mode 100644 index 0000000..8d355ee --- /dev/null +++ b/src/windows/guildsettings/rolespane.cpp @@ -0,0 +1,419 @@ +#include "rolespane.hpp" +#include "abaddon.hpp" + +GuildSettingsRolesPane::GuildSettingsRolesPane(Snowflake id) + : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) + , GuildID(id) + , m_roles_list(id) + , m_roles_perms(id) { + set_name("guild-roles-pane"); + set_hexpand(true); + set_vexpand(true); + + m_roles_list.signal_role_select().connect(sigc::mem_fun(*this, &GuildSettingsRolesPane::OnRoleSelect)); + + m_roles_perms.set_sensitive(false); + + m_layout.set_homogeneous(true); + m_layout.add(m_roles_list); + m_layout.add(m_roles_perms); + add(m_layout); + + m_roles_list.show(); + m_roles_perms.show(); + m_layout.show(); +} + +void GuildSettingsRolesPane::OnRoleSelect(Snowflake role_id) { + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto role = *discord.GetRole(role_id); + m_roles_perms.SetRole(role); + m_roles_perms.set_sensitive(discord.CanModifyRole(GuildID, role_id)); +} + +static std::vector g_target_entries = { + Gtk::TargetEntry("GTK_LIST_ROLES_ROW", Gtk::TARGET_SAME_APP, 0) +}; + +GuildSettingsRolesPaneRoles::GuildSettingsRolesPaneRoles(Snowflake guild_id) + : Gtk::Box(Gtk::ORIENTATION_VERTICAL) + , GuildID(guild_id) { + m_list.get_style_context()->add_class("guild-roles-pane-list"); + + m_list_scroll.set_hexpand(true); + m_list_scroll.set_vexpand(true); + m_list_scroll.set_propagate_natural_height(true); + m_list_scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + + m_list.set_selection_mode(Gtk::SELECTION_SINGLE); + m_list.signal_row_selected().connect([this](Gtk::ListBoxRow *selected_) { + if (auto *selected = dynamic_cast(selected_)) + m_signal_role_select.emit(selected->RoleID); + }); + + m_list.set_focus_vadjustment(m_list_scroll.get_vadjustment()); + m_list.signal_on_drop().connect([this](Gtk::ListBoxRow *row_, int new_index) -> bool { + if (auto *row = dynamic_cast(row_)) { + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto num_rows = m_list.get_children().size(); + const auto new_pos = num_rows - new_index - 1; + if (row->RoleID == GuildID) return true; // moving role @everyone + if (static_cast(new_index) == num_rows) return true; // trying to move row below @everyone + // make sure it wont modify a neighbor role u dont have perms to modify + if (!discord.CanModifyRole(GuildID, row->RoleID)) return false; + const auto cb = [this](DiscordError code) { + if (code != DiscordError::NONE) { + Gtk::MessageDialog dlg("Failed to set role position", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); + dlg.set_position(Gtk::WIN_POS_CENTER_ON_PARENT); + dlg.run(); + } + }; + discord.ModifyRolePosition(GuildID, row->RoleID, new_pos, sigc::track_obj(cb, *this)); + return true; + } + return false; + }); + + auto &discord = Abaddon::Get().GetDiscordClient(); + discord.signal_role_create().connect(sigc::mem_fun(*this, &GuildSettingsRolesPaneRoles::OnRoleCreate)); + discord.signal_role_delete().connect(sigc::mem_fun(*this, &GuildSettingsRolesPaneRoles::OnRoleDelete)); + + const auto guild = *discord.GetGuild(GuildID); + const auto roles = guild.FetchRoles(); + const bool can_modify = discord.HasGuildPermission(discord.GetUserData().ID, GuildID, Permission::MANAGE_ROLES); + for (const auto &role : roles) { + auto *row = Gtk::manage(new GuildSettingsRolesPaneRolesListItem(guild, role)); + row->drag_source_set(g_target_entries, Gdk::BUTTON1_MASK, Gdk::ACTION_MOVE); + row->set_margin_start(5); + row->set_halign(Gtk::ALIGN_FILL); + row->show(); + m_rows[role.ID] = row; + if (can_modify) + m_list.add_draggable(row); + else + m_list.add(*row); + } + + m_list.set_sort_func([this](Gtk::ListBoxRow *rowa_, Gtk::ListBoxRow *rowb_) -> int { + auto *rowa = dynamic_cast(rowa_); + auto *rowb = dynamic_cast(rowb_); + return rowb->Position - rowa->Position; + }); + m_list.invalidate_sort(); + + m_list.set_filter_func([this](Gtk::ListBoxRow *row_) -> bool { + const auto search_term = m_search.get_text(); + if (search_term.size() == 0) return true; + if (auto *row = dynamic_cast(row_)) + return StringContainsCaseless(row->DisplayTerm, m_search.get_text()); + return true; + }); + + m_search.set_placeholder_text("Filter"); + m_search.signal_changed().connect([this] { + m_list.invalidate_filter(); + }); + + m_list_scroll.add(m_list); + add(m_search); + add(m_list_scroll); + + m_search.show(); + m_list.show(); + m_list_scroll.show(); +} + +void GuildSettingsRolesPaneRoles::OnRoleCreate(Snowflake guild_id, Snowflake role_id) { + if (guild_id != GuildID) return; + auto &discord = Abaddon::Get().GetDiscordClient(); + const bool can_modify = discord.HasGuildPermission(discord.GetUserData().ID, guild_id, Permission::MANAGE_ROLES); + const auto guild = *discord.GetGuild(guild_id); + const auto role = *discord.GetRole(role_id); + auto *row = Gtk::manage(new GuildSettingsRolesPaneRolesListItem(guild, role)); + row->show(); + m_rows[role_id] = row; + if (can_modify) + m_list.add_draggable(row); + else + m_list.add(*row); +} + +void GuildSettingsRolesPaneRoles::OnRoleDelete(Snowflake guild_id, Snowflake role_id) { + if (guild_id != GuildID) return; + auto it = m_rows.find(role_id); + delete it->second; + m_rows.erase(it); +} + +GuildSettingsRolesPaneRoles::type_signal_role_select GuildSettingsRolesPaneRoles::signal_role_select() { + return m_signal_role_select; +} + +GuildSettingsRolesPaneRolesListItem::GuildSettingsRolesPaneRolesListItem(const GuildData &guild, const RoleData &role) + : GuildID(guild.ID) + , RoleID(role.ID) + , Position(role.Position) { + auto &discord = Abaddon::Get().GetDiscordClient(); + + set_hexpand(true); + + UpdateItem(role); + + discord.signal_role_update().connect(sigc::mem_fun(*this, &GuildSettingsRolesPaneRolesListItem::OnRoleUpdate)); + + m_name.set_ellipsize(Pango::ELLIPSIZE_END); + + m_ev.set_halign(Gtk::ALIGN_START); + m_ev.add(m_name); + add(m_ev); + + m_name.show(); + m_ev.show(); +} + +void GuildSettingsRolesPaneRolesListItem::UpdateItem(const RoleData &role) { + DisplayTerm = role.Name; + + if (role.Color != 0) + m_name.set_markup("" + + Glib::Markup::escape_text(role.Name) + + ""); + else + m_name.set_text(role.Name); +} + +void GuildSettingsRolesPaneRolesListItem::OnRoleUpdate(Snowflake guild_id, Snowflake role_id) { + if (guild_id != GuildID || role_id != RoleID) return; + const auto role = Abaddon::Get().GetDiscordClient().GetRole(RoleID); + if (!role.has_value()) return; + Position = role->Position; + UpdateItem(*role); + changed(); +} + +GuildSettingsRolesPaneInfo::GuildSettingsRolesPaneInfo(Snowflake guild_id) + : GuildID(guild_id) + , m_layout(Gtk::ORIENTATION_VERTICAL) + , m_meta(Gtk::ORIENTATION_HORIZONTAL) { + set_propagate_natural_height(true); + set_propagate_natural_width(true); + + auto &discord = Abaddon::Get().GetDiscordClient(); + discord.signal_role_update().connect(sigc::mem_fun(*this, &GuildSettingsRolesPaneInfo::OnRoleUpdate)); + + const auto cb = [this](GdkEventKey *e) -> bool { + if (e->keyval == GDK_KEY_Return) + UpdateRoleName(); + return false; + }; + m_role_name.signal_key_press_event().connect(cb, false); + + m_role_name.set_tooltip_text("Press enter to submit"); + + m_role_name.set_max_length(100); + + m_role_name.set_margin_top(5); + m_role_name.set_margin_bottom(5); + m_role_name.set_margin_start(5); + m_role_name.set_margin_end(5); + + m_color_button.set_margin_top(5); + m_color_button.set_margin_bottom(5); + m_color_button.set_margin_start(5); + m_color_button.set_margin_end(5); + + m_color_button.signal_color_set().connect([this, &discord]() { + const auto color = m_color_button.get_rgba(); + const auto cb = [this, &discord](DiscordError code) { + if (code != DiscordError::NONE) { + m_color_button.set_rgba(IntToRGBA(discord.GetRole(RoleID)->Color)); + Gtk::MessageDialog dlg("Failed to set role color", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); + dlg.set_position(Gtk::WIN_POS_CENTER_ON_PARENT); + dlg.run(); + } + }; + discord.ModifyRoleColor(GuildID, RoleID, color, cb); + }); + + int left_ypos = 0; + int right_ypos = 0; + + const int LEFT = 0; + const int RIGHT = 1; + + auto add_perms = [&](const std::string &label, int side, const std::initializer_list &perms) { + int &pos = side == LEFT ? left_ypos : right_ypos; + auto *header = Gtk::manage(new Gtk::Label(label)); + header->show(); + m_grid.attach(*header, side, pos++, 1, 1); + for (const auto perm : perms) { + auto *btn = Gtk::manage(new GuildSettingsRolesPanePermItem(perm)); + btn->signal_permission_click().connect(sigc::mem_fun(*this, &GuildSettingsRolesPaneInfo::OnPermissionToggle)); + m_perm_items[perm] = btn; + btn->show(); + m_grid.attach(*btn, side, pos++, 1, 1); + } + pos++; + }; + + // fuck you clang-format you suck + // clang-format off + add_perms("General", LEFT, { + Permission::VIEW_CHANNEL, + Permission::MANAGE_CHANNELS, + Permission::MANAGE_ROLES, + Permission::MANAGE_EMOJIS, + Permission::VIEW_AUDIT_LOG, + Permission::VIEW_GUILD_INSIGHTS, + Permission::MANAGE_WEBHOOKS, + Permission::MANAGE_GUILD }); + + add_perms("Membership", LEFT, { + Permission::CREATE_INSTANT_INVITE, + Permission::CHANGE_NICKNAME, + Permission::MANAGE_NICKNAMES, + Permission::KICK_MEMBERS, + Permission::BAN_MEMBERS }); + + add_perms("Text Channels", RIGHT, { + Permission::SEND_MESSAGES, + Permission::USE_PUBLIC_THREADS, + Permission::USE_PRIVATE_THREADS, + Permission::EMBED_LINKS, + Permission::ATTACH_FILES, + Permission::ADD_REACTIONS, + Permission::USE_EXTERNAL_EMOJIS, + Permission::MENTION_EVERYONE, + Permission::MANAGE_MESSAGES, + Permission::MANAGE_THREADS, + Permission::READ_MESSAGE_HISTORY, + Permission::SEND_TTS_MESSAGES, + Permission::USE_SLASH_COMMANDS }); + + add_perms("Voice Channels", RIGHT, { + Permission::CONNECT, + Permission::SPEAK, + Permission::STREAM, + Permission::USE_VAD, + Permission::PRIORITY_SPEAKER, + Permission::MUTE_MEMBERS, + Permission::DEAFEN_MEMBERS, + Permission::MOVE_MEMBERS }); + + add_perms("Advanced", LEFT, { Permission::ADMINISTRATOR }); + + // clang-format on + + m_meta.add(m_role_name); + m_meta.add(m_color_button); + m_layout.add(m_meta); + m_layout.add(m_grid); + add(m_layout); + m_meta.show(); + m_color_button.show(); + m_role_name.show(); + m_layout.show(); + m_grid.show(); +} + +void GuildSettingsRolesPaneInfo::SetRole(const RoleData &role) { + for (auto it = m_update_connections.begin(); it != m_update_connections.end();) { + it->disconnect(); + it = m_update_connections.erase(it); + } + + if (role.Color != 0) { + m_color_button.set_rgba(IntToRGBA(role.Color)); + } else { + static Gdk::RGBA trans; + trans.set_alpha(0.0); + m_color_button.set_rgba(trans); + } + + m_role_name.set_text(role.Name); + + RoleID = role.ID; + m_perms = role.Permissions; + for (const auto [perm, btn] : m_perm_items) { + btn->set_sensitive(true); + btn->set_active((role.Permissions & perm) == perm); + } +} + +void GuildSettingsRolesPaneInfo::OnRoleUpdate(Snowflake guild_id, Snowflake role_id) { + if (guild_id != GuildID || role_id != RoleID) return; + const auto role = *Abaddon::Get().GetDiscordClient().GetRole(RoleID); + m_role_name.set_text(role.Name); + + if (role.Color != 0) { + m_color_button.set_rgba(IntToRGBA(role.Color)); + } else { + static Gdk::RGBA trans; + trans.set_alpha(0.0); + m_color_button.set_rgba(trans); + } + + m_perms = role.Permissions; + for (const auto [perm, btn] : m_perm_items) + btn->set_active((role.Permissions & perm) == perm); +} + +void GuildSettingsRolesPaneInfo::OnPermissionToggle(Permission perm, bool new_set) { + auto btn = m_perm_items.at(perm); + btn->set_sensitive(false); + auto &discord = Abaddon::Get().GetDiscordClient(); + auto cb = [this, new_set, perm, btn](bool success) { + if (!success) { // undo + if (new_set) + m_perms &= ~perm; + else + m_perms |= perm; + } else + btn->set_active(new_set); + btn->set_sensitive(true); + }; + + if (new_set) + m_perms |= perm; + else + m_perms &= ~perm; + + sigc::signal tmp; + m_update_connections.push_back(tmp.connect(std::move(cb))); + const auto tmp_cb = [this, tmp = std::move(tmp)](DiscordError code) { tmp.emit(code == DiscordError::NONE); }; + discord.ModifyRolePermissions(GuildID, RoleID, m_perms, sigc::track_obj(tmp_cb, *this)); +} + +void GuildSettingsRolesPaneInfo::UpdateRoleName() { + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.GetRole(RoleID)->Name == m_role_name.get_text()) return; + + const auto cb = [this, &discord](DiscordError code) { + if (code != DiscordError::NONE) { + m_role_name.set_text(discord.GetRole(RoleID)->Name); + Gtk::MessageDialog dlg("Failed to set role name", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); + dlg.set_position(Gtk::WIN_POS_CENTER_ON_PARENT); + dlg.run(); + } + }; + discord.ModifyRoleName(GuildID, RoleID, m_role_name.get_text(), cb); +} + +GuildSettingsRolesPanePermItem::GuildSettingsRolesPanePermItem(Permission perm) + : Gtk::CheckButton(GetPermissionString(perm)) + , m_permission(perm) { + set_tooltip_text(GetPermissionDescription(m_permission)); + + const auto cb = [this](GdkEventButton *event) -> bool { + if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) { + m_signal_permission.emit(m_permission, !get_active()); + return true; + } + return false; + }; + signal_button_press_event().connect(cb, false); +} + +GuildSettingsRolesPanePermItem::type_signal_permission_click GuildSettingsRolesPanePermItem::signal_permission_click() { + return m_signal_permission; +} diff --git a/src/windows/guildsettings/rolespane.hpp b/src/windows/guildsettings/rolespane.hpp new file mode 100644 index 0000000..2999f32 --- /dev/null +++ b/src/windows/guildsettings/rolespane.hpp @@ -0,0 +1,102 @@ +#pragma once +#include +#include +#include "discord/guild.hpp" +#include "components/draglistbox.hpp" + +class GuildSettingsRolesPaneRolesListItem : public Gtk::ListBoxRow { +public: + GuildSettingsRolesPaneRolesListItem(const GuildData &guild, const RoleData &role); + + Glib::ustring DisplayTerm; + + Snowflake GuildID; + Snowflake RoleID; + int Position; + +private: + void UpdateItem(const RoleData &role); + void OnRoleUpdate(Snowflake guild_id, Snowflake role_id); + + Gtk::EventBox m_ev; + Gtk::Label m_name; +}; + +class GuildSettingsRolesPaneRoles : public Gtk::Box { +public: + GuildSettingsRolesPaneRoles(Snowflake guild_id); + +private: + void OnRoleCreate(Snowflake guild_id, Snowflake role_id); + void OnRoleDelete(Snowflake guild_id, Snowflake role_id); + + Snowflake GuildID; + + Gtk::Entry m_search; + Gtk::ScrolledWindow m_list_scroll; + DragListBox m_list; + + typedef sigc::signal type_signal_role_select; + type_signal_role_select m_signal_role_select; + +public: + std::unordered_map m_rows; + type_signal_role_select signal_role_select(); +}; + +class GuildSettingsRolesPanePermItem : public Gtk::CheckButton { +public: + GuildSettingsRolesPanePermItem(Permission perm); + +private: + Permission m_permission; + + typedef sigc::signal type_signal_permission_click; + + type_signal_permission_click m_signal_permission; + +public: + type_signal_permission_click signal_permission_click(); +}; + +class GuildSettingsRolesPaneInfo : public Gtk::ScrolledWindow { +public: + GuildSettingsRolesPaneInfo(Snowflake guild_id); + + void SetRole(const RoleData &role); + +private: + void OnRoleUpdate(Snowflake guild_id, Snowflake role_id); + void OnPermissionToggle(Permission perm, bool new_set); + + void UpdateRoleName(); + + Snowflake GuildID; + Snowflake RoleID; + + Permission m_perms; + + std::vector m_update_connections; + + Gtk::Box m_layout; + Gtk::Box m_meta; + Gtk::Entry m_role_name; + Gtk::ColorButton m_color_button; + Gtk::Grid m_grid; + + std::unordered_map m_perm_items; +}; + +class GuildSettingsRolesPane : public Gtk::Box { +public: + GuildSettingsRolesPane(Snowflake id); + +private: + void OnRoleSelect(Snowflake role_id); + + Snowflake GuildID; + + Gtk::Box m_layout; + GuildSettingsRolesPaneRoles m_roles_list; + GuildSettingsRolesPaneInfo m_roles_perms; +}; -- cgit v1.2.3