Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,18 @@ if (ENABLE_VOICE)

target_link_libraries(abaddon ${CMAKE_DL_LIBS})

# FFmpeg for video support
find_package(PkgConfig QUIET)
pkg_check_modules(FFMPEG QUIET libavcodec libavformat libavdevice libswscale libavutil)
if (FFMPEG_FOUND)
target_compile_definitions(abaddon PRIVATE WITH_VIDEO)
target_include_directories(abaddon PUBLIC ${FFMPEG_INCLUDE_DIRS})
target_link_libraries(abaddon ${FFMPEG_LIBRARIES})
target_link_directories(abaddon PUBLIC ${FFMPEG_LIBRARY_DIRS})
else()
message(WARNING "FFmpeg not found - video calling and screen sharing will be disabled")
endif()

if (ENABLE_RNNOISE)
target_compile_definitions(abaddon PRIVATE WITH_RNNOISE)

Expand Down Expand Up @@ -227,4 +239,3 @@ install(TARGETS abaddon RUNTIME)
install(DIRECTORY res/css DESTINATION ${ABADDON_RESOURCE_DIR})
install(DIRECTORY res/fonts DESTINATION ${ABADDON_RESOURCE_DIR})
install(DIRECTORY res/res DESTINATION ${ABADDON_RESOURCE_DIR})

Empty file added res/emojis.db
Empty file.
50 changes: 49 additions & 1 deletion src/abaddon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "dialogs/friendpicker.hpp"
#include "dialogs/verificationgate.hpp"
#include "dialogs/textinput.hpp"
#include "dialogs/call.hpp"
#include "windows/guildsettingswindow.hpp"
#include "windows/profilewindow.hpp"
#include "windows/pinnedwindow.hpp"
Expand Down Expand Up @@ -85,6 +86,7 @@ Abaddon::Abaddon()
spdlog::get("voice")->debug("{} SSRC: {}", m.UserID, m.SSRC);
m_audio.AddSSRC(m.SSRC);
});
m_discord.signal_call_create().connect(sigc::mem_fun(*this, &Abaddon::ActionCallCreate));
#endif

m_discord.signal_channel_accessibility_changed().connect([this](Snowflake id, bool accessible) {
Expand Down Expand Up @@ -320,6 +322,10 @@ int Abaddon::StartGTK() {
m_main_window->GetChatWindow()->signal_action_reaction_add().connect(sigc::mem_fun(*this, &Abaddon::ActionReactionAdd));
m_main_window->GetChatWindow()->signal_action_reaction_remove().connect(sigc::mem_fun(*this, &Abaddon::ActionReactionRemove));

#ifdef WITH_VOICE
m_main_window->GetChatWindow()->signal_action_start_call().connect(sigc::mem_fun(*this, &Abaddon::ActionStartCall));
#endif

ActionReloadCSS();
AttachCSSMonitor();

Expand Down Expand Up @@ -494,8 +500,17 @@ void Abaddon::OnVoiceConnected() {
void Abaddon::OnVoiceDisconnected() {
m_audio.StopCaptureDevice();
m_audio.RemoveAllSSRCs();

// Don't close the voice window if we're still in a voice channel
// This handles the case where we reconnect (e.g., for screen share mode change)
// The window should only close when we actually leave the channel
if (m_voice_window != nullptr) {
m_voice_window->close();
const auto channel_id = m_discord.GetVoiceChannelID();
if (!channel_id.IsValid() || static_cast<uint64_t>(channel_id) == 0) {
// We're not in a channel anymore, close the window
m_voice_window->close();
}
// Otherwise, we're still in a channel, just reconnecting - keep the window open
}
}

Expand Down Expand Up @@ -863,6 +878,7 @@ void Abaddon::ActionSetToken() {
m_discord.UpdateToken(m_discord_token);
m_main_window->UpdateComponents();
GetSettings().DiscordToken = m_discord_token;
m_settings.Save(); // Save immediately so token persists
}
m_main_window->UpdateMenus();
}
Expand Down Expand Up @@ -1092,6 +1108,35 @@ void Abaddon::ActionJoinVoiceChannel(Snowflake channel_id) {
void Abaddon::ActionDisconnectVoice() {
m_discord.DisconnectFromVoice();
}

void Abaddon::ActionCallCreate(CallCreateData data) {
const auto channel = m_discord.GetChannel(data.ChannelID);
if (!channel.has_value()) return;

bool is_group_call = (channel->Type == ChannelType::GROUP_DM);

CallDialog dlg(*m_main_window, data.ChannelID, is_group_call);
const auto response = dlg.run();

if (response == Gtk::RESPONSE_OK && dlg.GetAccepted()) {
if (is_group_call) {
m_discord.JoinCall(data.ChannelID);
} else {
m_discord.AcceptCall(data.ChannelID);
}
} else {
m_discord.RejectCall(data.ChannelID);
}
}

void Abaddon::ActionStartCall(Snowflake channel_id) {
const auto channel = m_discord.GetChannel(channel_id);
if (!channel.has_value()) return;

if (channel->Type == ChannelType::DM || channel->Type == ChannelType::GROUP_DM) {
m_discord.StartCall(channel_id);
}
}
#endif

std::optional<Glib::ustring> Abaddon::ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder, Gtk::Window *window) {
Expand Down Expand Up @@ -1190,6 +1235,9 @@ int main(int argc, char **argv) {
auto log_voice = spdlog::stdout_color_mt("voice");
auto log_discord = spdlog::stdout_color_mt("discord");
auto log_ra = spdlog::stdout_color_mt("remote-auth");
#ifdef WITH_VIDEO
auto log_video = spdlog::stdout_color_mt("video");
#endif

Gtk::Main::init_gtkmm_internals(); // why???
return Abaddon::Get().StartGTK();
Expand Down
2 changes: 2 additions & 0 deletions src/abaddon.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ class Abaddon {
#ifdef WITH_VOICE
void ActionJoinVoiceChannel(Snowflake channel_id);
void ActionDisconnectVoice();
void ActionCallCreate(CallCreateData data);
void ActionStartCall(Snowflake channel_id);
#endif

std::optional<Glib::ustring> ShowTextPrompt(const Glib::ustring &prompt, const Glib::ustring &title, const Glib::ustring &placeholder = "", Gtk::Window *window = nullptr);
Expand Down
12 changes: 7 additions & 5 deletions src/components/channellist/channellisttree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ ChannelListTree::ChannelListTree()
, m_menu_dm_copy_id("_Copy ID", true)
, m_menu_dm_close("") // changes depending on if group or not
#ifdef WITH_VOICE
, m_menu_dm_join_voice("Join _Voice", true)
, m_menu_dm_join_voice("Start _Call", true)
, m_menu_dm_disconnect_voice("_Disconnect Voice", true)
#endif
, m_menu_thread_copy_id("_Copy ID", true)
Expand Down Expand Up @@ -943,10 +943,12 @@ Gtk::TreeModel::iterator ChannelListTree::AddGuild(const GuildData &guild, const
}

std::map<Snowflake, std::vector<ChannelData>> threads;
for (const auto &tmp : *guild.Threads) {
const auto thread = discord.GetChannel(tmp.ID);
if (thread.has_value())
threads[*thread->ParentID].push_back(*thread);
if (guild.Threads.has_value()) {
for (const auto &tmp : *guild.Threads) {
const auto thread = discord.GetChannel(tmp.ID);
if (thread.has_value())
threads[*thread->ParentID].push_back(*thread);
}
}
const auto add_threads = [&](const ChannelData &channel, const Gtk::TreeRow &row) {
row[m_columns.m_expanded] = true;
Expand Down
35 changes: 35 additions & 0 deletions src/components/chatwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,22 @@ ChatWindow::ChatWindow() {
m_topic_text.set_halign(Gtk::ALIGN_START);
m_topic_text.show();

#ifdef WITH_VOICE
m_call_button.set_label("Start Call");
m_call_button.set_tooltip_text("Start a voice call");
m_call_button.set_halign(Gtk::ALIGN_START);
m_call_button.set_margin_start(5);
m_call_button.set_margin_end(5);
m_call_button.set_margin_top(2);
m_call_button.set_margin_bottom(2);
m_call_button.signal_clicked().connect([this]() {
if (m_active_channel.IsValid()) {
m_signal_action_start_call.emit(m_active_channel);
}
});
m_call_button.hide();
#endif

m_input->set_valign(Gtk::ALIGN_END);

m_input->signal_submit().connect(sigc::mem_fun(*this, &ChatWindow::OnInputSubmit));
Expand Down Expand Up @@ -104,6 +120,9 @@ ChatWindow::ChatWindow() {
m_tab_switcher->show();
#endif
m_main->add(m_topic);
#ifdef WITH_VOICE
m_main->add(m_call_button);
#endif
m_main->add(*m_chat);
m_main->add(m_completer);
m_main->add(*m_input);
Expand Down Expand Up @@ -142,6 +161,16 @@ void ChatWindow::SetActiveChannel(Snowflake id) {
if (m_is_replying) StopReplying();
if (m_is_editing) StopEditing();

#ifdef WITH_VOICE
const auto &discord = Abaddon::Get().GetDiscordClient();
const auto channel = discord.GetChannel(id);
if (channel.has_value() && (channel->Type == ChannelType::DM || channel->Type == ChannelType::GROUP_DM)) {
m_call_button.show();
} else {
m_call_button.hide();
}
#endif

#ifdef WITH_LIBHANDY
m_tab_switcher->ReplaceActiveTab(id);
#endif
Expand Down Expand Up @@ -396,3 +425,9 @@ ChatWindow::type_signal_action_reaction_add ChatWindow::signal_action_reaction_a
ChatWindow::type_signal_action_reaction_remove ChatWindow::signal_action_reaction_remove() {
return m_signal_action_reaction_remove;
}

#ifdef WITH_VOICE
ChatWindow::type_signal_action_start_call ChatWindow::signal_action_start_call() {
return m_signal_action_start_call;
}
#endif
13 changes: 13 additions & 0 deletions src/components/chatwindow.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ class ChatWindow {
Gtk::EventBox m_topic; // todo probably make everything else go on the stack
Gtk::Label m_topic_text;

#ifdef WITH_VOICE
Gtk::Button m_call_button;
#endif

ChatList *m_chat;

ChatInput *m_input;
Expand Down Expand Up @@ -110,6 +114,11 @@ class ChatWindow {
type_signal_action_reaction_add signal_action_reaction_add();
type_signal_action_reaction_remove signal_action_reaction_remove();

#ifdef WITH_VOICE
using type_signal_action_start_call = sigc::signal<void, Snowflake>;
type_signal_action_start_call signal_action_start_call();
#endif

private:
type_signal_action_message_edit m_signal_action_message_edit;
type_signal_action_chat_submit m_signal_action_chat_submit;
Expand All @@ -118,4 +127,8 @@ class ChatWindow {
type_signal_action_insert_mention m_signal_action_insert_mention;
type_signal_action_reaction_add m_signal_action_reaction_add;
type_signal_action_reaction_remove m_signal_action_reaction_remove;

#ifdef WITH_VOICE
type_signal_action_start_call m_signal_action_start_call;
#endif
};
110 changes: 110 additions & 0 deletions src/dialogs/call.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#include "call.hpp"
#include "abaddon.hpp"

CallDialog::CallDialog(Gtk::Window &parent, Snowflake channel_id, bool is_group_call)
: Gtk::Dialog(is_group_call ? "Incoming Group Call" : "Incoming Call", parent, true)
, m_channel_id(channel_id)
, m_is_group_call(is_group_call)
, m_main_layout(Gtk::ORIENTATION_VERTICAL)
, m_info_layout(Gtk::ORIENTATION_HORIZONTAL)
, m_button_box(Gtk::ORIENTATION_HORIZONTAL)
, m_accept_button(is_group_call ? "Join" : "Accept")
, m_reject_button("Reject") {
set_default_size(350, 150);
get_style_context()->add_class("app-window");
get_style_context()->add_class("app-popup");

const auto &discord = Abaddon::Get().GetDiscordClient();
const auto channel = discord.GetChannel(channel_id);

if (!channel.has_value()) {
m_title_label.set_text("Unknown Call");
m_info_label.set_text("Channel information unavailable");
} else if (m_is_group_call) {
m_title_label.set_markup("<b>Group Call</b>");
m_info_label.set_text(channel->GetRecipientsDisplay());
m_avatar.property_pixbuf() = Abaddon::Get().GetImageManager().GetPlaceholder(64);
} else {
const auto recipients = channel->GetDMRecipients();
if (recipients.empty()) {
m_title_label.set_text("Unknown Call");
m_info_label.set_text("User information unavailable");
m_avatar.property_pixbuf() = Abaddon::Get().GetImageManager().GetPlaceholder(64);
} else {
const auto &user = recipients[0];
m_title_label.set_markup("<b>" + Glib::Markup::escape_text(user.GetDisplayName()) + "</b>");
m_info_label.set_text("is calling you");

auto &img = Abaddon::Get().GetImageManager();
m_avatar.property_pixbuf() = img.GetPlaceholder(64);

if (user.HasAnimatedAvatar() && Abaddon::Get().GetSettings().ShowAnimations) {
auto cb = [this](const Glib::RefPtr<Gdk::PixbufAnimation> &pb) {
m_avatar.property_pixbuf_animation() = pb;
};
img.LoadAnimationFromURL(user.GetAvatarURL("gif", "64"), 64, 64, sigc::track_obj(cb, *this));
} else {
auto cb = [this](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
m_avatar.property_pixbuf() = pb->scale_simple(64, 64, Gdk::INTERP_BILINEAR);
};
img.LoadFromURL(user.GetAvatarURL("png", "64"), sigc::track_obj(cb, *this));
}
}
}

m_avatar.set_margin_end(10);
m_avatar.set_halign(Gtk::ALIGN_START);
m_avatar.set_valign(Gtk::ALIGN_CENTER);

m_title_label.set_halign(Gtk::ALIGN_START);
m_title_label.set_valign(Gtk::ALIGN_CENTER);
m_info_label.set_halign(Gtk::ALIGN_START);
m_info_label.set_valign(Gtk::ALIGN_CENTER);
m_info_label.set_single_line_mode(true);
m_info_label.set_ellipsize(Pango::ELLIPSIZE_END);

m_accept_button.signal_clicked().connect(sigc::mem_fun(*this, &CallDialog::OnAccept));
m_reject_button.signal_clicked().connect(sigc::mem_fun(*this, &CallDialog::OnReject));

m_button_box.pack_start(m_accept_button, Gtk::PACK_SHRINK);
m_button_box.pack_start(m_reject_button, Gtk::PACK_SHRINK);
m_button_box.set_layout(Gtk::BUTTONBOX_END);
m_button_box.set_margin_top(10);

Gtk::Box *title_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
title_box->add(m_title_label);
title_box->add(m_info_label);
title_box->set_halign(Gtk::ALIGN_START);
title_box->set_valign(Gtk::ALIGN_CENTER);

m_info_layout.add(m_avatar);
m_info_layout.add(*title_box);
m_info_layout.set_margin_bottom(10);

m_main_layout.add(m_info_layout);
m_main_layout.add(m_button_box);

get_content_area()->add(m_main_layout);

signal_response().connect([this](int response_id) {
if (response_id != Gtk::RESPONSE_OK) {
m_accepted = false;
}
});

show_all_children();
}

bool CallDialog::GetAccepted() const {
return m_accepted;
}

void CallDialog::OnAccept() {
m_accepted = true;
response(Gtk::RESPONSE_OK);
}

void CallDialog::OnReject() {
m_accepted = false;
response(Gtk::RESPONSE_CANCEL);
}
35 changes: 35 additions & 0 deletions src/dialogs/call.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once

#include <gtkmm/dialog.h>
#include <gtkmm/label.h>
#include <gtkmm/image.h>
#include <gtkmm/box.h>
#include <gtkmm/buttonbox.h>
#include <gtkmm/button.h>
#include "discord/snowflake.hpp"
#include "discord/objects.hpp"
#include "discord/channel.hpp"

class CallDialog : public Gtk::Dialog {
public:
CallDialog(Gtk::Window &parent, Snowflake channel_id, bool is_group_call);

bool GetAccepted() const;

protected:
void OnAccept();
void OnReject();

bool m_accepted = false;
Snowflake m_channel_id;
bool m_is_group_call;

Gtk::Box m_main_layout;
Gtk::Box m_info_layout;
Gtk::Image m_avatar;
Gtk::Label m_title_label;
Gtk::Label m_info_label;
Gtk::ButtonBox m_button_box;
Gtk::Button m_accept_button;
Gtk::Button m_reject_button;
};
Loading