From 6a2b5008be8104680826fe40fa8e674e9357c044 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Mon, 19 Oct 2020 23:33:23 +0200 Subject: Add thumbnail loading animation Use correct ref in matrix replies, make text that contains our user id also count as a mention. --- images/loading_icon.png | Bin 0 -> 2766 bytes include/Body.hpp | 8 +++++--- include/QuickMedia.hpp | 1 + src/Body.cpp | 27 +++++++++++++++++++-------- src/QuickMedia.cpp | 26 ++++++++++++++++---------- src/plugins/Matrix.cpp | 16 +++++++++++----- 6 files changed, 52 insertions(+), 26 deletions(-) create mode 100644 images/loading_icon.png diff --git a/images/loading_icon.png b/images/loading_icon.png new file mode 100644 index 0000000..6c11fbc Binary files /dev/null and b/images/loading_icon.png differ diff --git a/include/Body.hpp b/include/Body.hpp index 9c7c1c1..ce61811 100644 --- a/include/Body.hpp +++ b/include/Body.hpp @@ -3,7 +3,6 @@ #include "Text.hpp" #include "AsyncImageLoader.hpp" #include -#include #include #include #include "../external/RoundedRectangleShape.hpp" @@ -14,6 +13,7 @@ namespace sf { class RenderWindow; class Shader; + class Texture; } namespace QuickMedia { @@ -113,7 +113,7 @@ namespace QuickMedia { std::vector replies; std::string post_number; void *userdata; // Not managed, should be deallocated by whoever sets this - sf::Int32 last_drawn_time; + double last_drawn_time; EmbeddedItemStatus embedded_item_status = EmbeddedItemStatus::NONE; std::shared_ptr 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) ThumbnailMaskType thumbnail_mask_type = ThumbnailMaskType::NONE; @@ -134,7 +134,7 @@ namespace QuickMedia { class Body { public: - Body(Program *program, sf::Font *font, sf::Font *bold_font, sf::Font *cjk_font); + Body(Program *program, sf::Font *font, sf::Font *bold_font, sf::Font *cjk_font, sf::Texture &loading_icon_texture); // Select previous page, ignoring invisible items. Returns true if the item was changed. This can be used to check if the top was hit when wrap_around is set to false bool select_previous_page(); @@ -229,9 +229,11 @@ namespace QuickMedia { sf::RectangleShape item_separator; sf::RoundedRectangleShape item_background; sf::Sprite image; + sf::Sprite loading_icon; int num_visible_items; bool last_item_fully_visible; int last_fully_visible_item; sf::Clock draw_timer; + double elapsed_time_sec = 0.0; }; } \ No newline at end of file diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index 955fc9c..45c499a 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -97,6 +97,7 @@ namespace QuickMedia { std::unique_ptr cjk_font; const char *plugin_name = nullptr; sf::Texture plugin_logo; + sf::Texture loading_icon; PageType current_page; std::stack page_stack; int image_index; diff --git a/src/Body.cpp b/src/Body.cpp index 0294d97..1d06559 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -25,7 +25,7 @@ namespace QuickMedia { dirty_timestamp(false), thumbnail_is_local(false), userdata(nullptr), - last_drawn_time(0), + last_drawn_time(0.0), timestamp(0), title_color(sf::Color::White), author_color(sf::Color::White), @@ -35,7 +35,7 @@ namespace QuickMedia { set_title(std::move(_title)); } - Body::Body(Program *program, sf::Font *font, sf::Font *bold_font, sf::Font *cjk_font) : + Body::Body(Program *program, sf::Font *font, sf::Font *bold_font, sf::Font *cjk_font, sf::Texture &loading_icon_texture) : font(font), bold_font(bold_font), cjk_font(cjk_font), @@ -52,6 +52,7 @@ namespace QuickMedia { prev_selected_item(0), page_scroll(0.0f), item_background(sf::Vector2f(1.0f, 1.0f), 10.0f, 10), + loading_icon(loading_icon_texture), num_visible_items(0), last_item_fully_visible(true), last_fully_visible_item(-1) @@ -61,6 +62,8 @@ namespace QuickMedia { thumbnail_resize_target_size.x = 120; thumbnail_resize_target_size.y = 120; item_background.setFillColor(sf::Color(55, 60, 68)); + sf::Vector2f loading_icon_size(loading_icon.getTexture()->getSize().x, loading_icon.getTexture()->getSize().y); + loading_icon.setOrigin(loading_icon_size.x * 0.5f, loading_icon_size.y * 0.5f); } // TODO: Make this work with wraparound enabled? @@ -274,7 +277,7 @@ namespace QuickMedia { sf::Vector2f scissor_size = size; const float start_y = pos.y; - const sf::Int32 elapsed_time = draw_timer.getElapsedTime().asMilliseconds(); + elapsed_time_sec = draw_timer.getElapsedTime().asSeconds(); //item_background.setFillColor(front_color); //item_background.setOutlineThickness(1.0f); @@ -354,7 +357,7 @@ namespace QuickMedia { continue; update_dirty_state(item.get(), size); - item->last_drawn_time = elapsed_time; + item->last_drawn_time = elapsed_time_sec; float item_height = get_item_height(item.get()); prev_pos.y -= (item_height + spacing_y); @@ -383,7 +386,7 @@ namespace QuickMedia { update_dirty_state(item.get(), size); float item_height = get_item_height(item.get()); - item->last_drawn_time = elapsed_time; + item->last_drawn_time = elapsed_time_sec; // 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); @@ -410,10 +413,9 @@ namespace QuickMedia { } // TODO: Only do this for items that are not visible, do not loop all items. - // TODO: Verify if this only runs for items that are not visible, and only once // TODO: Improve performance! right now it can use up to 5-7% cpu with a lot of items! for(auto &body_item : items) { - if(elapsed_time - body_item->last_drawn_time >= 1500) { + if(elapsed_time_sec - body_item->last_drawn_time >= 1.5) { clear_body_item_cache(body_item.get()); } } @@ -600,7 +602,8 @@ namespace QuickMedia { if(item->thumbnail_size.x > 0 && item->thumbnail_size.y > 0) content_size = sf::Vector2f(item->thumbnail_size.x, item->thumbnail_size.y); - sf::Color fallback_color(52, 58, 70, (1.0 - thumbnail_fade_progress) * 255); + sf::Uint8 fallback_fade_alpha = (1.0 - thumbnail_fade_progress) * 255; + sf::Color fallback_color(52, 58, 70, fallback_fade_alpha); if(thumbnail_mask_shader && item->thumbnail_mask_type == ThumbnailMaskType::CIRCLE) { // TODO: Use the mask shader instead, but a vertex shader is also needed for that to pass the vertex coordinates since // shapes dont have texture coordinates. @@ -616,6 +619,14 @@ namespace QuickMedia { window.draw(image_fallback); } + sf::Vector2f loading_icon_size(loading_icon.getTexture()->getSize().x, loading_icon.getTexture()->getSize().y); + auto new_loading_icon_size = clamp_to_size(loading_icon_size, content_size); + loading_icon.setPosition(item_pos + sf::Vector2f(image_padding_x, padding_y) + (content_size * 0.5f)); + loading_icon.setScale(get_ratio(loading_icon_size, new_loading_icon_size)); + loading_icon.setRotation(-elapsed_time_sec * 400.0); + loading_icon.setColor(sf::Color(255, 255, 255, fallback_fade_alpha)); + window.draw(loading_icon); + if(!has_thumbnail_texture) text_offset_x += image_padding_x + content_size.x; } diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index a32febb..c47147f 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -355,6 +355,12 @@ namespace QuickMedia { abort(); } + if(!loading_icon.loadFromFile(resources_root + "images/loading_icon.png")) { + fprintf(stderr, "Failed to load %s/images/loading_icon.png", resources_root.c_str()); + abort(); + } + loading_icon.setSmooth(true); + struct sigaction action; action.sa_handler = sigpipe_handler; sigemptyset(&action.sa_mask); @@ -883,7 +889,7 @@ namespace QuickMedia { } std::unique_ptr Program::create_body() { - return std::make_unique(this, font.get(), bold_font.get(), cjk_font.get()); + return std::make_unique(this, font.get(), bold_font.get(), cjk_font.get(), loading_icon); } std::unique_ptr Program::create_search_bar(const std::string &placeholder, int search_delay) { @@ -2915,7 +2921,7 @@ namespace QuickMedia { 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; - if(message->mentions_me || (me && message_contains_user_mention(message->body, me->display_name))) + if(message->mentions_me || (me && (message_contains_user_mention(message->body, me->display_name) || message_contains_user_mention(message->body, me->user_id)))) body_item->set_description_color(sf::Color(255, 100, 100)); return body_item; } @@ -2938,7 +2944,7 @@ namespace QuickMedia { ChatTab messages_tab; messages_tab.type = ChatTabType::MESSAGES; - messages_tab.body = std::make_unique(this, font.get(), bold_font.get(), cjk_font.get()); + messages_tab.body = std::make_unique(this, font.get(), bold_font.get(), cjk_font.get(), loading_icon); messages_tab.body->draw_thumbnails = true; messages_tab.body->thumbnail_resize_target_size = CHAT_MESSAGE_THUMBNAIL_MAX_SIZE; messages_tab.body->thumbnail_mask_shader = &circle_mask_shader; @@ -2948,7 +2954,7 @@ namespace QuickMedia { ChatTab rooms_tab; rooms_tab.type = ChatTabType::ROOMS; - rooms_tab.body = std::make_unique(this, font.get(), bold_font.get(), cjk_font.get()); + rooms_tab.body = std::make_unique(this, font.get(), bold_font.get(), cjk_font.get(), loading_icon); rooms_tab.body->draw_thumbnails = true; //rooms_tab.body->line_separator_color = sf::Color::Transparent; rooms_tab.body->thumbnail_mask_shader = &circle_mask_shader; @@ -3001,11 +3007,11 @@ namespace QuickMedia { // Swap room with the top one // TODO: Optimize with hash map instead of linear search? or cache the index - int room_body_index = tabs[ROOMS_TAB_INDEX].body->get_index_by_body_item(room_body_item); - if(room_body_index != -1) { - std::swap(tabs[ROOMS_TAB_INDEX].body->items[room_body_index], tabs[ROOMS_TAB_INDEX].body->items[room_swap_index]); - ++room_swap_index; - } + // int room_body_index = tabs[ROOMS_TAB_INDEX].body->get_index_by_body_item(room_body_item); + // if(room_body_index != -1) { + // std::swap(tabs[ROOMS_TAB_INDEX].body->items[room_body_index], tabs[ROOMS_TAB_INDEX].body->items[room_swap_index]); + // ++room_swap_index; + // } } else if(is_first_sync) { room_body_item->set_description(matrix->message_get_author_displayname(messages.back().get()) + ": " + extract_first_line(messages.back()->body, 150)); room->has_unread_mention = false; @@ -3313,7 +3319,7 @@ namespace QuickMedia { const float chat_input_padding_x = 15.0f; const float chat_input_padding_y = 15.0f; - Body url_selection_body(this, font.get(), bold_font.get(), cjk_font.get()); + Body url_selection_body(this, font.get(), bold_font.get(), cjk_font.get(), loading_icon); auto launch_url = [this, &video_page, &redraw](const std::string &url) mutable { if(url.empty()) diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 35f5302..ff0d498 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -462,7 +462,7 @@ namespace QuickMedia { // 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 && me) - new_message->mentions_me = message_contains_user_mention(new_message->body, me->display_name) || message_contains_user_mention(new_message->body, "@room"); + new_message->mentions_me = message_contains_user_mention(new_message->body, me->display_name) || message_contains_user_mention(new_message->body, me->user_id) || message_contains_user_mention(new_message->body, "@room"); new_messages.push_back(std::move(new_message)); } @@ -978,17 +978,23 @@ namespace QuickMedia { static std::string create_body_for_message_reply(const Message *message, const std::string &body) { return "> <" + message->user->user_id + "> " + block_quote(get_reply_message(message)) + "\n\n" + body; } + + static std::string extract_homeserver_from_room_id(const std::string &room_id) { + size_t sep_index = room_id.find(':'); + if(sep_index != std::string::npos) + return room_id.substr(sep_index + 1); + return ""; + } - static std::string create_formatted_body_for_message_reply(const Message *message, const std::string &body) { + static std::string create_formatted_body_for_message_reply(RoomData *room, const Message *message, const std::string &body) { std::string formatted_body = body; 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" + "id + "/" + message->event_id + "?via=" + extract_homeserver_from_room_id(room->id) + "\">In reply to" "user->user_id + "\">" + message->user->user_id + "
" + std::move(related_to_body) + "
" "
" + std::move(formatted_body); @@ -1018,7 +1024,7 @@ namespace QuickMedia { relates_to_json.AddMember("m.in_reply_to", std::move(in_reply_to_json), relates_to_json.GetAllocator()); std::string message_reply_body = create_body_for_message_reply(relates_to_message_raw, body); // Yes, the reply is to the edited message but the event_id reference is to the original message... - std::string formatted_message_reply_body = create_formatted_body_for_message_reply(relates_to_message_raw, body); + std::string formatted_message_reply_body = create_formatted_body_for_message_reply(room, relates_to_message_raw, body); rapidjson::Document request_data(rapidjson::kObjectType); request_data.AddMember("msgtype", "m.text", request_data.GetAllocator()); // TODO: Allow image reply? element doesn't do that but we could! -- cgit v1.2.3