diff options
author | ouwou <26526779+ouwou@users.noreply.github.com> | 2022-03-26 02:58:59 -0400 |
---|---|---|
committer | ouwou <26526779+ouwou@users.noreply.github.com> | 2022-03-26 02:58:59 -0400 |
commit | a0b3c9f8a4f8d2c39258d4c142f8604423576d91 (patch) | |
tree | 841155a65b439b61a5c58c1f64878152c20d4f51 /src | |
parent | 481685b3bbb2b0270870dec5de87e60fc2d84d15 (diff) | |
parent | a2a45757e917aa97e71cf0b84a01dc843759a5f6 (diff) | |
download | abaddon-portaudio-a0b3c9f8a4f8d2c39258d4c142f8604423576d91.tar.gz abaddon-portaudio-a0b3c9f8a4f8d2c39258d4c142f8604423576d91.zip |
Merge branch 'master' into msys
Diffstat (limited to 'src')
-rw-r--r-- | src/abaddon.cpp | 23 | ||||
-rw-r--r-- | src/abaddon.hpp | 2 | ||||
-rw-r--r-- | src/components/channels.cpp | 55 | ||||
-rw-r--r-- | src/components/channels.hpp | 4 | ||||
-rw-r--r-- | src/components/channelscellrenderer.cpp | 95 | ||||
-rw-r--r-- | src/components/chatmessage.cpp | 114 | ||||
-rw-r--r-- | src/components/chatmessage.hpp | 7 | ||||
-rw-r--r-- | src/dialogs/token.cpp | 2 | ||||
-rw-r--r-- | src/discord/channel.cpp | 10 | ||||
-rw-r--r-- | src/discord/channel.hpp | 12 | ||||
-rw-r--r-- | src/discord/discord.cpp | 65 | ||||
-rw-r--r-- | src/discord/discord.hpp | 29 | ||||
-rw-r--r-- | src/discord/objects.cpp | 19 | ||||
-rw-r--r-- | src/discord/objects.hpp | 55 | ||||
-rw-r--r-- | src/discord/role.cpp | 8 | ||||
-rw-r--r-- | src/discord/role.hpp | 3 | ||||
-rw-r--r-- | src/discord/store.cpp | 45 | ||||
-rw-r--r-- | src/discord/store.hpp | 3 | ||||
-rw-r--r-- | src/discord/user.cpp | 29 | ||||
-rw-r--r-- | src/discord/user.hpp | 4 | ||||
-rw-r--r-- | src/settings.cpp | 10 | ||||
-rw-r--r-- | src/settings.hpp | 7 |
22 files changed, 516 insertions, 85 deletions
diff --git a/src/abaddon.cpp b/src/abaddon.cpp index bf1c6cf..1ab6f8c 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -348,6 +348,20 @@ void Abaddon::ShowGuildVerificationGateDialog(Snowflake guild_id) { } } +void Abaddon::CheckMessagesForMembers(const ChannelData &chan, const std::vector<Message> &msgs) { + if (!chan.GuildID.has_value()) return; + + std::vector<Snowflake> unknown; + std::transform(msgs.begin(), msgs.end(), + std::back_inserter(unknown), + [](const Message &msg) -> Snowflake { + return msg.Author.ID; + }); + + const auto fetch = m_discord.FilterUnknownMembersFrom(*chan.GuildID, unknown.begin(), unknown.end()); + m_discord.RequestMembers(*chan.GuildID, fetch.begin(), fetch.end()); +} + void Abaddon::SetupUserMenu() { m_user_menu = Gtk::manage(new Gtk::Menu); m_user_menu_insert_mention = Gtk::manage(new Gtk::MenuItem("Insert Mention")); @@ -559,7 +573,8 @@ void Abaddon::ActionChannelOpened(Snowflake id) { if (m_channels_requested.find(id) == m_channels_requested.end()) { // dont fire requests we know will fail if (can_access) { - m_discord.FetchMessagesInChannel(id, [this, id](const std::vector<Message> &msgs) { + m_discord.FetchMessagesInChannel(id, [channel, this, id](const std::vector<Message> &msgs) { + CheckMessagesForMembers(*channel, msgs); m_main_window->UpdateChatWindowContents(); m_channels_requested.insert(id); }); @@ -602,7 +617,11 @@ void Abaddon::ActionChatLoadHistory(Snowflake id) { m_discord.FetchMessagesInChannelBefore(id, before_id, [this, id](const std::vector<Message> &msgs) { m_channels_history_loading.erase(id); - if (msgs.size() == 0) { + const auto channel = m_discord.GetChannel(id); + if (channel.has_value()) + CheckMessagesForMembers(*channel, msgs); + + if (msgs.empty()) { m_channels_history_loaded.insert(id); } else { m_main_window->UpdateChatPrependHistory(msgs); diff --git a/src/abaddon.hpp b/src/abaddon.hpp index d9d0bb0..311dcc5 100644 --- a/src/abaddon.hpp +++ b/src/abaddon.hpp @@ -93,6 +93,8 @@ public: protected: void ShowGuildVerificationGateDialog(Snowflake guild_id); + void CheckMessagesForMembers(const ChannelData &chan, const std::vector<Message> &msgs); + void SetupUserMenu(); void SaveState(); void LoadState(); diff --git a/src/components/channels.cpp b/src/components/channels.cpp index 28eb288..99bd07b 100644 --- a/src/components/channels.cpp +++ b/src/components/channels.cpp @@ -22,7 +22,8 @@ ChannelList::ChannelList() , m_menu_thread_copy_id("_Copy ID", true) , m_menu_thread_leave("_Leave", true) , m_menu_thread_archive("_Archive", true) - , m_menu_thread_unarchive("_Unarchive", true) { + , m_menu_thread_unarchive("_Unarchive", true) + , m_menu_thread_mark_as_read("Mark as _Read", true) { get_style_context()->add_class("channel-list"); // todo: move to method @@ -161,6 +162,15 @@ ChannelList::ChannelList() else if (Abaddon::Get().ShowConfirm("Are you sure you want to leave this group DM?")) Abaddon::Get().GetDiscordClient().CloseDM(id); }); + m_menu_dm_toggle_mute.signal_activate().connect([this] { + const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.IsChannelMuted(id)) + discord.UnmuteChannel(id, NOOP_CALLBACK); + else + discord.MuteChannel(id, NOOP_CALLBACK); + }); + m_menu_dm.append(m_menu_dm_toggle_mute); m_menu_dm.append(m_menu_dm_close); m_menu_dm.append(m_menu_dm_copy_id); m_menu_dm.show_all(); @@ -178,15 +188,29 @@ ChannelList::ChannelList() m_menu_thread_unarchive.signal_activate().connect([this] { Abaddon::Get().GetDiscordClient().UnArchiveThread(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {}); }); - m_menu_thread.append(m_menu_thread_copy_id); + m_menu_thread_mark_as_read.signal_activate().connect([this] { + Abaddon::Get().GetDiscordClient().MarkChannelAsRead(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), NOOP_CALLBACK); + }); + m_menu_thread_toggle_mute.signal_activate().connect([this] { + const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]); + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.IsChannelMuted(id)) + discord.UnmuteThread(id, NOOP_CALLBACK); + else + discord.MuteThread(id, NOOP_CALLBACK); + }); + m_menu_thread.append(m_menu_thread_mark_as_read); + m_menu_thread.append(m_menu_thread_toggle_mute); m_menu_thread.append(m_menu_thread_leave); m_menu_thread.append(m_menu_thread_archive); m_menu_thread.append(m_menu_thread_unarchive); + m_menu_thread.append(m_menu_thread_copy_id); m_menu_thread.show_all(); m_menu_guild.signal_popped_up().connect(sigc::mem_fun(*this, &ChannelList::OnGuildSubmenuPopup)); m_menu_category.signal_popped_up().connect(sigc::mem_fun(*this, &ChannelList::OnCategorySubmenuPopup)); m_menu_channel.signal_popped_up().connect(sigc::mem_fun(*this, &ChannelList::OnChannelSubmenuPopup)); + m_menu_dm.signal_popped_up().connect(sigc::mem_fun(*this, &ChannelList::OnDMSubmenuPopup)); m_menu_thread.signal_popped_up().connect(sigc::mem_fun(*this, &ChannelList::OnThreadSubmenuPopup)); auto &discord = Abaddon::Get().GetDiscordClient(); @@ -728,7 +752,13 @@ void ChannelList::AddPrivateChannels() { else if (dm->Type == ChannelType::GROUP_DM) row[m_columns.m_name] = std::to_string(recipients.size()) + " members"; - if (top_recipient.has_value()) { + if (dm->HasIcon()) { + const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &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 (top_recipient.has_value()) { const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &pb) { if (iter) (*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR); @@ -887,6 +917,16 @@ void ChannelList::OnChannelSubmenuPopup(const Gdk::Rectangle *flipped_rect, cons m_menu_channel_toggle_mute.set_label("Mute"); } +void ChannelList::OnDMSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y) { + auto iter = m_model->get_iter(m_path_for_menu); + if (!iter) return; + const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]); + if (Abaddon::Get().GetDiscordClient().IsChannelMuted(id)) + m_menu_dm_toggle_mute.set_label("Unmute"); + else + m_menu_dm_toggle_mute.set_label("Mute"); +} + void ChannelList::OnThreadSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y) { m_menu_thread_archive.set_visible(false); m_menu_thread_unarchive.set_visible(false); @@ -894,7 +934,14 @@ void ChannelList::OnThreadSubmenuPopup(const Gdk::Rectangle *flipped_rect, const auto &discord = Abaddon::Get().GetDiscordClient(); auto iter = m_model->get_iter(m_path_for_menu); if (!iter) return; - auto channel = discord.GetChannel(static_cast<Snowflake>((*iter)[m_columns.m_id])); + const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]); + + if (discord.IsChannelMuted(id)) + m_menu_thread_toggle_mute.set_label("Unmute"); + else + m_menu_thread_toggle_mute.set_label("Mute"); + + auto channel = discord.GetChannel(id); if (!channel.has_value() || !channel->ThreadMetadata.has_value()) return; if (!discord.HasGuildPermission(discord.GetUserData().ID, *channel->GuildID, Permission::MANAGE_THREADS)) return; diff --git a/src/components/channels.hpp b/src/components/channels.hpp index ba75be8..a2553fd 100644 --- a/src/components/channels.hpp +++ b/src/components/channels.hpp @@ -124,16 +124,20 @@ protected: Gtk::Menu m_menu_dm; Gtk::MenuItem m_menu_dm_copy_id; Gtk::MenuItem m_menu_dm_close; + Gtk::MenuItem m_menu_dm_toggle_mute; Gtk::Menu m_menu_thread; Gtk::MenuItem m_menu_thread_copy_id; Gtk::MenuItem m_menu_thread_leave; Gtk::MenuItem m_menu_thread_archive; Gtk::MenuItem m_menu_thread_unarchive; + Gtk::MenuItem m_menu_thread_mark_as_read; + Gtk::MenuItem m_menu_thread_toggle_mute; void OnGuildSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y); void OnCategorySubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y); void OnChannelSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y); + void OnDMSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y); void OnThreadSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y); bool m_updating_listing = false; diff --git a/src/components/channelscellrenderer.cpp b/src/components/channelscellrenderer.cpp index 16fd25f..141556a 100644 --- a/src/components/channelscellrenderer.cpp +++ b/src/components/channelscellrenderer.cpp @@ -214,6 +214,8 @@ void CellRendererChannels::render_vfunc_guild(const Cairo::RefPtr<Cairo::Context Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h); + static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor); + m_renderer_text.property_foreground_rgba() = color; m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); const bool hover_only = Abaddon::Get().GetSettings().AnimatedGuildHoverOnly; @@ -247,6 +249,7 @@ void CellRendererChannels::render_vfunc_guild(const Cairo::RefPtr<Cairo::Context } // unread + if (!Abaddon::Get().GetSettings().Unreads) return; const auto id = m_property_id.get_value(); @@ -255,7 +258,8 @@ void CellRendererChannels::render_vfunc_guild(const Cairo::RefPtr<Cairo::Context const auto has_unread = discord.GetUnreadStateForGuild(id, total_mentions); if (has_unread && !discord.IsGuildMuted(id)) { - cr->set_source_rgb(1.0, 1.0, 1.0); + static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().UnreadIndicatorColor); + cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue()); const auto x = background_area.get_x(); const auto y = background_area.get_y(); const auto w = background_area.get_width(); @@ -327,9 +331,16 @@ void CellRendererChannels::render_vfunc_category(const Cairo::RefPtr<Cairo::Cont Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h); - static Gdk::RGBA muted_color("#7f7f7f"); - if (Abaddon::Get().GetDiscordClient().IsChannelMuted(m_property_id.get_value())) - m_renderer_text.property_foreground_rgba() = muted_color; + static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor); + if (Abaddon::Get().GetDiscordClient().IsChannelMuted(m_property_id.get_value())) { + auto muted = color; + muted.set_red(muted.get_red() * 0.5); + muted.set_green(muted.get_green() * 0.5); + muted.set_blue(muted.get_blue() * 0.5); + m_renderer_text.property_foreground_rgba() = muted; + } else { + m_renderer_text.property_foreground_rgba() = color; + } m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); m_renderer_text.property_foreground_set() = false; } @@ -367,10 +378,10 @@ void CellRendererChannels::render_vfunc_channel(const Cairo::RefPtr<Cairo::Conte const auto id = m_property_id.get_value(); const bool is_muted = discord.IsChannelMuted(id); - // move to style in msys? - static Gdk::RGBA sfw_unmuted("#FFFFFF"); + static const auto sfw_unmuted = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor); - const auto nsfw_color = Gdk::RGBA(Abaddon::Get().GetSettings().NSFWChannelColor); + m_renderer_text.property_sensitive() = false; + static const auto nsfw_color = Gdk::RGBA(Abaddon::Get().GetSettings().NSFWChannelColor); if (m_property_nsfw.get_value()) m_renderer_text.property_foreground_rgba() = nsfw_color; else @@ -387,12 +398,14 @@ void CellRendererChannels::render_vfunc_channel(const Cairo::RefPtr<Cairo::Conte m_renderer_text.property_foreground_set() = false; // unread + if (!Abaddon::Get().GetSettings().Unreads) return; const auto unread_state = discord.GetUnreadStateForChannel(id); if (unread_state < 0) return; if (!is_muted) { - cr->set_source_rgb(1.0, 1.0, 1.0); + static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().UnreadIndicatorColor); + cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue()); const auto x = background_area.get_x(); const auto y = background_area.get_y(); const auto w = background_area.get_width(); @@ -438,7 +451,48 @@ void CellRendererChannels::render_vfunc_thread(const Cairo::RefPtr<Cairo::Contex const int text_h = natural_size.height; Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h); + + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto id = m_property_id.get_value(); + const bool is_muted = discord.IsChannelMuted(id); + + static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor); + if (Abaddon::Get().GetDiscordClient().IsChannelMuted(m_property_id.get_value())) { + auto muted = color; + muted.set_red(muted.get_red() * 0.5); + muted.set_green(muted.get_green() * 0.5); + muted.set_blue(muted.get_blue() * 0.5); + m_renderer_text.property_foreground_rgba() = muted; + } else { + m_renderer_text.property_foreground_rgba() = color; + } m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); + m_renderer_text.property_foreground_set() = false; + + // unread + if (!Abaddon::Get().GetSettings().Unreads) return; + + const auto unread_state = discord.GetUnreadStateForChannel(id); + if (unread_state < 0) return; + + if (!is_muted) { + static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().UnreadIndicatorColor); + cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue()); + const auto x = background_area.get_x(); + const auto y = background_area.get_y(); + const auto w = background_area.get_width(); + const auto h = background_area.get_height(); + cr->rectangle(x, y, 3, h); + cr->fill(); + } + + if (unread_state < 1) return; + auto *paned = static_cast<Gtk::Paned *>(widget.get_ancestor(Gtk::Paned::get_type())); + if (paned != nullptr) { + const auto edge = std::min(paned->get_position(), cell_area.get_width()); + + unread_render_mentions(cr, widget, unread_state, edge, cell_area); + } } // dm header @@ -466,6 +520,8 @@ void CellRendererChannels::render_vfunc_dmheader(const Cairo::RefPtr<Cairo::Cont cell_area.get_width(), cell_area.get_height()); m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); + if (!Abaddon::Get().GetSettings().Unreads) return; + auto *paned = static_cast<Gtk::Paned *>(widget.get_ancestor(Gtk::Paned::get_type())); if (paned != nullptr) { const auto edge = std::min(paned->get_position(), background_area.get_width()); @@ -537,8 +593,16 @@ void CellRendererChannels::render_vfunc_dm(const Cairo::RefPtr<Cairo::Context> & const auto id = m_property_id.get_value(); const bool is_muted = discord.IsChannelMuted(id); - if (is_muted) - m_renderer_text.property_foreground() = "#7f7f7f"; + static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().ChannelColor); + if (Abaddon::Get().GetDiscordClient().IsChannelMuted(m_property_id.get_value())) { + auto muted = color; + muted.set_red(muted.get_red() * 0.5); + muted.set_green(muted.get_green() * 0.5); + muted.set_blue(muted.get_blue() * 0.5); + m_renderer_text.property_foreground_rgba() = muted; + } else { + m_renderer_text.property_foreground_rgba() = color; + } m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); m_renderer_text.property_foreground_set() = false; @@ -547,12 +611,14 @@ void CellRendererChannels::render_vfunc_dm(const Cairo::RefPtr<Cairo::Context> & cr->fill(); // unread + if (!Abaddon::Get().GetSettings().Unreads) return; const auto unread_state = discord.GetUnreadStateForChannel(id); if (unread_state < 0) return; if (!is_muted) { - cr->set_source_rgb(1.0, 1.0, 1.0); + static const auto color = Gdk::RGBA(Abaddon::Get().GetSettings().UnreadIndicatorColor); + cr->set_source_rgb(color.get_red(), color.get_green(), color.get_blue()); const auto x = background_area.get_x(); const auto y = background_area.get_y(); const auto w = background_area.get_width(); @@ -585,12 +651,15 @@ void CellRendererChannels::unread_render_mentions(const Cairo::RefPtr<Cairo::Con int width, height; layout->get_pixel_size(width, height); { + static const auto bg = Gdk::RGBA(Abaddon::Get().GetSettings().MentionBadgeColor); + static const auto text = Gdk::RGBA(Abaddon::Get().GetSettings().MentionBadgeTextColor); + const auto x = cell_area.get_x() + edge - width - MentionsRightPad; const auto y = cell_area.get_y() + cell_area.get_height() / 2.0 - height / 2.0 - 1; cairo_path_rounded_rect(cr, x - 4, y + 2, width + 8, height, 5); - cr->set_source_rgb(184.0 / 255.0, 37.0 / 255.0, 37.0 / 255.0); + cr->set_source_rgb(bg.get_red(), bg.get_green(), bg.get_blue()); cr->fill(); - cr->set_source_rgb(1.0, 1.0, 1.0); + cr->set_source_rgb(text.get_red(), text.get_green(), text.get_blue()); cr->move_to(x, y); layout->show_in_cairo_context(cr); } diff --git a/src/components/chatmessage.cpp b/src/components/chatmessage.cpp index ef972bb..823aaa6 100644 --- a/src/components/chatmessage.cpp +++ b/src/components/chatmessage.cpp @@ -1,7 +1,7 @@ -#include "chatmessage.hpp" #include "abaddon.hpp" -#include "util.hpp" +#include "chatmessage.hpp" #include "lazyimage.hpp" +#include "util.hpp" #include <unordered_map> constexpr static int EmojiSize = 24; // settings eventually @@ -44,18 +44,9 @@ ChatMessageItemContainer *ChatMessageItemContainer::FromMessage(const Message &d } } - // there should only ever be 1 embed (i think?) - if (data.Embeds.size() == 1) { - const auto &embed = data.Embeds[0]; - if (IsEmbedImageOnly(embed)) { - auto *widget = container->CreateImageComponent(*embed.Thumbnail->ProxyURL, *embed.Thumbnail->URL, *embed.Thumbnail->Width, *embed.Thumbnail->Height); - container->AttachEventHandlers(*widget); - container->m_main.add(*widget); - } else { - container->m_embed_component = container->CreateEmbedComponent(embed); - container->AttachEventHandlers(*container->m_embed_component); - container->m_main.add(*container->m_embed_component); - } + if (!data.Embeds.empty()) { + container->m_embed_component = container->CreateEmbedsComponent(data.Embeds); + container->m_main.add(*container->m_embed_component); } // i dont think attachments can be edited @@ -108,10 +99,11 @@ void ChatMessageItemContainer::UpdateContent() { m_embed_component = nullptr; } - if (data->Embeds.size() == 1) { - m_embed_component = CreateEmbedComponent(data->Embeds[0]); + if (!data->Embeds.empty()) { + m_embed_component = CreateEmbedsComponent(data->Embeds); AttachEventHandlers(*m_embed_component); m_main.add(*m_embed_component); + m_embed_component->show_all(); } } @@ -199,6 +191,7 @@ void ChatMessageItemContainer::UpdateTextComponent(Gtk::TextView *tv) { case MessageType::DEFAULT: case MessageType::INLINE_REPLY: b->insert(s, data->Content); + HandleRoleMentions(b); HandleUserMentions(b); HandleLinks(*tv); HandleChannelMentions(tv); @@ -298,6 +291,24 @@ void ChatMessageItemContainer::UpdateTextComponent(Gtk::TextView *tv) { } } +Gtk::Widget *ChatMessageItemContainer::CreateEmbedsComponent(const std::vector<EmbedData> &embeds) { + auto *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + for (const auto &embed : embeds) { + if (IsEmbedImageOnly(embed)) { + auto *widget = CreateImageComponent(*embed.Thumbnail->ProxyURL, *embed.Thumbnail->URL, *embed.Thumbnail->Width, *embed.Thumbnail->Height); + widget->show(); + AttachEventHandlers(*widget); + box->add(*widget); + } else { + auto *widget = CreateEmbedComponent(embed); + widget->show(); + AttachEventHandlers(*widget); + box->add(*widget); + } + } + return box; +} + Gtk::Widget *ChatMessageItemContainer::CreateEmbedComponent(const EmbedData &embed) { Gtk::EventBox *ev = Gtk::manage(new Gtk::EventBox); ev->set_can_focus(true); @@ -530,6 +541,7 @@ Gtk::Widget *ChatMessageItemContainer::CreateStickersComponent(const std::vector if (sticker.FormatType != StickerFormatType::PNG && sticker.FormatType != StickerFormatType::APNG) continue; auto *ev = Gtk::manage(new Gtk::EventBox); auto *img = Gtk::manage(new LazyImage(sticker.GetURL(), StickerComponentSize, StickerComponentSize, false)); + img->set_halign(Gtk::ALIGN_START); img->set_size_request(StickerComponentSize, StickerComponentSize); // should this go in LazyImage ? img->show(); ev->show(); @@ -732,7 +744,47 @@ bool ChatMessageItemContainer::IsEmbedImageOnly(const EmbedData &data) { return data.Thumbnail->ProxyURL.has_value() && data.Thumbnail->URL.has_value() && data.Thumbnail->Width.has_value() && data.Thumbnail->Height.has_value(); } -void ChatMessageItemContainer::HandleUserMentions(Glib::RefPtr<Gtk::TextBuffer> buf) { +void ChatMessageItemContainer::HandleRoleMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf) { + constexpr static const auto mentions_regex = R"(<@&(\d+)>)"; + + static auto rgx = Glib::Regex::create(mentions_regex); + + Glib::ustring text = GetText(buf); + const auto &discord = Abaddon::Get().GetDiscordClient(); + + int startpos = 0; + Glib::MatchInfo match; + while (rgx->match(text, startpos, match)) { + int mstart, mend; + if (!match.fetch_pos(0, mstart, mend)) break; + const Glib::ustring role_id = match.fetch(1); + const auto role = discord.GetRole(role_id); + if (!role.has_value()) { + startpos = mend; + continue; + } + + Glib::ustring replacement; + if (role->HasColor()) { + replacement = "<b><span color=\"#" + IntToCSSColor(role->Color) + "\">@" + role->GetEscapedName() + "</span></b>"; + } else { + replacement = "<b>@" + role->GetEscapedName() + "</b>"; + } + + const auto chars_start = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mstart); + const auto chars_end = g_utf8_pointer_to_offset(text.c_str(), text.c_str() + mend); + const auto start_it = buf->get_iter_at_offset(chars_start); + const auto end_it = buf->get_iter_at_offset(chars_end); + + auto it = buf->erase(start_it, end_it); + buf->insert_markup(it, replacement); + + text = GetText(buf); + startpos = 0; + } +} + +void ChatMessageItemContainer::HandleUserMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf) { constexpr static const auto mentions_regex = R"(<@!?(\d+)>)"; static auto rgx = Glib::Regex::create(mentions_regex); @@ -1064,11 +1116,11 @@ ChatMessageHeader::ChatMessageHeader(const Message &data) }; img.LoadFromURL(author->GetAvatarURL(data.GuildID), sigc::track_obj(cb, *this)); - if (author->HasAnimatedAvatar()) { + if (author->HasAnimatedAvatar(data.GuildID)) { auto cb = [this](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) { m_anim_avatar = pb; }; - img.LoadAnimationFromURL(author->GetAvatarURL("gif"), AvatarSize, AvatarSize, sigc::track_obj(cb, *this)); + img.LoadAnimationFromURL(author->GetAvatarURL(data.GuildID, "gif"), AvatarSize, AvatarSize, sigc::track_obj(cb, *this)); } get_style_context()->add_class("message-container"); @@ -1079,11 +1131,10 @@ ChatMessageHeader::ChatMessageHeader(const Message &data) m_avatar.set_valign(Gtk::ALIGN_START); m_avatar.set_margin_right(10); - m_author.set_markup(data.Author.GetEscapedBoldName()); m_author.set_single_line_mode(true); m_author.set_line_wrap(false); m_author.set_ellipsize(Pango::ELLIPSIZE_END); - m_author.set_xalign(0.f); + m_author.set_xalign(0.0F); m_author.set_can_focus(false); m_meta_ev.signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageHeader::on_author_button_press)); @@ -1160,30 +1211,32 @@ ChatMessageHeader::ChatMessageHeader(const Message &data) show_all(); auto &discord = Abaddon::Get().GetDiscordClient(); - auto role_update_cb = [this](...) { UpdateNameColor(); }; + auto role_update_cb = [this](...) { UpdateName(); }; discord.signal_role_update().connect(sigc::track_obj(role_update_cb, *this)); - auto guild_member_update_cb = [this](const auto &, const auto &) { UpdateNameColor(); }; + auto guild_member_update_cb = [this](const auto &, const auto &) { UpdateName(); }; discord.signal_guild_member_update().connect(sigc::track_obj(guild_member_update_cb, *this)); - UpdateNameColor(); + UpdateName(); AttachUserMenuHandler(m_meta_ev); AttachUserMenuHandler(m_avatar_ev); } -void ChatMessageHeader::UpdateNameColor() { +void ChatMessageHeader::UpdateName() { const auto &discord = Abaddon::Get().GetDiscordClient(); const auto user = discord.GetUser(UserID); if (!user.has_value()) return; const auto chan = discord.GetChannel(ChannelID); bool is_guild = chan.has_value() && chan->GuildID.has_value(); if (is_guild) { + const auto member = discord.GetMember(UserID, *chan->GuildID); const auto role_id = discord.GetMemberHoistedRole(*chan->GuildID, UserID, true); const auto role = discord.GetRole(role_id); + const auto name = GetEscapedDisplayName(*user, member); std::string md; if (role.has_value()) - m_author.set_markup("<span weight='bold' color='#" + IntToCSSColor(role->Color) + "'>" + user->GetEscapedName() + "</span>"); + m_author.set_markup("<span weight='bold' color='#" + IntToCSSColor(role->Color) + "'>" + name + "</span>"); else - m_author.set_markup("<span weight='bold'>" + user->GetEscapedName() + "</span>"); + m_author.set_markup("<span weight='bold'>" + name + "</span>"); } else m_author.set_markup("<span weight='bold'>" + user->GetEscapedName() + "</span>"); } @@ -1207,6 +1260,13 @@ void ChatMessageHeader::AttachUserMenuHandler(Gtk::Widget &widget) { }); } +Glib::ustring ChatMessageHeader::GetEscapedDisplayName(const UserData &user, const std::optional<GuildMember> &member) { + if (member.has_value() && !member->Nickname.empty()) + return Glib::Markup::escape_text(member->Nickname); + else + return Glib::Markup::escape_text(user.GetEscapedName()); +} + bool ChatMessageHeader::on_author_button_press(GdkEventButton *ev) { if (ev->button == GDK_BUTTON_PRIMARY && (ev->state & GDK_SHIFT_MASK)) { m_signal_action_insert_mention.emit(); diff --git a/src/components/chatmessage.hpp b/src/components/chatmessage.hpp index 8b69117..5e213ee 100644 --- a/src/components/chatmessage.hpp +++ b/src/components/chatmessage.hpp @@ -22,6 +22,7 @@ protected: void AddClickHandler(Gtk::Widget *widget, std::string); Gtk::TextView *CreateTextComponent(const Message &data); // Message.Content void UpdateTextComponent(Gtk::TextView *tv); + Gtk::Widget *CreateEmbedsComponent(const std::vector<EmbedData> &embeds); Gtk::Widget *CreateEmbedComponent(const EmbedData &data); // Message.Embeds[0] Gtk::Widget *CreateImageComponent(const std::string &proxy_url, const std::string &url, int inw, int inh); Gtk::Widget *CreateAttachmentComponent(const AttachmentData &data); // non-image attachments @@ -34,7 +35,8 @@ protected: static bool IsEmbedImageOnly(const EmbedData &data); - void HandleUserMentions(Glib::RefPtr<Gtk::TextBuffer> buf); + void HandleRoleMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf); + void HandleUserMentions(const Glib::RefPtr<Gtk::TextBuffer> &buf); void HandleStockEmojis(Gtk::TextView &tv); void HandleCustomEmojis(Gtk::TextView &tv); void HandleEmojis(Gtk::TextView &tv); @@ -89,11 +91,12 @@ public: ChatMessageHeader(const Message &data); void AddContent(Gtk::Widget *widget, bool prepend); - void UpdateNameColor(); + void UpdateName(); std::vector<Gtk::Widget *> GetChildContent(); protected: void AttachUserMenuHandler(Gtk::Widget &widget); + static Glib::ustring GetEscapedDisplayName(const UserData &user, const std::optional<GuildMember> &member); bool on_author_button_press(GdkEventButton *ev); diff --git a/src/dialogs/token.cpp b/src/dialogs/token.cpp index 85853e8..32fb785 100644 --- a/src/dialogs/token.cpp +++ b/src/dialogs/token.cpp @@ -30,6 +30,8 @@ TokenDialog::TokenDialog(Gtk::Window &parent) m_bbox.pack_start(m_cancel, Gtk::PACK_SHRINK); m_bbox.set_layout(Gtk::BUTTONBOX_END); + m_entry.set_input_purpose(Gtk::INPUT_PURPOSE_PASSWORD); + m_entry.set_visibility(false); m_entry.set_hexpand(true); m_layout.add(m_entry); m_layout.add(m_bbox); diff --git a/src/discord/channel.cpp b/src/discord/channel.cpp index 2f5c3c1..9d47076 100644 --- a/src/discord/channel.cpp +++ b/src/discord/channel.cpp @@ -13,6 +13,8 @@ void from_json(const nlohmann::json &j, ThreadMemberObject &m) { JS_O("user_id", m.UserID); JS_D("join_timestamp", m.JoinTimestamp); JS_D("flags", m.Flags); + JS_O("muted", m.IsMuted); + JS_ON("mute_config", m.MuteConfig); } void from_json(const nlohmann::json &j, ChannelData &m) { @@ -82,6 +84,14 @@ bool ChannelData::IsCategory() const noexcept { return Type == ChannelType::GUILD_CATEGORY; } +bool ChannelData::HasIcon() const noexcept { + return Icon.has_value(); +} + +std::string ChannelData::GetIconURL() const { + return "https://cdn.discordapp.com/channel-icons/" + std::to_string(ID) + "/" + *Icon + ".png"; +} + std::vector<Snowflake> ChannelData::GetChildIDs() const { return Abaddon::Get().GetDiscordClient().GetChildChannelIDs(ID); } diff --git a/src/discord/channel.hpp b/src/discord/channel.hpp index 195a09a..89e43a0 100644 --- a/src/discord/channel.hpp +++ b/src/discord/channel.hpp @@ -49,9 +49,19 @@ struct ThreadMetadataData { friend void from_json(const nlohmann::json &j, ThreadMetadataData &m); }; +struct MuteConfigData { + std::optional<std::string> EndTime; // nullopt is encoded as null + int SelectedTimeWindow; + + friend void from_json(const nlohmann::json &j, MuteConfigData &m); + friend void to_json(nlohmann::json &j, const MuteConfigData &m); +}; + struct ThreadMemberObject { std::optional<Snowflake> ThreadID; std::optional<Snowflake> UserID; + std::optional<bool> IsMuted; + std::optional<MuteConfigData> MuteConfig; std::string JoinTimestamp; int Flags; @@ -89,6 +99,8 @@ struct ChannelData { bool IsThread() const noexcept; bool IsJoinedThread() const; bool IsCategory() const noexcept; + bool HasIcon() const noexcept; + std::string GetIconURL() const; std::vector<Snowflake> GetChildIDs() const; std::optional<PermissionOverwrite> GetOverwrite(Snowflake id) const; std::vector<UserData> GetDMRecipients() const; diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index 5b3cdb5..c11210d 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -983,6 +983,24 @@ void DiscordClient::UnmuteGuild(Snowflake id, sigc::slot<void(DiscordError code) }); } +void DiscordClient::MuteThread(Snowflake id, sigc::slot<void(DiscordError code)> callback) { + m_http.MakePATCH("/channels/" + std::to_string(id) + "/thread-members/@me/settings", R"({"muted":true})", [this, callback](const http::response_type &response) { + if (CheckCode(response)) + callback(DiscordError::NONE); + else + callback(GetCodeFromResponse(response)); + }); +} + +void DiscordClient::UnmuteThread(Snowflake id, sigc::slot<void(DiscordError code)> callback) { + m_http.MakePATCH("/channels/" + std::to_string(id) + "/thread-members/@me/settings", R"({"muted":false})", [this, callback](const http::response_type &response) { + if (CheckCode(response)) + callback(DiscordError::NONE); + else + callback(GetCodeFromResponse(response)); + }); +} + void DiscordClient::FetchPinned(Snowflake id, sigc::slot<void(std::vector<Message>, DiscordError code)> callback) { // return from db if we know the pins have already been requested if (m_channels_pinned_requested.find(id) != m_channels_pinned_requested.end()) { @@ -1206,7 +1224,7 @@ int DiscordClient::GetUnreadDMsCount() const { const auto channels = GetPrivateChannels(); int count = 0; for (const auto channel_id : channels) - if (GetUnreadStateForChannel(channel_id) > -1) count++; + if (!IsChannelMuted(channel_id) && GetUnreadStateForChannel(channel_id) > -1) count++; return count; } @@ -1310,7 +1328,7 @@ void DiscordClient::HandleGatewayMessage(std::string str) { case GatewayOp::InvalidSession: { HandleGatewayInvalidSession(m); } break; - case GatewayOp::Event: { + case GatewayOp::Dispatch: { auto iter = m_event_map.find(m.Type); if (iter == m_event_map.end()) { printf("Unknown event %s\n", m.Type.c_str()); @@ -1446,6 +1464,9 @@ void DiscordClient::HandleGatewayMessage(std::string str) { case GatewayEvent::USER_GUILD_SETTINGS_UPDATE: { HandleGatewayUserGuildSettingsUpdate(m); } break; + case GatewayEvent::GUILD_MEMBERS_CHUNK: { + HandleGatewayGuildMembersChunk(m); + } break; } } break; default: @@ -1921,9 +1942,24 @@ void DiscordClient::HandleGatewayThreadMembersUpdate(const GatewayMessage &msg) void DiscordClient::HandleGatewayThreadMemberUpdate(const GatewayMessage &msg) { ThreadMemberUpdateData data = msg.Data; + if (!data.Member.ThreadID.has_value()) return; + m_joined_threads.insert(*data.Member.ThreadID); if (*data.Member.UserID == GetUserData().ID) m_signal_added_to_thread.emit(*data.Member.ThreadID); + + if (data.Member.IsMuted.has_value()) { + const bool was_muted = IsChannelMuted(*data.Member.ThreadID); + const bool now_muted = *data.Member.IsMuted; + + if (was_muted && !now_muted) { + m_muted_channels.erase(*data.Member.ThreadID); + m_signal_channel_unmuted.emit(*data.Member.ThreadID); + } else if (!was_muted && now_muted) { + m_muted_channels.insert(*data.Member.ThreadID); + m_signal_channel_muted.emit(*data.Member.ThreadID); + } + } } void DiscordClient::HandleGatewayThreadUpdate(const GatewayMessage &msg) { @@ -2016,6 +2052,14 @@ void DiscordClient::HandleGatewayUserGuildSettingsUpdate(const GatewayMessage &m } } +void DiscordClient::HandleGatewayGuildMembersChunk(const GatewayMessage &msg) { + GuildMembersChunkData data = msg.Data; + m_store.BeginTransaction(); + for (const auto &member : data.Members) + m_store.SetGuildMember(data.GuildID, member.User->ID, member); + m_store.EndTransaction(); +} + void DiscordClient::HandleGatewayReadySupplemental(const GatewayMessage &msg) { ReadySupplementalData data = msg.Data; for (const auto &p : data.MergedPresences.Friends) { @@ -2343,10 +2387,14 @@ void DiscordClient::StoreMessageData(Message &msg) { // here the absence of an entry in m_unread indicates a read channel and the value is only the mention count since the message doesnt matter // no entry.id cannot be a guild even though sometimes it looks like it void DiscordClient::HandleReadyReadState(const ReadyEventData &data) { - for (const auto &guild : data.Guilds) + for (const auto &guild : data.Guilds) { for (const auto &channel : *guild.Channels) if (channel.LastMessageID.has_value()) m_last_message_id[channel.ID] = *channel.LastMessageID; + for (const auto &thread : *guild.Threads) + if (thread.LastMessageID.has_value()) + m_last_message_id[thread.ID] = *thread.LastMessageID; + } for (const auto &channel : data.PrivateChannels) if (channel.LastMessageID.has_value()) m_last_message_id[channel.ID] = *channel.LastMessageID; @@ -2385,10 +2433,14 @@ void DiscordClient::HandleReadyGuildSettings(const ReadyEventData &data) { // i dont like this implementation for muted categories but its rather simple and doesnt use a horriiible amount of ram std::unordered_map<Snowflake, std::vector<Snowflake>> category_children; - for (const auto &guild : data.Guilds) + for (const auto &guild : data.Guilds) { for (const auto &channel : *guild.Channels) if (channel.ParentID.has_value() && !channel.IsThread()) category_children[*channel.ParentID].push_back(channel.ID); + for (const auto &thread : *guild.Threads) + if (thread.ThreadMember.has_value() && thread.ThreadMember->IsMuted.has_value() && *thread.ThreadMember->IsMuted) + m_muted_channels.insert(thread.ID); + } const auto now = Snowflake::FromNow(); for (const auto &entry : data.GuildSettings.Entries) { @@ -2470,6 +2522,7 @@ void DiscordClient::LoadEventMap() { m_event_map["THREAD_MEMBER_LIST_UPDATE"] = GatewayEvent::THREAD_MEMBER_LIST_UPDATE; m_event_map["MESSAGE_ACK"] = GatewayEvent::MESSAGE_ACK; m_event_map["USER_GUILD_SETTINGS_UPDATE"] = GatewayEvent::USER_GUILD_SETTINGS_UPDATE; + m_event_map["GUILD_MEMBERS_CHUNK"] = GatewayEvent::GUILD_MEMBERS_CHUNK; } DiscordClient::type_signal_gateway_ready DiscordClient::signal_gateway_ready() { @@ -2636,6 +2689,10 @@ DiscordClient::type_signal_message_ack DiscordClient::signal_message_ack() { return m_signal_message_ack; } +DiscordClient::type_signal_guild_members_chunk DiscordClient::signal_guild_members_chunk() { + return m_signal_guild_members_chunk; +} + DiscordClient::type_signal_added_to_thread DiscordClient::signal_added_to_thread() { return m_signal_added_to_thread; } diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index 1a6aa14..6add18f 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -84,6 +84,17 @@ public: void GetArchivedPrivateThreads(Snowflake channel_id, sigc::slot<void(DiscordError, const ArchivedThreadsResponseData &)> callback); std::vector<Snowflake> GetChildChannelIDs(Snowflake parent_id) const; + // get ids of given list of members for who we do not have the member data + template<typename Iter> + std::unordered_set<Snowflake> FilterUnknownMembersFrom(Snowflake guild_id, Iter begin, Iter end) { + std::unordered_set<Snowflake> ret; + const auto known = m_store.GetMembersInGuild(guild_id); + for (auto iter = begin; iter != end; iter++) + if (known.find(*iter) == known.end()) + ret.insert(*iter); + return ret; + } + bool IsThreadJoined(Snowflake thread_id) const; bool HasGuildPermission(Snowflake user_id, Snowflake guild_id, Permission perm) const; @@ -146,10 +157,24 @@ public: void MarkAllAsRead(sigc::slot<void(DiscordError code)> callback); void MuteGuild(Snowflake id, sigc::slot<void(DiscordError code)> callback); void UnmuteGuild(Snowflake id, sigc::slot<void(DiscordError code)> callback); + void MuteThread(Snowflake id, sigc::slot<void(DiscordError code)> callback); + void UnmuteThread(Snowflake id, sigc::slot<void(DiscordError code)> callback); bool CanModifyRole(Snowflake guild_id, Snowflake role_id) const; bool CanModifyRole(Snowflake guild_id, Snowflake role_id, Snowflake user_id) const; + // send op 8 to get member data for unknown members + template<typename Iter> + void RequestMembers(Snowflake guild_id, Iter begin, Iter end) { + if (std::distance(begin, end) == 0) return; + + RequestGuildMembersMessage obj; + obj.GuildID = guild_id; + obj.Presences = false; + obj.UserIDs = { begin, end }; + m_websocket.Send(obj); + } + // real client doesn't seem to use the single role endpoints so neither do we template<typename Iter> auto SetMemberRoles(Snowflake guild_id, Snowflake user_id, Iter begin, Iter end, sigc::slot<void(DiscordError code)> callback) { @@ -260,6 +285,7 @@ private: void HandleGatewayThreadMemberListUpdate(const GatewayMessage &msg); void HandleGatewayMessageAck(const GatewayMessage &msg); void HandleGatewayUserGuildSettingsUpdate(const GatewayMessage &msg); + void HandleGatewayGuildMembersChunk(const GatewayMessage &msg); void HandleGatewayReadySupplemental(const GatewayMessage &msg); void HandleGatewayReconnect(const GatewayMessage &msg); void HandleGatewayInvalidSession(const GatewayMessage &msg); @@ -370,6 +396,7 @@ public: typedef sigc::signal<void, ThreadUpdateData> type_signal_thread_update; typedef sigc::signal<void, ThreadMemberListUpdateData> type_signal_thread_member_list_update; typedef sigc::signal<void, MessageAckData> type_signal_message_ack; + typedef sigc::signal<void, GuildMembersChunkData> type_signal_guild_members_chunk; // not discord dispatch events typedef sigc::signal<void, Snowflake> type_signal_added_to_thread; @@ -425,6 +452,7 @@ public: type_signal_thread_update signal_thread_update(); type_signal_thread_member_list_update signal_thread_member_list_update(); type_signal_message_ack signal_message_ack(); + type_signal_guild_members_chunk signal_guild_members_chunk(); type_signal_added_to_thread signal_added_to_thread(); type_signal_removed_from_thread signal_removed_from_thread(); @@ -477,6 +505,7 @@ protected: type_signal_thread_update m_signal_thread_update; type_signal_thread_member_list_update m_signal_thread_member_list_update; type_signal_message_ack m_signal_message_ack; + type_signal_guild_members_chunk m_signal_guild_members_chunk; type_signal_removed_from_thread m_signal_removed_from_thread; type_signal_added_to_thread m_signal_added_to_thread; diff --git a/src/discord/objects.cpp b/src/discord/objects.cpp index 8ca8c0f..3c9f770 100644 --- a/src/discord/objects.cpp +++ b/src/discord/objects.cpp @@ -77,7 +77,7 @@ void from_json(const nlohmann::json &j, GuildMemberListUpdateMessage &m) { } void to_json(nlohmann::json &j, const LazyLoadRequestMessage &m) { - j["op"] = GatewayOp::LazyLoadRequest; + j["op"] = GatewayOp::GuildSubscriptions; j["d"] = nlohmann::json::object(); j["d"]["guild_id"] = m.GuildID; if (m.Channels.has_value()) { @@ -98,7 +98,7 @@ void to_json(nlohmann::json &j, const LazyLoadRequestMessage &m) { } void to_json(nlohmann::json &j, const UpdateStatusMessage &m) { - j["op"] = GatewayOp::UpdateStatus; + j["op"] = GatewayOp::PresenceUpdate; j["d"] = nlohmann::json::object(); j["d"]["since"] = m.Since; j["d"]["activities"] = m.Activities; @@ -119,6 +119,14 @@ void to_json(nlohmann::json &j, const UpdateStatusMessage &m) { } } +void to_json(nlohmann::json &j, const RequestGuildMembersMessage &m) { + j["op"] = GatewayOp::RequestGuildMembers; + j["d"] = nlohmann::json::object(); + j["d"]["guild_id"] = m.GuildID; + j["d"]["presences"] = m.Presences; + j["d"]["user_ids"] = m.UserIDs; +} + void from_json(const nlohmann::json &j, ReadStateEntry &m) { JS_ON("mention_count", m.MentionCount); JS_ON("last_message_id", m.LastMessageID); @@ -154,7 +162,7 @@ void to_json(nlohmann::json &j, const UserGuildSettingsChannelOverride &m) { void from_json(const nlohmann::json &j, MuteConfigData &m) { JS_ON("end_time", m.EndTime); - JS_D("selected_time_window", m.SelectedTimeWindow); + JS_ON("selected_time_window", m.SelectedTimeWindow); } void to_json(nlohmann::json &j, const MuteConfigData &m) { @@ -626,3 +634,8 @@ void to_json(nlohmann::json &j, const AckBulkData &m) { void from_json(const nlohmann::json &j, UserGuildSettingsUpdateData &m) { m.Settings = j; } + +void from_json(const nlohmann::json &j, GuildMembersChunkData &m) { + JS_D("members", m.Members); + JS_D("guild_id", m.GuildID); +} diff --git a/src/discord/objects.hpp b/src/discord/objects.hpp index c72361b..27542cc 100644 --- a/src/discord/objects.hpp +++ b/src/discord/objects.hpp @@ -24,16 +24,35 @@ // most stuff below should just be objects that get processed and thrown away immediately enum class GatewayOp : int { - Event = 0, + Dispatch = 0, Heartbeat = 1, Identify = 2, - UpdateStatus = 3, + PresenceUpdate = 3, + VoiceStateUpdate = 4, + VoiceServerPing = 5, Resume = 6, Reconnect = 7, + RequestGuildMembers = 8, InvalidSession = 9, Hello = 10, HeartbeatAck = 11, - LazyLoadRequest = 14, + // 12 unused + CallConnect = 13, + GuildSubscriptions = 14, + LobbyConnect = 15, + LobbyDisconnect = 16, + LobbyVoiceStatesUpdate = 17, + StreamCreate = 18, + StreamDelete = 19, + StreamWatch = 20, + StreamPing = 21, + StreamSetPaused = 22, + // 23 unused + RequestGuildApplicationCommands = 24, + EmbeddedActivityLaunch = 25, + EmbeddedActivityClose = 26, + EmbeddedActivityUpdate = 27, + RequestForumUnreads = 28, }; enum class GatewayEvent : int { @@ -80,6 +99,7 @@ enum class GatewayEvent : int { THREAD_MEMBER_LIST_UPDATE, MESSAGE_ACK, USER_GUILD_SETTINGS_UPDATE, + GUILD_MEMBERS_CHUNK, }; enum class GatewayCloseCode : uint16_t { @@ -226,6 +246,14 @@ struct UpdateStatusMessage { friend void to_json(nlohmann::json &j, const UpdateStatusMessage &m); }; +struct RequestGuildMembersMessage { + Snowflake GuildID; + bool Presences; + std::vector<Snowflake> UserIDs; + + friend void to_json(nlohmann::json &j, const RequestGuildMembersMessage &m); +}; + struct ReadStateEntry { int MentionCount; Snowflake LastMessageID; @@ -244,14 +272,6 @@ struct ReadStateData { friend void from_json(const nlohmann::json &j, ReadStateData &m); }; -struct MuteConfigData { - std::optional<std::string> EndTime; // nullopt is encoded as null - int SelectedTimeWindow; - - friend void from_json(const nlohmann::json &j, MuteConfigData &m); - friend void to_json(nlohmann::json &j, const MuteConfigData &m); -}; - struct UserGuildSettingsChannelOverride { bool Muted; MuteConfigData MuteConfig; @@ -830,3 +850,16 @@ struct UserGuildSettingsUpdateData { friend void from_json(const nlohmann::json &j, UserGuildSettingsUpdateData &m); }; + +struct GuildMembersChunkData { + /* + not needed so not deserialized + int ChunkCount; + int ChunkIndex; + std::vector<?> NotFound; + */ + Snowflake GuildID; + std::vector<GuildMember> Members; + + friend void from_json(const nlohmann::json &j, GuildMembersChunkData &m); +}; diff --git a/src/discord/role.cpp b/src/discord/role.cpp index 07a912e..8a9ed50 100644 --- a/src/discord/role.cpp +++ b/src/discord/role.cpp @@ -12,3 +12,11 @@ void from_json(const nlohmann::json &j, RoleData &m) { JS_D("managed", m.IsManaged); JS_D("mentionable", m.IsMentionable); } + +bool RoleData::HasColor() const noexcept { + return Color != 0; +} + +Glib::ustring RoleData::GetEscapedName() const { + return Glib::Markup::escape_text(Name); +} diff --git a/src/discord/role.hpp b/src/discord/role.hpp index f638b65..a526f4e 100644 --- a/src/discord/role.hpp +++ b/src/discord/role.hpp @@ -16,5 +16,8 @@ struct RoleData { bool IsManaged; bool IsMentionable; + bool HasColor() const noexcept; + Glib::ustring GetEscapedName() const; + friend void from_json(const nlohmann::json &j, RoleData &m); }; diff --git a/src/discord/store.cpp b/src/discord/store.cpp index 6a3d636..ede6364 100644 --- a/src/discord/store.cpp +++ b/src/discord/store.cpp @@ -13,18 +13,6 @@ Store::Store(bool mem_store) return; } - m_db.Execute(R"( - PRAGMA writable_schema = 1; - DELETE FROM sqlite_master WHERE TYPE IN ("view", "table", "index", "trigger"); - PRAGMA writable_schema = 0; - VACUUM; - PRAGMA integrity_check; - )"); - if (!m_db.OK()) { - fprintf(stderr, "failed to clear database: %s\n", m_db.ErrStr()); - return; - } - if (m_db.Execute("PRAGMA journal_mode = WAL") != SQLITE_OK) { fprintf(stderr, "enabling write-ahead-log failed: %s\n", m_db.ErrStr()); return; @@ -588,6 +576,23 @@ std::vector<Snowflake> Store::GetChannelIDsWithParentID(Snowflake channel_id) co return ret; } +std::unordered_set<Snowflake> Store::GetMembersInGuild(Snowflake guild_id) const { + auto &s = m_stmt_get_guild_member_ids; + + s->Bind(1, guild_id); + + std::unordered_set<Snowflake> ret; + while (s->FetchOne()) { + Snowflake x; + s->Get(0, x); + ret.insert(x); + } + + s->Reset(); + + return ret; +} + void Store::AddReaction(const MessageReactionAddObject &data, bool byself) { auto &s = m_stmt_add_reaction; @@ -646,6 +651,7 @@ std::optional<ChannelData> Store::GetChannel(Snowflake id) const { s->Get(6, r.IsNSFW); s->Get(7, r.LastMessageID); s->Get(10, r.RateLimitPerUser); + s->Get(11, r.Icon); s->Get(12, r.OwnerID); s->Get(14, r.ParentID); if (!s->IsNull(16)) { @@ -2145,10 +2151,25 @@ bool Store::CreateStatements() { return false; } + m_stmt_get_guild_member_ids = std::make_unique<Statement>(m_db, R"( + SELECT user_id FROM members WHERE guild_id = ? + )"); + if (!m_stmt_get_guild_member_ids->OK()) { + fprintf(stderr, "failed to prepare get guild member ids statement: %s\n", m_db.ErrStr()); + return false; + } + return true; } Store::Database::Database(const char *path) { + if (path != ":memory:"s) { + std::error_code ec; + if (std::filesystem::exists(path, ec) && !std::filesystem::remove(path, ec)) { + fprintf(stderr, "the database could not be removed. the database may be corrupted as a result\n"); + } + } + m_err = sqlite3_open(path, &m_db); } diff --git a/src/discord/store.hpp b/src/discord/store.hpp index 4320807..a1e5f81 100644 --- a/src/discord/store.hpp +++ b/src/discord/store.hpp @@ -44,6 +44,8 @@ public: std::vector<Message> GetPinnedMessages(Snowflake channel_id) const; std::vector<ChannelData> GetActiveThreads(Snowflake channel_id) const; // public std::vector<Snowflake> GetChannelIDsWithParentID(Snowflake channel_id) const; + std::unordered_set<Snowflake> GetMembersInGuild(Snowflake guild_id) const; + // ^ not the same as GetUsersInGuild since users in a guild may include users who do not have retrieved member data void AddReaction(const MessageReactionAddObject &data, bool byself); void RemoveReaction(const MessageReactionRemoveObject &data, bool byself); @@ -302,5 +304,6 @@ private: STMT(sub_reaction); STMT(get_reactions); STMT(get_chan_ids_parent); + STMT(get_guild_member_ids); #undef STMT }; diff --git a/src/discord/user.cpp b/src/discord/user.cpp index fae212d..c2e6069 100644 --- a/src/discord/user.cpp +++ b/src/discord/user.cpp @@ -6,22 +6,41 @@ bool UserData::IsDeleted() const { } bool UserData::HasAvatar() const { - return Avatar.size() > 0; + return !Avatar.empty(); } -bool UserData::HasAnimatedAvatar() const { - return Avatar.size() > 0 && Avatar[0] == 'a' && Avatar[1] == '_'; +bool UserData::HasAnimatedAvatar() const noexcept { + return !Avatar.empty() && Avatar[0] == 'a' && Avatar[1] == '_'; +} + +bool UserData::HasAnimatedAvatar(Snowflake guild_id) const { + const auto member = Abaddon::Get().GetDiscordClient().GetMember(ID, guild_id); + if (member.has_value() && member->Avatar.has_value() && member->Avatar.value()[0] == 'a' && member->Avatar.value()[1] == '_') + return true; + else if (member.has_value() && !member->Avatar.has_value()) + return HasAnimatedAvatar(); + return false; +} + +bool UserData::HasAnimatedAvatar(const std::optional<Snowflake> &guild_id) const { + if (guild_id.has_value()) + return HasAnimatedAvatar(*guild_id); + else + return HasAnimatedAvatar(); } std::string UserData::GetAvatarURL(Snowflake guild_id, std::string ext, std::string size) const { const auto member = Abaddon::Get().GetDiscordClient().GetMember(ID, guild_id); - if (member.has_value() && member->Avatar.has_value()) + if (member.has_value() && member->Avatar.has_value()) { + if (ext == "gif" && !(member->Avatar.value()[0] == 'a' && member->Avatar.value()[1] == '_')) + return GetAvatarURL(ext, size); return "https://cdn.discordapp.com/guilds/" + std::to_string(guild_id) + "/users/" + std::to_string(ID) + "/avatars/" + *member->Avatar + "." + ext + "?" + "size=" + size; - else + } else { return GetAvatarURL(ext, size); + } } std::string UserData::GetAvatarURL(const std::optional<Snowflake> &guild_id, std::string ext, std::string size) const { diff --git a/src/discord/user.hpp b/src/discord/user.hpp index d4711fa..c058ea1 100644 --- a/src/discord/user.hpp +++ b/src/discord/user.hpp @@ -62,7 +62,9 @@ struct UserData { bool IsDeleted() const; bool HasAvatar() const; - bool HasAnimatedAvatar() const; + bool HasAnimatedAvatar() const noexcept; + bool HasAnimatedAvatar(Snowflake guild_id) const; + bool HasAnimatedAvatar(const std::optional<Snowflake> &guild_id) const; std::string GetAvatarURL(Snowflake guild_id, std::string ext = "png", std::string size = "32") const; std::string GetAvatarURL(const std::optional<Snowflake> &guild_id, std::string ext = "png", std::string size = "32") const; std::string GetAvatarURL(std::string ext = "png", std::string size = "32") const; diff --git a/src/settings.cpp b/src/settings.cpp index 6820ed0..242bd7c 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -47,11 +47,16 @@ void SettingsManager::ReadSettings() { SMBOOL("gui", "owner_crown", ShowOwnerCrown); SMBOOL("gui", "save_state", SaveState); SMBOOL("gui", "stock_emojis", ShowStockEmojis); + SMBOOL("gui", "unreads", Unreads); SMINT("http", "concurrent", CacheHTTPConcurrency); SMSTR("http", "user_agent", UserAgent); SMSTR("style", "expandercolor", ChannelsExpanderColor); SMSTR("style", "linkcolor", LinkColor); SMSTR("style", "nsfwchannelcolor", NSFWChannelColor); + SMSTR("style", "channelcolor", ChannelColor); + SMSTR("style", "mentionbadgecolor", MentionBadgeColor); + SMSTR("style", "mentionbadgetextcolor", MentionBadgeTextColor); + SMSTR("style", "unreadcolor", UnreadIndicatorColor); #undef SMBOOL #undef SMSTR @@ -95,11 +100,16 @@ void SettingsManager::Close() { SMBOOL("gui", "owner_crown", ShowOwnerCrown); SMBOOL("gui", "save_state", SaveState); SMBOOL("gui", "stock_emojis", ShowStockEmojis); + SMBOOL("gui", "unreads", Unreads); SMINT("http", "concurrent", CacheHTTPConcurrency); SMSTR("http", "user_agent", UserAgent); SMSTR("style", "expandercolor", ChannelsExpanderColor); SMSTR("style", "linkcolor", LinkColor); SMSTR("style", "nsfwchannelcolor", NSFWChannelColor); + SMSTR("style", "channelcolor", ChannelColor); + SMSTR("style", "mentionbadgecolor", MentionBadgeColor); + SMSTR("style", "mentionbadgetextcolor", MentionBadgeTextColor); + SMSTR("style", "unreadcolor", UnreadIndicatorColor); #undef SMSTR #undef SMBOOL diff --git a/src/settings.hpp b/src/settings.hpp index ca2303f..52b20b9 100644 --- a/src/settings.hpp +++ b/src/settings.hpp @@ -26,16 +26,21 @@ public: #else bool ShowStockEmojis { true }; #endif + bool Unreads { true }; // [http] int CacheHTTPConcurrency { 20 }; std::string UserAgent { "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36" }; // [style] - // TODO: convert to StyleProperty + // TODO: convert to StyleProperty... or maybe not? i still cant figure out what the "correct" method is for this std::string LinkColor { "rgba(40, 200, 180, 255)" }; std::string ChannelsExpanderColor { "rgba(255, 83, 112, 255)" }; std::string NSFWChannelColor { "#ed6666" }; + std::string ChannelColor { "#fbfbfb" }; + std::string MentionBadgeColor { "#b82525" }; + std::string MentionBadgeTextColor { "#fbfbfb" }; + std::string UnreadIndicatorColor { "#ffffff" }; }; SettingsManager(const std::string &filename); |