From 54a8244bfdd26656260b3c82391db2213f937ac7 Mon Sep 17 00:00:00 2001 From: ouwou <26526779+ouwou@users.noreply.github.com> Date: Thu, 10 Sep 2020 18:28:42 -0400 Subject: basic avatars --- CMakeLists.txt | 2 ++ abaddon.cpp | 4 +++ abaddon.hpp | 5 +++ appveyor.yml | 1 + components/chatmessage.cpp | 13 ++++++++ components/chatmessage.hpp | 1 + discord/user.cpp | 8 +++++ discord/user.hpp | 3 ++ filecache.cpp | 80 +++++++++++++++++++++++++++++++++++++++++++++ filecache.hpp | 28 ++++++++++++++++ res/decamarks.png | Bin 0 -> 1926 bytes 11 files changed, 145 insertions(+) create mode 100644 filecache.cpp create mode 100644 filecache.hpp create mode 100644 res/decamarks.png diff --git a/CMakeLists.txt b/CMakeLists.txt index 4af6846..33e5c0a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -126,6 +126,8 @@ add_executable(abaddon settings.hpp settings.cpp abaddon.cpp + filecache.hpp + filecache.cpp components/channels.hpp components/channels.cpp components/chatmessage.hpp diff --git a/abaddon.cpp b/abaddon.cpp index 1f775b0..e241ca7 100644 --- a/abaddon.cpp +++ b/abaddon.cpp @@ -283,6 +283,10 @@ void Abaddon::ActionReloadCSS() { } } +Cache &Abaddon::GetCache() { + return m_cache; +} + int main(int argc, char **argv) { Gtk::Main::init_gtkmm_internals(); // why??? return Abaddon::Get().StartGTK(); diff --git a/abaddon.hpp b/abaddon.hpp index d70da81..a52e759 100644 --- a/abaddon.hpp +++ b/abaddon.hpp @@ -6,6 +6,7 @@ #include "discord/discord.hpp" #include "windows/mainwindow.hpp" #include "settings.hpp" +#include "filecache.hpp" #define APP_TITLE "Abaddon" @@ -42,6 +43,8 @@ public: void ActionReloadCSS(); + Cache &GetCache(); + std::string GetDiscordToken() const; bool IsDiscordActive() const; @@ -62,6 +65,8 @@ private: std::unordered_map m_oldest_listed_message; std::unordered_set m_channels_history_loading; + Cache m_cache; + mutable std::mutex m_mutex; Glib::RefPtr m_gtk_app; Glib::RefPtr m_css_provider; diff --git a/appveyor.yml b/appveyor.yml index 7c782c8..9d9b74c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -40,6 +40,7 @@ for: after_build: - cmd: | xcopy /E /I %APPVEYOR_BUILD_FOLDER%\css "%APPVEYOR_BUILD_FOLDER%\build\%CONFIGURATION%\css\" + xcopy /E /I %APPVEYOR_BUILD_FOLDER%\res "%APPVEYOR_BUILD_FOLDER%\build\%CONFIGURATION%\res\" 7z a %APPVEYOR_BUILD_FOLDER%\out\abaddon.zip "%APPVEYOR_BUILD_FOLDER%\build\%CONFIGURATION%" artifacts: diff --git a/components/chatmessage.cpp b/components/chatmessage.cpp index 57a1f99..d6b0cad 100644 --- a/components/chatmessage.cpp +++ b/components/chatmessage.cpp @@ -14,6 +14,18 @@ ChatMessageContainer::ChatMessageContainer(const Message *data) { m_author = Gtk::manage(new Gtk::Label); m_timestamp = Gtk::manage(new Gtk::Label); + static Glib::RefPtr test = Gdk::Pixbuf::create_from_file("res/decamarks.png", 32, 32); + m_avatar = Gtk::manage(new Gtk::Image(test)); + m_avatar->set_valign(Gtk::ALIGN_START); + m_avatar->set_margin_right(10); + + if (data->Author.HasAvatar()) { + Abaddon::Get().GetCache().GetFileFromURL(data->Author.GetAvatarURL(), [this](std::string filepath) { + auto buf = Gdk::Pixbuf::create_from_file(filepath, 32, 32); + m_avatar->property_pixbuf() = buf; + }); + } + get_style_context()->add_class("message-container"); m_author->get_style_context()->add_class("message-container-author"); m_timestamp->get_style_context()->add_class("message-container-timestamp"); @@ -47,6 +59,7 @@ ChatMessageContainer::ChatMessageContainer(const Message *data) { m_meta_box->add(*m_author); m_meta_box->add(*m_timestamp); m_content_box->add(*m_meta_box); + m_main_box->add(*m_avatar); m_main_box->add(*m_content_box); add(*m_main_box); set_margin_bottom(8); diff --git a/components/chatmessage.hpp b/components/chatmessage.hpp index d491322..3c3e29e 100644 --- a/components/chatmessage.hpp +++ b/components/chatmessage.hpp @@ -27,6 +27,7 @@ protected: Gtk::Box *m_meta_box; Gtk::Label *m_author; Gtk::Label *m_timestamp; + Gtk::Image *m_avatar; }; class ChatMessageItem { diff --git a/discord/user.cpp b/discord/user.cpp index a4c4424..cfc9e34 100644 --- a/discord/user.cpp +++ b/discord/user.cpp @@ -1,5 +1,13 @@ #include "user.hpp" +bool User::HasAvatar() const { + return Avatar.size() > 0; +} + +std::string User::GetAvatarURL(std::string ext, std::string size) const { + return "https://cdn.discordapp.com/avatars/" + std::to_string(ID) + "/" + Avatar + "." + ext + "?size=" + size; +} + void from_json(const nlohmann::json &j, User &m) { JS_D("id", m.ID); JS_D("username", m.Username); diff --git a/discord/user.hpp b/discord/user.hpp index 1a6beaa..e495b21 100644 --- a/discord/user.hpp +++ b/discord/user.hpp @@ -25,4 +25,7 @@ struct User { std::string Phone; // null? friend void from_json(const nlohmann::json &j, User &m); + + bool HasAvatar() const; + std::string GetAvatarURL(std::string ext = "png", std::string size = "64") const; }; diff --git a/filecache.cpp b/filecache.cpp new file mode 100644 index 0000000..7d1bdb4 --- /dev/null +++ b/filecache.cpp @@ -0,0 +1,80 @@ +#include "filecache.hpp" + +Cache::Cache() { + m_tmp_path = std::filesystem::temp_directory_path() / "abaddon-cache"; + std::filesystem::create_directories(m_tmp_path); +} + +Cache::~Cache() { + std::error_code err; + if (!std::filesystem::remove_all(m_tmp_path, err)) + fprintf(stderr, "error removing tmp dir\n"); +} + +std::string Cache::SanitizeString(std::string str) { + std::string ret; + for (const char c : str) { + if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z')) + ret += c; + else + ret += '_'; + } + return ret; +} + +void Cache::RespondFromPath(std::filesystem::path path, callback_type cb) { + /*if (!std::filesystem::exists(path)) return; + FILE *fp = std::fopen(path.string().c_str(), "rb"); + if (fp == nullptr) return; + std::fseek(fp, 0, SEEK_END); + int len = std::ftell(fp); + std::rewind(fp); + std::vector ret; + ret.resize(len); + std::fread(ret.data(), 1, len, fp); + std::fclose(fp);*/ + cb(path.string()); +} + +void Cache::GetFileFromURL(std::string url, callback_type cb) { + auto cache_path = m_tmp_path / SanitizeString(url); + if (std::filesystem::exists(cache_path)) { + RespondFromPath(cache_path, cb); + return; + } + + if (m_callbacks.find(url) != m_callbacks.end()) { + m_callbacks[url].push_back(cb); + } else { + m_futures.push_back(cpr::GetCallback(std::bind(&Cache::OnResponse, this, std::placeholders::_1), cpr::Url { url })); + m_callbacks[url].push_back(cb); + } +} + +// this just seems really yucky +void Cache::CleanupFutures() { + for (auto it = m_futures.begin(); it != m_futures.end();) { + if (it->wait_for(std::chrono::seconds(0)) == std::future_status::ready) + it = m_futures.erase(it); + else + it++; + } +} + +void Cache::OnResponse(const cpr::Response &r) { + CleanupFutures(); // see above comment + if (r.error || r.status_code > 300) return; + + std::vector data(r.text.begin(), r.text.end()); + auto path = m_tmp_path / SanitizeString(r.url); + FILE *fp = std::fopen(path.string().c_str(), "wb"); + if (fp == nullptr) + return; + std::fwrite(data.data(), 1, data.size(), fp); + std::fclose(fp); + + for (const auto &cb : m_callbacks[r.url]) { + cb(path.string()); + } + m_callbacks.erase(r.url); +} diff --git a/filecache.hpp b/filecache.hpp new file mode 100644 index 0000000..dfc8638 --- /dev/null +++ b/filecache.hpp @@ -0,0 +1,28 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +// todo throttle requests and keep track of active requests to stop redundant requests + +class Cache { +public: + Cache(); + ~Cache(); + + using callback_type = std::function; + void GetFileFromURL(std::string url, callback_type cb); + +private: + std::string SanitizeString(std::string str); + void CleanupFutures(); + void RespondFromPath(std::filesystem::path path, callback_type cb); + void OnResponse(const cpr::Response &r); + + std::unordered_map> m_callbacks; + std::vector> m_futures; + std::filesystem::path m_tmp_path; +}; diff --git a/res/decamarks.png b/res/decamarks.png new file mode 100644 index 0000000..972ec66 Binary files /dev/null and b/res/decamarks.png differ -- cgit v1.2.3