mirror of
https://github.com/uowuo/abaddon.git
synced 2025-03-04 03:03:16 -05:00
commit
68db143c89
@ -19,7 +19,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"
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
@ -1331,6 +1357,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]));
|
||||
@ -1442,6 +1472,25 @@ 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);
|
||||
}
|
||||
#else
|
||||
m_menu_voice_stage_join.set_sensitive(false);
|
||||
m_menu_voice_stage_disconnect.set_sensitive(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ChannelListTree::OnDMSubmenuPopup() {
|
||||
auto iter = m_model->get_iter(m_path_for_menu);
|
||||
if (!iter) return;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
}
|
||||
@ -462,6 +470,10 @@ bool DiscordClient::CanManageMember(Snowflake guild_id, Snowflake actor, Snowfla
|
||||
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) {
|
||||
if (!CheckCode(response)) {
|
||||
if (response.status_code == http::TooManyRequests) {
|
||||
@ -1288,6 +1300,78 @@ 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();
|
||||
}
|
||||
|
||||
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() {
|
||||
return m_voice;
|
||||
}
|
||||
@ -1303,7 +1387,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 +1736,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 +1805,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 +2403,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 +2523,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 +3083,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 +3148,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 +3329,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 +3418,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;
|
||||
}
|
@ -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>
|
||||
@ -86,6 +87,7 @@ public:
|
||||
Permission ComputePermissions(Snowflake member_id, Snowflake guild_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 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 SendChatMessageNoAttachments(const ChatSubmitParams ¶ms, const sigc::slot<void(DiscordError code)> &callback);
|
||||
@ -201,6 +203,13 @@ 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;
|
||||
[[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();
|
||||
|
||||
@ -208,7 +217,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 +304,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 +357,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 +402,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 +460,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 +494,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 +537,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 +567,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 +611,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 +641,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;
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -699,6 +699,18 @@ void from_json(const nlohmann::json &j, CallCreateData &m) {
|
||||
JS_D("channel_id", m.ChannelID);
|
||||
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
|
||||
|
||||
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_ON("member", m.Member);
|
||||
JS_D("session_id", m.SessionID);
|
||||
JS_ON("request_to_speak_timestamp", m.RequestToSpeakTimestamp);
|
||||
}
|
||||
|
@ -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);
|
||||
};
|
||||
@ -952,4 +957,12 @@ struct CallCreateData {
|
||||
|
||||
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
|
||||
|
12
src/discord/stage.cpp
Normal file
12
src/discord/stage.cpp
Normal 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
32
src/discord/stage.hpp
Normal 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);
|
||||
};
|
@ -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:
|
||||
|
@ -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;
|
||||
|
5
src/discord/voicestate.cpp
Normal file
5
src/discord/voicestate.cpp
Normal file
@ -0,0 +1,5 @@
|
||||
#include "voicestate.hpp"
|
||||
|
||||
bool PackedVoiceState::IsSpeaker() const noexcept {
|
||||
return ((Flags & VoiceStateFlags::Suppressed) != VoiceStateFlags::Suppressed) && !RequestToSpeakTimestamp.has_value();
|
||||
}
|
@ -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<>
|
@ -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;
|
||||
|
@ -1,89 +1,19 @@
|
||||
#include "util.hpp"
|
||||
#ifdef WITH_VOICE
|
||||
|
||||
// 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)
|
||||
@ -91,7 +21,11 @@ VoiceWindow::VoiceWindow(Snowflake channel_id)
|
||||
, m_deafen("Deafen")
|
||||
, m_noise_suppression("Suppress Noise")
|
||||
, m_mix_mono("Mix Mono")
|
||||
, m_stage_command("Request to Speak")
|
||||
, 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_menu_view("View")
|
||||
, m_menu_view_settings("More _Settings", true) {
|
||||
@ -102,14 +36,19 @@ 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));
|
||||
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()) {
|
||||
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,35 +192,97 @@ 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()) {
|
||||
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_deafen);
|
||||
m_controls.add(m_noise_suppression);
|
||||
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_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(*Gtk::make_managed<Gtk::Label>("Input Settings"), false, true);
|
||||
m_main.pack_start(*sliders_container, false, true);
|
||||
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);
|
||||
add(m_main);
|
||||
show_all_children();
|
||||
|
||||
Glib::signal_timeout().connect(sigc::mem_fun(*this, &VoiceWindow::UpdateVoiceMeters), 40);
|
||||
|
||||
UpdateStageCommand();
|
||||
}
|
||||
|
||||
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 (!m_is_stage || 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 +292,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 +311,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 +334,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;
|
||||
@ -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) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
return m_signal_mute;
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
#pragma once
|
||||
#include "discord/stage.hpp"
|
||||
#include "discord/voicestate.hpp"
|
||||
#ifdef WITH_VOICE
|
||||
// clang-format off
|
||||
|
||||
@ -16,7 +18,6 @@
|
||||
#include <unordered_set>
|
||||
// clang-format on
|
||||
|
||||
class VoiceWindowUserListEntry;
|
||||
class VoiceWindow : public Gtk::Window {
|
||||
public:
|
||||
VoiceWindow(Snowflake channel_id);
|
||||
@ -24,17 +25,25 @@ 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 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 OnDeafenChanged();
|
||||
|
||||
void TryDeleteRow(Snowflake id);
|
||||
bool UpdateVoiceMeters();
|
||||
|
||||
void UpdateVADParamValue();
|
||||
void UpdateStageCommand();
|
||||
void UpdateStageTopicLabel(const std::string &topic);
|
||||
|
||||
Gtk::Box m_main;
|
||||
Gtk::Box m_controls;
|
||||
@ -43,7 +52,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
|
||||
@ -56,21 +67,36 @@ private:
|
||||
Gtk::CheckButton m_noise_suppression;
|
||||
Gtk::CheckButton m_mix_mono;
|
||||
|
||||
Gtk::HBox m_buttons;
|
||||
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::ComboBox m_playback_combo;
|
||||
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_stage_topic_label;
|
||||
Gtk::Label m_speakers_label;
|
||||
Gtk::Label m_audience_label;
|
||||
|
||||
public:
|
||||
using type_signal_mute = sigc::signal<void(bool)>;
|
||||
using type_signal_deafen = sigc::signal<void(bool)>;
|
23
src/windows/voice/voicewindowaudiencelistentry.cpp
Normal file
23
src/windows/voice/voicewindowaudiencelistentry.cpp
Normal 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");
|
||||
}
|
||||
}
|
18
src/windows/voice/voicewindowaudiencelistentry.hpp
Normal file
18
src/windows/voice/voicewindowaudiencelistentry.hpp
Normal 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;
|
||||
};
|
58
src/windows/voice/voicewindowspeakerlistentry.cpp
Normal file
58
src/windows/voice/voicewindowspeakerlistentry.cpp
Normal 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;
|
||||
}
|
38
src/windows/voice/voicewindowspeakerlistentry.hpp
Normal file
38
src/windows/voice/voicewindowspeakerlistentry.hpp
Normal 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;
|
||||
};
|
Loading…
Reference in New Issue
Block a user