Restructure source and resource files (#46)

importantly, res is now res/res and css is now res/css
This commit is contained in:
Dylam De La Torre
2021-11-23 05:21:56 +01:00
committed by GitHub
parent d88079000a
commit a51a54bc59
216 changed files with 21 additions and 33 deletions

View File

@@ -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);
}
}

View File

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

View File

@@ -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);
}

View File

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

View File

@@ -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);
}

View File

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

View File

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

View File

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

View File

@@ -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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,76 @@
#include "guildsettingswindow.hpp"
#include "abaddon.hpp"
GuildSettingsWindow::GuildSettingsWindow(Snowflake id)
: m_main(Gtk::ORIENTATION_VERTICAL)
, m_pane_info(id)
, m_pane_members(id)
, m_pane_roles(id)
, m_pane_bans(id)
, m_pane_invites(id)
, m_pane_emojis(id)
, m_pane_audit_log(id)
, GuildID(id) {
auto &discord = Abaddon::Get().GetDiscordClient();
const auto guild = *discord.GetGuild(id);
auto guild_update_cb = [this](Snowflake id) {
if (id != GuildID) return;
const auto guild = *Abaddon::Get().GetDiscordClient().GetGuild(id);
set_title(guild.Name);
if (guild.HasIcon())
Abaddon::Get().GetImageManager().LoadFromURL(guild.GetIconURL(), sigc::mem_fun(*this, &GuildSettingsWindow::set_icon));
};
discord.signal_guild_update().connect(sigc::track_obj(guild_update_cb, *this));
set_name("guild-settings");
set_default_size(800, 600);
set_title(guild.Name);
set_position(Gtk::WIN_POS_CENTER);
get_style_context()->add_class("app-window");
get_style_context()->add_class("app-popup");
get_style_context()->add_class("guild-settings-window");
if (guild.HasIcon()) {
Abaddon::Get().GetImageManager().LoadFromURL(guild.GetIconURL(), sigc::mem_fun(*this, &GuildSettingsWindow::set_icon));
}
m_switcher.set_stack(m_stack);
m_switcher.set_halign(Gtk::ALIGN_CENTER);
m_switcher.set_hexpand(true);
m_switcher.set_margin_top(10);
m_switcher.show();
m_pane_info.show();
m_pane_members.show();
m_pane_roles.show();
m_pane_bans.show();
m_pane_invites.show();
m_pane_emojis.show();
m_pane_audit_log.show();
m_stack.set_transition_duration(100);
m_stack.set_transition_type(Gtk::STACK_TRANSITION_TYPE_CROSSFADE);
m_stack.set_margin_top(10);
m_stack.set_margin_bottom(10);
m_stack.set_margin_left(10);
m_stack.set_margin_right(10);
const auto self_id = discord.GetUserData().ID;
m_stack.add(m_pane_info, "info", "Info");
m_stack.add(m_pane_members, "members", "Members");
m_stack.add(m_pane_roles, "roles", "Roles");
m_stack.add(m_pane_bans, "bans", "Bans");
if (discord.HasGuildPermission(self_id, GuildID, Permission::MANAGE_GUILD))
m_stack.add(m_pane_invites, "invites", "Invites");
m_stack.add(m_pane_emojis, "emojis", "Emojis");
if (discord.HasGuildPermission(self_id, GuildID, Permission::VIEW_AUDIT_LOG))
m_stack.add(m_pane_audit_log, "audit-log", "Audit Log");
m_stack.show();
m_main.add(m_switcher);
m_main.add(m_stack);
m_main.show();
add(m_main);
}

View File

@@ -0,0 +1,30 @@
#pragma once
#include <gtkmm.h>
#include "discord/snowflake.hpp"
#include "guildsettings/infopane.hpp"
#include "guildsettings/banspane.hpp"
#include "guildsettings/invitespane.hpp"
#include "guildsettings/auditlogpane.hpp"
#include "guildsettings/memberspane.hpp"
#include "guildsettings/rolespane.hpp"
#include "guildsettings/emojispane.hpp"
class GuildSettingsWindow : public Gtk::Window {
public:
GuildSettingsWindow(Snowflake id);
private:
Gtk::Box m_main;
Gtk::Stack m_stack;
Gtk::StackSwitcher m_switcher;
GuildSettingsInfoPane m_pane_info;
GuildSettingsMembersPane m_pane_members;
GuildSettingsRolesPane m_pane_roles;
GuildSettingsBansPane m_pane_bans;
GuildSettingsInvitesPane m_pane_invites;
GuildSettingsEmojisPane m_pane_emojis;
GuildSettingsAuditLogPane m_pane_audit_log;
Snowflake GuildID;
};

308
src/windows/mainwindow.cpp Normal file
View File

@@ -0,0 +1,308 @@
#include "mainwindow.hpp"
#include "abaddon.hpp"
MainWindow::MainWindow()
: m_main_box(Gtk::ORIENTATION_VERTICAL)
, m_content_box(Gtk::ORIENTATION_HORIZONTAL)
, m_chan_content_paned(Gtk::ORIENTATION_HORIZONTAL)
, m_content_members_paned(Gtk::ORIENTATION_HORIZONTAL) {
set_default_size(1200, 800);
get_style_context()->add_class("app-window");
m_menu_discord.set_label("Discord");
m_menu_discord.set_submenu(m_menu_discord_sub);
m_menu_discord_connect.set_label("Connect");
m_menu_discord_connect.set_sensitive(false);
m_menu_discord_disconnect.set_label("Disconnect");
m_menu_discord_disconnect.set_sensitive(false);
m_menu_discord_set_token.set_label("Set Token");
m_menu_discord_join_guild.set_label("Accept Invite");
m_menu_discord_join_guild.set_sensitive(false);
m_menu_discord_set_status.set_label("Set Status");
m_menu_discord_set_status.set_sensitive(false);
m_menu_discord_add_recipient.set_label("Add user to DM");
m_menu_discord_sub.append(m_menu_discord_connect);
m_menu_discord_sub.append(m_menu_discord_disconnect);
m_menu_discord_sub.append(m_menu_discord_set_token);
m_menu_discord_sub.append(m_menu_discord_join_guild);
m_menu_discord_sub.append(m_menu_discord_set_status);
m_menu_discord_sub.append(m_menu_discord_add_recipient);
m_menu_discord_sub.signal_popped_up().connect(sigc::mem_fun(*this, &MainWindow::OnDiscordSubmenuPopup)); // this gets called twice for some reason
m_menu_discord.set_submenu(m_menu_discord_sub);
m_menu_file.set_label("File");
m_menu_file.set_submenu(m_menu_file_sub);
m_menu_file_reload_css.set_label("Reload CSS");
m_menu_file_clear_cache.set_label("Clear file cache");
m_menu_file_sub.append(m_menu_file_reload_css);
m_menu_file_sub.append(m_menu_file_clear_cache);
m_menu_view.set_label("View");
m_menu_view.set_submenu(m_menu_view_sub);
m_menu_view_friends.set_label("Friends");
m_menu_view_pins.set_label("Pins");
m_menu_view_threads.set_label("Threads");
m_menu_view_sub.append(m_menu_view_friends);
m_menu_view_sub.append(m_menu_view_pins);
m_menu_view_sub.append(m_menu_view_threads);
m_menu_view_sub.signal_popped_up().connect(sigc::mem_fun(*this, &MainWindow::OnViewSubmenuPopup));
m_menu_bar.append(m_menu_file);
m_menu_bar.append(m_menu_discord);
m_menu_bar.append(m_menu_view);
m_menu_bar.show_all();
m_menu_discord_connect.signal_activate().connect([this] {
m_signal_action_connect.emit();
});
m_menu_discord_disconnect.signal_activate().connect([this] {
m_signal_action_disconnect.emit();
});
m_menu_discord_set_token.signal_activate().connect([this] {
m_signal_action_set_token.emit();
});
m_menu_discord_join_guild.signal_activate().connect([this] {
m_signal_action_join_guild.emit();
});
m_menu_file_reload_css.signal_activate().connect([this] {
m_signal_action_reload_css.emit();
});
m_menu_discord_set_status.signal_activate().connect([this] {
m_signal_action_set_status.emit();
});
m_menu_file_clear_cache.signal_activate().connect([this] {
Abaddon::Get().GetImageManager().ClearCache();
});
m_menu_discord_add_recipient.signal_activate().connect([this] {
m_signal_action_add_recipient.emit(GetChatActiveChannel());
});
m_menu_view_friends.signal_activate().connect([this] {
UpdateChatActiveChannel(Snowflake::Invalid);
m_members.UpdateMemberList();
m_content_stack.set_visible_child("friends");
});
m_menu_view_pins.signal_activate().connect([this] {
m_signal_action_view_pins.emit(GetChatActiveChannel());
});
m_menu_view_threads.signal_activate().connect([this] {
m_signal_action_view_threads.emit(GetChatActiveChannel());
});
m_content_box.set_hexpand(true);
m_content_box.set_vexpand(true);
m_content_box.show();
m_main_box.add(m_menu_bar);
m_main_box.add(m_content_box);
m_main_box.show();
auto *member_list = m_members.GetRoot();
auto *chat = m_chat.GetRoot();
chat->set_vexpand(true);
chat->set_hexpand(true);
chat->show();
m_channel_list.set_vexpand(true);
m_channel_list.set_size_request(-1, -1);
m_channel_list.show();
member_list->set_vexpand(true);
member_list->show();
m_friends.set_vexpand(true);
m_friends.set_hexpand(true);
m_friends.show();
m_content_stack.add(*chat, "chat");
m_content_stack.add(m_friends, "friends");
m_content_stack.set_vexpand(true);
m_content_stack.set_hexpand(true);
m_content_stack.set_visible_child("chat");
m_content_stack.show();
m_chan_content_paned.pack1(m_channel_list);
m_chan_content_paned.pack2(m_content_members_paned);
m_chan_content_paned.child_property_shrink(m_channel_list) = false;
m_chan_content_paned.child_property_resize(m_channel_list) = false;
m_chan_content_paned.set_position(200);
m_chan_content_paned.show();
m_content_box.add(m_chan_content_paned);
m_content_members_paned.pack1(m_content_stack);
m_content_members_paned.pack2(*member_list);
m_content_members_paned.child_property_shrink(*member_list) = false;
m_content_members_paned.child_property_resize(*member_list) = false;
int w, h;
get_default_size(w, h); // :s
m_content_members_paned.set_position(w - m_chan_content_paned.get_position() - 150);
m_content_members_paned.show();
add(m_main_box);
}
void MainWindow::UpdateComponents() {
bool discord_active = Abaddon::Get().IsDiscordActive();
if (!discord_active) {
m_chat.Clear();
m_members.Clear();
} else {
m_members.UpdateMemberList();
}
UpdateChannelListing();
}
void MainWindow::UpdateMembers() {
m_members.UpdateMemberList();
}
void MainWindow::UpdateChannelListing() {
m_channel_list.UpdateListing();
}
void MainWindow::UpdateChatWindowContents() {
auto &discord = Abaddon::Get().GetDiscordClient();
auto msgs = discord.GetMessagesForChannel(m_chat.GetActiveChannel(), 50);
m_chat.SetMessages(msgs);
m_members.UpdateMemberList();
}
void MainWindow::UpdateChatActiveChannel(Snowflake id) {
m_chat.SetActiveChannel(id);
m_members.SetActiveChannel(id);
m_channel_list.SetActiveChannel(id);
m_content_stack.set_visible_child("chat");
}
Snowflake MainWindow::GetChatActiveChannel() const {
return m_chat.GetActiveChannel();
}
void MainWindow::UpdateChatNewMessage(const Message &data) {
if (data.ChannelID == GetChatActiveChannel()) {
m_chat.AddNewMessage(data);
}
}
void MainWindow::UpdateChatMessageDeleted(Snowflake id, Snowflake channel_id) {
if (channel_id == GetChatActiveChannel())
m_chat.DeleteMessage(id);
}
void MainWindow::UpdateChatMessageUpdated(Snowflake id, Snowflake channel_id) {
if (channel_id == GetChatActiveChannel())
m_chat.UpdateMessage(id);
}
void MainWindow::UpdateChatPrependHistory(const std::vector<Message> &msgs) {
m_chat.AddNewHistory(msgs); // given vector should be sorted ascending
}
void MainWindow::InsertChatInput(std::string text) {
m_chat.InsertChatInput(text);
}
Snowflake MainWindow::GetChatOldestListedMessage() {
return m_chat.GetOldestListedMessage();
}
void MainWindow::UpdateChatReactionAdd(Snowflake id, const Glib::ustring &param) {
m_chat.UpdateReactions(id);
}
void MainWindow::UpdateChatReactionRemove(Snowflake id, const Glib::ustring &param) {
m_chat.UpdateReactions(id);
}
void MainWindow::OnDiscordSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y) {
auto &discord = Abaddon::Get().GetDiscordClient();
auto channel_id = GetChatActiveChannel();
m_menu_discord_add_recipient.set_visible(false);
if (channel_id.IsValid()) {
auto channel = discord.GetChannel(channel_id);
if (channel.has_value() && channel->GetDMRecipients().size() + 1 < 10)
m_menu_discord_add_recipient.set_visible(channel->Type == ChannelType::GROUP_DM);
}
const bool discord_active = Abaddon::Get().GetDiscordClient().IsStarted();
std::string token = Abaddon::Get().GetDiscordToken();
m_menu_discord_connect.set_sensitive(token.size() > 0 && !discord_active);
m_menu_discord_disconnect.set_sensitive(discord_active);
m_menu_discord_join_guild.set_sensitive(discord_active);
m_menu_discord_set_token.set_sensitive(!discord_active);
m_menu_discord_set_status.set_sensitive(discord_active);
}
void MainWindow::OnViewSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y) {
m_menu_view_friends.set_sensitive(Abaddon::Get().GetDiscordClient().IsStarted());
auto channel_id = GetChatActiveChannel();
m_menu_view_pins.set_sensitive(false);
m_menu_view_threads.set_sensitive(false);
if (channel_id.IsValid()) {
auto channel = Abaddon::Get().GetDiscordClient().GetChannel(channel_id);
if (channel.has_value()) {
m_menu_view_threads.set_sensitive(channel->Type == ChannelType::GUILD_TEXT || channel->IsThread());
m_menu_view_pins.set_sensitive(channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::DM || channel->Type == ChannelType::GROUP_DM || channel->IsThread());
}
}
}
ChannelList *MainWindow::GetChannelList() {
return &m_channel_list;
}
ChatWindow *MainWindow::GetChatWindow() {
return &m_chat;
}
MemberList *MainWindow::GetMemberList() {
return &m_members;
}
MainWindow::type_signal_action_connect MainWindow::signal_action_connect() {
return m_signal_action_connect;
}
MainWindow::type_signal_action_disconnect MainWindow::signal_action_disconnect() {
return m_signal_action_disconnect;
}
MainWindow::type_signal_action_set_token MainWindow::signal_action_set_token() {
return m_signal_action_set_token;
}
MainWindow::type_signal_action_reload_css MainWindow::signal_action_reload_css() {
return m_signal_action_reload_css;
}
MainWindow::type_signal_action_join_guild MainWindow::signal_action_join_guild() {
return m_signal_action_join_guild;
}
MainWindow::type_signal_action_set_status MainWindow::signal_action_set_status() {
return m_signal_action_set_status;
}
MainWindow::type_signal_action_add_recipient MainWindow::signal_action_add_recipient() {
return m_signal_action_add_recipient;
}
MainWindow::type_signal_action_view_pins MainWindow::signal_action_view_pins() {
return m_signal_action_view_pins;
}
MainWindow::type_signal_action_view_threads MainWindow::signal_action_view_threads() {
return m_signal_action_view_threads;
}

View File

@@ -0,0 +1,99 @@
#pragma once
#include "components/channels.hpp"
#include "components/chatwindow.hpp"
#include "components/memberlist.hpp"
#include "components/friendslist.hpp"
#include <gtkmm.h>
class MainWindow : public Gtk::Window {
public:
MainWindow();
void UpdateComponents();
void UpdateMembers();
void UpdateChannelListing();
void UpdateChatWindowContents();
void UpdateChatActiveChannel(Snowflake id);
Snowflake GetChatActiveChannel() const;
void UpdateChatNewMessage(const Message &data);
void UpdateChatMessageDeleted(Snowflake id, Snowflake channel_id);
void UpdateChatMessageUpdated(Snowflake id, Snowflake channel_id);
void UpdateChatPrependHistory(const std::vector<Message> &msgs);
void InsertChatInput(std::string text);
Snowflake GetChatOldestListedMessage();
void UpdateChatReactionAdd(Snowflake id, const Glib::ustring &param);
void UpdateChatReactionRemove(Snowflake id, const Glib::ustring &param);
ChannelList *GetChannelList();
ChatWindow *GetChatWindow();
MemberList *GetMemberList();
public:
typedef sigc::signal<void> type_signal_action_connect;
typedef sigc::signal<void> type_signal_action_disconnect;
typedef sigc::signal<void> type_signal_action_set_token;
typedef sigc::signal<void> type_signal_action_reload_css;
typedef sigc::signal<void> type_signal_action_join_guild;
typedef sigc::signal<void> type_signal_action_set_status;
// this should probably be removed
typedef sigc::signal<void, Snowflake> type_signal_action_add_recipient; // channel id
typedef sigc::signal<void, Snowflake> type_signal_action_view_pins; // channel id
typedef sigc::signal<void, Snowflake> type_signal_action_view_threads; // channel id
type_signal_action_connect signal_action_connect();
type_signal_action_disconnect signal_action_disconnect();
type_signal_action_set_token signal_action_set_token();
type_signal_action_reload_css signal_action_reload_css();
type_signal_action_join_guild signal_action_join_guild();
type_signal_action_set_status signal_action_set_status();
type_signal_action_add_recipient signal_action_add_recipient();
type_signal_action_view_pins signal_action_view_pins();
type_signal_action_view_threads signal_action_view_threads();
protected:
type_signal_action_connect m_signal_action_connect;
type_signal_action_disconnect m_signal_action_disconnect;
type_signal_action_set_token m_signal_action_set_token;
type_signal_action_reload_css m_signal_action_reload_css;
type_signal_action_join_guild m_signal_action_join_guild;
type_signal_action_set_status m_signal_action_set_status;
type_signal_action_add_recipient m_signal_action_add_recipient;
type_signal_action_view_pins m_signal_action_view_pins;
type_signal_action_view_threads m_signal_action_view_threads;
protected:
Gtk::Box m_main_box;
Gtk::Box m_content_box;
Gtk::Paned m_chan_content_paned;
Gtk::Paned m_content_members_paned;
ChannelList m_channel_list;
ChatWindow m_chat;
MemberList m_members;
FriendsList m_friends;
Gtk::Stack m_content_stack;
Gtk::MenuBar m_menu_bar;
Gtk::MenuItem m_menu_discord;
Gtk::Menu m_menu_discord_sub;
Gtk::MenuItem m_menu_discord_connect;
Gtk::MenuItem m_menu_discord_disconnect;
Gtk::MenuItem m_menu_discord_set_token;
Gtk::MenuItem m_menu_discord_join_guild;
Gtk::MenuItem m_menu_discord_set_status;
Gtk::MenuItem m_menu_discord_add_recipient; // move me somewhere else some day
void OnDiscordSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y);
Gtk::MenuItem m_menu_file;
Gtk::Menu m_menu_file_sub;
Gtk::MenuItem m_menu_file_reload_css;
Gtk::MenuItem m_menu_file_clear_cache;
Gtk::MenuItem m_menu_view;
Gtk::Menu m_menu_view_sub;
Gtk::MenuItem m_menu_view_friends;
Gtk::MenuItem m_menu_view_pins;
Gtk::MenuItem m_menu_view_threads;
void OnViewSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y);
};

View File

@@ -0,0 +1,47 @@
#include "pinnedwindow.hpp"
#include "abaddon.hpp"
PinnedWindow::PinnedWindow(const ChannelData &data)
: ChannelID(data.ID) {
if (data.GuildID.has_value())
GuildID = *data.GuildID;
set_name("pinned-messages");
set_default_size(450, 375);
if (data.Name.has_value())
set_title("#" + *data.Name + " - Pinned Messages");
else
set_title("Pinned Messages");
set_position(Gtk::WIN_POS_CENTER);
get_style_context()->add_class("app-window");
get_style_context()->add_class("app-popup");
get_style_context()->add_class("pinned-messages-window");
add(m_chat);
m_chat.show();
m_chat.SetSeparateAll(true);
m_chat.SetActiveChannel(ChannelID);
m_chat.SetUsePinnedMenu();
Abaddon::Get().GetDiscordClient().signal_message_pinned().connect(sigc::mem_fun(*this, &PinnedWindow::OnMessagePinned));
Abaddon::Get().GetDiscordClient().signal_message_unpinned().connect(sigc::mem_fun(*this, &PinnedWindow::OnMessageUnpinned));
FetchPinned();
}
void PinnedWindow::OnMessagePinned(const Message &msg) {
FetchPinned();
}
void PinnedWindow::OnMessageUnpinned(const Message &msg) {
m_chat.ActuallyRemoveMessage(msg.ID);
}
void PinnedWindow::FetchPinned() {
Abaddon::Get().GetDiscordClient().FetchPinned(ChannelID, sigc::mem_fun(*this, &PinnedWindow::OnFetchedPinned));
}
void PinnedWindow::OnFetchedPinned(const std::vector<Message> &msgs, DiscordError code) {
if (code != DiscordError::NONE) return;
m_chat.SetMessages(msgs.begin(), msgs.end());
}

View File

@@ -0,0 +1,22 @@
#pragma once
#include <gtkmm.h>
#include "discord/errors.hpp"
#include "discord/channel.hpp"
#include "discord/message.hpp"
#include "components/chatlist.hpp"
class PinnedWindow : public Gtk::Window {
public:
PinnedWindow(const ChannelData &data);
Snowflake GuildID;
Snowflake ChannelID;
private:
void OnMessagePinned(const Message &msg);
void OnMessageUnpinned(const Message &msg);
void FetchPinned();
void OnFetchedPinned(const std::vector<Message> &msgs, DiscordError code);
ChatList m_chat;
};

View File

@@ -0,0 +1,58 @@
#include "mutualfriendspane.hpp"
#include "abaddon.hpp"
MutualFriendItem::MutualFriendItem(const UserData &user)
: Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) {
get_style_context()->add_class("mutual-friend-item");
m_name.get_style_context()->add_class("mutual-friend-item-name");
m_avatar.get_style_context()->add_class("mutual-friend-item-avatar");
m_avatar.set_margin_end(10);
const auto show_animations = Abaddon::Get().GetSettings().GetShowAnimations();
auto &img = Abaddon::Get().GetImageManager();
m_avatar.property_pixbuf() = img.GetPlaceholder(24);
if (user.HasAnimatedAvatar() && show_animations) {
auto cb = [this](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) {
m_avatar.property_pixbuf_animation() = pb;
};
img.LoadAnimationFromURL(user.GetAvatarURL("gif", "32"), 24, 24, sigc::track_obj(cb, *this));
} else {
auto cb = [this](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
m_avatar.property_pixbuf() = pb->scale_simple(24, 24, Gdk::INTERP_BILINEAR);
};
img.LoadFromURL(user.GetAvatarURL("png", "32"), sigc::track_obj(cb, *this));
}
m_name.set_markup(user.GetEscapedBoldString<false>());
m_name.set_valign(Gtk::ALIGN_CENTER);
add(m_avatar);
add(m_name);
show_all_children();
}
MutualFriendsPane::MutualFriendsPane(Snowflake id)
: UserID(id) {
signal_map().connect(sigc::mem_fun(*this, &MutualFriendsPane::OnMap));
add(m_list);
show_all_children();
}
void MutualFriendsPane::OnFetchRelationships(const std::vector<UserData> &users) {
for (auto child : m_list.get_children())
delete child;
for (const auto &user : users) {
auto *item = Gtk::manage(new MutualFriendItem(user));
item->show();
m_list.add(*item);
}
}
void MutualFriendsPane::OnMap() {
if (m_requested) return;
m_requested = true;
Abaddon::Get().GetDiscordClient().FetchUserRelationships(UserID, sigc::mem_fun(*this, &MutualFriendsPane::OnFetchRelationships));
}

View File

@@ -0,0 +1,28 @@
#pragma once
#include <gtkmm.h>
#include "discord/objects.hpp"
class MutualFriendItem : public Gtk::Box {
public:
MutualFriendItem(const UserData &user);
private:
Gtk::Image m_avatar;
Gtk::Label m_name;
};
class MutualFriendsPane : public Gtk::ScrolledWindow {
public:
MutualFriendsPane(Snowflake id);
Snowflake UserID;
private:
void OnMap();
bool m_requested = false;
void OnFetchRelationships(const std::vector<UserData> &users);
Gtk::ListBox m_list;
};

View File

@@ -0,0 +1,73 @@
#include "mutualguildspane.hpp"
#include "abaddon.hpp"
MutualGuildItem::MutualGuildItem(const MutualGuildData &guild)
: Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)
, m_box(Gtk::ORIENTATION_VERTICAL) {
get_style_context()->add_class("mutual-guild-item");
m_name.get_style_context()->add_class("mutual-guild-item-name");
m_icon.get_style_context()->add_class("mutual-guild-item-icon");
m_icon.set_margin_end(10);
// discord will return info (id + nick) for "deleted" guilds from this endpoint. strange !
const auto data = Abaddon::Get().GetDiscordClient().GetGuild(guild.ID);
if (data.has_value()) {
const auto show_animations = Abaddon::Get().GetSettings().GetShowAnimations();
auto &img = Abaddon::Get().GetImageManager();
m_icon.property_pixbuf() = img.GetPlaceholder(24);
if (data->HasIcon()) {
if (data->HasAnimatedIcon() && show_animations) {
auto cb = [this](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) {
m_icon.property_pixbuf_animation() = pb;
};
img.LoadAnimationFromURL(data->GetIconURL("gif", "32"), 24, 24, sigc::track_obj(cb, *this));
} else {
auto cb = [this](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
m_icon.property_pixbuf() = pb->scale_simple(24, 24, Gdk::INTERP_BILINEAR);
};
img.LoadFromURL(data->GetIconURL("png", "32"), sigc::track_obj(cb, *this));
}
}
m_name.set_markup("<b>" + Glib::Markup::escape_text(data->Name) + "</b>");
} else {
m_icon.property_pixbuf() = Abaddon::Get().GetImageManager().GetPlaceholder(24);
m_name.set_markup("<b>Unknown server</b>");
}
if (guild.Nick.has_value()) {
m_nick = Gtk::manage(new Gtk::Label(*guild.Nick));
m_nick->get_style_context()->add_class("mutual-guild-item-nick");
m_nick->set_margin_start(5);
m_nick->set_halign(Gtk::ALIGN_START);
m_nick->set_single_line_mode(true);
m_nick->set_ellipsize(Pango::ELLIPSIZE_END);
}
m_box.set_valign(Gtk::ALIGN_CENTER);
m_box.add(m_name);
if (m_nick != nullptr)
m_box.add(*m_nick);
add(m_icon);
add(m_box);
show_all_children();
}
UserMutualGuildsPane::UserMutualGuildsPane(Snowflake id)
: UserID(id) {
add(m_list);
show_all_children();
}
void UserMutualGuildsPane::SetMutualGuilds(const std::vector<MutualGuildData> &guilds) {
for (auto child : m_list.get_children())
delete child;
for (const auto &guild : guilds) {
auto *item = Gtk::manage(new MutualGuildItem(guild));
item->show();
m_list.add(*item);
}
}

View File

@@ -0,0 +1,26 @@
#pragma once
#include <gtkmm.h>
#include "discord/objects.hpp"
class MutualGuildItem : public Gtk::Box {
public:
MutualGuildItem(const MutualGuildData &guild);
private:
Gtk::Image m_icon;
Gtk::Box m_box;
Gtk::Label m_name;
Gtk::Label *m_nick = nullptr;
};
class UserMutualGuildsPane : public Gtk::ScrolledWindow {
public:
UserMutualGuildsPane(Snowflake id);
void SetMutualGuilds(const std::vector<MutualGuildData> &guilds);
Snowflake UserID;
private:
Gtk::ListBox m_list;
};

View File

@@ -0,0 +1,236 @@
#include "userinfopane.hpp"
#include <unordered_set>
#include "abaddon.hpp"
ConnectionItem::ConnectionItem(const ConnectionData &conn)
: m_box(Gtk::ORIENTATION_HORIZONTAL)
, m_name(conn.Name) {
Glib::RefPtr<Gdk::Pixbuf> pixbuf;
try {
pixbuf = Gdk::Pixbuf::create_from_file(Abaddon::GetResPath("/" + conn.Type + ".png"), 32, 32);
} catch (const Glib::Exception &e) {}
std::string url;
if (conn.Type == "github")
url = "https://github.com/" + conn.Name;
else if (conn.Type == "steam")
url = "https://steamcommunity.com/profiles/" + conn.ID;
else if (conn.Type == "twitch")
url = "https://twitch.tv/" + conn.Name;
else if (conn.Type == "twitter")
url = "https://twitter.com/i/user/" + conn.ID;
else if (conn.Type == "spotify")
url = "https://open.spotify.com/user/" + conn.ID;
else if (conn.Type == "reddit")
url = "https://reddit.com/u/" + conn.Name;
else if (conn.Type == "youtube")
url = "https://www.youtube.com/channel/" + conn.ID;
else if (conn.Type == "facebook")
url = "https://www.facebook.com/" + conn.ID;
if (pixbuf) {
m_image = Gtk::manage(new Gtk::Image(pixbuf));
m_image->get_style_context()->add_class("profile-connection-image");
m_box.add(*m_image);
}
m_box.set_halign(Gtk::ALIGN_START);
m_box.set_size_request(200, -1);
m_box.get_style_context()->add_class("profile-connection");
m_name.get_style_context()->add_class("profile-connection-label");
m_name.set_valign(Gtk::ALIGN_CENTER);
m_name.set_single_line_mode(true);
m_name.set_ellipsize(Pango::ELLIPSIZE_END);
m_box.add(m_name);
if (url != "") {
auto cb = [this, url](GdkEventButton *event) -> bool {
if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) {
LaunchBrowser(url);
return true;
}
return false;
};
signal_button_press_event().connect(sigc::track_obj(cb, *this));
AddPointerCursor(*this);
}
m_overlay.add(m_box);
if (conn.IsVerified) {
try {
const static auto checkmarks_path = Abaddon::GetResPath("/checkmark.png");
static auto pb = Gdk::Pixbuf::create_from_file(checkmarks_path, 24, 24);
m_check = Gtk::manage(new Gtk::Image(pb));
m_check->get_style_context()->add_class("profile-connection-check");
m_check->set_margin_end(25);
m_check->set_valign(Gtk::ALIGN_CENTER);
m_check->set_halign(Gtk::ALIGN_END);
m_check->show();
m_overlay.add_overlay(*m_check);
} catch (const Glib::Exception &e) {}
}
m_overlay.set_hexpand(false);
m_overlay.set_halign(Gtk::ALIGN_START);
add(m_overlay);
show_all_children();
}
ConnectionsContainer::ConnectionsContainer() {
get_style_context()->add_class("profile-connections");
set_column_homogeneous(true);
set_row_spacing(10);
set_column_spacing(10);
show_all_children();
}
void ConnectionsContainer::SetConnections(const std::vector<ConnectionData> &connections) {
for (auto child : get_children())
delete child;
static const std::unordered_set<std::string> supported_services = {
"battlenet",
"github",
"leagueoflegends",
"reddit",
"skype",
"spotify",
"steam",
"twitch",
"twitter",
"xbox",
"youtube",
"facebook"
};
for (size_t i = 0; i < connections.size(); i++) {
const auto &conn = connections[i];
if (supported_services.find(conn.Type) == supported_services.end()) continue;
auto widget = Gtk::manage(new ConnectionItem(conn));
widget->show();
attach(*widget, i % 2, i / 2, 1, 1);
}
set_halign(Gtk::ALIGN_FILL);
set_hexpand(true);
}
NotesContainer::NotesContainer()
: Gtk::Box(Gtk::ORIENTATION_VERTICAL) {
get_style_context()->add_class("profile-notes");
m_label.get_style_context()->add_class("profile-notes-label");
m_note.get_style_context()->add_class("profile-notes-text");
m_label.set_markup("<b>NOTE</b>");
m_label.set_halign(Gtk::ALIGN_START);
m_note.set_wrap_mode(Gtk::WRAP_WORD_CHAR);
m_note.signal_key_press_event().connect(sigc::mem_fun(*this, &NotesContainer::OnNoteKeyPress), false);
add(m_label);
add(m_note);
show_all_children();
}
void NotesContainer::SetNote(const std::string &note) {
m_note.get_buffer()->set_text(note);
}
void NotesContainer::UpdateNote() {
auto text = m_note.get_buffer()->get_text();
if (text.size() > 256)
text = text.substr(0, 256);
m_signal_update_note.emit(text);
}
bool NotesContainer::OnNoteKeyPress(GdkEventKey *event) {
if (event->type != GDK_KEY_PRESS) return false;
const auto text = m_note.get_buffer()->get_text();
if (event->keyval == GDK_KEY_Return) {
if (event->state & GDK_SHIFT_MASK) {
int newlines = 0;
for (const auto c : text)
if (c == '\n') newlines++;
return newlines >= 5;
} else {
UpdateNote();
return true;
}
}
return false;
}
NotesContainer::type_signal_update_note NotesContainer::signal_update_note() {
return m_signal_update_note;
}
BioContainer::BioContainer()
: Gtk::Box(Gtk::ORIENTATION_VERTICAL) {
m_label.set_markup("<b>ABOUT ME</b>");
m_label.set_halign(Gtk::ALIGN_START);
m_bio.set_halign(Gtk::ALIGN_START);
m_bio.set_line_wrap(true);
m_bio.set_line_wrap_mode(Pango::WRAP_WORD_CHAR);
m_label.show();
m_bio.show();
add(m_label);
add(m_bio);
}
void BioContainer::SetBio(const std::string &bio) {
m_bio.set_text(bio);
}
ProfileUserInfoPane::ProfileUserInfoPane(Snowflake ID)
: Gtk::Box(Gtk::ORIENTATION_VERTICAL)
, UserID(ID) {
get_style_context()->add_class("profile-info-pane");
m_created.get_style_context()->add_class("profile-info-created");
m_note.signal_update_note().connect([this](const Glib::ustring &note) {
auto cb = [this](DiscordError code) {
if (code != DiscordError::NONE) {
Gtk::MessageDialog dlg("Failed to set note", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
dlg.run();
}
};
Abaddon::Get().GetDiscordClient().SetUserNote(UserID, note, sigc::track_obj(cb, *this));
});
auto &discord = Abaddon::Get().GetDiscordClient();
auto note_update_cb = [this](Snowflake id, std::string note) {
if (id == UserID)
m_note.SetNote(note);
};
discord.signal_note_update().connect(sigc::track_obj(note_update_cb, m_note));
auto fetch_note_cb = [this](const std::string &note) {
m_note.SetNote(note);
};
discord.FetchUserNote(UserID, sigc::track_obj(fetch_note_cb, *this));
m_created.set_halign(Gtk::ALIGN_START);
m_created.set_margin_top(5);
m_created.set_text("Account created: " + ID.GetLocalTimestamp());
m_conns.set_halign(Gtk::ALIGN_START);
m_conns.set_hexpand(true);
m_created.show();
m_note.show();
m_conns.show();
add(m_created);
add(m_bio);
add(m_note);
add(m_conns);
}
void ProfileUserInfoPane::SetProfile(const UserProfileData &data) {
if (data.User.Bio.has_value() && *data.User.Bio != "") {
m_bio.SetBio(*data.User.Bio);
m_bio.show();
} else {
m_bio.hide();
}
m_conns.SetConnections(data.ConnectedAccounts);
}

View File

@@ -0,0 +1,65 @@
#pragma once
#include <gtkmm.h>
#include "discord/objects.hpp"
class ConnectionItem : public Gtk::EventBox {
public:
ConnectionItem(const ConnectionData &connection);
private:
Gtk::Overlay m_overlay;
Gtk::Box m_box;
Gtk::Label m_name;
Gtk::Image *m_image = nullptr;
Gtk::Image *m_check = nullptr;
};
class ConnectionsContainer : public Gtk::Grid {
public:
ConnectionsContainer();
void SetConnections(const std::vector<ConnectionData> &connections);
};
class NotesContainer : public Gtk::Box {
public:
NotesContainer();
void SetNote(const std::string &note);
private:
void UpdateNote();
bool OnNoteKeyPress(GdkEventKey *event);
Gtk::Label m_label;
Gtk::TextView m_note;
typedef sigc::signal<void, Glib::ustring> type_signal_update_note;
type_signal_update_note m_signal_update_note;
public:
type_signal_update_note signal_update_note();
};
class BioContainer : public Gtk::Box {
public:
BioContainer();
void SetBio(const std::string &bio);
private:
Gtk::Label m_label;
Gtk::Label m_bio;
};
class ProfileUserInfoPane : public Gtk::Box {
public:
ProfileUserInfoPane(Snowflake ID);
void SetProfile(const UserProfileData &data);
Snowflake UserID;
private:
Gtk::Label m_created;
BioContainer m_bio;
NotesContainer m_note;
ConnectionsContainer m_conns;
};

View File

@@ -0,0 +1,127 @@
#include "profilewindow.hpp"
#include "abaddon.hpp"
ProfileWindow::ProfileWindow(Snowflake user_id)
: ID(user_id)
, m_main(Gtk::ORIENTATION_VERTICAL)
, m_upper(Gtk::ORIENTATION_HORIZONTAL)
, m_badges(Gtk::ORIENTATION_HORIZONTAL)
, m_pane_info(user_id)
, m_pane_guilds(user_id)
, m_pane_friends(user_id) {
auto &discord = Abaddon::Get().GetDiscordClient();
auto user = *discord.GetUser(ID);
discord.FetchUserProfile(user_id, sigc::mem_fun(*this, &ProfileWindow::OnFetchProfile));
set_name("user-profile");
set_default_size(450, 375);
set_title(user.Username + "#" + user.Discriminator);
set_position(Gtk::WIN_POS_CENTER);
get_style_context()->add_class("app-window");
get_style_context()->add_class("app-popup");
get_style_context()->add_class("user-profile-window");
m_main.get_style_context()->add_class("profile-main-container");
m_avatar.get_style_context()->add_class("profile-avatar");
m_username.get_style_context()->add_class("profile-username");
m_switcher.get_style_context()->add_class("profile-switcher");
m_stack.get_style_context()->add_class("profile-stack");
m_badges.get_style_context()->add_class("profile-badges");
m_scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
m_scroll.set_vexpand(true);
m_scroll.set_propagate_natural_height(true);
if (user.HasAvatar())
AddPointerCursor(m_avatar_ev);
m_avatar_ev.signal_button_press_event().connect([this, user](GdkEventButton *event) -> bool {
if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) {
if (user.HasAnimatedAvatar())
LaunchBrowser(user.GetAvatarURL("gif", "512"));
else
LaunchBrowser(user.GetAvatarURL("png", "512"));
}
return false;
});
static const bool show_animations = Abaddon::Get().GetSettings().GetShowAnimations();
auto &img = Abaddon::Get().GetImageManager();
m_avatar.property_pixbuf() = img.GetPlaceholder(64);
auto icon_cb = [this](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
set_icon(pb);
};
img.LoadFromURL(user.GetAvatarURL("png", "64"), sigc::track_obj(icon_cb, *this));
if (show_animations && user.HasAnimatedAvatar()) {
auto cb = [this](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) {
m_avatar.property_pixbuf_animation() = pb;
};
img.LoadAnimationFromURL(user.GetAvatarURL("gif", "64"), 64, 64, sigc::track_obj(cb, *this));
} else {
auto cb = [this](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
m_avatar.property_pixbuf() = pb->scale_simple(64, 64, Gdk::INTERP_BILINEAR);
};
img.LoadFromURL(user.GetAvatarURL("png", "64"), sigc::track_obj(cb, *this));
}
m_username.set_markup(user.GetEscapedString());
m_switcher.set_stack(m_stack);
m_switcher.set_halign(Gtk::ALIGN_START);
m_switcher.set_hexpand(true);
m_stack.add(m_pane_info, "info", "User Info");
m_stack.add(m_pane_guilds, "guilds", "Mutual Servers");
m_stack.add(m_pane_friends, "friends", "Mutual Friends");
m_badges.set_valign(Gtk::ALIGN_CENTER);
m_badges_scroll.set_hexpand(true);
m_badges_scroll.set_propagate_natural_width(true);
m_badges_scroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_NEVER);
m_upper.set_halign(Gtk::ALIGN_START);
m_avatar.set_halign(Gtk::ALIGN_START);
m_username.set_halign(Gtk::ALIGN_START);
m_avatar_ev.add(m_avatar);
m_upper.add(m_avatar_ev);
m_upper.add(m_username);
m_badges_scroll.add(m_badges);
m_upper.add(m_badges_scroll);
m_main.add(m_upper);
m_main.add(m_switcher);
m_scroll.add(m_stack);
m_main.add(m_scroll);
add(m_main);
show_all_children();
}
void ProfileWindow::OnFetchProfile(const UserProfileData &data) {
m_pane_info.SetProfile(data);
m_pane_guilds.SetMutualGuilds(data.MutualGuilds);
for (auto child : m_badges.get_children())
delete child;
if (!data.User.PublicFlags.has_value()) return;
const auto x = *data.User.PublicFlags;
for (uint64_t i = 1; i <= UserData::MaxFlag; i <<= 1) {
if (!(x & i)) continue;
const std::string name = UserData::GetFlagName(i);
if (name == "unknown") continue;
Glib::RefPtr<Gdk::Pixbuf> pixbuf;
try {
if (name == "verifiedbot")
pixbuf = Gdk::Pixbuf::create_from_file(Abaddon::GetResPath("/checkmark.png"), 24, 24);
else
pixbuf = Gdk::Pixbuf::create_from_file(Abaddon::GetResPath("/" + name + ".png"), 24, 24);
} catch (const Glib::Exception &e) {
pixbuf = Abaddon::Get().GetImageManager().GetPlaceholder(24);
}
if (!pixbuf) continue;
auto *image = Gtk::manage(new Gtk::Image(pixbuf));
image->get_style_context()->add_class("profile-badge");
image->set_tooltip_text(UserData::GetFlagReadableName(i));
image->show();
m_badges.add(*image);
}
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include <gtkmm.h>
#include "discord/snowflake.hpp"
#include "profile/userinfopane.hpp"
#include "profile/mutualguildspane.hpp"
#include "profile/mutualfriendspane.hpp"
class ProfileWindow : public Gtk::Window {
public:
ProfileWindow(Snowflake user_id);
Snowflake ID;
private:
void OnFetchProfile(const UserProfileData &data);
Gtk::Box m_main;
Gtk::Box m_upper;
Gtk::Box m_badges;
Gtk::ScrolledWindow m_badges_scroll;
Gtk::EventBox m_avatar_ev;
Gtk::Image m_avatar;
Gtk::Label m_username;
Gtk::ScrolledWindow m_scroll;
Gtk::Stack m_stack;
Gtk::StackSwitcher m_switcher;
ProfileUserInfoPane m_pane_info;
UserMutualGuildsPane m_pane_guilds;
MutualFriendsPane m_pane_friends;
};

View File

@@ -0,0 +1,149 @@
#include "threadswindow.hpp"
#include "abaddon.hpp"
ThreadsWindow::ThreadsWindow(const ChannelData &channel)
: m_channel_id(channel.ID)
, m_filter_public(m_group, "Public")
, m_filter_private(m_group, "Private")
, m_box(Gtk::ORIENTATION_VERTICAL)
, m_active(channel, sigc::mem_fun(*this, &ThreadsWindow::ListFilterFunc))
, m_archived(channel, sigc::mem_fun(*this, &ThreadsWindow::ListFilterFunc)) {
set_name("threads-window");
set_default_size(450, 375);
set_title("#" + *channel.Name + " - Threads");
set_position(Gtk::WIN_POS_CENTER);
get_style_context()->add_class("app-window");
get_style_context()->add_class("app-popup");
get_style_context()->add_class("threads-window");
const auto cb = [this](Snowflake id) {
Abaddon::Get().ActionChannelOpened(id);
hide();
};
m_active.signal_thread_open().connect(cb);
m_archived.signal_thread_open().connect(cb);
m_switcher.set_halign(Gtk::ALIGN_CENTER);
m_switcher.set_stack(m_stack);
m_stack.add(m_active, "active", "Active Threads");
m_stack.add(m_archived, "archived", "Archived Threads");
m_filter_buttons.set_homogeneous(true);
m_filter_buttons.set_halign(Gtk::ALIGN_CENTER);
m_filter_buttons.add(m_filter_public);
m_filter_buttons.add(m_filter_private);
// a little strange
const auto btncb = [this](Gtk::RadioButton *btn) {
if (btn->get_active()) {
if (btn == &m_filter_public)
m_filter_mode = FILTER_PUBLIC;
else if (btn == &m_filter_private)
m_filter_mode = FILTER_PRIVATE;
m_active.InvalidateFilter();
m_archived.InvalidateFilter();
}
};
m_filter_public.signal_toggled().connect(sigc::bind(btncb, &m_filter_public));
m_filter_private.signal_toggled().connect(sigc::bind(btncb, &m_filter_private));
m_active.show();
m_archived.show();
m_switcher.show();
m_filter_buttons.show_all();
m_stack.show();
m_box.show();
m_box.add(m_switcher);
m_box.add(m_filter_buttons);
m_box.add(m_stack);
add(m_box);
}
bool ThreadsWindow::ListFilterFunc(Gtk::ListBoxRow *row_) {
if (auto *row = dynamic_cast<ThreadListRow *>(row_))
return (m_filter_mode == FILTER_PUBLIC && (row->Type == ChannelType::GUILD_PUBLIC_THREAD || row->Type == ChannelType::GUILD_NEWS_THREAD)) ||
(m_filter_mode == FILTER_PRIVATE && row->Type == ChannelType::GUILD_PRIVATE_THREAD);
return false;
}
ThreadListRow::ThreadListRow(const ChannelData &channel)
: ID(channel.ID)
, Type(channel.Type)
, m_label(*channel.Name, Gtk::ALIGN_START) {
m_label.show();
add(m_label);
}
ActiveThreadsList::ActiveThreadsList(const ChannelData &channel, const Gtk::ListBox::SlotFilter &filter) {
set_vexpand(true);
m_list.set_filter_func(filter);
m_list.set_selection_mode(Gtk::SELECTION_SINGLE);
m_list.set_hexpand(true);
m_list.show();
add(m_list);
m_list.signal_button_press_event().connect([this](GdkEventButton *ev) -> bool {
if (ev->button == GDK_BUTTON_PRIMARY && ev->type == GDK_2BUTTON_PRESS) {
if (auto row = dynamic_cast<ThreadListRow *>(m_list.get_selected_row()))
m_signal_thread_open.emit(row->ID);
}
return false;
});
const auto threads = Abaddon::Get().GetDiscordClient().GetActiveThreads(channel.ID);
for (const auto &thread : threads) {
auto row = Gtk::manage(new ThreadListRow(thread));
row->show();
m_list.add(*row);
}
}
void ActiveThreadsList::InvalidateFilter() {
m_list.invalidate_filter();
}
ActiveThreadsList::type_signal_thread_open ActiveThreadsList::signal_thread_open() {
return m_signal_thread_open;
}
ArchivedThreadsList::ArchivedThreadsList(const ChannelData &channel, const Gtk::ListBox::SlotFilter &filter) {
set_vexpand(true);
m_list.set_filter_func(filter);
m_list.set_selection_mode(Gtk::SELECTION_SINGLE);
m_list.set_hexpand(true);
m_list.show();
add(m_list);
m_list.signal_button_press_event().connect([this](GdkEventButton *ev) -> bool {
if (ev->button == GDK_BUTTON_PRIMARY && ev->type == GDK_2BUTTON_PRESS) {
if (auto row = dynamic_cast<ThreadListRow *>(m_list.get_selected_row()))
m_signal_thread_open.emit(row->ID);
}
return false;
});
Abaddon::Get().GetDiscordClient().GetArchivedPublicThreads(channel.ID, sigc::mem_fun(*this, &ArchivedThreadsList::OnPublicFetched));
}
void ArchivedThreadsList::InvalidateFilter() {
m_list.invalidate_filter();
}
void ArchivedThreadsList::OnPublicFetched(DiscordError code, const ArchivedThreadsResponseData &data) {
for (const auto &thread : data.Threads) {
auto row = Gtk::manage(new ThreadListRow(thread));
row->show();
m_list.add(*row);
}
}
ArchivedThreadsList::type_signal_thread_open ArchivedThreadsList::signal_thread_open() {
return m_signal_thread_open;
}

View File

@@ -0,0 +1,79 @@
#pragma once
#include <gtkmm.h>
#include "discord/objects.hpp"
class ActiveThreadsList : public Gtk::ScrolledWindow {
public:
ActiveThreadsList(const ChannelData &channel, const Gtk::ListBox::SlotFilter &filter);
void InvalidateFilter();
private:
Gtk::ListBox m_list;
using type_signal_thread_open = sigc::signal<void, Snowflake>;
type_signal_thread_open m_signal_thread_open;
public:
type_signal_thread_open signal_thread_open();
};
class ArchivedThreadsList : public Gtk::ScrolledWindow {
public:
ArchivedThreadsList(const ChannelData &channel, const Gtk::ListBox::SlotFilter &filter);
void InvalidateFilter();
private:
Gtk::ListBox m_list;
void OnPublicFetched(DiscordError code, const ArchivedThreadsResponseData &data);
using type_signal_thread_open = sigc::signal<void, Snowflake>;
type_signal_thread_open m_signal_thread_open;
public:
type_signal_thread_open signal_thread_open();
};
// view all threads in a channel
class ThreadsWindow : public Gtk::Window {
public:
ThreadsWindow(const ChannelData &channel);
private:
// this filtering is rather cringe but idk what a better alternative would be
bool ListFilterFunc(Gtk::ListBoxRow *row_);
enum FilterMode {
FILTER_PUBLIC = 0,
FILTER_PRIVATE = 1,
};
bool m_filter_mode = FILTER_PUBLIC;
Snowflake m_channel_id;
Gtk::StackSwitcher m_switcher;
Gtk::Stack m_stack;
Gtk::RadioButtonGroup m_group;
Gtk::ButtonBox m_filter_buttons;
Gtk::RadioButton m_filter_public;
Gtk::RadioButton m_filter_private;
Gtk::Box m_box;
ActiveThreadsList m_active;
ArchivedThreadsList m_archived;
};
class ThreadListRow : public Gtk::ListBoxRow {
public:
ThreadListRow(const ChannelData &channel);
Snowflake ID;
ChannelType Type;
private:
Gtk::Label m_label;
};