From 9bf163d51a252fb5a611e88c2e0b4123a98169e1 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 17 Oct 2020 09:40:10 +0200 Subject: Matrix: show reply messages embedded in messages that reply to them, like element does --- src/Body.cpp | 143 ++++++++++-------- src/DownloadUtils.cpp | 1 + src/QuickMedia.cpp | 148 +++++++++++++----- src/Storage.cpp | 1 + src/Text.cpp | 9 +- src/plugins/Matrix.cpp | 399 +++++++++++++++++++++++-------------------------- 6 files changed, 389 insertions(+), 312 deletions(-) (limited to 'src') 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 #include -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(*other.title_text); - else - title_text = nullptr; - if(other.description_text) - description_text = std::make_unique(*other.description_text); - else - description_text = nullptr; - if(other.author_text) - author_text = std::make_unique(*other.author_text); - else - author_text = nullptr; - if(other.timestamp_text) - timestamp_text = std::make_unique(*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 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 +#include #include 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 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 previous_messages_future_room; + std::future> fetch_reply_message_future; + bool fetching_reply_message_running = false; + std::shared_ptr 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 *body_items, size_t num_body_items, const std::string &event_id) -> std::shared_ptr { + for(size_t i = 0; i < num_body_items; ++i) { + auto &body_item = body_items[i]; + if(static_cast(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(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 *body_items, size_t num_body_items, const std::string &event_id) -> std::shared_ptr { - for(size_t i = 0; i < num_body_items; ++i) { - auto &body_item = body_items[i]; - if(static_cast(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, 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 *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 *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 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 #include #include +#include #include #include #include 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 #include #include #include @@ -447,173 +448,181 @@ namespace QuickMedia { std::vector> new_messages; for(const rapidjson::Value &event_item_json : events_json.GetArray()) { - if(!event_item_json.IsObject()) + std::shared_ptr 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 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->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->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(); - 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(); + 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 "" "
" "In reply to" @@ -1125,88 +1138,54 @@ namespace QuickMedia { // TODO: Right now this recursively calls /rooms//context/ 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 Matrix::get_edited_message_original_message(RoomData *room_data, std::shared_ptr 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 writer(buffer); - request_data.Accept(writer); - - std::vector 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 Matrix::get_message_by_id(RoomData *room, const std::string &event_id) { + std::shared_ptr 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 writer(buffer); + request_data.Accept(writer); - const rapidjson::Value &content_json = GetMember(event_json, "content"); - if(!content_json.IsObject()) - return nullptr; + std::vector 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(); - 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 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 -- cgit v1.2.3