From eac2ace1c14c1ae0564d757b26a359c6bd4b754a Mon Sep 17 00:00:00 2001 From: dec05eba Date: Fri, 25 Sep 2020 04:15:17 +0200 Subject: Matrix: fetch previous messages when reaching the top --- include/Body.hpp | 1 + include/SearchBar.hpp | 3 ++ plugins/Matrix.hpp | 9 ++-- src/Body.cpp | 8 ++++ src/QuickMedia.cpp | 75 ++++++++++++++++++++----------- src/SearchBar.cpp | 2 - src/plugins/Matrix.cpp | 117 +++++++++++++++++++++++++++++++++++-------------- 7 files changed, 151 insertions(+), 64 deletions(-) diff --git a/include/Body.hpp b/include/Body.hpp index f246b5a..79bece6 100644 --- a/include/Body.hpp +++ b/include/Body.hpp @@ -83,6 +83,7 @@ namespace QuickMedia { void select_first_item(); void reset_selected(); void clear_items(); + void prepend_items(BodyItems new_items); void append_items(BodyItems new_items); void clear_thumbnails(); diff --git a/include/SearchBar.hpp b/include/SearchBar.hpp index b4abd77..a90c69d 100644 --- a/include/SearchBar.hpp +++ b/include/SearchBar.hpp @@ -43,6 +43,9 @@ namespace QuickMedia { int text_autosearch_delay; int autocomplete_search_delay; bool caret_visible; + + float padding_vertical = 20.0f; + float background_margin_vertical = 4.0f; private: void clear_autocomplete_if_text_not_substring(); void clear_autocomplete_if_last_char_not_substr(); diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index 607ef81..396fc17 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -43,6 +43,7 @@ namespace QuickMedia { std::string avatar_url; std::string prev_batch; bool initial_fetch_finished = false; + size_t last_read_index = 0; }; enum class MessageDirection { @@ -62,9 +63,9 @@ namespace QuickMedia { PluginResult get_cached_sync(BodyItems &result_items); PluginResult sync(); PluginResult get_joined_rooms(BodyItems &result_items); - // Note: the number of items returned in |result_items| may not be the number of new messages because many messages can be combined - // into one if one user sends multiple messages. The number of messages is returned in |num_new_messages|. - PluginResult get_room_messages(const std::string &room_id, size_t start_index, BodyItems &result_items, size_t &num_new_messages); + PluginResult get_all_synced_room_messages(const std::string &room_id, BodyItems &result_items); + PluginResult get_new_room_messages(const std::string &room_id, BodyItems &result_items); + PluginResult get_previous_room_messages(const std::string &room_id, BodyItems &result_items); SearchResult search(const std::string &text, BodyItems &result_items) override; // |url| should only be set when uploading media. @@ -76,7 +77,7 @@ namespace QuickMedia { PluginResult load_and_verify_cached_session(); private: PluginResult sync_response_to_body_items(const Json::Value &root); - PluginResult load_initial_room_data(const std::string &room_id, RoomData *room_data); + PluginResult get_previous_room_messages(const std::string &room_id, RoomData *room_data); void events_add_user_info(const Json::Value &events_json, RoomData *room_data); void events_add_messages(const Json::Value &events_json, RoomData *room_data, MessageDirection message_dir); void events_set_room_name(const Json::Value &events_json, RoomData *room_data); diff --git a/src/Body.cpp b/src/Body.cpp index 52999e5..f97d611 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -157,6 +157,14 @@ namespace QuickMedia { selected_item = 0; } + // TODO: Optimize with memcpy and changing capacity before loop + void Body::prepend_items(BodyItems new_items) { + for(auto &body_item : new_items) { + items.insert(items.begin(), std::move(body_item)); + } + } + + // TODO: Optimize with memcpy and changing capacity before loop void Body::append_items(BodyItems new_items) { for(auto &body_item : new_items) { items.push_back(std::move(body_item)); diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index e367ae0..c701c14 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -3188,12 +3188,13 @@ namespace QuickMedia { std::vector tabs; int selected_tab = 0; - size_t room_message_index = 0; 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->draw_thumbnails = true; + messages_tab.body->thumbnail_resize_target_size.x = 600; + messages_tab.body->thumbnail_resize_target_size.y = 337; //messages_tab.body->line_seperator_color = sf::Color::Transparent; messages_tab.text = sf::Text("Messages", *font, tab_text_size); tabs.push_back(std::move(messages_tab)); @@ -3216,6 +3217,8 @@ namespace QuickMedia { fprintf(stderr, "Loaded matrix sync from cache, num items: %zu\n", tabs[MESSAGES_TAB_INDEX].body->items.size()); } */ + // This is needed to get initial data, with joined rooms etc. TODO: Remove this once its cached + // and allow asynchronous update of rooms if(matrix->sync() != PluginResult::OK) { show_notification("QuickMedia", "Intial matrix sync failed", Urgency::CRITICAL); current_page = Page::EXIT; @@ -3234,9 +3237,11 @@ namespace QuickMedia { if(!tabs[ROOMS_TAB_INDEX].body->items.empty()) current_room_id = tabs[ROOMS_TAB_INDEX].body->items[0]->url; - // TODO: Allow empty initial room (if the user hasn't joined any room yet) + // TODO: Allow empty initial room (if the user hasn't joined any room yet). assert(!current_room_id.empty()); + // get_all_room_messages is not needed here because its done in the loop, where the initial timeout is 0ms + { std::string plugin_logo_path = resources_root + "images/matrix_logo.png"; if(!plugin_logo.loadFromFile(plugin_logo_path)) { @@ -3249,13 +3254,16 @@ namespace QuickMedia { SearchBar chat_input(*font, &plugin_logo, "Send a message..."); chat_input.set_background_color(sf::Color::Transparent); + chat_input.padding_vertical = 10.0f; + + // TODO: Scroll to bottom when receiving new messages, but only if we are already at the bottom? // TODO: Filer for rooms and settings chat_input.onTextUpdateCallback = nullptr; // TODO: Show post message immediately, instead of waiting for sync. Otherwise it can take a while until we receive the message, // which happens when uploading an image. - chat_input.onTextSubmitCallback = [this, matrix, &tabs, &selected_tab, &room_message_index, ¤t_room_id](const std::string &text) -> bool { + chat_input.onTextSubmitCallback = [this, matrix, &tabs, &selected_tab, ¤t_room_id](const std::string &text) -> bool { if(tabs[selected_tab].type == ChatTabType::MESSAGES) { if(text.empty()) return false; @@ -3299,17 +3307,12 @@ namespace QuickMedia { if(selected_item) { current_room_id = selected_item->url; selected_tab = MESSAGES_TAB_INDEX; - room_message_index = 0; tabs[MESSAGES_TAB_INDEX].body->clear_items(); - size_t num_new_messages = 0; BodyItems new_items; // TODO: Make asynchronous - if(matrix->get_room_messages(current_room_id, 0, new_items, num_new_messages) == PluginResult::OK) { - room_message_index += num_new_messages; + if(matrix->get_all_synced_room_messages(current_room_id, new_items) == PluginResult::OK) { tabs[MESSAGES_TAB_INDEX].body->append_items(std::move(new_items)); - if(!tabs[MESSAGES_TAB_INDEX].body->items.empty() && num_new_messages > 0) - tabs[MESSAGES_TAB_INDEX].body->set_selected_item(tabs[MESSAGES_TAB_INDEX].body->items.size() - 1); } else { std::string err_msg = "Failed to get messages in room: " + current_room_id; show_notification("QuickMedia", err_msg, Urgency::CRITICAL); @@ -3320,17 +3323,16 @@ namespace QuickMedia { return false; }; - struct SyncFutureResult { - BodyItems body_items; - size_t num_new_messages; - }; - - std::future sync_future; + std::future sync_future; bool sync_running = false; std::string sync_future_room_id; sf::Clock sync_timer; sf::Int32 sync_min_time_ms = 0; // Sync immediately the first time + std::future previous_messages_future; + bool fetching_previous_messages_running = false; + std::string previous_messages_future_room_id; + const float tab_spacer_height = 0.0f; sf::Vector2f body_pos; sf::Vector2f body_size; @@ -3350,7 +3352,19 @@ namespace QuickMedia { redraw = true; } else if(event.type == sf::Event::KeyPressed) { if(event.key.code == sf::Keyboard::Up) { - tabs[selected_tab].body->select_previous_item(); + bool item_changed = tabs[selected_tab].body->select_previous_item(); + // Top hit + if(!item_changed && !fetching_previous_messages_running) { + fetching_previous_messages_running = true; + previous_messages_future_room_id = current_room_id; + previous_messages_future = std::async(std::launch::async, [this, &previous_messages_future_room_id]() { + Matrix *matrix = static_cast(current_plugin); + BodyItems result_items; + if(matrix->get_previous_room_messages(previous_messages_future_room_id, result_items) != PluginResult::OK) + fprintf(stderr, "Failed to get previous matrix messages in room: %s\n", previous_messages_future_room_id.c_str()); + return result_items; + }); + } } else if(event.key.code == sf::Keyboard::Down) { tabs[selected_tab].body->select_next_item(); } else if(event.key.code == sf::Keyboard::Escape) { @@ -3420,36 +3434,45 @@ namespace QuickMedia { sync_running = true; sync_timer.restart(); sync_future_room_id = current_room_id; - sync_future = std::async(std::launch::async, [this, &sync_future_room_id, room_message_index]() { + sync_future = std::async(std::launch::async, [this, &sync_future_room_id]() { Matrix *matrix = static_cast(current_plugin); - SyncFutureResult result; - result.num_new_messages = 0; + BodyItems result_items; if(matrix->sync() == PluginResult::OK) { fprintf(stderr, "Synced matrix\n"); - if(matrix->get_room_messages(sync_future_room_id, room_message_index, result.body_items, result.num_new_messages) != PluginResult::OK) { + if(matrix->get_new_room_messages(sync_future_room_id, result_items) != PluginResult::OK) { fprintf(stderr, "Failed to get new matrix messages in room: %s\n", sync_future_room_id.c_str()); } } else { fprintf(stderr, "Failed to sync matrix\n"); } - return result; + return result_items; }); } if(sync_future.valid() && sync_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - SyncFutureResult sync_future_result = sync_future.get(); + BodyItems new_body_items = sync_future.get(); // Ignore finished sync if it happened in another room. When we navigate back to the room we will get the messages again if(sync_future_room_id == current_room_id) { - room_message_index += sync_future_result.num_new_messages; - tabs[MESSAGES_TAB_INDEX].body->append_items(std::move(sync_future_result.body_items)); - if(!tabs[MESSAGES_TAB_INDEX].body->items.empty() && sync_future_result.num_new_messages > 0) - tabs[MESSAGES_TAB_INDEX].body->set_selected_item(tabs[MESSAGES_TAB_INDEX].body->items.size() - 1); + tabs[MESSAGES_TAB_INDEX].body->append_items(std::move(new_body_items)); } sync_running = false; } + if(previous_messages_future.valid() && previous_messages_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + BodyItems new_body_items = previous_messages_future.get(); + fprintf(stderr, "Finished fetching older messages, num new messages: %zu\n", new_body_items.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 + if(previous_messages_future_room_id == current_room_id) { + size_t num_new_messages = new_body_items.size(); + int selected_item_index = tabs[MESSAGES_TAB_INDEX].body->get_selected_item(); + tabs[MESSAGES_TAB_INDEX].body->prepend_items(std::move(new_body_items)); + tabs[MESSAGES_TAB_INDEX].body->set_selected_item(selected_item_index + num_new_messages); + } + fetching_previous_messages_running = false; + } + chat_input.update(); window.clear(back_color); diff --git a/src/SearchBar.cpp b/src/SearchBar.cpp index ad6f709..e646546 100644 --- a/src/SearchBar.cpp +++ b/src/SearchBar.cpp @@ -8,9 +8,7 @@ const sf::Color text_placeholder_color(255, 255, 255, 100); const sf::Color front_color(55, 60, 68); const float background_margin_horizontal = 15.0f; -const float background_margin_vertical = 4.0f; const float PADDING_HORIZONTAL = 25.0f; -const float padding_vertical = 20.0f; namespace QuickMedia { SearchBar::SearchBar(sf::Font &font, sf::Texture *plugin_logo, const std::string &placeholder, bool input_masked) : diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 2d4fd4a..529d42a 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -159,9 +159,37 @@ namespace QuickMedia { return PluginResult::OK; } - PluginResult Matrix::get_room_messages(const std::string &room_id, size_t start_index, BodyItems &result_items, size_t &num_new_messages) { - num_new_messages = 0; + static void room_messages_to_body_items(RoomData *room_data, Message *messages, size_t num_messages, BodyItems &result_items) { + // TODO: This prev_user_id should check previous message, otherwise if after a sync there is only 1 new message then it wont + // merge with the previous message. But for that to work then also have to send existing body items to this function, + // to get the body item to merge with + size_t prev_user_id = -1; + for(size_t i = 0; i < num_messages; ++i) { + const UserInfo &user_info = room_data->user_info[messages[i].user_id]; + if(messages[i].user_id == prev_user_id && messages[i].url.empty()) { + assert(!result_items.empty()); + result_items.back()->append_description("\n"); + result_items.back()->append_description(messages[i].body); + } else { + auto body_item = std::make_unique(""); + body_item->set_author(user_info.display_name); + body_item->set_description(messages[i].body); + if(!messages[i].thumbnail_url.empty()) + body_item->thumbnail_url = messages[i].thumbnail_url; + else if(!messages[i].url.empty()) + body_item->thumbnail_url = messages[i].url; + else + body_item->thumbnail_url = user_info.avatar_url; + // TODO: Show image thumbnail inline instead of url to image + body_item->url = messages[i].url; + result_items.push_back(std::move(body_item)); + prev_user_id = messages[i].user_id; + } + } + } + // TODO: Merge common code with |get_new_room_messages| + PluginResult Matrix::get_all_synced_room_messages(const std::string &room_id, BodyItems &result_items) { auto room_it = room_data_by_id.find(room_id); if(room_it == room_data_by_id.end()) { fprintf(stderr, "Error: no such room: %s\n", room_id.c_str()); @@ -169,7 +197,7 @@ namespace QuickMedia { } if(!room_it->second->initial_fetch_finished) { - PluginResult result = load_initial_room_data(room_id, room_it->second.get()); + PluginResult result = get_previous_room_messages(room_id, room_it->second.get()); if(result == PluginResult::OK) { room_it->second->initial_fetch_finished = true; } else { @@ -178,36 +206,49 @@ namespace QuickMedia { } } - // This will happen if there are no new messages - if(start_index >= room_it->second->messages.size()) - return PluginResult::OK; + room_messages_to_body_items(room_it->second.get(), room_it->second->messages.data(), room_it->second->messages.size(), result_items); + room_it->second->last_read_index = room_it->second->messages.size(); + return PluginResult::OK; + } - num_new_messages = room_it->second->messages.size() - start_index; + PluginResult Matrix::get_new_room_messages(const std::string &room_id, BodyItems &result_items) { + auto room_it = room_data_by_id.find(room_id); + if(room_it == room_data_by_id.end()) { + fprintf(stderr, "Error: no such room: %s\n", room_id.c_str()); + return PluginResult::ERR; + } - size_t prev_user_id = -1; - for(auto it = room_it->second->messages.begin() + start_index, end = room_it->second->messages.end(); it != end; ++it) { - const UserInfo &user_info = room_it->second->user_info[it->user_id]; - if(it->user_id == prev_user_id && it->url.empty()) { - assert(!result_items.empty()); - result_items.back()->append_description("\n"); - result_items.back()->append_description(it->body); + if(!room_it->second->initial_fetch_finished) { + PluginResult result = get_previous_room_messages(room_id, room_it->second.get()); + if(result == PluginResult::OK) { + room_it->second->initial_fetch_finished = true; } else { - auto body_item = std::make_unique(""); - body_item->set_author(user_info.display_name); - body_item->set_description(it->body); - if(!it->thumbnail_url.empty()) - body_item->thumbnail_url = it->thumbnail_url; - else if(!it->url.empty()) - body_item->thumbnail_url = it->url; - else - body_item->thumbnail_url = user_info.avatar_url; - // TODO: Show image thumbnail inline instead of url to image - body_item->url = it->url; - result_items.push_back(std::move(body_item)); - prev_user_id = it->user_id; + fprintf(stderr, "Initial sync failed for room: %s\n", room_id.c_str()); + return result; } } + size_t num_new_messages = room_it->second->messages.size() - room_it->second->last_read_index; + room_messages_to_body_items(room_it->second.get(), room_it->second->messages.data() + room_it->second->last_read_index, num_new_messages, result_items); + room_it->second->last_read_index = room_it->second->messages.size(); + return PluginResult::OK; + } + + PluginResult Matrix::get_previous_room_messages(const std::string &room_id, BodyItems &result_items) { + auto room_it = room_data_by_id.find(room_id); + if(room_it == room_data_by_id.end()) { + fprintf(stderr, "Error: no such room: %s\n", room_id.c_str()); + return PluginResult::ERR; + } + + size_t num_messages_before = room_it->second->messages.size(); + PluginResult result = get_previous_room_messages(room_id, room_it->second.get()); + if(result != PluginResult::OK) + return result; + + size_t num_messages_after = room_it->second->messages.size(); + size_t num_new_messages = num_messages_after - num_messages_before; + room_messages_to_body_items(room_it->second.get(), room_it->second->messages.data(), num_new_messages, result_items); return PluginResult::OK; } @@ -270,10 +311,12 @@ namespace QuickMedia { if(!timeline_json.isObject()) continue; - // This may be non-existent if this is the first event in the room - const Json::Value &prev_batch_json = timeline_json["prev_batch"]; - if(prev_batch_json.isString()) - room_it->second->prev_batch = prev_batch_json.asString(); + if(room_it->second->prev_batch.empty()) { + // This may be non-existent if this is the first event in the room + const Json::Value &prev_batch_json = timeline_json["prev_batch"]; + if(prev_batch_json.isString()) + room_it->second->prev_batch = prev_batch_json.asString(); + } const Json::Value &events_json = timeline_json["events"]; events_add_user_info(events_json, room_it->second.get()); @@ -415,8 +458,11 @@ namespace QuickMedia { } } + // TODO: Loop and std::move instead? doesn't insert create copies? if(message_dir == MessageDirection::BEFORE) { room_data->messages.insert(room_data->messages.begin(), new_messages.rbegin(), new_messages.rend()); + if(room_data->last_read_index != 0) + room_data->last_read_index += new_messages.size(); } else if(message_dir == MessageDirection::AFTER) { room_data->messages.insert(room_data->messages.end(), new_messages.begin(), new_messages.end()); } @@ -500,7 +546,7 @@ namespace QuickMedia { } } - PluginResult Matrix::load_initial_room_data(const std::string &room_id, RoomData *room_data) { + PluginResult Matrix::get_previous_room_messages(const std::string &room_id, RoomData *room_data) { std::string from = room_data->prev_batch; if(from.empty()) { fprintf(stderr, "Info: missing previous batch for room: %s, using /sync next batch\n", room_id.c_str()); @@ -554,6 +600,13 @@ namespace QuickMedia { const Json::Value &chunk_json = json_root["chunk"]; events_add_messages(chunk_json, room_data, MessageDirection::BEFORE); + const Json::Value &end_json = json_root["end"]; + if(!end_json.isString()) { + fprintf(stderr, "Warning: matrix messages response is missing 'end', this could happen if we received the very first messages in the room\n"); + return PluginResult::OK; + } + + room_data->prev_batch = end_json.asString(); return PluginResult::OK; } -- cgit v1.2.3