1
0
mirror of https://github.com/uowuo/abaddon.git synced 2025-03-04 03:03:16 -05:00

Merge pull request #242 from uowuo/classic-channels

Discord-style channel/server listing
This commit is contained in:
ouwou 2024-03-08 19:36:52 -05:00 committed by GitHub
commit 43f87ae381
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1061 additions and 146 deletions

View File

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

View File

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

View File

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

View File

@ -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(?)

View File

@ -1,4 +1,4 @@
#include "channelscellrenderer.hpp"
#include "cellrendererchannels.hpp"
#include <gdkmm/general.h>
#include "abaddon.hpp"

View File

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

View File

@ -0,0 +1,71 @@
#pragma once
#include <gtkmm/box.h>
#include <gtkmm/paned.h>
#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<void, Snowflake>;
using type_signal_action_guild_leave = sigc::signal<void, Snowflake>;
using type_signal_action_guild_settings = sigc::signal<void, Snowflake>;
#ifdef WITH_LIBHANDY
using type_signal_action_open_new_tab = sigc::signal<void, Snowflake>;
type_signal_action_open_new_tab signal_action_open_new_tab();
#endif
#ifdef WITH_VOICE
using type_signal_action_join_voice_channel = sigc::signal<void, Snowflake>;
using type_signal_action_disconnect_voice = sigc::signal<void>;
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
};

View File

@ -1,14 +1,20 @@
#include "channels.hpp"
#include "imgmanager.hpp"
#include "channellisttree.hpp"
#include <algorithm>
#include <map>
#include <unordered_map>
#include <gtkmm/main.h>
#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 ChannelList::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<int>(std::clamp(a_sort - b_sort, int64_t(-1), int64_t(1)));
}
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<Snowflake> 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<Snowflake>(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<Snowflake>(child[m_columns.m_id]);
if (static_cast<uint64_t>(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<Gtk::TreeModel::iterator> 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<Gtk::TreeModel::iterator> 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<Gtk::TreeModel> &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<Gtk::TreeModel> &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<UserData> 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<int>(ev->x), static_cast<int>(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<RenderType>(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<Snowflake>((*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<Snowflake>((*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<Snowflake>((*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<Snowflake>((*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<Snowflake>((*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);

View File

@ -8,20 +8,21 @@
#include <gtkmm/scrolledwindow.h>
#include <gtkmm/treemodel.h>
#include <gtkmm/treestore.h>
#include <gtkmm/treemodelfilter.h>
#include <gtkmm/treeview.h>
#include <sigc++/sigc++.h>
#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 @@ public:
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 @@ protected:
ModelColumns m_columns;
Glib::RefPtr<Gtk::TreeStore> m_model;
Glib::RefPtr<Gtk::TreeModelFilter> m_filter_model;
Glib::RefPtr<Gtk::TreeModelSort> 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 @@ protected:
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 @@ protected:
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<Snowflake, Gtk::TreeModel::iterator> m_tmp_row_map;
std::unordered_map<Snowflake, Gtk::TreeModel::iterator> m_tmp_guild_row_map;

View File

@ -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<GuildListDMsButton>();
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<Snowflake> 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<GuildListGuildItem>(*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<GdkEvent *>(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<GuildListFolderItem>(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;
}

View File

@ -0,0 +1,48 @@
#pragma once
#include <gtkmm/listbox.h>
#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<void, Snowflake>;
using type_signal_dms_selected = sigc::signal<void>;
using type_signal_action_guild_leave = sigc::signal<void, Snowflake>;
using type_signal_action_guild_settings = sigc::signal<void, Snowflake>;
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;
};

View File

@ -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<Snowflake> &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<Gdk::Pixbuf> &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");
}
}

View File

@ -0,0 +1,44 @@
#pragma once
#include <gtkmm/box.h>
#include <gtkmm/eventbox.h>
#include <gtkmm/grid.h>
#include <gtkmm/image.h>
#include <gtkmm/revealer.h>
#include <gtkmm/stack.h>
#include "guildlistguilditem.hpp"
#include "discord/usersettings.hpp"
class GuildListGuildItem;
class GuildListFolderButton : public Gtk::Grid {
public:
GuildListFolderButton();
void SetGuilds(const std::vector<Snowflake> &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<Snowflake> 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;
};

View File

@ -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<Gdk::Pixbuf> &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");
}
}

View File

@ -0,0 +1,22 @@
#pragma once
#include <gtkmm/box.h>
#include <gtkmm/eventbox.h>
#include <gtkmm/image.h>
#include "discord/guild.hpp"
class GuildListGuildItem : public Gtk::EventBox {
public:
GuildListGuildItem(const GuildData &guild);
Snowflake ID;
private:
void UpdateIcon();
void OnIconFetched(const Glib::RefPtr<Gdk::Pixbuf> &pb);
void OnMessageCreate(const Message &msg);
void OnMessageAck(const MessageAckData &data);
void CheckUnreadStatus();
Gtk::Box m_box;
Gtk::Image m_image;
};

View File

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

View File

@ -28,8 +28,11 @@ public:
bool HideToTray;
bool ShowDeletedIndicator;
double FontScale;
bool FolderIconOnly;
bool ClassicChangeGuildOnOpen;
int ImageEmbedClampWidth;
int ImageEmbedClampHeight;
bool ClassicChannels;
// [http]
int CacheHTTPConcurrency;

View File

@ -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]() {

View File

@ -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"