forked from OpenGamers/abaddon
Merge branch 'threads'
This commit is contained in:
38
abaddon.cpp
38
abaddon.cpp
@@ -15,6 +15,7 @@
|
||||
#include "windows/guildsettingswindow.hpp"
|
||||
#include "windows/profilewindow.hpp"
|
||||
#include "windows/pinnedwindow.hpp"
|
||||
#include "windows/threadswindow.hpp"
|
||||
|
||||
#ifdef _WIN32
|
||||
#pragma comment(lib, "crypt32.lib")
|
||||
@@ -35,9 +36,11 @@ Abaddon::Abaddon()
|
||||
m_discord.signal_message_delete().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageDelete));
|
||||
m_discord.signal_message_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageUpdate));
|
||||
m_discord.signal_guild_member_list_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnGuildMemberListUpdate));
|
||||
m_discord.signal_thread_member_list_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnThreadMemberListUpdate));
|
||||
m_discord.signal_reaction_add().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnReactionAdd));
|
||||
m_discord.signal_reaction_remove().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnReactionRemove));
|
||||
m_discord.signal_guild_join_request_create().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnGuildJoinRequestCreate));
|
||||
m_discord.signal_thread_update().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnThreadUpdate));
|
||||
m_discord.signal_message_sent().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnMessageSent));
|
||||
m_discord.signal_disconnected().connect(sigc::mem_fun(*this, &Abaddon::DiscordOnDisconnect));
|
||||
if (m_settings.GetPrefetch())
|
||||
@@ -94,6 +97,7 @@ int Abaddon::StartGTK() {
|
||||
m_main_window->signal_action_set_status().connect(sigc::mem_fun(*this, &Abaddon::ActionSetStatus));
|
||||
m_main_window->signal_action_add_recipient().connect(sigc::mem_fun(*this, &Abaddon::ActionAddRecipient));
|
||||
m_main_window->signal_action_view_pins().connect(sigc::mem_fun(*this, &Abaddon::ActionViewPins));
|
||||
m_main_window->signal_action_view_threads().connect(sigc::mem_fun(*this, &Abaddon::ActionViewThreads));
|
||||
|
||||
m_main_window->GetChannelList()->signal_action_channel_item_select().connect(sigc::mem_fun(*this, &Abaddon::ActionChannelOpened));
|
||||
m_main_window->GetChannelList()->signal_action_guild_leave().connect(sigc::mem_fun(*this, &Abaddon::ActionLeaveGuild));
|
||||
@@ -190,6 +194,10 @@ void Abaddon::DiscordOnGuildMemberListUpdate(Snowflake guild_id) {
|
||||
m_main_window->UpdateMembers();
|
||||
}
|
||||
|
||||
void Abaddon::DiscordOnThreadMemberListUpdate(const ThreadMemberListUpdateData &data) {
|
||||
m_main_window->UpdateMembers();
|
||||
}
|
||||
|
||||
void Abaddon::DiscordOnReactionAdd(Snowflake message_id, const Glib::ustring ¶m) {
|
||||
m_main_window->UpdateChatReactionAdd(message_id, param);
|
||||
}
|
||||
@@ -230,6 +238,15 @@ void Abaddon::DiscordOnDisconnect(bool is_reconnecting, GatewayCloseCode close_c
|
||||
}
|
||||
}
|
||||
|
||||
void Abaddon::DiscordOnThreadUpdate(const ThreadUpdateData &data) {
|
||||
if (data.Thread.ID == m_main_window->GetChatActiveChannel()) {
|
||||
if (data.Thread.ThreadMetadata->IsArchived)
|
||||
m_main_window->GetChatWindow()->SetTopic("This thread is archived. Sending a message will unarchive it");
|
||||
else
|
||||
m_main_window->GetChatWindow()->SetTopic("");
|
||||
}
|
||||
}
|
||||
|
||||
const SettingsManager &Abaddon::GetSettings() const {
|
||||
return m_settings;
|
||||
}
|
||||
@@ -446,7 +463,10 @@ void Abaddon::ActionJoinGuildDialog() {
|
||||
void Abaddon::ActionChannelOpened(Snowflake id) {
|
||||
if (id == m_main_window->GetChatActiveChannel()) return;
|
||||
|
||||
m_main_window->GetChatWindow()->SetTopic("");
|
||||
|
||||
const auto channel = m_discord.GetChannel(id);
|
||||
if (!channel.has_value()) return;
|
||||
if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS)
|
||||
m_main_window->set_title(std::string(APP_TITLE) + " - #" + *channel->Name);
|
||||
else {
|
||||
@@ -470,7 +490,11 @@ void Abaddon::ActionChannelOpened(Snowflake id) {
|
||||
m_main_window->UpdateChatWindowContents();
|
||||
}
|
||||
|
||||
if (channel->Type != ChannelType::DM && channel->Type != ChannelType::GROUP_DM && channel->GuildID.has_value()) {
|
||||
if (channel->IsThread()) {
|
||||
m_discord.SendThreadLazyLoad(id);
|
||||
if (channel->ThreadMetadata->IsArchived)
|
||||
m_main_window->GetChatWindow()->SetTopic("This thread is archived. Sending a message will unarchive it");
|
||||
} else if (channel->Type != ChannelType::DM && channel->Type != ChannelType::GROUP_DM && channel->GuildID.has_value()) {
|
||||
m_discord.SendLazyLoad(id);
|
||||
|
||||
if (m_discord.IsVerificationRequired(*channel->GuildID))
|
||||
@@ -616,6 +640,18 @@ void Abaddon::ActionViewPins(Snowflake channel_id) {
|
||||
window->show();
|
||||
}
|
||||
|
||||
void Abaddon::ActionViewThreads(Snowflake channel_id) {
|
||||
auto data = m_discord.GetChannel(channel_id);
|
||||
if (!data.has_value()) return;
|
||||
if (data->IsThread()) {
|
||||
data = m_discord.GetChannel(*data->ParentID);
|
||||
if (!data.has_value()) return;
|
||||
}
|
||||
auto window = new ThreadsWindow(*data);
|
||||
ManageHeapWindow(window);
|
||||
window->show();
|
||||
}
|
||||
|
||||
bool Abaddon::ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window) {
|
||||
ConfirmDialog dlg(window != nullptr ? *window : *m_main_window);
|
||||
dlg.SetConfirmText(prompt);
|
||||
|
||||
@@ -47,6 +47,7 @@ public:
|
||||
void ActionGuildSettings(Snowflake id);
|
||||
void ActionAddRecipient(Snowflake channel_id);
|
||||
void ActionViewPins(Snowflake channel_id);
|
||||
void ActionViewThreads(Snowflake channel_id);
|
||||
|
||||
bool ShowConfirm(const Glib::ustring &prompt, Gtk::Window *window = nullptr);
|
||||
|
||||
@@ -65,11 +66,13 @@ public:
|
||||
void DiscordOnMessageDelete(Snowflake id, Snowflake channel_id);
|
||||
void DiscordOnMessageUpdate(Snowflake id, Snowflake channel_id);
|
||||
void DiscordOnGuildMemberListUpdate(Snowflake guild_id);
|
||||
void DiscordOnThreadMemberListUpdate(const ThreadMemberListUpdateData &data);
|
||||
void DiscordOnReactionAdd(Snowflake message_id, const Glib::ustring ¶m);
|
||||
void DiscordOnReactionRemove(Snowflake message_id, const Glib::ustring ¶m);
|
||||
void DiscordOnGuildJoinRequestCreate(const GuildJoinRequestCreateData &data);
|
||||
void DiscordOnMessageSent(const Message &data);
|
||||
void DiscordOnDisconnect(bool is_reconnecting, GatewayCloseCode close_code);
|
||||
void DiscordOnThreadUpdate(const ThreadUpdateData &data);
|
||||
|
||||
const SettingsManager &GetSettings() const;
|
||||
|
||||
|
||||
@@ -17,20 +17,29 @@ ChannelList::ChannelList()
|
||||
, m_menu_category_copy_id("_Copy ID", true)
|
||||
, m_menu_channel_copy_id("_Copy ID", true)
|
||||
, m_menu_dm_close("") // changes depending on if group or not
|
||||
, m_menu_dm_copy_id("_Copy ID", true) {
|
||||
, m_menu_dm_copy_id("_Copy ID", true)
|
||||
, 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);
|
||||
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;
|
||||
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 (row[m_columns.m_type] == RenderType::TextChannel || row[m_columns.m_type] == RenderType::DM) {
|
||||
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]));
|
||||
}
|
||||
};
|
||||
@@ -116,6 +125,27 @@ ChannelList::ChannelList()
|
||||
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));
|
||||
@@ -123,6 +153,11 @@ ChannelList::ChannelList()
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -182,56 +217,48 @@ void ChannelList::UpdateChannel(Snowflake id) {
|
||||
if (channel->Type == ChannelType::GUILD_CATEGORY) return UpdateChannelCategory(*channel);
|
||||
if (!IsTextChannel(channel->Type)) return;
|
||||
|
||||
// delete and recreate
|
||||
m_model->erase(iter);
|
||||
// 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);
|
||||
|
||||
Gtk::TreeStore::iterator parent;
|
||||
bool is_orphan;
|
||||
if (channel->ParentID.has_value()) {
|
||||
is_orphan = false;
|
||||
parent = GetIteratorForChannelFromID(*channel->ParentID);
|
||||
} else {
|
||||
is_orphan = true;
|
||||
parent = GetIteratorForGuildFromID(*channel->GuildID);
|
||||
}
|
||||
if (!parent) return;
|
||||
auto channel_row = *m_model->append(parent->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 (is_orphan)
|
||||
channel_row[m_columns.m_sort] = *channel->Position + OrphanChannelSortOffset;
|
||||
// check if the parent has changed
|
||||
Gtk::TreeModel::iterator new_parent;
|
||||
if (channel->ParentID.has_value())
|
||||
new_parent = GetIteratorForChannelFromID(*channel->ParentID);
|
||||
else
|
||||
channel_row[m_columns.m_sort] = *channel->Position;
|
||||
new_parent = GetIteratorForGuildFromID(*channel->GuildID);
|
||||
|
||||
if (new_parent && iter->parent() != new_parent)
|
||||
MoveRow(iter, new_parent);
|
||||
}
|
||||
|
||||
void ChannelList::UpdateCreateChannel(Snowflake id) {
|
||||
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id);
|
||||
if (!channel.has_value()) return;
|
||||
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;
|
||||
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()) {
|
||||
if (channel.ParentID.has_value()) {
|
||||
orphan = false;
|
||||
auto iter = GetIteratorForChannelFromID(*channel->ParentID);
|
||||
auto iter = GetIteratorForChannelFromID(*channel.ParentID);
|
||||
channel_row = *m_model->append(iter->children());
|
||||
} else {
|
||||
orphan = true;
|
||||
auto iter = GetIteratorForGuildFromID(*channel->GuildID);
|
||||
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();
|
||||
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;
|
||||
channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset;
|
||||
else
|
||||
channel_row[m_columns.m_sort] = *channel->Position;
|
||||
channel_row[m_columns.m_sort] = *channel.Position;
|
||||
}
|
||||
|
||||
void ChannelList::UpdateGuild(Snowflake id) {
|
||||
@@ -260,11 +287,96 @@ void ChannelList::UpdateGuild(Snowflake id) {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,6 +425,22 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -320,6 +448,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
|
||||
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) {
|
||||
@@ -340,6 +469,7 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,6 +490,18 @@ Gtk::TreeModel::iterator ChannelList::UpdateCreateChannelCategory(const ChannelD
|
||||
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;
|
||||
@@ -423,7 +565,7 @@ bool ChannelList::SelectionFunc(const Glib::RefPtr<Gtk::TreeModel> &model, const
|
||||
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;
|
||||
return type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread;
|
||||
}
|
||||
|
||||
void ChannelList::AddPrivateChannels() {
|
||||
@@ -530,6 +672,10 @@ bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) {
|
||||
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;
|
||||
}
|
||||
@@ -539,6 +685,48 @@ bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) {
|
||||
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;
|
||||
}
|
||||
@@ -614,6 +802,8 @@ void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &m
|
||||
return get_preferred_width_vfunc_category(widget, minimum_width, natural_width);
|
||||
case RenderType::TextChannel:
|
||||
return get_preferred_width_vfunc_channel(widget, minimum_width, natural_width);
|
||||
case RenderType::Thread:
|
||||
return get_preferred_width_vfunc_thread(widget, minimum_width, natural_width);
|
||||
case RenderType::DMHeader:
|
||||
return get_preferred_width_vfunc_dmheader(widget, minimum_width, natural_width);
|
||||
case RenderType::DM:
|
||||
@@ -629,6 +819,8 @@ void CellRendererChannels::get_preferred_width_for_height_vfunc(Gtk::Widget &wid
|
||||
return get_preferred_width_for_height_vfunc_category(widget, height, minimum_width, natural_width);
|
||||
case RenderType::TextChannel:
|
||||
return get_preferred_width_for_height_vfunc_channel(widget, height, minimum_width, natural_width);
|
||||
case RenderType::Thread:
|
||||
return get_preferred_width_for_height_vfunc_thread(widget, height, minimum_width, natural_width);
|
||||
case RenderType::DMHeader:
|
||||
return get_preferred_width_for_height_vfunc_dmheader(widget, height, minimum_width, natural_width);
|
||||
case RenderType::DM:
|
||||
@@ -644,6 +836,8 @@ void CellRendererChannels::get_preferred_height_vfunc(Gtk::Widget &widget, int &
|
||||
return get_preferred_height_vfunc_category(widget, minimum_height, natural_height);
|
||||
case RenderType::TextChannel:
|
||||
return get_preferred_height_vfunc_channel(widget, minimum_height, natural_height);
|
||||
case RenderType::Thread:
|
||||
return get_preferred_height_vfunc_thread(widget, minimum_height, natural_height);
|
||||
case RenderType::DMHeader:
|
||||
return get_preferred_height_vfunc_dmheader(widget, minimum_height, natural_height);
|
||||
case RenderType::DM:
|
||||
@@ -659,6 +853,8 @@ void CellRendererChannels::get_preferred_height_for_width_vfunc(Gtk::Widget &wid
|
||||
return get_preferred_height_for_width_vfunc_category(widget, width, minimum_height, natural_height);
|
||||
case RenderType::TextChannel:
|
||||
return get_preferred_height_for_width_vfunc_channel(widget, width, minimum_height, natural_height);
|
||||
case RenderType::Thread:
|
||||
return get_preferred_height_for_width_vfunc_thread(widget, width, minimum_height, natural_height);
|
||||
case RenderType::DMHeader:
|
||||
return get_preferred_height_for_width_vfunc_dmheader(widget, width, minimum_height, natural_height);
|
||||
case RenderType::DM:
|
||||
@@ -674,6 +870,8 @@ void CellRendererChannels::render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr,
|
||||
return render_vfunc_category(cr, widget, background_area, cell_area, flags);
|
||||
case RenderType::TextChannel:
|
||||
return render_vfunc_channel(cr, widget, background_area, cell_area, flags);
|
||||
case RenderType::Thread:
|
||||
return render_vfunc_thread(cr, widget, background_area, cell_area, flags);
|
||||
case RenderType::DMHeader:
|
||||
return render_vfunc_dmheader(cr, widget, background_area, cell_area, flags);
|
||||
case RenderType::DM:
|
||||
@@ -883,6 +1081,37 @@ void CellRendererChannels::render_vfunc_channel(const Cairo::RefPtr<Cairo::Conte
|
||||
m_renderer_text.property_foreground_set() = false;
|
||||
}
|
||||
|
||||
// thread
|
||||
|
||||
void CellRendererChannels::get_preferred_width_vfunc_thread(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
|
||||
m_renderer_text.get_preferred_width(widget, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_width_for_height_vfunc_thread(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const {
|
||||
get_preferred_width_vfunc_thread(widget, minimum_width, natural_width);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_vfunc_thread(Gtk::Widget &widget, int &minimum_height, int &natural_height) const {
|
||||
m_renderer_text.get_preferred_height(widget, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::get_preferred_height_for_width_vfunc_thread(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const {
|
||||
get_preferred_height_vfunc_thread(widget, minimum_height, natural_height);
|
||||
}
|
||||
|
||||
void CellRendererChannels::render_vfunc_thread(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) {
|
||||
Gtk::Requisition minimum_size, natural_size;
|
||||
m_renderer_text.get_preferred_size(widget, minimum_size, natural_size);
|
||||
|
||||
const int text_x = background_area.get_x() + 26;
|
||||
const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2;
|
||||
const int text_w = natural_size.width;
|
||||
const int text_h = natural_size.height;
|
||||
|
||||
Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h);
|
||||
m_renderer_text.render(cr, widget, background_area, text_cell_area, flags);
|
||||
}
|
||||
|
||||
// dm header
|
||||
|
||||
void CellRendererChannels::get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const {
|
||||
|
||||
@@ -16,6 +16,7 @@ enum class RenderType : uint8_t {
|
||||
Guild,
|
||||
Category,
|
||||
TextChannel,
|
||||
Thread,
|
||||
|
||||
DMHeader,
|
||||
DM,
|
||||
@@ -77,6 +78,17 @@ protected:
|
||||
const Gdk::Rectangle &cell_area,
|
||||
Gtk::CellRendererState flags);
|
||||
|
||||
// thread
|
||||
void get_preferred_width_vfunc_thread(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_width_for_height_vfunc_thread(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_height_vfunc_thread(Gtk::Widget &widget, int &minimum_height, int &natural_height) const;
|
||||
void get_preferred_height_for_width_vfunc_thread(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const;
|
||||
void render_vfunc_thread(const Cairo::RefPtr<Cairo::Context> &cr,
|
||||
Gtk::Widget &widget,
|
||||
const Gdk::Rectangle &background_area,
|
||||
const Gdk::Rectangle &cell_area,
|
||||
Gtk::CellRendererState flags);
|
||||
|
||||
// dm header
|
||||
void get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
|
||||
void get_preferred_width_for_height_vfunc_dmheader(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const;
|
||||
@@ -127,8 +139,15 @@ protected:
|
||||
void UpdateRemoveGuild(Snowflake id);
|
||||
void UpdateRemoveChannel(Snowflake id);
|
||||
void UpdateChannel(Snowflake id);
|
||||
void UpdateCreateChannel(Snowflake id);
|
||||
void UpdateCreateChannel(const ChannelData &channel);
|
||||
void UpdateGuild(Snowflake id);
|
||||
void DeleteThreadRow(Snowflake id);
|
||||
|
||||
void OnThreadJoined(Snowflake id);
|
||||
void OnThreadRemoved(Snowflake id);
|
||||
void OnThreadDelete(const ThreadDeleteData &data);
|
||||
void OnThreadUpdate(const ThreadUpdateData &data);
|
||||
void OnThreadListSync(const ThreadListSyncData &data);
|
||||
|
||||
Gtk::TreeView m_view;
|
||||
|
||||
@@ -156,6 +175,7 @@ protected:
|
||||
|
||||
Gtk::TreeModel::iterator AddGuild(const GuildData &guild);
|
||||
Gtk::TreeModel::iterator UpdateCreateChannelCategory(const ChannelData &channel);
|
||||
Gtk::TreeModel::iterator CreateThreadRow(const Gtk::TreeNodeChildren &children, const ChannelData &channel);
|
||||
|
||||
void UpdateChannelCategory(const ChannelData &channel);
|
||||
|
||||
@@ -170,6 +190,8 @@ protected:
|
||||
bool SelectionFunc(const Glib::RefPtr<Gtk::TreeModel> &model, const Gtk::TreeModel::Path &path, bool is_currently_selected);
|
||||
bool OnButtonPressEvent(GdkEventButton *ev);
|
||||
|
||||
void MoveRow(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::iterator &new_parent);
|
||||
|
||||
Gtk::TreeModel::Path m_last_selected;
|
||||
Gtk::TreeModel::Path m_dm_header;
|
||||
|
||||
@@ -179,6 +201,9 @@ protected:
|
||||
void OnMessageCreate(const Message &msg);
|
||||
Gtk::TreeModel::Path m_path_for_menu;
|
||||
|
||||
// cant be recovered through selection
|
||||
Gtk::TreeModel::iterator m_temporary_thread_row;
|
||||
|
||||
Gtk::Menu m_menu_guild;
|
||||
Gtk::MenuItem m_menu_guild_copy_id;
|
||||
Gtk::MenuItem m_menu_guild_settings;
|
||||
@@ -194,6 +219,14 @@ protected:
|
||||
Gtk::MenuItem m_menu_dm_copy_id;
|
||||
Gtk::MenuItem m_menu_dm_close;
|
||||
|
||||
Gtk::Menu m_menu_thread;
|
||||
Gtk::MenuItem m_menu_thread_copy_id;
|
||||
Gtk::MenuItem m_menu_thread_leave;
|
||||
Gtk::MenuItem m_menu_thread_archive;
|
||||
Gtk::MenuItem m_menu_thread_unarchive;
|
||||
|
||||
void OnThreadSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y);
|
||||
|
||||
bool m_updating_listing = false;
|
||||
|
||||
public:
|
||||
|
||||
@@ -38,8 +38,10 @@ ChatMessageItemContainer *ChatMessageItemContainer::FromMessage(const Message &d
|
||||
|
||||
if ((data.MessageReference.has_value() || data.Interaction.has_value()) && data.Type != MessageType::CHANNEL_FOLLOW_ADD) {
|
||||
auto *widget = container->CreateReplyComponent(data);
|
||||
container->m_main.add(*widget);
|
||||
container->m_main.child_property_position(*widget) = 0; // eek
|
||||
if (widget != nullptr) {
|
||||
container->m_main.add(*widget);
|
||||
container->m_main.child_property_position(*widget) = 0; // eek
|
||||
}
|
||||
}
|
||||
|
||||
// there should only ever be 1 embed (i think?)
|
||||
@@ -274,6 +276,20 @@ void ChatMessageItemContainer::UpdateTextComponent(Gtk::TextView *tv) {
|
||||
case MessageType::GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING: {
|
||||
b->insert_markup(s, "<i><span color='#999999'>This server has failed Discovery activity requirements for 3 weeks in a row. If this server fails for 1 more week, it will be removed from Discovery.</span></i>");
|
||||
} break;
|
||||
case MessageType::THREAD_CREATED: {
|
||||
const auto author = Abaddon::Get().GetDiscordClient().GetUser(data->Author.ID);
|
||||
if (data->MessageReference.has_value() && data->MessageReference->ChannelID.has_value()) {
|
||||
auto iter = b->insert_markup(s, "<i><span color='#999999'>" + author->GetEscapedBoldName() + " started a thread: </span></i>");
|
||||
auto tag = b->create_tag();
|
||||
tag->property_weight() = Pango::WEIGHT_BOLD;
|
||||
m_channel_tagmap[tag] = *data->MessageReference->ChannelID;
|
||||
b->insert_with_tag(iter, data->Content, tag);
|
||||
|
||||
tv->signal_button_press_event().connect(sigc::mem_fun(*this, &ChatMessageItemContainer::OnClickChannel), false);
|
||||
} else {
|
||||
b->insert_markup(s, "<i><span color='#999999'>" + author->GetEscapedBoldName() + " started a thread: </span><b>" + Glib::Markup::escape_text(data->Content) + "</b></i>");
|
||||
}
|
||||
} break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -607,6 +623,8 @@ Gtk::Widget *ChatMessageItemContainer::CreateReactionsComponent(const Message &d
|
||||
}
|
||||
|
||||
Gtk::Widget *ChatMessageItemContainer::CreateReplyComponent(const Message &data) {
|
||||
if (data.Type == MessageType::THREAD_CREATED) return nullptr;
|
||||
|
||||
auto *box = Gtk::manage(new Gtk::Box);
|
||||
auto *lbl = Gtk::manage(new Gtk::Label);
|
||||
lbl->set_single_line_mode(true);
|
||||
|
||||
@@ -31,6 +31,11 @@ ChatWindow::ChatWindow() {
|
||||
m_main->set_hexpand(true);
|
||||
m_main->set_vexpand(true);
|
||||
|
||||
m_topic.get_style_context()->add_class("channel-topic");
|
||||
m_topic.add(m_topic_text);
|
||||
m_topic_text.set_halign(Gtk::ALIGN_START);
|
||||
m_topic_text.show();
|
||||
|
||||
m_input->signal_submit().connect(sigc::mem_fun(*this, &ChatWindow::OnInputSubmit));
|
||||
m_input->signal_escape().connect([this]() {
|
||||
if (m_is_replying)
|
||||
@@ -84,6 +89,7 @@ ChatWindow::ChatWindow() {
|
||||
m_meta->add(*m_input_indicator);
|
||||
m_meta->add(*m_rate_limit_indicator);
|
||||
//m_scroll->add(*m_list);
|
||||
m_main->add(m_topic);
|
||||
m_main->add(*m_chat);
|
||||
m_main->add(m_completer);
|
||||
m_main->add(*m_input);
|
||||
@@ -140,6 +146,11 @@ void ChatWindow::UpdateReactions(Snowflake id) {
|
||||
m_chat->UpdateMessageReactions(id);
|
||||
}
|
||||
|
||||
void ChatWindow::SetTopic(const std::string &text) {
|
||||
m_topic_text.set_text(text);
|
||||
m_topic.set_visible(text.length() > 0);
|
||||
}
|
||||
|
||||
Snowflake ChatWindow::GetActiveChannel() const {
|
||||
return m_active_channel;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public:
|
||||
void InsertChatInput(std::string text);
|
||||
Snowflake GetOldestListedMessage(); // oldest message that is currently in the ListBox
|
||||
void UpdateReactions(Snowflake id);
|
||||
void SetTopic(const std::string &text);
|
||||
|
||||
protected:
|
||||
bool m_is_replying = false;
|
||||
@@ -49,6 +50,9 @@ protected:
|
||||
//Gtk::ListBox *m_list;
|
||||
//Gtk::ScrolledWindow *m_scroll;
|
||||
|
||||
Gtk::EventBox m_topic; // todo probably make everything else go on the stack
|
||||
Gtk::Label m_topic_text;
|
||||
|
||||
ChatList *m_chat;
|
||||
|
||||
ChatInput *m_input;
|
||||
|
||||
@@ -127,7 +127,12 @@ void MemberList::UpdateMemberList() {
|
||||
return;
|
||||
}
|
||||
|
||||
auto ids = discord.GetUsersInGuild(m_guild_id);
|
||||
std::set<Snowflake> ids;
|
||||
if (chan->IsThread()) {
|
||||
const auto x = discord.GetUsersInThread(m_chan_id);
|
||||
ids = { x.begin(), x.end() };
|
||||
} else
|
||||
ids = discord.GetUsersInGuild(m_guild_id);
|
||||
|
||||
// process all the shit first so its in proper order
|
||||
std::map<int, RoleData> pos_to_role;
|
||||
|
||||
@@ -43,6 +43,12 @@ enum class AuditLogActionType {
|
||||
STAGE_INSTANCE_CREATE = 83,
|
||||
STAGE_INSTANCE_UPDATE = 84,
|
||||
STAGE_INSTANCE_DELETE = 85,
|
||||
STICKER_CREATE = 90,
|
||||
STICKER_UPDATE = 91,
|
||||
STICKER_DELETE = 92,
|
||||
THREAD_CREATE = 110,
|
||||
THREAD_UPDATE = 111,
|
||||
THREAD_DELETE = 112,
|
||||
};
|
||||
|
||||
struct AuditLogChange {
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
#include "../abaddon.hpp"
|
||||
#include "channel.hpp"
|
||||
|
||||
void from_json(const nlohmann::json &j, ThreadMetadataData &m) {
|
||||
JS_D("archived", m.IsArchived);
|
||||
JS_D("auto_archive_duration", m.AutoArchiveDuration);
|
||||
JS_D("archive_timestamp", m.ArchiveTimestamp);
|
||||
JS_O("locked", m.IsLocked);
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, ThreadMemberObject &m) {
|
||||
JS_O("id", m.ThreadID);
|
||||
JS_O("user_id", m.UserID);
|
||||
JS_D("join_timestamp", m.JoinTimestamp);
|
||||
JS_D("flags", m.Flags);
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, ChannelData &m) {
|
||||
JS_D("id", m.ID);
|
||||
JS_D("type", m.Type);
|
||||
@@ -21,6 +35,8 @@ void from_json(const nlohmann::json &j, ChannelData &m) {
|
||||
JS_O("application_id", m.ApplicationID);
|
||||
JS_ON("parent_id", m.ParentID);
|
||||
JS_ON("last_pin_timestamp", m.LastPinTimestamp);
|
||||
JS_O("thread_metadata", m.ThreadMetadata);
|
||||
JS_O("member", m.ThreadMember);
|
||||
}
|
||||
|
||||
void ChannelData::update_from_json(const nlohmann::json &j) {
|
||||
@@ -47,6 +63,16 @@ bool ChannelData::NSFW() const {
|
||||
return IsNSFW.has_value() && *IsNSFW;
|
||||
}
|
||||
|
||||
bool ChannelData::IsThread() const noexcept {
|
||||
return Type == ChannelType::GUILD_PUBLIC_THREAD ||
|
||||
Type == ChannelType::GUILD_PRIVATE_THREAD ||
|
||||
Type == ChannelType::GUILD_NEWS_THREAD;
|
||||
}
|
||||
|
||||
bool ChannelData::IsJoinedThread() const {
|
||||
return Abaddon::Get().GetDiscordClient().IsThreadJoined(ID);
|
||||
}
|
||||
|
||||
std::optional<PermissionOverwrite> ChannelData::GetOverwrite(Snowflake id) const {
|
||||
return Abaddon::Get().GetDiscordClient().GetPermissionOverwrite(ID, id);
|
||||
}
|
||||
|
||||
@@ -15,9 +15,10 @@ enum class ChannelType : int {
|
||||
GUILD_NEWS = 5,
|
||||
GUILD_STORE = 6,
|
||||
/* 7 and 8 were used for LFG */
|
||||
/* 9 and 10 were used for threads */
|
||||
PUBLIC_THREAD = 11,
|
||||
PRIVATE_THREAD = 12,
|
||||
/* 9 was used for threads */
|
||||
GUILD_NEWS_THREAD = 10,
|
||||
GUILD_PUBLIC_THREAD = 11,
|
||||
GUILD_PRIVATE_THREAD = 12,
|
||||
GUILD_STAGE_VOICE = 13,
|
||||
};
|
||||
|
||||
@@ -37,6 +38,26 @@ constexpr const char *GetStagePrivacyDisplayString(StagePrivacy e) {
|
||||
}
|
||||
}
|
||||
|
||||
// should be moved somewhere?
|
||||
|
||||
struct ThreadMetadataData {
|
||||
bool IsArchived;
|
||||
int AutoArchiveDuration;
|
||||
std::string ArchiveTimestamp;
|
||||
std::optional<bool> IsLocked;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, ThreadMetadataData &m);
|
||||
};
|
||||
|
||||
struct ThreadMemberObject {
|
||||
std::optional<Snowflake> ThreadID;
|
||||
std::optional<Snowflake> UserID;
|
||||
std::string JoinTimestamp;
|
||||
int Flags;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, ThreadMemberObject &m);
|
||||
};
|
||||
|
||||
struct ChannelData {
|
||||
Snowflake ID;
|
||||
ChannelType Type;
|
||||
@@ -57,11 +78,15 @@ struct ChannelData {
|
||||
std::optional<Snowflake> ApplicationID;
|
||||
std::optional<Snowflake> ParentID; // null
|
||||
std::optional<std::string> LastPinTimestamp; // null
|
||||
std::optional<ThreadMetadataData> ThreadMetadata;
|
||||
std::optional<ThreadMemberObject> ThreadMember;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, ChannelData &m);
|
||||
void update_from_json(const nlohmann::json &j);
|
||||
|
||||
bool NSFW() const;
|
||||
bool IsThread() const noexcept;
|
||||
bool IsJoinedThread() const;
|
||||
std::optional<PermissionOverwrite> GetOverwrite(Snowflake id) const;
|
||||
std::vector<UserData> GetDMRecipients() const;
|
||||
};
|
||||
|
||||
@@ -127,7 +127,23 @@ void DiscordClient::FetchInvite(std::string code, sigc::slot<void(std::optional<
|
||||
void DiscordClient::FetchMessagesInChannel(Snowflake id, sigc::slot<void(const std::vector<Message> &)> cb) {
|
||||
std::string path = "/channels/" + std::to_string(id) + "/messages?limit=50";
|
||||
m_http.MakeGET(path, [this, id, cb](const http::response_type &r) {
|
||||
if (!CheckCode(r)) return;
|
||||
if (!CheckCode(r)) {
|
||||
// fake a thread delete event if the requested channel is a thread and we get a 404
|
||||
|
||||
if (r.status_code == http::NotFound) {
|
||||
const auto channel = m_store.GetChannel(id);
|
||||
if (channel.has_value() && channel->IsThread()) {
|
||||
ThreadDeleteData data;
|
||||
data.GuildID = *channel->GuildID;
|
||||
data.ID = id;
|
||||
data.ParentID = *channel->ParentID;
|
||||
data.Type = channel->Type;
|
||||
m_signal_thread_delete.emit(data);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<Message> msgs;
|
||||
|
||||
@@ -249,6 +265,34 @@ std::set<Snowflake> DiscordClient::GetChannelsInGuild(Snowflake id) const {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<Snowflake> DiscordClient::GetUsersInThread(Snowflake id) const {
|
||||
if (auto it = m_thread_members.find(id); it != m_thread_members.end())
|
||||
return it->second;
|
||||
return {};
|
||||
}
|
||||
|
||||
// there is an endpoint for this but it should be synced before this is called anyways
|
||||
std::vector<ChannelData> DiscordClient::GetActiveThreads(Snowflake channel_id) const {
|
||||
return m_store.GetActiveThreads(channel_id);
|
||||
}
|
||||
|
||||
void DiscordClient::GetArchivedPublicThreads(Snowflake channel_id, sigc::slot<void(DiscordError, const ArchivedThreadsResponseData &)> callback) {
|
||||
m_http.MakeGET("/channels/" + std::to_string(channel_id) + "/threads/archived/public", [this, callback](const http::response_type &r) {
|
||||
if (CheckCode(r)) {
|
||||
const auto data = nlohmann::json::parse(r.text).get<ArchivedThreadsResponseData>();
|
||||
for (const auto &thread : data.Threads)
|
||||
m_store.SetChannel(thread.ID, thread);
|
||||
callback(DiscordError::NONE, data);
|
||||
} else {
|
||||
callback(GetCodeFromResponse(r), {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool DiscordClient::IsThreadJoined(Snowflake thread_id) const {
|
||||
return std::find(m_joined_threads.begin(), m_joined_threads.end(), thread_id) != m_joined_threads.end();
|
||||
}
|
||||
|
||||
bool DiscordClient::HasGuildPermission(Snowflake user_id, Snowflake guild_id, Permission perm) const {
|
||||
const auto base = ComputePermissions(user_id, guild_id);
|
||||
return (base & perm) == perm;
|
||||
@@ -429,6 +473,22 @@ void DiscordClient::SendLazyLoad(Snowflake id) {
|
||||
msg.GuildID = *GetChannel(id)->GuildID;
|
||||
msg.ShouldGetActivities = true;
|
||||
msg.ShouldGetTyping = true;
|
||||
msg.ShouldGetThreads = true;
|
||||
|
||||
m_websocket.Send(msg);
|
||||
|
||||
m_channels_lazy_loaded.insert(id);
|
||||
}
|
||||
|
||||
void DiscordClient::SendThreadLazyLoad(Snowflake id) {
|
||||
auto thread = GetChannel(id);
|
||||
if (thread.has_value())
|
||||
if (m_channels_lazy_loaded.find(*thread->ParentID) == m_channels_lazy_loaded.end())
|
||||
SendLazyLoad(*thread->ParentID);
|
||||
|
||||
LazyLoadRequestMessage msg;
|
||||
msg.GuildID = *GetChannel(id)->GuildID;
|
||||
msg.ThreadIDs.emplace().push_back(id);
|
||||
|
||||
m_websocket.Send(msg);
|
||||
}
|
||||
@@ -760,6 +820,41 @@ void DiscordClient::Unpin(Snowflake channel_id, Snowflake message_id, sigc::slot
|
||||
});
|
||||
}
|
||||
|
||||
// i dont know if the location parameter is necessary at all but discord's thread implementation is extremely strange
|
||||
// so its here just in case
|
||||
void DiscordClient::LeaveThread(Snowflake channel_id, const std::string &location, sigc::slot<void(DiscordError code)> callback) {
|
||||
m_http.MakeDELETE("/channels/" + std::to_string(channel_id) + "/thread-members/@me?location=" + location, [this, callback](const http::response_type &response) {
|
||||
if (CheckCode(response, 204))
|
||||
callback(DiscordError::NONE);
|
||||
else
|
||||
callback(GetCodeFromResponse(response));
|
||||
});
|
||||
}
|
||||
|
||||
void DiscordClient::ArchiveThread(Snowflake channel_id, sigc::slot<void(DiscordError code)> callback) {
|
||||
ModifyChannelObject obj;
|
||||
obj.Archived = true;
|
||||
obj.Locked = true;
|
||||
m_http.MakePATCH("/channels/" + std::to_string(channel_id), nlohmann::json(obj).dump(), [this, callback](const http::response_type &response) {
|
||||
if (CheckCode(response))
|
||||
callback(DiscordError::NONE);
|
||||
else
|
||||
callback(GetCodeFromResponse(response));
|
||||
});
|
||||
}
|
||||
|
||||
void DiscordClient::UnArchiveThread(Snowflake channel_id, sigc::slot<void(DiscordError code)> callback) {
|
||||
ModifyChannelObject obj;
|
||||
obj.Archived = false;
|
||||
obj.Locked = false;
|
||||
m_http.MakePATCH("/channels/" + std::to_string(channel_id), nlohmann::json(obj).dump(), [this, callback](const http::response_type &response) {
|
||||
if (CheckCode(response))
|
||||
callback(DiscordError::NONE);
|
||||
else
|
||||
callback(GetCodeFromResponse(response));
|
||||
});
|
||||
}
|
||||
|
||||
void DiscordClient::FetchPinned(Snowflake id, sigc::slot<void(std::vector<Message>, DiscordError code)> callback) {
|
||||
// return from db if we know the pins have already been requested
|
||||
if (m_channels_pinned_requested.find(id) != m_channels_pinned_requested.end()) {
|
||||
@@ -1155,6 +1250,27 @@ void DiscordClient::HandleGatewayMessage(std::string str) {
|
||||
case GatewayEvent::RELATIONSHIP_ADD: {
|
||||
HandleGatewayRelationshipAdd(m);
|
||||
} break;
|
||||
case GatewayEvent::THREAD_CREATE: {
|
||||
HandleGatewayThreadCreate(m);
|
||||
} break;
|
||||
case GatewayEvent::THREAD_DELETE: {
|
||||
HandleGatewayThreadDelete(m);
|
||||
} break;
|
||||
case GatewayEvent::THREAD_LIST_SYNC: {
|
||||
HandleGatewayThreadListSync(m);
|
||||
} break;
|
||||
case GatewayEvent::THREAD_MEMBERS_UPDATE: {
|
||||
HandleGatewayThreadMembersUpdate(m);
|
||||
} break;
|
||||
case GatewayEvent::THREAD_MEMBER_UPDATE: {
|
||||
HandleGatewayThreadMemberUpdate(m);
|
||||
} break;
|
||||
case GatewayEvent::THREAD_UPDATE: {
|
||||
HandleGatewayThreadUpdate(m);
|
||||
} break;
|
||||
case GatewayEvent::THREAD_MEMBER_LIST_UPDATE: {
|
||||
HandleGatewayThreadMemberListUpdate(m);
|
||||
} break;
|
||||
}
|
||||
} break;
|
||||
default:
|
||||
@@ -1209,7 +1325,7 @@ void DiscordClient::ProcessNewGuild(GuildData &guild) {
|
||||
m_store.BeginTransaction();
|
||||
|
||||
m_store.SetGuild(guild.ID, guild);
|
||||
if (guild.Channels.has_value())
|
||||
if (guild.Channels.has_value()) {
|
||||
for (auto &c : *guild.Channels) {
|
||||
c.GuildID = guild.ID;
|
||||
m_store.SetChannel(c.ID, c);
|
||||
@@ -1218,6 +1334,15 @@ void DiscordClient::ProcessNewGuild(GuildData &guild) {
|
||||
m_store.SetPermissionOverwrite(c.ID, p.ID, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (guild.Threads.has_value()) {
|
||||
for (auto &c : *guild.Threads) {
|
||||
m_joined_threads.insert(c.ID);
|
||||
c.GuildID = guild.ID;
|
||||
m_store.SetChannel(c.ID, c);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto &r : *guild.Roles)
|
||||
m_store.SetRole(r.ID, r);
|
||||
@@ -1373,7 +1498,7 @@ void DiscordClient::HandleGatewayChannelCreate(const GatewayMessage &msg) {
|
||||
for (const auto &p : *data.PermissionOverwrites)
|
||||
m_store.SetPermissionOverwrite(data.ID, p.ID, p);
|
||||
m_store.EndTransaction();
|
||||
m_signal_channel_create.emit(data.ID);
|
||||
m_signal_channel_create.emit(data);
|
||||
}
|
||||
|
||||
void DiscordClient::HandleGatewayGuildUpdate(const GatewayMessage &msg) {
|
||||
@@ -1627,6 +1752,73 @@ void DiscordClient::HandleGatewayRelationshipAdd(const GatewayMessage &msg) {
|
||||
m_signal_relationship_add.emit(std::move(data));
|
||||
}
|
||||
|
||||
// remarkably this doesnt actually mean a thread was created
|
||||
// it can also mean you gained access to a thread. yay ...
|
||||
// except sometimes it doesnt??? i dont know whats going on
|
||||
void DiscordClient::HandleGatewayThreadCreate(const GatewayMessage &msg) {
|
||||
ThreadCreateData data = msg.Data;
|
||||
m_store.SetChannel(data.Channel.ID, data.Channel);
|
||||
m_signal_thread_create.emit(data.Channel);
|
||||
}
|
||||
|
||||
void DiscordClient::HandleGatewayThreadDelete(const GatewayMessage &msg) {
|
||||
ThreadDeleteData data = msg.Data;
|
||||
m_store.ClearChannel(data.ID);
|
||||
m_signal_thread_delete.emit(data);
|
||||
}
|
||||
|
||||
// this message is received when you load a channel as part of the lazy load request
|
||||
// so the ui will only update thread when you load a channel in some guild
|
||||
// which is rather annoying but oh well
|
||||
void DiscordClient::HandleGatewayThreadListSync(const GatewayMessage &msg) {
|
||||
ThreadListSyncData data = msg.Data;
|
||||
for (const auto &thread : data.Threads)
|
||||
m_store.SetChannel(thread.ID, thread);
|
||||
m_signal_thread_list_sync.emit(data);
|
||||
}
|
||||
|
||||
void DiscordClient::HandleGatewayThreadMembersUpdate(const GatewayMessage &msg) {
|
||||
ThreadMembersUpdateData data = msg.Data;
|
||||
if (data.AddedMembers.has_value() &&
|
||||
std::find_if(data.AddedMembers->begin(), data.AddedMembers->end(), [this](const auto &x) {
|
||||
return *x.UserID == m_user_data.ID; // safe to assume UserID is present here
|
||||
}) != data.AddedMembers->end()) {
|
||||
m_joined_threads.insert(data.ID);
|
||||
m_signal_added_to_thread.emit(data.ID);
|
||||
} else if (data.RemovedMemberIDs.has_value() &&
|
||||
std::find(data.RemovedMemberIDs->begin(), data.RemovedMemberIDs->end(), m_user_data.ID) != data.RemovedMemberIDs->end()) {
|
||||
m_joined_threads.erase(data.ID);
|
||||
m_signal_removed_from_thread.emit(data.ID);
|
||||
}
|
||||
m_signal_thread_members_update.emit(data);
|
||||
}
|
||||
|
||||
void DiscordClient::HandleGatewayThreadMemberUpdate(const GatewayMessage &msg) {
|
||||
ThreadMemberUpdateData data = msg.Data;
|
||||
m_joined_threads.insert(*data.Member.ThreadID);
|
||||
if (*data.Member.UserID == GetUserData().ID)
|
||||
m_signal_added_to_thread.emit(*data.Member.ThreadID);
|
||||
}
|
||||
|
||||
void DiscordClient::HandleGatewayThreadUpdate(const GatewayMessage &msg) {
|
||||
ThreadUpdateData data = msg.Data;
|
||||
m_store.SetChannel(data.Thread.ID, data.Thread);
|
||||
m_signal_thread_update.emit(data);
|
||||
}
|
||||
|
||||
void DiscordClient::HandleGatewayThreadMemberListUpdate(const GatewayMessage &msg) {
|
||||
ThreadMemberListUpdateData data = msg.Data;
|
||||
m_store.BeginTransaction();
|
||||
for (const auto &entry : data.Members) {
|
||||
m_thread_members[data.ThreadID].push_back(entry.UserID);
|
||||
if (entry.Member.User.has_value())
|
||||
m_store.SetUser(entry.Member.User->ID, *entry.Member.User);
|
||||
m_store.SetGuildMember(data.GuildID, entry.Member.User->ID, entry.Member);
|
||||
}
|
||||
m_store.EndTransaction();
|
||||
m_signal_thread_member_list_update.emit(data);
|
||||
}
|
||||
|
||||
void DiscordClient::HandleGatewayReadySupplemental(const GatewayMessage &msg) {
|
||||
ReadySupplementalData data = msg.Data;
|
||||
for (const auto &p : data.MergedPresences.Friends) {
|
||||
@@ -1989,6 +2181,13 @@ void DiscordClient::LoadEventMap() {
|
||||
m_event_map["GUILD_JOIN_REQUEST_DELETE"] = GatewayEvent::GUILD_JOIN_REQUEST_DELETE;
|
||||
m_event_map["RELATIONSHIP_REMOVE"] = GatewayEvent::RELATIONSHIP_REMOVE;
|
||||
m_event_map["RELATIONSHIP_ADD"] = GatewayEvent::RELATIONSHIP_ADD;
|
||||
m_event_map["THREAD_CREATE"] = GatewayEvent::THREAD_CREATE;
|
||||
m_event_map["THREAD_DELETE"] = GatewayEvent::THREAD_DELETE;
|
||||
m_event_map["THREAD_LIST_SYNC"] = GatewayEvent::THREAD_LIST_SYNC;
|
||||
m_event_map["THREAD_MEMBERS_UPDATE"] = GatewayEvent::THREAD_MEMBERS_UPDATE;
|
||||
m_event_map["THREAD_MEMBER_UPDATE"] = GatewayEvent::THREAD_MEMBER_UPDATE;
|
||||
m_event_map["THREAD_UPDATE"] = GatewayEvent::THREAD_UPDATE;
|
||||
m_event_map["THREAD_MEMBER_LIST_UPDATE"] = GatewayEvent::THREAD_MEMBER_LIST_UPDATE;
|
||||
}
|
||||
|
||||
DiscordClient::type_signal_gateway_ready DiscordClient::signal_gateway_ready() {
|
||||
@@ -2127,6 +2326,38 @@ DiscordClient::type_signal_message_pinned DiscordClient::signal_message_pinned()
|
||||
return m_signal_message_pinned;
|
||||
}
|
||||
|
||||
DiscordClient::type_signal_thread_create DiscordClient::signal_thread_create() {
|
||||
return m_signal_thread_create;
|
||||
}
|
||||
|
||||
DiscordClient::type_signal_thread_delete DiscordClient::signal_thread_delete() {
|
||||
return m_signal_thread_delete;
|
||||
}
|
||||
|
||||
DiscordClient::type_signal_thread_list_sync DiscordClient::signal_thread_list_sync() {
|
||||
return m_signal_thread_list_sync;
|
||||
}
|
||||
|
||||
DiscordClient::type_signal_thread_members_update DiscordClient::signal_thread_members_update() {
|
||||
return m_signal_thread_members_update;
|
||||
}
|
||||
|
||||
DiscordClient::type_signal_thread_update DiscordClient::signal_thread_update() {
|
||||
return m_signal_thread_update;
|
||||
}
|
||||
|
||||
DiscordClient::type_signal_thread_member_list_update DiscordClient::signal_thread_member_list_update() {
|
||||
return m_signal_thread_member_list_update;
|
||||
}
|
||||
|
||||
DiscordClient::type_signal_added_to_thread DiscordClient::signal_added_to_thread() {
|
||||
return m_signal_added_to_thread;
|
||||
}
|
||||
|
||||
DiscordClient::type_signal_removed_from_thread DiscordClient::signal_removed_from_thread() {
|
||||
return m_signal_removed_from_thread;
|
||||
}
|
||||
|
||||
DiscordClient::type_signal_message_sent DiscordClient::signal_message_sent() {
|
||||
return m_signal_message_sent;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,11 @@ public:
|
||||
std::optional<RoleData> GetMemberHighestRole(Snowflake guild_id, Snowflake user_id) const;
|
||||
std::set<Snowflake> GetUsersInGuild(Snowflake id) const;
|
||||
std::set<Snowflake> GetChannelsInGuild(Snowflake id) const;
|
||||
std::vector<Snowflake> GetUsersInThread(Snowflake id) const;
|
||||
std::vector<ChannelData> GetActiveThreads(Snowflake channel_id) const;
|
||||
void GetArchivedPublicThreads(Snowflake channel_id, sigc::slot<void(DiscordError, const ArchivedThreadsResponseData &)> callback);
|
||||
|
||||
bool IsThreadJoined(Snowflake thread_id) const;
|
||||
bool HasGuildPermission(Snowflake user_id, Snowflake guild_id, Permission perm) const;
|
||||
|
||||
bool HasAnyChannelPermission(Snowflake user_id, Snowflake channel_id, Permission perm) const;
|
||||
@@ -102,6 +106,7 @@ public:
|
||||
void DeleteMessage(Snowflake channel_id, Snowflake id);
|
||||
void EditMessage(Snowflake channel_id, Snowflake id, std::string content);
|
||||
void SendLazyLoad(Snowflake id);
|
||||
void SendThreadLazyLoad(Snowflake id);
|
||||
void JoinGuild(std::string code);
|
||||
void LeaveGuild(Snowflake id);
|
||||
void KickUser(Snowflake user_id, Snowflake guild_id);
|
||||
@@ -137,6 +142,9 @@ public:
|
||||
void PutRelationship(Snowflake id, sigc::slot<void(DiscordError code)> callback); // send fr by id, accept incoming
|
||||
void Pin(Snowflake channel_id, Snowflake message_id, sigc::slot<void(DiscordError code)> callback);
|
||||
void Unpin(Snowflake channel_id, Snowflake message_id, sigc::slot<void(DiscordError code)> callback);
|
||||
void LeaveThread(Snowflake channel_id, const std::string &location, sigc::slot<void(DiscordError code)> callback);
|
||||
void ArchiveThread(Snowflake channel_id, sigc::slot<void(DiscordError code)> callback);
|
||||
void UnArchiveThread(Snowflake channel_id, sigc::slot<void(DiscordError code)> callback);
|
||||
|
||||
bool CanModifyRole(Snowflake guild_id, Snowflake role_id) const;
|
||||
bool CanModifyRole(Snowflake guild_id, Snowflake role_id, Snowflake user_id) const;
|
||||
@@ -236,6 +244,13 @@ private:
|
||||
void HandleGatewayGuildJoinRequestDelete(const GatewayMessage &msg);
|
||||
void HandleGatewayRelationshipRemove(const GatewayMessage &msg);
|
||||
void HandleGatewayRelationshipAdd(const GatewayMessage &msg);
|
||||
void HandleGatewayThreadCreate(const GatewayMessage &msg);
|
||||
void HandleGatewayThreadDelete(const GatewayMessage &msg);
|
||||
void HandleGatewayThreadListSync(const GatewayMessage &msg);
|
||||
void HandleGatewayThreadMembersUpdate(const GatewayMessage &msg);
|
||||
void HandleGatewayThreadMemberUpdate(const GatewayMessage &msg);
|
||||
void HandleGatewayThreadUpdate(const GatewayMessage &msg);
|
||||
void HandleGatewayThreadMemberListUpdate(const GatewayMessage &msg);
|
||||
void HandleGatewayReadySupplemental(const GatewayMessage &msg);
|
||||
void HandleGatewayReconnect(const GatewayMessage &msg);
|
||||
void HandleGatewayInvalidSession(const GatewayMessage &msg);
|
||||
@@ -255,13 +270,12 @@ private:
|
||||
|
||||
void AddUserToGuild(Snowflake user_id, Snowflake guild_id);
|
||||
std::map<Snowflake, std::set<Snowflake>> m_guild_to_users;
|
||||
|
||||
std::map<Snowflake, std::set<Snowflake>> m_guild_to_channels;
|
||||
std::map<Snowflake, GuildApplicationData> m_guild_join_requests;
|
||||
|
||||
std::map<Snowflake, PresenceStatus> m_user_to_status;
|
||||
|
||||
std::map<Snowflake, RelationshipType> m_user_relationships;
|
||||
std::set<Snowflake> m_joined_threads;
|
||||
std::map<Snowflake, std::vector<Snowflake>> m_thread_members;
|
||||
|
||||
UserData m_user_data;
|
||||
UserSettings m_user_settings;
|
||||
@@ -296,6 +310,7 @@ private:
|
||||
std::queue<std::function<void()>> m_generic_queue;
|
||||
|
||||
std::set<Snowflake> m_channels_pinned_requested;
|
||||
std::set<Snowflake> m_channels_lazy_loaded;
|
||||
|
||||
// signals
|
||||
public:
|
||||
@@ -308,7 +323,7 @@ public:
|
||||
typedef sigc::signal<void, Snowflake> type_signal_guild_delete;
|
||||
typedef sigc::signal<void, Snowflake> type_signal_channel_delete;
|
||||
typedef sigc::signal<void, Snowflake> type_signal_channel_update;
|
||||
typedef sigc::signal<void, Snowflake> type_signal_channel_create;
|
||||
typedef sigc::signal<void, ChannelData> type_signal_channel_create;
|
||||
typedef sigc::signal<void, Snowflake> type_signal_guild_update;
|
||||
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_role_update; // guild id, role id
|
||||
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_role_create; // guild id, role id
|
||||
@@ -329,9 +344,20 @@ public:
|
||||
typedef sigc::signal<void, GuildJoinRequestDeleteData> type_signal_guild_join_request_delete;
|
||||
typedef sigc::signal<void, Snowflake, RelationshipType> type_signal_relationship_remove;
|
||||
typedef sigc::signal<void, RelationshipAddData> type_signal_relationship_add;
|
||||
typedef sigc::signal<void, Message> type_signal_message_unpinned; // not a real event
|
||||
typedef sigc::signal<void, Message> type_signal_message_pinned; // not a real event either
|
||||
typedef sigc::signal<void, ChannelData> type_signal_thread_create;
|
||||
typedef sigc::signal<void, ThreadDeleteData> type_signal_thread_delete;
|
||||
typedef sigc::signal<void, ThreadListSyncData> type_signal_thread_list_sync;
|
||||
typedef sigc::signal<void, ThreadMembersUpdateData> type_signal_thread_members_update;
|
||||
typedef sigc::signal<void, ThreadUpdateData> type_signal_thread_update;
|
||||
typedef sigc::signal<void, ThreadMemberListUpdateData> type_signal_thread_member_list_update;
|
||||
|
||||
// not discord dispatch events
|
||||
typedef sigc::signal<void, Snowflake> type_signal_added_to_thread;
|
||||
typedef sigc::signal<void, Snowflake> type_signal_removed_from_thread;
|
||||
typedef sigc::signal<void, Message> type_signal_message_unpinned;
|
||||
typedef sigc::signal<void, Message> type_signal_message_pinned;
|
||||
typedef sigc::signal<void, Message> type_signal_message_sent;
|
||||
|
||||
typedef sigc::signal<void, std::string /* nonce */, float /* retry_after */> type_signal_message_send_fail; // retry after param will be 0 if it failed for a reason that isnt slowmode
|
||||
typedef sigc::signal<void, bool, GatewayCloseCode> type_signal_disconnected; // bool true if reconnecting
|
||||
typedef sigc::signal<void> type_signal_connected;
|
||||
@@ -368,6 +394,15 @@ public:
|
||||
type_signal_relationship_add signal_relationship_add();
|
||||
type_signal_message_unpinned signal_message_unpinned();
|
||||
type_signal_message_pinned signal_message_pinned();
|
||||
type_signal_thread_create signal_thread_create();
|
||||
type_signal_thread_delete signal_thread_delete();
|
||||
type_signal_thread_list_sync signal_thread_list_sync();
|
||||
type_signal_thread_members_update signal_thread_members_update();
|
||||
type_signal_thread_update signal_thread_update();
|
||||
type_signal_thread_member_list_update signal_thread_member_list_update();
|
||||
|
||||
type_signal_added_to_thread signal_added_to_thread();
|
||||
type_signal_removed_from_thread signal_removed_from_thread();
|
||||
type_signal_message_sent signal_message_sent();
|
||||
type_signal_message_send_fail signal_message_send_fail();
|
||||
type_signal_disconnected signal_disconnected();
|
||||
@@ -406,6 +441,15 @@ protected:
|
||||
type_signal_relationship_add m_signal_relationship_add;
|
||||
type_signal_message_unpinned m_signal_message_unpinned;
|
||||
type_signal_message_pinned m_signal_message_pinned;
|
||||
type_signal_thread_create m_signal_thread_create;
|
||||
type_signal_thread_delete m_signal_thread_delete;
|
||||
type_signal_thread_list_sync m_signal_thread_list_sync;
|
||||
type_signal_thread_members_update m_signal_thread_members_update;
|
||||
type_signal_thread_update m_signal_thread_update;
|
||||
type_signal_thread_member_list_update m_signal_thread_member_list_update;
|
||||
|
||||
type_signal_removed_from_thread m_signal_removed_from_thread;
|
||||
type_signal_added_to_thread m_signal_added_to_thread;
|
||||
type_signal_message_sent m_signal_message_sent;
|
||||
type_signal_message_send_fail m_signal_message_send_fail;
|
||||
type_signal_disconnected m_signal_disconnected;
|
||||
|
||||
@@ -43,6 +43,7 @@ void from_json(const nlohmann::json &j, GuildData &m) {
|
||||
// JS_O("voice_states", m.VoiceStates);
|
||||
// JS_O("members", m.Members);
|
||||
JS_O("channels", m.Channels);
|
||||
JS_O("threads", m.Threads);
|
||||
// JS_O("presences", m.Presences);
|
||||
JS_ON("max_presences", m.MaxPresences);
|
||||
JS_O("max_members", m.MaxMembers);
|
||||
|
||||
@@ -80,6 +80,7 @@ struct GuildData {
|
||||
std::optional<int> MaxVideoChannelUsers;
|
||||
std::optional<int> ApproximateMemberCount;
|
||||
std::optional<int> ApproximatePresenceCount;
|
||||
std::optional<std::vector<ChannelData>> Threads; // only with permissions to view, id only
|
||||
|
||||
// undocumented
|
||||
// std::map<std::string, Unknown> GuildHashes;
|
||||
|
||||
@@ -27,7 +27,7 @@ enum class MessageType {
|
||||
GUILD_DISCOVERY_REQUALIFIED = 15, // yep
|
||||
GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING = 16, // yep
|
||||
GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING = 17, // yep
|
||||
THREAD_CREATED = 18, // nope
|
||||
THREAD_CREATED = 18, // yep
|
||||
INLINE_REPLY = 19, // yep
|
||||
APPLICATION_COMMAND = 20, // yep
|
||||
THREAD_STARTER_MESSAGE = 21, // nope
|
||||
|
||||
@@ -85,10 +85,16 @@ void to_json(nlohmann::json &j, const LazyLoadRequestMessage &m) {
|
||||
for (const auto &[key, chans] : *m.Channels)
|
||||
j["d"]["channels"][std::to_string(key)] = chans;
|
||||
}
|
||||
j["d"]["typing"] = m.ShouldGetTyping;
|
||||
j["d"]["activities"] = m.ShouldGetActivities;
|
||||
if (m.ShouldGetTyping)
|
||||
j["d"]["typing"] = *m.ShouldGetTyping;
|
||||
if (m.ShouldGetActivities)
|
||||
j["d"]["activities"] = *m.ShouldGetActivities;
|
||||
if (m.ShouldGetThreads)
|
||||
j["d"]["threads"] = *m.ShouldGetThreads;
|
||||
if (m.Members.has_value())
|
||||
j["d"]["members"] = *m.Members;
|
||||
if (m.ThreadIDs.has_value())
|
||||
j["d"]["thread_member_lists"] = *m.ThreadIDs;
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const UpdateStatusMessage &m) {
|
||||
@@ -472,3 +478,57 @@ void to_json(nlohmann::json &j, const FriendRequestObject &m) {
|
||||
void to_json(nlohmann::json &j, const PutRelationshipObject &m) {
|
||||
JS_IF("type", m.Type);
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, ThreadCreateData &m) {
|
||||
j.get_to(m.Channel);
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, ThreadDeleteData &m) {
|
||||
JS_D("id", m.ID);
|
||||
JS_D("guild_id", m.GuildID);
|
||||
JS_D("parent_id", m.ParentID);
|
||||
JS_D("type", m.Type);
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, ThreadListSyncData &m) {
|
||||
JS_D("threads", m.Threads);
|
||||
JS_D("guild_id", m.GuildID);
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, ThreadMembersUpdateData &m) {
|
||||
JS_D("id", m.ID);
|
||||
JS_D("guild_id", m.GuildID);
|
||||
JS_D("member_count", m.MemberCount);
|
||||
JS_O("added_members", m.AddedMembers);
|
||||
JS_O("removed_member_ids", m.RemovedMemberIDs);
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, ArchivedThreadsResponseData &m) {
|
||||
JS_D("threads", m.Threads);
|
||||
JS_D("members", m.Members);
|
||||
JS_D("has_more", m.HasMore);
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, ThreadMemberUpdateData &m) {
|
||||
m.Member = j;
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, ThreadUpdateData &m) {
|
||||
m.Thread = j;
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, ThreadMemberListUpdateData::UserEntry &m) {
|
||||
JS_D("user_id", m.UserID);
|
||||
JS_D("member", m.Member);
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, ThreadMemberListUpdateData &m) {
|
||||
JS_D("thread_id", m.ThreadID);
|
||||
JS_D("guild_id", m.GuildID);
|
||||
JS_D("members", m.Members);
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const ModifyChannelObject &m) {
|
||||
JS_IF("archived", m.Archived);
|
||||
JS_IF("locked", m.Locked);
|
||||
}
|
||||
|
||||
@@ -71,6 +71,13 @@ enum class GatewayEvent : int {
|
||||
GUILD_JOIN_REQUEST_DELETE,
|
||||
RELATIONSHIP_REMOVE,
|
||||
RELATIONSHIP_ADD,
|
||||
THREAD_CREATE,
|
||||
THREAD_UPDATE,
|
||||
THREAD_DELETE,
|
||||
THREAD_LIST_SYNC,
|
||||
THREAD_MEMBER_UPDATE,
|
||||
THREAD_MEMBERS_UPDATE,
|
||||
THREAD_MEMBER_LIST_UPDATE,
|
||||
};
|
||||
|
||||
enum class GatewayCloseCode : uint16_t {
|
||||
@@ -198,10 +205,12 @@ struct GuildMemberListUpdateMessage {
|
||||
|
||||
struct LazyLoadRequestMessage {
|
||||
Snowflake GuildID;
|
||||
bool ShouldGetTyping = false;
|
||||
bool ShouldGetActivities = false;
|
||||
std::optional<std::vector<std::string>> Members; // snowflake?
|
||||
std::optional<std::unordered_map<Snowflake, std::vector<std::pair<int, int>>>> Channels; // channel ID -> range of sidebar
|
||||
std::optional<bool> ShouldGetTyping;
|
||||
std::optional<bool> ShouldGetActivities;
|
||||
std::optional<bool> ShouldGetThreads;
|
||||
std::optional<std::vector<std::string>> Members; // snowflake?
|
||||
std::optional<std::map<Snowflake, std::vector<std::pair<int, int>>>> Channels; // channel ID -> range of sidebar
|
||||
std::optional<std::vector<Snowflake>> ThreadIDs;
|
||||
|
||||
friend void to_json(nlohmann::json &j, const LazyLoadRequestMessage &m);
|
||||
};
|
||||
@@ -659,3 +668,80 @@ struct PutRelationshipObject {
|
||||
|
||||
friend void to_json(nlohmann::json &j, const PutRelationshipObject &m);
|
||||
};
|
||||
|
||||
struct ThreadCreateData {
|
||||
ChannelData Channel;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, ThreadCreateData &m);
|
||||
};
|
||||
|
||||
struct ThreadDeleteData {
|
||||
Snowflake ID;
|
||||
Snowflake GuildID;
|
||||
Snowflake ParentID;
|
||||
ChannelType Type;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, ThreadDeleteData &m);
|
||||
};
|
||||
|
||||
// pretty different from docs
|
||||
struct ThreadListSyncData {
|
||||
std::vector<ChannelData> Threads;
|
||||
Snowflake GuildID;
|
||||
// std::optional<std::vector<???>> MostRecentMessages;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, ThreadListSyncData &m);
|
||||
};
|
||||
|
||||
struct ThreadMembersUpdateData {
|
||||
Snowflake ID;
|
||||
Snowflake GuildID;
|
||||
int MemberCount;
|
||||
std::optional<std::vector<ThreadMemberObject>> AddedMembers;
|
||||
std::optional<std::vector<Snowflake>> RemovedMemberIDs;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, ThreadMembersUpdateData &m);
|
||||
};
|
||||
|
||||
struct ArchivedThreadsResponseData {
|
||||
std::vector<ChannelData> Threads;
|
||||
std::vector<ThreadMemberObject> Members;
|
||||
bool HasMore;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, ArchivedThreadsResponseData &m);
|
||||
};
|
||||
|
||||
struct ThreadMemberUpdateData {
|
||||
ThreadMemberObject Member;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, ThreadMemberUpdateData &m);
|
||||
};
|
||||
|
||||
struct ThreadUpdateData {
|
||||
ChannelData Thread;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, ThreadUpdateData &m);
|
||||
};
|
||||
|
||||
struct ThreadMemberListUpdateData {
|
||||
struct UserEntry {
|
||||
Snowflake UserID;
|
||||
// PresenceData Presence;
|
||||
GuildMember Member;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, UserEntry &m);
|
||||
};
|
||||
|
||||
Snowflake ThreadID;
|
||||
Snowflake GuildID;
|
||||
std::vector<UserEntry> Members;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, ThreadMemberListUpdateData &m);
|
||||
};
|
||||
|
||||
struct ModifyChannelObject {
|
||||
std::optional<bool> Archived;
|
||||
std::optional<bool> Locked;
|
||||
|
||||
friend void to_json(nlohmann::json &j, const ModifyChannelObject &m);
|
||||
};
|
||||
|
||||
@@ -105,6 +105,16 @@ void Store::SetChannel(Snowflake id, const ChannelData &chan) {
|
||||
Bind(m_set_chan_stmt, 17, chan.ParentID);
|
||||
Bind(m_set_chan_stmt, 18, chan.LastPinTimestamp);
|
||||
|
||||
if (chan.ThreadMetadata.has_value()) {
|
||||
Bind(m_set_chan_stmt, 19, chan.ThreadMetadata->IsArchived);
|
||||
Bind(m_set_chan_stmt, 20, chan.ThreadMetadata->AutoArchiveDuration);
|
||||
Bind(m_set_chan_stmt, 21, chan.ThreadMetadata->ArchiveTimestamp);
|
||||
} else {
|
||||
Bind(m_set_chan_stmt, 19, nullptr);
|
||||
Bind(m_set_chan_stmt, 20, nullptr);
|
||||
Bind(m_set_chan_stmt, 21, nullptr);
|
||||
}
|
||||
|
||||
if (!RunInsert(m_set_chan_stmt))
|
||||
fprintf(stderr, "channel insert failed: %s\n", sqlite3_errstr(m_db_err));
|
||||
|
||||
@@ -194,6 +204,12 @@ void Store::SetGuild(Snowflake id, const GuildData &guild) {
|
||||
Bind(m_set_guild_stmt, 37, guild.ApproximateMemberCount);
|
||||
Bind(m_set_guild_stmt, 38, guild.ApproximatePresenceCount);
|
||||
Bind(m_set_guild_stmt, 39, guild.IsLazy);
|
||||
if (guild.Threads.has_value()) {
|
||||
snowflakes.clear();
|
||||
for (const auto &x : *guild.Threads) snowflakes.push_back(x.ID);
|
||||
Bind(m_set_guild_stmt, 40, nlohmann::json(snowflakes).dump());
|
||||
} else
|
||||
Bind(m_set_guild_stmt, 40, "[]"s);
|
||||
|
||||
if (!RunInsert(m_set_guild_stmt))
|
||||
fprintf(stderr, "guild insert failed: %s\n", sqlite3_errstr(m_db_err));
|
||||
@@ -493,6 +509,25 @@ std::vector<Message> Store::GetPinnedMessages(Snowflake channel_id) const {
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<ChannelData> Store::GetActiveThreads(Snowflake channel_id) const {
|
||||
std::vector<ChannelData> ret;
|
||||
|
||||
Bind(m_get_threads_stmt, 1, channel_id);
|
||||
while (FetchOne(m_get_threads_stmt)) {
|
||||
Snowflake x;
|
||||
Get(m_get_threads_stmt, 0, x);
|
||||
auto chan = GetChannel(x);
|
||||
if (chan.has_value())
|
||||
ret.push_back(*chan);
|
||||
}
|
||||
|
||||
Reset(m_get_threads_stmt);
|
||||
|
||||
if (m_db_err != SQLITE_DONE)
|
||||
fprintf(stderr, "error while fetching threads: %s\n", sqlite3_errstr(m_db_err));
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::optional<ChannelData> Store::GetChannel(Snowflake id) const {
|
||||
Bind(m_get_chan_stmt, 1, id);
|
||||
if (!FetchOne(m_get_chan_stmt)) {
|
||||
@@ -527,6 +562,12 @@ std::optional<ChannelData> Store::GetChannel(Snowflake id) const {
|
||||
Get(m_get_chan_stmt, 15, ret.ApplicationID);
|
||||
Get(m_get_chan_stmt, 16, ret.ParentID);
|
||||
Get(m_get_chan_stmt, 17, ret.LastPinTimestamp);
|
||||
if (!IsNull(m_get_chan_stmt, 18)) {
|
||||
ret.ThreadMetadata.emplace();
|
||||
Get(m_get_chan_stmt, 18, ret.ThreadMetadata->IsArchived);
|
||||
Get(m_get_chan_stmt, 19, ret.ThreadMetadata->AutoArchiveDuration);
|
||||
Get(m_get_chan_stmt, 20, ret.ThreadMetadata->ArchiveTimestamp);
|
||||
}
|
||||
|
||||
Reset(m_get_chan_stmt);
|
||||
|
||||
@@ -626,6 +667,10 @@ std::optional<GuildData> Store::GetGuild(Snowflake id) const {
|
||||
Get(m_get_guild_stmt, 36, ret.ApproximateMemberCount);
|
||||
Get(m_get_guild_stmt, 37, ret.ApproximatePresenceCount);
|
||||
Get(m_get_guild_stmt, 38, ret.IsLazy);
|
||||
Get(m_get_guild_stmt, 39, tmp);
|
||||
ret.Threads.emplace();
|
||||
for (const auto &id : nlohmann::json::parse(tmp).get<std::vector<Snowflake>>())
|
||||
ret.Threads->emplace_back().ID = id;
|
||||
|
||||
Reset(m_get_guild_stmt);
|
||||
|
||||
@@ -760,6 +805,12 @@ void Store::ClearGuild(Snowflake id) {
|
||||
|
||||
void Store::ClearChannel(Snowflake id) {
|
||||
m_channels.erase(id);
|
||||
Bind(m_clear_chan_stmt, 1, id);
|
||||
|
||||
if ((m_db_err = sqlite3_step(m_clear_chan_stmt)) != SQLITE_DONE)
|
||||
printf("clearing channel failed: %s\n", sqlite3_errstr(m_db_err));
|
||||
|
||||
Reset(m_clear_chan_stmt);
|
||||
}
|
||||
|
||||
void Store::ClearBan(Snowflake guild_id, Snowflake user_id) {
|
||||
@@ -934,7 +985,8 @@ bool Store::CreateTables() {
|
||||
max_video_users INTEGER,
|
||||
approx_members INTEGER,
|
||||
approx_presences INTEGER,
|
||||
lazy BOOL
|
||||
lazy BOOL,
|
||||
threads TEXT NOT NULL /* json */
|
||||
)
|
||||
)";
|
||||
|
||||
@@ -957,7 +1009,10 @@ bool Store::CreateTables() {
|
||||
owner_id INTEGER,
|
||||
application_id INTEGER,
|
||||
parent_id INTEGER,
|
||||
last_pin_timestamp TEXT
|
||||
last_pin_timestamp TEXT,
|
||||
archived BOOL, /* threads */
|
||||
auto_archive INTEGER, /* threads */
|
||||
archived_ts TEXT /* threads */
|
||||
)
|
||||
)";
|
||||
|
||||
@@ -1116,7 +1171,7 @@ bool Store::CreateStatements() {
|
||||
|
||||
const char *set_guild = R"(
|
||||
REPLACE INTO guilds VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
)";
|
||||
|
||||
@@ -1126,7 +1181,7 @@ bool Store::CreateStatements() {
|
||||
|
||||
const char *set_chan = R"(
|
||||
REPLACE INTO channels VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
)";
|
||||
|
||||
@@ -1175,6 +1230,14 @@ bool Store::CreateStatements() {
|
||||
SELECT id FROM messages WHERE channel_id = ? AND pinned = 1 ORDER BY id ASC
|
||||
)";
|
||||
|
||||
const char *get_threads = R"(
|
||||
SELECT id FROM channels WHERE parent_id = ? AND (type = 10 OR type = 11 OR type = 12) AND archived = FALSE
|
||||
)";
|
||||
|
||||
const char *clear_chan = R"(
|
||||
DELETE FROM channels WHERE id = ?
|
||||
)";
|
||||
|
||||
m_db_err = sqlite3_prepare_v2(m_db, set_user, -1, &m_set_user_stmt, nullptr);
|
||||
if (m_db_err != SQLITE_OK) {
|
||||
fprintf(stderr, "failed to prepare set user statement: %s\n", sqlite3_errstr(m_db_err));
|
||||
@@ -1315,7 +1378,19 @@ bool Store::CreateStatements() {
|
||||
|
||||
m_db_err = sqlite3_prepare_v2(m_db, get_pins, -1, &m_get_pins_stmt, nullptr);
|
||||
if (m_db_err != SQLITE_OK) {
|
||||
fprintf(stderr, "failed to prepare getp ins statement: %s\n", sqlite3_errstr(m_db_err));
|
||||
fprintf(stderr, "failed to prepare get pins statement: %s\n", sqlite3_errstr(m_db_err));
|
||||
return false;
|
||||
}
|
||||
|
||||
m_db_err = sqlite3_prepare_v2(m_db, get_threads, -1, &m_get_threads_stmt, nullptr);
|
||||
if (m_db_err != SQLITE_OK) {
|
||||
fprintf(stderr, "failed to prepare get threads statement: %s\n", sqlite3_errstr(m_db_err));
|
||||
return false;
|
||||
}
|
||||
|
||||
m_db_err = sqlite3_prepare_v2(m_db, clear_chan, -1, &m_clear_chan_stmt, nullptr);
|
||||
if (m_db_err != SQLITE_OK) {
|
||||
fprintf(stderr, "failed to prepare clear channel statement: %s\n", sqlite3_errstr(m_db_err));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1347,6 +1422,8 @@ void Store::Cleanup() {
|
||||
sqlite3_finalize(m_get_last_msgs_stmt);
|
||||
sqlite3_finalize(m_get_msg_ids_stmt);
|
||||
sqlite3_finalize(m_get_pins_stmt);
|
||||
sqlite3_finalize(m_get_threads_stmt);
|
||||
sqlite3_finalize(m_clear_chan_stmt);
|
||||
}
|
||||
|
||||
void Store::Bind(sqlite3_stmt *stmt, int index, int num) const {
|
||||
|
||||
@@ -44,6 +44,7 @@ public:
|
||||
std::vector<Message> GetLastMessages(Snowflake id, size_t num) const;
|
||||
std::vector<Snowflake> GetChannelMessageIDs(Snowflake id) const;
|
||||
std::vector<Message> GetPinnedMessages(Snowflake channel_id) const;
|
||||
std::vector<ChannelData> GetActiveThreads(Snowflake channel_id) const; // public
|
||||
|
||||
void ClearGuild(Snowflake id);
|
||||
void ClearChannel(Snowflake id);
|
||||
@@ -135,6 +136,8 @@ private:
|
||||
mutable sqlite3_stmt *m_get_last_msgs_stmt;
|
||||
mutable sqlite3_stmt *m_get_msg_ids_stmt;
|
||||
mutable sqlite3_stmt *m_get_pins_stmt;
|
||||
mutable sqlite3_stmt *m_get_threads_stmt;
|
||||
mutable sqlite3_stmt *m_clear_chan_stmt;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
|
||||
@@ -537,6 +537,70 @@ void GuildSettingsAuditLogPane::OnAuditLogFetch(const AuditLogData &data) {
|
||||
"</b>";
|
||||
}
|
||||
} break;
|
||||
case AuditLogActionType::THREAD_CREATE: {
|
||||
const auto channel = discord.GetChannel(entry.TargetID);
|
||||
markup = user_markup +
|
||||
" created a thread <b>" +
|
||||
(channel.has_value()
|
||||
? Glib::Markup::escape_text(*channel->Name)
|
||||
: Glib::ustring(*entry.GetNewFromKey<std::string>("name"))) +
|
||||
"</b>";
|
||||
if (entry.Changes.has_value()) {
|
||||
for (const auto &change : *entry.Changes) {
|
||||
if (change.Key == "name")
|
||||
extra_markup.push_back("Set the name to <b>" + Glib::Markup::escape_text(change.NewValue->get<std::string>()) + "</b>");
|
||||
else if (change.Key == "archived")
|
||||
extra_markup.push_back(change.NewValue->get<bool>() ? "Archived the thread" : "Unarchived the thread");
|
||||
else if (change.Key == "auto_archive_duration")
|
||||
extra_markup.push_back("Set auto archive duration to <b>"s + std::to_string(change.NewValue->get<int>()) + " minutes</b>"s);
|
||||
else if (change.Key == "rate_limit_per_user" && change.NewValue.has_value()) {
|
||||
const int secs = change.NewValue->get<int>();
|
||||
if (secs == 0)
|
||||
extra_markup.push_back("Disabled slowmode");
|
||||
else
|
||||
extra_markup.push_back("Set slowmode to <b>" +
|
||||
std::to_string(secs) + " seconds</b>");
|
||||
} else if (change.Key == "locked")
|
||||
extra_markup.push_back(change.NewValue->get<bool>() ? "Locked the thread, restricting it to only be unarchived by moderators" : "Unlocked the thread, allowing it to be unarchived by non-moderators");
|
||||
}
|
||||
}
|
||||
} break;
|
||||
case AuditLogActionType::THREAD_UPDATE: {
|
||||
const auto channel = discord.GetChannel(entry.TargetID);
|
||||
markup = user_markup +
|
||||
" made changes to the thread <b>" +
|
||||
(channel.has_value()
|
||||
? Glib::Markup::escape_text(*channel->Name)
|
||||
: Glib::ustring(entry.TargetID)) +
|
||||
"</b>";
|
||||
for (const auto &change : *entry.Changes) {
|
||||
if (change.Key == "name")
|
||||
extra_markup.push_back(
|
||||
"Changed the name from <b>" +
|
||||
Glib::Markup::escape_text(change.OldValue->get<std::string>()) +
|
||||
"</b> to <b>" +
|
||||
Glib::Markup::escape_text(change.NewValue->get<std::string>()) +
|
||||
"</b>");
|
||||
else if (change.Key == "auto_archive_duration")
|
||||
extra_markup.push_back("Set auto archive duration to <b>"s + std::to_string(change.NewValue->get<int>()) + " minutes</b>"s);
|
||||
else if (change.Key == "rate_limit_per_user" && change.NewValue.has_value()) {
|
||||
const int secs = change.NewValue->get<int>();
|
||||
if (secs == 0)
|
||||
extra_markup.push_back("Disabled slowmode");
|
||||
else
|
||||
extra_markup.push_back("Set slowmode to <b>" +
|
||||
std::to_string(secs) +
|
||||
" seconds</b>");
|
||||
} else if (change.Key == "locked")
|
||||
extra_markup.push_back(change.NewValue->get<bool>() ? "Locked the thread, restricting it to only be unarchived by moderators" : "Unlocked the thread, allowing it to be unarchived by non-moderators");
|
||||
else if (change.Key == "archived")
|
||||
extra_markup.push_back(change.NewValue->get<bool>() ? "Archived the thread" : "Unarchived the thread");
|
||||
}
|
||||
} break;
|
||||
case AuditLogActionType::THREAD_DELETE: {
|
||||
markup = user_markup +
|
||||
" deleted the thread <b>" + Glib::Markup::escape_text(*entry.GetOldFromKey<std::string>("name")) + "</b>";
|
||||
} break;
|
||||
default:
|
||||
markup = "<i>Unknown action</i>";
|
||||
break;
|
||||
|
||||
@@ -41,8 +41,10 @@ MainWindow::MainWindow()
|
||||
m_menu_view.set_submenu(m_menu_view_sub);
|
||||
m_menu_view_friends.set_label("Friends");
|
||||
m_menu_view_pins.set_label("Pins");
|
||||
m_menu_view_threads.set_label("Threads");
|
||||
m_menu_view_sub.append(m_menu_view_friends);
|
||||
m_menu_view_sub.append(m_menu_view_pins);
|
||||
m_menu_view_sub.append(m_menu_view_threads);
|
||||
m_menu_view_sub.signal_popped_up().connect(sigc::mem_fun(*this, &MainWindow::OnViewSubmenuPopup));
|
||||
|
||||
m_menu_bar.append(m_menu_file);
|
||||
@@ -92,6 +94,10 @@ MainWindow::MainWindow()
|
||||
m_signal_action_view_pins.emit(GetChatActiveChannel());
|
||||
});
|
||||
|
||||
m_menu_view_threads.signal_activate().connect([this] {
|
||||
m_signal_action_view_threads.emit(GetChatActiveChannel());
|
||||
});
|
||||
|
||||
m_content_box.set_hexpand(true);
|
||||
m_content_box.set_vexpand(true);
|
||||
m_content_box.show();
|
||||
@@ -243,8 +249,11 @@ void MainWindow::OnViewSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gd
|
||||
auto channel_id = GetChatActiveChannel();
|
||||
auto channel = Abaddon::Get().GetDiscordClient().GetChannel(channel_id);
|
||||
m_menu_view_pins.set_sensitive(false);
|
||||
if (channel.has_value())
|
||||
m_menu_view_pins.set_sensitive(channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::DM || channel->Type == ChannelType::GROUP_DM);
|
||||
m_menu_view_threads.set_sensitive(false);
|
||||
if (channel.has_value()) {
|
||||
m_menu_view_threads.set_sensitive(channel->Type == ChannelType::GUILD_TEXT || channel->IsThread());
|
||||
m_menu_view_pins.set_sensitive(channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::DM || channel->Type == ChannelType::GROUP_DM || channel->IsThread());
|
||||
}
|
||||
}
|
||||
|
||||
ChannelList *MainWindow::GetChannelList() {
|
||||
@@ -290,3 +299,7 @@ MainWindow::type_signal_action_add_recipient MainWindow::signal_action_add_recip
|
||||
MainWindow::type_signal_action_view_pins MainWindow::signal_action_view_pins() {
|
||||
return m_signal_action_view_pins;
|
||||
}
|
||||
|
||||
MainWindow::type_signal_action_view_threads MainWindow::signal_action_view_threads() {
|
||||
return m_signal_action_view_threads;
|
||||
}
|
||||
|
||||
@@ -35,8 +35,10 @@ public:
|
||||
typedef sigc::signal<void> type_signal_action_reload_css;
|
||||
typedef sigc::signal<void> type_signal_action_join_guild;
|
||||
typedef sigc::signal<void> type_signal_action_set_status;
|
||||
// this should probably be removed
|
||||
typedef sigc::signal<void, Snowflake> type_signal_action_add_recipient; // channel id
|
||||
typedef sigc::signal<void, Snowflake> type_signal_action_view_pins; // channel id
|
||||
typedef sigc::signal<void, Snowflake> type_signal_action_view_threads; // channel id
|
||||
|
||||
type_signal_action_connect signal_action_connect();
|
||||
type_signal_action_disconnect signal_action_disconnect();
|
||||
@@ -46,6 +48,7 @@ public:
|
||||
type_signal_action_set_status signal_action_set_status();
|
||||
type_signal_action_add_recipient signal_action_add_recipient();
|
||||
type_signal_action_view_pins signal_action_view_pins();
|
||||
type_signal_action_view_threads signal_action_view_threads();
|
||||
|
||||
protected:
|
||||
type_signal_action_connect m_signal_action_connect;
|
||||
@@ -56,6 +59,7 @@ protected:
|
||||
type_signal_action_set_status m_signal_action_set_status;
|
||||
type_signal_action_add_recipient m_signal_action_add_recipient;
|
||||
type_signal_action_view_pins m_signal_action_view_pins;
|
||||
type_signal_action_view_threads m_signal_action_view_threads;
|
||||
|
||||
protected:
|
||||
Gtk::Box m_main_box;
|
||||
@@ -90,5 +94,6 @@ protected:
|
||||
Gtk::Menu m_menu_view_sub;
|
||||
Gtk::MenuItem m_menu_view_friends;
|
||||
Gtk::MenuItem m_menu_view_pins;
|
||||
Gtk::MenuItem m_menu_view_threads;
|
||||
void OnViewSubmenuPopup(const Gdk::Rectangle *flipped_rect, const Gdk::Rectangle *final_rect, bool flipped_x, bool flipped_y);
|
||||
};
|
||||
|
||||
149
windows/threadswindow.cpp
Normal file
149
windows/threadswindow.cpp
Normal file
@@ -0,0 +1,149 @@
|
||||
#include "threadswindow.hpp"
|
||||
#include "../abaddon.hpp"
|
||||
|
||||
ThreadsWindow::ThreadsWindow(const ChannelData &channel)
|
||||
: m_channel_id(channel.ID)
|
||||
, m_box(Gtk::ORIENTATION_VERTICAL)
|
||||
, m_active(channel, sigc::mem_fun(*this, &ThreadsWindow::ListFilterFunc))
|
||||
, m_archived(channel, sigc::mem_fun(*this, &ThreadsWindow::ListFilterFunc))
|
||||
, m_filter_public(m_group, "Public")
|
||||
, m_filter_private(m_group, "Private") {
|
||||
set_name("threads-window");
|
||||
set_default_size(450, 375);
|
||||
set_title("#" + *channel.Name + " - Threads");
|
||||
set_position(Gtk::WIN_POS_CENTER);
|
||||
get_style_context()->add_class("app-window");
|
||||
get_style_context()->add_class("app-popup");
|
||||
get_style_context()->add_class("threads-window");
|
||||
|
||||
const auto cb = [this](Snowflake id) {
|
||||
Abaddon::Get().ActionChannelOpened(id);
|
||||
hide();
|
||||
};
|
||||
m_active.signal_thread_open().connect(cb);
|
||||
m_archived.signal_thread_open().connect(cb);
|
||||
|
||||
m_switcher.set_halign(Gtk::ALIGN_CENTER);
|
||||
m_switcher.set_stack(m_stack);
|
||||
|
||||
m_stack.add(m_active, "active", "Active Threads");
|
||||
m_stack.add(m_archived, "archived", "Archived Threads");
|
||||
|
||||
m_filter_buttons.set_homogeneous(true);
|
||||
m_filter_buttons.set_halign(Gtk::ALIGN_CENTER);
|
||||
m_filter_buttons.add(m_filter_public);
|
||||
m_filter_buttons.add(m_filter_private);
|
||||
|
||||
// a little strange
|
||||
const auto btncb = [this](Gtk::RadioButton *btn) {
|
||||
if (btn->get_active()) {
|
||||
if (btn == &m_filter_public)
|
||||
m_filter_mode = FILTER_PUBLIC;
|
||||
else if (btn == &m_filter_private)
|
||||
m_filter_mode = FILTER_PRIVATE;
|
||||
|
||||
m_active.InvalidateFilter();
|
||||
m_archived.InvalidateFilter();
|
||||
}
|
||||
};
|
||||
m_filter_public.signal_toggled().connect(sigc::bind(btncb, &m_filter_public));
|
||||
m_filter_private.signal_toggled().connect(sigc::bind(btncb, &m_filter_private));
|
||||
|
||||
m_active.show();
|
||||
m_archived.show();
|
||||
m_switcher.show();
|
||||
m_filter_buttons.show_all();
|
||||
m_stack.show();
|
||||
m_box.show();
|
||||
|
||||
m_box.add(m_switcher);
|
||||
m_box.add(m_filter_buttons);
|
||||
m_box.add(m_stack);
|
||||
add(m_box);
|
||||
}
|
||||
|
||||
bool ThreadsWindow::ListFilterFunc(Gtk::ListBoxRow *row_) {
|
||||
if (auto *row = dynamic_cast<ThreadListRow *>(row_))
|
||||
return (m_filter_mode == FILTER_PUBLIC && (row->Type == ChannelType::GUILD_PUBLIC_THREAD || row->Type == ChannelType::GUILD_NEWS_THREAD)) ||
|
||||
(m_filter_mode == FILTER_PRIVATE && row->Type == ChannelType::GUILD_PRIVATE_THREAD);
|
||||
return false;
|
||||
}
|
||||
|
||||
ThreadListRow::ThreadListRow(const ChannelData &channel)
|
||||
: ID(channel.ID)
|
||||
, Type(channel.Type)
|
||||
, m_label(*channel.Name, Gtk::ALIGN_START) {
|
||||
m_label.show();
|
||||
add(m_label);
|
||||
}
|
||||
|
||||
ActiveThreadsList::ActiveThreadsList(const ChannelData &channel, const Gtk::ListBox::SlotFilter &filter) {
|
||||
set_vexpand(true);
|
||||
|
||||
m_list.set_filter_func(filter);
|
||||
m_list.set_selection_mode(Gtk::SELECTION_SINGLE);
|
||||
m_list.set_hexpand(true);
|
||||
m_list.show();
|
||||
|
||||
add(m_list);
|
||||
|
||||
m_list.signal_button_press_event().connect([this](GdkEventButton *ev) -> bool {
|
||||
if (ev->button == GDK_BUTTON_PRIMARY && ev->type == GDK_2BUTTON_PRESS) {
|
||||
if (auto row = dynamic_cast<ThreadListRow *>(m_list.get_selected_row()))
|
||||
m_signal_thread_open.emit(row->ID);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const auto threads = Abaddon::Get().GetDiscordClient().GetActiveThreads(channel.ID);
|
||||
for (const auto &thread : threads) {
|
||||
auto row = Gtk::manage(new ThreadListRow(thread));
|
||||
row->show();
|
||||
m_list.add(*row);
|
||||
}
|
||||
}
|
||||
|
||||
void ActiveThreadsList::InvalidateFilter() {
|
||||
m_list.invalidate_filter();
|
||||
}
|
||||
|
||||
ActiveThreadsList::type_signal_thread_open ActiveThreadsList::signal_thread_open() {
|
||||
return m_signal_thread_open;
|
||||
}
|
||||
|
||||
ArchivedThreadsList::ArchivedThreadsList(const ChannelData &channel, const Gtk::ListBox::SlotFilter &filter) {
|
||||
set_vexpand(true);
|
||||
|
||||
m_list.set_filter_func(filter);
|
||||
m_list.set_selection_mode(Gtk::SELECTION_SINGLE);
|
||||
m_list.set_hexpand(true);
|
||||
m_list.show();
|
||||
|
||||
add(m_list);
|
||||
|
||||
m_list.signal_button_press_event().connect([this](GdkEventButton *ev) -> bool {
|
||||
if (ev->button == GDK_BUTTON_PRIMARY && ev->type == GDK_2BUTTON_PRESS) {
|
||||
if (auto row = dynamic_cast<ThreadListRow *>(m_list.get_selected_row()))
|
||||
m_signal_thread_open.emit(row->ID);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
Abaddon::Get().GetDiscordClient().GetArchivedPublicThreads(channel.ID, sigc::mem_fun(*this, &ArchivedThreadsList::OnPublicFetched));
|
||||
}
|
||||
|
||||
void ArchivedThreadsList::InvalidateFilter() {
|
||||
m_list.invalidate_filter();
|
||||
}
|
||||
|
||||
void ArchivedThreadsList::OnPublicFetched(DiscordError code, const ArchivedThreadsResponseData &data) {
|
||||
for (const auto &thread : data.Threads) {
|
||||
auto row = Gtk::manage(new ThreadListRow(thread));
|
||||
row->show();
|
||||
m_list.add(*row);
|
||||
}
|
||||
}
|
||||
|
||||
ArchivedThreadsList::type_signal_thread_open ArchivedThreadsList::signal_thread_open() {
|
||||
return m_signal_thread_open;
|
||||
}
|
||||
79
windows/threadswindow.hpp
Normal file
79
windows/threadswindow.hpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
#include <gtkmm.h>
|
||||
#include "../discord/objects.hpp"
|
||||
|
||||
class ActiveThreadsList : public Gtk::ScrolledWindow {
|
||||
public:
|
||||
ActiveThreadsList(const ChannelData &channel, const Gtk::ListBox::SlotFilter &filter);
|
||||
|
||||
void InvalidateFilter();
|
||||
|
||||
private:
|
||||
Gtk::ListBox m_list;
|
||||
|
||||
using type_signal_thread_open = sigc::signal<void, Snowflake>;
|
||||
type_signal_thread_open m_signal_thread_open;
|
||||
|
||||
public:
|
||||
type_signal_thread_open signal_thread_open();
|
||||
};
|
||||
|
||||
class ArchivedThreadsList : public Gtk::ScrolledWindow {
|
||||
public:
|
||||
ArchivedThreadsList(const ChannelData &channel, const Gtk::ListBox::SlotFilter &filter);
|
||||
|
||||
void InvalidateFilter();
|
||||
|
||||
private:
|
||||
Gtk::ListBox m_list;
|
||||
|
||||
void OnPublicFetched(DiscordError code, const ArchivedThreadsResponseData &data);
|
||||
|
||||
using type_signal_thread_open = sigc::signal<void, Snowflake>;
|
||||
type_signal_thread_open m_signal_thread_open;
|
||||
|
||||
public:
|
||||
type_signal_thread_open signal_thread_open();
|
||||
};
|
||||
|
||||
// view all threads in a channel
|
||||
class ThreadsWindow : public Gtk::Window {
|
||||
public:
|
||||
ThreadsWindow(const ChannelData &channel);
|
||||
|
||||
private:
|
||||
// this filtering is rather cringe but idk what a better alternative would be
|
||||
bool ListFilterFunc(Gtk::ListBoxRow *row_);
|
||||
|
||||
enum FilterMode {
|
||||
FILTER_PUBLIC = 0,
|
||||
FILTER_PRIVATE = 1,
|
||||
};
|
||||
bool m_filter_mode = FILTER_PUBLIC;
|
||||
|
||||
Snowflake m_channel_id;
|
||||
|
||||
Gtk::StackSwitcher m_switcher;
|
||||
Gtk::Stack m_stack;
|
||||
|
||||
Gtk::RadioButtonGroup m_group;
|
||||
Gtk::ButtonBox m_filter_buttons;
|
||||
Gtk::RadioButton m_filter_public;
|
||||
Gtk::RadioButton m_filter_private;
|
||||
|
||||
Gtk::Box m_box;
|
||||
|
||||
ActiveThreadsList m_active;
|
||||
ArchivedThreadsList m_archived;
|
||||
};
|
||||
|
||||
class ThreadListRow : public Gtk::ListBoxRow {
|
||||
public:
|
||||
ThreadListRow(const ChannelData &channel);
|
||||
|
||||
Snowflake ID;
|
||||
ChannelType Type;
|
||||
|
||||
private:
|
||||
Gtk::Label m_label;
|
||||
};
|
||||
Reference in New Issue
Block a user