Merge pull request #279 from uowuo/stages

Support for stages
This commit is contained in:
ouwou 2024-07-05 03:51:58 -04:00 committed by GitHub
commit 68db143c89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 707 additions and 135 deletions

View File

@ -19,7 +19,7 @@
#include "windows/profilewindow.hpp" #include "windows/profilewindow.hpp"
#include "windows/pinnedwindow.hpp" #include "windows/pinnedwindow.hpp"
#include "windows/threadswindow.hpp" #include "windows/threadswindow.hpp"
#include "windows/voicewindow.hpp" #include "windows/voice/voicewindow.hpp"
#include "startup.hpp" #include "startup.hpp"
#include "notifications/notifications.hpp" #include "notifications/notifications.hpp"
#include "remoteauth/remoteauthdialog.hpp" #include "remoteauth/remoteauthdialog.hpp"

View File

@ -123,6 +123,7 @@ void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &m
case RenderType::Thread: case RenderType::Thread:
return get_preferred_width_vfunc_thread(widget, minimum_width, natural_width); return get_preferred_width_vfunc_thread(widget, minimum_width, natural_width);
case RenderType::VoiceChannel: case RenderType::VoiceChannel:
case RenderType::VoiceStage:
return get_preferred_width_vfunc_voice_channel(widget, minimum_width, natural_width); return get_preferred_width_vfunc_voice_channel(widget, minimum_width, natural_width);
case RenderType::VoiceParticipant: case RenderType::VoiceParticipant:
return get_preferred_width_vfunc_voice_participant(widget, minimum_width, natural_width); return get_preferred_width_vfunc_voice_participant(widget, minimum_width, natural_width);
@ -146,6 +147,7 @@ void CellRendererChannels::get_preferred_width_for_height_vfunc(Gtk::Widget &wid
case RenderType::Thread: case RenderType::Thread:
return get_preferred_width_for_height_vfunc_thread(widget, height, minimum_width, natural_width); return get_preferred_width_for_height_vfunc_thread(widget, height, minimum_width, natural_width);
case RenderType::VoiceChannel: case RenderType::VoiceChannel:
case RenderType::VoiceStage:
return get_preferred_width_for_height_vfunc_voice_channel(widget, height, minimum_width, natural_width); return get_preferred_width_for_height_vfunc_voice_channel(widget, height, minimum_width, natural_width);
case RenderType::VoiceParticipant: case RenderType::VoiceParticipant:
return get_preferred_width_for_height_vfunc_voice_participant(widget, height, minimum_width, natural_width); return get_preferred_width_for_height_vfunc_voice_participant(widget, height, minimum_width, natural_width);
@ -169,6 +171,7 @@ void CellRendererChannels::get_preferred_height_vfunc(Gtk::Widget &widget, int &
case RenderType::Thread: case RenderType::Thread:
return get_preferred_height_vfunc_thread(widget, minimum_height, natural_height); return get_preferred_height_vfunc_thread(widget, minimum_height, natural_height);
case RenderType::VoiceChannel: case RenderType::VoiceChannel:
case RenderType::VoiceStage:
return get_preferred_height_vfunc_voice_channel(widget, minimum_height, natural_height); return get_preferred_height_vfunc_voice_channel(widget, minimum_height, natural_height);
case RenderType::VoiceParticipant: case RenderType::VoiceParticipant:
return get_preferred_height_vfunc_voice_participant(widget, minimum_height, natural_height); return get_preferred_height_vfunc_voice_participant(widget, minimum_height, natural_height);
@ -192,6 +195,7 @@ void CellRendererChannels::get_preferred_height_for_width_vfunc(Gtk::Widget &wid
case RenderType::Thread: case RenderType::Thread:
return get_preferred_height_for_width_vfunc_thread(widget, width, minimum_height, natural_height); return get_preferred_height_for_width_vfunc_thread(widget, width, minimum_height, natural_height);
case RenderType::VoiceChannel: case RenderType::VoiceChannel:
case RenderType::VoiceStage:
return get_preferred_height_for_width_vfunc_voice_channel(widget, width, minimum_height, natural_height); return get_preferred_height_for_width_vfunc_voice_channel(widget, width, minimum_height, natural_height);
case RenderType::VoiceParticipant: case RenderType::VoiceParticipant:
return get_preferred_height_for_width_vfunc_voice_participant(widget, width, minimum_height, natural_height); return get_preferred_height_for_width_vfunc_voice_participant(widget, width, minimum_height, natural_height);
@ -215,7 +219,9 @@ void CellRendererChannels::render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr,
case RenderType::Thread: case RenderType::Thread:
return render_vfunc_thread(cr, widget, background_area, cell_area, flags); return render_vfunc_thread(cr, widget, background_area, cell_area, flags);
case RenderType::VoiceChannel: case RenderType::VoiceChannel:
return render_vfunc_voice_channel(cr, widget, background_area, cell_area, flags); return render_vfunc_voice_channel(cr, widget, background_area, cell_area, flags, "\U0001F50A");
case RenderType::VoiceStage:
return render_vfunc_voice_channel(cr, widget, background_area, cell_area, flags, "\U0001F4E1");
case RenderType::VoiceParticipant: case RenderType::VoiceParticipant:
return render_vfunc_voice_participant(cr, widget, background_area, cell_area, flags); return render_vfunc_voice_participant(cr, widget, background_area, cell_area, flags);
case RenderType::DMHeader: case RenderType::DMHeader:
@ -571,7 +577,7 @@ void CellRendererChannels::get_preferred_height_for_width_vfunc_voice_channel(Gt
m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height); m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height);
} }
void CellRendererChannels::render_vfunc_voice_channel(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { void CellRendererChannels::render_vfunc_voice_channel(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags, const char *emoji) {
// channel name text // channel name text
Gtk::Requisition minimum_size, natural_size; Gtk::Requisition minimum_size, natural_size;
m_renderer_text.get_preferred_size(widget, minimum_size, natural_size); m_renderer_text.get_preferred_size(widget, minimum_size, natural_size);
@ -588,7 +594,7 @@ void CellRendererChannels::render_vfunc_voice_channel(const Cairo::RefPtr<Cairo:
Pango::FontDescription font; Pango::FontDescription font;
font.set_family("sans 14"); font.set_family("sans 14");
auto layout = widget.create_pango_layout("\U0001F50A"); auto layout = widget.create_pango_layout(emoji);
layout->set_font_description(font); layout->set_font_description(font);
layout->set_alignment(Pango::ALIGN_LEFT); layout->set_alignment(Pango::ALIGN_LEFT);
cr->set_source_rgba(1.0, 1.0, 1.0, 1.0); cr->set_source_rgba(1.0, 1.0, 1.0, 1.0);

View File

@ -6,7 +6,7 @@
#include <gtkmm/cellrendererpixbuf.h> #include <gtkmm/cellrendererpixbuf.h>
#include <gtkmm/cellrenderertext.h> #include <gtkmm/cellrenderertext.h>
#include "discord/snowflake.hpp" #include "discord/snowflake.hpp"
#include "discord/voicestateflags.hpp" #include "discord/voicestate.hpp"
#include "misc/bitwise.hpp" #include "misc/bitwise.hpp"
enum class RenderType : uint8_t { enum class RenderType : uint8_t {
@ -16,6 +16,7 @@ enum class RenderType : uint8_t {
TextChannel, TextChannel,
Thread, Thread,
VoiceChannel, VoiceChannel,
VoiceStage, // identical to non-stage except for icon
VoiceParticipant, VoiceParticipant,
DMHeader, DMHeader,
@ -112,7 +113,8 @@ protected:
Gtk::Widget &widget, Gtk::Widget &widget,
const Gdk::Rectangle &background_area, const Gdk::Rectangle &background_area,
const Gdk::Rectangle &cell_area, const Gdk::Rectangle &cell_area,
Gtk::CellRendererState flags); Gtk::CellRendererState flags,
const char *emoji);
// voice participant // voice participant
void get_preferred_width_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; void get_preferred_width_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;

View File

@ -28,6 +28,8 @@ ChannelListTree::ChannelListTree()
#endif #endif
, m_menu_voice_channel_join("_Join", true) , m_menu_voice_channel_join("_Join", true)
, m_menu_voice_channel_disconnect("_Disconnect", true) , m_menu_voice_channel_disconnect("_Disconnect", true)
, m_menu_voice_stage_join("_Join", true)
, m_menu_voice_stage_disconnect("_Disconnect", true)
, m_menu_voice_channel_mark_as_read("Mark as _Read", true) , m_menu_voice_channel_mark_as_read("Mark as _Read", true)
, m_menu_voice_open_chat("Open _Chat", true) , m_menu_voice_open_chat("Open _Chat", true)
, m_menu_dm_copy_id("_Copy ID", true) , m_menu_dm_copy_id("_Copy ID", true)
@ -225,6 +227,21 @@ ChannelListTree::ChannelListTree()
m_menu_voice_channel.append(m_menu_voice_open_chat); m_menu_voice_channel.append(m_menu_voice_open_chat);
m_menu_voice_channel.show_all(); m_menu_voice_channel.show_all();
#ifdef WITH_VOICE
m_menu_voice_stage_join.signal_activate().connect([this]() {
const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
m_signal_action_join_voice_channel.emit(id);
});
m_menu_voice_stage_disconnect.signal_activate().connect([this]() {
m_signal_action_disconnect_voice.emit();
});
#endif
m_menu_voice_stage.append(m_menu_voice_stage_join);
m_menu_voice_stage.append(m_menu_voice_stage_disconnect);
m_menu_voice_stage.show_all();
m_menu_dm_copy_id.signal_activate().connect([this] { m_menu_dm_copy_id.signal_activate().connect([this] {
Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id])); Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]));
}); });
@ -366,8 +383,8 @@ int ChannelListTree::SortFunc(const Gtk::TreeModel::iterator &a, const Gtk::Tree
const int64_t b_sort = (*b)[m_columns.m_sort]; const int64_t b_sort = (*b)[m_columns.m_sort];
if (a_type == RenderType::DMHeader) return -1; if (a_type == RenderType::DMHeader) return -1;
if (b_type == RenderType::DMHeader) return 1; if (b_type == RenderType::DMHeader) return 1;
if (a_type == RenderType::TextChannel && b_type == RenderType::VoiceChannel) return -1; if (a_type == RenderType::TextChannel && (b_type == RenderType::VoiceChannel || b_type == RenderType::VoiceStage)) return -1;
if (b_type == RenderType::TextChannel && a_type == RenderType::VoiceChannel) return 1; if (b_type == RenderType::TextChannel && (a_type == RenderType::VoiceChannel || a_type == RenderType::VoiceStage)) return 1;
return static_cast<int>(std::clamp(a_sort - b_sort, int64_t(-1), int64_t(1))); return static_cast<int>(std::clamp(a_sort - b_sort, int64_t(-1), int64_t(1)));
} }
@ -634,6 +651,7 @@ void ChannelListTree::OnThreadListSync(const ThreadListSyncData &data) {
void ChannelListTree::OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id) { void ChannelListTree::OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id) {
auto parent_iter = GetIteratorForRowFromIDOfType(channel_id, RenderType::VoiceChannel); auto parent_iter = GetIteratorForRowFromIDOfType(channel_id, RenderType::VoiceChannel);
if (!parent_iter) parent_iter = GetIteratorForRowFromIDOfType(channel_id, RenderType::VoiceStage);
if (!parent_iter) parent_iter = GetIteratorForRowFromIDOfType(channel_id, RenderType::DM); if (!parent_iter) parent_iter = GetIteratorForRowFromIDOfType(channel_id, RenderType::DM);
if (!parent_iter) return; if (!parent_iter) return;
const auto user = Abaddon::Get().GetDiscordClient().GetUser(user_id); const auto user = Abaddon::Get().GetDiscordClient().GetUser(user_id);
@ -914,7 +932,7 @@ Gtk::TreeModel::iterator ChannelListTree::AddGuild(const GuildData &guild, const
for (const auto &channel_ : *guild.Channels) { for (const auto &channel_ : *guild.Channels) {
const auto channel = discord.GetChannel(channel_.ID); const auto channel = discord.GetChannel(channel_.ID);
if (!channel.has_value()) continue; if (!channel.has_value()) continue;
if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS || channel->Type == ChannelType::GUILD_VOICE) { if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS || channel->Type == ChannelType::GUILD_VOICE || channel->Type == ChannelType::GUILD_STAGE_VOICE) {
if (channel->ParentID.has_value()) if (channel->ParentID.has_value())
categories[*channel->ParentID].push_back(*channel); categories[*channel->ParentID].push_back(*channel);
else else
@ -954,6 +972,10 @@ Gtk::TreeModel::iterator ChannelListTree::AddGuild(const GuildData &guild, const
if (IsTextChannel(channel.Type)) { if (IsTextChannel(channel.Type)) {
channel_row[m_columns.m_type] = RenderType::TextChannel; channel_row[m_columns.m_type] = RenderType::TextChannel;
channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name);
} else if (channel.Type == ChannelType::GUILD_STAGE_VOICE) {
channel_row[m_columns.m_type] = RenderType::VoiceStage;
channel_row[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name);
add_voice_participants(channel, channel_row->children());
} else { } else {
channel_row[m_columns.m_type] = RenderType::VoiceChannel; channel_row[m_columns.m_type] = RenderType::VoiceChannel;
channel_row[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name); channel_row[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name);
@ -983,6 +1005,10 @@ Gtk::TreeModel::iterator ChannelListTree::AddGuild(const GuildData &guild, const
if (IsTextChannel(channel.Type)) { if (IsTextChannel(channel.Type)) {
channel_row[m_columns.m_type] = RenderType::TextChannel; channel_row[m_columns.m_type] = RenderType::TextChannel;
channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name);
} else if (channel.Type == ChannelType::GUILD_STAGE_VOICE) {
channel_row[m_columns.m_type] = RenderType::VoiceStage;
channel_row[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name);
add_voice_participants(channel, channel_row->children());
} else { } else {
channel_row[m_columns.m_type] = RenderType::VoiceChannel; channel_row[m_columns.m_type] = RenderType::VoiceChannel;
channel_row[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name); channel_row[m_columns.m_name] = Glib::Markup::escape_text(*channel.Name);
@ -1033,7 +1059,7 @@ Gtk::TreeModel::iterator ChannelListTree::CreateVoiceParticipantRow(const UserDa
const auto voice_state = Abaddon::Get().GetDiscordClient().GetVoiceState(user.ID); const auto voice_state = Abaddon::Get().GetDiscordClient().GetVoiceState(user.ID);
if (voice_state.has_value()) { if (voice_state.has_value()) {
row[m_columns.m_voice_flags] = voice_state->second; row[m_columns.m_voice_flags] = voice_state->second.Flags;
} }
auto &img = Abaddon::Get().GetImageManager(); auto &img = Abaddon::Get().GetImageManager();
@ -1331,6 +1357,10 @@ bool ChannelListTree::OnButtonPressEvent(GdkEventButton *ev) {
OnVoiceChannelSubmenuPopup(); OnVoiceChannelSubmenuPopup();
m_menu_voice_channel.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev)); m_menu_voice_channel.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
break; break;
case RenderType::VoiceStage:
OnVoiceStageSubmenuPopup();
m_menu_voice_stage.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
break;
case RenderType::DM: { case RenderType::DM: {
OnDMSubmenuPopup(); OnDMSubmenuPopup();
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(static_cast<Snowflake>(row[m_columns.m_id])); const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(static_cast<Snowflake>(row[m_columns.m_id]));
@ -1442,6 +1472,25 @@ void ChannelListTree::OnVoiceChannelSubmenuPopup() {
#endif #endif
} }
void ChannelListTree::OnVoiceStageSubmenuPopup() {
#ifdef WITH_VOICE
const auto iter = m_model->get_iter(m_path_for_menu);
if (!iter) return;
const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]);
auto &discord = Abaddon::Get().GetDiscordClient();
if (discord.IsVoiceConnected() || discord.IsVoiceConnecting()) {
m_menu_voice_stage_join.set_sensitive(false);
m_menu_voice_stage_disconnect.set_sensitive(discord.GetVoiceChannelID() == id);
} else {
m_menu_voice_stage_join.set_sensitive(true);
m_menu_voice_stage_disconnect.set_sensitive(false);
}
#else
m_menu_voice_stage_join.set_sensitive(false);
m_menu_voice_stage_disconnect.set_sensitive(false);
#endif
}
void ChannelListTree::OnDMSubmenuPopup() { void ChannelListTree::OnDMSubmenuPopup() {
auto iter = m_model->get_iter(m_path_for_menu); auto iter = m_model->get_iter(m_path_for_menu);
if (!iter) return; if (!iter) return;

View File

@ -162,6 +162,10 @@ protected:
Gtk::Menu m_menu_voice_channel; Gtk::Menu m_menu_voice_channel;
Gtk::MenuItem m_menu_voice_channel_join; Gtk::MenuItem m_menu_voice_channel_join;
Gtk::MenuItem m_menu_voice_channel_disconnect; Gtk::MenuItem m_menu_voice_channel_disconnect;
Gtk::Menu m_menu_voice_stage;
Gtk::MenuItem m_menu_voice_stage_join;
Gtk::MenuItem m_menu_voice_stage_disconnect;
Gtk::MenuItem m_menu_voice_channel_mark_as_read; Gtk::MenuItem m_menu_voice_channel_mark_as_read;
Gtk::MenuItem m_menu_voice_open_chat; Gtk::MenuItem m_menu_voice_open_chat;
@ -192,6 +196,7 @@ protected:
void OnDMSubmenuPopup(); void OnDMSubmenuPopup();
void OnThreadSubmenuPopup(); void OnThreadSubmenuPopup();
void OnVoiceChannelSubmenuPopup(); void OnVoiceChannelSubmenuPopup();
void OnVoiceStageSubmenuPopup();
bool m_updating_listing = false; bool m_updating_listing = false;

View File

@ -27,22 +27,6 @@ enum class ChannelType : int {
GUILD_MEDIA = 16, GUILD_MEDIA = 16,
}; };
enum class StagePrivacy {
PUBLIC = 1,
GUILD_ONLY = 2,
};
constexpr const char *GetStagePrivacyDisplayString(StagePrivacy e) {
switch (e) {
case StagePrivacy::PUBLIC:
return "Public";
case StagePrivacy::GUILD_ONLY:
return "Guild Only";
default:
return "Unknown";
}
}
// should be moved somewhere? // should be moved somewhere?
struct ThreadMetadataData { struct ThreadMetadataData {

View File

@ -360,6 +360,14 @@ std::optional<WebhookMessageData> DiscordClient::GetWebhookMessageData(Snowflake
return m_store.GetWebhookMessage(message_id); return m_store.GetWebhookMessage(message_id);
} }
std::optional<StageInstance> DiscordClient::GetStageInstanceFromChannel(Snowflake channel_id) const {
const auto iter1 = m_channel_to_stage_instance.find(channel_id);
if (iter1 == m_channel_to_stage_instance.end()) return {};
const auto iter2 = m_stage_instances.find(iter1->second);
if (iter2 == m_stage_instances.end()) return {};
return iter2->second;
}
bool DiscordClient::IsThreadJoined(Snowflake thread_id) const { bool DiscordClient::IsThreadJoined(Snowflake thread_id) const {
return std::find(m_joined_threads.begin(), m_joined_threads.end(), thread_id) != m_joined_threads.end(); return std::find(m_joined_threads.begin(), m_joined_threads.end(), thread_id) != m_joined_threads.end();
} }
@ -462,6 +470,10 @@ bool DiscordClient::CanManageMember(Snowflake guild_id, Snowflake actor, Snowfla
return actor_highest->Position > target_highest->Position; return actor_highest->Position > target_highest->Position;
} }
bool DiscordClient::IsStageModerator(Snowflake user_id, Snowflake channel_id) const {
return HasChannelPermission(user_id, channel_id, Permission::MANAGE_CHANNELS | Permission::MOVE_MEMBERS | Permission::MUTE_MEMBERS);
}
void DiscordClient::ChatMessageCallback(const std::string &nonce, const http::response_type &response, const sigc::slot<void(DiscordError)> &callback) { void DiscordClient::ChatMessageCallback(const std::string &nonce, const http::response_type &response, const sigc::slot<void(DiscordError)> &callback) {
if (!CheckCode(response)) { if (!CheckCode(response)) {
if (response.status_code == http::TooManyRequests) { if (response.status_code == http::TooManyRequests) {
@ -1288,6 +1300,78 @@ std::optional<uint32_t> DiscordClient::GetSSRCOfUser(Snowflake id) const {
return m_voice.GetSSRCOfUser(id); return m_voice.GetSSRCOfUser(id);
} }
bool DiscordClient::IsUserSpeaker(Snowflake user_id) const {
const auto state = GetVoiceState(user_id);
return state.has_value() && state->second.IsSpeaker();
}
bool DiscordClient::HasUserRequestedToSpeak(Snowflake user_id) const {
const auto state = GetVoiceState(user_id);
return state.has_value() && state->second.RequestToSpeakTimestamp.has_value() && util::FlagSet(state->second.Flags, VoiceStateFlags::Suppressed);
}
bool DiscordClient::IsUserInvitedToSpeak(Snowflake user_id) const {
const auto state = GetVoiceState(user_id);
return state.has_value() && state->second.RequestToSpeakTimestamp.has_value() && !util::FlagSet(state->second.Flags, VoiceStateFlags::Suppressed);
}
void DiscordClient::RequestToSpeak(Snowflake channel_id, bool want, const sigc::slot<void(DiscordError code)> &callback) {
if (want && !HasSelfChannelPermission(channel_id, Permission::REQUEST_TO_SPEAK)) return;
const auto channel = GetChannel(channel_id);
if (!channel.has_value() || !channel->GuildID.has_value()) return;
ModifyCurrentUserVoiceStateObject d;
d.ChannelID = channel_id;
if (want) {
d.RequestToSpeakTimestamp = Glib::DateTime::create_now_utc().format_iso8601();
} else {
d.RequestToSpeakTimestamp = "";
}
m_http.MakePATCH("/guilds/" + std::to_string(*channel->GuildID) + "/voice-states/@me", nlohmann::json(d).dump(), [callback](const http::response_type &response) {
if (CheckCode(response, 204)) {
callback(DiscordError::NONE);
} else {
callback(GetCodeFromResponse(response));
}
});
}
void DiscordClient::SetStageSpeaking(Snowflake channel_id, bool want, const sigc::slot<void(DiscordError code)> &callback) {
const auto channel = GetChannel(channel_id);
if (!channel.has_value() || !channel->GuildID.has_value()) return;
ModifyCurrentUserVoiceStateObject d;
d.ChannelID = channel_id;
d.Suppress = !want;
if (want) {
d.RequestToSpeakTimestamp = "";
}
m_http.MakePATCH("/guilds/" + std::to_string(*channel->GuildID) + "/voice-states/@me", nlohmann::json(d).dump(), [callback](const http::response_type &response) {
if (CheckCode(response, 204)) {
callback(DiscordError::NONE);
} else {
callback(GetCodeFromResponse(response));
}
});
}
void DiscordClient::DeclineInviteToSpeak(Snowflake channel_id, const sigc::slot<void(DiscordError code)> &callback) {
const auto channel = GetChannel(channel_id);
if (!channel.has_value() || !channel->GuildID.has_value()) return;
ModifyCurrentUserVoiceStateObject d;
d.ChannelID = channel_id;
d.Suppress = true;
d.RequestToSpeakTimestamp = "";
m_http.MakePATCH("/guilds/" + std::to_string(*channel->GuildID) + "/voice-states/@me", nlohmann::json(d).dump(), [callback](const http::response_type &response) {
if (CheckCode(response, 204)) {
callback(DiscordError::NONE);
} else {
callback(GetCodeFromResponse(response));
}
});
}
DiscordVoiceClient &DiscordClient::GetVoiceClient() { DiscordVoiceClient &DiscordClient::GetVoiceClient() {
return m_voice; return m_voice;
} }
@ -1303,7 +1387,7 @@ void DiscordClient::SetVoiceDeafened(bool is_deaf) {
} }
#endif #endif
std::optional<std::pair<Snowflake, VoiceStateFlags>> DiscordClient::GetVoiceState(Snowflake user_id) const { std::optional<std::pair<Snowflake, PackedVoiceState>> DiscordClient::GetVoiceState(Snowflake user_id) const {
if (const auto it = m_voice_states.find(user_id); it != m_voice_states.end()) { if (const auto it = m_voice_states.find(user_id); it != m_voice_states.end()) {
return it->second; return it->second;
} }
@ -1652,6 +1736,15 @@ void DiscordClient::HandleGatewayMessage(std::string str) {
case GatewayEvent::GUILD_MEMBERS_CHUNK: { case GatewayEvent::GUILD_MEMBERS_CHUNK: {
HandleGatewayGuildMembersChunk(m); HandleGatewayGuildMembersChunk(m);
} break; } break;
case GatewayEvent::STAGE_INSTANCE_CREATE: {
HandleGatewayStageInstanceCreate(m);
} break;
case GatewayEvent::STAGE_INSTANCE_UPDATE: {
HandleGatewayStageInstanceUpdate(m);
} break;
case GatewayEvent::STAGE_INSTANCE_DELETE: {
HandleGatewayStageInstanceDelete(m);
} break;
case GatewayEvent::VOICE_STATE_UPDATE: { case GatewayEvent::VOICE_STATE_UPDATE: {
HandleGatewayVoiceStateUpdate(m); HandleGatewayVoiceStateUpdate(m);
} break; } break;
@ -1712,6 +1805,14 @@ void DiscordClient::ProcessNewGuild(GuildData &guild) {
return; return;
} }
if (guild.StageInstances.has_value()) {
for (const auto &stage : *guild.StageInstances) {
spdlog::get("discord")->debug("storing stage {} in channel {}", stage.ID, stage.ChannelID);
m_stage_instances[stage.ID] = stage;
m_channel_to_stage_instance[stage.ChannelID] = stage.ID;
}
}
m_store.BeginTransaction(); m_store.BeginTransaction();
m_store.SetGuild(guild.ID, guild); m_store.SetGuild(guild.ID, guild);
@ -2302,6 +2403,29 @@ void DiscordClient::HandleGatewayGuildMembersChunk(const GatewayMessage &msg) {
m_store.EndTransaction(); m_store.EndTransaction();
} }
void DiscordClient::HandleGatewayStageInstanceCreate(const GatewayMessage &msg) {
StageInstance data = msg.Data;
spdlog::get("discord")->debug("STAGE_INSTANCE_CREATE: {} in {}", data.ID, data.ChannelID);
m_stage_instances[data.ID] = data;
m_channel_to_stage_instance[data.ChannelID] = data.ID;
m_signal_stage_instance_create.emit(data);
}
void DiscordClient::HandleGatewayStageInstanceUpdate(const GatewayMessage &msg) {
StageInstance data = msg.Data;
spdlog::get("discord")->debug("STAGE_INSTANCE_UPDATE: {} in {}", data.ID, data.ChannelID);
m_stage_instances[data.ID] = data;
m_signal_stage_instance_update.emit(data);
}
void DiscordClient::HandleGatewayStageInstanceDelete(const GatewayMessage &msg) {
StageInstance data = msg.Data;
spdlog::get("discord")->debug("STAGE_INSTANCE_DELETE: {} in {}", data.ID, data.ChannelID);
m_stage_instances.erase(data.ID);
m_channel_to_stage_instance.erase(data.ChannelID);
m_signal_stage_instance_delete.emit(data);
}
#ifdef WITH_VOICE #ifdef WITH_VOICE
/* /*
@ -2399,9 +2523,14 @@ void DiscordClient::CheckVoiceState(const VoiceState &data) {
if (data.ChannelID.has_value()) { if (data.ChannelID.has_value()) {
const auto old_state = GetVoiceState(data.UserID); const auto old_state = GetVoiceState(data.UserID);
SetVoiceState(data.UserID, data); SetVoiceState(data.UserID, data);
if (old_state.has_value() && old_state->first != *data.ChannelID) { const auto new_state = GetVoiceState(data.UserID);
if (old_state.has_value()) {
if (old_state->first != *data.ChannelID) {
m_signal_voice_user_disconnect.emit(data.UserID, old_state->first); m_signal_voice_user_disconnect.emit(data.UserID, old_state->first);
m_signal_voice_user_connect.emit(data.UserID, *data.ChannelID); m_signal_voice_user_connect.emit(data.UserID, *data.ChannelID);
} else if (old_state->second.IsSpeaker() != new_state.value().second.IsSpeaker()) {
m_signal_voice_speaker_state_changed.emit(*data.ChannelID, data.UserID, new_state->second.IsSpeaker());
}
} else if (!old_state.has_value()) { } else if (!old_state.has_value()) {
m_signal_voice_user_connect.emit(data.UserID, *data.ChannelID); m_signal_voice_user_connect.emit(data.UserID, *data.ChannelID);
} }
@ -2954,8 +3083,9 @@ void DiscordClient::SetVoiceState(Snowflake user_id, const VoiceState &state) {
if (state.IsDeafened) flags |= VoiceStateFlags::Deaf; if (state.IsDeafened) flags |= VoiceStateFlags::Deaf;
if (state.IsSelfStream) flags |= VoiceStateFlags::SelfStream; if (state.IsSelfStream) flags |= VoiceStateFlags::SelfStream;
if (state.IsSelfVideo) flags |= VoiceStateFlags::SelfVideo; if (state.IsSelfVideo) flags |= VoiceStateFlags::SelfVideo;
if (state.IsSuppressed) flags |= VoiceStateFlags::Suppressed;
m_voice_states[user_id] = std::make_pair(*state.ChannelID, flags); m_voice_states[user_id] = std::make_pair(*state.ChannelID, PackedVoiceState { flags, state.RequestToSpeakTimestamp });
m_voice_state_channel_users[*state.ChannelID].insert(user_id); m_voice_state_channel_users[*state.ChannelID].insert(user_id);
m_signal_voice_state_set.emit(user_id, *state.ChannelID, flags); m_signal_voice_state_set.emit(user_id, *state.ChannelID, flags);
@ -3018,6 +3148,9 @@ void DiscordClient::LoadEventMap() {
m_event_map["VOICE_STATE_UPDATE"] = GatewayEvent::VOICE_STATE_UPDATE; m_event_map["VOICE_STATE_UPDATE"] = GatewayEvent::VOICE_STATE_UPDATE;
m_event_map["VOICE_SERVER_UPDATE"] = GatewayEvent::VOICE_SERVER_UPDATE; m_event_map["VOICE_SERVER_UPDATE"] = GatewayEvent::VOICE_SERVER_UPDATE;
m_event_map["CALL_CREATE"] = GatewayEvent::CALL_CREATE; m_event_map["CALL_CREATE"] = GatewayEvent::CALL_CREATE;
m_event_map["STAGE_INSTANCE_CREATE"] = GatewayEvent::STAGE_INSTANCE_CREATE;
m_event_map["STAGE_INSTANCE_UPDATE"] = GatewayEvent::STAGE_INSTANCE_UPDATE;
m_event_map["STAGE_INSTANCE_DELETE"] = GatewayEvent::STAGE_INSTANCE_DELETE;
} }
DiscordClient::type_signal_gateway_ready DiscordClient::signal_gateway_ready() { DiscordClient::type_signal_gateway_ready DiscordClient::signal_gateway_ready() {
@ -3196,6 +3329,18 @@ DiscordClient::type_signal_guild_members_chunk DiscordClient::signal_guild_membe
return m_signal_guild_members_chunk; return m_signal_guild_members_chunk;
} }
DiscordClient::type_signal_stage_instance_create DiscordClient::signal_stage_instance_create() {
return m_signal_stage_instance_create;
}
DiscordClient::type_signal_stage_instance_update DiscordClient::signal_stage_instance_update() {
return m_signal_stage_instance_update;
}
DiscordClient::type_signal_stage_instance_delete DiscordClient::signal_stage_instance_delete() {
return m_signal_stage_instance_delete;
}
DiscordClient::type_signal_added_to_thread DiscordClient::signal_added_to_thread() { DiscordClient::type_signal_added_to_thread DiscordClient::signal_added_to_thread() {
return m_signal_added_to_thread; return m_signal_added_to_thread;
} }
@ -3273,3 +3418,7 @@ DiscordClient::type_signal_voice_user_connect DiscordClient::signal_voice_user_c
DiscordClient::type_signal_voice_state_set DiscordClient::signal_voice_state_set() { DiscordClient::type_signal_voice_state_set DiscordClient::signal_voice_state_set() {
return m_signal_voice_state_set; return m_signal_voice_state_set;
} }
DiscordClient::type_signal_voice_speaker_state_changed DiscordClient::signal_voice_speaker_state_changed() {
return m_signal_voice_speaker_state_changed;
}

View File

@ -5,7 +5,7 @@
#include "objects.hpp" #include "objects.hpp"
#include "store.hpp" #include "store.hpp"
#include "voiceclient.hpp" #include "voiceclient.hpp"
#include "voicestateflags.hpp" #include "voicestate.hpp"
#include "websocket.hpp" #include "websocket.hpp"
#include <gdkmm/rgba.h> #include <gdkmm/rgba.h>
#include <sigc++/sigc++.h> #include <sigc++/sigc++.h>
@ -65,6 +65,7 @@ public:
void GetArchivedPrivateThreads(Snowflake channel_id, const sigc::slot<void(DiscordError, const ArchivedThreadsResponseData &)> &callback); void GetArchivedPrivateThreads(Snowflake channel_id, const sigc::slot<void(DiscordError, const ArchivedThreadsResponseData &)> &callback);
std::vector<Snowflake> GetChildChannelIDs(Snowflake parent_id) const; std::vector<Snowflake> GetChildChannelIDs(Snowflake parent_id) const;
std::optional<WebhookMessageData> GetWebhookMessageData(Snowflake message_id) const; std::optional<WebhookMessageData> GetWebhookMessageData(Snowflake message_id) const;
std::optional<StageInstance> GetStageInstanceFromChannel(Snowflake channel_id) const;
// get ids of given list of members for who we do not have the member data // get ids of given list of members for who we do not have the member data
template<typename Iter> template<typename Iter>
@ -86,6 +87,7 @@ public:
Permission ComputePermissions(Snowflake member_id, Snowflake guild_id) const; Permission ComputePermissions(Snowflake member_id, Snowflake guild_id) const;
Permission ComputeOverwrites(Permission base, Snowflake member_id, Snowflake channel_id) const; Permission ComputeOverwrites(Permission base, Snowflake member_id, Snowflake channel_id) const;
bool CanManageMember(Snowflake guild_id, Snowflake actor, Snowflake target) const; // kick, ban, edit nickname (cant think of a better name) bool CanManageMember(Snowflake guild_id, Snowflake actor, Snowflake target) const; // kick, ban, edit nickname (cant think of a better name)
bool IsStageModerator(Snowflake user_id, Snowflake channel_id) const;
void ChatMessageCallback(const std::string &nonce, const http::response_type &response, const sigc::slot<void(DiscordError code)> &callback); void ChatMessageCallback(const std::string &nonce, const http::response_type &response, const sigc::slot<void(DiscordError code)> &callback);
void SendChatMessageNoAttachments(const ChatSubmitParams &params, const sigc::slot<void(DiscordError code)> &callback); void SendChatMessageNoAttachments(const ChatSubmitParams &params, const sigc::slot<void(DiscordError code)> &callback);
@ -201,6 +203,13 @@ public:
[[nodiscard]] bool IsVoiceConnecting() const noexcept; [[nodiscard]] bool IsVoiceConnecting() const noexcept;
[[nodiscard]] Snowflake GetVoiceChannelID() const noexcept; [[nodiscard]] Snowflake GetVoiceChannelID() const noexcept;
[[nodiscard]] std::optional<uint32_t> GetSSRCOfUser(Snowflake id) const; [[nodiscard]] std::optional<uint32_t> GetSSRCOfUser(Snowflake id) const;
[[nodiscard]] bool IsUserSpeaker(Snowflake user_id) const;
[[nodiscard]] bool HasUserRequestedToSpeak(Snowflake user_id) const;
[[nodiscard]] bool IsUserInvitedToSpeak(Snowflake user_id) const;
void RequestToSpeak(Snowflake channel_id, bool want, const sigc::slot<void(DiscordError code)> &callback);
void SetStageSpeaking(Snowflake channel_id, bool want, const sigc::slot<void(DiscordError code)> &callback);
void DeclineInviteToSpeak(Snowflake channel_id, const sigc::slot<void(DiscordError code)> &callback);
DiscordVoiceClient &GetVoiceClient(); DiscordVoiceClient &GetVoiceClient();
@ -208,7 +217,7 @@ public:
void SetVoiceDeafened(bool is_deaf); void SetVoiceDeafened(bool is_deaf);
#endif #endif
[[nodiscard]] std::optional<std::pair<Snowflake, VoiceStateFlags>> GetVoiceState(Snowflake user_id) const; [[nodiscard]] std::optional<std::pair<Snowflake, PackedVoiceState>> GetVoiceState(Snowflake user_id) const;
[[nodiscard]] std::unordered_set<Snowflake> GetUsersInVoiceChannel(Snowflake channel_id); [[nodiscard]] std::unordered_set<Snowflake> GetUsersInVoiceChannel(Snowflake channel_id);
void SetReferringChannel(Snowflake id); void SetReferringChannel(Snowflake id);
@ -295,6 +304,9 @@ private:
void HandleGatewayMessageAck(const GatewayMessage &msg); void HandleGatewayMessageAck(const GatewayMessage &msg);
void HandleGatewayUserGuildSettingsUpdate(const GatewayMessage &msg); void HandleGatewayUserGuildSettingsUpdate(const GatewayMessage &msg);
void HandleGatewayGuildMembersChunk(const GatewayMessage &msg); void HandleGatewayGuildMembersChunk(const GatewayMessage &msg);
void HandleGatewayStageInstanceCreate(const GatewayMessage &msg);
void HandleGatewayStageInstanceUpdate(const GatewayMessage &msg);
void HandleGatewayStageInstanceDelete(const GatewayMessage &msg);
void HandleGatewayReadySupplemental(const GatewayMessage &msg); void HandleGatewayReadySupplemental(const GatewayMessage &msg);
void HandleGatewayReconnect(const GatewayMessage &msg); void HandleGatewayReconnect(const GatewayMessage &msg);
void HandleGatewayInvalidSession(const GatewayMessage &msg); void HandleGatewayInvalidSession(const GatewayMessage &msg);
@ -345,6 +357,8 @@ private:
std::unordered_set<Snowflake> m_muted_channels; std::unordered_set<Snowflake> m_muted_channels;
std::unordered_map<Snowflake, int> m_unread; std::unordered_map<Snowflake, int> m_unread;
std::unordered_set<Snowflake> m_channel_muted_parent; std::unordered_set<Snowflake> m_channel_muted_parent;
std::map<Snowflake, StageInstance> m_stage_instances;
std::map<Snowflake, Snowflake> m_channel_to_stage_instance;
UserData m_user_data; UserData m_user_data;
UserSettings m_user_settings; UserSettings m_user_settings;
@ -388,7 +402,7 @@ private:
void ClearVoiceState(Snowflake user_id); void ClearVoiceState(Snowflake user_id);
// todo sql i guess // todo sql i guess
std::unordered_map<Snowflake, std::pair<Snowflake, VoiceStateFlags>> m_voice_states; std::unordered_map<Snowflake, std::pair<Snowflake, PackedVoiceState>> m_voice_states;
std::unordered_map<Snowflake, std::unordered_set<Snowflake>> m_voice_state_channel_users; std::unordered_map<Snowflake, std::unordered_set<Snowflake>> m_voice_state_channel_users;
mutable std::mutex m_msg_mutex; mutable std::mutex m_msg_mutex;
@ -446,6 +460,9 @@ public:
typedef sigc::signal<void, ThreadMemberListUpdateData> type_signal_thread_member_list_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, MessageAckData> type_signal_message_ack;
typedef sigc::signal<void, GuildMembersChunkData> type_signal_guild_members_chunk; typedef sigc::signal<void, GuildMembersChunkData> type_signal_guild_members_chunk;
typedef sigc::signal<void, StageInstance> type_signal_stage_instance_create;
typedef sigc::signal<void, StageInstance> type_signal_stage_instance_update;
typedef sigc::signal<void, StageInstance> type_signal_stage_instance_delete;
// not discord dispatch events // not discord dispatch events
typedef sigc::signal<void, Snowflake> type_signal_added_to_thread; typedef sigc::signal<void, Snowflake> type_signal_added_to_thread;
@ -477,6 +494,7 @@ public:
using type_signal_voice_user_disconnect = sigc::signal<void(Snowflake, Snowflake)>; using type_signal_voice_user_disconnect = sigc::signal<void(Snowflake, Snowflake)>;
using type_signal_voice_user_connect = sigc::signal<void(Snowflake, Snowflake)>; using type_signal_voice_user_connect = sigc::signal<void(Snowflake, Snowflake)>;
using type_signal_voice_state_set = sigc::signal<void(Snowflake, Snowflake, VoiceStateFlags)>; using type_signal_voice_state_set = sigc::signal<void(Snowflake, Snowflake, VoiceStateFlags)>;
using type_signal_voice_speaker_state_changed = sigc::signal<void(Snowflake /* channel_id */, Snowflake /* user_id */, bool /* is_speaker */)>;
type_signal_gateway_ready signal_gateway_ready(); type_signal_gateway_ready signal_gateway_ready();
type_signal_gateway_ready_supplemental signal_gateway_ready_supplemental(); type_signal_gateway_ready_supplemental signal_gateway_ready_supplemental();
@ -519,6 +537,9 @@ public:
type_signal_thread_member_list_update signal_thread_member_list_update(); type_signal_thread_member_list_update signal_thread_member_list_update();
type_signal_message_ack signal_message_ack(); type_signal_message_ack signal_message_ack();
type_signal_guild_members_chunk signal_guild_members_chunk(); type_signal_guild_members_chunk signal_guild_members_chunk();
type_signal_stage_instance_create signal_stage_instance_create();
type_signal_stage_instance_update signal_stage_instance_update();
type_signal_stage_instance_delete signal_stage_instance_delete();
type_signal_added_to_thread signal_added_to_thread(); type_signal_added_to_thread signal_added_to_thread();
type_signal_removed_from_thread signal_removed_from_thread(); type_signal_removed_from_thread signal_removed_from_thread();
@ -546,6 +567,7 @@ public:
type_signal_voice_user_disconnect signal_voice_user_disconnect(); type_signal_voice_user_disconnect signal_voice_user_disconnect();
type_signal_voice_user_connect signal_voice_user_connect(); type_signal_voice_user_connect signal_voice_user_connect();
type_signal_voice_state_set signal_voice_state_set(); type_signal_voice_state_set signal_voice_state_set();
type_signal_voice_speaker_state_changed signal_voice_speaker_state_changed();
protected: protected:
type_signal_gateway_ready m_signal_gateway_ready; type_signal_gateway_ready m_signal_gateway_ready;
@ -589,6 +611,9 @@ protected:
type_signal_thread_member_list_update m_signal_thread_member_list_update; type_signal_thread_member_list_update m_signal_thread_member_list_update;
type_signal_message_ack m_signal_message_ack; type_signal_message_ack m_signal_message_ack;
type_signal_guild_members_chunk m_signal_guild_members_chunk; type_signal_guild_members_chunk m_signal_guild_members_chunk;
type_signal_stage_instance_create m_signal_stage_instance_create;
type_signal_stage_instance_update m_signal_stage_instance_update;
type_signal_stage_instance_delete m_signal_stage_instance_delete;
type_signal_removed_from_thread m_signal_removed_from_thread; type_signal_removed_from_thread m_signal_removed_from_thread;
type_signal_added_to_thread m_signal_added_to_thread; type_signal_added_to_thread m_signal_added_to_thread;
@ -616,4 +641,5 @@ protected:
type_signal_voice_user_disconnect m_signal_voice_user_disconnect; type_signal_voice_user_disconnect m_signal_voice_user_disconnect;
type_signal_voice_user_connect m_signal_voice_user_connect; type_signal_voice_user_connect m_signal_voice_user_connect;
type_signal_voice_state_set m_signal_voice_state_set; type_signal_voice_state_set m_signal_voice_state_set;
type_signal_voice_speaker_state_changed m_signal_voice_speaker_state_changed;
}; };

View File

@ -54,6 +54,7 @@ void from_json(const nlohmann::json &j, GuildData &m) {
JS_O("preferred_locale", m.PreferredLocale); JS_O("preferred_locale", m.PreferredLocale);
JS_ON("public_updates_channel_id", m.PublicUpdatesChannelID); JS_ON("public_updates_channel_id", m.PublicUpdatesChannelID);
JS_O("max_video_channel_users", m.MaxVideoChannelUsers); JS_O("max_video_channel_users", m.MaxVideoChannelUsers);
JS_ON("stage_instances", m.StageInstances);
JS_O("approximate_member_count", tmp); JS_O("approximate_member_count", tmp);
if (tmp.has_value()) if (tmp.has_value())
m.ApproximateMemberCount = std::stol(*tmp); m.ApproximateMemberCount = std::stol(*tmp);

View File

@ -4,6 +4,7 @@
#include "role.hpp" #include "role.hpp"
#include "channel.hpp" #include "channel.hpp"
#include "emoji.hpp" #include "emoji.hpp"
#include "stage.hpp"
#include <vector> #include <vector>
#include <string> #include <string>
#include <unordered_set> #include <unordered_set>
@ -90,6 +91,7 @@ struct GuildData {
std::optional<int> ApproximateMemberCount; std::optional<int> ApproximateMemberCount;
std::optional<int> ApproximatePresenceCount; std::optional<int> ApproximatePresenceCount;
std::optional<std::vector<ChannelData>> Threads; // only with permissions to view, id only std::optional<std::vector<ChannelData>> Threads; // only with permissions to view, id only
std::optional<std::vector<StageInstance>> StageInstances;
// undocumented // undocumented
// std::map<std::string, Unknown> GuildHashes; // std::map<std::string, Unknown> GuildHashes;

View File

@ -699,6 +699,18 @@ void from_json(const nlohmann::json &j, CallCreateData &m) {
JS_D("channel_id", m.ChannelID); JS_D("channel_id", m.ChannelID);
JS_ON("voice_states", m.VoiceStates); JS_ON("voice_states", m.VoiceStates);
} }
void to_json(nlohmann::json &j, const ModifyCurrentUserVoiceStateObject &m) {
JS_IF("channel_id", m.ChannelID);
JS_IF("suppress", m.Suppress);
if (m.RequestToSpeakTimestamp.has_value()) {
if (m.RequestToSpeakTimestamp->empty()) {
j["request_to_speak_timestamp"] = nullptr;
} else {
j["request_to_speak_timestamp"] = *m.RequestToSpeakTimestamp;
}
}
}
#endif #endif
void from_json(const nlohmann::json &j, VoiceState &m) { void from_json(const nlohmann::json &j, VoiceState &m) {
@ -714,4 +726,5 @@ void from_json(const nlohmann::json &j, VoiceState &m) {
JS_D("user_id", m.UserID); JS_D("user_id", m.UserID);
JS_ON("member", m.Member); JS_ON("member", m.Member);
JS_D("session_id", m.SessionID); JS_D("session_id", m.SessionID);
JS_ON("request_to_speak_timestamp", m.RequestToSpeakTimestamp);
} }

View File

@ -20,6 +20,7 @@
#include "auditlog.hpp" #include "auditlog.hpp"
#include "relationship.hpp" #include "relationship.hpp"
#include "errors.hpp" #include "errors.hpp"
#include "stage.hpp"
// most stuff below should just be objects that get processed and thrown away immediately // most stuff below should just be objects that get processed and thrown away immediately
@ -110,6 +111,9 @@ enum class GatewayEvent : int {
VOICE_STATE_UPDATE, VOICE_STATE_UPDATE,
VOICE_SERVER_UPDATE, VOICE_SERVER_UPDATE,
CALL_CREATE, CALL_CREATE,
STAGE_INSTANCE_CREATE,
STAGE_INSTANCE_UPDATE,
STAGE_INSTANCE_DELETE,
}; };
enum class GatewayCloseCode : uint16_t { enum class GatewayCloseCode : uint16_t {
@ -917,6 +921,7 @@ struct VoiceState {
std::string SessionID; std::string SessionID;
bool IsSuppressed; bool IsSuppressed;
Snowflake UserID; Snowflake UserID;
std::optional<std::string> RequestToSpeakTimestamp;
friend void from_json(const nlohmann::json &j, VoiceState &m); friend void from_json(const nlohmann::json &j, VoiceState &m);
}; };
@ -952,4 +957,12 @@ struct CallCreateData {
friend void from_json(const nlohmann::json &j, CallCreateData &m); friend void from_json(const nlohmann::json &j, CallCreateData &m);
}; };
struct ModifyCurrentUserVoiceStateObject {
std::optional<Snowflake> ChannelID;
std::optional<bool> Suppress;
std::optional<std::string> RequestToSpeakTimestamp;
friend void to_json(nlohmann::json &j, const ModifyCurrentUserVoiceStateObject &m);
};
#endif #endif

12
src/discord/stage.cpp Normal file
View File

@ -0,0 +1,12 @@
#include "stage.hpp"
#include "json.hpp"
void from_json(const nlohmann::json &j, StageInstance &m) {
JS_D("id", m.ID);
JS_D("guild_id", m.GuildID);
JS_D("channel_id", m.ChannelID);
JS_N("topic", m.Topic);
JS_N("privacy_level", m.PrivacyLevel);
JS_N("guild_scheduled_event_id", m.GuildScheduledEventID);
}

32
src/discord/stage.hpp Normal file
View File

@ -0,0 +1,32 @@
#pragma once
#include <nlohmann/json.hpp>
#include "snowflake.hpp"
enum class StagePrivacy {
PUBLIC = 1,
GUILD_ONLY = 2,
};
constexpr const char *GetStagePrivacyDisplayString(StagePrivacy e) {
switch (e) {
case StagePrivacy::PUBLIC:
return "Public";
case StagePrivacy::GUILD_ONLY:
return "Guild Only";
default:
return "Unknown";
}
}
struct StageInstance {
Snowflake ID;
Snowflake GuildID;
Snowflake ChannelID;
std::string Topic;
StagePrivacy PrivacyLevel;
Snowflake GuildScheduledEventID;
friend void from_json(const nlohmann::json &j, StageInstance &m);
};

View File

@ -250,6 +250,7 @@ bool DiscordVoiceClient::IsConnecting() const noexcept {
} }
void DiscordVoiceClient::OnGatewayMessage(const std::string &str) { void DiscordVoiceClient::OnGatewayMessage(const std::string &str) {
m_log->trace("IN: {}", str);
VoiceGatewayMessage msg = nlohmann::json::parse(str); VoiceGatewayMessage msg = nlohmann::json::parse(str);
switch (msg.Opcode) { switch (msg.Opcode) {
case VoiceGatewayOp::Hello: case VoiceGatewayOp::Hello:

View File

@ -43,6 +43,23 @@ enum class VoiceGatewayOp : int {
Hello = 8, Hello = 8,
Resumed = 9, Resumed = 9,
ClientDisconnect = 13, ClientDisconnect = 13,
SessionUpdate = 14,
MediaSinkWants = 15,
VoiceBackendVersion = 16,
ChannelOptionsUpdate = 17,
Flags = 18,
SpeedTest = 19,
Platform = 20,
SecureFramesPrepareProtocolTransition = 21,
SecureFramesExecuteTransition = 22,
SecureFramesReadyForTransition = 23,
SecureFramesPrepareEpoch = 24,
MlsExternalSenderPackage = 25,
MlsKeyPackage = 26,
MlsProposals = 27,
MlsCommitWelcome = 28,
MlsPrepareCommitTransition = 29,
MlsWelcome = 30,
}; };
struct VoiceGatewayMessage { struct VoiceGatewayMessage {
@ -156,11 +173,11 @@ public:
private: private:
void ReadThread(); void ReadThread();
#ifdef _WIN32 #ifdef _WIN32
SOCKET m_socket; SOCKET m_socket;
#else #else
int m_socket; int m_socket;
#endif #endif
sockaddr_in m_server; sockaddr_in m_server;
std::atomic<bool> m_running = false; std::atomic<bool> m_running = false;

View File

@ -0,0 +1,5 @@
#include "voicestate.hpp"
bool PackedVoiceState::IsSpeaker() const noexcept {
return ((Flags & VoiceStateFlags::Suppressed) != VoiceStateFlags::Suppressed) && !RequestToSpeakTimestamp.has_value();
}

View File

@ -1,7 +1,10 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <optional>
#include <string>
#include "misc/bitwise.hpp" #include "misc/bitwise.hpp"
// this is packed into a enum cuz it makes implementing tree models easier
enum class VoiceStateFlags : uint8_t { enum class VoiceStateFlags : uint8_t {
Clear = 0, Clear = 0,
Deaf = 1 << 0, Deaf = 1 << 0,
@ -10,6 +13,14 @@ enum class VoiceStateFlags : uint8_t {
SelfMute = 1 << 3, SelfMute = 1 << 3,
SelfStream = 1 << 4, SelfStream = 1 << 4,
SelfVideo = 1 << 5, SelfVideo = 1 << 5,
Suppressed = 1 << 6,
};
struct PackedVoiceState {
VoiceStateFlags Flags;
std::optional<std::string> RequestToSpeakTimestamp;
[[nodiscard]] bool IsSpeaker() const noexcept;
}; };
template<> template<>

View File

@ -1,6 +1,13 @@
#pragma once #pragma once
#include <type_traits> #include <type_traits>
namespace util {
template<typename T>
bool FlagSet(T flags, T value) {
return (flags & value) == value;
}
} // namespace util
template<typename T> template<typename T>
struct Bitwise { struct Bitwise {
static const bool enable = false; static const bool enable = false;

View File

@ -1,89 +1,19 @@
#include "util.hpp"
#ifdef WITH_VOICE #ifdef WITH_VOICE
// clang-format off // clang-format off
#include "voicewindow.hpp"
#include "abaddon.hpp" #include "abaddon.hpp"
#include "audio/manager.hpp" #include "audio/manager.hpp"
#include "components/lazyimage.hpp" #include "components/lazyimage.hpp"
#include "voicesettingswindow.hpp" #include "voicewindowaudiencelistentry.hpp"
#include "voicewindow.hpp" #include "voicewindowspeakerlistentry.hpp"
#include "windows/voicesettingswindow.hpp"
// clang-format on // clang-format on
class VoiceWindowUserListEntry : public Gtk::ListBoxRow {
public:
VoiceWindowUserListEntry(Snowflake id)
: m_main(Gtk::ORIENTATION_VERTICAL)
, m_horz(Gtk::ORIENTATION_HORIZONTAL)
, m_avatar(32, 32)
, m_mute("Mute") {
m_name.set_halign(Gtk::ALIGN_START);
m_name.set_hexpand(true);
m_mute.set_halign(Gtk::ALIGN_END);
m_volume.set_range(0.0, 200.0);
m_volume.set_value_pos(Gtk::POS_LEFT);
m_volume.set_value(100.0);
m_volume.signal_value_changed().connect([this]() {
m_signal_volume.emit(m_volume.get_value() * 0.01);
});
m_horz.add(m_avatar);
m_horz.add(m_name);
m_horz.add(m_mute);
m_main.add(m_horz);
m_main.add(m_volume);
m_main.add(m_meter);
add(m_main);
show_all_children();
auto &discord = Abaddon::Get().GetDiscordClient();
const auto user = discord.GetUser(id);
if (user.has_value()) {
m_name.set_text(user->GetUsername());
m_avatar.SetURL(user->GetAvatarURL("png", "32"));
} else {
m_name.set_text("Unknown user");
}
m_mute.signal_toggled().connect([this]() {
m_signal_mute_cs.emit(m_mute.get_active());
});
}
void SetVolumeMeter(double frac) {
m_meter.SetVolume(frac);
}
void RestoreGain(double frac) {
m_volume.set_value(frac * 100.0);
}
private:
Gtk::Box m_main;
Gtk::Box m_horz;
LazyImage m_avatar;
Gtk::Label m_name;
Gtk::CheckButton m_mute;
Gtk::Scale m_volume;
VolumeMeter m_meter;
public:
using type_signal_mute_cs = sigc::signal<void(bool)>;
using type_signal_volume = sigc::signal<void(double)>;
type_signal_mute_cs signal_mute_cs() {
return m_signal_mute_cs;
}
type_signal_volume signal_volume() {
return m_signal_volume;
}
private:
type_signal_mute_cs m_signal_mute_cs;
type_signal_volume m_signal_volume;
};
VoiceWindow::VoiceWindow(Snowflake channel_id) VoiceWindow::VoiceWindow(Snowflake channel_id)
: m_main(Gtk::ORIENTATION_VERTICAL) : m_main(Gtk::ORIENTATION_VERTICAL)
, m_controls(Gtk::ORIENTATION_HORIZONTAL) , m_controls(Gtk::ORIENTATION_HORIZONTAL)
@ -91,7 +21,11 @@ VoiceWindow::VoiceWindow(Snowflake channel_id)
, m_deafen("Deafen") , m_deafen("Deafen")
, m_noise_suppression("Suppress Noise") , m_noise_suppression("Suppress Noise")
, m_mix_mono("Mix Mono") , m_mix_mono("Mix Mono")
, m_stage_command("Request to Speak")
, m_disconnect("Disconnect") , m_disconnect("Disconnect")
, m_stage_invite_lbl("You've been invited to speak")
, m_stage_accept("Accept")
, m_stage_decline("Decline")
, m_channel_id(channel_id) , m_channel_id(channel_id)
, m_menu_view("View") , m_menu_view("View")
, m_menu_view_settings("More _Settings", true) { , m_menu_view_settings("More _Settings", true) {
@ -102,14 +36,19 @@ VoiceWindow::VoiceWindow(Snowflake channel_id)
auto &discord = Abaddon::Get().GetDiscordClient(); auto &discord = Abaddon::Get().GetDiscordClient();
auto &audio = Abaddon::Get().GetAudio(); auto &audio = Abaddon::Get().GetAudio();
const auto channel = discord.GetChannel(m_channel_id);
m_is_stage = channel.has_value() && channel->Type == ChannelType::GUILD_STAGE_VOICE;
SetUsers(discord.GetUsersInVoiceChannel(m_channel_id)); SetUsers(discord.GetUsersInVoiceChannel(m_channel_id));
discord.signal_voice_user_disconnect().connect(sigc::mem_fun(*this, &VoiceWindow::OnUserDisconnect)); discord.signal_voice_user_disconnect().connect(sigc::mem_fun(*this, &VoiceWindow::OnUserDisconnect));
discord.signal_voice_user_connect().connect(sigc::mem_fun(*this, &VoiceWindow::OnUserConnect)); discord.signal_voice_user_connect().connect(sigc::mem_fun(*this, &VoiceWindow::OnUserConnect));
discord.signal_voice_speaker_state_changed().connect(sigc::mem_fun(*this, &VoiceWindow::OnSpeakerStateChanged));
discord.signal_voice_state_set().connect(sigc::mem_fun(*this, &VoiceWindow::OnVoiceStateUpdate));
if (const auto self_state = discord.GetVoiceState(discord.GetUserData().ID); self_state.has_value()) { if (const auto self_state = discord.GetVoiceState(discord.GetUserData().ID); self_state.has_value()) {
m_mute.set_active((self_state->second & VoiceStateFlags::SelfMute) == VoiceStateFlags::SelfMute); m_mute.set_active(util::FlagSet(self_state->second.Flags, VoiceStateFlags::SelfMute));
m_deafen.set_active((self_state->second & VoiceStateFlags::SelfDeaf) == VoiceStateFlags::SelfDeaf); m_deafen.set_active(util::FlagSet(self_state->second.Flags, VoiceStateFlags::SelfDeaf));
} }
m_mute.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnMuteChanged)); m_mute.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnMuteChanged));
@ -253,35 +192,97 @@ VoiceWindow::VoiceWindow(Snowflake channel_id)
combos_combos->pack_start(m_playback_combo); combos_combos->pack_start(m_playback_combo);
combos_combos->pack_start(m_capture_combo); combos_combos->pack_start(m_capture_combo);
m_scroll.add(m_user_list); if (const auto instance = discord.GetStageInstanceFromChannel(channel_id); instance.has_value()) {
m_stage_topic_label.show();
UpdateStageTopicLabel(instance->Topic);
} else {
m_stage_topic_label.hide();
}
discord.signal_stage_instance_create().connect(sigc::mem_fun(*this, &VoiceWindow::OnStageInstanceCreate));
discord.signal_stage_instance_update().connect(sigc::mem_fun(*this, &VoiceWindow::OnStageInstanceUpdate));
discord.signal_stage_instance_delete().connect(sigc::mem_fun(*this, &VoiceWindow::OnStageInstanceDelete));
m_stage_command.signal_clicked().connect([this]() {
auto &discord = Abaddon::Get().GetDiscordClient();
const auto user_id = discord.GetUserData().ID;
const bool is_moderator = discord.IsStageModerator(user_id, m_channel_id);
const bool is_speaker = discord.IsUserSpeaker(user_id);
const bool is_invited_to_speak = discord.IsUserInvitedToSpeak(user_id);
if (is_speaker) {
discord.SetStageSpeaking(m_channel_id, false, NOOP_CALLBACK);
} else if (is_moderator) {
discord.SetStageSpeaking(m_channel_id, true, NOOP_CALLBACK);
} else if (is_invited_to_speak) {
discord.DeclineInviteToSpeak(m_channel_id, NOOP_CALLBACK);
} else {
const bool requested = discord.HasUserRequestedToSpeak(user_id);
discord.RequestToSpeak(m_channel_id, !requested, NOOP_CALLBACK);
}
});
m_stage_accept.signal_clicked().connect([this]() {
Abaddon::Get().GetDiscordClient().SetStageSpeaking(m_channel_id, true, NOOP_CALLBACK);
});
m_stage_decline.signal_clicked().connect([this]() {
Abaddon::Get().GetDiscordClient().DeclineInviteToSpeak(m_channel_id, NOOP_CALLBACK);
});
m_speakers_label.set_markup("<b>Speakers</b>");
if (m_is_stage) m_listing.pack_start(m_speakers_label, false, true);
m_listing.pack_start(m_speakers_list, false, true);
m_audience_label.set_markup("<b>Audience</b>");
if (m_is_stage) m_listing.pack_start(m_audience_label, false, true);
if (m_is_stage) m_listing.pack_start(m_audience_list, false, true);
m_scroll.add(m_listing);
m_controls.add(m_mute); m_controls.add(m_mute);
m_controls.add(m_deafen); m_controls.add(m_deafen);
m_controls.add(m_noise_suppression); m_controls.add(m_noise_suppression);
m_controls.add(m_mix_mono); m_controls.add(m_mix_mono);
m_controls.pack_end(m_disconnect, false, true); m_buttons.set_halign(Gtk::ALIGN_CENTER);
if (m_is_stage) m_buttons.pack_start(m_stage_command, false, true);
m_buttons.pack_start(m_disconnect, false, true);
m_stage_invite_box.pack_start(m_stage_invite_lbl, false, true);
m_stage_invite_box.pack_start(m_stage_invite_btns);
m_stage_invite_btns.set_halign(Gtk::ALIGN_CENTER);
m_stage_invite_btns.pack_start(m_stage_accept, false, true);
m_stage_invite_btns.pack_start(m_stage_decline, false, true);
m_main.pack_start(m_menu_bar, false, true); m_main.pack_start(m_menu_bar, false, true);
m_main.pack_start(m_controls, false, true); m_main.pack_start(m_controls, false, true);
m_main.pack_start(m_buttons, false, true);
m_main.pack_start(m_stage_invite_box, false, true);
m_main.pack_start(m_vad_value, false, true); m_main.pack_start(m_vad_value, false, true);
m_main.pack_start(*Gtk::make_managed<Gtk::Label>("Input Settings"), false, true); m_main.pack_start(*Gtk::make_managed<Gtk::Label>("Input Settings"), false, true);
m_main.pack_start(*sliders_container, false, true); m_main.pack_start(*sliders_container, false, true);
m_main.pack_start(m_scroll); m_main.pack_start(m_scroll);
m_stage_topic_label.set_ellipsize(Pango::ELLIPSIZE_END);
m_stage_topic_label.set_halign(Gtk::ALIGN_CENTER);
m_main.pack_start(m_stage_topic_label, false, true);
m_main.pack_start(*combos_container, false, true, 2); m_main.pack_start(*combos_container, false, true, 2);
add(m_main); add(m_main);
show_all_children(); show_all_children();
Glib::signal_timeout().connect(sigc::mem_fun(*this, &VoiceWindow::UpdateVoiceMeters), 40); Glib::signal_timeout().connect(sigc::mem_fun(*this, &VoiceWindow::UpdateVoiceMeters), 40);
UpdateStageCommand();
} }
void VoiceWindow::SetUsers(const std::unordered_set<Snowflake> &user_ids) { void VoiceWindow::SetUsers(const std::unordered_set<Snowflake> &user_ids) {
const auto me = Abaddon::Get().GetDiscordClient().GetUserData().ID; auto &discord = Abaddon::Get().GetDiscordClient();
const auto me = discord.GetUserData().ID;
for (auto id : user_ids) { for (auto id : user_ids) {
if (id == me) continue; if (!m_is_stage || discord.IsUserSpeaker(id)) {
m_user_list.add(*CreateRow(id)); if (id != me) m_speakers_list.add(*CreateSpeakerRow(id));
} else {
m_audience_list.add(*CreateAudienceRow(id));
}
} }
} }
Gtk::ListBoxRow *VoiceWindow::CreateRow(Snowflake id) { Gtk::ListBoxRow *VoiceWindow::CreateSpeakerRow(Snowflake id) {
auto *row = Gtk::make_managed<VoiceWindowUserListEntry>(id); auto *row = Gtk::make_managed<VoiceWindowSpeakerListEntry>(id);
m_rows[id] = row; m_rows[id] = row;
auto &vc = Abaddon::Get().GetDiscordClient().GetVoiceClient(); auto &vc = Abaddon::Get().GetDiscordClient().GetVoiceClient();
row->RestoreGain(vc.GetUserVolume(id)); row->RestoreGain(vc.GetUserVolume(id));
@ -291,7 +292,14 @@ Gtk::ListBoxRow *VoiceWindow::CreateRow(Snowflake id) {
row->signal_volume().connect([this, id](double volume) { row->signal_volume().connect([this, id](double volume) {
m_signal_user_volume_changed.emit(id, volume); m_signal_user_volume_changed.emit(id, volume);
}); });
row->show_all(); row->show();
return row;
}
Gtk::ListBoxRow *VoiceWindow::CreateAudienceRow(Snowflake id) {
auto *row = Gtk::make_managed<VoiceWindowAudienceListEntry>(id);
m_rows[id] = row;
row->show();
return row; return row;
} }
@ -303,6 +311,13 @@ void VoiceWindow::OnDeafenChanged() {
m_signal_deafen.emit(m_deafen.get_active()); m_signal_deafen.emit(m_deafen.get_active());
} }
void VoiceWindow::TryDeleteRow(Snowflake id) {
if (auto it = m_rows.find(id); it != m_rows.end()) {
delete it->second;
m_rows.erase(it);
}
}
bool VoiceWindow::UpdateVoiceMeters() { bool VoiceWindow::UpdateVoiceMeters() {
auto &audio = Abaddon::Get().GetAudio(); auto &audio = Abaddon::Get().GetAudio();
switch (audio.GetVADMethod()) { switch (audio.GetVADMethod()) {
@ -319,7 +334,9 @@ bool VoiceWindow::UpdateVoiceMeters() {
for (auto [id, row] : m_rows) { for (auto [id, row] : m_rows) {
const auto ssrc = Abaddon::Get().GetDiscordClient().GetSSRCOfUser(id); const auto ssrc = Abaddon::Get().GetDiscordClient().GetSSRCOfUser(id);
if (ssrc.has_value()) { if (ssrc.has_value()) {
row->SetVolumeMeter(audio.GetSSRCVolumeLevel(*ssrc)); if (auto *speaker_row = dynamic_cast<VoiceWindowSpeakerListEntry *>(row)) {
speaker_row->SetVolumeMeter(audio.GetSSRCVolumeLevel(*ssrc));
}
} }
} }
return true; return true;
@ -339,23 +356,80 @@ void VoiceWindow::UpdateVADParamValue() {
} }
} }
void VoiceWindow::UpdateStageCommand() {
auto &discord = Abaddon::Get().GetDiscordClient();
const auto user_id = discord.GetUserData().ID;
m_has_requested_to_speak = discord.HasUserRequestedToSpeak(user_id);
const bool is_moderator = discord.IsStageModerator(user_id, m_channel_id);
const bool is_speaker = discord.IsUserSpeaker(user_id);
const bool is_invited_to_speak = discord.IsUserInvitedToSpeak(user_id);
m_stage_invite_box.set_visible(is_invited_to_speak);
if (is_speaker) {
m_stage_command.set_label("Leave the Stage");
} else if (is_moderator) {
m_stage_command.set_label("Speak on Stage");
} else if (m_has_requested_to_speak) {
m_stage_command.set_label("Cancel Request");
} else if (is_invited_to_speak) {
m_stage_command.set_label("Decline Invite");
} else {
m_stage_command.set_label("Request to Speak");
}
}
void VoiceWindow::UpdateStageTopicLabel(const std::string &topic) {
m_stage_topic_label.set_markup("Topic: " + topic);
}
void VoiceWindow::OnUserConnect(Snowflake user_id, Snowflake to_channel_id) { void VoiceWindow::OnUserConnect(Snowflake user_id, Snowflake to_channel_id) {
if (m_channel_id == to_channel_id) { if (m_channel_id == to_channel_id) {
if (auto it = m_rows.find(user_id); it == m_rows.end()) { if (auto it = m_rows.find(user_id); it == m_rows.end()) {
m_user_list.add(*CreateRow(user_id)); if (Abaddon::Get().GetDiscordClient().IsUserSpeaker(user_id)) {
m_speakers_list.add(*CreateSpeakerRow(user_id));
} else {
m_audience_list.add(*CreateAudienceRow(user_id));
}
} }
} }
} }
void VoiceWindow::OnUserDisconnect(Snowflake user_id, Snowflake from_channel_id) { void VoiceWindow::OnUserDisconnect(Snowflake user_id, Snowflake from_channel_id) {
if (m_channel_id == from_channel_id) { if (m_channel_id == from_channel_id) TryDeleteRow(user_id);
if (auto it = m_rows.find(user_id); it != m_rows.end()) { }
delete it->second;
m_rows.erase(it); void VoiceWindow::OnSpeakerStateChanged(Snowflake channel_id, Snowflake user_id, bool is_speaker) {
} if (m_channel_id != channel_id) return;
TryDeleteRow(user_id);
if (is_speaker) {
m_speakers_list.add(*CreateSpeakerRow(user_id));
} else {
m_audience_list.add(*CreateAudienceRow(user_id));
} }
} }
void VoiceWindow::OnVoiceStateUpdate(Snowflake user_id, Snowflake channel_id, VoiceStateFlags flags) {
auto &discord = Abaddon::Get().GetDiscordClient();
if (user_id != discord.GetUserData().ID) return;
UpdateStageCommand();
}
void VoiceWindow::OnStageInstanceCreate(const StageInstance &instance) {
m_stage_topic_label.show();
UpdateStageTopicLabel(instance.Topic);
}
void VoiceWindow::OnStageInstanceUpdate(const StageInstance &instance) {
UpdateStageTopicLabel(instance.Topic);
}
void VoiceWindow::OnStageInstanceDelete(const StageInstance &instance) {
m_stage_topic_label.hide();
}
VoiceWindow::type_signal_mute VoiceWindow::signal_mute() { VoiceWindow::type_signal_mute VoiceWindow::signal_mute() {
return m_signal_mute; return m_signal_mute;
} }

View File

@ -1,4 +1,6 @@
#pragma once #pragma once
#include "discord/stage.hpp"
#include "discord/voicestate.hpp"
#ifdef WITH_VOICE #ifdef WITH_VOICE
// clang-format off // clang-format off
@ -16,7 +18,6 @@
#include <unordered_set> #include <unordered_set>
// clang-format on // clang-format on
class VoiceWindowUserListEntry;
class VoiceWindow : public Gtk::Window { class VoiceWindow : public Gtk::Window {
public: public:
VoiceWindow(Snowflake channel_id); VoiceWindow(Snowflake channel_id);
@ -24,17 +25,25 @@ public:
private: private:
void SetUsers(const std::unordered_set<Snowflake> &user_ids); void SetUsers(const std::unordered_set<Snowflake> &user_ids);
Gtk::ListBoxRow *CreateRow(Snowflake id); Gtk::ListBoxRow *CreateSpeakerRow(Snowflake id);
Gtk::ListBoxRow *CreateAudienceRow(Snowflake id);
void OnUserConnect(Snowflake user_id, Snowflake to_channel_id); void OnUserConnect(Snowflake user_id, Snowflake to_channel_id);
void OnUserDisconnect(Snowflake user_id, Snowflake from_channel_id); void OnUserDisconnect(Snowflake user_id, Snowflake from_channel_id);
void OnSpeakerStateChanged(Snowflake channel_id, Snowflake user_id, bool is_speaker);
void OnVoiceStateUpdate(Snowflake user_id, Snowflake channel_id, VoiceStateFlags flags);
void OnStageInstanceCreate(const StageInstance &instance);
void OnStageInstanceUpdate(const StageInstance &instance);
void OnStageInstanceDelete(const StageInstance &instance);
void OnMuteChanged(); void OnMuteChanged();
void OnDeafenChanged(); void OnDeafenChanged();
void TryDeleteRow(Snowflake id);
bool UpdateVoiceMeters(); bool UpdateVoiceMeters();
void UpdateVADParamValue(); void UpdateVADParamValue();
void UpdateStageCommand();
void UpdateStageTopicLabel(const std::string &topic);
Gtk::Box m_main; Gtk::Box m_main;
Gtk::Box m_controls; Gtk::Box m_controls;
@ -43,7 +52,9 @@ private:
Gtk::CheckButton m_deafen; Gtk::CheckButton m_deafen;
Gtk::ScrolledWindow m_scroll; Gtk::ScrolledWindow m_scroll;
Gtk::ListBox m_user_list; Gtk::VBox m_listing;
Gtk::ListBox m_speakers_list;
Gtk::ListBox m_audience_list;
// Shows volume for gate VAD method // Shows volume for gate VAD method
// Shows probability for RNNoise VAD method // Shows probability for RNNoise VAD method
@ -56,21 +67,36 @@ private:
Gtk::CheckButton m_noise_suppression; Gtk::CheckButton m_noise_suppression;
Gtk::CheckButton m_mix_mono; Gtk::CheckButton m_mix_mono;
Gtk::HBox m_buttons;
Gtk::Button m_disconnect; Gtk::Button m_disconnect;
Gtk::Button m_stage_command;
Gtk::VBox m_stage_invite_box;
Gtk::Label m_stage_invite_lbl;
Gtk::HBox m_stage_invite_btns;
Gtk::Button m_stage_accept;
Gtk::Button m_stage_decline;
bool m_has_requested_to_speak = false;
Gtk::ComboBoxText m_vad_combo; Gtk::ComboBoxText m_vad_combo;
Gtk::ComboBox m_playback_combo; Gtk::ComboBox m_playback_combo;
Gtk::ComboBox m_capture_combo; Gtk::ComboBox m_capture_combo;
Snowflake m_channel_id; Snowflake m_channel_id;
bool m_is_stage;
std::unordered_map<Snowflake, VoiceWindowUserListEntry *> m_rows; std::unordered_map<Snowflake, Gtk::ListBoxRow *> m_rows;
Gtk::MenuBar m_menu_bar; Gtk::MenuBar m_menu_bar;
Gtk::MenuItem m_menu_view; Gtk::MenuItem m_menu_view;
Gtk::Menu m_menu_view_sub; Gtk::Menu m_menu_view_sub;
Gtk::MenuItem m_menu_view_settings; Gtk::MenuItem m_menu_view_settings;
Gtk::Label m_stage_topic_label;
Gtk::Label m_speakers_label;
Gtk::Label m_audience_label;
public: public:
using type_signal_mute = sigc::signal<void(bool)>; using type_signal_mute = sigc::signal<void(bool)>;
using type_signal_deafen = sigc::signal<void(bool)>; using type_signal_deafen = sigc::signal<void(bool)>;

View File

@ -0,0 +1,23 @@
#include "voicewindowaudiencelistentry.hpp"
#include "abaddon.hpp"
VoiceWindowAudienceListEntry::VoiceWindowAudienceListEntry(Snowflake id)
: m_main(Gtk::ORIENTATION_HORIZONTAL)
, m_avatar(32, 32) {
m_name.set_halign(Gtk::ALIGN_START);
m_name.set_hexpand(true);
m_main.add(m_avatar);
m_main.add(m_name);
add(m_main);
show_all_children();
auto &discord = Abaddon::Get().GetDiscordClient();
const auto user = discord.GetUser(id);
if (user.has_value()) {
m_name.set_text(user->GetUsername());
m_avatar.SetURL(user->GetAvatarURL("png", "32"));
} else {
m_name.set_text("Unknown user");
}
}

View File

@ -0,0 +1,18 @@
#pragma once
#include "components/lazyimage.hpp"
#include "discord/snowflake.hpp"
#include <gtkmm/box.h>
#include <gtkmm/label.h>
#include <gtkmm/listboxrow.h>
class VoiceWindowAudienceListEntry : public Gtk::ListBoxRow {
public:
VoiceWindowAudienceListEntry(Snowflake id);
private:
Gtk::Box m_main;
LazyImage m_avatar;
Gtk::Label m_name;
};

View File

@ -0,0 +1,58 @@
#include "voicewindowspeakerlistentry.hpp"
#include "abaddon.hpp"
VoiceWindowSpeakerListEntry::VoiceWindowSpeakerListEntry(Snowflake id)
: m_main(Gtk::ORIENTATION_VERTICAL)
, m_horz(Gtk::ORIENTATION_HORIZONTAL)
, m_avatar(32, 32)
, m_mute("Mute") {
m_name.set_halign(Gtk::ALIGN_START);
m_name.set_hexpand(true);
m_mute.set_halign(Gtk::ALIGN_END);
m_volume.set_range(0.0, 200.0);
m_volume.set_value_pos(Gtk::POS_LEFT);
m_volume.set_value(100.0);
m_volume.signal_value_changed().connect([this]() {
m_signal_volume.emit(m_volume.get_value() * 0.01);
});
m_horz.add(m_avatar);
m_horz.add(m_name);
m_horz.add(m_mute);
m_main.add(m_horz);
m_main.add(m_volume);
m_main.add(m_meter);
add(m_main);
show_all_children();
auto &discord = Abaddon::Get().GetDiscordClient();
const auto user = discord.GetUser(id);
if (user.has_value()) {
m_name.set_text(user->GetUsername());
m_avatar.SetURL(user->GetAvatarURL("png", "32"));
} else {
m_name.set_text("Unknown user");
}
m_mute.signal_toggled().connect([this]() {
m_signal_mute_cs.emit(m_mute.get_active());
});
}
void VoiceWindowSpeakerListEntry::SetVolumeMeter(double frac) {
m_meter.SetVolume(frac);
}
void VoiceWindowSpeakerListEntry::RestoreGain(double frac) {
m_volume.set_value(frac * 100.0);
}
VoiceWindowSpeakerListEntry::type_signal_mute_cs VoiceWindowSpeakerListEntry::signal_mute_cs() {
return m_signal_mute_cs;
}
VoiceWindowSpeakerListEntry::type_signal_volume VoiceWindowSpeakerListEntry::signal_volume() {
return m_signal_volume;
}

View File

@ -0,0 +1,38 @@
#pragma once
#include "components/lazyimage.hpp"
#include "components/volumemeter.hpp"
#include "discord/snowflake.hpp"
#include <gtkmm/box.h>
#include <gtkmm/checkbutton.h>
#include <gtkmm/label.h>
#include <gtkmm/listboxrow.h>
#include <gtkmm/scale.h>
class VoiceWindowSpeakerListEntry : public Gtk::ListBoxRow {
public:
VoiceWindowSpeakerListEntry(Snowflake id);
void SetVolumeMeter(double frac);
void RestoreGain(double frac);
private:
Gtk::Box m_main;
Gtk::Box m_horz;
LazyImage m_avatar;
Gtk::Label m_name;
Gtk::CheckButton m_mute;
Gtk::Scale m_volume;
VolumeMeter m_meter;
public:
using type_signal_mute_cs = sigc::signal<void(bool)>;
using type_signal_volume = sigc::signal<void(double)>;
type_signal_mute_cs signal_mute_cs();
type_signal_volume signal_volume();
private:
type_signal_mute_cs m_signal_mute_cs;
type_signal_volume m_signal_volume;
};