diff options
-rw-r--r-- | TODO | 11 | ||||
-rw-r--r-- | include/Body.hpp | 23 | ||||
-rw-r--r-- | include/DownloadUtils.hpp | 2 | ||||
-rw-r--r-- | include/Storage.hpp | 5 | ||||
-rw-r--r-- | include/Text.hpp | 1 | ||||
-rw-r--r-- | plugins/Matrix.hpp | 11 | ||||
-rw-r--r-- | src/Body.cpp | 143 | ||||
-rw-r--r-- | src/DownloadUtils.cpp | 1 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 148 | ||||
-rw-r--r-- | src/Storage.cpp | 1 | ||||
-rw-r--r-- | src/Text.cpp | 9 | ||||
-rw-r--r-- | src/plugins/Matrix.cpp | 399 |
12 files changed, 429 insertions, 325 deletions
@@ -42,7 +42,7 @@ Add ctrl+s to save the previewing image/video (for images that would be a copy, Show the last message in a room as the body item description in matrix room view. Show some kind of indication that there are new messages in a room in matrix room view, and also show another indication if somebody mentioned us (and how many times). Show the rooms menu on the left side when the window is large in matrix. -Use https://github.com/simdjson/simdjson as a json library. +Use https://github.com/simdjson/simdjson as a json library in other parts than matrix. Sanitize check: do not allow pasting more than 2gb of text. Add search bar for matrix rooms. Put rooms with recent messages at the top and the ones that mention us further at the top (matrix), and also add a tab for favorited rooms? or tag them, whatever. @@ -71,7 +71,6 @@ Move rooms in matrix to previous page instead, then messages can be beside users Add /me to matrix, emoji, reactions... Set the icon of the window to be the icon of the plugin. Nice for KDE, GNOME, etc with titlebars. Set a minimum wrap size for text. We dont want one line of text to fully fill the window vertically when the window size width is small. Its better to cut off the text and add eclipses. -Get the related message and show that (async) instead of what the reply says the original text is in body, for matrix replies. Support matrix html (for replies and text styling, such as greentext). Use linear-interpolation for thumbnail creation. If --no-audio is used then music should be played with a lightweight music player instead. MPV is heavy even for music (60mb RAM). @@ -103,4 +102,10 @@ Retry download if it fails, at least 3 times (observed to be needed for mangadex Readd autocomplete, but make it better with a proper list. Also readd 4chan login page and manganelo creators page. Fix logout/login in matrix. Currently it doesn't work because data is cleared while sync is in progress, leading to the first sync sometimes being with previous data... Modify sfml to use GL_COMPRESSED_LUMINANCE and other texture compression modes (in sf::Texture). This reduces memory usage by half. -Decrease memory usage even further (mostly in matrix /sync when part of large rooms) by using rapidjson SAX style API to stream json string into SAX style parsing.
\ No newline at end of file +Decrease memory usage even further (mostly in matrix /sync when part of large rooms) by using rapidjson SAX style API to stream json string into SAX style parsing. +Dont show red marker when receiving edit/redaction events in matrix. +Sometimes we fail to get images in mangadex, most common reason being that the manga is licensed and we can't view the manga on mangadex. QuickMedia should implement mangaplus and redirect us to mangaplus plugin to view the manga, or simply show that we cant view the manga because its licensed. +Show redacted messages even when part of the initial sync in matrix. Right now they are hidden while new sync redacted messages are not. +Update displayname/avatar in matrix when updated in /sync. +Fix inconsistent behavior when editing a message that is replied to in matrix. Right now if the replied to message already exits in the body then its used directly and when editing that message the reply message shows the edit embedded, but not if the edit is of an body item that is created because we dont already have it, +to fix this we could perhaps replace the newly created body items for replies when loading old messages and one of the old messages is also one of the embedded messages (by event id).
\ No newline at end of file diff --git a/include/Body.hpp b/include/Body.hpp index 09b477d..cdb0ad0 100644 --- a/include/Body.hpp +++ b/include/Body.hpp @@ -18,10 +18,16 @@ namespace sf { namespace QuickMedia { class Program; + enum class EmbeddedItemStatus { + NONE, + LOADING, + FINISHED_LOADING, + FAILED_TO_LOAD + }; + class BodyItem { public: BodyItem(std::string _title); - BodyItem(const BodyItem &other); static std::shared_ptr<BodyItem> create(std::string title) { return std::make_shared<BodyItem>(std::move(title)); } @@ -89,7 +95,10 @@ namespace QuickMedia { sf::Color author_color; void *userdata; // Not managed, should be deallocated by whoever sets this sf::Int32 last_drawn_time; + EmbeddedItemStatus embedded_item_status = EmbeddedItemStatus::NONE; + std::shared_ptr<BodyItem> embedded_item; // Used by matrix for example to display reply message body. Note: only the first level of embedded items is rendered (not recursive, this is done on purpose) private: + // TODO: Clean up these strings when set in text, and get_title for example should return |title_text.getString()| std::string title; std::string description; std::string author; @@ -97,6 +106,7 @@ namespace QuickMedia { }; using BodyItems = std::vector<std::shared_ptr<BodyItem>>; + using BodyItemRenderCallback = std::function<void(BodyItem *body_item)>; class Body { public: @@ -141,9 +151,11 @@ namespace QuickMedia { // |size| is the clip size, another outside this will be cut off. // Note: this should be called after |draw|, or thumbnails will be messed up. TODO: find a way to solve this issue in a clean way. // This happens because of |draw| sets thumbnails as unreferenced at the beginning and cleans them up at the end if they are not drawn in the same function call. - void draw_item(sf::RenderWindow &window, BodyItem *item, sf::Vector2f pos, sf::Vector2f size); + // TODO: Right now drawing an item that also exists in the body will cause the text to update geometry every frame if the text is wrapping text and the items are drawn at different sizes, + // because of Text::setMaxWidth + void draw_item(sf::RenderWindow &window, BodyItem *item, sf::Vector2f pos, sf::Vector2f size, bool include_embedded_item = true); - float get_item_height(BodyItem *item, bool load_texture = true); + float get_item_height(BodyItem *item, bool load_texture = true, bool include_embedded_item = true); float get_spacing_y() const; static bool string_find_case_insensitive(const std::string &str, const std::string &substr); @@ -156,6 +168,7 @@ namespace QuickMedia { bool no_items_visible() const; int get_selected_item() const { return selected_item; } + bool is_selected_item_last_visible_item() const; void set_page_scroll(float scroll) { page_scroll = scroll; } float get_page_scroll() const { return page_scroll; } @@ -166,6 +179,7 @@ namespace QuickMedia { sf::Font *cjk_font; sf::Text progress_text; sf::Text replies_text; + sf::Text embedded_item_load_text; BodyItems items; bool draw_thumbnails; bool wrap_around; @@ -173,8 +187,9 @@ namespace QuickMedia { sf::Vector2i thumbnail_resize_target_size; sf::Vector2f thumbnail_fallback_size; sf::Color line_seperator_color; + BodyItemRenderCallback body_item_render_callback; private: - void draw_item(sf::RenderWindow &window, BodyItem *item, const sf::Vector2f &pos, const sf::Vector2f &size, const float item_height, const int item_index, const Json::Value &content_progress); + void draw_item(sf::RenderWindow &window, BodyItem *item, const sf::Vector2f &pos, const sf::Vector2f &size, const float item_height, const int item_index, const Json::Value &content_progress, bool include_embedded_item = true); void update_dirty_state(BodyItem *body_item, sf::Vector2f size); void clear_body_item_cache(BodyItem *body_item); private: diff --git a/include/DownloadUtils.hpp b/include/DownloadUtils.hpp index 2d2b813..52c5b80 100644 --- a/include/DownloadUtils.hpp +++ b/include/DownloadUtils.hpp @@ -2,7 +2,7 @@ #include <string> #include <vector> -#include <rapidjson/document.h> +#include <rapidjson/fwd.h> namespace QuickMedia { enum class DownloadResult { diff --git a/include/Storage.hpp b/include/Storage.hpp index 10cbbb2..ae7db04 100644 --- a/include/Storage.hpp +++ b/include/Storage.hpp @@ -4,10 +4,7 @@ #include <functional> #include <filesystem> #include <json/value.h> -#ifdef Bool -#undef Bool -#endif -#include <rapidjson/document.h> +#include <rapidjson/fwd.h> namespace QuickMedia { // Return false to stop the iterator diff --git a/include/Text.hpp b/include/Text.hpp index 7e9dda2..a71bc8e 100644 --- a/include/Text.hpp +++ b/include/Text.hpp @@ -141,6 +141,7 @@ namespace QuickMedia bool editable; CaretMoveDirection caretMoveDirection; sf::FloatRect boundingBox; + int num_lines; float lineSpacing; float characterSpacing; std::vector<TextElement> textElements; diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index 8d078a0..9f58b24 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -6,7 +6,7 @@ #include <SFML/Graphics/Color.hpp> #include <unordered_map> #include <mutex> -#include <rapidjson/document.h> +#include <rapidjson/fwd.h> namespace QuickMedia { // Dummy, only play one video. TODO: Play all videos in room, as related videos? @@ -91,6 +91,11 @@ namespace QuickMedia { std::string avatar_url; std::string prev_batch; bool initial_fetch_finished = false; + + // These are messages fetched with |Matrix::get_message_by_id|. Needed to show replies, when replying to old message not part of /sync. + // The value is nullptr if the message is fetched and cached but the event if referenced an invalid message. + // TODO: Verify if replied to messages are also part of /sync; then this is not needed. + std::unordered_map<std::string, std::shared_ptr<Message>> fetched_messages_by_event_id; private: std::mutex user_mutex; std::mutex room_mutex; @@ -157,6 +162,9 @@ namespace QuickMedia { std::shared_ptr<UserInfo> get_me(std::shared_ptr<RoomData> room); + // Returns nullptr if message cant be found. Note: cached + std::shared_ptr<Message> get_message_by_id(RoomData *room, const std::string &event_id); + bool use_tor = false; private: PluginResult sync_response_to_body_items(const rapidjson::Document &root, RoomSyncMessages &room_messages); @@ -165,6 +173,7 @@ namespace QuickMedia { void events_add_user_read_markers(const rapidjson::Value &events_json, RoomData *room_data); void events_add_messages(const rapidjson::Value &events_json, std::shared_ptr<RoomData> &room_data, MessageDirection message_dir, RoomSyncMessages *room_messages, bool has_unread_notifications); void events_set_room_name(const rapidjson::Value &events_json, RoomData *room_data); + std::shared_ptr<Message> parse_message_event(const rapidjson::Value &event_item_json, RoomData *room_data); PluginResult upload_file(std::shared_ptr<RoomData> room, const std::string &filepath, UploadInfo &file_info, UploadInfo &thumbnail_info, std::string &err_msg); std::shared_ptr<Message> get_edited_message_original_message(RoomData *room_data, std::shared_ptr<Message> message); diff --git a/src/Body.cpp b/src/Body.cpp index 9b12f43..f6800b8 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -6,13 +6,14 @@ #include <assert.h> #include <cmath> -const sf::Color front_color(32, 36, 42); -const sf::Color back_color(33, 35, 37); -float image_max_height = 100.0f; -const float spacing_y = 15.0f; -const float padding_x = 10.0f; -const float image_padding_x = 5.0f; -const float padding_y = 5.0f; +static const sf::Color front_color(32, 36, 42); +static const sf::Color back_color(33, 35, 37); +static float image_max_height = 100.0f; +static const float spacing_y = 15.0f; +static const float padding_x = 10.0f; +static const float image_padding_x = 5.0f; +static const float padding_y = 5.0f; +static const float embedded_item_padding_y = 0.0f; namespace QuickMedia { BodyItem::BodyItem(std::string _title) : @@ -32,50 +33,13 @@ namespace QuickMedia { set_title(std::move(_title)); } - BodyItem::BodyItem(const BodyItem &other) { - title = other.title; - description = other.description; - url = other.url; - thumbnail_url = other.thumbnail_url; - attached_content_url = other.attached_content_url; - author = other.author; - visible = other.visible; - dirty = other.dirty; - dirty_description = other.dirty_description; - dirty_author = other.dirty_author; - dirty_timestamp = other.dirty_timestamp; - thumbnail_is_local = other.thumbnail_is_local; - if(other.title_text) - title_text = std::make_unique<Text>(*other.title_text); - else - title_text = nullptr; - if(other.description_text) - description_text = std::make_unique<Text>(*other.description_text); - else - description_text = nullptr; - if(other.author_text) - author_text = std::make_unique<Text>(*other.author_text); - else - author_text = nullptr; - if(other.timestamp_text) - timestamp_text = std::make_unique<sf::Text>(*other.timestamp_text); - else - timestamp_text = nullptr; - replies = other.replies; - post_number = other.post_number; - title_color = other.title_color; - author_color = other.author_color; - userdata = other.userdata; - last_drawn_time = other.last_drawn_time; - timestamp = other.timestamp; - } - Body::Body(Program *program, sf::Font *font, sf::Font *bold_font, sf::Font *cjk_font) : font(font), bold_font(bold_font), cjk_font(cjk_font), progress_text("", *font, 14), replies_text("", *font, 14), + embedded_item_load_text("", *font, 14), draw_thumbnails(false), wrap_around(false), line_seperator_color(sf::Color(32, 37, 43, 255)), @@ -305,7 +269,7 @@ namespace QuickMedia { } void Body::draw(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size) { - draw(window, pos, size, Json::nullValue); + draw(window, pos, size, Json::Value::nullSingleton()); } // TODO: Use a render target for the whole body so all images can be put into one. @@ -395,6 +359,7 @@ namespace QuickMedia { continue; update_dirty_state(item.get(), size); + item->last_drawn_time = elapsed_time; float item_height = get_item_height(item.get()); prev_pos.y -= (item_height + spacing_y); @@ -402,7 +367,6 @@ namespace QuickMedia { if(prev_pos.y + item_height + spacing_y < start_y) break; - item->last_drawn_time = elapsed_time; // This is needed here rather than above the loop, since update_dirty_text cant be called inside scissor because it corrupts the text for some reason glEnable(GL_SCISSOR_TEST); glScissor(scissor_pos.x, (int)window_size.y - (int)scissor_pos.y - (int)scissor_size.y, scissor_size.x, scissor_size.y); @@ -420,6 +384,7 @@ namespace QuickMedia { continue; update_dirty_state(item.get(), size); + item->last_drawn_time = elapsed_time; float item_height = get_item_height(item.get()); @@ -431,7 +396,6 @@ namespace QuickMedia { if(after_pos.y - start_y >= size.y) break; - item->last_drawn_time = elapsed_time; // This is needed here rather than above the loop, since update_dirty_text cant be called inside scissor because it corrupts the text for some reason glEnable(GL_SCISSOR_TEST); glScissor(scissor_pos.x, (int)window_size.y - (int)scissor_pos.y - (int)scissor_size.y, scissor_size.x, scissor_size.y); @@ -543,17 +507,32 @@ namespace QuickMedia { } } - void Body::draw_item(sf::RenderWindow &window, BodyItem *item, sf::Vector2f pos, sf::Vector2f size) { + void Body::draw_item(sf::RenderWindow &window, BodyItem *item, sf::Vector2f pos, sf::Vector2f size, bool include_embedded_item) { update_dirty_state(item, size); item->last_drawn_time = draw_timer.getElapsedTime().asMilliseconds(); sf::Vector2u window_size = window.getSize(); glEnable(GL_SCISSOR_TEST); glScissor(pos.x, (int)window_size.y - (int)pos.y - (int)size.y, size.x, size.y); - draw_item(window, item, pos, size, get_item_height(item) + spacing_y, -1, Json::nullValue); + draw_item(window, item, pos, size, size.y + spacing_y, -1, Json::Value::nullSingleton(), include_embedded_item); glDisable(GL_SCISSOR_TEST); } - void Body::draw_item(sf::RenderWindow &window, BodyItem *item, const sf::Vector2f &pos, const sf::Vector2f &size, const float item_height, const int item_index, const Json::Value &content_progress) { + // TODO: Better message? maybe fallback to the reply message, or message status (such as message redacted) + static const char* embedded_item_status_to_string(EmbeddedItemStatus embedded_item_status) { + switch(embedded_item_status) { + case EmbeddedItemStatus::NONE: + return ""; + case EmbeddedItemStatus::LOADING: + return "Loading message..."; + case EmbeddedItemStatus::FINISHED_LOADING: + return "Finished loading message..."; + case EmbeddedItemStatus::FAILED_TO_LOAD: + return "Failed to load message!"; + } + return ""; + } + + void Body::draw_item(sf::RenderWindow &window, BodyItem *item, const sf::Vector2f &pos, const sf::Vector2f &size, const float item_height, const int item_index, const Json::Value &content_progress, bool include_embedded_item) { // TODO: Instead of generating a new hash everytime to access textures, cache the hash of the thumbnail url std::shared_ptr<ThumbnailData> item_thumbnail; if(draw_thumbnails) { @@ -567,6 +546,9 @@ namespace QuickMedia { item_thumbnail->referenced = true; } + if(body_item_render_callback) + body_item_render_callback(item); + sf::Vector2f item_pos; item_pos.x = std::floor(pos.x); item_pos.y = std::floor(pos.y); @@ -605,9 +587,10 @@ namespace QuickMedia { } } + const float timestamp_text_y = std::floor(item_pos.y + padding_y - 6.0f); if(item->author_text) { item->author_text->setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y - 6.0f)); - item->author_text->setMaxWidth(size.x - text_offset_x - image_padding_x * 2.0f); + item->author_text->setMaxWidth(size.x - text_offset_x - image_padding_x); item->author_text->draw(window); sf::Vector2f replies_text_pos = item->author_text->getPosition() + sf::Vector2f(0.0f, 5.0f); @@ -625,28 +608,48 @@ namespace QuickMedia { item_pos.y += item->author_text->getHeight() - 2.0f; } + + if(include_embedded_item && item->embedded_item_status != EmbeddedItemStatus::NONE) { + float embedded_item_height = item->embedded_item ? get_item_height(item->embedded_item.get(), true, false) : (embedded_item_load_text.getLocalBounds().height + embedded_item_padding_y * 2.0f); + const float border_width = 4.0f; + sf::RectangleShape border_left(sf::Vector2f(border_width, std::floor(embedded_item_height))); + border_left.setFillColor(sf::Color::White); + border_left.setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + 4.0f)); + window.draw(border_left); + + if(item->embedded_item) { + sf::Vector2f embedded_item_pos(std::floor(item_pos.x + text_offset_x + border_width + padding_x), std::floor(item_pos.y + embedded_item_padding_y + 4.0f)); + sf::Vector2f embedded_item_size(std::floor(size.x - text_offset_x - border_width - padding_x), embedded_item_height); + draw_item(window, item->embedded_item.get(), embedded_item_pos, embedded_item_size, false); + } else { + embedded_item_load_text.setString(embedded_item_status_to_string(item->embedded_item_status)); + embedded_item_load_text.setPosition(std::floor(item_pos.x + text_offset_x + border_width + padding_x), std::floor(item_pos.y + embedded_item_height * 0.5f - embedded_item_load_text.getLocalBounds().height * 0.5f + 4.0f)); + window.draw(embedded_item_load_text); + } + item_pos.y += embedded_item_height + 4.0f; + } + //title_text.setString(item->title); //title_text.setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y)); //window.draw(title_text); if(item->title_text) { item->title_text->setFillColor(item->title_color); - item->title_text->setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y - 8.0f)); - item->title_text->setMaxWidth(size.x - text_offset_x - image_padding_x * 2.0f); + item->title_text->setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y - 6.0f)); + item->title_text->setMaxWidth(size.x - text_offset_x - image_padding_x); item->title_text->draw(window); + item_pos.y += item->title_text->getHeight() - 2.0f; } if(item->description_text) { float height_offset = 0.0f; - if(!item->get_title().empty()) { - height_offset = item->title_text->getHeight(); - } - item->description_text->setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y - 8.0f + height_offset)); - item->description_text->setMaxWidth(size.x - text_offset_x - image_padding_x * 2.0f); + item->description_text->setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y - 6.0f + height_offset)); + item->description_text->setMaxWidth(size.x - text_offset_x - image_padding_x); item->description_text->draw(window); + item_pos.y += item->description_text->getHeight() - 2.0f; } if(item->timestamp_text) { - item->timestamp_text->setPosition(std::floor(item_pos.x + size.x - item->timestamp_text->getLocalBounds().width - padding_x), std::floor(item_pos.y + padding_y - 18.0f)); + item->timestamp_text->setPosition(std::floor(item_pos.x + size.x - item->timestamp_text->getLocalBounds().width - padding_x), timestamp_text_y + 4.0f); window.draw(*item->timestamp_text); } @@ -662,13 +665,13 @@ namespace QuickMedia { if(current_json.isNumeric() && total_json.isNumeric()) { progress_text.setString(std::string("Page: ") + std::to_string(current_json.asInt()) + "/" + std::to_string(total_json.asInt())); auto bounds = progress_text.getLocalBounds(); - progress_text.setPosition(std::floor(item_pos.x + size.x - bounds.width - padding_x), std::floor(item_pos.y + padding_y)); + progress_text.setPosition(std::floor(item_pos.x + size.x - bounds.width - padding_x), timestamp_text_y + 6.0f); window.draw(progress_text); } } } - float Body::get_item_height(BodyItem *item, bool load_texture) { + float Body::get_item_height(BodyItem *item, bool load_texture, bool include_embedded_item) { float item_height = 0.0f; if(item->title_text) { item_height += item->title_text->getHeight() - 2.0f; @@ -676,6 +679,12 @@ namespace QuickMedia { if(item->author_text) { item_height += item->author_text->getHeight() - 2.0f; } + if(include_embedded_item && item->embedded_item_status != EmbeddedItemStatus::NONE) { + if(item->embedded_item) + item_height += (get_item_height(item->embedded_item.get(), load_texture, false) + 4.0f + embedded_item_padding_y * 2.0f); + else + item_height += (embedded_item_load_text.getLocalBounds().height + 4.0f + embedded_item_padding_y * 2.0f); + } if(item->description_text) { item_height += item->description_text->getHeight() - 2.0f; } @@ -757,4 +766,14 @@ namespace QuickMedia { } return true; } + + bool Body::is_selected_item_last_visible_item() const { + if(selected_item < 0 || selected_item >= (int)items.size() || !items[selected_item]->visible) + return false; + for(int i = selected_item + 1; i < (int)items.size(); ++i) { + if(items[i]->visible) + return false; + } + return true; + } }
\ No newline at end of file diff --git a/src/DownloadUtils.cpp b/src/DownloadUtils.cpp index 5b39bdd..8782020 100644 --- a/src/DownloadUtils.cpp +++ b/src/DownloadUtils.cpp @@ -3,6 +3,7 @@ #include "../include/Storage.hpp" #include "../include/base64_url.hpp" #include <SFML/System/Clock.hpp> +#include <rapidjson/document.h> #include <rapidjson/filereadstream.h> static const bool debug_download = false; diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 80567f2..bd4b2d4 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -2872,27 +2872,45 @@ namespace QuickMedia { } } + static std::string remove_reply_formatting(const std::string &str) { + if(strncmp(str.c_str(), "> <@", 4) == 0) { + size_t index = str.find("> ", 4); + if(index != std::string::npos) { + size_t msg_begin = str.find("\n\n", index + 2); + if(msg_begin != std::string::npos) + return str.substr(msg_begin + 2); + } + } + return str; + } + + static std::shared_ptr<BodyItem> message_to_body_item(Message *message) { + auto body_item = BodyItem::create(""); + body_item->set_author(message->user->display_name); + std::string text = message->body; + if(message->related_event_type == RelatedEventType::REPLY) + text = remove_reply_formatting(text); + body_item->set_description(std::move(text)); + body_item->set_timestamp(message->timestamp); + if(!message->thumbnail_url.empty()) + body_item->thumbnail_url = message->thumbnail_url; + else if(!message->url.empty() && message->type == MessageType::IMAGE) + body_item->thumbnail_url = message->url; + else + body_item->thumbnail_url = message->user->avatar_url; + // TODO: Show image thumbnail inline instead of url to image and showing it as the thumbnail of the body item + body_item->url = message->url; + body_item->author_color = message->user->display_name_color; + body_item->userdata = (void*)message; // Note: message has to be valid as long as body_item is used! + if(message->related_event_type == RelatedEventType::REDACTION || message->related_event_type == RelatedEventType::EDIT) + body_item->visible = false; + return body_item; + } + static BodyItems messages_to_body_items(const Messages &messages) { BodyItems result_items(messages.size()); for(size_t i = 0; i < messages.size(); ++i) { - auto &message = messages[i]; - auto body_item = BodyItem::create(""); - body_item->set_author(message->user->display_name); - body_item->set_description(message->body); - body_item->set_timestamp(message->timestamp); - if(!message->thumbnail_url.empty()) - body_item->thumbnail_url = message->thumbnail_url; - else if(!message->url.empty() && message->type == MessageType::IMAGE) - body_item->thumbnail_url = message->url; - else - body_item->thumbnail_url = message->user->avatar_url; - // TODO: Show image thumbnail inline instead of url to image and showing it as the thumbnail of the body item - body_item->url = message->url; - body_item->author_color = message->user->display_name_color; - body_item->userdata = (void*)message.get(); // Note: message has to be valid as long as body_item is used! - result_items[i] = std::move(body_item); - if(message->related_event_type == RelatedEventType::REDACTION || message->related_event_type == RelatedEventType::EDIT) - result_items[i]->visible = false; + result_items[i] = message_to_body_item(messages[i].get()); } return result_items; } @@ -3092,6 +3110,50 @@ namespace QuickMedia { bool fetching_previous_messages_running = false; std::shared_ptr<RoomData> previous_messages_future_room; + std::future<std::shared_ptr<Message>> fetch_reply_message_future; + bool fetching_reply_message_running = false; + std::shared_ptr<RoomData> fetch_reply_future_room; + BodyItem *fetch_reply_body_item = nullptr; + + // TODO: Optimize with hash map? + auto find_body_item_by_event_id = [](std::shared_ptr<BodyItem> *body_items, size_t num_body_items, const std::string &event_id) -> std::shared_ptr<BodyItem> { + for(size_t i = 0; i < num_body_items; ++i) { + auto &body_item = body_items[i]; + if(static_cast<Message*>(body_item->userdata)->event_id == event_id) + return body_item; + } + return nullptr; + }; + + // TODO: How about instead fetching all messages we have, not only the visible ones? also fetch with multiple threads. + // TODO: Cancel when going to another room? + tabs[MESSAGES_TAB_INDEX].body->body_item_render_callback = [this, ¤t_room, &fetch_reply_message_future, &tabs, &find_body_item_by_event_id, &fetching_reply_message_running, &fetch_reply_future_room, &fetch_reply_body_item](BodyItem *body_item) { + if(fetching_reply_message_running || !current_room) + return; + + Message *message = static_cast<Message*>(body_item->userdata); + if(message->related_event_id.empty() || body_item->embedded_item_status != EmbeddedItemStatus::NONE) + return; + + // Check if we already have the referenced message as a body item, so we dont create a new one + auto related_body_item = find_body_item_by_event_id(tabs[MESSAGES_TAB_INDEX].body->items.data(), tabs[MESSAGES_TAB_INDEX].body->items.size(), message->related_event_id); + if(related_body_item) { + body_item->embedded_item = related_body_item; + body_item->embedded_item_status = EmbeddedItemStatus::FINISHED_LOADING; + return; + } + + fetching_reply_message_running = true; + std::string message_event_id = message->related_event_id; + fetch_reply_future_room = current_room; + fetch_reply_body_item = body_item; + body_item->embedded_item_status = EmbeddedItemStatus::LOADING; + // TODO: Check if the message is already cached before calling async? is this needed? is async creation expensive? + fetch_reply_message_future = std::async(std::launch::async, [this, &fetch_reply_future_room, message_event_id]() { + return matrix->get_message_by_id(fetch_reply_future_room.get(), message_event_id); + }); + }; + const float tab_spacer_height = 0.0f; sf::Vector2f body_pos; sf::Vector2f body_size; @@ -3220,7 +3282,7 @@ namespace QuickMedia { auto add_new_messages_to_current_room = [&tabs](Messages &messages) { int num_items = tabs[MESSAGES_TAB_INDEX].body->items.size(); - bool scroll_to_end = (num_items == 0 || (num_items > 0 && tabs[MESSAGES_TAB_INDEX].body->get_selected_item() == num_items - 1)); + bool scroll_to_end = (num_items == 0 || tabs[MESSAGES_TAB_INDEX].body->is_selected_item_last_visible_item()); BodyItem *selected_item = tabs[MESSAGES_TAB_INDEX].body->get_selected(); tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps(messages_to_body_items(messages)); @@ -3269,28 +3331,21 @@ namespace QuickMedia { return nullptr; }; - // TODO: Optimize with hash map? - auto find_body_items_by_event_id = [](std::shared_ptr<BodyItem> *body_items, size_t num_body_items, const std::string &event_id) -> std::shared_ptr<BodyItem> { - for(size_t i = 0; i < num_body_items; ++i) { - auto &body_item = body_items[i]; - if(static_cast<Message*>(body_item->userdata)->event_id == event_id) - return body_item; - } - return nullptr; - }; - // TODO: What if these never end up referencing events? clean up automatically after a while? std::unordered_map<std::shared_ptr<RoomData>, Messages> unreferenced_event_by_room; - auto resolve_unreferenced_events_with_body_items = [&unreferenced_event_by_room, ¤t_room, &find_body_items_by_event_id](std::shared_ptr<BodyItem> *body_items, size_t num_body_items) { + // TODO: Optimize with hash map? + auto resolve_unreferenced_events_with_body_items = [&unreferenced_event_by_room, ¤t_room, &find_body_item_by_event_id](std::shared_ptr<BodyItem> *body_items, size_t num_body_items) { auto &unreferenced_events = unreferenced_event_by_room[current_room]; for(auto it = unreferenced_events.begin(); it != unreferenced_events.end(); ) { auto &message = *it; // TODO: Make redacted/edited events as (redacted)/(edited) in the body if(message->related_event_type == RelatedEventType::REDACTION || message->related_event_type == RelatedEventType::EDIT) { - auto body_item = find_body_items_by_event_id(body_items, num_body_items, message->related_event_id); + auto body_item = find_body_item_by_event_id(body_items, num_body_items, message->related_event_id); if(body_item) { body_item->set_description(message->body); + if(message->related_event_type == RelatedEventType::REDACTION) + body_item->thumbnail_url = message->user->avatar_url; it = unreferenced_events.erase(it); } else { ++it; @@ -3301,17 +3356,21 @@ namespace QuickMedia { } }; - auto modify_related_messages_in_current_room = [&unreferenced_event_by_room, ¤t_room, &find_body_items_by_event_id, &tabs](Messages &messages) { + // TODO: Optimize with hash map? + auto modify_related_messages_in_current_room = [&unreferenced_event_by_room, ¤t_room, &find_body_item_by_event_id, &tabs](Messages &messages) { auto &unreferenced_events = unreferenced_event_by_room[current_room]; + auto &body_items = tabs[MESSAGES_TAB_INDEX].body->items; for(auto &message : messages) { // TODO: Make redacted/edited events as (redacted)/(edited) in the body if(message->related_event_type == RelatedEventType::REDACTION || message->related_event_type == RelatedEventType::EDIT) { - auto &body_items = tabs[MESSAGES_TAB_INDEX].body->items; - auto body_item = find_body_items_by_event_id(body_items.data(), body_items.size(), message->related_event_id); - if(body_item) + auto body_item = find_body_item_by_event_id(body_items.data(), body_items.size(), message->related_event_id); + if(body_item) { body_item->set_description(message->body); - else + if(message->related_event_type == RelatedEventType::REDACTION) + body_item->thumbnail_url = message->user->avatar_url; + } else { unreferenced_events.push_back(message); + } } } }; @@ -3770,7 +3829,7 @@ namespace QuickMedia { fprintf(stderr, "Finished fetching older messages, num new messages: %zu\n", new_messages.size()); // Ignore finished fetch of messages if it happened in another room. When we navigate back to the room we will get the messages again size_t num_new_messages = new_messages.size(); - if(previous_messages_future_room == current_room && num_new_messages > 0) { + if(num_new_messages > 0 && previous_messages_future_room == current_room) { BodyItem *selected_item = tabs[MESSAGES_TAB_INDEX].body->get_selected(); BodyItems new_body_items = messages_to_body_items(new_messages); size_t num_new_body_items = new_body_items.size(); @@ -3786,6 +3845,21 @@ namespace QuickMedia { fetching_previous_messages_running = false; } + if(fetching_reply_message_running && fetch_reply_message_future.valid() && fetch_reply_message_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + std::shared_ptr<Message> replied_to_message = fetch_reply_message_future.get(); + fprintf(stderr, "Finished fetching reply to message: %s\n", replied_to_message ? replied_to_message->event_id.c_str() : "(null)"); + // Ignore finished fetch of messages if it happened in another room. When we navigate back to the room we will get the messages again + if(fetch_reply_future_room == current_room) { + if(replied_to_message) { + fetch_reply_body_item->embedded_item = message_to_body_item(replied_to_message.get()); + fetch_reply_body_item->embedded_item_status = EmbeddedItemStatus::FINISHED_LOADING; + } else { + fetch_reply_body_item->embedded_item_status = EmbeddedItemStatus::FAILED_TO_LOAD; + } + } + fetching_reply_message_running = false; + } + //chat_input.update(); window.clear(back_color); diff --git a/src/Storage.cpp b/src/Storage.cpp index a1dc777..086f6d8 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -5,6 +5,7 @@ #include <assert.h> #include <json/reader.h> #include <json/writer.h> +#include <rapidjson/document.h> #include <rapidjson/writer.h> #include <rapidjson/stringbuffer.h> #include <unordered_set> diff --git a/src/Text.cpp b/src/Text.cpp index c1adc5d..a00d068 100644 --- a/src/Text.cpp +++ b/src/Text.cpp @@ -38,6 +38,7 @@ namespace QuickMedia dirtyCaret(false), editable(false), caretMoveDirection(CaretMoveDirection::NONE), + num_lines(1), lineSpacing(0.0f), characterSpacing(0.0f), caretIndex(0), @@ -96,8 +97,10 @@ namespace QuickMedia if(std::abs(maxWidth - this->maxWidth) > 1.0f) { this->maxWidth = maxWidth; - dirty = true; - dirtyCaret = true; + if(num_lines > 1 || maxWidth < boundingBox.width) { + dirty = true; + dirtyCaret = true; + } } } @@ -383,7 +386,7 @@ namespace QuickMedia float text_wrap_offset = 0.0f; float text_offset_y = 0.0f; int last_space_index = -1; - int num_lines = 1; + num_lines = 1; // TODO: Binary search? for(int i = 0; i < (int)vertices_linear.size(); ++i) { VertexRef &vertex_ref = vertices_linear[i]; diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 0866bac..1cb2aa5 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -1,6 +1,7 @@ #include "../../plugins/Matrix.hpp" #include "../../include/Storage.hpp" #include "../../include/StringUtils.hpp" +#include <rapidjson/document.h> #include <rapidjson/writer.h> #include <rapidjson/stringbuffer.h> #include <fcntl.h> @@ -447,173 +448,181 @@ namespace QuickMedia { std::vector<std::shared_ptr<Message>> new_messages; for(const rapidjson::Value &event_item_json : events_json.GetArray()) { - if(!event_item_json.IsObject()) + std::shared_ptr<Message> new_message = parse_message_event(event_item_json, room_data.get()); + if(!new_message) continue; - const rapidjson::Value &sender_json = GetMember(event_item_json, "sender"); - if(!sender_json.IsString()) - continue; + // TODO: Is @room ok? shouldn't we also check if the user has permission to do @room? (only when notifications are limited to @mentions) + if(has_unread_notifications && !username.empty()) + new_message->mentions_me = message_contains_user_mention(new_message->body, username) || message_contains_user_mention(new_message->body, "@room"); - std::string sender_json_str = sender_json.GetString(); + new_messages.push_back(std::move(new_message)); + } - const rapidjson::Value &event_id_json = GetMember(event_item_json, "event_id"); - if(!event_id_json.IsString()) - continue; + // TODO: Add directly to this instead when set? otherwise add to new_messages + if(room_messages) + (*room_messages)[room_data] = new_messages; - std::string event_id_str = event_id_json.GetString(); - - const rapidjson::Value *content_json = &GetMember(event_item_json, "content"); - if(!content_json->IsObject()) - continue; + // TODO: Loop and std::move instead? doesn't insert create copies? + if(message_dir == MessageDirection::BEFORE) { + room_data->prepend_messages_reverse(std::move(new_messages)); + } else if(message_dir == MessageDirection::AFTER) { + room_data->append_messages(std::move(new_messages)); + } + } - auto user = room_data->get_user_by_id(sender_json_str); - if(!user) { - // Note: this is important because otherwise replying and such is broken - fprintf(stderr, "Warning: skipping unknown user: %s\n", sender_json_str.c_str()); - continue; - } + std::shared_ptr<Message> Matrix::parse_message_event(const rapidjson::Value &event_item_json, RoomData *room_data) { + if(!event_item_json.IsObject()) + return nullptr; - time_t timestamp = 0; - const rapidjson::Value &origin_server_ts = GetMember(event_item_json, "origin_server_ts"); - if(origin_server_ts.IsNumber()) - timestamp = origin_server_ts.GetInt64(); + const rapidjson::Value &sender_json = GetMember(event_item_json, "sender"); + if(!sender_json.IsString()) + return nullptr; - const rapidjson::Value &type_json = GetMember(event_item_json, "type"); - if(!type_json.IsString()) - continue; + std::string sender_json_str = sender_json.GetString(); - RelatedEventType related_event_type = RelatedEventType::NONE; - std::string related_event_id; - const rapidjson::Value &relates_to_json = GetMember(*content_json, "m.relates_to"); - if(relates_to_json.IsObject()) { - const rapidjson::Value &replaces_event_id_json = GetMember(relates_to_json, "event_id"); - const rapidjson::Value &rel_type_json = GetMember(relates_to_json, "rel_type"); - if(replaces_event_id_json.IsString() && rel_type_json.IsString() && strcmp(rel_type_json.GetString(), "m.replace") == 0) { - related_event_id = replaces_event_id_json.GetString(); - related_event_type = RelatedEventType::EDIT; - } else { - const rapidjson::Value &in_reply_to_json = GetMember(relates_to_json, "m.in_reply_to"); - if(in_reply_to_json.IsObject()) { - const rapidjson::Value &in_reply_to_event_id = GetMember(in_reply_to_json, "event_id"); - if(in_reply_to_event_id.IsString()) { - related_event_id = in_reply_to_event_id.GetString(); - related_event_type = RelatedEventType::REPLY; - } + const rapidjson::Value &event_id_json = GetMember(event_item_json, "event_id"); + if(!event_id_json.IsString()) + return nullptr; + + std::string event_id_str = event_id_json.GetString(); + + const rapidjson::Value *content_json = &GetMember(event_item_json, "content"); + if(!content_json->IsObject()) + return nullptr; + + auto user = room_data->get_user_by_id(sender_json_str); + if(!user) { + // Note: this is important because otherwise replying and such is broken + fprintf(stderr, "Warning: skipping unknown user: %s\n", sender_json_str.c_str()); + return nullptr; + } + + time_t timestamp = 0; + const rapidjson::Value &origin_server_ts = GetMember(event_item_json, "origin_server_ts"); + if(origin_server_ts.IsNumber()) + timestamp = origin_server_ts.GetInt64(); + + const rapidjson::Value &type_json = GetMember(event_item_json, "type"); + if(!type_json.IsString()) + return nullptr; + + RelatedEventType related_event_type = RelatedEventType::NONE; + std::string related_event_id; + const rapidjson::Value &relates_to_json = GetMember(*content_json, "m.relates_to"); + if(relates_to_json.IsObject()) { + const rapidjson::Value &replaces_event_id_json = GetMember(relates_to_json, "event_id"); + const rapidjson::Value &rel_type_json = GetMember(relates_to_json, "rel_type"); + if(replaces_event_id_json.IsString() && rel_type_json.IsString() && strcmp(rel_type_json.GetString(), "m.replace") == 0) { + related_event_id = replaces_event_id_json.GetString(); + related_event_type = RelatedEventType::EDIT; + } else { + const rapidjson::Value &in_reply_to_json = GetMember(relates_to_json, "m.in_reply_to"); + if(in_reply_to_json.IsObject()) { + const rapidjson::Value &in_reply_to_event_id = GetMember(in_reply_to_json, "event_id"); + if(in_reply_to_event_id.IsString()) { + related_event_id = in_reply_to_event_id.GetString(); + related_event_type = RelatedEventType::REPLY; } } } + } - const rapidjson::Value &new_content_json = GetMember(*content_json, "m.new_content"); - if(new_content_json.IsObject()) - content_json = &new_content_json; - - const rapidjson::Value &content_type = GetMember(*content_json, "msgtype"); - if(!content_type.IsString() || strcmp(type_json.GetString(), "m.room.redaction") == 0) { - auto message = std::make_shared<Message>(); - message->type = MessageType::REDACTION; - message->user = user; - message->event_id = event_id_str; - message->body = "Message deleted"; - message->timestamp = timestamp; - message->related_event_type = RelatedEventType::REDACTION; - - const rapidjson::Value &reason_json = GetMember(*content_json, "reason"); - if(reason_json.IsString()) { - message->body += ", reason: "; - message->body += reason_json.GetString(); - } + const rapidjson::Value &new_content_json = GetMember(*content_json, "m.new_content"); + if(new_content_json.IsObject()) + content_json = &new_content_json; - const rapidjson::Value &redacts_json = GetMember(event_item_json, "redacts"); - if(redacts_json.IsString()) - message->related_event_id = redacts_json.GetString(); + const rapidjson::Value &content_type = GetMember(*content_json, "msgtype"); + if(!content_type.IsString() || strcmp(type_json.GetString(), "m.room.redaction") == 0) { + auto message = std::make_shared<Message>(); + message->type = MessageType::REDACTION; + message->user = user; + message->event_id = event_id_str; + message->body = "Message deleted"; + message->timestamp = timestamp; + message->related_event_type = RelatedEventType::REDACTION; - new_messages.push_back(message); - continue; + const rapidjson::Value &reason_json = GetMember(*content_json, "reason"); + if(reason_json.IsString()) { + message->body += ", reason: "; + message->body += reason_json.GetString(); } - if(strcmp(type_json.GetString(), "m.room.message") != 0) - continue; - - const rapidjson::Value &body_json = GetMember(*content_json, "body"); - if(!body_json.IsString()) - continue; + const rapidjson::Value &redacts_json = GetMember(event_item_json, "redacts"); + if(redacts_json.IsString()) + message->related_event_id = redacts_json.GetString(); - auto message = std::make_shared<Message>(); - std::string prefix; + return message; + } - // TODO: Also show joins, leave, invites, bans, kicks, mutes, etc + if(strcmp(type_json.GetString(), "m.room.message") != 0) + return nullptr; - if(strcmp(content_type.GetString(), "m.text") == 0) { - message->type = MessageType::TEXT; - } else if(strcmp(content_type.GetString(), "m.image") == 0) { - const rapidjson::Value &url_json = GetMember(*content_json, "url"); - if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) - continue; + const rapidjson::Value &body_json = GetMember(*content_json, "body"); + if(!body_json.IsString()) + return nullptr; - message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6); - message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver); - message->type = MessageType::IMAGE; - } else if(strcmp(content_type.GetString(), "m.video") == 0) { - const rapidjson::Value &url_json = GetMember(*content_json, "url"); - if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) - continue; + auto message = std::make_shared<Message>(); + std::string prefix; - message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6); - message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver); - message->type = MessageType::VIDEO; - } else if(strcmp(content_type.GetString(), "m.audio") == 0) { - const rapidjson::Value &url_json = GetMember(*content_json, "url"); - if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) - continue; + // TODO: Also show joins, leave, invites, bans, kicks, mutes, etc - message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6); - message->type = MessageType::AUDIO; - } else if(strcmp(content_type.GetString(), "m.file") == 0) { - const rapidjson::Value &url_json = GetMember(*content_json, "url"); - if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) - continue; + if(strcmp(content_type.GetString(), "m.text") == 0) { + message->type = MessageType::TEXT; + } else if(strcmp(content_type.GetString(), "m.image") == 0) { + const rapidjson::Value &url_json = GetMember(*content_json, "url"); + if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) + return nullptr; - message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6); - message->type = MessageType::FILE; - } else if(strcmp(content_type.GetString(), "m.emote") == 0) { // this is a /me message, TODO: show /me messages differently - message->type = MessageType::TEXT; - prefix = "*" + user->display_name + "* "; - } else if(strcmp(content_type.GetString(), "m.notice") == 0) { // TODO: show notices differently - message->type = MessageType::TEXT; - prefix = "* NOTICE * "; - } else if(strcmp(content_type.GetString(), "m.location") == 0) { // TODO: show locations differently - const rapidjson::Value &geo_uri_json = GetMember(*content_json, "geo_uri"); - if(geo_uri_json.IsString()) - prefix = geo_uri_json.GetString() + std::string(" | "); - - message->type = MessageType::TEXT; - message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver); - } else { - continue; - } + message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6); + message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver); + message->type = MessageType::IMAGE; + } else if(strcmp(content_type.GetString(), "m.video") == 0) { + const rapidjson::Value &url_json = GetMember(*content_json, "url"); + if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) + return nullptr; - message->user = user; - message->event_id = event_id_str; - message->body = prefix + body_json.GetString(); - message->related_event_id = std::move(related_event_id); - message->related_event_type = related_event_type; - // TODO: Is @room ok? shouldn't we also check if the user has permission to do @room? (only when notifications are limited to @mentions) - if(has_unread_notifications && !username.empty()) - message->mentions_me = message_contains_user_mention(message->body, username) || message_contains_user_mention(message->body, "@room"); - message->timestamp = timestamp; - new_messages.push_back(message); - } + message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6); + message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver); + message->type = MessageType::VIDEO; + } else if(strcmp(content_type.GetString(), "m.audio") == 0) { + const rapidjson::Value &url_json = GetMember(*content_json, "url"); + if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) + return nullptr; - // TODO: Add directly to this instead when set? otherwise add to new_messages - if(room_messages) - (*room_messages)[room_data] = new_messages; + message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6); + message->type = MessageType::AUDIO; + } else if(strcmp(content_type.GetString(), "m.file") == 0) { + const rapidjson::Value &url_json = GetMember(*content_json, "url"); + if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) + return nullptr; - // TODO: Loop and std::move instead? doesn't insert create copies? - if(message_dir == MessageDirection::BEFORE) { - room_data->prepend_messages_reverse(std::move(new_messages)); - } else if(message_dir == MessageDirection::AFTER) { - room_data->append_messages(std::move(new_messages)); + message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6); + message->type = MessageType::FILE; + } else if(strcmp(content_type.GetString(), "m.emote") == 0) { // this is a /me message, TODO: show /me messages differently + message->type = MessageType::TEXT; + prefix = "*" + user->display_name + "* "; + } else if(strcmp(content_type.GetString(), "m.notice") == 0) { // TODO: show notices differently + message->type = MessageType::TEXT; + prefix = "* NOTICE * "; + } else if(strcmp(content_type.GetString(), "m.location") == 0) { // TODO: show locations differently + const rapidjson::Value &geo_uri_json = GetMember(*content_json, "geo_uri"); + if(geo_uri_json.IsString()) + prefix = geo_uri_json.GetString() + std::string(" | "); + + message->type = MessageType::TEXT; + message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver); + } else { + return nullptr; } + + message->user = user; + message->event_id = event_id_str; + message->body = prefix + body_json.GetString(); + message->related_event_id = std::move(related_event_id); + message->related_event_type = related_event_type; + message->timestamp = timestamp; + return message; } // Returns empty string on error @@ -851,7 +860,7 @@ namespace QuickMedia { } if(file_info->duration_seconds) { // TODO: Check for overflow? - info_json.AddMember("duration", (int)file_info->duration_seconds.value() * 1000, request_data.GetAllocator()); + info_json.AddMember("duration", (int)(file_info->duration_seconds.value() * 1000.0), request_data.GetAllocator()); } if(thumbnail_info) { @@ -932,6 +941,8 @@ namespace QuickMedia { case MessageType::TEXT: { if(message->related_event_type != RelatedEventType::NONE) related_to_body = remove_reply_formatting(message->body); + else + related_to_body = message->body; break; } case MessageType::IMAGE: @@ -962,6 +973,8 @@ namespace QuickMedia { std::string related_to_body = get_reply_message(message); html_escape_sequences(formatted_body); html_escape_sequences(related_to_body); + // TODO: Fix invalid.url, etc to use same as element. This is required to navigate to reply message in element mobile. + // TODO: Add keybind to navigate to the reply message, which would also depend on this formatting. return "<mx-reply>" "<blockquote>" "<a href=\"https://invalid.url\">In reply to</a>" @@ -1125,88 +1138,54 @@ namespace QuickMedia { // TODO: Right now this recursively calls /rooms/<room_id>/context/<event_id> and trusts server to not make it recursive. To make this robust, check iteration count and do not trust server. // TODO: Optimize? std::shared_ptr<Message> Matrix::get_edited_message_original_message(RoomData *room_data, std::shared_ptr<Message> message) { - if(message->related_event_type != RelatedEventType::EDIT) + if(!message || message->related_event_type != RelatedEventType::EDIT) return message; + return get_edited_message_original_message(room_data, get_message_by_id(room_data, message->related_event_id)); + } - auto replaced_message = room_data->get_message_by_id(message->related_event_id); - if(!replaced_message) { - rapidjson::Document request_data(rapidjson::kObjectType); - request_data.AddMember("lazy_load_members", true, request_data.GetAllocator()); - - rapidjson::StringBuffer buffer; - rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); - request_data.Accept(writer); - - std::vector<CommandArg> additional_args = { - { "-H", "Authorization: Bearer " + access_token } - }; - - std::string filter = url_param_encode(buffer.GetString()); - - char url[512]; - snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/context/%s?limit=0&filter=%s", homeserver.c_str(), room_data->id.c_str(), message->event_id.c_str(), filter.c_str()); - - rapidjson::Document json_root; - DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true); - if(download_result != DownloadResult::OK) return nullptr; - - if(!json_root.IsObject()) - return nullptr; + std::shared_ptr<Message> Matrix::get_message_by_id(RoomData *room, const std::string &event_id) { + std::shared_ptr<Message> existing_room_message = room->get_message_by_id(event_id); + if(existing_room_message) + return existing_room_message; - const rapidjson::Value &event_json = GetMember(json_root, "event"); - if(!event_json.IsObject()) - return nullptr; + auto fetched_message_it = room->fetched_messages_by_event_id.find(event_id); + if(fetched_message_it != room->fetched_messages_by_event_id.end()) + return fetched_message_it->second; + + rapidjson::Document request_data(rapidjson::kObjectType); + request_data.AddMember("lazy_load_members", true, request_data.GetAllocator()); - const rapidjson::Value &event_id_json = GetMember(event_json, "event_id"); - if(!event_id_json.IsString()) - return nullptr; + rapidjson::StringBuffer buffer; + rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); + request_data.Accept(writer); - const rapidjson::Value &content_json = GetMember(event_json, "content"); - if(!content_json.IsObject()) - return nullptr; + std::vector<CommandArg> additional_args = { + { "-H", "Authorization: Bearer " + access_token } + }; - const rapidjson::Value &body_json = GetMember(content_json, "body"); - if(!body_json.IsString()) - return nullptr; + std::string filter = url_param_encode(buffer.GetString()); - RelatedEventType related_event_type = RelatedEventType::NONE; - std::string related_event_id; - const rapidjson::Value &relates_to_json = GetMember(content_json, "m.relates_to"); - if(relates_to_json.IsObject()) { - const rapidjson::Value &event_id_json = GetMember(relates_to_json, "event_id"); - const rapidjson::Value &rel_type_json = GetMember(relates_to_json, "rel_type"); - if(event_id_json.IsString() && rel_type_json.IsString() && strcmp(rel_type_json.GetString(), "m.replace") == 0) { - related_event_id = event_id_json.GetString(); - related_event_type = RelatedEventType::EDIT; - } - } + char url[512]; + snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/context/%s?limit=0&filter=%s", homeserver.c_str(), room->id.c_str(), event_id.c_str(), filter.c_str()); - const rapidjson::Value &content_type = GetMember(content_json, "msgtype"); - if(!content_type.IsString()) - return nullptr; + std::string err_msg; + rapidjson::Document json_root; + DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true, &err_msg); + if(download_result != DownloadResult::OK) return nullptr; - auto new_message = std::make_shared<Message>(); - new_message->event_id = event_id_json.GetString(); - new_message->related_event_id = std::move(related_event_id); - new_message->related_event_type = related_event_type; - if(strcmp(content_type.GetString(), "m.text") == 0) { - new_message->type = MessageType::TEXT; - } else if(strcmp(content_type.GetString(), "m.image") == 0) { - new_message->type = MessageType::IMAGE; - } else if(strcmp(content_type.GetString(), "m.video") == 0) { - new_message->type = MessageType::VIDEO; - } else if(strcmp(content_type.GetString(), "m.audio") == 0) { - new_message->type = MessageType::AUDIO; - } else if(strcmp(content_type.GetString(), "m.file") == 0) { - new_message->type = MessageType::FILE; - } else { + if(json_root.IsObject()) { + const rapidjson::Value &error_json = GetMember(json_root, "error"); + if(error_json.IsString()) { + fprintf(stderr, "Matrix::get_message_by_id, error: %s\n", error_json.GetString()); + room->fetched_messages_by_event_id.insert(std::make_pair(event_id, nullptr)); return nullptr; } - - return get_edited_message_original_message(room_data, std::move(new_message)); - } else { - return get_edited_message_original_message(room_data, replaced_message); } + + const rapidjson::Value &event_json = GetMember(json_root, "event"); + std::shared_ptr<Message> new_message = parse_message_event(event_json, room); + room->fetched_messages_by_event_id.insert(std::make_pair(event_id, new_message)); + return new_message; } // Returns empty string on error |