diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 7f4281d6..25a0a391 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -1,29 +1,32 @@ #include "abaddon.hpp" -#include -#include -#include -#include -#include -#include -#include + #include "platform.hpp" +#include "startup.hpp" +#include "util.hpp" #include "audio/manager.hpp" -#include "discord/discord.hpp" -#include "dialogs/token.hpp" #include "dialogs/confirm.hpp" -#include "dialogs/setstatus.hpp" #include "dialogs/friendpicker.hpp" -#include "dialogs/verificationgate.hpp" +#include "dialogs/setstatus.hpp" #include "dialogs/textinput.hpp" +#include "dialogs/token.hpp" +#include "dialogs/verificationgate.hpp" +#include "dialogs/quickswitcher/quickswitcher.hpp" +#include "discord/discord.hpp" +#include "notifications/notifications.hpp" +#include "remoteauth/remoteauthdialog.hpp" #include "windows/guildsettingswindow.hpp" -#include "windows/profilewindow.hpp" #include "windows/pinnedwindow.hpp" +#include "windows/profilewindow.hpp" #include "windows/threadswindow.hpp" #include "windows/voicewindow.hpp" -#include "startup.hpp" -#include "notifications/notifications.hpp" -#include "remoteauth/remoteauthdialog.hpp" -#include "util.hpp" + +#include +#include +#include +#include +#include +#include +#include #if defined(__APPLE__) #include @@ -1109,6 +1112,11 @@ bool Abaddon::ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window) { return dlg.run() == Gtk::RESPONSE_OK; } +void Abaddon::ShowQuickSwitcher(Gtk::Window *window) { + QuickSwitcher dlg(*window); + dlg.run(); +} + void Abaddon::ActionReloadCSS() { try { Gtk::StyleContext::remove_provider_for_screen(Gdk::Screen::get_default(), m_css_provider); diff --git a/src/abaddon.hpp b/src/abaddon.hpp index 6093523f..073f4d1d 100644 --- a/src/abaddon.hpp +++ b/src/abaddon.hpp @@ -66,6 +66,7 @@ class Abaddon { std::optional ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder = "", Gtk::Window *window = nullptr); bool ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window = nullptr); + void ShowQuickSwitcher(Gtk::Window *window); void ActionReloadCSS(); diff --git a/src/components/channellist/channellist.cpp b/src/components/channellist/channellist.cpp index f4095920..364a8f64 100644 --- a/src/components/channellist/channellist.cpp +++ b/src/components/channellist/channellist.cpp @@ -33,7 +33,7 @@ void ChannelList::UpdateListing() { } void ChannelList::SetActiveChannel(Snowflake id, bool expand_to) { - if (Abaddon::Get().GetSettings().ClassicChangeGuildOnOpen) { + if (Abaddon::Get().GetSettings().ClassicChannels && Abaddon::Get().GetSettings().ClassicChangeGuildOnOpen) { if (const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); channel.has_value() && channel->GuildID.has_value()) { m_tree.SetSelectedGuild(*channel->GuildID); } else { diff --git a/src/components/channellist/channellisttree.cpp b/src/components/channellist/channellisttree.cpp index ebbed589..74d30cc5 100644 --- a/src/components/channellist/channellisttree.cpp +++ b/src/components/channellist/channellisttree.cpp @@ -1235,58 +1235,13 @@ void ChannelListTree::UpdateCreateDMChannel(const ChannelData &dm) { void ChannelListTree::SetDMChannelIcon(Gtk::TreeIter iter, const ChannelData &dm) { auto &img = Abaddon::Get().GetImageManager(); - std::optional top_recipient; - const auto recipients = dm.GetDMRecipients(); - if (!recipients.empty()) - top_recipient = recipients[0]; - - if (dm.HasIcon()) { - const auto cb = [this, iter](const Glib::RefPtr &pb) { - if (iter) - (*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR); - }; - img.LoadFromURL(dm.GetIconURL(), sigc::track_obj(cb, *this)); - } else if (dm.Type == ChannelType::DM && top_recipient.has_value()) { - const auto cb = [this, iter](const Glib::RefPtr &pb) { - if (iter) - (*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR); - }; - img.LoadFromURL(top_recipient->GetAvatarURL("png", "32"), sigc::track_obj(cb, *this)); - } else { // GROUP_DM - std::string hash; - switch (dm.ID.GetUnixMilliseconds() % 8) { - case 0: - hash = "ee9275c5a437f7dc7f9430ba95f12ebd"; - break; - case 1: - hash = "9baf45aac2a0ec2e2dab288333acb9d9"; - break; - case 2: - hash = "7ba11ffb1900fa2b088cb31324242047"; - break; - case 3: - hash = "f90fca70610c4898bc57b58bce92f587"; - break; - case 4: - hash = "e2779af34b8d9126b77420e5f09213ce"; - break; - case 5: - hash = "c6851bd0b03f1cca5a8c1e720ea6ea17"; - break; - case 6: - hash = "f7e38ac976a2a696161c923502a8345b"; - break; - case 7: - default: - hash = "3cb840d03313467838d658bbec801fcd"; - break; - } - const auto cb = [this, iter](const Glib::RefPtr &pb) { - if (iter) - (*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR); - }; - img.LoadFromURL("https://discord.com/assets/" + hash + ".png", sigc::track_obj(cb, *this)); - } + auto icon_url = dm.GetIconURL(); + + const auto cb = [this, iter](const Glib::RefPtr &pb) { + if (iter) + (*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR); + }; + img.LoadFromURL(icon_url, sigc::track_obj(cb, *this)); } void ChannelListTree::RedrawUnreadIndicatorsForChannel(const ChannelData &channel) { diff --git a/src/dialogs/quickswitcher/quickswitcher.cpp b/src/dialogs/quickswitcher/quickswitcher.cpp new file mode 100644 index 00000000..ed6930e6 --- /dev/null +++ b/src/dialogs/quickswitcher/quickswitcher.cpp @@ -0,0 +1,228 @@ +#include "quickswitcher.hpp" + +#include "abaddon.hpp" +#include "quickswitcherresultrow.hpp" +#include "util.hpp" + +QuickSwitcher::QuickSwitcher(Gtk::Window &parent) + : Gtk::Dialog("Quick Switcher", parent, true) { + Index(); + + set_decorated(false); + set_default_size(350, 175); + + m_entry.set_placeholder_text("Where would you like to go?"); + m_entry.set_hexpand(true); + m_entry.signal_stop_search().connect([this]() { + response(Gtk::RESPONSE_OK); + }); + m_entry.signal_activate().connect(sigc::mem_fun(*this, &QuickSwitcher::OnEntryActivate)); + m_entry.signal_changed().connect(sigc::mem_fun(*this, &QuickSwitcher::Search)); + m_entry.signal_previous_match().connect([this]() { GoDown(); }); + m_entry.signal_next_match().connect([this]() { GoUp(); }); + m_entry.signal_key_press_event().connect(sigc::mem_fun(*this, &QuickSwitcher::OnEntryKeyPress), false); + get_content_area()->add(m_entry); + + m_results.set_activate_on_single_click(true); + m_results.set_selection_mode(Gtk::SELECTION_SINGLE); + m_results.set_vexpand(true); + m_results.signal_row_activated().connect(sigc::mem_fun(*this, &QuickSwitcher::OnResultRowActivate)); + m_results_scroll.add(m_results); + get_content_area()->add(m_results_scroll); + + show_all_children(); +} + +void QuickSwitcher::Index() { + m_index.clear(); + IndexPrivateChannels(); + IndexChannels(); + IndexGuilds(); +} + +void QuickSwitcher::IndexPrivateChannels() { + auto &discord = Abaddon::Get().GetDiscordClient(); + for (auto &dm_id : discord.GetPrivateChannels()) { + if (auto dm = discord.GetChannel(dm_id); dm.has_value()) { + const auto sort = static_cast(-(dm->LastMessageID.has_value() ? *dm->LastMessageID : dm_id)); + m_index[dm_id] = { SwitcherEntry::ResultType::DM, + dm->GetDisplayName(), + sort, + dm_id }; + } + } +} + +void QuickSwitcher::IndexChannels() { + auto &discord = Abaddon::Get().GetDiscordClient(); + + const auto channels = discord.GetAllChannelData(); + // grab literally everything to do in memory otherwise we get a shit ton of IOs + auto overwrites = discord.GetAllPermissionOverwrites(); + + auto member_roles = discord.GetAllMemberRoles(discord.GetUserData().ID); + std::unordered_map roles; + for (const auto &[guild_id, guild_roles] : member_roles) { + for (const auto &role_data : guild_roles) { + roles.emplace(role_data.ID, role_data); + } + } + + for (auto &channel : channels) { + if (!channel.Name.has_value()) continue; + if (!channel.IsText()) continue; + if (channel.GuildID.has_value() && + !discord.HasSelfChannelPermission(channel, Permission::VIEW_CHANNEL, roles, member_roles[*channel.GuildID], overwrites[channel.ID])) continue; + m_index[channel.ID] = { SwitcherEntry::ResultType::Channel, + *channel.Name, + static_cast(channel.ID), + channel.ID }; + } +} + +void QuickSwitcher::IndexGuilds() { + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto guilds = discord.GetGuilds(); + for (auto guild_id : guilds) { + const auto guild = discord.GetGuild(guild_id); + if (!guild.has_value()) continue; + // todo make this smart + m_index[guild->ID] = { SwitcherEntry::ResultType::Guild, + guild->Name, + static_cast(guild->ID), + guild->ID }; + } +} + +void QuickSwitcher::Search() { + for (auto child : m_results.get_children()) delete child; + + const auto query = m_entry.get_text(); + if (query.empty()) return; + + std::vector results; + for (auto &[id, item] : m_index) { + if (StringContainsCaseless(item.Name, query)) { + results.push_back(item); + } + } + + std::sort(results.begin(), results.end(), [](const SwitcherEntry &a, const SwitcherEntry &b) -> bool { + return a.Sort < b.Sort; + }); + + auto &discord = Abaddon::Get().GetDiscordClient(); + + int result_count = 0; + const int MAX_RESULTS = 15; + for (auto &result : results) { + QuickSwitcherResultRow *row = nullptr; + switch (result.Type) { + case SwitcherEntry::ResultType::DM: { + if (const auto channel = discord.GetChannel(result.ID); channel.has_value()) { + row = Gtk::make_managed(*channel); + } + } break; + case SwitcherEntry::ResultType::Channel: { + if (const auto channel = discord.GetChannel(result.ID); channel.has_value()) { + row = Gtk::make_managed(*channel); + } + } break; + case SwitcherEntry::ResultType::Guild: { + if (const auto guild = discord.GetGuild(result.ID); guild.has_value()) { + row = Gtk::make_managed(*guild); + } + } break; + } + if (row != nullptr) { + row->show(); + m_results.add(*row); + if (++result_count >= MAX_RESULTS) break; + } + } + + GoUp(); +} + +void QuickSwitcher::GoUp() { + Move(-1); +} + +void QuickSwitcher::GoDown() { + Move(1); +} + +void QuickSwitcher::Move(int dir) { + auto children = m_results.get_children(); + if (children.empty()) return; + + auto selected = m_results.get_selected_row(); + if (selected == nullptr) { + if (auto row = dynamic_cast(children[0])) { + m_results.select_row(*row); + } + return; + } + + int idx = selected->get_index() + dir; + if (idx < 0) { + idx = children.size() - 1; + } else if (idx >= children.size()) { + idx = 0; + } + + if (auto row = dynamic_cast(children[idx])) { + m_results.select_row(*row); + } + + ScrollListBoxToSelected(m_results); +} + +void QuickSwitcher::AcceptResult(Snowflake id) { + const auto result = m_index.find(id); + if (result == m_index.end()) return; + const auto &entry = result->second; + + switch (entry.Type) { + case SwitcherEntry::ResultType::Channel: + case SwitcherEntry::ResultType::DM: + Abaddon::Get().ActionChannelOpened(entry.ID, false); + break; + case SwitcherEntry::ResultType::Guild: { + const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(entry.ID); + if (!guild.has_value()) return; + const auto channel = guild->GetDefaultTextChannel(); + if (channel.has_value()) { + Abaddon::Get().ActionChannelOpened(*channel, false); + } + } break; + } +} + +void QuickSwitcher::OnEntryActivate() { + if (auto *row = dynamic_cast(m_results.get_selected_row())) { + AcceptResult(row->ID); + } + response(Gtk::RESPONSE_OK); +} + +bool QuickSwitcher::OnEntryKeyPress(GdkEventKey *event) { + if (event->type != GDK_KEY_PRESS) return false; + switch (event->keyval) { + case GDK_KEY_Up: + GoUp(); + return true; + case GDK_KEY_Down: + GoDown(); + return true; + default: + return false; + } +} + +void QuickSwitcher::OnResultRowActivate(Gtk::ListBoxRow *row_) { + if (auto *row = dynamic_cast(row_)) { + AcceptResult(row->ID); + } + response(Gtk::RESPONSE_OK); +} diff --git a/src/dialogs/quickswitcher/quickswitcher.hpp b/src/dialogs/quickswitcher/quickswitcher.hpp new file mode 100644 index 00000000..e1557ec4 --- /dev/null +++ b/src/dialogs/quickswitcher/quickswitcher.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include "discord/snowflake.hpp" + +#include + +#include +#include +#include +#include + +class QuickSwitcher : public Gtk::Dialog { +public: + QuickSwitcher(Gtk::Window &parent); + +private: + void Index(); + void IndexPrivateChannels(); + void IndexChannels(); + void IndexGuilds(); + void Search(); + + void GoUp(); + void GoDown(); + void Move(int dir); + + void AcceptResult(Snowflake id); + + void OnEntryActivate(); + bool OnEntryKeyPress(GdkEventKey *event); + + void OnResultRowActivate(Gtk::ListBoxRow *row); + + struct SwitcherEntry { + enum class ResultType { + DM, + Channel, + Guild, + } Type; + Glib::ustring Name; + uint64_t Sort; // lower = further up + Snowflake ID; + }; + std::unordered_map m_index; + + Gtk::SearchEntry m_entry; + Gtk::ScrolledWindow m_results_scroll; + Gtk::ListBox m_results; +}; diff --git a/src/dialogs/quickswitcher/quickswitcherresultrow.cpp b/src/dialogs/quickswitcher/quickswitcherresultrow.cpp new file mode 100644 index 00000000..8d21b4bc --- /dev/null +++ b/src/dialogs/quickswitcher/quickswitcherresultrow.cpp @@ -0,0 +1,65 @@ +#include "quickswitcherresultrow.hpp" + +#include "abaddon.hpp" + +static constexpr int HeightRequest = 24; // probably change this part up + +QuickSwitcherResultRow::QuickSwitcherResultRow(Snowflake id) + : ID(id) {} + +QuickSwitcherResultRowDM::QuickSwitcherResultRowDM(const ChannelData &channel) + : QuickSwitcherResultRow(channel.ID) + , m_img(channel.GetIconURL(), 24, 24, true) { + set_size_request(-1, HeightRequest); + + m_label.set_text(channel.GetDisplayName()); + + m_img.set_halign(Gtk::ALIGN_START); + m_img.set_margin_right(5); + m_label.set_halign(Gtk::ALIGN_START); + + m_box.pack_start(m_img, false, true); + m_box.pack_start(m_label, true, true); + add(m_box); + show_all_children(); +} + +QuickSwitcherResultRowChannel::QuickSwitcherResultRowChannel(const ChannelData &channel) + : QuickSwitcherResultRow(channel.ID) { + set_size_request(-1, HeightRequest); + + if (channel.GuildID.has_value()) { + const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(*channel.GuildID); + if (guild.has_value()) { + m_guild_label.set_text(guild->Name); + } + } + + m_channel_label.set_text("#" + (channel.Name ? *channel.Name : std::string("???"))); + + m_channel_label.set_halign(Gtk::ALIGN_START); + m_guild_label.set_halign(Gtk::ALIGN_END); + + m_box.pack_start(m_channel_label, true, true); + m_box.pack_start(m_guild_label, true, true); + add(m_box); + show_all_children(); +} + +QuickSwitcherResultRowGuild::QuickSwitcherResultRowGuild(const GuildData &guild) + : QuickSwitcherResultRow(guild.ID) + , m_img(24, 24) { + set_size_request(-1, HeightRequest); + + if (guild.HasIcon()) m_img.SetURL(guild.GetIconURL("png", "32")); + m_label.set_text(guild.Name); + + m_img.set_halign(Gtk::ALIGN_START); + m_img.set_margin_right(5); + m_label.set_halign(Gtk::ALIGN_START); + + m_box.pack_start(m_img, false, true); + m_box.pack_start(m_label, true, true); + add(m_box); + show_all_children(); +} diff --git a/src/dialogs/quickswitcher/quickswitcherresultrow.hpp b/src/dialogs/quickswitcher/quickswitcherresultrow.hpp new file mode 100644 index 00000000..aa0b3362 --- /dev/null +++ b/src/dialogs/quickswitcher/quickswitcherresultrow.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "components/lazyimage.hpp" +#include "discord/channel.hpp" +#include "discord/guild.hpp" + +#include +#include +#include + +class QuickSwitcherResultRow : public Gtk::ListBoxRow { +public: + QuickSwitcherResultRow(Snowflake id); + Snowflake ID; +}; + +class QuickSwitcherResultRowDM : public QuickSwitcherResultRow { +public: + QuickSwitcherResultRowDM(const ChannelData &channel); + +private: + Gtk::HBox m_box; + LazyImage m_img; + Gtk::Label m_label; +}; + +class QuickSwitcherResultRowChannel : public QuickSwitcherResultRow { +public: + QuickSwitcherResultRowChannel(const ChannelData &channel); + +private: + Gtk::HBox m_box; + Gtk::Label m_channel_label; + Gtk::Label m_guild_label; +}; + +class QuickSwitcherResultRowGuild : public QuickSwitcherResultRow { +public: + QuickSwitcherResultRowGuild(const GuildData &guild); + +private: + Gtk::HBox m_box; + LazyImage m_img; + Gtk::Label m_label; +}; diff --git a/src/discord/channel.cpp b/src/discord/channel.cpp index 8b3ee19a..29ca0640 100644 --- a/src/discord/channel.cpp +++ b/src/discord/channel.cpp @@ -96,13 +96,46 @@ bool ChannelData::HasIcon() const noexcept { std::string ChannelData::GetIconURL() const { if (HasIcon()) { return "https://cdn.discordapp.com/channel-icons/" + std::to_string(ID) + "/" + *Icon + ".png"; - } else { + } else if (Type == ChannelType::DM) { const auto recipients = GetDMRecipients(); - if (!recipients.empty()) + if (!recipients.empty()) { return recipients[0].GetAvatarURL("png", "32"); - else + } else { return "https://cdn.discordapp.com/embed/avatars/0.png"; + } + } else if (Type == ChannelType::GROUP_DM) { + std::string hash; + switch (ID.GetUnixMilliseconds() % 8) { + case 0: + hash = "ee9275c5a437f7dc7f9430ba95f12ebd"; + break; + case 1: + hash = "9baf45aac2a0ec2e2dab288333acb9d9"; + break; + case 2: + hash = "7ba11ffb1900fa2b088cb31324242047"; + break; + case 3: + hash = "f90fca70610c4898bc57b58bce92f587"; + break; + case 4: + hash = "e2779af34b8d9126b77420e5f09213ce"; + break; + case 5: + hash = "c6851bd0b03f1cca5a8c1e720ea6ea17"; + break; + case 6: + hash = "f7e38ac976a2a696161c923502a8345b"; + break; + case 7: + default: + hash = "3cb840d03313467838d658bbec801fcd"; + break; + } + return "https://discord.com/assets/" + hash + ".png"; } + + return "https://cdn.discordapp.com/embed/avatars/0.png"; } std::string ChannelData::GetDisplayName() const { diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index 385d6b7e..bcf384dd 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -358,6 +358,18 @@ std::optional DiscordClient::GetWebhookMessageData(Snowflake return m_store.GetWebhookMessage(message_id); } +std::vector DiscordClient::GetAllChannelData() const { + return m_store.GetAllChannelData(); +} + +std::unordered_map> DiscordClient::GetAllPermissionOverwrites() const { + return m_store.GetAllPermissionOverwriteData(); +} + +std::unordered_map> DiscordClient::GetAllMemberRoles(Snowflake user_id) const { + return m_store.GetAllMemberRoles(user_id); +} + bool DiscordClient::IsThreadJoined(Snowflake thread_id) const { return std::find(m_joined_threads.begin(), m_joined_threads.end(), thread_id) != m_joined_threads.end(); } @@ -459,6 +471,88 @@ bool DiscordClient::CanManageMember(Snowflake guild_id, Snowflake actor, Snowfla if (!target_highest.has_value()) return true; return actor_highest->Position > target_highest->Position; } +bool DiscordClient::HasSelfChannelPermission(const ChannelData &channel, + Permission perm, + const std::unordered_map &roles, + const std::vector &member_roles, + const std::unordered_map &overwrites) const { + return HasChannelPermission(m_user_data.ID, channel, perm, roles, member_roles, overwrites); +} + +bool DiscordClient::HasChannelPermission(Snowflake user_id, + const ChannelData &channel, + Permission perm, + const std::unordered_map &roles, + const std::vector &member_roles, + const std::unordered_map &overwrites) const { + if (channel.IsDM()) return true; + if (!channel.GuildID.has_value()) return false; + const auto base = ComputePermissions(user_id, *channel.GuildID, roles, member_roles); + const auto computed_overwrites = ComputeOverwrites(base, user_id, channel, member_roles, overwrites); + return (computed_overwrites & perm) == perm; +} + +Permission DiscordClient::ComputePermissions(Snowflake member_id, + Snowflake guild_id, + const std::unordered_map &roles, + const std::vector &member_roles) const { + const auto guild_owner = m_store.GetGuildOwner(guild_id); + + if (guild_owner == member_id) + return Permission::ALL; + + if (const auto everyone_it = roles.find(guild_id); everyone_it != roles.end()) { + const auto &everyone = everyone_it->second; + + Permission perms = everyone.Permissions; + for (const auto &role : member_roles) { + perms |= role.Permissions; + } + + if ((perms & Permission::ADMINISTRATOR) == Permission::ADMINISTRATOR) + return Permission::ALL; + + return perms; + } + + return Permission::NONE; +} + +Permission DiscordClient::ComputeOverwrites(Permission base, + Snowflake member_id, + const ChannelData &channel, + const std::vector &member_roles, + const std::unordered_map &overwrites) const { + if ((base & Permission::ADMINISTRATOR) == Permission::ADMINISTRATOR) + return Permission::ALL; + + if (!channel.GuildID.has_value()) return Permission::NONE; + + Permission perms = base; + if (const auto overwrite_everyone = overwrites.find(*channel.GuildID); overwrite_everyone != overwrites.end()) { + perms &= ~overwrite_everyone->second.Deny; + perms |= overwrite_everyone->second.Allow; + } + + Permission allow = Permission::NONE; + Permission deny = Permission::NONE; + for (const auto &role : member_roles) { + if (const auto overwrite = overwrites.find(role.ID); overwrite != overwrites.end()) { + allow |= overwrite->second.Allow; + deny |= overwrite->second.Deny; + } + } + + perms &= ~deny; + perms |= allow; + + if (const auto member_overwrite = overwrites.find(member_id); member_overwrite != overwrites.end()) { + perms &= ~member_overwrite->second.Deny; + perms |= member_overwrite->second.Allow; + } + + return perms; +} void DiscordClient::ChatMessageCallback(const std::string &nonce, const http::response_type &response, const sigc::slot &callback) { if (!CheckCode(response)) { diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index 9c2c0809..fc1407fe 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -65,6 +65,9 @@ class DiscordClient { void GetArchivedPrivateThreads(Snowflake channel_id, const sigc::slot &callback); std::vector GetChildChannelIDs(Snowflake parent_id) const; std::optional GetWebhookMessageData(Snowflake message_id) const; + std::vector GetAllChannelData() const; + std::unordered_map> GetAllPermissionOverwrites() const; + std::unordered_map> GetAllMemberRoles(Snowflake user_id) const; // get ids of given list of members for who we do not have the member data template @@ -87,6 +90,28 @@ class DiscordClient { Permission ComputeOverwrites(Permission base, Snowflake member_id, Snowflake channel_id) const; bool CanManageMember(Snowflake guild_id, Snowflake actor, Snowflake target) const; // kick, ban, edit nickname (cant think of a better name) + // IO-less calls + bool HasSelfChannelPermission(const ChannelData &channel, + Permission perm, + const std::unordered_map &roles, + const std::vector &member_roles, + const std::unordered_map &overwrites) const; + bool HasChannelPermission(Snowflake user_id, + const ChannelData &channel, + Permission perm, + const std::unordered_map &roles, + const std::vector &member_roles, + const std::unordered_map &overwrites) const; + Permission ComputePermissions(Snowflake member_id, + Snowflake guild_id, + const std::unordered_map &roles, + const std::vector &member_roles) const; + Permission ComputeOverwrites(Permission base, + Snowflake member_id, + const ChannelData &channel, + const std::vector &member_roles, + const std::unordered_map &overwrites) const; + void ChatMessageCallback(const std::string &nonce, const http::response_type &response, const sigc::slot &callback); void SendChatMessageNoAttachments(const ChatSubmitParams ¶ms, const sigc::slot &callback); void SendChatMessageAttachments(const ChatSubmitParams ¶ms, const sigc::slot &callback); diff --git a/src/discord/guild.cpp b/src/discord/guild.cpp index 06c4acf7..c54cd033 100644 --- a/src/discord/guild.cpp +++ b/src/discord/guild.cpp @@ -1,5 +1,7 @@ #include "guild.hpp" +#include "abaddon.hpp" + void from_json(const nlohmann::json &j, GuildData &m) { JS_D("id", m.ID); if (j.contains("unavailable")) { @@ -138,6 +140,27 @@ std::string GuildData::GetIconURL(const std::string &ext, const std::string &siz return "https://cdn.discordapp.com/icons/" + std::to_string(ID) + "/" + Icon + "." + ext + "?size=" + size; } +std::optional GuildData::GetDefaultTextChannel() const { + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto channel_ids = discord.GetChannelsInGuild(ID); + std::vector channels; + for (auto channel_id : channel_ids) { + const auto channel = discord.GetChannel(channel_id); + if (!channel.has_value() || !channel->IsText() || !channel->Position.has_value()) continue; + channels.push_back(*channel); + } + + std::sort(channels.begin(), channels.end(), [](const ChannelData &a, const ChannelData &b) -> bool { + return *a.Position < *b.Position; + }); + + for (const auto &channel : channels) { + if (discord.HasSelfChannelPermission(channel.ID, Permission::VIEW_CHANNEL)) return channel.ID; + } + + return {}; +} + void from_json(const nlohmann::json &j, GuildApplicationData &m) { JS_D("user_id", m.UserID); JS_D("guild_id", m.GuildID); diff --git a/src/discord/guild.hpp b/src/discord/guild.hpp index 4895d307..dc7375eb 100644 --- a/src/discord/guild.hpp +++ b/src/discord/guild.hpp @@ -104,4 +104,5 @@ struct GuildData { bool HasIcon() const; bool HasAnimatedIcon() const; std::string GetIconURL(const std::string &ext = "png", const std::string &size = "32") const; + std::optional GetDefaultTextChannel() const; }; diff --git a/src/discord/store.cpp b/src/discord/store.cpp index bf630aa6..671080b7 100644 --- a/src/discord/store.cpp +++ b/src/discord/store.cpp @@ -1222,6 +1222,83 @@ std::unordered_set Store::GetGuilds() const { return r; } +std::vector Store::GetAllChannelData() const { + auto &s = m_stmt_get_all_chans; + std::vector r; + + while (s->FetchOne()) { + auto &c = r.emplace_back(); + s->Get(0, c.ID); + s->Get(1, c.Type); + s->Get(2, c.GuildID); + s->Get(3, c.Position); + s->Get(4, c.Name); + s->Get(5, c.Topic); + s->Get(6, c.IsNSFW); + s->Get(7, c.LastMessageID); + s->Get(10, c.RateLimitPerUser); + s->Get(11, c.Icon); + s->Get(12, c.OwnerID); + s->Get(14, c.ParentID); + if (!s->IsNull(16)) { + c.ThreadMetadata.emplace(); + s->Get(16, c.ThreadMetadata->IsArchived); + s->Get(17, c.ThreadMetadata->AutoArchiveDuration); + s->Get(18, c.ThreadMetadata->ArchiveTimestamp); + } + } + + s->Reset(); + + return r; +} + +std::unordered_map> Store::GetAllPermissionOverwriteData() const { + auto &s = m_stmt_get_all_perms; + std::unordered_map> r; + + while (s->FetchOne()) { + PermissionOverwrite d; + Snowflake channel_id; + s->Get(0, d.ID); + s->Get(1, channel_id); + s->Get(2, d.Type); + s->Get(3, d.Allow); + s->Get(4, d.Deny); + r[channel_id][d.ID] = d; + } + + s->Reset(); + + return r; +} + +std::unordered_map> Store::GetAllMemberRoles(Snowflake user_id) const { + auto &s = m_stmt_get_self_member_roles; + std::unordered_map> r; + + s->Bind(1, user_id); + + while (s->FetchOne()) { + Snowflake guild_id; + RoleData role; + s->Get(0, role.ID); + s->Get(1, guild_id); + s->Get(2, role.Name); + s->Get(3, role.Color); + s->Get(4, role.IsHoisted); + s->Get(5, role.Position); + s->Get(6, role.Permissions); + s->Get(7, role.IsManaged); + s->Get(8, role.IsMentionable); + r[guild_id].push_back(std::move(role)); + } + + s->Reset(); + + return r; +} + void Store::ClearAll() { if (m_db.Execute(R"( DELETE FROM attachments; @@ -1739,6 +1816,14 @@ bool Store::CreateStatements() { return false; } + m_stmt_get_all_chans = std::make_unique(m_db, R"( + SELECT * FROM channels + )"); + if (!m_stmt_get_all_chans->OK()) { + fprintf(stderr, "failed to prepare get channels statement: %s\n", m_db.ErrStr()); + return false; + } + m_stmt_clr_chan = std::make_unique(m_db, R"( DELETE FROM channels WHERE id = ? )"); @@ -1944,6 +2029,14 @@ bool Store::CreateStatements() { return false; } + m_stmt_get_all_perms = std::make_unique(m_db, R"( + SELECT * FROM permissions + )"); + if (!m_stmt_get_all_perms->OK()) { + fprintf(stderr, "failed to prepare get all permissions statement: %s\n", m_db.ErrStr()); + return false; + } + m_stmt_set_ban = std::make_unique(m_db, R"( REPLACE INTO bans VALUES ( ?, ?, ? @@ -2023,6 +2116,18 @@ bool Store::CreateStatements() { return false; } + m_stmt_get_self_member_roles = std::make_unique(m_db, R"( + SELECT DISTINCT roles.* + FROM member_roles, roles + WHERE (member_roles.user = ? + AND member_roles.role = roles.id) + OR roles.id = roles.guild /* @everyone */ + )"); + if (!m_stmt_get_self_member_roles->OK()) { + fprintf(stderr, "failed to prepare get self member roles statement: %s\n", m_db.ErrStr()); + return false; + } + m_stmt_set_guild_emoji = std::make_unique(m_db, R"( REPLACE INTO guild_emojis VALUES ( ?, ? diff --git a/src/discord/store.hpp b/src/discord/store.hpp index 6157f09f..a5bb9f4a 100644 --- a/src/discord/store.hpp +++ b/src/discord/store.hpp @@ -96,6 +96,11 @@ class Store { std::unordered_set GetChannels() const; std::unordered_set GetGuilds() const; + // this does NOT include recipients + std::vector GetAllChannelData() const; + std::unordered_map> GetAllPermissionOverwriteData() const; + std::unordered_map> GetAllMemberRoles(Snowflake user_id) const; + void ClearAll(); void BeginTransaction(); @@ -289,6 +294,7 @@ class Store { STMT(set_chan); STMT(get_chan); STMT(get_chan_ids); + STMT(get_all_chans); STMT(clr_chan); STMT(set_msg); STMT(get_msg); @@ -305,6 +311,7 @@ class Store { STMT(get_emoji); STMT(set_perm); STMT(get_perm); + STMT(get_all_perms); STMT(set_ban); STMT(get_ban); STMT(get_bans); @@ -313,6 +320,7 @@ class Store { STMT(set_member_roles); STMT(get_member_roles); STMT(clr_member_roles); + STMT(get_self_member_roles); STMT(set_guild_emoji); STMT(get_guild_emojis); STMT(clr_guild_emoji); diff --git a/src/windows/mainwindow.cpp b/src/windows/mainwindow.cpp index ee28d175..1c2a1528 100644 --- a/src/windows/mainwindow.cpp +++ b/src/windows/mainwindow.cpp @@ -296,6 +296,8 @@ void MainWindow::SetupMenu() { m_menu_view_members.set_label("Members"); m_menu_view_members.add_accelerator("activate", m_accels, GDK_KEY_M, Gdk::CONTROL_MASK, Gtk::ACCEL_VISIBLE); m_menu_view_members.set_active(true); + m_menu_view_quick_switcher.set_label("Quick Switcher"); + m_menu_view_quick_switcher.add_accelerator("activate", m_accels, GDK_KEY_K, Gdk::CONTROL_MASK, Gtk::ACCEL_VISIBLE); #ifdef WITH_LIBHANDY m_menu_view_go_back.set_label("Go Back"); m_menu_view_go_forward.set_label("Go Forward"); @@ -308,6 +310,7 @@ void MainWindow::SetupMenu() { m_menu_view_sub.append(m_menu_view_mark_guild_as_read); m_menu_view_sub.append(m_menu_view_channels); m_menu_view_sub.append(m_menu_view_members); + m_menu_view_sub.append(m_menu_view_quick_switcher); #ifdef WITH_LIBHANDY m_menu_view_sub.append(m_menu_view_go_back); m_menu_view_sub.append(m_menu_view_go_forward); @@ -403,6 +406,10 @@ void MainWindow::SetupMenu() { m_members.GetRoot()->set_visible(m_menu_view_members.get_active()); }); + m_menu_view_quick_switcher.signal_activate().connect([this]() { + Abaddon::Get().ShowQuickSwitcher(this); + }); + #ifdef WITH_LIBHANDY m_menu_view_go_back.signal_activate().connect([this] { GoBack(); diff --git a/src/windows/mainwindow.hpp b/src/windows/mainwindow.hpp index 37c1b87a..98b67a3e 100644 --- a/src/windows/mainwindow.hpp +++ b/src/windows/mainwindow.hpp @@ -94,6 +94,7 @@ class MainWindow : public Gtk::Window { Gtk::MenuItem m_menu_view_mark_guild_as_read; Gtk::CheckMenuItem m_menu_view_channels; Gtk::CheckMenuItem m_menu_view_members; + Gtk::MenuItem m_menu_view_quick_switcher; #ifdef WITH_LIBHANDY Gtk::MenuItem m_menu_view_go_back; Gtk::MenuItem m_menu_view_go_forward;