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