view user profile (notes+connections+badges)

change some stuff with callbacks
This commit is contained in:
ouwou 2021-02-04 23:41:53 -05:00
parent 0479bf52c2
commit 64adcffe42
40 changed files with 686 additions and 37 deletions

View File

@ -44,6 +44,8 @@ file(GLOB ABADDON_SOURCES
"windows/*.cpp"
"windows/guildsettings/*.hpp"
"windows/guildsettings/*.cpp"
"windows/profile/*.hpp"
"windows/profile/*.cpp"
"dialogs/*.hpp"
"dialogs/*.cpp"
)

View File

@ -10,6 +10,7 @@
#include "dialogs/setstatus.hpp"
#include "abaddon.hpp"
#include "windows/guildsettingswindow.hpp"
#include "windows/profilewindow.hpp"
#ifdef _WIN32
#pragma comment(lib, "crypt32.lib")
@ -84,6 +85,7 @@ int Abaddon::StartGTK() {
m_user_menu_copy_id = Gtk::manage(new Gtk::MenuItem("Copy ID"));
m_user_menu_open_dm = Gtk::manage(new Gtk::MenuItem("Open DM"));
m_user_menu_roles = Gtk::manage(new Gtk::MenuItem("Roles"));
m_user_menu_info = Gtk::manage(new Gtk::MenuItem("View Profile"));
m_user_menu_roles_submenu = Gtk::manage(new Gtk::Menu);
m_user_menu_roles->set_submenu(*m_user_menu_roles_submenu);
m_user_menu_insert_mention->signal_activate().connect(sigc::mem_fun(*this, &Abaddon::on_user_menu_insert_mention));
@ -91,6 +93,11 @@ int Abaddon::StartGTK() {
m_user_menu_kick->signal_activate().connect(sigc::mem_fun(*this, &Abaddon::on_user_menu_kick));
m_user_menu_copy_id->signal_activate().connect(sigc::mem_fun(*this, &Abaddon::on_user_menu_copy_id));
m_user_menu_open_dm->signal_activate().connect(sigc::mem_fun(*this, &Abaddon::on_user_menu_open_dm));
m_user_menu_info->signal_activate().connect([this]() {
auto *window = new ProfileWindow(m_shown_user_menu_id);
window->show();
});
m_user_menu->append(*m_user_menu_info);
m_user_menu->append(*m_user_menu_insert_mention);
m_user_menu->append(*m_user_menu_ban);
m_user_menu->append(*m_user_menu_kick);
@ -503,7 +510,7 @@ void Abaddon::ActionReactionRemove(Snowflake id, const Glib::ustring &param) {
}
void Abaddon::ActionGuildSettings(Snowflake id) {
auto *window = new GuildSettingsWindow(id);
auto window = new GuildSettingsWindow(id);
window->show();
}

View File

@ -83,6 +83,7 @@ protected:
void ShowUserMenu(const GdkEvent *event, Snowflake id, Snowflake guild_id);
Gtk::Menu *m_user_menu;
Gtk::MenuItem *m_user_menu_info;
Gtk::MenuItem *m_user_menu_insert_mention;
Gtk::MenuItem *m_user_menu_ban;
Gtk::MenuItem *m_user_menu_kick;

View File

@ -233,3 +233,57 @@
.status-indicator.idle {
color: #FAA61A;
}
.profile-main-container {
padding: 20px;
}
.profile-username {
margin-left: 10px;
font-size: 20px;
}
.profile-badge {
margin-right: 10px;
}
.profile-switcher {
padding-top: 5px;
}
.profile-connections {
margin-top: 10px;
}
.profile-connection {
background: @secondary_color;
border-radius: 15px;
margin-right: 20px;
}
.profile-connection box {
padding: 5px;
}
.profile-stack {
padding-top: 5px;
}
.profile-notes-text, .profile-notes-text text {
background: @secondary_color;
}
.profile-notes-text text {
border-radius: 5px;
border: 1px solid #36515e;
color: @text_color;
padding-bottom: 5px;
}
.profile-badges {
padding-left: 5px;
}
.app-window textview text {
caret-color: #ababab;
}

View File

@ -112,8 +112,6 @@ std::set<Snowflake> DiscordClient::GetMessagesForChannel(Snowflake id) const {
}
void DiscordClient::FetchInvite(std::string code, sigc::slot<void(std::optional<InviteData>)> callback) {
sigc::signal<void, std::optional<InviteData>> signal;
signal.connect(callback);
m_http.MakeGET("/invites/" + code + "?with_counts=true", [this, callback](http::response_type r) {
if (!CheckCode(r)) {
if (r.status_code == 404)
@ -371,18 +369,16 @@ void DiscordClient::EditMessage(Snowflake channel_id, Snowflake id, std::string
void DiscordClient::SendLazyLoad(Snowflake id) {
LazyLoadRequestMessage msg;
std::unordered_map<Snowflake, std::vector<std::pair<int, int>>> c;
c[id] = {
msg.Channels.emplace();
msg.Channels.value()[id] = {
std::make_pair(0, 99),
std::make_pair(100, 199)
};
msg.Channels = c;
msg.GuildID = *GetChannel(id)->GuildID;
msg.ShouldGetActivities = true;
msg.ShouldGetTyping = true;
nlohmann::json j = msg;
m_websocket.Send(j);
m_websocket.Send(msg);
}
void DiscordClient::JoinGuild(std::string code) {
@ -477,11 +473,9 @@ void DiscordClient::SetGuildName(Snowflake id, const Glib::ustring &name) {
void DiscordClient::SetGuildName(Snowflake id, const Glib::ustring &name, sigc::slot<void(bool success)> callback) {
ModifyGuildObject obj;
obj.Name = name;
sigc::signal<void, bool> signal;
signal.connect(callback);
m_http.MakePATCH("/guilds/" + std::to_string(id), nlohmann::json(obj).dump(), [this, signal](const http::response_type &r) {
m_http.MakePATCH("/guilds/" + std::to_string(id), nlohmann::json(obj).dump(), [this, callback](const http::response_type &r) {
const auto success = r.status_code == 200;
signal.emit(success);
callback(success);
});
}
@ -492,11 +486,9 @@ void DiscordClient::SetGuildIcon(Snowflake id, const std::string &data) {
void DiscordClient::SetGuildIcon(Snowflake id, const std::string &data, sigc::slot<void(bool success)> callback) {
ModifyGuildObject obj;
obj.IconData = data;
sigc::signal<void, bool> signal;
signal.connect(callback);
m_http.MakePATCH("/guilds/" + std::to_string(id), nlohmann::json(obj).dump(), [this, signal](const http::response_type &r) {
m_http.MakePATCH("/guilds/" + std::to_string(id), nlohmann::json(obj).dump(), [this, callback](const http::response_type &r) {
const auto success = r.status_code == 200;
signal.emit(success);
callback(success);
});
}
@ -505,8 +497,6 @@ void DiscordClient::UnbanUser(Snowflake guild_id, Snowflake user_id) {
}
void DiscordClient::UnbanUser(Snowflake guild_id, Snowflake user_id, sigc::slot<void(bool success)> callback) {
sigc::signal<void, bool> signal;
signal.connect(callback);
m_http.MakeDELETE("/guilds/" + std::to_string(guild_id) + "/bans/" + std::to_string(user_id), [this, callback](const http::response_type &response) {
callback(response.status_code == 204);
});
@ -517,8 +507,6 @@ void DiscordClient::DeleteInvite(const std::string &code) {
}
void DiscordClient::DeleteInvite(const std::string &code, sigc::slot<void(bool success)> callback) {
sigc::signal<void, bool> signal;
signal.connect(callback);
m_http.MakeDELETE("/invites/" + code, [this, callback](const http::response_type &response) {
callback(CheckCode(response));
});
@ -529,8 +517,6 @@ std::vector<BanData> DiscordClient::GetBansInGuild(Snowflake guild_id) {
}
void DiscordClient::FetchGuildBan(Snowflake guild_id, Snowflake user_id, sigc::slot<void(BanData)> callback) {
sigc::signal<void, BanData> signal;
signal.connect(callback);
m_http.MakeGET("/guilds/" + std::to_string(guild_id) + "/bans/" + std::to_string(user_id), [this, callback, guild_id](const http::response_type &response) {
if (!CheckCode(response)) return;
auto ban = nlohmann::json::parse(response.text).get<BanData>();
@ -541,8 +527,6 @@ void DiscordClient::FetchGuildBan(Snowflake guild_id, Snowflake user_id, sigc::s
}
void DiscordClient::FetchGuildBans(Snowflake guild_id, sigc::slot<void(std::vector<BanData>)> callback) {
sigc::signal<void, std::vector<BanData>> signal;
signal.connect(callback);
m_http.MakeGET("/guilds/" + std::to_string(guild_id) + "/bans", [this, callback, guild_id](const http::response_type &response) {
if (!CheckCode(response)) return;
auto bans = nlohmann::json::parse(response.text).get<std::vector<BanData>>();
@ -557,8 +541,6 @@ void DiscordClient::FetchGuildBans(Snowflake guild_id, sigc::slot<void(std::vect
}
void DiscordClient::FetchGuildInvites(Snowflake guild_id, sigc::slot<void(std::vector<InviteData>)> callback) {
sigc::signal<void, std::vector<InviteData>> signal;
signal.connect(callback);
m_http.MakeGET("/guilds/" + std::to_string(guild_id) + "/invites", [this, callback, guild_id](const http::response_type &response) {
// store?
if (!CheckCode(response)) return;
@ -575,8 +557,6 @@ void DiscordClient::FetchGuildInvites(Snowflake guild_id, sigc::slot<void(std::v
}
void DiscordClient::FetchAuditLog(Snowflake guild_id, sigc::slot<void(AuditLogData)> callback) {
sigc::signal<void, AuditLogData> signal;
signal.connect(callback);
m_http.MakeGET("/guilds/" + std::to_string(guild_id) + "/audit-logs", [this, callback](const http::response &response) {
if (!CheckCode(response)) return;
auto data = nlohmann::json::parse(response.text).get<AuditLogData>();
@ -590,6 +570,35 @@ void DiscordClient::FetchAuditLog(Snowflake guild_id, sigc::slot<void(AuditLogDa
});
}
void DiscordClient::FetchUserProfile(Snowflake user_id, sigc::slot<void(UserProfileData)> callback) {
m_http.MakeGET("/users/" + std::to_string(user_id) + "/profile", [this, callback](const http::response_type &response) {
if (!CheckCode(response)) return;
callback(nlohmann::json::parse(response.text).get<UserProfileData>());
});
}
void DiscordClient::FetchUserNote(Snowflake user_id, sigc::slot<void(std::string note)> callback) {
m_http.MakeGET("/users/@me/notes/" + std::to_string(user_id), [this, callback](const http::response_type &response) {
if (response.status_code == 404) return;
if (!CheckCode(response)) return;
const auto note = nlohmann::json::parse(response.text).get<UserNoteObject>().Note;
if (note.has_value())
callback(*note);
});
}
void DiscordClient::SetUserNote(Snowflake user_id, std::string note) {
SetUserNote(user_id, note, [](auto) {});
}
void DiscordClient::SetUserNote(Snowflake user_id, std::string note, sigc::slot<void(bool success)> callback) {
UserSetNoteObject obj;
obj.Note = note;
m_http.MakePUT("/users/@me/notes/" + std::to_string(user_id), nlohmann::json(obj).dump(), [this, callback](const http::response_type &response) {
callback(response.status_code == 204);
});
}
void DiscordClient::UpdateToken(std::string token) {
if (!IsStarted()) {
m_token = token;
@ -769,6 +778,9 @@ void DiscordClient::HandleGatewayMessage(std::string str) {
case GatewayEvent::INVITE_DELETE: {
HandleGatewayInviteDelete(m);
} break;
case GatewayEvent::USER_NOTE_UPDATE: {
HandleGatewayUserNoteUpdate(m);
} break;
}
} break;
default:
@ -1147,6 +1159,11 @@ void DiscordClient::HandleGatewayInviteDelete(const GatewayMessage &msg) {
m_signal_invite_delete.emit(data);
}
void DiscordClient::HandleGatewayUserNoteUpdate(const GatewayMessage &msg) {
UserNoteUpdateMessage data = msg.Data;
m_signal_note_update.emit(data.ID, data.Note);
}
void DiscordClient::HandleGatewayReconnect(const GatewayMessage &msg) {
printf("received reconnect\n");
m_signal_disconnected.emit(true, GatewayCloseCode::Reconnecting);
@ -1427,6 +1444,7 @@ void DiscordClient::LoadEventMap() {
m_event_map["GUILD_BAN_ADD"] = GatewayEvent::GUILD_BAN_ADD;
m_event_map["INVITE_CREATE"] = GatewayEvent::INVITE_CREATE;
m_event_map["INVITE_DELETE"] = GatewayEvent::INVITE_DELETE;
m_event_map["USER_NOTE_UPDATE"] = GatewayEvent::USER_NOTE_UPDATE;
}
DiscordClient::type_signal_gateway_ready DiscordClient::signal_gateway_ready() {
@ -1528,3 +1546,7 @@ DiscordClient::type_signal_invite_delete DiscordClient::signal_invite_delete() {
DiscordClient::type_signal_presence_update DiscordClient::signal_presence_update() {
return m_signal_presence_update;
}
DiscordClient::type_signal_note_update DiscordClient::signal_note_update() {
return m_signal_note_update;
}

View File

@ -130,6 +130,11 @@ public:
void FetchAuditLog(Snowflake guild_id, sigc::slot<void(AuditLogData)> callback);
void FetchUserProfile(Snowflake user_id, sigc::slot<void(UserProfileData)> callback);
void FetchUserNote(Snowflake user_id, sigc::slot<void(std::string note)> callback);
void SetUserNote(Snowflake user_id, std::string note);
void SetUserNote(Snowflake user_id, std::string note, sigc::slot<void(bool success)> callback);
void UpdateToken(std::string token);
void SetUserAgent(std::string agent);
@ -172,6 +177,7 @@ private:
void HandleGatewayGuildBanAdd(const GatewayMessage &msg);
void HandleGatewayInviteCreate(const GatewayMessage &msg);
void HandleGatewayInviteDelete(const GatewayMessage &msg);
void HandleGatewayUserNoteUpdate(const GatewayMessage &msg);
void HandleGatewayReconnect(const GatewayMessage &msg);
void HandleGatewayInvalidSession(const GatewayMessage &msg);
void HeartbeatThread();
@ -252,6 +258,7 @@ public:
typedef sigc::signal<void, InviteData> type_signal_invite_create;
typedef sigc::signal<void, InviteDeleteObject> type_signal_invite_delete;
typedef sigc::signal<void, Snowflake, PresenceStatus> type_signal_presence_update;
typedef sigc::signal<void, Snowflake, std::string> type_signal_note_update;
typedef sigc::signal<void, bool, GatewayCloseCode> type_signal_disconnected; // bool true if reconnecting
typedef sigc::signal<void> type_signal_connected;
@ -278,6 +285,7 @@ public:
type_signal_invite_create signal_invite_create();
type_signal_invite_delete signal_invite_delete(); // safe to assume guild id is set
type_signal_presence_update signal_presence_update();
type_signal_note_update signal_note_update();
type_signal_disconnected signal_disconnected();
type_signal_connected signal_connected();
@ -305,6 +313,7 @@ protected:
type_signal_invite_create m_signal_invite_create;
type_signal_invite_delete m_signal_invite_delete;
type_signal_presence_update m_signal_presence_update;
type_signal_note_update m_signal_note_update;
type_signal_disconnected m_signal_disconnected;
type_signal_connected m_signal_connected;
};

View File

@ -74,14 +74,15 @@ void to_json(nlohmann::json &j, const LazyLoadRequestMessage &m) {
j["op"] = GatewayOp::LazyLoadRequest;
j["d"] = nlohmann::json::object();
j["d"]["guild_id"] = m.GuildID;
j["d"]["channels"] = nlohmann::json::object();
for (const auto &[key, chans] : m.Channels) { // apparently a map gets written as a list
j["d"]["channels"][std::to_string(key)] = chans;
if (m.Channels.has_value()) {
j["d"]["channels"] = nlohmann::json::object();
for (const auto &[key, chans] : *m.Channels)
j["d"]["channels"][std::to_string(key)] = chans;
}
j["d"]["typing"] = m.ShouldGetTyping;
j["d"]["activities"] = m.ShouldGetActivities;
if (m.Members.size() > 0)
j["d"]["members"] = m.Members;
if (m.Members.has_value())
j["d"]["members"] = *m.Members;
}
void to_json(nlohmann::json &j, const UpdateStatusMessage &m) {
@ -301,3 +302,38 @@ void from_json(const nlohmann::json &j, InviteDeleteObject &m) {
JS_O("guild_id", m.GuildID);
JS_D("code", m.Code);
}
void from_json(const nlohmann::json &j, ConnectionData &m) {
JS_D("id", m.ID);
JS_D("type", m.Type);
JS_D("name", m.Name);
JS_D("verified", m.IsVerified);
}
void from_json(const nlohmann::json &j, MutualGuildData &m) {
JS_D("id", m.ID);
JS_ON("nick", m.Nick);
}
void from_json(const nlohmann::json &j, UserProfileData &m) {
JS_D("connected_accounts", m.ConnectedAccounts);
JS_D("mutual_guilds", m.MutualGuilds);
JS_ON("premium_guild_since", m.PremiumGuildSince);
JS_ON("premium_since", m.PremiumSince);
JS_D("user", m.User);
}
void from_json(const nlohmann::json &j, UserNoteObject &m) {
JS_ON("note", m.Note);
JS_ON("note_user_id", m.NoteUserID);
JS_ON("user_id", m.UserID);
}
void to_json(nlohmann::json &j, UserSetNoteObject &m) {
j["note"] = m.Note;
}
void from_json(const nlohmann::json &j, UserNoteUpdateMessage &m) {
JS_D("note", m.Note);
JS_D("id", m.ID);
}

View File

@ -61,6 +61,7 @@ enum class GatewayEvent : int {
GUILD_BAN_ADD,
INVITE_CREATE,
INVITE_DELETE,
USER_NOTE_UPDATE,
};
enum class GatewayCloseCode : uint16_t {
@ -170,8 +171,8 @@ struct LazyLoadRequestMessage {
Snowflake GuildID;
bool ShouldGetTyping = false;
bool ShouldGetActivities = false;
std::vector<std::string> Members; // snowflake?
std::unordered_map<Snowflake, std::vector<std::pair<int, int>>> Channels; // channel ID -> range of sidebar
std::optional<std::vector<std::string>> Members; // snowflake?
std::optional<std::unordered_map<Snowflake, std::vector<std::pair<int, int>>>> Channels; // channel ID -> range of sidebar
friend void to_json(nlohmann::json &j, const LazyLoadRequestMessage &m);
};
@ -424,3 +425,51 @@ struct InviteDeleteObject {
friend void from_json(const nlohmann::json &j, InviteDeleteObject &m);
};
struct ConnectionData {
std::string ID;
std::string Type;
std::string Name;
bool IsVerified;
friend void from_json(const nlohmann::json &j, ConnectionData &m);
};
struct MutualGuildData {
Snowflake ID;
std::optional<std::string> Nick; // null
friend void from_json(const nlohmann::json &j, MutualGuildData &m);
};
struct UserProfileData {
std::vector<ConnectionData> ConnectedAccounts;
std::vector<MutualGuildData> MutualGuilds;
std::optional<std::string> PremiumGuildSince; // null
std::optional<std::string> PremiumSince; // null
UserData User;
friend void from_json(const nlohmann::json &j, UserProfileData &m);
};
struct UserNoteObject {
// idk if these can be null or missing but i play it safe
std::optional<std::string> Note;
std::optional<Snowflake> NoteUserID;
std::optional<Snowflake> UserID;
friend void from_json(const nlohmann::json &j, UserNoteObject &m);
};
struct UserSetNoteObject {
std::string Note;
friend void to_json(nlohmann::json &j, UserSetNoteObject &m);
};
struct UserNoteUpdateMessage {
std::string Note;
Snowflake ID;
friend void from_json(const nlohmann::json &j, UserNoteUpdateMessage &m);
};

View File

@ -86,3 +86,69 @@ void UserData::update_from_json(const nlohmann::json &j) {
JS_RD("nsfw_allowed", IsNSFWAllowed);
JS_RD("phone", Phone);
}
const char *UserData::GetFlagName(uint64_t flag) {
switch (flag) {
case DiscordEmployee:
return "discordstaff";
case PartneredServerOwner:
return "partneredowner";
case HypeSquadEvents:
return "hypesquadevents";
case BugHunterLevel1:
return "discordbughunter";
case HouseBravery:
return "hypesquadbravery";
case HouseBrilliance:
return "hypesquadbrilliance";
case HouseBalance:
return "hypesquadbalance";
case EarlySupporter:
return "earlysupporter";
case TeamUser:
return "teamuser";
case System:
return "system";
case BugHunterLevel2:
return "discordbughunter2";
case VerifiedBot:
return "verifiedbot";
case EarlyVerifiedBotDeveloper:
return "earlyverifiedbotdeveloper";
default:
return "unknown";
}
}
const char *UserData::GetFlagReadableName(uint64_t flag) {
switch (flag) {
case DiscordEmployee:
return "Discord Staff";
case PartneredServerOwner:
return "Partnered Server Owner";
case HypeSquadEvents:
return "HypeSquad Events";
case BugHunterLevel1:
return "Discord Bug Hunter";
case HouseBravery:
return "HypeSquad Bravery";
case HouseBrilliance:
return "HypeSquad Brilliance";
case HouseBalance:
return "HypeSquad Balance";
case EarlySupporter:
return "Early Supporter";
case TeamUser:
return "Team User"; // ???
case System:
return "System";
case BugHunterLevel2:
return "Discord Bug Hunter Level 2";
case VerifiedBot:
return "Verified Bot";
case EarlyVerifiedBotDeveloper:
return "Early Verified Bot Developer";
default:
return "";
}
}

View File

@ -4,6 +4,27 @@
#include <string>
struct UserData {
enum {
DiscordEmployee = 1 << 0,
PartneredServerOwner = 1 << 1,
HypeSquadEvents = 1 << 2,
BugHunterLevel1 = 1 << 3,
HouseBravery = 1 << 6,
HouseBrilliance = 1 << 7,
HouseBalance = 1 << 8,
EarlySupporter = 1 << 9,
TeamUser = 1 << 10, // no idea what this is
System = 1 << 12,
BugHunterLevel2 = 1 << 14,
VerifiedBot = 1 << 16,
EarlyVerifiedBotDeveloper = 1 << 17,
MaxFlag = EarlyVerifiedBotDeveloper,
};
static const char *GetFlagName(uint64_t flag);
static const char *GetFlagReadableName(uint64_t flag);
Snowflake ID;
std::string Username;
std::string Discriminator;
@ -14,9 +35,9 @@ struct UserData {
std::optional<std::string> Locale;
std::optional<bool> IsVerified;
std::optional<std::string> Email; // null
std::optional<int> Flags;
std::optional<uint64_t> Flags;
std::optional<int> PremiumType; // null
std::optional<int> PublicFlags;
std::optional<uint64_t> PublicFlags;
// undocumented (opt)
std::optional<bool> IsDesktop;

BIN
res/battlenet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
res/discordbughunter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
res/discordbughunter2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
res/discordstaff.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
res/earlysupporter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
res/facebook.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
res/github.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
res/guildsubscriber.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
res/hypesquadbalance.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
res/hypesquadbravery.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
res/hypesquadbrilliance.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
res/hypesquadevents.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
res/leagueoflegends.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
res/partneredowner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
res/premium.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
res/reddit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
res/skype.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
res/spotify.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
res/steam.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
res/twitch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
res/twitter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
res/xbox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
res/youtube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -182,3 +182,12 @@ bool IsURLViewableImage(const std::string &url) {
return true;
return false;
}
void AddPointerCursor(Gtk::Widget &widget) {
widget.signal_realize().connect([&widget]() {
auto window = widget.get_window();
auto display = window->get_display();
auto cursor = Gdk::Cursor::create(display, "pointer");
window->set_cursor(cursor);
});
}

View File

@ -46,6 +46,7 @@ bool IsURLViewableImage(const std::string &url);
std::vector<uint8_t> ReadWholeFile(std::string path);
std::string HumanReadableBytes(uint64_t bytes);
std::string FormatISO8601(const std::string &in, int extra_offset = 0, const std::string &fmt = "%x %X");
void AddPointerCursor(Gtk::Widget &widget);
template<typename T>
struct Bitwise {

View File

@ -0,0 +1,179 @@
#include "userinfopane.hpp"
#include <unordered_set>
#include "../../abaddon.hpp"
ConnectionsContainer::ConnectionsContainer() {
get_style_context()->add_class("profile-connections");
set_column_homogeneous(true);
set_row_spacing(10);
set_column_spacing(10);
show_all_children();
}
void ConnectionsContainer::SetConnections(const std::vector<ConnectionData> &connections) {
for (auto child : get_children())
delete child;
static const std::unordered_set<std::string> supported_services = {
"battlenet",
"github",
"leagueoflegends",
"reddit",
"skype",
"spotify",
"steam",
"twitch",
"twitter",
"xbox",
"youtube",
"facebook"
};
for (int i = 0; i < connections.size(); i++) {
const auto &conn = connections[i];
if (supported_services.find(conn.Type) == supported_services.end()) continue;
Glib::RefPtr<Gdk::Pixbuf> pixbuf;
try {
pixbuf = Gdk::Pixbuf::create_from_file("./res/" + conn.Type + ".png", 32, 32);
} catch (const Glib::Exception &e) {}
std::string url;
if (conn.Type == "github")
url = "https://github.com/" + conn.Name;
else if (conn.Type == "steam")
url = "https://steamcommunity.com/profiles/" + conn.ID;
else if (conn.Type == "twitch")
url = "https://twitch.tv/" + conn.Name;
else if (conn.Type == "twitter")
url = "https://twitter.com/i/user/" + conn.ID;
else if (conn.Type == "spotify")
url = "https://open.spotify.com/user/" + conn.ID;
else if (conn.Type == "reddit")
url = "https://reddit.com/u/" + conn.Name;
else if (conn.Type == "youtube")
url = "https://www.youtube.com/channel/" + conn.ID;
else if (conn.Type == "facebook")
url = "https://www.facebook.com/" + conn.ID;
auto *ev = Gtk::manage(new Gtk::EventBox);
auto *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
if (pixbuf) {
auto *img = Gtk::manage(new Gtk::Image(pixbuf));
img->get_style_context()->add_class("profile-connection-image");
box->add(*img);
}
auto *lbl = Gtk::manage(new Gtk::Label(conn.Name));
box->set_halign(Gtk::ALIGN_START);
box->set_size_request(200, -1);
box->get_style_context()->add_class("profile-connection");
lbl->get_style_context()->add_class("profile-connection-label");
lbl->set_valign(Gtk::ALIGN_CENTER);
lbl->set_single_line_mode(true);
lbl->set_ellipsize(Pango::ELLIPSIZE_END);
box->add(*lbl);
if (url != "") {
auto cb = [this, url](GdkEventButton *event) -> bool {
if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY) {
LaunchBrowser(url);
return true;
}
return false;
};
ev->signal_button_press_event().connect(sigc::track_obj(cb, *ev));
AddPointerCursor(*ev);
}
ev->add(*box);
ev->show_all();
attach(*ev, i % 2, i / 2, 1, 1);
}
set_halign(Gtk::ALIGN_FILL);
set_hexpand(true);
}
NotesContainer::NotesContainer()
: Gtk::Box(Gtk::ORIENTATION_VERTICAL) {
get_style_context()->add_class("profile-notes");
m_label.get_style_context()->add_class("profile-notes-label");
m_note.get_style_context()->add_class("profile-notes-text");
m_label.set_markup("<b>NOTE</b>");
m_label.set_halign(Gtk::ALIGN_START);
m_note.set_wrap_mode(Gtk::WRAP_WORD_CHAR);
m_note.signal_key_press_event().connect(sigc::mem_fun(*this, &NotesContainer::OnNoteKeyPress), false);
add(m_label);
add(m_note);
show_all_children();
}
void NotesContainer::SetNote(const std::string &note) {
m_note.get_buffer()->set_text(note);
}
void NotesContainer::UpdateNote() {
auto text = m_note.get_buffer()->get_text();
if (text.size() > 256)
text = text.substr(0, 256);
m_signal_update_note.emit(text);
}
bool NotesContainer::OnNoteKeyPress(GdkEventKey *event) {
if (event->type != GDK_KEY_PRESS) return false;
const auto text = m_note.get_buffer()->get_text();
if (event->keyval == GDK_KEY_Return) {
if (event->state & GDK_SHIFT_MASK) {
int newlines = 0;
for (const auto c : text)
if (c == '\n') newlines++;
return newlines >= 5;
} else {
UpdateNote();
return true;
}
}
return false;
}
NotesContainer::type_signal_update_note NotesContainer::signal_update_note() {
return m_signal_update_note;
}
ProfileUserInfoPane::ProfileUserInfoPane(Snowflake ID)
: Gtk::Box(Gtk::ORIENTATION_VERTICAL)
, UserID(ID) {
get_style_context()->add_class("profile-info-pane");
m_note.signal_update_note().connect([this](const Glib::ustring &note) {
auto cb = [this](bool success) {
if (!success) {
Gtk::MessageDialog dlg("Failed to set note", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
dlg.set_position(Gtk::WIN_POS_CENTER);
dlg.run();
}
};
Abaddon::Get().GetDiscordClient().SetUserNote(UserID, note, sigc::track_obj(cb, *this));
});
auto &discord = Abaddon::Get().GetDiscordClient();
discord.signal_note_update().connect([this](Snowflake id, std::string note) {
if (id == UserID)
m_note.SetNote(note);
});
auto fetch_note_cb = [this](const std::string &note) {
m_note.SetNote(note);
};
discord.FetchUserNote(UserID, sigc::track_obj(fetch_note_cb, *this));
m_conns.set_halign(Gtk::ALIGN_START);
m_conns.set_hexpand(true);
add(m_note);
add(m_conns);
show_all_children();
}
void ProfileUserInfoPane::SetConnections(const std::vector<ConnectionData> &connections) {
m_conns.SetConnections(connections);
}

View File

@ -0,0 +1,40 @@
#pragma once
#include <gtkmm.h>
#include "../../discord/objects.hpp"
class ConnectionsContainer : public Gtk::Grid {
public:
ConnectionsContainer();
void SetConnections(const std::vector<ConnectionData> &connections);
};
class NotesContainer : public Gtk::Box {
public:
NotesContainer();
void SetNote(const std::string &note);
private:
void UpdateNote();
bool OnNoteKeyPress(GdkEventKey *event);
Gtk::Label m_label;
Gtk::TextView m_note;
typedef sigc::signal<void, Glib::ustring> type_signal_update_note;
type_signal_update_note m_signal_update_note;
public:
type_signal_update_note signal_update_note();
};
class ProfileUserInfoPane : public Gtk::Box {
public:
ProfileUserInfoPane(Snowflake ID);
void SetConnections(const std::vector<ConnectionData> &connections);
Snowflake UserID;
private:
NotesContainer m_note;
ConnectionsContainer m_conns;
};

124
windows/profilewindow.cpp Normal file
View File

@ -0,0 +1,124 @@
#include "profilewindow.hpp"
#include "../abaddon.hpp"
ProfileWindow::ProfileWindow(Snowflake user_id)
: ID(user_id)
, m_main(Gtk::ORIENTATION_VERTICAL)
, m_upper(Gtk::ORIENTATION_HORIZONTAL)
, m_badges(Gtk::ORIENTATION_HORIZONTAL)
, m_pane_info(user_id) {
const auto &discord = Abaddon::Get().GetDiscordClient();
auto user = *discord.GetUser(ID);
Abaddon::Get().GetDiscordClient().FetchUserProfile(user_id, sigc::mem_fun(*this, &ProfileWindow::OnFetchProfile));
set_name("user-profile");
set_default_size(450, 375);
set_title(user.Username + "#" + user.Discriminator);
set_position(Gtk::WIN_POS_CENTER);
get_style_context()->add_class("app-window");
get_style_context()->add_class("app-popup");
get_style_context()->add_class("user-profile-window");
m_main.get_style_context()->add_class("profile-main-container");
m_avatar.get_style_context()->add_class("profile-avatar");
m_username.get_style_context()->add_class("profile-username");
m_switcher.get_style_context()->add_class("profile-switcher");
m_stack.get_style_context()->add_class("profile-stack");
m_badges.get_style_context()->add_class("profile-badges");
m_scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
m_scroll.set_vexpand(true);
m_scroll.set_propagate_natural_height(true);
if (user.HasAvatar())
AddPointerCursor(m_avatar_ev);
m_avatar_ev.signal_button_press_event().connect([this, user](GdkEventButton *event) -> bool {
if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY)
if (user.HasAnimatedAvatar())
LaunchBrowser(user.GetAvatarURL("gif", "512"));
else
LaunchBrowser(user.GetAvatarURL("png", "512"));
return false;
});
auto &img = Abaddon::Get().GetImageManager();
m_avatar.property_pixbuf() = img.GetPlaceholder(64);
if (user.HasAvatar()) {
auto icon_cb = [this](const Glib::RefPtr<Gdk::Pixbuf> &pb) {
set_icon(pb);
};
img.LoadFromURL(user.GetAvatarURL("png", "64"), sigc::track_obj(icon_cb, *this));
if (user.HasAnimatedAvatar()) {
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_username.set_markup(user.GetEscapedString());
m_switcher.set_stack(m_stack);
m_switcher.set_halign(Gtk::ALIGN_START);
m_switcher.set_hexpand(true);
m_stack.add(m_pane_info, "info", "User Info");
m_badges.set_valign(Gtk::ALIGN_CENTER);
m_badges_scroll.set_hexpand(true);
m_badges_scroll.set_propagate_natural_width(true);
m_badges_scroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_NEVER);
m_upper.set_halign(Gtk::ALIGN_START);
m_avatar.set_halign(Gtk::ALIGN_START);
m_username.set_halign(Gtk::ALIGN_START);
m_avatar_ev.add(m_avatar);
m_upper.add(m_avatar_ev);
m_upper.add(m_username);
m_badges_scroll.add(m_badges);
m_upper.add(m_badges_scroll);
m_main.add(m_upper);
m_main.add(m_switcher);
m_scroll.add(m_stack);
m_main.add(m_scroll);
add(m_main);
show_all_children();
}
void ProfileWindow::on_hide() {
Gtk::Window::on_hide();
delete this;
}
void ProfileWindow::OnFetchProfile(const UserProfileData &data) {
m_pane_info.SetConnections(data.ConnectedAccounts);
for (auto child : m_badges.get_children())
delete child;
if (!data.User.PublicFlags.has_value()) return;
const auto x = *data.User.PublicFlags;
for (uint64_t i = 1; i <= UserData::MaxFlag; i <<= 1) {
if (!(x & i)) continue;
const std::string name = UserData::GetFlagName(i);
if (name == "unknown") continue;
Glib::RefPtr<Gdk::Pixbuf> pixbuf;
try {
pixbuf = Gdk::Pixbuf::create_from_file("./res/" + name + ".png", 24, 24);
} catch (const Glib::Exception &e) {
pixbuf = Abaddon::Get().GetImageManager().GetPlaceholder(24);
}
if (!pixbuf) continue;
auto *image = Gtk::manage(new Gtk::Image(pixbuf));
image->get_style_context()->add_class("profile-badge");
image->set_tooltip_text(UserData::GetFlagReadableName(i));
image->show();
m_badges.add(*image);
}
}

29
windows/profilewindow.hpp Normal file
View File

@ -0,0 +1,29 @@
#pragma once
#include <gtkmm.h>
#include "../discord/snowflake.hpp"
#include "profile/userinfopane.hpp"
class ProfileWindow : public Gtk::Window {
public:
ProfileWindow(Snowflake user_id);
void on_hide() override;
Snowflake ID;
private:
void OnFetchProfile(const UserProfileData &data);
Gtk::Box m_main;
Gtk::Box m_upper;
Gtk::Box m_badges;
Gtk::ScrolledWindow m_badges_scroll;
Gtk::EventBox m_avatar_ev;
Gtk::Image m_avatar;
Gtk::Label m_username;
Gtk::ScrolledWindow m_scroll;
Gtk::Stack m_stack;
Gtk::StackSwitcher m_switcher;
ProfileUserInfoPane m_pane_info;
};