diff options
-rw-r--r-- | TODO | 1 | ||||
m--------- | depends/html-parser | 0 | ||||
-rw-r--r-- | include/Body.hpp | 1 | ||||
-rw-r--r-- | src/Body.cpp | 10 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 336 |
5 files changed, 202 insertions, 146 deletions
@@ -53,7 +53,6 @@ Implement mentions in matrix with an autofill list, like on element. Also do the Add option to disable autosearch and search when pressing enter instead or something? this would be needed for mobile phones where typing is slow. Sleep when idle, to reduce cpu usage from 1-2% to 0%, important for mobile devices. Also render view to a rendertexture and render that instead of redrawing every time every time. Provide a way to specify when notifications should be received (using matrix api) and also read the notification config from matrix. Also provide a way to disable notifications globally. -Add room search. Use quickmedia to show image in matrix rooms, instead of mpv. Respect ~/.Xresources dpi (read the file, loop lines and look for Xft.dpi). Fallback to 96 dpi. Merge body items in matrix if they are posted by the same author (there is a git stash for this). diff --git a/depends/html-parser b/depends/html-parser -Subproject fdfdf20d085a7c705477d878c11ed208577facb +Subproject 32b65e91db5b01b6b6da5117789c9dc473fec13 diff --git a/include/Body.hpp b/include/Body.hpp index 56b18e3..1e21093 100644 --- a/include/Body.hpp +++ b/include/Body.hpp @@ -188,6 +188,7 @@ namespace QuickMedia { // TODO: Highlight the part of the text that matches the search. // TODO: Ignore dot, whitespace and special characters void filter_search_fuzzy(const std::string &text); + void filter_search_fuzzy_item(const std::string &text, BodyItem *body_item); bool no_items_visible() const; diff --git a/src/Body.cpp b/src/Body.cpp index fe57130..e60a691 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -774,14 +774,18 @@ namespace QuickMedia { } for(auto &item : items) { - item->visible = string_find_case_insensitive(item->get_title(), text); - if(!item->visible && !item->get_description().empty()) - item->visible = string_find_case_insensitive(item->get_description(), text); + filter_search_fuzzy_item(text, item.get()); } select_first_item(); } + void Body::filter_search_fuzzy_item(const std::string &text, BodyItem *body_item) { + body_item->visible = string_find_case_insensitive(body_item->get_title(), text); + if(!body_item->visible && !body_item->get_description().empty()) + body_item->visible = string_find_case_insensitive(body_item->get_description(), text); + } + bool Body::no_items_visible() const { for(auto &item : items) { if(item->visible) diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 625a426..dc7e6f3 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -2978,8 +2978,13 @@ namespace QuickMedia { bool is_window_focused = window.hasFocus(); - auto process_new_room_messages = [this, &body_items_by_room, ¤t_room, &is_window_focused](RoomSyncMessages &room_sync_messages, bool only_show_mentions) mutable { + // TODO: Always keep the mentioned rooms at the top, and then the ones with unread messages below? + auto process_new_room_messages = [this, &tabs, &body_items_by_room, ¤t_room, &is_window_focused](RoomSyncMessages &room_sync_messages, bool is_first_sync) mutable { + size_t room_swap_index = 0; for(auto &[room, messages] : room_sync_messages) { + if(messages.empty()) + continue; + bool was_mentioned = false; for(auto &message : messages) { if(message->mentions_me) { @@ -2996,31 +3001,28 @@ namespace QuickMedia { continue; // TODO: this wont always work because we dont display all types of messages from server, such as "joined", "left", "kicked", "banned", "changed avatar", "changed display name", etc. - // TODO: Update local marker when another client with our user sets read marker, in that case our read marker (room->get_user_read_marker) will be updated. - bool unread_messages_previous_session = false; - if(!messages.empty()) { - std::shared_ptr<UserInfo> me = matrix->get_me(room); - if(me && room->get_user_read_marker(me) != messages.back()->event_id) - unread_messages_previous_session = true; - } - - if(only_show_mentions && !unread_messages_previous_session) { - std::string room_desc; - if(!messages.empty()) - room_desc = matrix->message_get_author_displayname(messages.back().get()) + ": " + extract_first_line(messages.back()->body, 150); - if(was_mentioned) { - room_desc += "\n** You were mentioned **"; // TODO: Better notification? - room_body_item_it->second.body_item->set_title_color(sf::Color(255, 100, 100)); - room_body_item_it->second.last_message_read = false; - } - room_body_item_it->second.body_item->set_description(std::move(room_desc)); - } else if(!messages.empty()) { + bool unread_messages = false; + std::shared_ptr<UserInfo> me = matrix->get_me(room); + if(me && room->get_user_read_marker(me) != messages.back()->event_id) + unread_messages = true; + + if(unread_messages) { std::string room_desc = "Unread: " + matrix->message_get_author_displayname(messages.back().get()) + ": " + extract_first_line(messages.back()->body, 150); if(was_mentioned) room_desc += "\n** You were mentioned **"; // TODO: Better notification? room_body_item_it->second.body_item->set_description(std::move(room_desc)); room_body_item_it->second.body_item->set_title_color(sf::Color(255, 100, 100)); room_body_item_it->second.last_message_read = false; + + // 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_it->second.body_item.get()); + 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_it->second.body_item->set_description(matrix->message_get_author_displayname(messages.back().get()) + ": " + extract_first_line(messages.back()->body, 150)); } } }; @@ -3041,6 +3043,141 @@ namespace QuickMedia { sf::Sprite logo_sprite(plugin_logo); + sf::Text room_name_text("", *bold_font, 18); + const float room_name_text_height = 20.0f; + const float room_name_text_padding_y = 10.0f; + const float room_name_total_height = room_name_text_height + room_name_text_padding_y * 2.0f; + const float room_avatar_height = 32.0f; + + sf::Sprite room_avatar_sprite; + auto room_avatar_thumbnail_data = std::make_shared<ThumbnailData>(); + AsyncImageLoader async_image_loader; + + sf::Clock read_marker_timer; + const sf::Int32 read_marker_timeout_ms_default = 3000; + sf::Int32 read_marker_timeout_ms = 0; + + std::future<void> set_read_marker_future; + bool setting_read_marker = false; + + bool redraw = true; + + auto get_room_by_ptr = [&body_items_by_room](RoomData *room_ptr) -> std::shared_ptr<RoomData> { + for(auto &[room, body_data] : body_items_by_room) { + if(room.get() == room_ptr) + return room; + } + return 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: 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 set_body_as_deleted = [](Message *message, BodyItem *body_item) { + body_item->embedded_item = nullptr; + body_item->embedded_item_status = EmbeddedItemStatus::NONE; + body_item->thumbnail_url = message->user->avatar_url; + body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; + body_item->set_description_color(sf::Color::White); + body_item->thumbnail_size = sf::Vector2i(32, 32); + }; + + // TODO: Optimize with hash map? + auto resolve_unreferenced_events_with_body_items = [&set_body_as_deleted, &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_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) + set_body_as_deleted(message.get(), body_item.get()); + it = unreferenced_events.erase(it); + } else { + ++it; + } + } else { + ++it; + } + } + }; + + // TODO: Optimize with hash map? + auto modify_related_messages_in_current_room = [&set_body_as_deleted, &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_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); + if(message->related_event_type == RelatedEventType::REDACTION) + set_body_as_deleted(message.get(), body_item.get()); + } else { + unreferenced_events.push_back(message); + } + } + } + }; + + SearchBar room_search_bar(*font, &plugin_logo, "Search..."); + room_search_bar.autocomplete_search_delay = SEARCH_DELAY_FILTER; + room_search_bar.onTextUpdateCallback = [&tabs](const std::string &text) { + tabs[ROOMS_TAB_INDEX].body->filter_search_fuzzy(text); + //tabs[ROOMS_TAB_INDEX].body->select_first_item(); + }; + + room_search_bar.onTextSubmitCallback = + [this, &tabs, &selected_tab, ¤t_room, ¤t_room_body_data, &room_name_text, + &get_room_by_ptr, &body_items_by_room, &modify_related_messages_in_current_room, + &room_avatar_thumbnail_data, &read_marker_timeout_ms, &redraw] + (const std::string&) + { + BodyItem *selected_item = tabs[ROOMS_TAB_INDEX].body->get_selected(); + if(!selected_item) + return; + + tabs[ROOMS_TAB_INDEX].body->clear_cache(); + + current_room = get_room_by_ptr((RoomData*)selected_item->userdata); + assert(current_room); + selected_tab = MESSAGES_TAB_INDEX; + tabs[MESSAGES_TAB_INDEX].body->clear_items(); + + Messages new_messages; + if(matrix->get_all_synced_room_messages(current_room, new_messages) == PluginResult::OK) { + tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps(messages_to_body_items(new_messages, matrix->get_me(current_room).get())); + tabs[MESSAGES_TAB_INDEX].body->select_last_item(); + modify_related_messages_in_current_room(new_messages); + } else { + std::string err_msg = "Failed to get messages in room: " + current_room->id; + show_notification("QuickMedia", err_msg, Urgency::CRITICAL); + } + + auto room_body_item_it = body_items_by_room.find(current_room); + if(room_body_item_it != body_items_by_room.end()) { + current_room_body_data = &room_body_item_it->second; + room_name_text.setString(current_room_body_data->body_item->get_title()); + room_avatar_thumbnail_data = std::make_shared<ThumbnailData>(); + } + + read_marker_timeout_ms = 0; + redraw = true; + }; + Entry chat_input("Press m or i to begin writing a message...", font.get(), cjk_font.get()); chat_input.draw_background = false; chat_input.set_editable(false); @@ -3129,16 +3266,6 @@ namespace QuickMedia { 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) { @@ -3171,7 +3298,6 @@ namespace QuickMedia { const float tab_spacer_height = 0.0f; sf::Vector2f body_pos; sf::Vector2f body_size; - bool redraw = true; sf::Event event; sf::RectangleShape tab_shade; @@ -3194,17 +3320,7 @@ namespace QuickMedia { const double typing_timeout_seconds = 3.0; bool typing = false; - const float tab_vertical_offset = 10.0f; - - sf::Text room_name_text("", *bold_font, 18); - const float room_name_text_height = 20.0f; - const float room_name_text_padding_y = 10.0f; - const float room_name_total_height = room_name_text_height + room_name_text_padding_y * 2.0f; - const float room_avatar_height = 32.0f; - - sf::Sprite room_avatar_sprite; - auto room_avatar_thumbnail_data = std::make_shared<ThumbnailData>(); - AsyncImageLoader async_image_loader; + float tab_vertical_offset = 0.0f; auto typing_async_func = [this](bool new_state, std::shared_ptr<RoomData> room) { if(new_state) { @@ -3225,13 +3341,6 @@ namespace QuickMedia { Body url_selection_body(this, font.get(), bold_font.get(), cjk_font.get()); - sf::Clock read_marker_timer; - const sf::Int32 read_marker_timeout_ms_default = 3000; - sf::Int32 read_marker_timeout_ms = 0; - - std::future<void> set_read_marker_future; - bool setting_read_marker = false; - auto launch_url = [this, &video_page, &redraw](const std::string &url) mutable { if(url.empty()) return; @@ -3276,10 +3385,12 @@ namespace QuickMedia { } }; - auto add_new_rooms = [&tabs, &body_items_by_room, ¤t_room, ¤t_room_body_data, &room_name_text](Rooms &rooms) { + auto add_new_rooms = [&tabs, &body_items_by_room, ¤t_room, ¤t_room_body_data, &room_name_text, &room_search_bar](Rooms &rooms) { if(rooms.empty()) return; + std::string search_filter_text = room_search_bar.get_text(); + for(size_t i = 0; i < rooms.size(); ++i) { auto &room = rooms[i]; std::string room_name = room->name; @@ -3291,6 +3402,7 @@ namespace QuickMedia { body_item->userdata = room.get(); // Note: this has to be valid as long as the room list is valid! body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; body_item->thumbnail_size = sf::Vector2i(32, 32); + tabs[ROOMS_TAB_INDEX].body->filter_search_fuzzy_item(search_filter_text, body_item.get()); tabs[ROOMS_TAB_INDEX].body->items.push_back(body_item); body_items_by_room[room] = { body_item, true, 0 }; } @@ -3306,71 +3418,19 @@ namespace QuickMedia { } }; - auto get_room_by_ptr = [&body_items_by_room](RoomData *room_ptr) -> std::shared_ptr<RoomData> { - for(auto &[room, body_data] : body_items_by_room) { - if(room.get() == room_ptr) - return room; - } - 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 set_body_as_deleted = [](Message *message, BodyItem *body_item) { - body_item->embedded_item = nullptr; - body_item->embedded_item_status = EmbeddedItemStatus::NONE; - body_item->thumbnail_url = message->user->avatar_url; - body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; - body_item->set_description_color(sf::Color::White); - body_item->thumbnail_size = sf::Vector2i(32, 32); - }; - - // TODO: Optimize with hash map? - auto resolve_unreferenced_events_with_body_items = [&set_body_as_deleted, &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_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) - set_body_as_deleted(message.get(), body_item.get()); - it = unreferenced_events.erase(it); - } else { - ++it; - } - } else { - ++it; - } - } - }; - - // TODO: Optimize with hash map? - auto modify_related_messages_in_current_room = [&set_body_as_deleted, &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_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); - if(message->related_event_type == RelatedEventType::REDACTION) - set_body_as_deleted(message.get(), body_item.get()); - } else { - unreferenced_events.push_back(message); - } - } - } - }; + float tab_shade_height = 0.0f; while (current_page == PageType::CHAT) { sf::Int32 frame_time_ms = frame_timer.restart().asMilliseconds(); while (window.pollEvent(event)) { base_event_handler(event, PageType::EXIT, tabs[selected_tab].body.get(), nullptr, false, false); + + if(tabs[selected_tab].type == ChatTabType::ROOMS) { + if(event.type == sf::Event::TextEntered) + room_search_bar.onTextEntered(event.text.unicode); + room_search_bar.on_event(event); + } + if(event.type == sf::Event::GainedFocus) { is_window_focused = true; redraw = true; @@ -3610,36 +3670,6 @@ namespace QuickMedia { } //chat_input.on_event(event); chat_input.process_event(event); - } else if(tabs[selected_tab].type == ChatTabType::ROOMS && event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Enter) { - BodyItem *selected_item = tabs[selected_tab].body->get_selected(); - if(selected_item) { - tabs[selected_tab].body->clear_cache(); - - current_room = get_room_by_ptr((RoomData*)selected_item->userdata); - assert(current_room); - selected_tab = MESSAGES_TAB_INDEX; - tabs[MESSAGES_TAB_INDEX].body->clear_items(); - - Messages new_messages; - if(matrix->get_all_synced_room_messages(current_room, new_messages) == PluginResult::OK) { - tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps(messages_to_body_items(new_messages, matrix->get_me(current_room).get())); - tabs[MESSAGES_TAB_INDEX].body->select_last_item(); - modify_related_messages_in_current_room(new_messages); - } else { - std::string err_msg = "Failed to get messages in room: " + current_room->id; - show_notification("QuickMedia", err_msg, Urgency::CRITICAL); - } - - auto room_body_item_it = body_items_by_room.find(current_room); - if(room_body_item_it != body_items_by_room.end()) { - current_room_body_data = &room_body_item_it->second; - room_name_text.setString(current_room_body_data->body_item->get_title()); - room_avatar_thumbnail_data = std::make_shared<ThumbnailData>(); - } - - read_marker_timeout_ms = 0; - redraw = true; - } } } @@ -3735,8 +3765,11 @@ namespace QuickMedia { } } - const float room_name_padding_y = (selected_tab == MESSAGES_TAB_INDEX ? room_name_total_height : 0.0f); - const float tab_shade_height = tab_spacer_height + std::floor(tab_vertical_offset) + tab_height + 10.0f + room_name_padding_y; + float room_name_padding_y = 0.0f; + if(selected_tab == MESSAGES_TAB_INDEX) + room_name_padding_y = room_name_total_height; + else if(selected_tab == ROOMS_TAB_INDEX) + room_name_padding_y = room_search_bar.getBottomWithoutShadow(); chat_input_height_full = chat_input.get_height() + chat_input_padding_y * 2.0f; if(selected_tab != MESSAGES_TAB_INDEX) @@ -3750,6 +3783,21 @@ namespace QuickMedia { if(redraw) { redraw = false; + room_search_bar.onWindowResize(window_size); + + float room_name_padding_y = 0.0f; + float padding_bottom = 0.0f; + if(selected_tab == MESSAGES_TAB_INDEX) { + room_name_padding_y = 10.0f + room_name_total_height; + tab_vertical_offset = 10.0f; + } else if(selected_tab == ROOMS_TAB_INDEX) { + room_name_padding_y = room_search_bar.getBottomWithoutShadow(); + tab_vertical_offset = 0.0f; + padding_bottom = 10.0f; + } + + tab_shade_height = tab_spacer_height + std::floor(tab_vertical_offset) + tab_height + room_name_padding_y + padding_bottom; + chat_input.set_max_width(window_size.x - (logo_padding_x + plugin_logo.getSize().x + chat_input_padding_x * 2.0f)); chat_input.set_position(sf::Vector2f(logo_padding_x + plugin_logo.getSize().x + chat_input_padding_x, window_size.y - chat_height - chat_input_padding_y)); @@ -3773,6 +3821,8 @@ namespace QuickMedia { logo_sprite.setPosition(logo_padding_x, window_size.y - chat_input_shade.getSize().y * 0.5f - plugin_logo.getSize().y * 0.5f); } + room_search_bar.update(); + if(!sync_running && sync_timer.getElapsedTime().asMilliseconds() >= sync_min_time_ms) { fprintf(stderr, "Time since last sync: %d ms\n", sync_timer.getElapsedTime().asMilliseconds()); sync_min_time_ms = 1000; @@ -3877,6 +3927,8 @@ namespace QuickMedia { } room_name_text.setPosition(body_pos.x + room_name_text_offset_x, room_name_text_padding_y + 4.0f); window.draw(room_name_text); + } else if(tabs[selected_tab].type == ChatTabType::ROOMS) { + room_search_bar.draw(window, false); } gradient_points[0].position.x = 0.0f; |