From 09a8ade6becca2a71f45ff0db5f4bf6d64afb212 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 21 Apr 2018 03:46:58 +0200 Subject: Add support for static image emoji Emoji are downloaded asynchronously using remote program (curl). Need to add support for converting [inline](url) chat message emoji and gifs. --- src/Cache.cpp | 158 +++++++++++++++++++++++++++++++++++++++++++++++ src/Channel.cpp | 79 ++++++++++++++++++++++++ src/ChannelSidePanel.cpp | 9 +++ src/Message.cpp | 5 ++ src/MessageBoard.cpp | 104 ++++++++++++++++++++----------- src/MessagePart.cpp | 24 +++++++ src/ResourceCache.cpp | 25 +++++++- src/main.cpp | 71 ++++----------------- 8 files changed, 376 insertions(+), 99 deletions(-) create mode 100644 src/Cache.cpp create mode 100644 src/Channel.cpp create mode 100644 src/ChannelSidePanel.cpp (limited to 'src') diff --git a/src/Cache.cpp b/src/Cache.cpp new file mode 100644 index 0000000..7e3272a --- /dev/null +++ b/src/Cache.cpp @@ -0,0 +1,158 @@ +#include "../include/Cache.hpp" +#include "../include/env.hpp" +#include "../include/ResourceCache.hpp" +#include +#include +#include +#include + +#if OS_FAMILY == OS_FAMILY_POSIX +#include +#else +#include +#endif + +using namespace std; +using namespace TinyProcessLib; + +namespace dchat +{ + unordered_map imageUrlCache; + + boost::filesystem::path getHomeDir() + { + #if OS_FAMILY == OS_FAMILY_POSIX + const char *homeDir = getenv("HOME"); + if(!homeDir) + { + passwd *pw = getpwuid(getuid()); + homeDir = pw->pw_dir; + } + return boost::filesystem::path(homeDir); + #elif OS_FAMILY == OS_FAMILY_WINDOWS + BOOL ret; + HANDLE hToken; + std::wstring homeDir; + DWORD homeDirLen = MAX_PATH; + homeDir.resize(homeDirLen); + + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_READ, &hToken)) + return Result::Err("Failed to open process token"); + + if (!GetUserProfileDirectory(hToken, &homeDir[0], &homeDirLen)) + { + CloseHandle(hToken); + return Result::Err("Failed to get home directory"); + } + + CloseHandle(hToken); + homeDir.resize(wcslen(homeDir.c_str())); + return boost::filesystem::path(homeDir); + #endif + } + + boost::filesystem::path Cache::getDchatDir() + { + boost::filesystem::path dchatHomeDir = getHomeDir() / ".dchat"; + boost::filesystem::create_directories(dchatHomeDir); + return dchatHomeDir; + } + + Cache::Cache() + { + downloadWaitThread = thread([this] + { + while(true) + { + imageDownloadMutex.lock(); + for(vector::iterator it = imageDownloadProcesses.begin(); it != imageDownloadProcesses.end();) + { + int exitStatus; + if(it->process->try_get_exit_status(exitStatus)) + { + bool failed = exitStatus != 0; + + if(!failed) + { + boost::filesystem::path filepath = getDchatDir(); + odhtdb::Hash urlHash(it->url.data(), it->url.size()); + filepath /= urlHash.toString(); + + try + { + const sf::Texture *texture = ResourceCache::getTexture(filepath.string()); + ImageByUrlResult &imageByUrlResult = imageUrlCache[it->url]; + imageByUrlResult.texture = texture; + imageByUrlResult.type = ImageByUrlResult::Type::CACHED; + } + catch(FailedToLoadResourceException &e) + { + fprintf(stderr, "%s\n", e.what()); + failed = true; + } + } + + if(failed) + { + imageUrlCache[it->url].type = ImageByUrlResult::Type::FAILED_DOWNLOAD; + fprintf(stderr, "Image download failed for url: %s\n", it->url.c_str()); + } + + it = imageDownloadProcesses.erase(it); + } + else + ++it; + } + imageDownloadMutex.unlock(); + + while(imageDownloadProcesses.empty()) + this_thread::sleep_for(chrono::milliseconds(20)); + } + }); + downloadWaitThread.detach(); + } + + const ImageByUrlResult Cache::getImageByUrl(const string &url, int downloadLimitBytes) + { + auto it = imageUrlCache.find(url); + if(it != imageUrlCache.end()) + return it->second; + + // TODO: Verify hashed url is not too long for filepath on windows + boost::filesystem::path filepath = getDchatDir(); + odhtdb::Hash urlHash(url.data(), url.size()); + filepath /= urlHash.toString(); + + // Check if file exists because we dont want sfml spam with "Failed to load image""... + if(boost::filesystem::exists(filepath)) + { + try + { + const sf::Texture *texture = ResourceCache::getTexture(filepath.string()); + lock_guard lock(imageDownloadMutex); + ImageByUrlResult result { texture, ImageByUrlResult::Type::CACHED }; + imageUrlCache[url] = result; + return result; + } + catch(FailedToLoadResourceException &e) + { + + } + } + + lock_guard lock(imageDownloadMutex); + ImageByUrlResult result { nullptr, ImageByUrlResult::Type::DOWNLOADING }; + imageUrlCache[url] = result; + + string downloadLimitBytesStr = to_string(downloadLimitBytes); + + Process::string_type cmd = "curl -L --silent -o '"; + cmd += filepath.native(); + cmd += "' --max-filesize " + downloadLimitBytesStr + " --range 0-" + downloadLimitBytesStr + " --url '" + url + "'"; + // certutil.exe -urlcache -split -f "https://url/to/file" path/and/name/to/save/as/file + Process *process = new Process(cmd, "", nullptr, nullptr, false); + ImageDownloadInfo imageDownloadInfo { process, url }; + imageDownloadProcesses.emplace_back(imageDownloadInfo); + return result; + } +} diff --git a/src/Channel.cpp b/src/Channel.cpp new file mode 100644 index 0000000..0fc7ec5 --- /dev/null +++ b/src/Channel.cpp @@ -0,0 +1,79 @@ +#include "../include/Channel.hpp" +#include + +using namespace std; + +namespace dchat +{ + Channel::Channel() : + messageBoard(sf::Vector2u(1.0f, 1.0f)), + localOfflineUser("You") + { + { + Message *message = new Message(&localOfflineUser); + message->addText(u8"hello, worldåäö1!"); + message->addImage("https://discordemoji.com/assets/emoji/think_fish.png"); + messageBoard.addMessage(message); + } + + { + Message *message = new Message(&localOfflineUser); + message->addText(u8"hello, world2!"); + messageBoard.addMessage(message); + } + + { + Message *message = new Message(&localOfflineUser); + message->addText(u8"hello, world3!"); + messageBoard.addMessage(message); + } + } + + Channel::~Channel() + { + + } + + void Channel::processEvent(const sf::Event &event) + { + if(event.type == sf::Event::TextEntered) + { + if(event.text.unicode == 8) // backspace + chatbar.removePreviousChar(); + else if(event.text.unicode == 13) // enter + { + Message *message = new Message(&localOfflineUser); + auto chatbarMsgUtf8 = chatbar.getString().toUtf8(); + string msg; + msg.resize(chatbarMsgUtf8.size()); + memcpy(&msg[0], chatbarMsgUtf8.data(), chatbarMsgUtf8.size()); + + message->addText(msg); + messageBoard.addMessage(message); + chatbar.clear(); + } + else if(event.text.unicode == 127) // delete + { + chatbar.removeNextChar(); + } + else + { + chatbar.addChar(event.text.unicode); + } + } + else if(event.type == sf::Event::KeyPressed) + { + if(event.key.code == sf::Keyboard::Left) + chatbar.moveCaretLeft(); + else if(event.key.code == sf::Keyboard::Right) + chatbar.moveCaretRight(); + } + messageBoard.processEvent(event); + } + + void Channel::draw(sf::RenderWindow &window, Cache &cache) + { + messageBoard.draw(window, cache); + chatbar.draw(window); + } +} diff --git a/src/ChannelSidePanel.cpp b/src/ChannelSidePanel.cpp new file mode 100644 index 0000000..23693b3 --- /dev/null +++ b/src/ChannelSidePanel.cpp @@ -0,0 +1,9 @@ +#include "../include/ChannelSidePanel.hpp" + +namespace dchat +{ + void ChannelSidePanel::addChannel(Channel *channel) + { + channels.push_back(channel); + } +} diff --git a/src/Message.cpp b/src/Message.cpp index 44174ec..2740c11 100644 --- a/src/Message.cpp +++ b/src/Message.cpp @@ -23,6 +23,11 @@ namespace dchat messageParts.push_back(new MessagePartText(text)); } + void Message::addImage(const string &url) + { + messageParts.push_back(new MessagePartEmoji(url)); + } + vector& Message::getParts() { return messageParts; diff --git a/src/MessageBoard.cpp b/src/MessageBoard.cpp index 2b9115a..acb7be1 100644 --- a/src/MessageBoard.cpp +++ b/src/MessageBoard.cpp @@ -84,7 +84,7 @@ namespace dchat } } - void MessageBoard::draw(sf::RenderWindow &window) + void MessageBoard::draw(sf::RenderWindow &window, Cache &cache) { sf::RenderTarget *renderTarget = nullptr; if(useStaticContentTexture) @@ -99,55 +99,85 @@ namespace dchat dirty = true; } - if(dirty) + auto renderTargetSize = renderTarget->getSize(); + + if(useStaticContentTexture) { - dirty = false; - auto renderTargetSize = renderTarget->getSize(); - - if(useStaticContentTexture) + if(dirty) staticContentTexture.clear(BACKGROUND_COLOR); - else - { - sf::RectangleShape background(sf::Vector2f(renderTargetSize.x, renderTargetSize.y)); - background.setFillColor(BACKGROUND_COLOR); - renderTarget->draw(background); - } - - const sf::Font &usernameFont = ResourceCache::getFont("fonts/Roboto-Regular.ttf"); + } + else + { + sf::RectangleShape background(sf::Vector2f(renderTargetSize.x, renderTargetSize.y)); + background.setFillColor(BACKGROUND_COLOR); + renderTarget->draw(background); + } + + const sf::Font &usernameFont = ResourceCache::getFont("fonts/Roboto-Regular.ttf"); + + sf::Vector2f position; + for(Message *message : messages) + { + sf::Text usernameText(message->user->getName(), usernameFont, MessagePartText::getFontSizeScaled() * 1.3f); + usernameText.setFillColor(sf::Color(15, 192, 252)); + usernameText.setPosition(position); + if(dirty) + renderTarget->draw(usernameText); + position.y += usernameText.getCharacterSize() + USERNAME_PADDING_BOTTOM; - sf::Vector2f position; - for(Message *message : messages) + for(MessagePart *messagePart : message->getParts()) { - sf::Text usernameText(message->user->getName(), usernameFont, MessagePartText::getFontSizeScaled() * 1.3f); - usernameText.setFillColor(sf::Color(15, 192, 252)); - usernameText.setPosition(position); - renderTarget->draw(usernameText); - position.y += usernameText.getCharacterSize() + USERNAME_PADDING_BOTTOM; - - for(MessagePart *messagePart : message->getParts()) + switch(messagePart->type) { - switch(messagePart->type) + case MessagePart::Type::TEXT: { - case MessagePart::Type::TEXT: - { - MessagePartText *messagePartText = static_cast(messagePart); - messagePartText->text.setFillColor(sf::Color(240, 240, 240)); - messagePartText->text.setCharacterSize(MessagePartText::getFontSizeScaled()); - messagePartText->text.setPosition(floor(position.x), floor(position.y + MessagePart::getSizeScaled() * 0.5f - MessagePartText::getFontSizeScaled() * 0.5f)); + MessagePartText *messagePartText = static_cast(messagePart); + messagePartText->text.setFillColor(sf::Color(240, 240, 240)); + messagePartText->text.setCharacterSize(MessagePartText::getFontSizeScaled()); + messagePartText->text.setPosition(floor(position.x), floor(position.y + MessagePart::getSizeScaled() * 0.5f - MessagePartText::getFontSizeScaled() * 0.5f)); + if(dirty) renderTarget->draw(messagePartText->text); - position.x += messagePartText->text.getLocalBounds().width; - break; + position.x += messagePartText->text.getLocalBounds().width; + break; + } + case MessagePart::Type::EMOJI: + { + MessagePartEmoji *messagePartEmoji = static_cast(messagePart); + auto imageByUrlResult = cache.getImageByUrl(messagePartEmoji->url, 1024 * 512); + position.x += 5.0f; + if(imageByUrlResult.texture) + { + // TODO: Verify this doesn't cause lag + messagePartEmoji->sprite.setTexture(*imageByUrlResult.texture, true); + sf::Vector2f spriteSize(MessagePartEmoji::getHeightScaled(), MessagePartEmoji::getHeightScaled()); + messagePartEmoji->sprite.setScale(spriteSize.x / (float)imageByUrlResult.texture->getSize().x, spriteSize.y / (float)imageByUrlResult.texture->getSize().y); + messagePartEmoji->sprite.setPosition(floor(position.x), floor(position.y + MessagePart::getSizeScaled() * 0.5f - MessagePartEmoji::getHeightScaled() * 0.5f)); + if(dirty) + renderTarget->draw(messagePartEmoji->sprite); + } + else + { + // TODO: Replace this with a loading gif + sf::RectangleShape emojiDownloadRect(sf::Vector2f(MessagePartEmoji::getHeightScaled(), MessagePartEmoji::getHeightScaled())); + emojiDownloadRect.setPosition(floor(position.x), floor(position.y + MessagePart::getSizeScaled() * 0.5f - MessagePartEmoji::getHeightScaled() * 0.5f)); + emojiDownloadRect.setFillColor(sf::Color::White); + if(dirty) + renderTarget->draw(emojiDownloadRect); } + position.x += MessagePartEmoji::getHeightScaled() + 5.0f; + break; } } - position.x = 0.0f; - position.y += MessagePart::getSizeScaled() + MESSAGE_PADDING_BOTTOM; } - - if(useStaticContentTexture) - staticContentTexture.display(); + position.x = 0.0f; + position.y += MessagePart::getSizeScaled() + MESSAGE_PADDING_BOTTOM; } + if(useStaticContentTexture) + staticContentTexture.display(); + + dirty = false; + if(useStaticContentTexture) window.draw(sf::Sprite(staticContentTexture.getTexture())); diff --git a/src/MessagePart.cpp b/src/MessagePart.cpp index ea8e4a5..dcef3f3 100644 --- a/src/MessagePart.cpp +++ b/src/MessagePart.cpp @@ -34,4 +34,28 @@ namespace dchat { return sf::Vector2f(text.getLocalBounds().width, getFontSizeScaled()); } + + MessagePartEmoji::MessagePartEmoji(const string &_url) : + MessagePart(Type::EMOJI), + url(_url) + { + + } + + float MessagePartEmoji::getHeightScaled() + { + return MessagePart::getSizeScaled() * 1.0f; + } + + sf::Vector2f MessagePartEmoji::getPosition() const + { + return sprite.getPosition(); + } + + sf::Vector2f MessagePartEmoji::getSize() const + { + auto spriteScale = sprite.getScale(); + auto textureSize = sprite.getTexture()->getSize(); + return { (float)textureSize.x * spriteScale.x, (float)textureSize.y * spriteScale.y }; + } } diff --git a/src/ResourceCache.cpp b/src/ResourceCache.cpp index 474360c..4bdd75d 100644 --- a/src/ResourceCache.cpp +++ b/src/ResourceCache.cpp @@ -1,12 +1,12 @@ #include "../include/ResourceCache.hpp" #include -#include using namespace std; namespace dchat { unordered_map fonts; + unordered_map textures; const sf::Font& ResourceCache::getFont(const string &filepath) { @@ -20,10 +20,31 @@ namespace dchat delete font; string errMsg = "Failed to load font: "; errMsg += filepath; - throw runtime_error(errMsg); + throw FailedToLoadResourceException(errMsg); } fonts[filepath] = font; return *font; } + + const sf::Texture* ResourceCache::getTexture(const string &filepath) + { + auto it = textures.find(filepath); + if(it != textures.end()) + return it->second; + + sf::Texture *texture = new sf::Texture(); + if(!texture->loadFromFile(filepath)) + { + delete texture; + string errMsg = "Failed to load texture: "; + errMsg += filepath; + throw FailedToLoadResourceException(errMsg); + } + + texture->setSmooth(true); + texture->generateMipmap(); + textures[filepath] = texture; + return texture; + } } diff --git a/src/main.cpp b/src/main.cpp index 481f73a..284df56 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,12 +1,14 @@ -#include "../include/MessageBoard.hpp" -#include "../include/User.hpp" -#include "../include/Chatbar.hpp" +#include "../include/Channel.hpp" +#include "../include/ChannelSidePanel.hpp" +#include "../include/Cache.hpp" #include #include #include +#include "process.hpp" using namespace std; using namespace dchat; +using namespace TinyProcessLib; int main() { @@ -14,29 +16,11 @@ int main() window.setVerticalSyncEnabled(false); window.setFramerateLimit(60); - OfflineUser *localOfflineUser = new OfflineUser("You"); + Cache cache; - MessageBoard messageBoard(window.getSize()); - - { - Message *message = new Message(localOfflineUser); - message->addText(u8"hello, world!"); - messageBoard.addMessage(message); - } - - { - Message *message = new Message(localOfflineUser); - message->addText(u8"hello, world!"); - messageBoard.addMessage(message); - } - - { - Message *message = new Message(localOfflineUser); - message->addText(u8"hello, world!"); - messageBoard.addMessage(message); - } - - Chatbar chatbar; + Channel channel; + ChannelSidePanel channelSidePanel; + channelSidePanel.addChannel(&channel); while (window.isOpen()) { @@ -50,44 +34,11 @@ int main() sf::View view(sf::FloatRect(0.0f, 0.0f, event.size.width, event.size.height)); window.setView(view); } - else if(event.type == sf::Event::TextEntered) - { - if(event.text.unicode == 8) // backspace - chatbar.removePreviousChar(); - else if(event.text.unicode == 13) // enter - { - Message *message = new Message(localOfflineUser); - auto chatbarMsgUtf8 = chatbar.getString().toUtf8(); - string msg; - msg.resize(chatbarMsgUtf8.size()); - memcpy(&msg[0], chatbarMsgUtf8.data(), chatbarMsgUtf8.size()); - - message->addText(msg); - messageBoard.addMessage(message); - chatbar.clear(); - } - else if(event.text.unicode == 127) // delete - { - chatbar.removeNextChar(); - } - else - { - chatbar.addChar(event.text.unicode); - } - } - else if(event.type == sf::Event::KeyPressed) - { - if(event.key.code == sf::Keyboard::Left) - chatbar.moveCaretLeft(); - else if(event.key.code == sf::Keyboard::Right) - chatbar.moveCaretRight(); - } - messageBoard.processEvent(event); + channel.processEvent(event); } window.clear(); - messageBoard.draw(window); - chatbar.draw(window); + channel.draw(window, cache); window.display(); } -- cgit v1.2.3