summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/abaddon.cpp2
-rw-r--r--src/components/channellist/cellrendererchannels.cpp12
-rw-r--r--src/components/channellist/cellrendererchannels.hpp6
-rw-r--r--src/components/channellist/channellisttree.cpp56
-rw-r--r--src/components/channellist/channellisttree.hpp5
-rw-r--r--src/discord/channel.hpp16
-rw-r--r--src/discord/discord.cpp88
-rw-r--r--src/discord/discord.hpp25
-rw-r--r--src/discord/guild.cpp1
-rw-r--r--src/discord/guild.hpp2
-rw-r--r--src/discord/objects.cpp1
-rw-r--r--src/discord/objects.hpp5
-rw-r--r--src/discord/stage.cpp12
-rw-r--r--src/discord/stage.hpp32
-rw-r--r--src/discord/voiceclient.cpp1
-rw-r--r--src/discord/voiceclient.hpp23
-rw-r--r--src/discord/voicestate.cpp5
-rw-r--r--src/discord/voicestate.hpp (renamed from src/discord/voicestateflags.hpp)11
-rw-r--r--src/misc/bitwise.hpp7
-rw-r--r--src/windows/voice/voicewindow.cpp (renamed from src/windows/voicewindow.cpp)179
-rw-r--r--src/windows/voice/voicewindow.hpp (renamed from src/windows/voicewindow.hpp)18
-rw-r--r--src/windows/voice/voicewindowaudiencelistentry.cpp23
-rw-r--r--src/windows/voice/voicewindowaudiencelistentry.hpp18
-rw-r--r--src/windows/voice/voicewindowspeakerlistentry.cpp58
-rw-r--r--src/windows/voice/voicewindowspeakerlistentry.hpp38
25 files changed, 510 insertions, 134 deletions
diff --git a/src/abaddon.cpp b/src/abaddon.cpp
index 23abfd3..b568f47 100644
--- a/src/abaddon.cpp
+++ b/src/abaddon.cpp
@@ -20,7 +20,7 @@
#include "windows/profilewindow.hpp"
#include "windows/pinnedwindow.hpp"
#include "windows/threadswindow.hpp"
-#include "windows/voicewindow.hpp"
+#include "windows/voice/voicewindow.hpp"
#include "startup.hpp"
#include "notifications/notifications.hpp"
#include "remoteauth/remoteauthdialog.hpp"
diff --git a/src/components/channellist/cellrendererchannels.cpp b/src/components/channellist/cellrendererchannels.cpp
index 8a6097e..af9109a 100644
--- a/src/components/channellist/cellrendererchannels.cpp
+++ b/src/components/channellist/cellrendererchannels.cpp
@@ -123,6 +123,7 @@ void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &m
case RenderType::Thread:
return get_preferred_width_vfunc_thread(widget, minimum_width, natural_width);
case RenderType::VoiceChannel:
+ case RenderType::VoiceStage:
return get_preferred_width_vfunc_voice_channel(widget, minimum_width, natural_width);
case RenderType::VoiceParticipant:
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:
return get_preferred_width_for_height_vfunc_thread(widget, height, minimum_width, natural_width);
case RenderType::VoiceChannel:
+ case RenderType::VoiceStage:
return get_preferred_width_for_height_vfunc_voice_channel(widget, height, minimum_width, natural_width);
case RenderType::VoiceParticipant:
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:
return get_preferred_height_vfunc_thread(widget, minimum_height, natural_height);
case RenderType::VoiceChannel:
+ case RenderType::VoiceStage:
return get_preferred_height_vfunc_voice_channel(widget, minimum_height, natural_height);
case RenderType::VoiceParticipant:
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:
return get_preferred_height_for_width_vfunc_thread(widget, width, minimum_height, natural_height);
case RenderType::VoiceChannel:
+ case RenderType::VoiceStage:
return get_preferred_height_for_width_vfunc_voice_channel(widget, width, minimum_height, natural_height);
case RenderType::VoiceParticipant:
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:
return render_vfunc_thread(cr, widget, background_area, cell_area, flags);
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:
return render_vfunc_voice_participant(cr, widget, background_area, cell_area, flags);
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);
}
-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
Gtk::Requisition 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;
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_alignment(Pango::ALIGN_LEFT);
cr->set_source_rgba(1.0, 1.0, 1.0, 1.0);
diff --git a/src/components/channellist/cellrendererchannels.hpp b/src/components/channellist/cellrendererchannels.hpp
index ebe4957..7059c6f 100644
--- a/src/components/channellist/cellrendererchannels.hpp
+++ b/src/components/channellist/cellrendererchannels.hpp
@@ -6,7 +6,7 @@
#include <gtkmm/cellrendererpixbuf.h>
#include <gtkmm/cellrenderertext.h>
#include "discord/snowflake.hpp"
-#include "discord/voicestateflags.hpp"
+#include "discord/voicestate.hpp"
#include "misc/bitwise.hpp"
enum class RenderType : uint8_t {
@@ -16,6 +16,7 @@ enum class RenderType : uint8_t {
TextChannel,
Thread,
VoiceChannel,
+ VoiceStage, // identical to non-stage except for icon
VoiceParticipant,
DMHeader,
@@ -112,7 +113,8 @@ protected:
Gtk::Widget &widget,
const Gdk::Rectangle &background_area,
const Gdk::Rectangle &cell_area,
- Gtk::CellRendererState flags);
+ Gtk::CellRendererState flags,
+ const char *emoji);
// voice participant
void get_preferred_width_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_width, int &natural_width) const;
diff --git a/src/components/channellist/channellisttree.cpp b/src/components/channellist/channellisttree.cpp
index 4597a1f..9adee61 100644
--- a/src/components/channellist/channellisttree.cpp
+++ b/src/components/channellist/channellisttree.cpp
@@ -28,6 +28,8 @@ ChannelListTree::ChannelListTree()
#endif
, m_menu_voice_channel_join("_Join", 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_open_chat("Open _Chat", 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.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] {
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];
if (a_type == RenderType::DMHeader) return -1;
if (b_type == RenderType::DMHeader) return 1;
- if (a_type == RenderType::TextChannel && b_type == RenderType::VoiceChannel) return -1;
- if (b_type == RenderType::TextChannel && a_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 || a_type == RenderType::VoiceStage)) return 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) {
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) return;
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) {
const auto channel = discord.GetChannel(channel_.ID);
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())
categories[*channel->ParentID].push_back(*channel);
else
@@ -954,6 +972,10 @@ Gtk::TreeModel::iterator ChannelListTree::AddGuild(const GuildData &guild, const
if (IsTextChannel(channel.Type)) {
channel_row[m_columns.m_type] = RenderType::TextChannel;
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 {
channel_row[m_columns.m_type] = RenderType::VoiceChannel;
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)) {
channel_row[m_columns.m_type] = RenderType::TextChannel;
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 {
channel_row[m_columns.m_type] = RenderType::VoiceChannel;
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);
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();
@@ -1328,6 +1354,10 @@ bool ChannelListTree::OnButtonPressEvent(GdkEventButton *ev) {
OnVoiceChannelSubmenuPopup();
m_menu_voice_channel.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
break;
+ case RenderType::VoiceStage:
+ OnVoiceStageSubmenuPopup();
+ m_menu_voice_stage.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
+ break;
case RenderType::DM: {
OnDMSubmenuPopup();
const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(static_cast<Snowflake>(row[m_columns.m_id]));
@@ -1439,6 +1469,24 @@ void ChannelListTree::OnVoiceChannelSubmenuPopup() {
#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);
+ }
+#endif
+ m_menu_voice_stage_join.set_sensitive(false);
+ m_menu_voice_stage_disconnect.set_sensitive(false);
+}
+
void ChannelListTree::OnDMSubmenuPopup() {
auto iter = m_model->get_iter(m_path_for_menu);
if (!iter) return;
diff --git a/src/components/channellist/channellisttree.hpp b/src/components/channellist/channellisttree.hpp
index 9e2c544..323f843 100644
--- a/src/components/channellist/channellisttree.hpp
+++ b/src/components/channellist/channellisttree.hpp
@@ -162,6 +162,10 @@ protected:
Gtk::Menu m_menu_voice_channel;
Gtk::MenuItem m_menu_voice_channel_join;
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_open_chat;
@@ -192,6 +196,7 @@ protected:
void OnDMSubmenuPopup();
void OnThreadSubmenuPopup();
void OnVoiceChannelSubmenuPopup();
+ void OnVoiceStageSubmenuPopup();
bool m_updating_listing = false;
diff --git a/src/discord/channel.hpp b/src/discord/channel.hpp
index cac8b4c..ebf67b0 100644
--- a/src/discord/channel.hpp
+++ b/src/discord/channel.hpp
@@ -27,22 +27,6 @@ enum class ChannelType : int {
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?
struct ThreadMetadataData {
diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp
index 062a871..8f88a91 100644
--- a/src/discord/discord.cpp
+++ b/src/discord/discord.cpp
@@ -360,6 +360,14 @@ std::optional<WebhookMessageData> DiscordClient::GetWebhookMessageData(Snowflake
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 {
return std::find(m_joined_threads.begin(), m_joined_threads.end(), thread_id) != m_joined_threads.end();
}
@@ -1288,6 +1296,11 @@ std::optional<uint32_t> DiscordClient::GetSSRCOfUser(Snowflake id) const {
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();
+}
+
DiscordVoiceClient &DiscordClient::GetVoiceClient() {
return m_voice;
}
@@ -1303,7 +1316,7 @@ void DiscordClient::SetVoiceDeafened(bool is_deaf) {
}
#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()) {
return it->second;
}
@@ -1652,6 +1665,15 @@ void DiscordClient::HandleGatewayMessage(std::string str) {
case GatewayEvent::GUILD_MEMBERS_CHUNK: {
HandleGatewayGuildMembersChunk(m);
} 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: {
HandleGatewayVoiceStateUpdate(m);
} break;
@@ -1712,6 +1734,14 @@ void DiscordClient::ProcessNewGuild(GuildData &guild) {
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.SetGuild(guild.ID, guild);
@@ -2302,6 +2332,29 @@ void DiscordClient::HandleGatewayGuildMembersChunk(const GatewayMessage &msg) {
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
/*
@@ -2399,9 +2452,14 @@ void DiscordClient::CheckVoiceState(const VoiceState &data) {
if (data.ChannelID.has_value()) {
const auto old_state = GetVoiceState(data.UserID);
SetVoiceState(data.UserID, data);
- if (old_state.has_value() && old_state->first != *data.ChannelID) {
- m_signal_voice_user_disconnect.emit(data.UserID, old_state->first);
- m_signal_voice_user_connect.emit(data.UserID, *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_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()) {
m_signal_voice_user_connect.emit(data.UserID, *data.ChannelID);
}
@@ -2954,8 +3012,9 @@ void DiscordClient::SetVoiceState(Snowflake user_id, const VoiceState &state) {
if (state.IsDeafened) flags |= VoiceStateFlags::Deaf;
if (state.IsSelfStream) flags |= VoiceStateFlags::SelfStream;
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_signal_voice_state_set.emit(user_id, *state.ChannelID, flags);
@@ -3018,6 +3077,9 @@ void DiscordClient::LoadEventMap() {
m_event_map["VOICE_STATE_UPDATE"] = GatewayEvent::VOICE_STATE_UPDATE;
m_event_map["VOICE_SERVER_UPDATE"] = GatewayEvent::VOICE_SERVER_UPDATE;
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() {
@@ -3196,6 +3258,18 @@ DiscordClient::type_signal_guild_members_chunk DiscordClient::signal_guild_membe
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() {
return m_signal_added_to_thread;
}
@@ -3273,3 +3347,7 @@ DiscordClient::type_signal_voice_user_connect DiscordClient::signal_voice_user_c
DiscordClient::type_signal_voice_state_set DiscordClient::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;
+} \ No newline at end of file
diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp
index 4e898dc..ab051aa 100644
--- a/src/discord/discord.hpp
+++ b/src/discord/discord.hpp
@@ -5,7 +5,7 @@
#include "objects.hpp"
#include "store.hpp"
#include "voiceclient.hpp"
-#include "voicestateflags.hpp"
+#include "voicestate.hpp"
#include "websocket.hpp"
#include <gdkmm/rgba.h>
#include <sigc++/sigc++.h>
@@ -65,6 +65,7 @@ public:
void GetArchivedPrivateThreads(Snowflake channel_id, const sigc::slot<void(DiscordError, const ArchivedThreadsResponseData &)> &callback);
std::vector<Snowflake> GetChildChannelIDs(Snowflake parent_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
template<typename Iter>
@@ -201,6 +202,7 @@ public:
[[nodiscard]] bool IsVoiceConnecting() const noexcept;
[[nodiscard]] Snowflake GetVoiceChannelID() const noexcept;
[[nodiscard]] std::optional<uint32_t> GetSSRCOfUser(Snowflake id) const;
+ [[nodiscard]] bool IsUserSpeaker(Snowflake user_id) const;
DiscordVoiceClient &GetVoiceClient();
@@ -208,7 +210,7 @@ public:
void SetVoiceDeafened(bool is_deaf);
#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);
void SetReferringChannel(Snowflake id);
@@ -295,6 +297,9 @@ private:
void HandleGatewayMessageAck(const GatewayMessage &msg);
void HandleGatewayUserGuildSettingsUpdate(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 HandleGatewayReconnect(const GatewayMessage &msg);
void HandleGatewayInvalidSession(const GatewayMessage &msg);
@@ -345,6 +350,8 @@ private:
std::unordered_set<Snowflake> m_muted_channels;
std::unordered_map<Snowflake, int> m_unread;
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;
UserSettings m_user_settings;
@@ -388,7 +395,7 @@ private:
void ClearVoiceState(Snowflake user_id);
// 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;
mutable std::mutex m_msg_mutex;
@@ -446,6 +453,9 @@ public:
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;
+ 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
typedef sigc::signal<void, Snowflake> type_signal_added_to_thread;
@@ -477,6 +487,7 @@ public:
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_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_supplemental signal_gateway_ready_supplemental();
@@ -519,6 +530,9 @@ public:
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_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_removed_from_thread signal_removed_from_thread();
@@ -546,6 +560,7 @@ public:
type_signal_voice_user_disconnect signal_voice_user_disconnect();
type_signal_voice_user_connect signal_voice_user_connect();
type_signal_voice_state_set signal_voice_state_set();
+ type_signal_voice_speaker_state_changed signal_voice_speaker_state_changed();
protected:
type_signal_gateway_ready m_signal_gateway_ready;
@@ -589,6 +604,9 @@ protected:
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_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_added_to_thread m_signal_added_to_thread;
@@ -616,4 +634,5 @@ protected:
type_signal_voice_user_disconnect m_signal_voice_user_disconnect;
type_signal_voice_user_connect m_signal_voice_user_connect;
type_signal_voice_state_set m_signal_voice_state_set;
+ type_signal_voice_speaker_state_changed m_signal_voice_speaker_state_changed;
};
diff --git a/src/discord/guild.cpp b/src/discord/guild.cpp
index 06c4acf..9cf94c2 100644
--- a/src/discord/guild.cpp
+++ b/src/discord/guild.cpp
@@ -54,6 +54,7 @@ void from_json(const nlohmann::json &j, GuildData &m) {
JS_O("preferred_locale", m.PreferredLocale);
JS_ON("public_updates_channel_id", m.PublicUpdatesChannelID);
JS_O("max_video_channel_users", m.MaxVideoChannelUsers);
+ JS_ON("stage_instances", m.StageInstances);
JS_O("approximate_member_count", tmp);
if (tmp.has_value())
m.ApproximateMemberCount = std::stol(*tmp);
diff --git a/src/discord/guild.hpp b/src/discord/guild.hpp
index 4895d30..1ea858d 100644
--- a/src/discord/guild.hpp
+++ b/src/discord/guild.hpp
@@ -4,6 +4,7 @@
#include "role.hpp"
#include "channel.hpp"
#include "emoji.hpp"
+#include "stage.hpp"
#include <vector>
#include <string>
#include <unordered_set>
@@ -90,6 +91,7 @@ struct GuildData {
std::optional<int> ApproximateMemberCount;
std::optional<int> ApproximatePresenceCount;
std::optional<std::vector<ChannelData>> Threads; // only with permissions to view, id only
+ std::optional<std::vector<StageInstance>> StageInstances;
// undocumented
// std::map<std::string, Unknown> GuildHashes;
diff --git a/src/discord/objects.cpp b/src/discord/objects.cpp
index 804f10d..1c5dd39 100644
--- a/src/discord/objects.cpp
+++ b/src/discord/objects.cpp
@@ -714,4 +714,5 @@ void from_json(const nlohmann::json &j, VoiceState &m) {
JS_D("user_id", m.UserID);
JS_ON("member", m.Member);
JS_D("session_id", m.SessionID);
+ JS_ON("request_to_speak_timestamp", m.RequestToSpeakTimestamp);
}
diff --git a/src/discord/objects.hpp b/src/discord/objects.hpp
index dfe99f0..e026311 100644
--- a/src/discord/objects.hpp
+++ b/src/discord/objects.hpp
@@ -20,6 +20,7 @@
#include "auditlog.hpp"
#include "relationship.hpp"
#include "errors.hpp"
+#include "stage.hpp"
// 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_SERVER_UPDATE,
CALL_CREATE,
+ STAGE_INSTANCE_CREATE,
+ STAGE_INSTANCE_UPDATE,
+ STAGE_INSTANCE_DELETE,
};
enum class GatewayCloseCode : uint16_t {
@@ -917,6 +921,7 @@ struct VoiceState {
std::string SessionID;
bool IsSuppressed;
Snowflake UserID;
+ std::optional<std::string> RequestToSpeakTimestamp;
friend void from_json(const nlohmann::json &j, VoiceState &m);
};
diff --git a/src/discord/stage.cpp b/src/discord/stage.cpp
new file mode 100644
index 0000000..428e1f3
--- /dev/null
+++ b/src/discord/stage.cpp
@@ -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);
+}
diff --git a/src/discord/stage.hpp b/src/discord/stage.hpp
new file mode 100644
index 0000000..3df4433
--- /dev/null
+++ b/src/discord/stage.hpp
@@ -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);
+};
diff --git a/src/discord/voiceclient.cpp b/src/discord/voiceclient.cpp
index 212b878..c0c83b2 100644
--- a/src/discord/voiceclient.cpp
+++ b/src/discord/voiceclient.cpp
@@ -250,6 +250,7 @@ bool DiscordVoiceClient::IsConnecting() const noexcept {
}
void DiscordVoiceClient::OnGatewayMessage(const std::string &str) {
+ m_log->trace("IN: {}", str);
VoiceGatewayMessage msg = nlohmann::json::parse(str);
switch (msg.Opcode) {
case VoiceGatewayOp::Hello:
diff --git a/src/discord/voiceclient.hpp b/src/discord/voiceclient.hpp
index 0112749..aa1014c 100644
--- a/src/discord/voiceclient.hpp
+++ b/src/discord/voiceclient.hpp
@@ -43,6 +43,23 @@ enum class VoiceGatewayOp : int {
Hello = 8,
Resumed = 9,
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 {
@@ -156,11 +173,11 @@ public:
private:
void ReadThread();
- #ifdef _WIN32
+#ifdef _WIN32
SOCKET m_socket;
- #else
+#else
int m_socket;
- #endif
+#endif
sockaddr_in m_server;
std::atomic<bool> m_running = false;
diff --git a/src/discord/voicestate.cpp b/src/discord/voicestate.cpp
new file mode 100644
index 0000000..05c050d
--- /dev/null
+++ b/src/discord/voicestate.cpp
@@ -0,0 +1,5 @@
+#include "voicestate.hpp"
+
+bool PackedVoiceState::IsSpeaker() const noexcept {
+ return ((Flags & VoiceStateFlags::Suppressed) != VoiceStateFlags::Suppressed) && !RequestToSpeakTimestamp.has_value();
+}
diff --git a/src/discord/voicestateflags.hpp b/src/discord/voicestate.hpp
index 01fb762..cc75b0c 100644
--- a/src/discord/voicestateflags.hpp
+++ b/src/discord/voicestate.hpp
@@ -1,7 +1,10 @@
#pragma once
#include <cstdint>
+#include <optional>
+#include <string>
#include "misc/bitwise.hpp"
+// this is packed into a enum cuz it makes implementing tree models easier
enum class VoiceStateFlags : uint8_t {
Clear = 0,
Deaf = 1 << 0,
@@ -10,6 +13,14 @@ enum class VoiceStateFlags : uint8_t {
SelfMute = 1 << 3,
SelfStream = 1 << 4,
SelfVideo = 1 << 5,
+ Suppressed = 1 << 6,
+};
+
+struct PackedVoiceState {
+ VoiceStateFlags Flags;
+ std::optional<std::string> RequestToSpeakTimestamp;
+
+ [[nodiscard]] bool IsSpeaker() const noexcept;
};
template<>
diff --git a/src/misc/bitwise.hpp b/src/misc/bitwise.hpp
index ecce333..4d4cf8f 100644
--- a/src/misc/bitwise.hpp
+++ b/src/misc/bitwise.hpp
@@ -1,6 +1,13 @@
#pragma once
#include <type_traits>
+namespace util {
+template<typename T>
+bool FlagSet(T flags, T value) {
+ return (flags & value) == value;
+}
+} // namespace util
+
template<typename T>
struct Bitwise {
static const bool enable = false;
diff --git a/src/windows/voicewindow.cpp b/src/windows/voice/voicewindow.cpp
index a005e79..7607a0f 100644
--- a/src/windows/voicewindow.cpp
+++ b/src/windows/voice/voicewindow.cpp
@@ -2,88 +2,17 @@
// clang-format off
+#include "voicewindow.hpp"
+
#include "abaddon.hpp"
#include "audio/manager.hpp"
#include "components/lazyimage.hpp"
-#include "voicesettingswindow.hpp"
-#include "voicewindow.hpp"
+#include "voicewindowaudiencelistentry.hpp"
+#include "voicewindowspeakerlistentry.hpp"
+#include "windows/voicesettingswindow.hpp"
// 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)
: m_main(Gtk::ORIENTATION_VERTICAL)
, m_controls(Gtk::ORIENTATION_HORIZONTAL)
@@ -102,14 +31,18 @@ VoiceWindow::VoiceWindow(Snowflake channel_id)
auto &discord = Abaddon::Get().GetDiscordClient();
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));
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_speaker_state_changed().connect(sigc::mem_fun(*this, &VoiceWindow::OnSpeakerStateChanged));
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_deafen.set_active((self_state->second & VoiceStateFlags::SelfDeaf) == VoiceStateFlags::SelfDeaf);
+ m_mute.set_active(util::FlagSet(self_state->second.Flags, VoiceStateFlags::SelfMute));
+ m_deafen.set_active(util::FlagSet(self_state->second.Flags, VoiceStateFlags::SelfDeaf));
}
m_mute.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnMuteChanged));
@@ -253,13 +186,44 @@ VoiceWindow::VoiceWindow(Snowflake channel_id)
combos_combos->pack_start(m_playback_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()) {
+ printf("%s\n", instance->Topic.c_str());
+ m_TMP_stagelabel.show();
+ m_TMP_stagelabel.set_markup("<span foreground='green'>" + instance->Topic + "</span>");
+ } else {
+ m_TMP_stagelabel.hide();
+ }
+
+ discord.signal_stage_instance_create().connect(sigc::track_obj([this](const StageInstance &instance) {
+ m_TMP_stagelabel.show();
+ m_TMP_stagelabel.set_markup("<span foreground='green'>" + instance.Topic + "</span>");
+ },
+ *this));
+
+ discord.signal_stage_instance_update().connect(sigc::track_obj([this](const StageInstance &instance) {
+ m_TMP_stagelabel.set_markup("<span foreground='green'>" + instance.Topic + "</span>");
+ },
+ *this));
+
+ discord.signal_stage_instance_delete().connect(sigc::track_obj([this](const StageInstance &instance) {
+ m_TMP_stagelabel.hide();
+ },
+ *this));
+
+ m_TMP_speakers_label.set_markup("<b>Speakers</b>");
+ m_listing.pack_start(m_TMP_speakers_label, false, true);
+ m_listing.pack_start(m_speakers_list, false, true);
+ m_TMP_audience_label.set_markup("<b>Audience</b>");
+ m_listing.pack_start(m_TMP_audience_label, false, true);
+ m_listing.pack_start(m_audience_list, false, true);
+ m_scroll.add(m_listing);
m_controls.add(m_mute);
m_controls.add(m_deafen);
m_controls.add(m_noise_suppression);
m_controls.add(m_mix_mono);
m_controls.pack_end(m_disconnect, false, true);
m_main.pack_start(m_menu_bar, false, true);
+ m_main.pack_start(m_TMP_stagelabel, false, true);
m_main.pack_start(m_controls, false, true);
m_main.pack_start(m_vad_value, false, true);
m_main.pack_start(*Gtk::make_managed<Gtk::Label>("Input Settings"), false, true);
@@ -268,20 +232,24 @@ VoiceWindow::VoiceWindow(Snowflake channel_id)
m_main.pack_start(*combos_container, false, true, 2);
add(m_main);
show_all_children();
-
+
Glib::signal_timeout().connect(sigc::mem_fun(*this, &VoiceWindow::UpdateVoiceMeters), 40);
}
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) {
- if (id == me) continue;
- m_user_list.add(*CreateRow(id));
+ if (discord.IsUserSpeaker(id)) {
+ if (id != me) m_speakers_list.add(*CreateSpeakerRow(id));
+ } else {
+ m_audience_list.add(*CreateAudienceRow(id));
+ }
}
}
-Gtk::ListBoxRow *VoiceWindow::CreateRow(Snowflake id) {
- auto *row = Gtk::make_managed<VoiceWindowUserListEntry>(id);
+Gtk::ListBoxRow *VoiceWindow::CreateSpeakerRow(Snowflake id) {
+ auto *row = Gtk::make_managed<VoiceWindowSpeakerListEntry>(id);
m_rows[id] = row;
auto &vc = Abaddon::Get().GetDiscordClient().GetVoiceClient();
row->RestoreGain(vc.GetUserVolume(id));
@@ -291,7 +259,14 @@ Gtk::ListBoxRow *VoiceWindow::CreateRow(Snowflake id) {
row->signal_volume().connect([this, id](double 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;
}
@@ -303,6 +278,13 @@ void VoiceWindow::OnDeafenChanged() {
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() {
auto &audio = Abaddon::Get().GetAudio();
switch (audio.GetVADMethod()) {
@@ -319,7 +301,9 @@ bool VoiceWindow::UpdateVoiceMeters() {
for (auto [id, row] : m_rows) {
const auto ssrc = Abaddon::Get().GetDiscordClient().GetSSRCOfUser(id);
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;
@@ -342,17 +326,26 @@ void VoiceWindow::UpdateVADParamValue() {
void VoiceWindow::OnUserConnect(Snowflake user_id, Snowflake to_channel_id) {
if (m_channel_id == to_channel_id) {
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) {
- if (m_channel_id == from_channel_id) {
- if (auto it = m_rows.find(user_id); it != m_rows.end()) {
- delete it->second;
- m_rows.erase(it);
- }
+ if (m_channel_id == from_channel_id) TryDeleteRow(user_id);
+}
+
+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));
}
}
diff --git a/src/windows/voicewindow.hpp b/src/windows/voice/voicewindow.hpp
index fb64010..0df9fa8 100644
--- a/src/windows/voicewindow.hpp
+++ b/src/windows/voice/voicewindow.hpp
@@ -16,7 +16,6 @@
#include <unordered_set>
// clang-format on
-class VoiceWindowUserListEntry;
class VoiceWindow : public Gtk::Window {
public:
VoiceWindow(Snowflake channel_id);
@@ -24,14 +23,18 @@ public:
private:
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 OnUserDisconnect(Snowflake user_id, Snowflake from_channel_id);
+ void OnSpeakerStateChanged(Snowflake channel_id, Snowflake user_id, bool is_speaker);
void OnMuteChanged();
void OnDeafenChanged();
+ void TryDeleteRow(Snowflake id);
+
bool UpdateVoiceMeters();
void UpdateVADParamValue();
@@ -43,7 +46,9 @@ private:
Gtk::CheckButton m_deafen;
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 probability for RNNoise VAD method
@@ -63,14 +68,19 @@ private:
Gtk::ComboBox m_capture_combo;
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::MenuItem m_menu_view;
Gtk::Menu m_menu_view_sub;
Gtk::MenuItem m_menu_view_settings;
+ Gtk::Label m_TMP_stagelabel;
+ Gtk::Label m_TMP_speakers_label;
+ Gtk::Label m_TMP_audience_label;
+
public:
using type_signal_mute = sigc::signal<void(bool)>;
using type_signal_deafen = sigc::signal<void(bool)>;
diff --git a/src/windows/voice/voicewindowaudiencelistentry.cpp b/src/windows/voice/voicewindowaudiencelistentry.cpp
new file mode 100644
index 0000000..cf93343
--- /dev/null
+++ b/src/windows/voice/voicewindowaudiencelistentry.cpp
@@ -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");
+ }
+}
diff --git a/src/windows/voice/voicewindowaudiencelistentry.hpp b/src/windows/voice/voicewindowaudiencelistentry.hpp
new file mode 100644
index 0000000..e7bdbb1
--- /dev/null
+++ b/src/windows/voice/voicewindowaudiencelistentry.hpp
@@ -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;
+};
diff --git a/src/windows/voice/voicewindowspeakerlistentry.cpp b/src/windows/voice/voicewindowspeakerlistentry.cpp
new file mode 100644
index 0000000..a7bf2b8
--- /dev/null
+++ b/src/windows/voice/voicewindowspeakerlistentry.cpp
@@ -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;
+}
diff --git a/src/windows/voice/voicewindowspeakerlistentry.hpp b/src/windows/voice/voicewindowspeakerlistentry.hpp
new file mode 100644
index 0000000..a3b6429
--- /dev/null
+++ b/src/windows/voice/voicewindowspeakerlistentry.hpp
@@ -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;
+};