Files
abaddon/src/components/channels.cpp

794 lines
33 KiB
C++

#include "channels.hpp"
#include <algorithm>
#include <map>
#include <unordered_map>
#include "abaddon.hpp"
#include "imgmanager.hpp"
#include "util.hpp"
#include "statusindicator.hpp"
ChannelList::ChannelList()
: Glib::ObjectBase(typeid(ChannelList))
, Gtk::ScrolledWindow()
, m_model(Gtk::TreeStore::create(m_columns))
, m_menu_guild_copy_id("_Copy ID", true)
, m_menu_guild_settings("View _Settings", true)
, m_menu_guild_leave("_Leave", true)
, m_menu_category_copy_id("_Copy ID", true)
, m_menu_channel_copy_id("_Copy ID", true)
, m_menu_dm_copy_id("_Copy ID", true)
, m_menu_dm_close("") // changes depending on if group or not
, m_menu_thread_copy_id("_Copy ID", true)
, m_menu_thread_leave("_Leave", true)
, m_menu_thread_archive("_Archive", true)
, m_menu_thread_unarchive("_Unarchive", true) {
get_style_context()->add_class("channel-list");
const auto cb = [this](const Gtk::TreeModel::Path &path, Gtk::TreeViewColumn *column) {
auto row = *m_model->get_iter(path);
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
if (type != RenderType::TextChannel) {
if (row[m_columns.m_expanded]) {
m_view.collapse_row(path);
row[m_columns.m_expanded] = false;
} else {
m_view.expand_row(path, false);
row[m_columns.m_expanded] = true;
}
}
if (type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread) {
m_signal_action_channel_item_select.emit(static_cast<Snowflake>(row[m_columns.m_id]));
}
};
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.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.set_hexpand(true);
m_view.set_vexpand(true);
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_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);
});
m_view.show();
add(m_view);
auto *column = Gtk::manage(new Gtk::TreeView::Column("display"));
auto *renderer = Gtk::manage(new CellRendererChannels);
column->pack_start(*renderer);
column->add_attribute(renderer->property_type(), m_columns.m_type);
column->add_attribute(renderer->property_icon(), m_columns.m_icon);
column->add_attribute(renderer->property_icon_animation(), m_columns.m_icon_anim);
column->add_attribute(renderer->property_name(), m_columns.m_name);
column->add_attribute(renderer->property_expanded(), m_columns.m_expanded);
column->add_attribute(renderer->property_nsfw(), m_columns.m_nsfw);
m_view.append_column(*column);
m_menu_guild_copy_id.signal_activate().connect([this] {
Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
});
m_menu_guild_settings.signal_activate().connect([this] {
m_signal_action_guild_settings.emit(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
});
m_menu_guild_leave.signal_activate().connect([this] {
m_signal_action_guild_leave.emit(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
});
m_menu_guild.append(m_menu_guild_copy_id);
m_menu_guild.append(m_menu_guild_settings);
m_menu_guild.append(m_menu_guild_leave);
m_menu_guild.show_all();
m_menu_category_copy_id.signal_activate().connect([this] {
Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
});
m_menu_category.append(m_menu_category_copy_id);
m_menu_category.show_all();
m_menu_channel_copy_id.signal_activate().connect([this] {
Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
});
m_menu_channel.append(m_menu_channel_copy_id);
m_menu_channel.show_all();
m_menu_dm_copy_id.signal_activate().connect([this] {
Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
});
m_menu_dm_close.signal_activate().connect([this] {
const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
auto &discord = Abaddon::Get().GetDiscordClient();
const auto channel = discord.GetChannel(id);
if (!channel.has_value()) return;
if (channel->Type == ChannelType::DM)
discord.CloseDM(id);
else if (Abaddon::Get().ShowConfirm("Are you sure you want to leave this group DM?"))
Abaddon::Get().GetDiscordClient().CloseDM(id);
});
m_menu_dm.append(m_menu_dm_copy_id);
m_menu_dm.append(m_menu_dm_close);
m_menu_dm.show_all();
m_menu_thread_copy_id.signal_activate().connect([this] {
Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
});
m_menu_thread_leave.signal_activate().connect([this] {
if (Abaddon::Get().ShowConfirm("Are you sure you want to leave this thread?"))
Abaddon::Get().GetDiscordClient().LeaveThread(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), "Context%20Menu", [](...) {});
});
m_menu_thread_archive.signal_activate().connect([this] {
Abaddon::Get().GetDiscordClient().ArchiveThread(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {});
});
m_menu_thread_unarchive.signal_activate().connect([this] {
Abaddon::Get().GetDiscordClient().UnArchiveThread(static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]), [](...) {});
});
m_menu_thread.append(m_menu_thread_copy_id);
m_menu_thread.append(m_menu_thread_leave);
m_menu_thread.append(m_menu_thread_archive);
m_menu_thread.append(m_menu_thread_unarchive);
m_menu_thread.show_all();
m_menu_thread.signal_popped_up().connect(sigc::mem_fun(*this, &ChannelList::OnThreadSubmenuPopup));
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));
}
void ChannelList::UpdateListing() {
m_updating_listing = true;
m_model->clear();
auto &discord = Abaddon::Get().GetDiscordClient();
const auto guild_ids = discord.GetUserSortedGuilds();
int sortnum = 0;
for (const auto &guild_id : guild_ids) {
const auto guild = discord.GetGuild(guild_id);
if (!guild.has_value()) continue;
auto iter = AddGuild(*guild);
(*iter)[m_columns.m_sort] = sortnum++;
}
m_updating_listing = false;
AddPrivateChannels();
}
void ChannelList::UpdateNewGuild(const GuildData &guild) {
AddGuild(guild);
// update sort order
int sortnum = 0;
for (const auto guild_id : Abaddon::Get().GetDiscordClient().GetUserSortedGuilds()) {
auto iter = GetIteratorForGuildFromID(guild_id);
if (iter)
(*iter)[m_columns.m_sort] = ++sortnum;
}
}
void ChannelList::UpdateRemoveGuild(Snowflake id) {
auto iter = GetIteratorForGuildFromID(id);
if (!iter) return;
m_model->erase(iter);
}
void ChannelList::UpdateRemoveChannel(Snowflake id) {
auto iter = GetIteratorForChannelFromID(id);
if (!iter) return;
m_model->erase(iter);
}
void ChannelList::UpdateChannel(Snowflake id) {
auto iter = GetIteratorForChannelFromID(id);
auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id);
if (!iter || !channel.has_value()) return;
if (channel->Type == ChannelType::GUILD_CATEGORY) return UpdateChannelCategory(*channel);
if (!IsTextChannel(channel->Type)) return;
// refresh stuff that might have changed
const bool is_orphan_TMP = !channel->ParentID.has_value();
(*iter)[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel->Name);
(*iter)[m_columns.m_nsfw] = channel->NSFW();
(*iter)[m_columns.m_sort] = *channel->Position + (is_orphan_TMP ? OrphanChannelSortOffset : 0);
// check if the parent has changed
Gtk::TreeModel::iterator new_parent;
if (channel->ParentID.has_value())
new_parent = GetIteratorForChannelFromID(*channel->ParentID);
else if (channel->GuildID.has_value())
new_parent = GetIteratorForGuildFromID(*channel->GuildID);
if (new_parent && iter->parent() != new_parent)
MoveRow(iter, new_parent);
}
void ChannelList::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;
Gtk::TreeRow channel_row;
bool orphan;
if (channel.ParentID.has_value()) {
orphan = false;
auto iter = GetIteratorForChannelFromID(*channel.ParentID);
channel_row = *m_model->append(iter->children());
} else {
orphan = true;
auto iter = GetIteratorForGuildFromID(*channel.GuildID);
channel_row = *m_model->append(iter->children());
}
channel_row[m_columns.m_type] = RenderType::TextChannel;
channel_row[m_columns.m_id] = channel.ID;
channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name);
channel_row[m_columns.m_nsfw] = channel.NSFW();
if (orphan)
channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset;
else
channel_row[m_columns.m_sort] = *channel.Position;
}
void ChannelList::UpdateGuild(Snowflake id) {
auto iter = GetIteratorForGuildFromID(id);
auto &img = Abaddon::Get().GetImageManager();
const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(id);
if (!iter || !guild.has_value()) return;
(*iter)[m_columns.m_name] = "<b>" + Glib::Markup::escape_text(guild->Name) + "</b>";
(*iter)[m_columns.m_icon] = img.GetPlaceholder(GuildIconSize);
if (Abaddon::Get().GetSettings().ShowAnimations && guild->HasAnimatedIcon()) {
const auto cb = [this, id](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) {
auto iter = GetIteratorForGuildFromID(id);
if (iter) (*iter)[m_columns.m_icon_anim] = pb;
};
img.LoadAnimationFromURL(guild->GetIconURL("gif", "32"), GuildIconSize, GuildIconSize, sigc::track_obj(cb, *this));
} else if (guild->HasIcon()) {
const auto cb = [this, id](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
// iter might be invalid
auto iter = GetIteratorForGuildFromID(id);
if (iter) (*iter)[m_columns.m_icon] = pb->scale_simple(GuildIconSize, GuildIconSize, Gdk::INTERP_BILINEAR);
};
img.LoadFromURL(guild->GetIconURL("png", "32"), sigc::track_obj(cb, *this));
}
}
void ChannelList::OnThreadJoined(Snowflake id) {
if (GetIteratorForChannelFromID(id)) return;
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id);
if (!channel.has_value()) return;
const auto parent = GetIteratorForChannelFromID(*channel->ParentID);
if (parent)
CreateThreadRow(parent->children(), *channel);
}
void ChannelList::OnThreadRemoved(Snowflake id) {
DeleteThreadRow(id);
}
void ChannelList::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) {
auto iter = GetIteratorForChannelFromID(data.Thread.ID);
if (iter)
(*iter)[m_columns.m_name] = "- " + Glib::Markup::escape_text(*data.Thread.Name);
if (data.Thread.ThreadMetadata->IsArchived)
DeleteThreadRow(data.Thread.ID);
}
void ChannelList::OnThreadListSync(const ThreadListSyncData &data) {
// get the threads in the guild
std::vector<Snowflake> threads;
auto guild_iter = GetIteratorForGuildFromID(data.GuildID);
std::queue<Gtk::TreeModel::iterator> queue;
queue.push(guild_iter);
while (!queue.empty()) {
auto item = queue.front();
queue.pop();
if ((*item)[m_columns.m_type] == RenderType::Thread)
threads.push_back(static_cast<Snowflake>((*item)[m_columns.m_id]));
for (auto child : item->children())
queue.push(child);
}
// delete all threads not present in the synced data
for (auto thread_id : threads) {
if (std::find_if(data.Threads.begin(), data.Threads.end(), [thread_id](const auto &x) { return x.ID == thread_id; }) == data.Threads.end()) {
auto iter = GetIteratorForChannelFromID(thread_id);
m_model->erase(iter);
}
}
// delete all archived threads
for (auto thread : data.Threads) {
if (thread.ThreadMetadata->IsArchived) {
if (auto iter = GetIteratorForChannelFromID(thread.ID))
m_model->erase(iter);
}
}
}
void ChannelList::DeleteThreadRow(Snowflake id) {
auto iter = GetIteratorForChannelFromID(id);
if (iter)
m_model->erase(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) {
if (m_temporary_thread_row) {
const auto thread_id = static_cast<Snowflake>((*m_temporary_thread_row)[m_columns.m_id]);
const auto thread = Abaddon::Get().GetDiscordClient().GetChannel(thread_id);
if (thread.has_value() && (!thread->IsJoinedThread() || thread->ThreadMetadata->IsArchived))
m_model->erase(m_temporary_thread_row);
m_temporary_thread_row = {};
}
const auto channel_iter = GetIteratorForChannelFromID(id);
if (channel_iter) {
m_view.expand_to_path(m_model->get_path(channel_iter));
m_view.get_selection()->select(channel_iter);
} else {
m_view.get_selection()->unselect_all();
// SetActiveChannel should probably just take the channel object
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id);
if (!channel.has_value() || !channel->IsThread()) return;
auto parent_iter = GetIteratorForChannelFromID(*channel->ParentID);
if (!parent_iter) return;
m_temporary_thread_row = CreateThreadRow(parent_iter->children(), *channel);
m_view.get_selection()->select(m_temporary_thread_row);
}
}
void ChannelList::UseExpansionState(const ExpansionStateRoot &root) {
auto recurse = [this](auto &self, const ExpansionStateRoot &root) -> void {
// and these are only channels
for (const auto &[id, state] : root.Children) {
if (const auto iter = GetIteratorForChannelFromID(id)) {
if (state.IsExpanded)
m_view.expand_row(m_model->get_path(iter), false);
else
m_view.collapse_row(m_model->get_path(iter));
}
self(self, state.Children);
}
};
// top level is guild
for (const auto &[id, state] : root.Children) {
if (const auto iter = GetIteratorForGuildFromID(id)) {
if (state.IsExpanded)
m_view.expand_row(m_model->get_path(iter), false);
else
m_view.collapse_row(m_model->get_path(iter));
}
recurse(recurse, state.Children);
}
}
ExpansionStateRoot ChannelList::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())
r.Children.Children[static_cast<Snowflake>(child[m_columns.m_id])] = self(self, child);
return r;
};
for (const 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);
}
return r;
}
Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
auto &discord = Abaddon::Get().GetDiscordClient();
auto &img = Abaddon::Get().GetImageManager();
auto guild_row = *m_model->append();
guild_row[m_columns.m_type] = RenderType::Guild;
guild_row[m_columns.m_id] = guild.ID;
guild_row[m_columns.m_name] = "<b>" + Glib::Markup::escape_text(guild.Name) + "</b>";
guild_row[m_columns.m_icon] = img.GetPlaceholder(GuildIconSize);
if (Abaddon::Get().GetSettings().ShowAnimations && guild.HasAnimatedIcon()) {
const auto cb = [this, id = guild.ID](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) {
auto iter = GetIteratorForGuildFromID(id);
if (iter) (*iter)[m_columns.m_icon_anim] = pb;
};
img.LoadAnimationFromURL(guild.GetIconURL("gif", "32"), GuildIconSize, GuildIconSize, sigc::track_obj(cb, *this));
} else if (guild.HasIcon()) {
const auto cb = [this, id = guild.ID](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
auto iter = GetIteratorForGuildFromID(id);
if (iter) (*iter)[m_columns.m_icon] = pb->scale_simple(GuildIconSize, GuildIconSize, Gdk::INTERP_BILINEAR);
};
img.LoadFromURL(guild.GetIconURL("png", "32"), sigc::track_obj(cb, *this));
}
if (!guild.Channels.has_value()) return guild_row;
// separate out the channels
std::vector<ChannelData> orphan_channels;
std::map<Snowflake, std::vector<ChannelData>> categories;
for (const auto &channel_ : *guild.Channels) {
const auto channel = discord.GetChannel(channel_.ID);
if (!channel.has_value()) continue;
if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS) {
if (channel->ParentID.has_value())
categories[*channel->ParentID].push_back(*channel);
else
orphan_channels.push_back(*channel);
} else if (channel->Type == ChannelType::GUILD_CATEGORY) {
categories[channel->ID];
}
}
std::map<Snowflake, std::vector<ChannelData>> threads;
for (const auto &tmp : *guild.Threads) {
const auto thread = discord.GetChannel(tmp.ID);
if (thread.has_value())
threads[*thread->ParentID].push_back(*thread);
}
const auto add_threads = [&](const ChannelData &channel, Gtk::TreeRow row) {
row[m_columns.m_expanded] = true;
const auto it = threads.find(channel.ID);
if (it == threads.end()) return;
for (const auto &thread : it->second)
CreateThreadRow(row.children(), thread);
};
for (const auto &channel : orphan_channels) {
auto channel_row = *m_model->append(guild_row.children());
channel_row[m_columns.m_type] = RenderType::TextChannel;
channel_row[m_columns.m_id] = channel.ID;
channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name);
channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset;
channel_row[m_columns.m_nsfw] = channel.NSFW();
add_threads(channel, channel_row);
}
for (const auto &[category_id, channels] : categories) {
const auto category = discord.GetChannel(category_id);
if (!category.has_value()) continue;
auto cat_row = *m_model->append(guild_row.children());
cat_row[m_columns.m_type] = RenderType::Category;
cat_row[m_columns.m_id] = category_id;
cat_row[m_columns.m_name] = Glib::Markup::escape_text(*category->Name);
cat_row[m_columns.m_sort] = *category->Position;
cat_row[m_columns.m_expanded] = true;
// m_view.expand_row wont work because it might not have channels
for (const auto &channel : channels) {
auto channel_row = *m_model->append(cat_row.children());
channel_row[m_columns.m_type] = RenderType::TextChannel;
channel_row[m_columns.m_id] = channel.ID;
channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name);
channel_row[m_columns.m_sort] = *channel.Position;
channel_row[m_columns.m_nsfw] = channel.NSFW();
add_threads(channel, channel_row);
}
}
return guild_row;
}
Gtk::TreeModel::iterator ChannelList::UpdateCreateChannelCategory(const ChannelData &channel) {
const auto iter = GetIteratorForGuildFromID(*channel.GuildID);
if (!iter) return {};
auto cat_row = *m_model->append(iter->children());
cat_row[m_columns.m_type] = RenderType::Category;
cat_row[m_columns.m_id] = channel.ID;
cat_row[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name);
cat_row[m_columns.m_sort] = *channel.Position;
cat_row[m_columns.m_expanded] = true;
return cat_row;
}
Gtk::TreeModel::iterator ChannelList::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;
thread_row[m_columns.m_id] = channel.ID;
thread_row[m_columns.m_name] = "- " + Glib::Markup::escape_text(*channel.Name);
thread_row[m_columns.m_sort] = channel.ID;
thread_row[m_columns.m_nsfw] = false;
return thread_iter;
}
void ChannelList::UpdateChannelCategory(const ChannelData &channel) {
auto iter = GetIteratorForChannelFromID(channel.ID);
if (!iter) return;
(*iter)[m_columns.m_sort] = *channel.Position;
(*iter)[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name);
}
Gtk::TreeModel::iterator ChannelList::GetIteratorForGuildFromID(Snowflake id) {
for (const auto &child : m_model->children()) {
if (child[m_columns.m_id] == id)
return child;
}
return {};
}
Gtk::TreeModel::iterator ChannelList::GetIteratorForChannelFromID(Snowflake id) {
std::queue<Gtk::TreeModel::iterator> queue;
for (const auto &child : m_model->children())
for (const auto &child2 : child.children())
queue.push(child2);
while (!queue.empty()) {
auto item = queue.front();
if ((*item)[m_columns.m_id] == id) return item;
for (const auto &child : item->children())
queue.push(child);
queue.pop();
}
return {};
}
bool ChannelList::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) {
(*iter)[m_columns.m_expanded] = false;
}
void ChannelList::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);
}
// try and restore selection if previous collapsed
if (auto selection = m_view.get_selection(); selection && !selection->get_selected()) {
selection->select(m_last_selected);
}
(*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);
auto type = (*m_model->get_iter(path))[m_columns.m_type];
return type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread;
}
void ChannelList::AddPrivateChannels() {
auto header_row = *m_model->append();
header_row[m_columns.m_type] = RenderType::DMHeader;
header_row[m_columns.m_sort] = -1;
header_row[m_columns.m_name] = "<b>Direct Messages</b>";
m_dm_header = m_model->get_path(header_row);
auto &discord = Abaddon::Get().GetDiscordClient();
auto &img = Abaddon::Get().GetImageManager();
const auto dm_ids = discord.GetPrivateChannels();
for (const auto dm_id : dm_ids) {
const auto dm = discord.GetChannel(dm_id);
if (!dm.has_value()) continue;
std::optional<UserData> top_recipient;
const auto recipients = dm->GetDMRecipients();
if (recipients.size() > 0)
top_recipient = recipients[0];
auto iter = m_model->append(header_row->children());
auto row = *iter;
row[m_columns.m_type] = RenderType::DM;
row[m_columns.m_id] = dm_id;
row[m_columns.m_sort] = -(dm->LastMessageID.has_value() ? *dm->LastMessageID : dm_id);
row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize);
if (dm->Type == ChannelType::DM && top_recipient.has_value())
row[m_columns.m_name] = Glib::Markup::escape_text(top_recipient->Username);
else if (dm->Type == ChannelType::GROUP_DM)
row[m_columns.m_name] = std::to_string(recipients.size()) + " members";
if (top_recipient.has_value()) {
const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
if (iter)
(*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR);
};
img.LoadFromURL(top_recipient->GetAvatarURL("png", "32"), sigc::track_obj(cb, *this));
}
}
}
void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) {
auto header_row = m_model->get_iter(m_dm_header);
auto &img = Abaddon::Get().GetImageManager();
std::optional<UserData> top_recipient;
const auto recipients = dm.GetDMRecipients();
if (recipients.size() > 0)
top_recipient = recipients[0];
auto iter = m_model->append(header_row->children());
auto row = *iter;
row[m_columns.m_type] = RenderType::DM;
row[m_columns.m_id] = dm.ID;
row[m_columns.m_sort] = -(dm.LastMessageID.has_value() ? *dm.LastMessageID : dm.ID);
row[m_columns.m_icon] = img.GetPlaceholder(DMIconSize);
if (dm.Type == ChannelType::DM && top_recipient.has_value())
row[m_columns.m_name] = Glib::Markup::escape_text(top_recipient->Username);
else if (dm.Type == ChannelType::GROUP_DM)
row[m_columns.m_name] = std::to_string(recipients.size()) + " members";
if (top_recipient.has_value()) {
const auto cb = [this, iter](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
if (iter)
(*iter)[m_columns.m_icon] = pb->scale_simple(DMIconSize, DMIconSize, Gdk::INTERP_BILINEAR);
};
img.LoadFromURL(top_recipient->GetAvatarURL("png", "32"), sigc::track_obj(cb, *this));
}
}
void ChannelList::OnMessageCreate(const Message &msg) {
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(msg.ChannelID);
if (!channel.has_value()) return;
if (channel->Type != ChannelType::DM && channel->Type != ChannelType::GROUP_DM) return;
auto iter = GetIteratorForChannelFromID(msg.ChannelID);
if (iter)
(*iter)[m_columns.m_sort] = -msg.ID;
}
bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) {
if (ev->button == GDK_BUTTON_SECONDARY && ev->type == GDK_BUTTON_PRESS) {
if (m_view.get_path_at_pos(ev->x, ev->y, m_path_for_menu)) {
auto row = (*m_model->get_iter(m_path_for_menu));
switch (static_cast<RenderType>(row[m_columns.m_type])) {
case RenderType::Guild:
m_menu_guild.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
break;
case RenderType::Category:
m_menu_category.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
break;
case RenderType::TextChannel:
m_menu_channel.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
break;
case RenderType::DM: {
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(static_cast<Snowflake>(row[m_columns.m_id]));
if (channel.has_value()) {
m_menu_dm_close.set_label(channel->Type == ChannelType::DM ? "Close" : "Leave");
m_menu_dm_close.show();
} else
m_menu_dm_close.hide();
m_menu_dm.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
} break;
case RenderType::Thread: {
m_menu_thread.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
break;
} break;
default:
break;
}
}
return true;
}
return false;
}
void ChannelList::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
#define M(name) \
row[m_columns.name] = static_cast<decltype(m_columns.name)::ElementType>((*iter)[m_columns.name]);
M(m_type);
M(m_id);
M(m_name);
M(m_icon);
M(m_icon_anim);
M(m_sort);
M(m_nsfw);
M(m_expanded);
#undef M
// recursively move children
// weird construct to work around iterator invalidation (at least i think thats what the problem was)
const auto tmp = iter->children();
const auto children = std::vector<Gtk::TreeRow>(tmp.begin(), tmp.end());
for (size_t i = 0; i < children.size(); i++)
MoveRow(children[i], row);
// delete original
m_model->erase(iter);
}
void ChannelList::OnThreadSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y) {
m_menu_thread_archive.set_visible(false);
m_menu_thread_unarchive.set_visible(false);
auto &discord = Abaddon::Get().GetDiscordClient();
auto iter = m_model->get_iter(m_path_for_menu);
if (!iter) return;
auto channel = discord.GetChannel(static_cast<Snowflake>((*iter)[m_columns.m_id]));
if (!channel.has_value() || !channel->ThreadMetadata.has_value()) return;
if (!discord.HasGuildPermission(discord.GetUserData().ID, *channel->GuildID, Permission::MANAGE_THREADS)) return;
m_menu_thread_archive.set_visible(!channel->ThreadMetadata->IsArchived);
m_menu_thread_unarchive.set_visible(channel->ThreadMetadata->IsArchived);
}
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;
}
ChannelList::ModelColumns::ModelColumns() {
add(m_type);
add(m_id);
add(m_name);
add(m_icon);
add(m_icon_anim);
add(m_sort);
add(m_nsfw);
add(m_expanded);
}