diff --git a/README.md b/README.md index 41f415a5..cbbe341b 100644 --- a/README.md +++ b/README.md @@ -291,23 +291,25 @@ For example, memory_db would be set by adding `memory_db = true` under the line #### gui -| Setting | Type | Default | Description | -|-----------------------------|---------|---------|----------------------------------------------------------------------------------------------------------------------------| -| `member_list_discriminator` | boolean | true | show user discriminators in the member list | -| `stock_emojis` | boolean | true | allow abaddon to substitute unicode emojis with images from emojis.bin, must be false to allow GTK to render emojis itself | -| `custom_emojis` | boolean | true | download and use custom Discord emojis | -| `css` | string | | path to the main CSS file | -| `animations` | boolean | true | use animated images where available (e.g. server icons, emojis, avatars). false means static images will be used | -| `animated_guild_hover_only` | boolean | true | only animate guild icons when the guild is being hovered over | -| `owner_crown` | boolean | true | show a crown next to the owner | -| `unreads` | boolean | true | show unread indicators and mention badges | -| `save_state` | boolean | true | save the state of the gui (active channels, tabs, expanded channels) | -| `alt_menu` | boolean | false | keep the menu hidden unless revealed with alt key | -| `hide_to_tray` | boolean | false | hide abaddon to the system tray on window close | -| `show_deleted_indicator` | boolean | true | show \[deleted\] indicator next to deleted messages instead of actually deleting the message | -| `font_scale` | double | | scale font rendering. 1 is unchanged | -| `image_embed_clamp_width` | int | 400 | maximum width of image embeds | -| `image_embed_clamp_height` | int | 300 | maximum height of image embeds | +| Setting | Type | Default | Description | +|--------------------------------|---------|---------|----------------------------------------------------------------------------------------------------------------------------| +| `member_list_discriminator` | boolean | true | show user discriminators in the member list | +| `stock_emojis` | boolean | true | allow abaddon to substitute unicode emojis with images from emojis.bin, must be false to allow GTK to render emojis itself | +| `custom_emojis` | boolean | true | download and use custom Discord emojis | +| `css` | string | | path to the main CSS file | +| `animations` | boolean | true | use animated images where available (e.g. server icons, emojis, avatars). false means static images will be used | +| `animated_guild_hover_only` | boolean | true | only animate guild icons when the guild is being hovered over | +| `owner_crown` | boolean | true | show a crown next to the owner | +| `unreads` | boolean | true | show unread indicators and mention badges | +| `save_state` | boolean | true | save the state of the gui (active channels, tabs, expanded channels) | +| `alt_menu` | boolean | false | keep the menu hidden unless revealed with alt key | +| `hide_to_tray` | boolean | false | hide abaddon to the system tray on window close | +| `show_deleted_indicator` | boolean | true | show \[deleted\] indicator next to deleted messages instead of actually deleting the message | +| `font_scale` | double | | scale font rendering. 1 is unchanged | +| `image_embed_clamp_width` | int | 400 | maximum width of image embeds | +| `image_embed_clamp_height` | int | 300 | maximum height of image embeds | +| `classic_channels` | boolean | false | use classic Discord-style interface for server/channel listing | +| `classic_change_guild_on_open` | boolean | true | change displayed guild when selecting a channel (classic channel list) | #### style diff --git a/ci/used-icons.txt b/ci/used-icons.txt index 0ea18ae4..870c05e9 100644 --- a/ci/used-icons.txt +++ b/ci/used-icons.txt @@ -3,3 +3,5 @@ actions/call-stop-symbolic status/microphone-disabled-symbolic status/audio-volume-muted-symbolic devices/camera-web-symbolic +status/user-available-symbolic +places/folder-symbolic diff --git a/res/css/main.css b/res/css/main.css index ae7bf609..db548ac1 100644 --- a/res/css/main.css +++ b/res/css/main.css @@ -145,3 +145,37 @@ .message-text.failed { color: red; } + +.guild-list-scroll > scrollbar { + border: none; +} + +.guild-list-scroll > scrollbar slider { + border: none; + margin: 0px; + min-width: 0px; + min-height: 0px; +} + +.channel-list .view:selected { + background-color: @theme_selected_bg_color; +} + +.classic-guild-list > row { + padding-left: 0px; + box-shadow: none; + border: none; + outline: none; +} + +.classic-guild-list > row:hover { + background: none; +} + +.classic-guild-list-guild.has-unread { + background: radial-gradient(7px circle at left, @theme_selected_bg_color 50%, transparent 20%); +} + +.classic-guild-list-guild box, .classic-guild-list-folder stack { + padding-left: 10px; +} diff --git a/src/abaddon.cpp b/src/abaddon.cpp index f8c4b397..045b8a72 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -54,7 +54,10 @@ Abaddon::Abaddon() : m_settings(Platform::FindConfigFile()) , m_discord(GetSettings().UseMemoryDB) // stupid but easy , m_emojis(GetResPath("/emojis.bin")) - , m_audio(GetSettings().Backends) { +#ifdef WITH_VOICE + , m_audio(GetSettings().Backends) +#endif +{ LoadFromSettings(); // todo: set user agent for non-client(?) diff --git a/src/components/channelscellrenderer.cpp b/src/components/channellist/cellrendererchannels.cpp similarity index 99% rename from src/components/channelscellrenderer.cpp rename to src/components/channellist/cellrendererchannels.cpp index 35494aba..b0492528 100644 --- a/src/components/channelscellrenderer.cpp +++ b/src/components/channellist/cellrendererchannels.cpp @@ -1,4 +1,4 @@ -#include "channelscellrenderer.hpp" +#include "cellrendererchannels.hpp" #include #include "abaddon.hpp" diff --git a/src/components/channelscellrenderer.hpp b/src/components/channellist/cellrendererchannels.hpp similarity index 100% rename from src/components/channelscellrenderer.hpp rename to src/components/channellist/cellrendererchannels.hpp diff --git a/src/components/channellist/channellist.cpp b/src/components/channellist/channellist.cpp new file mode 100644 index 00000000..f4095920 --- /dev/null +++ b/src/components/channellist/channellist.cpp @@ -0,0 +1,131 @@ +#include "channellist.hpp" + +#include "abaddon.hpp" + +ChannelList::ChannelList() { + get_style_context()->add_class("channel-browser-pane"); + + ConnectSignals(); + + m_guilds.set_halign(Gtk::ALIGN_START); + + m_guilds_scroll.get_style_context()->add_class("guild-list-scroll"); + m_guilds_scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + + m_guilds.signal_guild_selected().connect([this](Snowflake guild_id) { + m_tree.SetSelectedGuild(guild_id); + }); + + m_guilds.signal_dms_selected().connect([this]() { + m_tree.SetSelectedDMs(); + }); + + m_guilds.show(); + m_tree.show(); + m_guilds_scroll.add(m_guilds); + pack_start(m_guilds_scroll, false, false); // only take the space it needs + pack_start(m_tree, true, true); // use all the remaining space +} + +void ChannelList::UpdateListing() { + m_tree.UpdateListing(); + if (m_is_classic) m_guilds.UpdateListing(); +} + +void ChannelList::SetActiveChannel(Snowflake id, bool expand_to) { + if (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 { + m_tree.SetSelectedDMs(); + } + } + + m_tree.SetActiveChannel(id, expand_to); +} + +void ChannelList::UseExpansionState(const ExpansionStateRoot &state) { + m_tree.UseExpansionState(state); +} + +ExpansionStateRoot ChannelList::GetExpansionState() const { + return m_tree.GetExpansionState(); +} + +void ChannelList::UsePanedHack(Gtk::Paned &paned) { + m_tree.UsePanedHack(paned); +} + +void ChannelList::SetClassic(bool value) { + m_is_classic = value; + m_tree.SetClassic(value); + m_guilds_scroll.set_visible(value); +} + +void ChannelList::ConnectSignals() { + // TODO: if these all just travel upwards to the singleton then get rid of them but mayeb they dont + +#ifdef WITH_LIBHANDY + m_tree.signal_action_open_new_tab().connect([this](Snowflake id) { + m_signal_action_open_new_tab.emit(id); + }); +#endif + +#ifdef WITH_VOICE + m_tree.signal_action_join_voice_channel().connect([this](Snowflake id) { + m_signal_action_join_voice_channel.emit(id); + }); + + m_tree.signal_action_disconnect_voice().connect([this]() { + m_signal_action_disconnect_voice.emit(); + }); +#endif + + m_tree.signal_action_channel_item_select().connect([this](Snowflake id) { + m_signal_action_channel_item_select.emit(id); + }); + + m_tree.signal_action_guild_leave().connect([this](Snowflake id) { + m_signal_action_guild_leave.emit(id); + }); + + m_tree.signal_action_guild_settings().connect([this](Snowflake id) { + m_signal_action_guild_settings.emit(id); + }); + + m_guilds.signal_action_guild_leave().connect([this](Snowflake id) { + m_signal_action_guild_leave.emit(id); + }); + + m_guilds.signal_action_guild_settings().connect([this](Snowflake id) { + m_signal_action_guild_settings.emit(id); + }); +} + +#ifdef WITH_LIBHANDY +ChannelList::type_signal_action_open_new_tab ChannelList::signal_action_open_new_tab() { + return m_signal_action_open_new_tab; +} +#endif + +#ifdef WITH_VOICE +ChannelList::type_signal_action_join_voice_channel ChannelList::signal_action_join_voice_channel() { + return m_signal_action_join_voice_channel; +} + +ChannelList::type_signal_action_disconnect_voice ChannelList::signal_action_disconnect_voice() { + return m_signal_action_disconnect_voice; +} +#endif + +ChannelList::type_signal_action_channel_item_select ChannelList::signal_action_channel_item_select() { + return m_signal_action_channel_item_select; +} + +ChannelList::type_signal_action_guild_leave ChannelList::signal_action_guild_leave() { + return m_signal_action_guild_leave; +} + +ChannelList::type_signal_action_guild_settings ChannelList::signal_action_guild_settings() { + return m_signal_action_guild_settings; +} diff --git a/src/components/channellist/channellist.hpp b/src/components/channellist/channellist.hpp new file mode 100644 index 00000000..78d6372f --- /dev/null +++ b/src/components/channellist/channellist.hpp @@ -0,0 +1,71 @@ +#pragma once +#include +#include +#include "channellisttree.hpp" +#include "classic/guildlist.hpp" +#include "discord/snowflake.hpp" +#include "state.hpp" + +// Contains the actual ChannelListTree and the classic listing if enabled +class ChannelList : public Gtk::HBox { + // have to proxy public and signals to underlying tree... ew!!! +public: + ChannelList(); + + void UpdateListing(); + void SetActiveChannel(Snowflake id, bool expand_to); + + // channel list should be populated when this is called + void UseExpansionState(const ExpansionStateRoot &state); + ExpansionStateRoot GetExpansionState() const; + + void UsePanedHack(Gtk::Paned &paned); + + void SetClassic(bool value); + +private: + void ConnectSignals(); + + ChannelListTree m_tree; + + Gtk::ScrolledWindow m_guilds_scroll; + GuildList m_guilds; + + bool m_is_classic = false; + +public: + using type_signal_action_channel_item_select = sigc::signal; + using type_signal_action_guild_leave = sigc::signal; + using type_signal_action_guild_settings = sigc::signal; + +#ifdef WITH_LIBHANDY + using type_signal_action_open_new_tab = sigc::signal; + type_signal_action_open_new_tab signal_action_open_new_tab(); +#endif + +#ifdef WITH_VOICE + using type_signal_action_join_voice_channel = sigc::signal; + using type_signal_action_disconnect_voice = sigc::signal; + + type_signal_action_join_voice_channel signal_action_join_voice_channel(); + type_signal_action_disconnect_voice signal_action_disconnect_voice(); +#endif + + type_signal_action_channel_item_select signal_action_channel_item_select(); + type_signal_action_guild_leave signal_action_guild_leave(); + type_signal_action_guild_settings signal_action_guild_settings(); + +private: + type_signal_action_channel_item_select m_signal_action_channel_item_select; + type_signal_action_guild_leave m_signal_action_guild_leave; + type_signal_action_guild_settings m_signal_action_guild_settings; + +#ifdef WITH_LIBHANDY + type_signal_action_open_new_tab m_signal_action_open_new_tab; +#endif + +#ifdef WITH_VOICE + type_signal_action_join_voice_channel m_signal_action_join_voice_channel; + type_signal_action_disconnect_voice m_signal_action_disconnect_voice; +#endif +}; diff --git a/src/components/channels.cpp b/src/components/channellist/channellisttree.cpp similarity index 79% rename from src/components/channels.cpp rename to src/components/channellist/channellisttree.cpp index cafb8e0c..4816b423 100644 --- a/src/components/channels.cpp +++ b/src/components/channellist/channellisttree.cpp @@ -1,14 +1,20 @@ -#include "channels.hpp" -#include "imgmanager.hpp" +#include "channellisttree.hpp" + #include #include #include + +#include + #include "abaddon.hpp" +#include "imgmanager.hpp" #include "util.hpp" -ChannelList::ChannelList() - : Glib::ObjectBase(typeid(ChannelList)) +ChannelListTree::ChannelListTree() + : Glib::ObjectBase(typeid(ChannelListTree)) , m_model(Gtk::TreeStore::create(m_columns)) + , m_filter_model(Gtk::TreeModelFilter::create(m_model)) + , m_sort_model(Gtk::TreeModelSort::create(m_filter_model)) , m_menu_guild_copy_id("_Copy ID", true) , m_menu_guild_settings("View _Settings", true) , m_menu_guild_leave("_Leave", true) @@ -37,9 +43,12 @@ ChannelList::ChannelList() , m_menu_thread_mark_as_read("Mark as _Read", true) { get_style_context()->add_class("channel-list"); - // todo: move to method + // Filter iters const auto cb = [this](const Gtk::TreeModel::Path &path, Gtk::TreeViewColumn *column) { - auto row = *m_model->get_iter(path); + auto view_path = ConvertViewPathToModel(path); + if (!view_path) return; + auto row = *m_model->get_iter(view_path); + if (!row) return; const auto type = row[m_columns.m_type]; // text channels should not be allowed to be collapsed // maybe they should be but it seems a little difficult to handle expansion to permit this @@ -60,12 +69,12 @@ ChannelList::ChannelList() } }; m_view.signal_row_activated().connect(cb, false); - m_view.signal_row_collapsed().connect(sigc::mem_fun(*this, &ChannelList::OnRowCollapsed), false); - m_view.signal_row_expanded().connect(sigc::mem_fun(*this, &ChannelList::OnRowExpanded), false); + m_view.signal_row_collapsed().connect(sigc::mem_fun(*this, &ChannelListTree::OnRowCollapsed), false); + m_view.signal_row_expanded().connect(sigc::mem_fun(*this, &ChannelListTree::OnRowExpanded), false); m_view.set_activate_on_single_click(true); m_view.get_selection()->set_mode(Gtk::SELECTION_SINGLE); - m_view.get_selection()->set_select_function(sigc::mem_fun(*this, &ChannelList::SelectionFunc)); - m_view.signal_button_press_event().connect(sigc::mem_fun(*this, &ChannelList::OnButtonPressEvent), false); + m_view.get_selection()->set_select_function(sigc::mem_fun(*this, &ChannelListTree::SelectionFunc)); + m_view.signal_button_press_event().connect(sigc::mem_fun(*this, &ChannelListTree::OnButtonPressEvent), false); m_view.set_hexpand(true); m_view.set_vexpand(true); @@ -73,13 +82,33 @@ ChannelList::ChannelList() m_view.set_show_expanders(false); m_view.set_enable_search(false); m_view.set_headers_visible(false); - m_view.set_model(m_model); - m_model->set_sort_column(m_columns.m_sort, Gtk::SORT_ASCENDING); + m_view.set_model(m_sort_model); + m_sort_model->set_sort_column(m_columns.m_sort, Gtk::SORT_ASCENDING); + m_sort_model->set_sort_func(m_columns.m_sort, sigc::mem_fun(*this, &ChannelListTree::SortFunc)); m_model->signal_row_inserted().connect([this](const Gtk::TreeModel::Path &path, const Gtk::TreeModel::iterator &iter) { if (m_updating_listing) return; - if (auto parent = iter->parent(); parent && (*parent)[m_columns.m_expanded]) - m_view.expand_row(m_model->get_path(parent), false); + if (auto parent = iter->parent(); parent && (*parent)[m_columns.m_expanded]) { + if (const auto view_path = ConvertModelPathToView(m_model->get_path(parent))) { + m_view.expand_row(view_path, false); + } + } + }); + + m_filter_model->set_visible_func([this](const Gtk::TreeModel::const_iterator &iter) -> bool { + if (!m_classic || m_updating_listing) return true; + + const RenderType type = (*iter)[m_columns.m_type]; + + if (m_classic_selected_dms) { + if (iter->parent()) return true; + return type == RenderType::DMHeader; + } + + if (type == RenderType::Guild) { + return (*iter)[m_columns.m_id] == m_classic_selected_guild; + } + return type != RenderType::DMHeader; }); m_view.show(); @@ -263,40 +292,107 @@ ChannelList::ChannelList() m_menu_thread.show_all(); auto &discord = Abaddon::Get().GetDiscordClient(); - discord.signal_message_create().connect(sigc::mem_fun(*this, &ChannelList::OnMessageCreate)); - discord.signal_guild_create().connect(sigc::mem_fun(*this, &ChannelList::UpdateNewGuild)); - discord.signal_guild_delete().connect(sigc::mem_fun(*this, &ChannelList::UpdateRemoveGuild)); - discord.signal_channel_delete().connect(sigc::mem_fun(*this, &ChannelList::UpdateRemoveChannel)); - discord.signal_channel_update().connect(sigc::mem_fun(*this, &ChannelList::UpdateChannel)); - discord.signal_channel_create().connect(sigc::mem_fun(*this, &ChannelList::UpdateCreateChannel)); - discord.signal_thread_delete().connect(sigc::mem_fun(*this, &ChannelList::OnThreadDelete)); - discord.signal_thread_update().connect(sigc::mem_fun(*this, &ChannelList::OnThreadUpdate)); - discord.signal_thread_list_sync().connect(sigc::mem_fun(*this, &ChannelList::OnThreadListSync)); - discord.signal_added_to_thread().connect(sigc::mem_fun(*this, &ChannelList::OnThreadJoined)); - discord.signal_removed_from_thread().connect(sigc::mem_fun(*this, &ChannelList::OnThreadRemoved)); - discord.signal_guild_update().connect(sigc::mem_fun(*this, &ChannelList::UpdateGuild)); - discord.signal_message_ack().connect(sigc::mem_fun(*this, &ChannelList::OnMessageAck)); - discord.signal_channel_muted().connect(sigc::mem_fun(*this, &ChannelList::OnChannelMute)); - discord.signal_channel_unmuted().connect(sigc::mem_fun(*this, &ChannelList::OnChannelUnmute)); - discord.signal_guild_muted().connect(sigc::mem_fun(*this, &ChannelList::OnGuildMute)); - discord.signal_guild_unmuted().connect(sigc::mem_fun(*this, &ChannelList::OnGuildUnmute)); + discord.signal_message_create().connect(sigc::mem_fun(*this, &ChannelListTree::OnMessageCreate)); + discord.signal_guild_create().connect(sigc::mem_fun(*this, &ChannelListTree::UpdateNewGuild)); + discord.signal_guild_delete().connect(sigc::mem_fun(*this, &ChannelListTree::UpdateRemoveGuild)); + discord.signal_channel_delete().connect(sigc::mem_fun(*this, &ChannelListTree::UpdateRemoveChannel)); + discord.signal_channel_update().connect(sigc::mem_fun(*this, &ChannelListTree::UpdateChannel)); + discord.signal_channel_create().connect(sigc::mem_fun(*this, &ChannelListTree::UpdateCreateChannel)); + discord.signal_thread_delete().connect(sigc::mem_fun(*this, &ChannelListTree::OnThreadDelete)); + discord.signal_thread_update().connect(sigc::mem_fun(*this, &ChannelListTree::OnThreadUpdate)); + discord.signal_thread_list_sync().connect(sigc::mem_fun(*this, &ChannelListTree::OnThreadListSync)); + discord.signal_added_to_thread().connect(sigc::mem_fun(*this, &ChannelListTree::OnThreadJoined)); + discord.signal_removed_from_thread().connect(sigc::mem_fun(*this, &ChannelListTree::OnThreadRemoved)); + discord.signal_guild_update().connect(sigc::mem_fun(*this, &ChannelListTree::UpdateGuild)); + discord.signal_message_ack().connect(sigc::mem_fun(*this, &ChannelListTree::OnMessageAck)); + discord.signal_channel_muted().connect(sigc::mem_fun(*this, &ChannelListTree::OnChannelMute)); + discord.signal_channel_unmuted().connect(sigc::mem_fun(*this, &ChannelListTree::OnChannelUnmute)); + discord.signal_guild_muted().connect(sigc::mem_fun(*this, &ChannelListTree::OnGuildMute)); + discord.signal_guild_unmuted().connect(sigc::mem_fun(*this, &ChannelListTree::OnGuildUnmute)); #if WITH_VOICE - discord.signal_voice_user_connect().connect(sigc::mem_fun(*this, &ChannelList::OnVoiceUserConnect)); - discord.signal_voice_user_disconnect().connect(sigc::mem_fun(*this, &ChannelList::OnVoiceUserDisconnect)); - discord.signal_voice_state_set().connect(sigc::mem_fun(*this, &ChannelList::OnVoiceStateSet)); + discord.signal_voice_user_connect().connect(sigc::mem_fun(*this, &ChannelListTree::OnVoiceUserConnect)); + discord.signal_voice_user_disconnect().connect(sigc::mem_fun(*this, &ChannelListTree::OnVoiceUserDisconnect)); + discord.signal_voice_state_set().connect(sigc::mem_fun(*this, &ChannelListTree::OnVoiceStateSet)); #endif } -void ChannelList::UsePanedHack(Gtk::Paned &paned) { - paned.property_position().signal_changed().connect(sigc::mem_fun(*this, &ChannelList::OnPanedPositionChanged)); +void ChannelListTree::UsePanedHack(Gtk::Paned &paned) { + paned.property_position().signal_changed().connect(sigc::mem_fun(*this, &ChannelListTree::OnPanedPositionChanged)); +} + +void ChannelListTree::SetClassic(bool value) { + m_classic = value; + m_filter_model->refilter(); +} + +void ChannelListTree::SetSelectedGuild(Snowflake guild_id) { + m_classic_selected_guild = guild_id; + m_classic_selected_dms = false; + m_filter_model->refilter(); + auto guild_iter = GetIteratorForGuildFromID(guild_id); + if (guild_iter) { + if (auto view_iter = ConvertModelIterToView(guild_iter)) { + m_view.expand_row(GetViewPathFromViewIter(view_iter), false); + } + } +} + +void ChannelListTree::SetSelectedDMs() { + m_classic_selected_dms = true; + m_filter_model->refilter(); + if (m_dm_header) { + if (auto view_path = ConvertModelPathToView(m_dm_header)) { + m_view.expand_row(view_path, false); + } + } +} + +int ChannelListTree::SortFunc(const Gtk::TreeModel::iterator &a, const Gtk::TreeModel::iterator &b) { + const RenderType a_type = (*a)[m_columns.m_type]; + const RenderType b_type = (*b)[m_columns.m_type]; + const int64_t a_sort = (*a)[m_columns.m_sort]; + const int64_t b_sort = (*b)[m_columns.m_sort]; + if (a_type == RenderType::DMHeader) return -1; + if (b_type == RenderType::DMHeader) return 1; +#ifdef WITH_VOICE + if (a_type == RenderType::TextChannel && b_type == RenderType::VoiceChannel) return -1; + if (b_type == RenderType::TextChannel && a_type == RenderType::VoiceChannel) return 1; +#endif + return static_cast(std::clamp(a_sort - b_sort, int64_t(-1), int64_t(1))); } -void ChannelList::OnPanedPositionChanged() { +void ChannelListTree::OnPanedPositionChanged() { m_view.queue_draw(); } -void ChannelList::UpdateListing() { +void ChannelListTree::UpdateListingClassic() { + m_updating_listing = true; + + // refilter so every row is visible + // otherwise clear() causes a CRITICAL assert in a slot for the filter model + m_filter_model->refilter(); + m_model->clear(); + + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto guild_ids = discord.GetUserSortedGuilds(); + for (const auto guild_id : guild_ids) { + if (const auto guild = discord.GetGuild(guild_id); guild.has_value()) { + AddGuild(*guild, m_model->children()); + } + } + + m_updating_listing = false; + + AddPrivateChannels(); +} + +void ChannelListTree::UpdateListing() { + if (m_classic) { + UpdateListingClassic(); + return; + } + m_updating_listing = true; m_model->clear(); @@ -366,7 +462,7 @@ void ChannelList::UpdateListing() { } // TODO update for folders -void ChannelList::UpdateNewGuild(const GuildData &guild) { +void ChannelListTree::UpdateNewGuild(const GuildData &guild) { AddGuild(guild, m_model->children()); // update sort order int sortnum = 0; @@ -377,19 +473,19 @@ void ChannelList::UpdateNewGuild(const GuildData &guild) { } } -void ChannelList::UpdateRemoveGuild(Snowflake id) { +void ChannelListTree::UpdateRemoveGuild(Snowflake id) { auto iter = GetIteratorForGuildFromID(id); if (!iter) return; m_model->erase(iter); } -void ChannelList::UpdateRemoveChannel(Snowflake id) { +void ChannelListTree::UpdateRemoveChannel(Snowflake id) { auto iter = GetIteratorForRowFromID(id); if (!iter) return; m_model->erase(iter); } -void ChannelList::UpdateChannel(Snowflake id) { +void ChannelListTree::UpdateChannel(Snowflake id) { auto iter = GetIteratorForRowFromID(id); auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); if (!iter || !channel.has_value()) return; @@ -413,7 +509,7 @@ void ChannelList::UpdateChannel(Snowflake id) { MoveRow(iter, new_parent); } -void ChannelList::UpdateCreateChannel(const ChannelData &channel) { +void ChannelListTree::UpdateCreateChannel(const ChannelData &channel) { if (channel.Type == ChannelType::GUILD_CATEGORY) return (void)UpdateCreateChannelCategory(channel); if (channel.Type == ChannelType::DM || channel.Type == ChannelType::GROUP_DM) return UpdateCreateDMChannel(channel); if (channel.Type != ChannelType::GUILD_TEXT && channel.Type != ChannelType::GUILD_NEWS) return; @@ -439,7 +535,7 @@ void ChannelList::UpdateCreateChannel(const ChannelData &channel) { channel_row[m_columns.m_sort] = *channel.Position; } -void ChannelList::UpdateGuild(Snowflake id) { +void ChannelListTree::UpdateGuild(Snowflake id) { auto iter = GetIteratorForGuildFromID(id); auto &img = Abaddon::Get().GetImageManager(); const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(id); @@ -463,7 +559,7 @@ void ChannelList::UpdateGuild(Snowflake id) { } } -void ChannelList::OnThreadJoined(Snowflake id) { +void ChannelListTree::OnThreadJoined(Snowflake id) { if (GetIteratorForRowFromID(id)) return; const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); if (!channel.has_value()) return; @@ -472,16 +568,16 @@ void ChannelList::OnThreadJoined(Snowflake id) { CreateThreadRow(parent->children(), *channel); } -void ChannelList::OnThreadRemoved(Snowflake id) { +void ChannelListTree::OnThreadRemoved(Snowflake id) { DeleteThreadRow(id); } -void ChannelList::OnThreadDelete(const ThreadDeleteData &data) { +void ChannelListTree::OnThreadDelete(const ThreadDeleteData &data) { DeleteThreadRow(data.ID); } // todo probably make the row stick around if its selected until the selection changes -void ChannelList::OnThreadUpdate(const ThreadUpdateData &data) { +void ChannelListTree::OnThreadUpdate(const ThreadUpdateData &data) { auto iter = GetIteratorForRowFromID(data.Thread.ID); if (iter) (*iter)[m_columns.m_name] = "- " + Glib::Markup::escape_text(*data.Thread.Name); @@ -490,7 +586,7 @@ void ChannelList::OnThreadUpdate(const ThreadUpdateData &data) { DeleteThreadRow(data.Thread.ID); } -void ChannelList::OnThreadListSync(const ThreadListSyncData &data) { +void ChannelListTree::OnThreadListSync(const ThreadListSyncData &data) { // get the threads in the guild std::vector threads; auto guild_iter = GetIteratorForGuildFromID(data.GuildID); @@ -526,7 +622,7 @@ void ChannelList::OnThreadListSync(const ThreadListSyncData &data) { } #ifdef WITH_VOICE -void ChannelList::OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id) { +void ChannelListTree::OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id) { auto parent_iter = GetIteratorForRowFromIDOfType(channel_id, RenderType::VoiceChannel); if (!parent_iter) parent_iter = GetIteratorForRowFromIDOfType(channel_id, RenderType::DM); if (!parent_iter) return; @@ -536,48 +632,50 @@ void ChannelList::OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id) { CreateVoiceParticipantRow(*user, parent_iter->children()); } -void ChannelList::OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id) { +void ChannelListTree::OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id) { if (auto iter = GetIteratorForRowFromIDOfType(user_id, RenderType::VoiceParticipant)) { m_model->erase(iter); } } -void ChannelList::OnVoiceStateSet(Snowflake user_id, Snowflake channel_id, VoiceStateFlags flags) { +void ChannelListTree::OnVoiceStateSet(Snowflake user_id, Snowflake channel_id, VoiceStateFlags flags) { if (auto iter = GetIteratorForRowFromIDOfType(user_id, RenderType::VoiceParticipant)) { (*iter)[m_columns.m_voice_flags] = flags; } } #endif -void ChannelList::DeleteThreadRow(Snowflake id) { +void ChannelListTree::DeleteThreadRow(Snowflake id) { auto iter = GetIteratorForRowFromID(id); if (iter) m_model->erase(iter); } -void ChannelList::OnChannelMute(Snowflake id) { +void ChannelListTree::OnChannelMute(Snowflake id) { if (auto iter = GetIteratorForRowFromID(id)) m_model->row_changed(m_model->get_path(iter), iter); } -void ChannelList::OnChannelUnmute(Snowflake id) { +void ChannelListTree::OnChannelUnmute(Snowflake id) { if (auto iter = GetIteratorForRowFromID(id)) m_model->row_changed(m_model->get_path(iter), iter); } -void ChannelList::OnGuildMute(Snowflake id) { +void ChannelListTree::OnGuildMute(Snowflake id) { if (auto iter = GetIteratorForGuildFromID(id)) m_model->row_changed(m_model->get_path(iter), iter); } -void ChannelList::OnGuildUnmute(Snowflake id) { +void ChannelListTree::OnGuildUnmute(Snowflake id) { if (auto iter = GetIteratorForGuildFromID(id)) m_model->row_changed(m_model->get_path(iter), iter); } // create a temporary channel row for non-joined threads // and delete them when the active channel switches off of them if still not joined -void ChannelList::SetActiveChannel(Snowflake id, bool expand_to) { +void ChannelListTree::SetActiveChannel(Snowflake id, bool expand_to) { + while (Gtk::Main::events_pending()) Gtk::Main::iteration(); + // mark channel as read when switching off if (m_active_channel.IsValid()) Abaddon::Get().GetDiscordClient().MarkChannelAsRead(m_active_channel, [](...) {}); @@ -594,10 +692,14 @@ void ChannelList::SetActiveChannel(Snowflake id, bool expand_to) { const auto channel_iter = GetIteratorForRowFromID(id); if (channel_iter) { - if (expand_to) { - m_view.expand_to_path(m_model->get_path(channel_iter)); + m_view.get_selection()->unselect_all(); + const auto view_iter = ConvertModelIterToView(channel_iter); + if (view_iter) { + if (expand_to) { + m_view.expand_to_path(GetViewPathFromViewIter(view_iter)); + } + m_view.get_selection()->select(view_iter); } - m_view.get_selection()->select(channel_iter); } else { m_view.get_selection()->unselect_all(); const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); @@ -605,11 +707,17 @@ void ChannelList::SetActiveChannel(Snowflake id, bool expand_to) { auto parent_iter = GetIteratorForRowFromID(*channel->ParentID); if (!parent_iter) return; m_temporary_thread_row = CreateThreadRow(parent_iter->children(), *channel); - m_view.get_selection()->select(m_temporary_thread_row); + const auto view_iter = ConvertModelIterToView(m_temporary_thread_row); + if (view_iter) { + m_view.get_selection()->select(view_iter); + } } } -void ChannelList::UseExpansionState(const ExpansionStateRoot &root) { +void ChannelListTree::UseExpansionState(const ExpansionStateRoot &root) { + m_updating_listing = true; + m_filter_model->refilter(); + auto recurse = [this](auto &self, const ExpansionStateRoot &root) -> void { for (const auto &[id, state] : root.Children) { Gtk::TreeModel::iterator row_iter; @@ -620,10 +728,15 @@ void ChannelList::UseExpansionState(const ExpansionStateRoot &root) { } if (row_iter) { - if (state.IsExpanded) - m_view.expand_row(m_model->get_path(row_iter), false); - else - m_view.collapse_row(m_model->get_path(row_iter)); + (*row_iter)[m_columns.m_expanded] = state.IsExpanded; + auto view_iter = ConvertModelIterToView(row_iter); + if (view_iter) { + if (state.IsExpanded) { + m_view.expand_row(GetViewPathFromViewIter(view_iter), false); + } else { + m_view.collapse_row(GetViewPathFromViewIter(view_iter)); + } + } } self(self, state.Children); @@ -632,32 +745,42 @@ void ChannelList::UseExpansionState(const ExpansionStateRoot &root) { for (const auto &[id, state] : root.Children) { if (const auto iter = GetIteratorForTopLevelFromID(id)) { - if (state.IsExpanded) - m_view.expand_row(m_model->get_path(iter), false); - else - m_view.collapse_row(m_model->get_path(iter)); + (*iter)[m_columns.m_expanded] = state.IsExpanded; + auto view_iter = ConvertModelIterToView(iter); + if (view_iter) { + if (state.IsExpanded) { + m_view.expand_row(GetViewPathFromViewIter(view_iter), false); + } else { + m_view.collapse_row(GetViewPathFromViewIter(view_iter)); + } + } } recurse(recurse, state.Children); } + m_updating_listing = false; + m_filter_model->refilter(); + m_tmp_row_map.clear(); + m_tmp_guild_row_map.clear(); } -ExpansionStateRoot ChannelList::GetExpansionState() const { +ExpansionStateRoot ChannelListTree::GetExpansionState() const { ExpansionStateRoot r; auto recurse = [this](auto &self, const Gtk::TreeRow &row) -> ExpansionState { ExpansionState r; r.IsExpanded = row[m_columns.m_expanded]; - for (const auto &child : row.children()) + for (auto child : row.children()) { r.Children.Children[static_cast(child[m_columns.m_id])] = self(self, child); + } return r; }; - for (const auto &child : m_model->children()) { + for (auto child : m_model->children()) { const auto id = static_cast(child[m_columns.m_id]); if (static_cast(id) == 0ULL) continue; // dont save DM header r.Children[id] = recurse(recurse, child); @@ -666,7 +789,51 @@ ExpansionStateRoot ChannelList::GetExpansionState() const { return r; } -Gtk::TreeModel::iterator ChannelList::AddFolder(const UserSettingsGuildFoldersEntry &folder) { +Gtk::TreePath ChannelListTree::ConvertModelPathToView(const Gtk::TreePath &path) { + if (const auto filter_path = m_filter_model->convert_child_path_to_path(path)) { + if (const auto sort_path = m_sort_model->convert_child_path_to_path(filter_path)) { + return sort_path; + } + } + + return {}; +} + +Gtk::TreeIter ChannelListTree::ConvertModelIterToView(const Gtk::TreeIter &iter) { + if (const auto filter_iter = m_filter_model->convert_child_iter_to_iter(iter)) { + if (const auto sort_iter = m_sort_model->convert_child_iter_to_iter(filter_iter)) { + return sort_iter; + } + } + + return {}; +} + +Gtk::TreePath ChannelListTree::ConvertViewPathToModel(const Gtk::TreePath &path) { + if (const auto filter_path = m_sort_model->convert_path_to_child_path(path)) { + if (const auto model_path = m_filter_model->convert_path_to_child_path(filter_path)) { + return model_path; + } + } + + return {}; +} + +Gtk::TreeIter ChannelListTree::ConvertViewIterToModel(const Gtk::TreeIter &iter) { + if (const auto filter_iter = m_sort_model->convert_iter_to_child_iter(iter)) { + if (const auto model_iter = m_filter_model->convert_iter_to_child_iter(filter_iter)) { + return model_iter; + } + } + + return {}; +} + +Gtk::TreePath ChannelListTree::GetViewPathFromViewIter(const Gtk::TreeIter &iter) { + return m_sort_model->get_path(iter); +} + +Gtk::TreeModel::iterator ChannelListTree::AddFolder(const UserSettingsGuildFoldersEntry &folder) { if (!folder.ID.has_value()) { // just a guild if (!folder.GuildIDs.empty()) { @@ -704,7 +871,7 @@ Gtk::TreeModel::iterator ChannelList::AddFolder(const UserSettingsGuildFoldersEn return {}; } -Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk::TreeNodeChildren &root) { +Gtk::TreeModel::iterator ChannelListTree::AddGuild(const GuildData &guild, const Gtk::TreeNodeChildren &root) { auto &discord = Abaddon::Get().GetDiscordClient(); auto &img = Abaddon::Get().GetImageManager(); @@ -764,8 +931,9 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk const auto it = threads.find(channel.ID); if (it == threads.end()) return; - for (const auto &thread : it->second) - m_tmp_row_map[thread.ID] = CreateThreadRow(row.children(), thread); + for (const auto &thread : it->second) { + CreateThreadRow(row.children(), thread); + } }; #ifdef WITH_VOICE @@ -834,7 +1002,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk return guild_row; } -Gtk::TreeModel::iterator ChannelList::UpdateCreateChannelCategory(const ChannelData &channel) { +Gtk::TreeModel::iterator ChannelListTree::UpdateCreateChannelCategory(const ChannelData &channel) { const auto iter = GetIteratorForGuildFromID(*channel.GuildID); if (!iter) return {}; @@ -848,7 +1016,7 @@ Gtk::TreeModel::iterator ChannelList::UpdateCreateChannelCategory(const ChannelD return cat_row; } -Gtk::TreeModel::iterator ChannelList::CreateThreadRow(const Gtk::TreeNodeChildren &children, const ChannelData &channel) { +Gtk::TreeModel::iterator ChannelListTree::CreateThreadRow(const Gtk::TreeNodeChildren &children, const ChannelData &channel) { auto thread_iter = m_model->append(children); auto thread_row = *thread_iter; thread_row[m_columns.m_type] = RenderType::Thread; @@ -861,7 +1029,7 @@ Gtk::TreeModel::iterator ChannelList::CreateThreadRow(const Gtk::TreeNodeChildre } #ifdef WITH_VOICE -Gtk::TreeModel::iterator ChannelList::CreateVoiceParticipantRow(const UserData &user, const Gtk::TreeNodeChildren &parent) { +Gtk::TreeModel::iterator ChannelListTree::CreateVoiceParticipantRow(const UserData &user, const Gtk::TreeNodeChildren &parent) { auto row = *m_model->append(parent); row[m_columns.m_type] = RenderType::VoiceParticipant; row[m_columns.m_id] = user.ID; @@ -884,7 +1052,7 @@ Gtk::TreeModel::iterator ChannelList::CreateVoiceParticipantRow(const UserData & } #endif -void ChannelList::UpdateChannelCategory(const ChannelData &channel) { +void ChannelListTree::UpdateChannelCategory(const ChannelData &channel) { auto iter = GetIteratorForRowFromID(channel.ID); if (!iter) return; @@ -893,7 +1061,7 @@ void ChannelList::UpdateChannelCategory(const ChannelData &channel) { } // todo this all needs refactoring for shooore -Gtk::TreeModel::iterator ChannelList::GetIteratorForTopLevelFromID(Snowflake id) { +Gtk::TreeModel::iterator ChannelListTree::GetIteratorForTopLevelFromID(Snowflake id) { for (const auto &child : m_model->children()) { if ((child[m_columns.m_type] == RenderType::Guild || child[m_columns.m_type] == RenderType::Folder) && child[m_columns.m_id] == id) { return child; @@ -908,7 +1076,7 @@ Gtk::TreeModel::iterator ChannelList::GetIteratorForTopLevelFromID(Snowflake id) return {}; } -Gtk::TreeModel::iterator ChannelList::GetIteratorForGuildFromID(Snowflake id) { +Gtk::TreeModel::iterator ChannelListTree::GetIteratorForGuildFromID(Snowflake id) { for (const auto &child : m_model->children()) { if (child[m_columns.m_type] == RenderType::Guild && child[m_columns.m_id] == id) { return child; @@ -923,7 +1091,7 @@ Gtk::TreeModel::iterator ChannelList::GetIteratorForGuildFromID(Snowflake id) { return {}; } -Gtk::TreeModel::iterator ChannelList::GetIteratorForRowFromID(Snowflake id) { +Gtk::TreeModel::iterator ChannelListTree::GetIteratorForRowFromID(Snowflake id) { std::queue queue; for (const auto &child : m_model->children()) for (const auto &child2 : child.children()) @@ -940,7 +1108,7 @@ Gtk::TreeModel::iterator ChannelList::GetIteratorForRowFromID(Snowflake id) { return {}; } -Gtk::TreeModel::iterator ChannelList::GetIteratorForRowFromIDOfType(Snowflake id, RenderType type) { +Gtk::TreeModel::iterator ChannelListTree::GetIteratorForRowFromIDOfType(Snowflake id, RenderType type) { std::queue queue; for (const auto &child : m_model->children()) for (const auto &child2 : child.children()) @@ -957,20 +1125,22 @@ Gtk::TreeModel::iterator ChannelList::GetIteratorForRowFromIDOfType(Snowflake id return {}; } -bool ChannelList::IsTextChannel(ChannelType type) { +bool ChannelListTree::IsTextChannel(ChannelType type) { return type == ChannelType::GUILD_TEXT || type == ChannelType::GUILD_NEWS; } // this should be unncessary but something is behaving strange so its just in case -void ChannelList::OnRowCollapsed(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) const { +void ChannelListTree::OnRowCollapsed(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) const { (*iter)[m_columns.m_expanded] = false; } -void ChannelList::OnRowExpanded(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) { +void ChannelListTree::OnRowExpanded(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) { // restore previous expansion - for (auto it = iter->children().begin(); it != iter->children().end(); it++) { - if ((*it)[m_columns.m_expanded]) - m_view.expand_row(m_model->get_path(it), false); + auto model_iter = ConvertViewIterToModel(iter); + for (auto it = model_iter->children().begin(); it != model_iter->children().end(); it++) { + if ((*it)[m_columns.m_expanded]) { + m_view.expand_row(GetViewPathFromViewIter(ConvertModelIterToView(it)), false); + } } // try and restore selection if previous collapsed @@ -978,19 +1148,21 @@ void ChannelList::OnRowExpanded(const Gtk::TreeModel::iterator &iter, const Gtk: selection->select(m_last_selected); } - (*iter)[m_columns.m_expanded] = true; + (*model_iter)[m_columns.m_expanded] = true; } -bool ChannelList::SelectionFunc(const Glib::RefPtr &model, const Gtk::TreeModel::Path &path, bool is_currently_selected) { - if (auto selection = m_view.get_selection()) - if (auto row = selection->get_selected()) - m_last_selected = m_model->get_path(row); +bool ChannelListTree::SelectionFunc(const Glib::RefPtr &model, const Gtk::TreeModel::Path &path, bool is_currently_selected) { + if (auto selection = m_view.get_selection()) { + if (auto row = selection->get_selected()) { + m_last_selected = GetViewPathFromViewIter(row); + } + } - auto type = (*m_model->get_iter(path))[m_columns.m_type]; + auto type = (*model->get_iter(path))[m_columns.m_type]; return type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread; } -void ChannelList::AddPrivateChannels() { +void ChannelListTree::AddPrivateChannels() { auto header_row = *m_model->append(); header_row[m_columns.m_type] = RenderType::DMHeader; header_row[m_columns.m_sort] = -1; @@ -1031,7 +1203,7 @@ void ChannelList::AddPrivateChannels() { } } -void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) { +void ChannelListTree::UpdateCreateDMChannel(const ChannelData &dm) { auto header_row = m_model->get_iter(m_dm_header); auto &img = Abaddon::Get().GetImageManager(); @@ -1046,7 +1218,7 @@ void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) { SetDMChannelIcon(iter, dm); } -void ChannelList::SetDMChannelIcon(Gtk::TreeIter iter, const ChannelData &dm) { +void ChannelListTree::SetDMChannelIcon(Gtk::TreeIter iter, const ChannelData &dm) { auto &img = Abaddon::Get().GetImageManager(); std::optional top_recipient; @@ -1103,7 +1275,7 @@ void ChannelList::SetDMChannelIcon(Gtk::TreeIter iter, const ChannelData &dm) { } } -void ChannelList::RedrawUnreadIndicatorsForChannel(const ChannelData &channel) { +void ChannelListTree::RedrawUnreadIndicatorsForChannel(const ChannelData &channel) { if (channel.GuildID.has_value()) { auto iter = GetIteratorForGuildFromID(*channel.GuildID); if (iter) m_model->row_changed(m_model->get_path(iter), iter); @@ -1114,7 +1286,7 @@ void ChannelList::RedrawUnreadIndicatorsForChannel(const ChannelData &channel) { } } -void ChannelList::OnMessageAck(const MessageAckData &data) { +void ChannelListTree::OnMessageAck(const MessageAckData &data) { // trick renderer into redrawing m_model->row_changed(Gtk::TreeModel::Path("0"), m_model->get_iter("0")); // 0 is always path for dm header auto iter = GetIteratorForRowFromID(data.ChannelID); @@ -1125,7 +1297,7 @@ void ChannelList::OnMessageAck(const MessageAckData &data) { } } -void ChannelList::OnMessageCreate(const Message &msg) { +void ChannelListTree::OnMessageCreate(const Message &msg) { auto iter = GetIteratorForRowFromID(msg.ChannelID); if (iter) m_model->row_changed(m_model->get_path(iter), iter); // redraw const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(msg.ChannelID); @@ -1137,9 +1309,11 @@ void ChannelList::OnMessageCreate(const Message &msg) { RedrawUnreadIndicatorsForChannel(*channel); } -bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) { +bool ChannelListTree::OnButtonPressEvent(GdkEventButton *ev) { if (ev->button == GDK_BUTTON_SECONDARY && ev->type == GDK_BUTTON_PRESS) { if (m_view.get_path_at_pos(static_cast(ev->x), static_cast(ev->y), m_path_for_menu)) { + m_path_for_menu = m_filter_model->convert_path_to_child_path(m_sort_model->convert_path_to_child_path(m_path_for_menu)); + if (!m_path_for_menu) return true; auto row = (*m_model->get_iter(m_path_for_menu)); switch (static_cast(row[m_columns.m_type])) { case RenderType::Guild: @@ -1184,7 +1358,7 @@ bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) { return false; } -void ChannelList::MoveRow(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::iterator &new_parent) { +void ChannelListTree::MoveRow(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::iterator &new_parent) { // duplicate the row data under the new parent and then delete the old row auto row = *m_model->append(new_parent->children()); // would be nice to be able to get all columns out at runtime so i dont need this @@ -1212,7 +1386,7 @@ void ChannelList::MoveRow(const Gtk::TreeModel::iterator &iter, const Gtk::TreeM m_model->erase(iter); } -void ChannelList::OnGuildSubmenuPopup() { +void ChannelListTree::OnGuildSubmenuPopup() { const auto iter = m_model->get_iter(m_path_for_menu); if (!iter) return; const auto id = static_cast((*iter)[m_columns.m_id]); @@ -1227,7 +1401,7 @@ void ChannelList::OnGuildSubmenuPopup() { m_menu_guild_leave.set_sensitive(!(guild.has_value() && guild->OwnerID == self_id)); } -void ChannelList::OnCategorySubmenuPopup() { +void ChannelListTree::OnCategorySubmenuPopup() { const auto iter = m_model->get_iter(m_path_for_menu); if (!iter) return; const auto id = static_cast((*iter)[m_columns.m_id]); @@ -1237,7 +1411,7 @@ void ChannelList::OnCategorySubmenuPopup() { m_menu_category_toggle_mute.set_label("Mute"); } -void ChannelList::OnChannelSubmenuPopup() { +void ChannelListTree::OnChannelSubmenuPopup() { const auto iter = m_model->get_iter(m_path_for_menu); if (!iter) return; const auto id = static_cast((*iter)[m_columns.m_id]); @@ -1253,7 +1427,7 @@ void ChannelList::OnChannelSubmenuPopup() { } #ifdef WITH_VOICE -void ChannelList::OnVoiceChannelSubmenuPopup() { +void ChannelListTree::OnVoiceChannelSubmenuPopup() { const auto iter = m_model->get_iter(m_path_for_menu); if (!iter) return; const auto id = static_cast((*iter)[m_columns.m_id]); @@ -1268,7 +1442,7 @@ void ChannelList::OnVoiceChannelSubmenuPopup() { } #endif -void ChannelList::OnDMSubmenuPopup() { +void ChannelListTree::OnDMSubmenuPopup() { auto iter = m_model->get_iter(m_path_for_menu); if (!iter) return; const auto id = static_cast((*iter)[m_columns.m_id]); @@ -1289,7 +1463,7 @@ void ChannelList::OnDMSubmenuPopup() { #endif } -void ChannelList::OnThreadSubmenuPopup() { +void ChannelListTree::OnThreadSubmenuPopup() { m_menu_thread_archive.set_visible(false); m_menu_thread_unarchive.set_visible(false); @@ -1311,35 +1485,35 @@ void ChannelList::OnThreadSubmenuPopup() { m_menu_thread_unarchive.set_visible(channel->ThreadMetadata->IsArchived); } -ChannelList::type_signal_action_channel_item_select ChannelList::signal_action_channel_item_select() { +ChannelListTree::type_signal_action_channel_item_select ChannelListTree::signal_action_channel_item_select() { return m_signal_action_channel_item_select; } -ChannelList::type_signal_action_guild_leave ChannelList::signal_action_guild_leave() { +ChannelListTree::type_signal_action_guild_leave ChannelListTree::signal_action_guild_leave() { return m_signal_action_guild_leave; } -ChannelList::type_signal_action_guild_settings ChannelList::signal_action_guild_settings() { +ChannelListTree::type_signal_action_guild_settings ChannelListTree::signal_action_guild_settings() { return m_signal_action_guild_settings; } #ifdef WITH_LIBHANDY -ChannelList::type_signal_action_open_new_tab ChannelList::signal_action_open_new_tab() { +ChannelListTree::type_signal_action_open_new_tab ChannelListTree::signal_action_open_new_tab() { return m_signal_action_open_new_tab; } #endif #ifdef WITH_VOICE -ChannelList::type_signal_action_join_voice_channel ChannelList::signal_action_join_voice_channel() { +ChannelListTree::type_signal_action_join_voice_channel ChannelListTree::signal_action_join_voice_channel() { return m_signal_action_join_voice_channel; } -ChannelList::type_signal_action_disconnect_voice ChannelList::signal_action_disconnect_voice() { +ChannelListTree::type_signal_action_disconnect_voice ChannelListTree::signal_action_disconnect_voice() { return m_signal_action_disconnect_voice; } #endif -ChannelList::ModelColumns::ModelColumns() { +ChannelListTree::ModelColumns::ModelColumns() { add(m_type); add(m_id); add(m_name); diff --git a/src/components/channels.hpp b/src/components/channellist/channellisttree.hpp similarity index 88% rename from src/components/channels.hpp rename to src/components/channellist/channellisttree.hpp index 9d449e4c..136522b5 100644 --- a/src/components/channels.hpp +++ b/src/components/channellist/channellisttree.hpp @@ -8,20 +8,21 @@ #include #include #include +#include #include #include #include "discord/discord.hpp" #include "state.hpp" -#include "channelscellrenderer.hpp" +#include "cellrendererchannels.hpp" constexpr static int GuildIconSize = 24; constexpr static int DMIconSize = 20; constexpr static int VoiceParticipantIconSize = 18; constexpr static int OrphanChannelSortOffset = -100; // forces orphan channels to the top of the list -class ChannelList : public Gtk::ScrolledWindow { +class ChannelListTree : public Gtk::ScrolledWindow { public: - ChannelList(); + ChannelListTree(); void UpdateListing(); void SetActiveChannel(Snowflake id, bool expand_to); @@ -32,9 +33,17 @@ class ChannelList : public Gtk::ScrolledWindow { void UsePanedHack(Gtk::Paned &paned); + void SetClassic(bool value); + void SetSelectedGuild(Snowflake guild_id); + void SetSelectedDMs(); + protected: + int SortFunc(const Gtk::TreeModel::iterator &a, const Gtk::TreeModel::iterator &b); + void OnPanedPositionChanged(); + void UpdateListingClassic(); + void UpdateNewGuild(const GuildData &guild); void UpdateRemoveGuild(Snowflake id); void UpdateRemoveChannel(Snowflake id); @@ -82,7 +91,14 @@ class ChannelList : public Gtk::ScrolledWindow { ModelColumns m_columns; Glib::RefPtr m_model; - + Glib::RefPtr m_filter_model; + Glib::RefPtr m_sort_model; + + Gtk::TreePath ConvertModelPathToView(const Gtk::TreePath &path); + Gtk::TreeIter ConvertModelIterToView(const Gtk::TreeIter &iter); + Gtk::TreePath ConvertViewPathToModel(const Gtk::TreePath &path); + Gtk::TreeIter ConvertViewIterToModel(const Gtk::TreeIter &iter); + Gtk::TreePath GetViewPathFromViewIter(const Gtk::TreeIter &iter); Gtk::TreeModel::iterator AddFolder(const UserSettingsGuildFoldersEntry &folder); Gtk::TreeModel::iterator AddGuild(const GuildData &guild, const Gtk::TreeNodeChildren &root); Gtk::TreeModel::iterator UpdateCreateChannelCategory(const ChannelData &channel); @@ -116,7 +132,7 @@ class ChannelList : public Gtk::ScrolledWindow { void UpdateCreateDMChannel(const ChannelData &channel); void SetDMChannelIcon(Gtk::TreeIter iter, const ChannelData &dm); - void RedrawUnreadIndicatorsForChannel(const ChannelData& channel); + void RedrawUnreadIndicatorsForChannel(const ChannelData &channel); void OnMessageAck(const MessageAckData &data); void OnMessageCreate(const Message &msg); @@ -184,10 +200,13 @@ class ChannelList : public Gtk::ScrolledWindow { bool m_updating_listing = false; + bool m_classic = false; + Snowflake m_classic_selected_guild; + bool m_classic_selected_dms = false; + Snowflake m_active_channel; - // (GetIteratorForChannelFromID is rather slow) - // only temporary since i dont want to worry about maintaining this map + // hashtable for the billion lookups done in UseExpansionState std::unordered_map m_tmp_row_map; std::unordered_map m_tmp_guild_row_map; diff --git a/src/components/channellist/classic/guildlist.cpp b/src/components/channellist/classic/guildlist.cpp new file mode 100644 index 00000000..d756c6fb --- /dev/null +++ b/src/components/channellist/classic/guildlist.cpp @@ -0,0 +1,177 @@ +#include "guildlist.hpp" + +#include "abaddon.hpp" +#include "guildlistfolderitem.hpp" + +class GuildListDMsButton : public Gtk::EventBox { +public: + GuildListDMsButton() { + set_size_request(48, 48); + + m_img.property_icon_name() = "user-available-symbolic"; // meh + m_img.property_icon_size() = Gtk::ICON_SIZE_DND; + add(m_img); + show_all_children(); + } + +private: + Gtk::Image m_img; +}; + +GuildList::GuildList() + : m_menu_guild_copy_id("_Copy ID", true) + , m_menu_guild_settings("View _Settings", true) + , m_menu_guild_leave("_Leave", true) + , m_menu_guild_mark_as_read("Mark as _Read", true) { + get_style_context()->add_class("classic-guild-list"); + set_selection_mode(Gtk::SELECTION_NONE); + show_all_children(); + + m_menu_guild_copy_id.signal_activate().connect([this] { + Gtk::Clipboard::get()->set_text(std::to_string(m_menu_guild_target)); + }); + m_menu_guild_settings.signal_activate().connect([this] { + m_signal_action_guild_settings.emit(m_menu_guild_target); + }); + m_menu_guild_leave.signal_activate().connect([this] { + m_signal_action_guild_leave.emit(m_menu_guild_target); + }); + m_menu_guild_mark_as_read.signal_activate().connect([this] { + Abaddon::Get().GetDiscordClient().MarkGuildAsRead(m_menu_guild_target, [](...) {}); + }); + m_menu_guild_toggle_mute.signal_activate().connect([this] { + const auto id = m_menu_guild_target; + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.IsGuildMuted(id)) + discord.UnmuteGuild(id, NOOP_CALLBACK); + else + discord.MuteGuild(id, NOOP_CALLBACK); + }); + m_menu_guild.append(m_menu_guild_mark_as_read); + m_menu_guild.append(m_menu_guild_settings); + m_menu_guild.append(m_menu_guild_leave); + m_menu_guild.append(m_menu_guild_toggle_mute); + m_menu_guild.append(m_menu_guild_copy_id); + m_menu_guild.show_all(); +} + +void GuildList::UpdateListing() { + auto &discord = Abaddon::Get().GetDiscordClient(); + + Clear(); + + auto *dms = Gtk::make_managed(); + dms->show(); + dms->signal_button_press_event().connect([this](GdkEventButton *ev) -> bool { + if (ev->type == GDK_BUTTON_PRESS && ev->button == GDK_BUTTON_PRIMARY) { + m_signal_dms_selected.emit(); + } + return false; + }); + add(*dms); + + // does this function still even work ??lol + const auto folders = discord.GetUserSettings().GuildFolders; + const auto guild_ids = discord.GetUserSortedGuilds(); + + // same logic from ChannelListTree + + std::set foldered_guilds; + for (const auto &group : folders) { + foldered_guilds.insert(group.GuildIDs.begin(), group.GuildIDs.end()); + } + + for (auto iter = guild_ids.crbegin(); iter != guild_ids.crend(); iter++) { + if (foldered_guilds.find(*iter) == foldered_guilds.end()) { + AddGuild(*iter); + } + } + + for (const auto &group : folders) { + AddFolder(group); + } +} + +void GuildList::AddGuild(Snowflake id) { + if (auto item = CreateGuildWidget(id)) { + item->show(); + add(*item); + } +} + +GuildListGuildItem *GuildList::CreateGuildWidget(Snowflake id) { + const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(id); + if (!guild.has_value()) return nullptr; + + auto *item = Gtk::make_managed(*guild); + item->signal_button_press_event().connect([this, id](GdkEventButton *event) -> bool { + if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) { + m_signal_guild_selected.emit(id); + } else if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_SECONDARY) { + m_menu_guild_target = id; + OnGuildSubmenuPopup(); + m_menu_guild.popup_at_pointer(reinterpret_cast(event)); + } + return true; + }); + + return item; +} + +void GuildList::AddFolder(const UserSettingsGuildFoldersEntry &folder) { + // groups with no ID arent actually folders + if (!folder.ID.has_value()) { + if (!folder.GuildIDs.empty()) { + AddGuild(folder.GuildIDs[0]); + } + return; + } + + auto *folder_widget = Gtk::make_managed(folder); + for (const auto guild_id : folder.GuildIDs) { + if (auto *guild_widget = CreateGuildWidget(guild_id)) { + guild_widget->show(); + folder_widget->AddGuildWidget(guild_widget); + } + } + + folder_widget->show(); + add(*folder_widget); +} + +void GuildList::Clear() { + const auto children = get_children(); + for (auto *child : children) { + delete child; + } +} + +void GuildList::OnGuildSubmenuPopup() { + const auto id = m_menu_guild_target; + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.IsGuildMuted(id)) { + m_menu_guild_toggle_mute.set_label("Unmute"); + } else { + m_menu_guild_toggle_mute.set_label("Mute"); + } + + const auto guild = discord.GetGuild(id); + const auto self_id = discord.GetUserData().ID; + m_menu_guild_leave.set_sensitive(!(guild.has_value() && guild->OwnerID == self_id)); +} + +GuildList::type_signal_guild_selected GuildList::signal_guild_selected() { + return m_signal_guild_selected; +} + +GuildList::type_signal_dms_selected GuildList::signal_dms_selected() { + return m_signal_dms_selected; +} + +GuildList::type_signal_action_guild_leave GuildList::signal_action_guild_leave() { + return m_signal_action_guild_leave; +} + +GuildList::type_signal_action_guild_settings GuildList::signal_action_guild_settings() { + return m_signal_action_guild_settings; +} diff --git a/src/components/channellist/classic/guildlist.hpp b/src/components/channellist/classic/guildlist.hpp new file mode 100644 index 00000000..72e88e83 --- /dev/null +++ b/src/components/channellist/classic/guildlist.hpp @@ -0,0 +1,48 @@ +#pragma once +#include +#include "discord/snowflake.hpp" +#include "discord/usersettings.hpp" + +class GuildListGuildItem; + +class GuildList : public Gtk::ListBox { +public: + GuildList(); + + void UpdateListing(); + +private: + void AddGuild(Snowflake id); + void AddFolder(const UserSettingsGuildFoldersEntry &folder); + void Clear(); + + GuildListGuildItem *CreateGuildWidget(Snowflake id); + + // todo code duplication not good no sir + Gtk::Menu m_menu_guild; + Gtk::MenuItem m_menu_guild_copy_id; + Gtk::MenuItem m_menu_guild_settings; + Gtk::MenuItem m_menu_guild_leave; + Gtk::MenuItem m_menu_guild_mark_as_read; + Gtk::MenuItem m_menu_guild_toggle_mute; + Snowflake m_menu_guild_target; + + void OnGuildSubmenuPopup(); + +public: + using type_signal_guild_selected = sigc::signal; + using type_signal_dms_selected = sigc::signal; + using type_signal_action_guild_leave = sigc::signal; + using type_signal_action_guild_settings = sigc::signal; + + type_signal_guild_selected signal_guild_selected(); + type_signal_dms_selected signal_dms_selected(); + type_signal_action_guild_leave signal_action_guild_leave(); + type_signal_action_guild_settings signal_action_guild_settings(); + +private: + type_signal_guild_selected m_signal_guild_selected; + type_signal_dms_selected m_signal_dms_selected; + type_signal_action_guild_leave m_signal_action_guild_leave; + type_signal_action_guild_settings m_signal_action_guild_settings; +}; diff --git a/src/components/channellist/classic/guildlistfolderitem.cpp b/src/components/channellist/classic/guildlistfolderitem.cpp new file mode 100644 index 00000000..e062d42d --- /dev/null +++ b/src/components/channellist/classic/guildlistfolderitem.cpp @@ -0,0 +1,129 @@ +#include "guildlistfolderitem.hpp" + +#include "abaddon.hpp" +#include "guildlistguilditem.hpp" +#include "util.hpp" + +// doing my best to copy discord here + +const int FolderGridButtonSize = 48; +const int FolderGridImageSize = 24; + +GuildListFolderButton::GuildListFolderButton() { + set_size_request(FolderGridButtonSize, FolderGridButtonSize); +} + +void GuildListFolderButton::SetGuilds(const std::vector &guild_ids) { + for (int y = 0; y < 2; y++) { + for (int x = 0; x < 2; x++) { + const size_t i = y * 2 + x; + auto &widget = m_images[x][y]; + widget.property_pixbuf() = Abaddon::Get().GetImageManager().GetPlaceholder(FolderGridImageSize); + attach(widget, x, y, 1, 1); + + if (i < guild_ids.size()) { + widget.show(); + + if (const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(guild_ids[i]); guild.has_value() && guild->HasIcon()) { + const auto cb = [&widget](const Glib::RefPtr &pb) { + widget.property_pixbuf() = pb->scale_simple(FolderGridImageSize, FolderGridImageSize, Gdk::INTERP_BILINEAR); + }; + Abaddon::Get().GetImageManager().LoadFromURL(guild->GetIconURL("png", "32"), sigc::track_obj(cb, *this)); + } + } + } + } +} + +GuildListFolderItem::GuildListFolderItem(const UserSettingsGuildFoldersEntry &folder) { + m_guild_ids = folder.GuildIDs; + + get_style_context()->add_class("classic-guild-list-folder"); + + if (folder.Name.has_value()) { + set_tooltip_text(*folder.Name); + } + + m_revealer.add(m_box); + m_revealer.set_reveal_child(false); + + m_image.property_pixbuf() = Abaddon::Get().GetImageManager().GetPlaceholder(48); + + m_ev.signal_button_press_event().connect([this](GdkEventButton *event) -> bool { + if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) { + m_revealer.set_reveal_child(!m_revealer.get_reveal_child()); + if (!Abaddon::Get().GetSettings().FolderIconOnly) { + if (m_revealer.get_reveal_child()) { + m_stack.set_visible_child("icon", Gtk::STACK_TRANSITION_TYPE_SLIDE_DOWN); + } else { + m_stack.set_visible_child("grid", Gtk::STACK_TRANSITION_TYPE_SLIDE_UP); + } + } + } + + return false; + }); + + m_grid.SetGuilds(folder.GuildIDs); + m_grid.show(); + + m_icon.property_icon_name() = "folder-symbolic"; + m_icon.property_icon_size() = Gtk::ICON_SIZE_DND; + if (folder.Color.has_value()) { + m_icon.override_color(IntToRGBA(*folder.Color)); + } + m_icon.show(); + + m_stack.add(m_grid, "grid"); + m_stack.add(m_icon, "icon"); + m_stack.set_visible_child(Abaddon::Get().GetSettings().FolderIconOnly ? "icon" : "grid"); + m_stack.show(); + + m_ev.add(m_stack); + add(m_ev); + add(m_revealer); + + m_ev.show(); + m_revealer.show(); + m_box.show(); + m_image.show(); + show(); + + Abaddon::Get().GetDiscordClient().signal_message_create().connect(sigc::mem_fun(*this, &GuildListFolderItem::OnMessageCreate)); + Abaddon::Get().GetDiscordClient().signal_message_ack().connect(sigc::mem_fun(*this, &GuildListFolderItem::OnMessageAck)); + + CheckUnreadStatus(); +} + +void GuildListFolderItem::AddGuildWidget(GuildListGuildItem *widget) { + m_box.add(*widget); +} + +void GuildListFolderItem::OnMessageCreate(const Message &msg) { + if (msg.GuildID.has_value() && std::find(m_guild_ids.begin(), m_guild_ids.end(), *msg.GuildID) != m_guild_ids.end()) CheckUnreadStatus(); +} + +void GuildListFolderItem::OnMessageAck(const MessageAckData &data) { + CheckUnreadStatus(); +} + +void GuildListFolderItem::CheckUnreadStatus() { + auto &discord = Abaddon::Get().GetDiscordClient(); + if (!Abaddon::Get().GetSettings().Unreads) return; + + bool has_any_unreads = false; + + for (auto guild_id : m_guild_ids) { + int mentions; + if (!discord.IsGuildMuted(guild_id) && discord.GetUnreadStateForGuild(guild_id, mentions)) { + has_any_unreads = true; + break; + } + } + + if (has_any_unreads) { + get_style_context()->add_class("has-unread"); + } else { + get_style_context()->remove_class("has-unread"); + } +} diff --git a/src/components/channellist/classic/guildlistfolderitem.hpp b/src/components/channellist/classic/guildlistfolderitem.hpp new file mode 100644 index 00000000..e5772c00 --- /dev/null +++ b/src/components/channellist/classic/guildlistfolderitem.hpp @@ -0,0 +1,44 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +#include "guildlistguilditem.hpp" +#include "discord/usersettings.hpp" + +class GuildListGuildItem; + +class GuildListFolderButton : public Gtk::Grid { +public: + GuildListFolderButton(); + void SetGuilds(const std::vector &guild_ids); + +private: + Gtk::Image m_images[2][2]; +}; + +class GuildListFolderItem : public Gtk::VBox { +public: + GuildListFolderItem(const UserSettingsGuildFoldersEntry &folder); + + void AddGuildWidget(GuildListGuildItem *widget); + +private: + void OnMessageCreate(const Message &msg); + void OnMessageAck(const MessageAckData &data); + void CheckUnreadStatus(); + + std::vector m_guild_ids; + + Gtk::Stack m_stack; + GuildListFolderButton m_grid; + Gtk::Image m_icon; + + Gtk::EventBox m_ev; + Gtk::Image m_image; + Gtk::Revealer m_revealer; + Gtk::VBox m_box; +}; diff --git a/src/components/channellist/classic/guildlistguilditem.cpp b/src/components/channellist/classic/guildlistguilditem.cpp new file mode 100644 index 00000000..5b578be3 --- /dev/null +++ b/src/components/channellist/classic/guildlistguilditem.cpp @@ -0,0 +1,52 @@ +#include "guildlistguilditem.hpp" + +#include "abaddon.hpp" + +GuildListGuildItem::GuildListGuildItem(const GuildData &guild) + : ID(guild.ID) { + get_style_context()->add_class("classic-guild-list-guild"); + + m_image.property_pixbuf() = Abaddon::Get().GetImageManager().GetPlaceholder(48); + + add(m_box); + m_box.pack_start(m_image); + show_all_children(); + + set_tooltip_text(guild.Name); + + UpdateIcon(); + + Abaddon::Get().GetDiscordClient().signal_message_create().connect(sigc::mem_fun(*this, &GuildListGuildItem::OnMessageCreate)); + Abaddon::Get().GetDiscordClient().signal_message_ack().connect(sigc::mem_fun(*this, &GuildListGuildItem::OnMessageAck)); + + CheckUnreadStatus(); +} + +void GuildListGuildItem::UpdateIcon() { + const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(ID); + if (!guild.has_value() || !guild->HasIcon()) return; + Abaddon::Get().GetImageManager().LoadFromURL(guild->GetIconURL("png", "64"), sigc::mem_fun(*this, &GuildListGuildItem::OnIconFetched)); +} + +void GuildListGuildItem::OnIconFetched(const Glib::RefPtr &pb) { + m_image.property_pixbuf() = pb->scale_simple(48, 48, Gdk::INTERP_BILINEAR); +} + +void GuildListGuildItem::OnMessageCreate(const Message &msg) { + if (msg.GuildID.has_value() && *msg.GuildID == ID) CheckUnreadStatus(); +} + +void GuildListGuildItem::OnMessageAck(const MessageAckData &data) { + CheckUnreadStatus(); +} + +void GuildListGuildItem::CheckUnreadStatus() { + auto &discord = Abaddon::Get().GetDiscordClient(); + if (!Abaddon::Get().GetSettings().Unreads) return; + int mentions; + if (!discord.IsGuildMuted(ID) && discord.GetUnreadStateForGuild(ID, mentions)) { + get_style_context()->add_class("has-unread"); + } else { + get_style_context()->remove_class("has-unread"); + } +} diff --git a/src/components/channellist/classic/guildlistguilditem.hpp b/src/components/channellist/classic/guildlistguilditem.hpp new file mode 100644 index 00000000..6e2b2415 --- /dev/null +++ b/src/components/channellist/classic/guildlistguilditem.hpp @@ -0,0 +1,22 @@ +#pragma once +#include +#include +#include +#include "discord/guild.hpp" + +class GuildListGuildItem : public Gtk::EventBox { +public: + GuildListGuildItem(const GuildData &guild); + + Snowflake ID; + +private: + void UpdateIcon(); + void OnIconFetched(const Glib::RefPtr &pb); + void OnMessageCreate(const Message &msg); + void OnMessageAck(const MessageAckData &data); + void CheckUnreadStatus(); + + Gtk::Box m_box; + Gtk::Image m_image; +}; diff --git a/src/settings.cpp b/src/settings.cpp index 78213644..fc76ddb5 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -99,8 +99,11 @@ void SettingsManager::DefineSettings() { AddSetting("gui", "hide_to_try", false, &Settings::HideToTray); AddSetting("gui", "show_deleted_indicator", true, &Settings::ShowDeletedIndicator); AddSetting("gui", "font_scale", -1.0, &Settings::FontScale); + AddSetting("gui", "folder_icon_only", false, &Settings::FolderIconOnly); + AddSetting("gui", "classic_change_guild_on_open", true, &Settings::ClassicChangeGuildOnOpen); AddSetting("gui", "image_embed_clamp_width", 400, &Settings::ImageEmbedClampWidth); AddSetting("gui", "image_embed_clamp_height", 300, &Settings::ImageEmbedClampHeight); + AddSetting("gui", "classic_channels", false, &Settings::ClassicChannels); AddSetting("http", "concurrent", 20, &Settings::CacheHTTPConcurrency); AddSetting("http", "user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36"s, &Settings::UserAgent); diff --git a/src/settings.hpp b/src/settings.hpp index 72b26824..5805452b 100644 --- a/src/settings.hpp +++ b/src/settings.hpp @@ -28,8 +28,11 @@ class SettingsManager { bool HideToTray; bool ShowDeletedIndicator; double FontScale; + bool FolderIconOnly; + bool ClassicChangeGuildOnOpen; int ImageEmbedClampWidth; int ImageEmbedClampHeight; + bool ClassicChannels; // [http] int CacheHTTPConcurrency; diff --git a/src/windows/mainwindow.cpp b/src/windows/mainwindow.cpp index b0f22e3d..ee28d175 100644 --- a/src/windows/mainwindow.cpp +++ b/src/windows/mainwindow.cpp @@ -36,6 +36,7 @@ MainWindow::MainWindow() }); #endif + m_channel_list.SetClassic(Abaddon::Get().GetSettings().ClassicChannels); m_channel_list.set_vexpand(true); m_channel_list.set_size_request(-1, -1); m_channel_list.show(); @@ -395,7 +396,7 @@ void MainWindow::SetupMenu() { }); m_menu_view_channels.signal_activate().connect([this]() { - m_channel_list.set_visible(m_menu_view_channels.get_active()); + m_left_pane.set_visible(m_menu_view_channels.get_active()); }); m_menu_view_members.signal_activate().connect([this]() { diff --git a/src/windows/mainwindow.hpp b/src/windows/mainwindow.hpp index ce2e6366..37c1b87a 100644 --- a/src/windows/mainwindow.hpp +++ b/src/windows/mainwindow.hpp @@ -1,5 +1,5 @@ #pragma once -#include "components/channels.hpp" +#include "components/channellist/channellist.hpp" #include "components/chatwindow.hpp" #include "components/memberlist.hpp" #include "components/friendslist.hpp"