summaryrefslogtreecommitdiff
path: root/src/windows/guildsettings
diff options
context:
space:
mode:
authorDylam De La Torre <DyXel04@gmail.com>2021-11-23 05:21:56 +0100
committerGitHub <noreply@github.com>2021-11-23 04:21:56 +0000
commita51a54bc5979a2491f152abc47ad54e6b63f27c8 (patch)
treece67092b2f6df366033a65a6111e4650866766b2 /src/windows/guildsettings
parentd88079000a79e6bcbe51c5a2868d57b303b5fcb6 (diff)
downloadabaddon-portaudio-a51a54bc5979a2491f152abc47ad54e6b63f27c8.tar.gz
abaddon-portaudio-a51a54bc5979a2491f152abc47ad54e6b63f27c8.zip
Restructure source and resource files (#46)
importantly, res is now res/res and css is now res/css
Diffstat (limited to 'src/windows/guildsettings')
-rw-r--r--src/windows/guildsettings/auditlogpane.cpp636
-rw-r--r--src/windows/guildsettings/auditlogpane.hpp19
-rw-r--r--src/windows/guildsettings/banspane.cpp161
-rw-r--r--src/windows/guildsettings/banspane.hpp45
-rw-r--r--src/windows/guildsettings/emojispane.cpp257
-rw-r--r--src/windows/guildsettings/emojispane.hpp53
-rw-r--r--src/windows/guildsettings/infopane.cpp220
-rw-r--r--src/windows/guildsettings/infopane.hpp26
-rw-r--r--src/windows/guildsettings/invitespane.cpp136
-rw-r--r--src/windows/guildsettings/invitespane.hpp43
-rw-r--r--src/windows/guildsettings/memberspane.cpp410
-rw-r--r--src/windows/guildsettings/memberspane.hpp135
-rw-r--r--src/windows/guildsettings/rolespane.cpp419
-rw-r--r--src/windows/guildsettings/rolespane.hpp102
14 files changed, 2662 insertions, 0 deletions
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 = "<b>Unknown User</b>";
+ if (entry.UserID.has_value()) {
+ if (auto user = discord.GetUser(*entry.UserID); user.has_value())
+ user_markup = discord.GetUser(*entry.UserID)->GetEscapedBoldString<false>();
+ }
+
+ // spaghetti moment
+ Glib::ustring markup;
+ std::vector<Glib::ustring> extra_markup;
+ switch (entry.Type) {
+ case AuditLogActionType::GUILD_UPDATE: {
+ markup =
+ user_markup +
+ " made changes to <b>" +
+ Glib::Markup::escape_text(guild.Name) +
+ "</b>";
+
+ 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 <b>" +
+ Glib::Markup::escape_text(new_name->get<std::string>()) +
+ "</b>");
+ else
+ extra_markup.push_back("Set the server name");
+ }
+ }
+ } break;
+ case AuditLogActionType::CHANNEL_CREATE: {
+ const auto type = *entry.GetNewFromKey<ChannelType>("type");
+ markup = user_markup +
+ " created a " + (type == ChannelType::GUILD_VOICE ? "voice" : "text") +
+ " channel <b>#" +
+ Glib::Markup::escape_text(*entry.GetNewFromKey<std::string>("name")) +
+ "</b>";
+ 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 <b>" +
+ Glib::Markup::escape_text(change.NewValue->get<std::string>()) +
+ "</b>");
+ 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 <b>#" +
+ Glib::Markup::escape_text(*target_channel->Name) +
+ "</b>";
+ } else {
+ markup = user_markup +
+ " made changes to <b>&lt;#" +
+ entry.TargetID +
+ "&gt;</b>";
+ }
+ 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 <b>" +
+ Glib::Markup::escape_text(change.OldValue->get<std::string>()) +
+ "</b> to <b>" +
+ Glib::Markup::escape_text(change.NewValue->get<std::string>()) +
+ "</b>");
+ else
+ extra_markup.push_back("Changed the name to <b>" +
+ Glib::Markup::escape_text(change.NewValue->get<std::string>()) +
+ "</b>");
+ } else if (change.Key == "topic") {
+ if (change.NewValue.has_value())
+ extra_markup.push_back("Changed the topic to <b>" +
+ Glib::Markup::escape_text(change.NewValue->get<std::string>()) +
+ "</b>");
+ 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<int>();
+ if (secs == 0)
+ extra_markup.push_back("Disabled slowmode");
+ else
+ extra_markup.push_back("Set slowmode to <b>" +
+ std::to_string(secs) + " seconds</b>");
+ }
+ }
+ } break;
+ case AuditLogActionType::CHANNEL_DELETE: {
+ markup = user_markup +
+ " removed <b>#" +
+ Glib::Markup::escape_text(*entry.GetOldFromKey<std::string>("name")) +
+ "</b>";
+ } break;
+ case AuditLogActionType::CHANNEL_OVERWRITE_CREATE: {
+ const auto channel = discord.GetChannel(entry.TargetID);
+ if (channel.has_value()) {
+ markup = user_markup +
+ " created channel overrides for <b>#" +
+ Glib::Markup::escape_text(*channel->Name) + "</b>";
+ } else {
+ markup = user_markup +
+ " created channel overrides for <b>&lt;#" +
+ entry.TargetID + "&gt;</b>";
+ }
+ } break;
+ case AuditLogActionType::CHANNEL_OVERWRITE_UPDATE: {
+ const auto channel = discord.GetChannel(entry.TargetID);
+ if (channel.has_value()) {
+ markup = user_markup +
+ " updated channel overrides for <b>#" +
+ Glib::Markup::escape_text(*channel->Name) + "</b>";
+ } else {
+ markup = user_markup +
+ " updated channel overrides for <b>&lt;#" +
+ entry.TargetID + "&gt;</b>";
+ }
+ } break;
+ case AuditLogActionType::CHANNEL_OVERWRITE_DELETE: {
+ const auto channel = discord.GetChannel(entry.TargetID);
+ if (channel.has_value()) {
+ markup = user_markup +
+ " removed channel overrides for <b>#" +
+ Glib::Markup::escape_text(*channel->Name) + "</b>";
+ } else {
+ markup = user_markup +
+ " removed channel overrides for <b>&lt;#" +
+ entry.TargetID + "&gt;</b>";
+ }
+ } break;
+ case AuditLogActionType::MEMBER_KICK: {
+ const auto target_user = discord.GetUser(entry.TargetID);
+ markup = user_markup +
+ " kicked <b>" +
+ target_user->GetEscapedString() +
+ "</b>";
+ } break;
+ case AuditLogActionType::MEMBER_PRUNE: {
+ markup = user_markup +
+ " pruned <b>" +
+ *entry.Options->MembersRemoved +
+ "</b> members";
+ extra_markup.push_back("For <b>" +
+ *entry.Options->DeleteMemberDays +
+ " days</b> of inactivity");
+ } break;
+ case AuditLogActionType::MEMBER_BAN_ADD: {
+ const auto target_user = discord.GetUser(entry.TargetID);
+ markup = user_markup +
+ " banned <b>" +
+ target_user->GetEscapedString() +
+ "</b>";
+ } break;
+ case AuditLogActionType::MEMBER_BAN_REMOVE: {
+ const auto target_user = discord.GetUser(entry.TargetID);
+ markup = user_markup +
+ " removed the ban for <b>" +
+ target_user->GetEscapedString() +
+ "</b>";
+ } break;
+ case AuditLogActionType::MEMBER_UPDATE: {
+ const auto target_user = discord.GetUser(entry.TargetID);
+ markup = user_markup +
+ " updated <b>" +
+ target_user->GetEscapedString() +
+ "</b>";
+ 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<bool>() ? "<b>Deafened</b>"s : "<b>Undeafened</b>"s) +
+ " them");
+ else if (change.Key == "mute" && change.NewValue.has_value())
+ extra_markup.push_back(
+ (change.NewValue->get<bool>() ? "<b>Muted</b>"s : "<b>Unmuted</b>"s) +
+ " them");
+ else if (change.Key == "nick" && change.NewValue.has_value())
+ extra_markup.push_back("Set their nickname to <b>" +
+ Glib::Markup::escape_text(change.NewValue->get<std::string>()) +
+ "</b>");
+ }
+ } break;
+ case AuditLogActionType::MEMBER_ROLE_UPDATE: {
+ const auto target_user = discord.GetUser(entry.TargetID);
+ markup = user_markup +
+ " updated roles for <b>" +
+ target_user->GetEscapedString() + "</b>";
+ if (entry.Changes.has_value())
+ for (const auto &change : *entry.Changes) {
+ if (change.Key == "$remove" && change.NewValue.has_value()) {
+ extra_markup.push_back("<b>Removed</b> a role <b>" +
+ Glib::Markup::escape_text(change.NewValue.value()[0].at("name").get<std::string>()) +
+ "</b>");
+ } else if (change.Key == "$add" && change.NewValue.has_value()) {
+ extra_markup.push_back("<b>Added</b> a role <b>" +
+ Glib::Markup::escape_text(change.NewValue.value()[0].at("name").get<std::string>()) +
+ "</b>");
+ }
+ }
+ } break;
+ case AuditLogActionType::MEMBER_MOVE: {
+ const auto channel = discord.GetChannel(*entry.Options->ChannelID);
+ markup = user_markup +
+ " moved <b>" +
+ *entry.Options->Count +
+ " user" +
+ (*entry.Options->Count == "1" ? ""s : "s"s) +
+ "</b> to <b>" +
+ Glib::Markup::escape_text(*channel->Name) +
+ "</b>";
+ } break;
+ case AuditLogActionType::MEMBER_DISCONNECT: {
+ markup = user_markup +
+ " disconnected <b>" +
+ *entry.Options->Count +
+ "</b> users from voice";
+ } break;
+ case AuditLogActionType::BOT_ADD: {
+ const auto target_user = discord.GetUser(entry.TargetID);
+ markup = user_markup +
+ " added <b>" +
+ target_user->GetEscapedString() +
+ "</b> to the server";
+ } break;
+ case AuditLogActionType::ROLE_CREATE: {
+ markup = user_markup +
+ " created the role <b>" +
+ *entry.GetNewFromKey<std::string>("name") +
+ "</b>";
+ } break;
+ case AuditLogActionType::ROLE_UPDATE: {
+ const auto role = discord.GetRole(entry.TargetID);
+ markup = user_markup +
+ " updated the role <b>" +
+ (role.has_value() ? Glib::Markup::escape_text(role->Name) : Glib::ustring(entry.TargetID)) +
+ "</b>";
+ 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 <b>" +
+ Glib::Markup::escape_text(change.NewValue->get<std::string>()) +
+ "</b>");
+ } else if (change.Key == "color" && change.NewValue.has_value()) {
+ const auto col = change.NewValue->get<int>();
+ if (col == 0)
+ extra_markup.push_back("Removed the color");
+ else
+ extra_markup.push_back("Set the color to <b>" +
+ IntToCSSColor(col) +
+ "</b>");
+ } 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<bool>() ? "Mentionable" : "Not mentionable");
+ } else if (change.Key == "hoist" && change.NewValue.has_value()) {
+ extra_markup.push_back(change.NewValue->get<bool>() ? "Not hoisted" : "Hoisted");
+ }
+ }
+ } break;
+ case AuditLogActionType::ROLE_DELETE: {
+ markup = user_markup +
+ " deleted the role <b>" +
+ *entry.GetOldFromKey<std::string>("name") +
+ "</b>";
+ } break;
+ case AuditLogActionType::INVITE_CREATE: {
+ const auto code = *entry.GetNewFromKey<std::string>("code");
+ markup = user_markup +
+ " created an invite <b>" + code + "</b>";
+ 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<Snowflake>());
+ if (!channel.has_value()) continue;
+ extra_markup.push_back("For channel <b>#" +
+ Glib::Markup::escape_text(*channel->Name) +
+ "</b>");
+ } else if (change.Key == "max_uses" && change.NewValue.has_value()) {
+ const auto uses = change.NewValue->get<int>();
+ if (uses == 0)
+ extra_markup.push_back("Which has <b>unlimited</b> uses");
+ else
+ extra_markup.push_back("Which has <b>" + std::to_string(uses) + "</b> uses");
+ } else if (change.Key == "temporary" && change.NewValue.has_value()) {
+ extra_markup.push_back("With temporary <b>"s +
+ (change.NewValue->get<bool>() ? "on" : "off") +
+ "</b>");
+ } // no max_age cuz fuck time
+ }
+ } break;
+ case AuditLogActionType::INVITE_DELETE: {
+ markup = user_markup +
+ " deleted an invite <b>" +
+ *entry.GetOldFromKey<std::string>("code") +
+ "</b>";
+ } break;
+ case AuditLogActionType::WEBHOOK_CREATE: {
+ markup = user_markup +
+ " created the webhook <b>" +
+ Glib::Markup::escape_text(*entry.GetNewFromKey<std::string>("name")) +
+ "</b>";
+ for (const auto &change : *entry.Changes) {
+ if (change.Key == "channel_id" && change.NewValue.has_value()) {
+ const auto channel = discord.GetChannel(change.NewValue->get<Snowflake>());
+ if (channel.has_value()) {
+ extra_markup.push_back("With channel <b>#" +
+ Glib::Markup::escape_text(*channel->Name) +
+ "</b>");
+ }
+ }
+ }
+ } 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 <b>" +
+ Glib::Markup::escape_text(webhookptr->Name) +
+ "</b>";
+ } 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 <b>" +
+ Glib::Markup::escape_text(change.NewValue->get<std::string>()) +
+ "</b>");
+ } 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<Snowflake>());
+ if (channel.has_value()) {
+ extra_markup.push_back("Changed the channel to <b>#" +
+ Glib::Markup::escape_text(*channel->Name) +
+ "</b>");
+ } else {
+ extra_markup.push_back("Changed the channel");
+ }
+ }
+ }
+ } break;
+ case AuditLogActionType::WEBHOOK_DELETE: {
+ markup = user_markup +
+ " deleted the webhook <b>" +
+ Glib::Markup::escape_text(*entry.GetOldFromKey<std::string>("name")) +
+ "</b>";
+ } break;
+ case AuditLogActionType::EMOJI_CREATE: {
+ markup = user_markup +
+ " created the emoji <b>" +
+ Glib::Markup::escape_text(*entry.GetNewFromKey<std::string>("name")) +
+ "</b>";
+ } break;
+ case AuditLogActionType::EMOJI_UPDATE: {
+ markup = user_markup +
+ " updated the emoji <b>" +
+ Glib::Markup::escape_text(*entry.GetOldFromKey<std::string>("name")) +
+ "</b>";
+ extra_markup.push_back("Changed the name from <b>" +
+ Glib::Markup::escape_text(*entry.GetOldFromKey<std::string>("name")) +
+ "</b> to <b>" +
+ Glib::Markup::escape_text(*entry.GetNewFromKey<std::string>("name")) +
+ "</b>");
+ } break;
+ case AuditLogActionType::EMOJI_DELETE: {
+ markup = user_markup +
+ " deleted the emoji <b>" +
+ Glib::Markup::escape_text(*entry.GetOldFromKey<std::string>("name")) +
+ "</b>";
+ } 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 <b>" + count + "</b> messages in <b>#" +
+ Glib::Markup::escape_text(*channel->Name) +
+ "</b>";
+ } else {
+ markup = user_markup +
+ " deleted <b>" + count + "</b> messages";
+ }
+ } break;
+ case AuditLogActionType::MESSAGE_BULK_DELETE: {
+ const auto channel = discord.GetChannel(entry.TargetID);
+ if (channel.has_value()) {
+ markup = user_markup +
+ " deleted <b>" +
+ *entry.Options->Count +
+ "</b> messages in <b>#" +
+ Glib::Markup::escape_text(*channel->Name) +
+ "</b>";
+ } else {
+ markup = user_markup +
+ " deleted <b>" +
+ *entry.Options->Count +
+ "</b> messages";
+ }
+ } break;
+ case AuditLogActionType::MESSAGE_PIN: {
+ const auto target_user = discord.GetUser(entry.TargetID);
+ markup = user_markup +
+ " pinned a message by <b>" +
+ target_user->GetEscapedString() +
+ "</b>";
+ } break;
+ case AuditLogActionType::MESSAGE_UNPIN: {
+ const auto target_user = discord.GetUser(entry.TargetID);
+ markup = user_markup +
+ " unpinned a message by <b>" +
+ target_user->GetEscapedString() +
+ "</b>";
+ } 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 <b>" +
+ Glib::Markup::escape_text(*channel->Name) +
+ "</b>";
+ } else {
+ markup = user_markup +
+ " started the stage for <b>" +
+ std::to_string(*entry.Options->ChannelID) +
+ "</b>";
+ }
+
+ 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 <b>" +
+ Glib::Markup::escape_text(change.NewValue->get<std::string>()) +
+ "</b>");
+ } else if (change.Key == "privacy_level" && change.NewValue.has_value()) {
+ Glib::ustring str = Glib::Markup::escape_text(GetStagePrivacyDisplayString(change.NewValue->get<StagePrivacy>()));
+ extra_markup.push_back(
+ "Set the privacy level to <b>" +
+ str +
+ "</b>");
+ }
+ }
+ }
+ } 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 <b>" +
+ Glib::Markup::escape_text(*channel->Name) +
+ "</b>";
+ } else {
+ markup = user_markup +
+ " updated the stage for <b>" +
+ std::to_string(*entry.Options->ChannelID) +
+ "</b>";
+ }
+
+ 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 <b>" +
+ Glib::Markup::escape_text(change.NewValue->get<std::string>()) +
+ "</b>");
+ } else if (change.Key == "privacy_level" && change.NewValue.has_value()) {
+ Glib::ustring str = Glib::Markup::escape_text(GetStagePrivacyDisplayString(change.NewValue->get<StagePrivacy>()));
+ extra_markup.push_back(
+ "Set the privacy level to <b>" +
+ str +
+ "</b>");
+ }
+ }
+ }
+ } 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 <b>" +
+ Glib::Markup::escape_text(*channel->Name) +
+ "</b>";
+ } else {
+ markup = user_markup +
+ " ended the stage for <b>" +
+ std::to_string(*entry.Options->ChannelID) +
+ "</b>";
+ }
+ } break;
+ case AuditLogActionType::THREAD_CREATE: {
+ const auto channel = discord.GetChannel(entry.TargetID);
+ markup = user_markup +
+ " created a thread <b>" +
+ (channel.has_value()
+ ? Glib::Markup::escape_text(*channel->Name)
+ : Glib::ustring(*entry.GetNewFromKey<std::string>("name"))) +
+ "</b>";
+ if (entry.Changes.has_value()) {
+ for (const auto &change : *entry.Changes) {
+ if (change.Key == "name")
+ extra_markup.push_back("Set the name to <b>" + Glib::Markup::escape_text(change.NewValue->get<std::string>()) + "</b>");
+ else if (change.Key == "archived")
+ extra_markup.push_back(change.NewValue->get<bool>() ? "Archived the thread" : "Unarchived the thread");
+ else if (change.Key == "auto_archive_duration")
+ extra_markup.push_back("Set auto archive duration to <b>"s + std::to_string(change.NewValue->get<int>()) + " minutes</b>"s);
+ else if (change.Key == "rate_limit_per_user" && change.NewValue.has_value()) {
+ const int secs = change.NewValue->get<int>();
+ if (secs == 0)
+ extra_markup.push_back("Disabled slowmode");
+ else
+ extra_markup.push_back("Set slowmode to <b>" +
+ std::to_string(secs) + " seconds</b>");
+ } else if (change.Key == "locked")
+ extra_markup.push_back(change.NewValue->get<bool>() ? "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 <b>" +
+ (channel.has_value()
+ ? Glib::Markup::escape_text(*channel->Name)
+ : Glib::ustring(entry.TargetID)) +
+ "</b>";
+ for (const auto &change : *entry.Changes) {
+ if (change.Key == "name")
+ extra_markup.push_back(
+ "Changed the name from <b>" +
+ Glib::Markup::escape_text(change.OldValue->get<std::string>()) +
+ "</b> to <b>" +
+ Glib::Markup::escape_text(change.NewValue->get<std::string>()) +
+ "</b>");
+ else if (change.Key == "auto_archive_duration")
+ extra_markup.push_back("Set auto archive duration to <b>"s + std::to_string(change.NewValue->get<int>()) + " minutes</b>"s);
+ else if (change.Key == "rate_limit_per_user" && change.NewValue.has_value()) {
+ const int secs = change.NewValue->get<int>();
+ if (secs == 0)
+ extra_markup.push_back("Disabled slowmode");
+ else
+ extra_markup.push_back("Set slowmode to <b>" +
+ std::to_string(secs) +
+ " seconds</b>");
+ } else if (change.Key == "locked")
+ extra_markup.push_back(change.NewValue->get<bool>() ? "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<bool>() ? "Archived the thread" : "Unarchived the thread");
+ }
+ } break;
+ case AuditLogActionType::THREAD_DELETE: {
+ markup = user_markup +
+ " deleted the thread <b>" + Glib::Markup::escape_text(*entry.GetOldFromKey<std::string>("name")) + "</b>";
+ } break;
+ default:
+ markup = "<i>Unknown action</i>";
+ break;
+ }
+
+ label->set_markup(markup);
+ expander->set_label_widget(*label);
+
+ if (entry.Reason.has_value()) {
+ extra_markup.push_back("With reason <b>" +
+ Glib::Markup::escape_text(*entry.Reason) +
+ "</b>");
+ }
+
+ 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 <gtkmm.h>
+#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<BanData> &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<Snowflake>(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<GdkEvent *>(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<Snowflake>(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 <gtkmm.h>
+#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<BanData> &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<Glib::ustring> m_col_user;
+ Gtk::TreeModelColumn<Glib::ustring> m_col_reason;
+ Gtk::TreeModelColumn<Snowflake> m_col_id;
+ };
+
+ ModelColumns m_columns;
+ Glib::RefPtr<Gtk::ListStore> 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<Gdk::PixbufAnimation> &pb) {
+ for (auto &row : m_model->children()) {
+ if (static_cast<Snowflake>(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<Gdk::Pixbuf> &pb) {
+ for (auto &row : m_model->children()) {
+ if (static_cast<Snowflake>(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<EmojiData> 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<Snowflake>(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<Glib::ustring>(selected_row[m_columns.m_col_name]);
+ const auto id = static_cast<Snowflake>(selected_row[m_columns.m_col_id]);
+ if (auto *window = dynamic_cast<Gtk::Window *>(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<Snowflake>(selected_row[m_columns.m_col_id]);
+ const bool is_animated = static_cast<Glib::ustring>(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<Snowflake>(selected_row[m_columns.m_col_id]);
+ const bool is_animated = static_cast<Glib::ustring>(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<GdkEvent *>(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 <gtkmm.h>
+#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<EmojiData> 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<Glib::RefPtr<Gdk::Pixbuf>> m_col_pixbuf;
+ Gtk::TreeModelColumn<Glib::RefPtr<Gdk::PixbufAnimation>> m_col_pixbuf_animation;
+ Gtk::TreeModelColumn<Glib::ustring> m_col_name;
+ Gtk::TreeModelColumn<Glib::ustring> m_col_creator;
+ Gtk::TreeModelColumn<Glib::ustring> m_col_animated;
+ Gtk::TreeModelColumn<Glib::ustring> m_col_available;
+ Gtk::TreeModelColumn<Snowflake> m_col_id;
+ };
+
+ ModelColumns m_columns;
+ Glib::RefPtr<Gtk::ListStore> m_model;
+ Glib::RefPtr<Gtk::TreeModelFilter> 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 <filesystem>
+
+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<Gdk::PixbufAnimation> &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<Gdk::Pixbuf> &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<uint8_t> &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<Gdk::Pixbuf> 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<uint8_t> 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<uint8_t> 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 <gtkmm.h>
+#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<uint8_t> &data, const std::string &mime);
+ void UpdateGuildIconFromPixbuf(Glib::RefPtr<Gdk::Pixbuf> 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<InviteData> &invite) {
+ if (!invite.has_value()) return;
+ AppendInvite(*invite);
+}
+
+void GuildSettingsInvitesPane::OnInvitesFetch(const std::vector<InviteData> &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<Glib::ustring>(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<GdkEvent *>(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 <gtkmm.h>
+#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<InviteData> &invite);
+ void OnInvitesFetch(const std::vector<InviteData> &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<Glib::ustring> m_col_code;
+ Gtk::TreeModelColumn<Glib::ustring> m_col_expires;
+ Gtk::TreeModelColumn<Glib::ustring> m_col_inviter;
+ Gtk::TreeModelColumn<Glib::ustring> m_col_temporary;
+ Gtk::TreeModelColumn<int> m_col_uses;
+ Gtk::TreeModelColumn<Glib::ustring> m_col_max_uses;
+ };
+
+ ModelColumns m_columns;
+ Glib::RefPtr<Gtk::ListStore> 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<GuildSettingsMembersListItem *>(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<GuildSettingsMembersListItem *>(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<GdkEvent *>(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("<span color='#" + IntToCSSColor(role.Color) + "'>" + user.GetEscapedBoldString<false>() + "</span>");
+ } else
+ m_name.set_markup(user.GetEscapedBoldString<false>());
+}
+
+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<GuildSettingsMembersPaneRolesItem *>(a);
+ auto *rowb = dynamic_cast<GuildSettingsMembersPaneRolesItem *>(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<Snowflake> &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<void, bool> 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("<span color='#" + IntToCSSColor(m_role.Color) + "'>" + Glib::Markup::escape_text(m_role.Name) + "</span>");
+ 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 <unordered_set>
+#include <gtkmm.h>
+#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<void, Snowflake, bool> 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<Snowflake> &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<sigc::connection> m_update_connection;
+
+ std::unordered_set<Snowflake> m_set_role_ids;
+
+ Snowflake GuildID;
+ Snowflake UserID;
+
+ Gtk::ListBox m_list;
+
+ std::unordered_map<Snowflake, GuildSettingsMembersPaneRolesItem *> 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<void, Snowflake> 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<Gtk::TargetEntry> 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<GuildSettingsRolesPaneRolesListItem *>(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<GuildSettingsRolesPaneRolesListItem *>(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<size_t>(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<GuildSettingsRolesPaneRolesListItem *>(rowa_);
+ auto *rowb = dynamic_cast<GuildSettingsRolesPaneRolesListItem *>(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<GuildSettingsRolesPaneRolesListItem *>(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("<span color='#" + IntToCSSColor(role.Color) + "'>" +
+ Glib::Markup::escape_text(role.Name) +
+ "</span>");
+ 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<Permission> &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<void, bool> 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 <gtkmm.h>
+#include <unordered_map>
+#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<void, Snowflake /* role_id */> type_signal_role_select;
+ type_signal_role_select m_signal_role_select;
+
+public:
+ std::unordered_map<Snowflake, GuildSettingsRolesPaneRolesListItem *> 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<void, Permission, bool> 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<sigc::connection> 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<Permission, GuildSettingsRolesPanePermItem *> 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;
+};