#include "../include/MessageBoard.hpp" #include "../include/Settings.hpp" #include "../include/ResourceCache.hpp" #include "../include/RoomSidePanel.hpp" #include "../include/UsersSidePanel.hpp" #include "../include/RoomTopPanel.hpp" #include "../include/Chatbar.hpp" #include "../include/ColorScheme.hpp" #include "../include/StaticImage.hpp" #include "../include/Gif.hpp" #include "../include/Theme.hpp" #include "../include/GlobalContextMenu.hpp" #include #include #include #include #include #include #include #include #include #include using namespace std; namespace dchat { struct LineColor { sf::Color sideColor, centerColor; }; const float USERNAME_PADDING_BOTTOM = 0.0f; const float PADDING_SIDE = 40.0f; const float PADDING_TOP = 0.0f; const float LINE_SIDE_PADDING = 20.0f; const float LINE_HEIGHT = 1.0f; const float TEXT_LINE_SPACING = 5.0f; const float USERNAME_TIMESTAMP_SIDE_PADDING = 15.0f; const float AVATAR_DIAMETER = 70.0f; const float AVATAR_PADDING_SIDE = 20.0f; const double SCROLL_MAX_SPEED = 20.0; // Merge messages from same user that are sent within one minute const int MERGE_TEXT_TIMESTAMP_DIFF_SEC = 60; MessageBoard::MessageBoard(std::shared_ptr _room) : room(_room), scroll(0.0), scrollSpeed(0.0), totalHeight(0.0), scrollToBottom(false), visibleMessageStartIndex(-1), visibleMessageEndIndex(-1) { scrollbar.backgroundColor = sf::Color(49, 52, 57); scrollbar.scrollColor = sf::Color(37, 39, 44); offlineUser = make_shared(odhtdb::Signature::PublicKey()); offlineUser->nickname = "You"; systemUser = make_shared(odhtdb::Signature::PublicKey()); systemUser->nickname = "System"; } MessageBoard::~MessageBoard() { // TODO: Re-add for(Message *message : messages) { delete message; } } // TODO: Optimize this with binary search usize MessageBoard::findPositionToInsertMessageByTimestamp(Message *message) { for(usize i = 0; i < messages.size(); ++i) { if(message->roomMessage->timestampSeconds < messages[i]->roomMessage->timestampSeconds) return i; } return messages.size(); } void MessageBoard::updateStaticContentTexture(const sf::Vector2u &newSize) { //if(!staticContentTexture.create(newSize.x, newSize.y)) // throw std::runtime_error("Failed to create render target for message board!"); dirty = true; } bool MessageBoard::addMessage(Message *message) { lock_guard lock(messageProcessMutex); bool emptyHash = message->roomMessage->id.isEmpty(); if(!emptyHash && messageIdMap.find(message->roomMessage->id) != messageIdMap.end()) { delete message; return false; } auto positionToAddMessage = findPositionToInsertMessageByTimestamp(message); if(positionToAddMessage == messages.size()) scrollToBottom = true; messages.insert(messages.begin() + positionToAddMessage, message); if(!emptyHash) messageIdMap[message->roomMessage->id] = message; dirty = true; return true; } void MessageBoard::deleteMessage(const odhtdb::Hash &id, const odhtdb::Signature::PublicKey &requestedByUser) { lock_guard lock(messageProcessMutex); auto it = messageIdMap.find(id); if(it == messageIdMap.end()) return; if(it->second->onlineUser) { const auto &publicKey = it->second->roomMessage->creator->publicKey; if(publicKey != requestedByUser) { fprintf(stderr, "Warning: user %s requested to delete a message owned by user %s, ignoring request\n", requestedByUser.toString().c_str(), publicKey.toString().c_str()); return; } } // TODO: Instead of deleting message, cover it with black rectangle for(usize i = 0; i < messages.size(); ++i) { Message *message = messages[i]; if(message == it->second) { delete message; messages.erase(messages.begin() + i); messageIdMap.erase(it); break; } } dirty = true; } void MessageBoard::addOfflineUserMessage(std::string msg, bool plainText) { auto roomMessage = make_shared(); roomMessage->creator = offlineUser; roomMessage->timestampSeconds = time(NULL); roomMessage->text = move(msg); Message *message = new Message(roomMessage, plainText); message->onlineUser = false; addMessage(message); } void MessageBoard::addSystemUserMessage(std::string msg, bool plainText) { auto roomMessage = make_shared(); roomMessage->creator = systemUser; roomMessage->timestampSeconds = time(NULL); roomMessage->text = move(msg); Message *message = new Message(roomMessage, plainText); message->onlineUser = false; addMessage(message); } void MessageBoard::drawDefault(sf::RenderWindow &window, Cache *cache) { const float LINE_SPACING = 5.0f * Settings::getScaling(); const float MESSAGE_PADDING_TOP = 25.0f; const float MESSAGE_PADDING_BOTTOM = 30.0f; const sf::Font *usernameFont = ResourceCache::getFont("fonts/Nunito-Regular.ttf"); const int usernameTextCharacterSize = 20 * Settings::getScaling(); const float usernameTextHeight = usernameFont->getLineSpacing(usernameTextCharacterSize); const sf::Font *timestampFont = ResourceCache::getFont("fonts/Nunito-Regular.ttf"); const int timestampTextCharacterSize = (float)usernameTextCharacterSize * 0.75f; const float timestampTextHeight = timestampFont->getLineSpacing(timestampTextCharacterSize); sf::RectangleShape lineRect(sf::Vector2f(backgroundSizeWithoutPadding.x - LINE_SIDE_PADDING * Settings::getScaling() * 2.0f, LINE_HEIGHT)); lineRect.setFillColor(ColorScheme::getBackgroundColor() + sf::Color(10, 10, 10)); sf::Shader *circleShader = ResourceCache::getShader("shaders/circleMask.glsl", sf::Shader::Fragment); sf::Vector2 position(RoomSidePanel::getWidth() + PADDING_SIDE * Settings::getScaling(), RoomTopPanel::getHeight() + PADDING_TOP); double startHeight = position.y; position.y += scroll; visibleMessageStartIndex = -1; visibleMessageEndIndex = -1; usize numMessages = messages.size(); for(usize i = 0; i < numMessages; ++i) { Message *message = messages[i]; bool mergeTextWithPrev = false; if(i > 0) { const Message *prevMessage = messages[i - 1]; mergeTextWithPrev = prevMessage->roomMessage->creator == message->roomMessage->creator && (message->roomMessage->timestampSeconds == 0 || message->roomMessage->timestampSeconds - prevMessage->roomMessage->timestampSeconds <= MERGE_TEXT_TIMESTAMP_DIFF_SEC); } bool mergeTextWithNext = false; if(i < numMessages - 1) { const Message *nextMessage = messages[i + 1]; mergeTextWithNext = nextMessage->roomMessage->creator == message->roomMessage->creator && (nextMessage->roomMessage->timestampSeconds == 0 || nextMessage->roomMessage->timestampSeconds - message->roomMessage->timestampSeconds <= MERGE_TEXT_TIMESTAMP_DIFF_SEC); } bool visible = false; float startX = floor(position.x + LINE_SIDE_PADDING * Settings::getScaling() - PADDING_SIDE * Settings::getScaling()); if(!mergeTextWithPrev) { position.y += (MESSAGE_PADDING_TOP * Settings::getScaling()); if(position.y + usernameTextHeight > 0.0f && position.y < backgroundPos.y + backgroundSize.y) { visible = true; string usernameStr; // TODO: Add bridge user // if(message->creator->type == User::Type::ONLINE_DISCORD_USER) // { // usernameStr = "(Discord) "; // usernameStr += message->user->getName(); // } // else // { usernameStr = message->roomMessage->creator->nickname; //} sf::Text usernameText(sf::String::fromUtf8(usernameStr.begin(), usernameStr.end()), *usernameFont, usernameTextCharacterSize); usernameText.setFillColor(sf::Color(15, 192, 252)); usernameText.setPosition(sf::Vector2f(floor(startX + (AVATAR_DIAMETER + AVATAR_PADDING_SIDE) * Settings::getScaling()), floor(position.y))); window.draw(usernameText); if(message->roomMessage->timestampSeconds) { time_t time = (time_t)message->roomMessage->timestampSeconds; struct tm *localTimePtr = localtime(&time); char date[30]; strftime(date, sizeof(date), "%Y-%m-%d at %T", localTimePtr); sf::Text timestamp(date, *timestampFont, timestampTextCharacterSize); timestamp.setFillColor(ColorScheme::getTextRegularColor() * sf::Color(255, 255, 255, 50)); timestamp.setPosition(sf::Vector2f(floor(startX + (AVATAR_DIAMETER + AVATAR_PADDING_SIDE) * Settings::getScaling() + usernameText.getLocalBounds().width + USERNAME_TIMESTAMP_SIDE_PADDING * Settings::getScaling()), floor(position.y + 2.0f * Settings::getScaling() + usernameTextHeight * 0.5f - timestampTextHeight * 0.5f))); window.draw(timestamp); } // Max avatar size = 1mb const ContentByUrlResult avatarResult = cache->getContentByUrl(message->roomMessage->creator->avatarUrl, 1024 * 1024); if(avatarResult.type == ContentByUrlResult::Type::CACHED) { circleShader->setUniform("texture", sf::Shader::CurrentTexture); if(avatarResult.cachedType == ContentByUrlResult::CachedType::STATIC_IMAGE) { auto *staticImage = static_cast(avatarResult.staticImage); // TODO: Store this sprite somewhere, might not be efficient to create a new sprite object every frame sf::Sprite sprite(staticImage->texture); auto textureSize = staticImage->texture.getSize(); sprite.setPosition(sf::Vector2f(startX, floor(position.y))); sprite.setScale(sf::Vector2f(AVATAR_DIAMETER * Settings::getScaling() / (float)textureSize.x, AVATAR_DIAMETER * Settings::getScaling() / (float)textureSize.y)); window.draw(sprite, circleShader); } else if(avatarResult.cachedType == ContentByUrlResult::CachedType::GIF) { auto *gif = static_cast(avatarResult.gif); gif->update(); auto gifSize = gif->getSize(); sf::Sprite sprite(gif->texture); sprite.setPosition(sf::Vector2f(startX, floor(position.y))); sprite.setScale(sf::Vector2f(AVATAR_DIAMETER * Settings::getScaling() / (float)gifSize.x, AVATAR_DIAMETER * Settings::getScaling() / (float)gifSize.y)); sprite.setColor(sf::Color::White); window.draw(sprite, circleShader); } } else { sf::CircleShape avatarCircle(AVATAR_DIAMETER * 0.5f * Settings::getScaling(), 60 * Settings::getScaling()); avatarCircle.setPosition(sf::Vector2f(startX, floor(position.y))); avatarCircle.setFillColor(ColorScheme::getBackgroundColor() + sf::Color(30, 30, 30)); window.draw(avatarCircle); } } position.y += usernameTextHeight + USERNAME_PADDING_BOTTOM * Settings::getScaling(); } // No need to perform culling here, that is done in @Text draw function message->text.setCharacterSize(18.0f * Settings::getScaling()); message->text.setMaxWidth(lineRect.getSize().x - (AVATAR_DIAMETER + AVATAR_PADDING_SIDE * 2.0f) * Settings::getScaling()); message->text.setPosition(sf::Vector2f(floor(startX + (AVATAR_DIAMETER + AVATAR_PADDING_SIDE) * Settings::getScaling()), floor(position.y))); bool textDrawn = message->text.draw(window, cache); if(!visible) visible = textDrawn; position.y += message->text.getHeight() + (TEXT_LINE_SPACING * Settings::getScaling()); if(!mergeTextWithNext) { position.y += (MESSAGE_PADDING_BOTTOM * Settings::getScaling()); if(position.y + LINE_HEIGHT > 0.0f && position.y < backgroundPos.y + backgroundSize.y && i + 1 != numMessages) { visible = true; lineRect.setPosition(sf::Vector2f(startX, floor(position.y))); window.draw(lineRect); //drawGradientLine(sf::Vector2f(position.x + LINE_SIDE_PADDING, floor(position.y)), sf::Vector2f(backgroundSizeWithoutPadding.x - LINE_SIDE_PADDING * 2.0f, LINE_HEIGHT), LINE_COLOR, window); } } position.y += LINE_HEIGHT; if(visible) { if(visibleMessageStartIndex == -1) visibleMessageStartIndex = i; visibleMessageEndIndex = i; } } totalHeight = (position.y - scroll) - startHeight; } void MessageBoard::drawSimple(sf::RenderWindow &window, Cache *cache) { const float LINE_SPACING = 20.0f * Settings::getScaling(); const float MESSAGE_PADDING_TOP = 0.0f; const float MESSAGE_PADDING_BOTTOM = 0.0f; const sf::Font *usernameFont = ResourceCache::getFont("fonts/Nunito-Regular.ttf"); const int usernameTextCharacterSize = 20 * Settings::getScaling(); const float usernameTextHeight = usernameFont->getLineSpacing(usernameTextCharacterSize); const float usernameMaxWidth = usernameTextHeight * 5.0f; const sf::Font *timestampFont = ResourceCache::getFont("fonts/Nunito-Regular.ttf"); const int timestampTextCharacterSize = 15 * Settings::getScaling(); const float timestampTextHeight = timestampFont->getLineSpacing(timestampTextCharacterSize); sf::Vector2 position(RoomSidePanel::getWidth() + PADDING_SIDE * Settings::getScaling() + usernameMaxWidth, RoomTopPanel::getHeight() + PADDING_TOP); double startHeight = position.y; position.y += scroll; usize numMessages = messages.size(); for(usize i = 0; i < numMessages; ++i) { Message *message = messages[i]; position.y += (MESSAGE_PADDING_TOP * Settings::getScaling()); if(position.y + usernameTextHeight > 0.0f && position.y < backgroundPos.y + backgroundSize.y) { sf::String usernameTextStr = sf::String::fromUtf8(message->roomMessage->creator->nickname.begin(), message->roomMessage->creator->nickname.end()); usernameTextStr += " - "; sf::Text usernameText(usernameTextStr, *usernameFont, usernameTextCharacterSize); usernameText.setFillColor(sf::Color(15, 192, 252)); usernameText.setPosition(sf::Vector2f(floor(position.x - usernameText.getLocalBounds().width), floor(position.y))); window.draw(usernameText); if(message->roomMessage->timestampSeconds) { time_t time = (time_t)message->roomMessage->timestampSeconds; struct tm *localTimePtr = localtime(&time); char date[30]; strftime(date, sizeof(date), "%Y-%m-%d at %T", localTimePtr); //sf::Text timestamp(date, *timestampFont, timestampTextCharacterSize); //timestamp.setFillColor(ColorScheme::getTextRegularColor() * sf::Color(255, 255, 255, 30)); //timestamp.setPosition(sf::Vector2f(floor(position.x - usernameText.getLocalBounds().width + usernameText.getLocalBounds().width + USERNAME_TIMESTAMP_SIDE_PADDING * Settings::getScaling()), floor(position.y + 2.0f * Settings::getScaling() + usernameTextHeight * 0.5f - timestampTextHeight * 0.5f))); //window.draw(timestamp); } } // No need to perform culling here, that is done in @Text draw function message->text.setCharacterSize(18 * Settings::getScaling()); message->text.setMaxWidth(backgroundSize.x - usernameMaxWidth * 2.0f); message->text.setPosition(sf::Vector2f(floor(position.x), floor(position.y))); message->text.setLineSpacing(LINE_SPACING); message->text.draw(window, cache); position.y += (message->text.getHeight() + MESSAGE_PADDING_BOTTOM * Settings::getScaling()); } totalHeight = (position.y - scroll) - startHeight; } void MessageBoard::processEvent(const sf::Event &event, Cache *cache) { lock_guard lock(messageProcessMutex); std::shared_ptr onlineLocalUser = nullptr; if(room) onlineLocalUser = room->localUser; bool openContextMenu = false; if(onlineLocalUser && event.type == sf::Event::MouseButtonPressed && event.mouseButton.button == sf::Mouse::Button::Right) openContextMenu = true; if(visibleMessageStartIndex != -1 && visibleMessageStartIndex < messages.size() && visibleMessageEndIndex < messages.size()) { //printf("visibleMessageStartIndex: %u, visibleMessageEndIndex: %u\n", visibleMessageStartIndex, visibleMessageEndIndex); for(usize i = visibleMessageStartIndex; i <= visibleMessageEndIndex; ++i) { Message *message = messages[i]; message->text.processEvent(event, cache); auto textPos = message->text.getPosition(); if(openContextMenu && message->roomMessage->creator == onlineLocalUser && event.mouseButton.x >= textPos.x && event.mouseButton.x <= textPos.x + message->text.getMaxWidth() && event.mouseButton.y >= textPos.y && event.mouseButton.y <= textPos.y + message->text.getHeight()) { auto contextMenu = GlobalContextMenu::getEditMessageContextMenu(); contextMenu->setPosition(sf::Vector2f(event.mouseButton.x, event.mouseButton.y)); contextMenu->setVisible(true); GlobalContextMenu::setClickDeleteMessageCallbackFunc([](ContextMenuItem *menuItem) { // TODO: Add: room->deleteMessage(); //channel->deleteMessage(message->id, onlineLocalUser->getPublicKey()); GlobalContextMenu::setClickDeleteMessageCallbackFunc(nullptr); }); } } } if(event.type == sf::Event::MouseWheelScrolled && event.mouseWheelScroll.wheel == sf::Mouse::Wheel::VerticalWheel) { scrollSpeed += (event.mouseWheelScroll.delta * 5.0); if(scrollSpeed > SCROLL_MAX_SPEED) scrollSpeed = SCROLL_MAX_SPEED; else if(scrollSpeed < -SCROLL_MAX_SPEED) scrollSpeed = -SCROLL_MAX_SPEED; } } void MessageBoard::draw(sf::RenderWindow &window, Cache *cache) { auto windowSize = window.getSize(); backgroundSizeWithoutPadding = sf::Vector2f(floor(windowSize.x - RoomSidePanel::getWidth()), floor(windowSize.y - RoomTopPanel::getHeight() - Chatbar::getHeight())); backgroundSize = sf::Vector2f(floor(windowSize.x - RoomSidePanel::getWidth() - PADDING_SIDE * Settings::getScaling() * 2.0f), floor(windowSize.y - RoomTopPanel::getHeight() - Chatbar::getHeight() - PADDING_TOP)); backgroundPos = sf::Vector2f(RoomSidePanel::getWidth(), RoomTopPanel::getHeight()); //if(backgroundSize != staticContentTexture.getSize()) // updateStaticContentTexture(backgroundSize); // TODO: Remove this when dchat::Text can render to static and dynamic render target dirty = true; //if(dirty) // staticContentTexture.clear(BACKGROUND_COLOR); sf::RectangleShape backgroundRect(backgroundSizeWithoutPadding); backgroundRect.setFillColor(ColorScheme::getBackgroundColor()); backgroundRect.setPosition(RoomSidePanel::getWidth(), RoomTopPanel::getHeight()); window.draw(backgroundRect); double deltaTimeMicro = (double)frameTimer.getElapsedTime().asMicroseconds(); frameTimer.restart(); if(dirty) { lock_guard lock(messageProcessMutex); switch(Theme::getType()) { case Theme::Type::DEFAULT: drawDefault(window, cache); break; case Theme::Type::SIMPLE: drawSimple(window, cache); break; } } scroll += scrollSpeed; double deltaTimeScrollMultiplier = deltaTimeMicro * 0.0001; if(scrollSpeed > 0.0) { scrollSpeed -= deltaTimeScrollMultiplier; } else { scrollSpeed += deltaTimeScrollMultiplier; } if(abs(scrollSpeed - deltaTimeScrollMultiplier) <= deltaTimeScrollMultiplier) scrollSpeed = 0.0; double textOverflow = (double)backgroundSize.y - totalHeight; if(scroll > 0.0 || textOverflow > 0.0) { scroll = 0.0; scrollSpeed = 0.0; } else if(textOverflow < 0.0 && scroll < textOverflow) { scroll = textOverflow; scrollSpeed = 0.0; } if(scrollToBottom) { scrollToBottom = false; if(textOverflow < 0.0) scroll = textOverflow; else scroll = 0.0; } scrollbar.scroll = abs(scroll); scrollbar.maxScroll = totalHeight; scrollbar.width = 10.0f * Settings::getScaling(); scrollbar.maxHeight = (float)backgroundSize.y; scrollbar.position.x = windowSize.x - scrollbar.width; scrollbar.position.y = backgroundPos.y; scrollbar.draw(window); scroll = scrollbar.getScrollingForContent(); //staticContentTexture.display(); dirty = false; // TODO: Save this, expensive to create on fly? //sf::Sprite textureSprite(staticContentTexture.getTexture()); //textureSprite.setPosition(backgroundPos); //window.draw(textureSprite); } }