diff options
author | dec05eba <dec05eba@protonmail.com> | 2021-08-19 16:40:42 +0200 |
---|---|---|
committer | dec05eba <dec05eba@protonmail.com> | 2021-08-19 17:13:17 +0200 |
commit | 4ff87535e7aae35bfbd66fe88402dcb513af249c (patch) | |
tree | 6427d23446d8f2aec80abc15d912010c26ae38fa | |
parent | f4a02cabfd7452ed13b9c7b2e8b20ea5886768c8 (diff) |
Matrix: add ctrl+r to navigate to replied to message and navigate to message from notifications tab
-rw-r--r-- | README.md | 7 | ||||
-rw-r--r-- | plugins/Matrix.hpp | 23 | ||||
-rw-r--r-- | src/Body.cpp | 4 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 357 | ||||
-rw-r--r-- | src/Tabs.cpp | 6 | ||||
-rw-r--r-- | src/plugins/Matrix.cpp | 194 |
6 files changed, 448 insertions, 143 deletions
@@ -101,9 +101,12 @@ Type text and then wait and QuickMedia will automatically search.\ `Alt+Down`/`Ctrl+Alt+J`: Select the room below the currently selected room.\ `Alt+Home`: Select the first room in the room list.\ `Alt+End`: Select the last room in the room list.\ -`Ctrl+P`: Pin the selected message. +`Ctrl+P`: Pin the selected message.\ +`Ctrl+R`: Navigate to the replied to message.\ +`Ctrl+B`: Navigate to the bottom (the latest message). #### Pinned messages page controls -`Ctrl+D`: Unpin the selected message. +`Ctrl+D`: Unpin the selected message.\ +`Ctrl+R`: Navigate to the pinned message in the messages tab. #### Message input controls `Esc`: Stop typing (also clears the input text).\ `Enter`: Post message.\ diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index a27a4f6..0d29821 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -91,6 +91,8 @@ namespace QuickMedia { // TODO: Store body item ref here }; + using Messages = std::vector<std::shared_ptr<Message>>; + struct RoomData { std::shared_ptr<UserInfo> get_user_by_id(const std::string &user_id); void add_user(std::shared_ptr<UserInfo> user); @@ -106,9 +108,9 @@ namespace QuickMedia { void set_user_avatar_url(std::shared_ptr<UserInfo> &user, std::string avatar_url); // Ignores duplicates, returns the number of added messages - size_t prepend_messages_reverse(const std::vector<std::shared_ptr<Message>> &new_messages); + size_t prepend_messages_reverse(const Messages &new_messages); // Ignores duplicates, returns the number of added messages - size_t append_messages(const std::vector<std::shared_ptr<Message>> &new_messages); + size_t append_messages(const Messages &new_messages); std::shared_ptr<Message> get_message_by_id(const std::string &id); @@ -118,7 +120,7 @@ namespace QuickMedia { void acquire_room_lock(); void release_room_lock(); - const std::vector<std::shared_ptr<Message>>& get_messages_thread_unsafe() const; + const Messages& get_messages_thread_unsafe() const; const std::vector<std::string>& get_pinned_events_thread_unsafe() const; bool has_prev_batch(); @@ -177,7 +179,7 @@ namespace QuickMedia { // Each room has its own list of user data, even if multiple rooms has the same user // because users can have different display names and avatars in different rooms. std::unordered_map<std::string, std::shared_ptr<UserInfo>> user_info_by_user_id; - std::vector<std::shared_ptr<Message>> messages; + Messages messages; std::unordered_map<std::string, std::shared_ptr<Message>> message_by_event_id; std::vector<std::string> pinned_events; std::set<std::string> tags; @@ -204,8 +206,6 @@ namespace QuickMedia { std::string content_uri; }; - using Messages = std::vector<std::shared_ptr<Message>>; - struct SyncData { Messages messages; std::optional<std::vector<std::string>> pinned_events; @@ -422,7 +422,7 @@ namespace QuickMedia { class MatrixChatPage : public Page { public: - MatrixChatPage(Program *program, std::string room_id, MatrixRoomsPage *rooms_page); + MatrixChatPage(Program *program, std::string room_id, MatrixRoomsPage *rooms_page, std::string jump_to_event_id = ""); ~MatrixChatPage(); const char* get_title() const override { return ""; } @@ -443,6 +443,8 @@ namespace QuickMedia { Body *chat_body = nullptr; bool messages_tab_visible = false; + + const std::string jump_to_event_id; private: RoomData *current_room = nullptr; Body *users_body = nullptr; @@ -522,7 +524,8 @@ namespace QuickMedia { void get_all_synced_room_messages(RoomData *room, Messages &messages); void get_all_pinned_events(RoomData *room, std::vector<std::string> &events); - PluginResult get_previous_room_messages(RoomData *room, Messages &messages, bool latest_messages = false); + PluginResult get_messages_in_direction(RoomData *room, const std::string &token, MessageDirection message_dir, Messages &messages, std::string &new_token); + PluginResult get_previous_room_messages(RoomData *room, Messages &messages, bool latest_messages = false, bool *reached_end = nullptr); PluginResult get_previous_notifications(std::function<void(const MatrixNotification&)> callback_func); void get_cached_notifications(std::function<void(const MatrixNotification&)> callback_func); @@ -577,6 +580,8 @@ namespace QuickMedia { // Returns nullptr if message cant be found. Note: cached std::shared_ptr<Message> get_message_by_id(RoomData *room, const std::string &event_id); + PluginResult get_message_context(RoomData *room, const std::string &event_id, std::shared_ptr<Message> &message, Messages &before_messages, Messages &after_messages, std::string &before_token, std::string &after_token); + void clear_previous_messages_token(RoomData *room); RoomData* get_room_by_id(const std::string &id); void update_room_users(RoomData *room); @@ -600,7 +605,7 @@ namespace QuickMedia { PluginResult parse_notifications(const rapidjson::Value ¬ifications_json, std::function<void(const MatrixNotification&)> callback_func); PluginResult parse_sync_account_data(const rapidjson::Value &account_data_json, std::optional<std::set<std::string>> &dm_rooms); PluginResult parse_sync_room_data(const rapidjson::Value &rooms_json, bool is_additional_messages_sync, bool initial_sync); - PluginResult get_previous_room_messages(RoomData *room_data, bool latest_messages, size_t &num_new_messages); + PluginResult get_previous_room_messages(RoomData *room_data, bool latest_messages, size_t &num_new_messages, bool *reached_end = nullptr); void events_add_user_info(const rapidjson::Value &events_json, RoomData *room_data); std::shared_ptr<UserInfo> parse_user_info(const rapidjson::Value &json, const std::string &user_id, RoomData *room_data); void events_set_user_read_marker(const rapidjson::Value &events_json, RoomData *room_data, std::shared_ptr<UserInfo> &me); diff --git a/src/Body.cpp b/src/Body.cpp index b7424c5..f9a3717 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -1581,6 +1581,7 @@ namespace QuickMedia { has_loaded_text = true; } + // TODO: Only do this if reactions dirty? if(!item->reactions.empty() && include_embedded_item) { float reaction_offset_x = 0.0f; item_height += body_spacing[body_theme].reaction_padding_y; @@ -1613,7 +1614,8 @@ namespace QuickMedia { const bool has_thumbnail = draw_thumbnails && !item->thumbnail_url.empty() && !merge_with_previous; const float padding_y = has_thumbnail ? body_spacing[body_theme].padding_y : body_spacing[body_theme].padding_y_text_only; item_height = std::max(item_height, item->loaded_image_size.y); - item_height += (padding_y * 2.0f); + if(item_height > 0.0f) + item_height += (padding_y * 2.0f); } item->loaded_height = item_height; diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 050ae01..f809b70 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -1885,6 +1885,7 @@ namespace QuickMedia { video_content_page(tabs[selected_tab].page.get(), static_cast<VideoPage*>(new_tabs[0].page.get()), "", false, tabs[selected_tab].body.get(), selected_index, &tab_associated_data[selected_tab].fetched_page, tab_associated_data[selected_tab].update_search_text); } else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::CHAT) { MatrixChatPage *tmp_matrix_chat_page = static_cast<MatrixChatPage*>(new_tabs[0].page.get()); + std::string jump_to_event_id = tmp_matrix_chat_page->jump_to_event_id; MatrixRoomsPage *rooms_page = tmp_matrix_chat_page->rooms_page; Body *room_list_body = rooms_page->body; rooms_page->clear_search(); @@ -1895,7 +1896,7 @@ namespace QuickMedia { rooms_page->body->show_drop_shadow = false; while(window.isOpen() && current_chat_room) { - auto matrix_chat_page = std::make_unique<MatrixChatPage>(this, current_chat_room->id, rooms_page); + auto matrix_chat_page = std::make_unique<MatrixChatPage>(this, current_chat_room->id, rooms_page, jump_to_event_id); bool move_room = chat_page(matrix_chat_page.get(), current_chat_room); matrix_chat_page->messages_tab_visible = false; if(!move_room) @@ -1906,6 +1907,7 @@ namespace QuickMedia { break; current_chat_room = matrix->get_room_by_id(selected_item->url); + jump_to_event_id.clear(); } rooms_page->body->show_drop_shadow = true; @@ -3768,7 +3770,6 @@ namespace QuickMedia { sf::Text captcha_solution_text("", *FontLoader::get_font(FontLoader::FontType::LATIN_BOLD), captcha_solution_text_height); int solved_captcha_ttl = 0; int64_t last_posted_time = time(nullptr); - fprintf(stderr, "unix time: %ld\n", last_posted_time); int64_t seconds_until_post_again = 60; // TODO: Timeout for other imageboards bool has_post_timeout = thread_page->get_pass_id().empty(); @@ -4545,6 +4546,22 @@ namespace QuickMedia { return nullptr; } + // TODO: Optimize + static std::shared_ptr<BodyItem> find_body_item_by_event_id(BodyItemList body_items, const std::string &event_id, int *index_result) { + if(event_id.empty()) + return nullptr; + + for(int i = 0; i < (int)body_items.size(); ++i) { + auto &body_item = body_items[i]; + if(body_item->userdata && static_cast<Message*>(body_item->userdata)->event_id == event_id) { + if(index_result) + *index_result = i; + return body_item; + } + } + return nullptr; + } + // Returns true if cached and loaded static bool load_cached_related_embedded_item(BodyItem *body_item, Message *message, UserInfo *me, const std::string &my_display_name, const std::string &my_user_id, const BodyItemList &message_body_items) { // Check if we already have the referenced message as a body item, so we dont create a new one. @@ -4683,7 +4700,7 @@ namespace QuickMedia { return replaces; replaces = replaces->replaces; } - return nullptr; + return message; } bool Program::chat_page(MatrixChatPage *matrix_chat_page, RoomData *current_room) { @@ -5398,7 +5415,13 @@ namespace QuickMedia { return false; }; - AsyncTask<Messages> previous_messages_future; + struct FetchMessagesResult { + Messages messages; + MessageDirection message_dir; + bool reached_end = false; + }; + AsyncTask<FetchMessagesResult> fetch_messages_future; + MessageDirection fetch_messages_dir = MessageDirection::BEFORE; enum class FetchMessageType { MESSAGE, @@ -5533,20 +5556,11 @@ namespace QuickMedia { sf::Vertex gradient_points[4]; double gradient_inc = 0; - bool fetched_enough_messages = false; - - auto fetch_more_previous_messages_if_needed = [this, &tabs, ¤t_room, &fetched_enough_messages, &previous_messages_future, MESSAGES_TAB_INDEX]() { - if(!fetched_enough_messages && !previous_messages_future.valid()) { - if(tabs[MESSAGES_TAB_INDEX].body->get_num_items() < 30) { - previous_messages_future = AsyncTask<Messages>([this, ¤t_room]() { - Messages messages; - if(matrix->get_previous_room_messages(current_room, messages) != PluginResult::OK) - fprintf(stderr, "Failed to get previous matrix messages in room: %s\n", current_room->id.c_str()); - return messages; - }); - } - } - }; + std::string before_token; + std::string after_token; + bool fetched_enough_messages_top = false; + bool fetched_enough_messages_bottom = false; + bool has_unread_messages = false; sf::RectangleShape more_messages_below_rect; more_messages_below_rect.setFillColor(get_current_theme().new_items_alert_color); @@ -5566,6 +5580,80 @@ namespace QuickMedia { bool avatar_applied = false; + auto jump_to_message = [&](const std::string &event_id) { + const int selected_tab = ui_tabs.get_selected(); + if(selected_tab != MESSAGES_TAB_INDEX || event_id.empty()) + return false; + + int body_item_index = -1; + auto body_item = find_body_item_by_event_id(tabs[MESSAGES_TAB_INDEX].body->get_items(), event_id, &body_item_index); + if(body_item) { + tabs[MESSAGES_TAB_INDEX].body->set_selected_item(body_item_index, false); + return true; + } else { + std::shared_ptr<Message> fetched_message; + Messages before_messages; + Messages after_messages; + std::string new_before_token; + std::string new_after_token; + TaskResult task_result = run_task_with_loading_screen([this, current_room, &event_id, &fetched_message, &before_messages, &after_messages, &new_before_token, &new_after_token]{ + std::string message_to_fetch = event_id; + for(int i = 0; i < 10; ++i) { // TODO: Delay between tries? + auto message = matrix->get_message_by_id(current_room, message_to_fetch); + if(!message) + return false; + + message_to_fetch = message->event_id; + if(message->related_event_type != RelatedEventType::EDIT) + break; + } + return matrix->get_message_context(current_room, message_to_fetch, fetched_message, before_messages, after_messages, new_before_token, new_after_token) == PluginResult::OK; + }); + + if(task_result == TaskResult::TRUE) { + Messages this_message = { fetched_message }; + Messages *messages[3] = { + &before_messages, + &this_message, + &after_messages + }; + + before_token = std::move(new_before_token); + after_token = std::move(new_after_token); + + fetch_messages_future.cancel(); + fetched_enough_messages_top = false; + fetched_enough_messages_bottom = false; + unresolved_reactions.clear(); + unreferenced_events.clear(); + sent_messages.clear(); + fetched_messages_set.clear(); + tabs[MESSAGES_TAB_INDEX].body->clear_items(); + matrix->clear_previous_messages_token(current_room); + + for(Messages *message_list : messages) { + for(auto &message : *message_list) { + fetched_messages_set.insert(message->event_id); + } + + all_messages.insert(all_messages.end(), message_list->begin(), message_list->end()); + auto new_body_items = messages_to_body_items(current_room, *message_list, current_room->get_user_display_name(me), me->user_id); + messages_load_cached_related_embedded_item(new_body_items, tabs[MESSAGES_TAB_INDEX].body->get_items(), me, current_room); + tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps(std::move(new_body_items)); + modify_related_messages_in_current_room(*message_list); + process_reactions(*message_list); + } + + int fetched_message_index = -1; + find_body_item_by_event_id(tabs[MESSAGES_TAB_INDEX].body->get_items(), fetched_message->event_id, &fetched_message_index); + tabs[MESSAGES_TAB_INDEX].body->set_selected_item(fetched_message_index, false); + return true; + } else { + return false; + } + } + }; + auto launch_url = [this, matrix_chat_page, &tabs, MESSAGES_TAB_INDEX, &redraw, &avatar_applied](std::string url) mutable { if(url.empty()) return; @@ -5610,7 +5698,7 @@ namespace QuickMedia { } }; - auto add_new_messages_to_current_room = [&me, &tabs, &ui_tabs, ¤t_room, MESSAGES_TAB_INDEX](Messages &messages) { + auto add_new_messages_to_current_room = [&me, &tabs, &ui_tabs, ¤t_room, MESSAGES_TAB_INDEX, &after_token](Messages &messages) { if(messages.empty()) return; @@ -5626,6 +5714,9 @@ namespace QuickMedia { scroll_to_end = true; } + if(!after_token.empty()) + scroll_to_end = false; + auto new_body_items = messages_to_body_items(current_room, messages, current_room->get_user_display_name(me), me->user_id); messages_load_cached_related_embedded_item(new_body_items, tabs[MESSAGES_TAB_INDEX].body->get_items(), me, current_room); tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps(std::move(new_body_items)); @@ -5810,21 +5901,64 @@ namespace QuickMedia { move_room = true; }; - std::function<void()> on_top_reached = [this, &previous_messages_future, &ui_tabs, &MESSAGES_TAB_INDEX, &gradient_inc, current_room] { + std::function<void()> on_top_reached = [this, &fetch_messages_future, &fetch_messages_dir, &ui_tabs, &MESSAGES_TAB_INDEX, &gradient_inc, current_room, &before_token, &after_token, &fetched_enough_messages_top] { const int selected_tab = ui_tabs.get_selected(); - if(!previous_messages_future.valid() && selected_tab == MESSAGES_TAB_INDEX) { - gradient_inc = 0; - previous_messages_future = AsyncTask<Messages>([this, current_room]() { - Messages messages; - if(matrix->get_previous_room_messages(current_room, messages) != PluginResult::OK) + if(fetched_enough_messages_top || fetch_messages_future.valid() || selected_tab != MESSAGES_TAB_INDEX) + return; + + gradient_inc = 0; + if(before_token.empty() && after_token.empty()) { + fetch_messages_dir = MessageDirection::BEFORE; + fetch_messages_future = AsyncTask<FetchMessagesResult>([this, current_room]() { + FetchMessagesResult messages; + messages.message_dir = MessageDirection::BEFORE; + messages.reached_end = false; + if(matrix->get_previous_room_messages(current_room, messages.messages, false, &messages.reached_end) != PluginResult::OK) + fprintf(stderr, "Failed to get previous matrix messages in room: %s\n", current_room->id.c_str()); + return messages; + }); + } else if(!before_token.empty()) { + fetch_messages_dir = MessageDirection::BEFORE; + fetch_messages_future = AsyncTask<FetchMessagesResult>([this, current_room, &before_token]() { + std::string token = before_token; + FetchMessagesResult messages; + messages.message_dir = MessageDirection::BEFORE; + messages.reached_end = false; + if(matrix->get_messages_in_direction(current_room, token, MessageDirection::BEFORE, messages.messages, before_token) != PluginResult::OK) fprintf(stderr, "Failed to get previous matrix messages in room: %s\n", current_room->id.c_str()); + messages.reached_end = before_token.empty(); + return messages; + }); + } + }; + + std::function<void()> on_bottom_reached = [this, &fetch_messages_future, &fetch_messages_dir, &ui_tabs, &MESSAGES_TAB_INDEX, &gradient_inc, current_room, &after_token, &fetched_enough_messages_bottom] { + const int selected_tab = ui_tabs.get_selected(); + if(!fetched_enough_messages_bottom && !after_token.empty() && !fetch_messages_future.valid() && selected_tab == MESSAGES_TAB_INDEX) { + fetch_messages_dir = MessageDirection::AFTER; + gradient_inc = 0; + fetch_messages_future = AsyncTask<FetchMessagesResult>([this, current_room, &after_token]() { + std::string token = after_token; + FetchMessagesResult messages; + messages.message_dir = MessageDirection::AFTER; + messages.reached_end = false; + if(matrix->get_messages_in_direction(current_room, token, MessageDirection::AFTER, messages.messages, after_token) != PluginResult::OK) + fprintf(stderr, "Failed to get next matrix messages in room: %s\n", current_room->id.c_str()); + messages.reached_end = after_token.empty(); return messages; }); + return; } }; for(size_t i = 0; i < tabs.size(); ++i) { tabs[i].body->on_top_reached = on_top_reached; + tabs[i].body->on_bottom_reached = on_bottom_reached; + } + + if(!matrix_chat_page->jump_to_event_id.empty()) { + if(!jump_to_message(matrix_chat_page->jump_to_event_id)) + goto chat_page_end; } while (current_page == PageType::CHAT && window.isOpen() && !move_room) { @@ -5972,11 +6106,22 @@ namespace QuickMedia { upload_file(std::string(clipboard.begin(), clipboard.end())); } - if(event.key.code == sf::Keyboard::R) { - frame_skip_text_entry = true; + if(event.key.code == sf::Keyboard::R && tabs[selected_tab].body->get_selected_shared()) { std::shared_ptr<BodyItem> selected = tabs[selected_tab].body->get_selected_shared(); - if(selected) { - if(!is_state_message_type(static_cast<Message*>(selected->userdata))) { + Message *selected_message = static_cast<Message*>(selected->userdata); + const bool go_to_replied_message = event.key.control; + if(!is_state_message_type(selected_message)) { + if(go_to_replied_message && selected_message->related_event_type == RelatedEventType::REPLY) { + int replied_to_body_item_index = -1; + auto replied_to_body_item = find_body_item_by_event_id(tabs[MESSAGES_TAB_INDEX].body->get_items(), selected_message->related_event_id, &replied_to_body_item_index); + if(replied_to_body_item) { + assert(replied_to_body_item->userdata); + Message *orig_message = get_original_message(static_cast<Message*>(replied_to_body_item->userdata)); + jump_to_message(orig_message->event_id); + } else { + jump_to_message(selected_message->related_event_id); + } + } else if(!go_to_replied_message) { if(static_cast<Message*>(selected->userdata)->event_id.empty()) { // TODO: Show inline notification show_notification("QuickMedia", "You can't reply to a message that hasn't been sent yet"); @@ -5985,14 +6130,18 @@ namespace QuickMedia { currently_operating_on_item = selected; chat_input.set_editable(true); replying_to_text.setString("Replying to:"); + frame_skip_text_entry = true; } } - } else { - // TODO: Show inline notification - show_notification("QuickMedia", "No message selected for replying"); } } + if(event.key.code == sf::Keyboard::B && event.key.control) { + // Reload room, goes to latest message l0l + move_room = true; + goto chat_page_end; + } + if(event.key.code == sf::Keyboard::E) { frame_skip_text_entry = true; std::shared_ptr<BodyItem> selected = tabs[selected_tab].body->get_selected_shared(); @@ -6086,6 +6235,15 @@ namespace QuickMedia { show_notification("QuickMedia", "No message selected for unpinning"); } } + + if(event.key.code == sf::Keyboard::R && event.key.control && tabs[selected_tab].body->get_selected_shared()) { + std::shared_ptr<BodyItem> selected = tabs[selected_tab].body->get_selected_shared(); + PinnedEventData *selected_event_data = static_cast<PinnedEventData*>(selected->userdata); + if(selected_event_data && !selected_event_data->event_id.empty()) { + ui_tabs.set_selected(MESSAGES_TAB_INDEX); + jump_to_message(selected_event_data->event_id); + } + } } } else if(event.type == sf::Event::KeyPressed && chat_state == ChatState::URL_SELECTION) { if(event.key.code == sf::Keyboard::Escape) { @@ -6189,7 +6347,7 @@ namespace QuickMedia { } case PageType::CHAT_LOGIN: { matrix_chat_page->set_current_room(nullptr, nullptr); - previous_messages_future.cancel(); + fetch_messages_future.cancel(); cleanup_tasks(); tabs.clear(); unreferenced_events.clear(); @@ -6314,14 +6472,22 @@ namespace QuickMedia { sync_data.messages.clear(); sync_data.pinned_events = std::nullopt; matrix->get_room_sync_data(current_room, sync_data); - if(!sync_data.messages.empty()) { + if(!sync_data.messages.empty() && after_token.empty()) { all_messages.insert(all_messages.end(), sync_data.messages.begin(), sync_data.messages.end()); filter_existing_messages(sync_data.messages); } filter_provisional_messages(sync_data.messages); - add_new_messages_to_current_room(sync_data.messages); - modify_related_messages_in_current_room(sync_data.messages); - process_reactions(sync_data.messages); + if(after_token.empty()) { + add_new_messages_to_current_room(sync_data.messages); + modify_related_messages_in_current_room(sync_data.messages); + process_reactions(sync_data.messages); + } else { + auto it = std::find_if(sync_data.messages.begin(), sync_data.messages.end(), [](const std::shared_ptr<Message> &message) { + return message->related_event_type == RelatedEventType::NONE || message->related_event_type == RelatedEventType::REPLY; + }); + if(it != sync_data.messages.end()) + has_unread_messages = true; + } process_pinned_events(sync_data.pinned_events); if(set_read_marker_future.ready()) { @@ -6330,20 +6496,22 @@ namespace QuickMedia { setting_read_marker = false; } - if(previous_messages_future.ready()) { - Messages new_messages = previous_messages_future.get(); - all_messages.insert(all_messages.end(), new_messages.begin(), new_messages.end()); - if(new_messages.empty()) { - fetched_enough_messages = true; - } - filter_sent_messages(new_messages); - filter_existing_messages(new_messages); - fprintf(stderr, "Finished fetching older messages, num new messages: %zu\n", new_messages.size()); - size_t num_new_messages = new_messages.size(); + if(fetch_messages_future.ready()) { + FetchMessagesResult new_messages_result = fetch_messages_future.get(); + all_messages.insert(all_messages.end(), new_messages_result.messages.begin(), new_messages_result.messages.end()); + + if(new_messages_result.message_dir == MessageDirection::BEFORE) + fetched_enough_messages_top = new_messages_result.reached_end; + if(new_messages_result.message_dir == MessageDirection::AFTER) + fetched_enough_messages_bottom = new_messages_result.reached_end; + + filter_sent_messages(new_messages_result.messages); + filter_existing_messages(new_messages_result.messages); + size_t num_new_messages = new_messages_result.messages.size(); if(num_new_messages > 0) { - add_new_messages_to_current_room(new_messages); - modify_related_messages_in_current_room(new_messages); - process_reactions(new_messages); + add_new_messages_to_current_room(new_messages_result.messages); + modify_related_messages_in_current_room(new_messages_result.messages); + process_reactions(new_messages_result.messages); // TODO: Do not loop all items, only loop the new items resolve_unreferenced_events_with_body_items(tabs[MESSAGES_TAB_INDEX].body->get_items().data(), tabs[MESSAGES_TAB_INDEX].body->get_items().size()); } @@ -6437,42 +6605,14 @@ namespace QuickMedia { sf::RectangleShape room_list_background(sf::Vector2f(this->body_size.x, window_size.y)); //room_list_background.setPosition(this->body_pos); room_list_background.setFillColor(get_current_theme().shade_color); - glEnable(GL_SCISSOR_TEST); - glScissor(0.0f, 0.0f, this->body_size.x, window_size.y); window.draw(room_list_background); window.draw(room_label); const float tab_y = std::floor(tab_vertical_offset) + room_name_padding_y; matrix_chat_page->rooms_page->body->draw(window, sf::Vector2f(0.0f, tab_y), sf::Vector2f(this->body_size.x, window_size.y - tab_y), Json::Value::nullSingleton()); - glDisable(GL_SCISSOR_TEST); } ui_tabs.draw(window, sf::Vector2f(body_pos.x, std::floor(tab_vertical_offset) + room_name_padding_y), body_size.x); - // TODO: Have one for each room. Also add bottom one? for fetching new messages (currently not implemented, is it needed?) - if(previous_messages_future.valid() && selected_tab == MESSAGES_TAB_INDEX) { - double progress = 0.5 + std::sin(std::fmod(gradient_inc, 360.0) * 0.017453292519943295 - 1.5707963267948966*0.5) * 0.5; - gradient_inc += (frame_time_ms * 0.5); - sf::Color top_color = interpolate_colors(get_current_theme().background_color, get_current_theme().loading_page_color, progress); - - gradient_points[0].position.x = chat_input_shade.getPosition().x; - gradient_points[0].position.y = tab_shade_height; - - gradient_points[1].position.x = window_size.x; - gradient_points[1].position.y = tab_shade_height; - - gradient_points[2].position.x = window_size.x; - gradient_points[2].position.y = tab_shade_height + gradient_height; - - gradient_points[3].position.x = chat_input_shade.getPosition().x; - gradient_points[3].position.y = tab_shade_height + gradient_height; - - gradient_points[0].color = top_color; - gradient_points[1].color = top_color; - gradient_points[2].color = get_current_theme().background_color; - gradient_points[3].color = get_current_theme().background_color; - window.draw(gradient_points, 4, sf::Quads); // Note: sf::Quads doesn't work with egl - } - if(chat_state == ChatState::REPLYING || chat_state == ChatState::EDITING) { const float margin = 5.0f; const float replying_to_text_height = replying_to_text.getLocalBounds().height + margin; @@ -6515,8 +6655,8 @@ namespace QuickMedia { tabs[MESSAGES_TAB_INDEX].body->draw_item(window, currently_operating_on_item, body_item_pos, body_item_size); } - if(selected_tab == MESSAGES_TAB_INDEX && current_room && current_room->body_item && !current_room->last_message_read && matrix->is_initial_sync_finished()) { - if(!tabs[selected_tab].body->is_bottom_cut_off() && is_window_focused && chat_state != ChatState::URL_SELECTION && !setting_read_marker && read_marker_timer.getElapsedTime().asMilliseconds() >= read_marker_timeout_ms) { + if(selected_tab == MESSAGES_TAB_INDEX && current_room && current_room->body_item && (!current_room->last_message_read || has_unread_messages) && matrix->is_initial_sync_finished()) { + if(after_token.empty() && !tabs[selected_tab].body->is_bottom_cut_off() && is_window_focused && chat_state != ChatState::URL_SELECTION && !setting_read_marker && read_marker_timer.getElapsedTime().asMilliseconds() >= read_marker_timeout_ms) { auto body_items = tabs[selected_tab].body->get_items(); int last_timeline_message = (int)body_items.size() - 1; for(int i = last_timeline_message - 1; i >= 0; --i) { @@ -6564,12 +6704,51 @@ namespace QuickMedia { }); } } - } else if(tabs[selected_tab].body->is_bottom_cut_off()) { + } else if(tabs[selected_tab].body->is_bottom_cut_off() || has_unread_messages) { window.draw(more_messages_below_rect); } } - if(selected_tab == MESSAGES_TAB_INDEX && current_room) { + // TODO: Have one for each room. Also add bottom one? for fetching new messages (currently not implemented, is it needed?) + if(fetch_messages_future.valid() && selected_tab == MESSAGES_TAB_INDEX) { + // TODO: fetch_messages_dir + double progress = 0.5 + std::sin(std::fmod(gradient_inc, 360.0) * 0.017453292519943295 - 1.5707963267948966*0.5) * 0.5; + gradient_inc += (frame_time_ms * 0.5); + sf::Color top_color = interpolate_colors(get_current_theme().background_color, get_current_theme().loading_page_color, progress); + + gradient_points[0].position.x = chat_input_shade.getPosition().x; + gradient_points[0].position.y = tab_shade_height; + + gradient_points[1].position.x = window_size.x; + gradient_points[1].position.y = tab_shade_height; + + gradient_points[2].position.x = window_size.x; + gradient_points[2].position.y = tab_shade_height + gradient_height; + + gradient_points[3].position.x = chat_input_shade.getPosition().x; + gradient_points[3].position.y = tab_shade_height + gradient_height; + + gradient_points[0].color = top_color; + gradient_points[1].color = top_color; + gradient_points[2].color = get_current_theme().background_color; + gradient_points[3].color = get_current_theme().background_color; + + if(fetch_messages_dir == MessageDirection::AFTER) { + gradient_points[0].position.y = more_messages_below_rect.getPosition().y; + gradient_points[1].position.y = more_messages_below_rect.getPosition().y; + gradient_points[2].position.y = more_messages_below_rect.getPosition().y + gradient_height; + gradient_points[3].position.y = more_messages_below_rect.getPosition().y + gradient_height; + + gradient_points[0].color = get_current_theme().background_color; + gradient_points[1].color = get_current_theme().background_color; + gradient_points[2].color = top_color; + gradient_points[3].color = top_color; + } + + window.draw(gradient_points, 4, sf::Quads); // Note: sf::Quads doesn't work with egl + } + + if(selected_tab == MESSAGES_TAB_INDEX) { //window.draw(chat_input_shade); chat_input.draw(window); //chat_input.draw(window, false); window.draw(logo_sprite); @@ -6579,7 +6758,7 @@ namespace QuickMedia { std::string err_msg; if(matrix->did_initial_sync_fail(err_msg)) { matrix_chat_page->set_current_room(nullptr, nullptr); - previous_messages_future.cancel(); + fetch_messages_future.cancel(); cleanup_tasks(); tabs.clear(); unreferenced_events.clear(); @@ -6598,8 +6777,12 @@ namespace QuickMedia { AsyncImageLoader::get_instance().update(); window.display(); - if(selected_tab == MESSAGES_TAB_INDEX) - fetch_more_previous_messages_if_needed(); + if(selected_tab == MESSAGES_TAB_INDEX) { + if(!tabs[selected_tab].body->is_top_cut_off()) + tabs[selected_tab].body->on_top_reached(); + if(!tabs[selected_tab].body->is_bottom_cut_off()) + tabs[selected_tab].body->on_bottom_reached(); + } if(matrix_chat_page->should_clear_data) { matrix_chat_page->should_clear_data = false; @@ -6609,7 +6792,7 @@ namespace QuickMedia { std::this_thread::sleep_for(std::chrono::milliseconds(10)); if(matrix->did_initial_sync_fail(err_msg)) { matrix_chat_page->set_current_room(nullptr, nullptr); - previous_messages_future.cancel(); + fetch_messages_future.cancel(); cleanup_tasks(); tabs.clear(); unreferenced_events.clear(); @@ -6659,7 +6842,7 @@ namespace QuickMedia { chat_page_end: matrix_chat_page->set_current_room(nullptr, nullptr); - previous_messages_future.cancel(); + fetch_messages_future.cancel(); cleanup_tasks(); window.setTitle("QuickMedia - matrix"); return move_room; diff --git a/src/Tabs.cpp b/src/Tabs.cpp index 62a6fe4..9237d3b 100644 --- a/src/Tabs.cpp +++ b/src/Tabs.cpp @@ -212,11 +212,7 @@ namespace QuickMedia { } void Tabs::set_selected(int index) { - if(tabs.empty()) { - selected_tab = 0; - } else { - selected_tab = std::min(std::max(index, 0), (int)tabs.size() - 1); - } + move_selected_tab(index); } int Tabs::get_selected() const { diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 8c7c884..df4fa52 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -182,7 +182,7 @@ namespace QuickMedia { user->avatar_url = std::move(avatar_url); } - size_t RoomData::prepend_messages_reverse(const std::vector<std::shared_ptr<Message>> &new_messages) { + size_t RoomData::prepend_messages_reverse(const Messages &new_messages) { std::lock_guard<std::recursive_mutex> lock(room_mutex); int64_t last_new_message_timestamp = last_message_timestamp; size_t num_new_messages = 0; @@ -199,7 +199,7 @@ namespace QuickMedia { return num_new_messages; } - size_t RoomData::append_messages(const std::vector<std::shared_ptr<Message>> &new_messages) { + size_t RoomData::append_messages(const Messages &new_messages) { std::lock_guard<std::recursive_mutex> lock(room_mutex); int64_t last_new_message_timestamp = last_message_timestamp; size_t num_new_messages = 0; @@ -253,7 +253,7 @@ namespace QuickMedia { room_mutex.unlock(); } - const std::vector<std::shared_ptr<Message>>& RoomData::get_messages_thread_unsafe() const { + const Messages& RoomData::get_messages_thread_unsafe() const { return messages; } @@ -268,9 +268,7 @@ namespace QuickMedia { void RoomData::set_prev_batch(const std::string &new_prev_batch) { std::lock_guard<std::recursive_mutex> lock(room_mutex); - // TODO: Check if this always works and if it also works for other homeservers than synapse - if(prev_batch.empty() || new_prev_batch < prev_batch) - prev_batch = new_prev_batch; + prev_batch = new_prev_batch; } std::string RoomData::get_prev_batch() { @@ -884,7 +882,9 @@ namespace QuickMedia { title = "Invites (0)"; } - MatrixChatPage::MatrixChatPage(Program *program, std::string room_id, MatrixRoomsPage *rooms_page) : Page(program), room_id(std::move(room_id)), rooms_page(rooms_page) { + MatrixChatPage::MatrixChatPage(Program *program, std::string room_id, MatrixRoomsPage *rooms_page, std::string jump_to_event_id) : + Page(program), room_id(std::move(room_id)), rooms_page(rooms_page), jump_to_event_id(std::move(jump_to_event_id)) + { assert(rooms_page); rooms_page->set_current_chat_page(this); rooms_page->matrix_delegate->chat_page = this; @@ -1059,7 +1059,7 @@ namespace QuickMedia { return PluginResult::OK; NotificationsExtraData *extra_data = static_cast<NotificationsExtraData*>(selected_item->extra.get()); - result_tabs.push_back(Tab{nullptr, std::make_unique<MatrixChatPage>(program, extra_data->room->id, all_rooms_page), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique<MatrixChatPage>(program, extra_data->room->id, all_rooms_page, selected_item->url), nullptr}); return PluginResult::OK; } @@ -1450,9 +1450,59 @@ namespace QuickMedia { room->release_room_lock(); } - PluginResult Matrix::get_previous_room_messages(RoomData *room, Messages &messages, bool latest_messages) { + PluginResult Matrix::get_messages_in_direction(RoomData *room, const std::string &token, MessageDirection message_dir, Messages &messages, std::string &new_token) { + // TODO: Retry on failure (after a timeout) instead of setting new token to an empty string + new_token.clear(); + + rapidjson::Document request_data(rapidjson::kObjectType); + request_data.AddMember("lazy_load_members", true, request_data.GetAllocator()); + + rapidjson::StringBuffer buffer; + rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); + request_data.Accept(writer); + + std::vector<CommandArg> additional_args = { + { "-H", "Authorization: Bearer " + access_token } + }; + + std::string filter = url_param_encode(buffer.GetString()); + + char url[512]; + snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/messages?from=%s&limit=20&dir=%s&filter=%s", homeserver.c_str(), room->id.c_str(), token.c_str(), message_dir == MessageDirection::BEFORE ? "b" : "f", 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 download_result_to_plugin_result(download_result); + + if(!json_root.IsObject()) + return PluginResult::ERR; + + const rapidjson::Value &state_json = GetMember(json_root, "state"); + events_add_user_info(state_json, room); + //events_set_room_info(state_json, room_data); + + const rapidjson::Value &chunk_json = GetMember(json_root, "chunk"); + if(chunk_json.IsArray()) { + for(const rapidjson::Value &event_item_json : chunk_json.GetArray()) { + std::shared_ptr<Message> new_message = parse_message_event(event_item_json, room); + if(new_message) + messages.push_back(std::move(new_message)); + } + } + + const rapidjson::Value &end_json = GetMember(json_root, "end"); + if(end_json.IsString()) + new_token.assign(end_json.GetString(), end_json.GetStringLength()); + + if(new_token == token) + new_token.clear(); + + return PluginResult::OK; + } + + PluginResult Matrix::get_previous_room_messages(RoomData *room, Messages &messages, bool latest_messages, bool *reached_end) { size_t num_new_messages = 0; - PluginResult result = get_previous_room_messages(room, latest_messages, num_new_messages); + PluginResult result = get_previous_room_messages(room, latest_messages, num_new_messages, reached_end); if(result != PluginResult::OK) return result; @@ -1993,7 +2043,7 @@ namespace QuickMedia { return 0; // TODO: Preallocate - std::vector<std::shared_ptr<Message>> new_messages; + Messages new_messages; auto me = get_me(room_data); std::string my_display_name = room_data->get_user_display_name(me); @@ -2839,7 +2889,7 @@ namespace QuickMedia { } } - PluginResult Matrix::get_previous_room_messages(RoomData *room_data, bool latest_messages, size_t &num_new_messages) { + PluginResult Matrix::get_previous_room_messages(RoomData *room_data, bool latest_messages, size_t &num_new_messages, bool *reached_end) { num_new_messages = 0; std::string from = room_data->get_prev_batch(); if(from.empty() || latest_messages) @@ -2869,6 +2919,7 @@ namespace QuickMedia { return PluginResult::ERR; const rapidjson::Value &state_json = GetMember(json_root, "state"); + // TODO: Remove? events_add_user_info(state_json, room_data); //events_set_room_info(state_json, room_data); @@ -2879,9 +2930,14 @@ namespace QuickMedia { if(!end_json.IsString()) { room_data->set_prev_batch("invalid"); fprintf(stderr, "Warning: matrix messages response is missing 'end', this could happen if we received the very first messages in the room\n"); + if(reached_end) + *reached_end = true; return PluginResult::OK; } + if(reached_end) + *reached_end = strcmp(end_json.GetString(), from.c_str()) == 0; + room_data->set_prev_batch(end_json.GetString()); return PluginResult::OK; } @@ -3424,30 +3480,14 @@ namespace QuickMedia { 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; -#if 0 - rapidjson::Document request_data(rapidjson::kObjectType); - request_data.AddMember("lazy_load_members", true, request_data.GetAllocator()); - - rapidjson::StringBuffer buffer; - rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); - request_data.Accept(writer); std::vector<CommandArg> additional_args = { { "-H", "Authorization: Bearer " + access_token } }; - std::string filter = url_param_encode(buffer.GetString()); - - char url[512]; - snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/context/%s?limit=0&filter=%s", homeserver.c_str(), room->id.c_str(), event_id.c_str(), filter.c_str()); -#else - std::vector<CommandArg> additional_args = { - { "-H", "Authorization: Bearer " + access_token } - }; - char url[512]; snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/event/%s", homeserver.c_str(), room->id.c_str(), event_id.c_str()); -#endif + std::string response; DownloadResult download_result = download_to_string_cache(url, response, std::move(additional_args), true, [](std::string &response) { rapidjson::Document json_root; @@ -3491,21 +3531,97 @@ namespace QuickMedia { room->fetched_messages_by_event_id.insert(std::make_pair(event_id, nullptr)); return nullptr; } -#if 0 - const rapidjson::Value &state_json = GetMember(json_root, "state"); - events_add_user_info(state_json, room); -#endif - //events_set_room_info(state_json, room); -#if 0 - const rapidjson::Value &event_json = GetMember(json_root, "event"); - std::shared_ptr<Message> new_message = parse_message_event(event_json, room); -#else + + // TODO: Do this? what about state apply order? + //const rapidjson::Value &state_json = GetMember(json_root, "state"); + //events_add_user_info(state_json, room); + std::shared_ptr<Message> new_message = parse_message_event(json_root, room); -#endif room->fetched_messages_by_event_id.insert(std::make_pair(event_id, new_message)); return new_message; } + PluginResult Matrix::get_message_context(RoomData *room, const std::string &event_id, std::shared_ptr<Message> &message, Messages &before_messages, Messages &after_messages, std::string &before_token, std::string &after_token) { + rapidjson::Document request_data(rapidjson::kObjectType); + request_data.AddMember("lazy_load_members", true, request_data.GetAllocator()); + + rapidjson::StringBuffer buffer; + rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); + request_data.Accept(writer); + + std::vector<CommandArg> additional_args = { + { "-H", "Authorization: Bearer " + access_token } + }; + + std::string filter = url_param_encode(buffer.GetString()); + + char url[512]; + snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/context/%s?limit=20&filter=%s", homeserver.c_str(), room->id.c_str(), event_id.c_str(), filter.c_str()); + + rapidjson::Document json_root; + std::string err_msg; + DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true, &err_msg); + if(download_result != DownloadResult::OK) { + show_notification("QuickMedia", "Failed to get message", Urgency::CRITICAL); + return download_result_to_plugin_result(download_result); + } + + if(!json_root.IsObject()) { + show_notification("QuickMedia", "Failed to parse server response", Urgency::CRITICAL); + return PluginResult::ERR; + } + + const rapidjson::Value &errcode_json = GetMember(json_root, "errcode"); + if(errcode_json.IsString() && strcmp(errcode_json.GetString(), "M_NOT_FOUND") != 0) { + const rapidjson::Value &error_json = GetMember(json_root, "error"); + if(error_json.IsString()) { + show_notification("QuickMedia", "Failed to get message, error: " + std::string(error_json.GetString(), error_json.GetStringLength()), Urgency::CRITICAL); + return PluginResult::ERR; + } + } + + // TODO: Do this? what about state apply order? + //const rapidjson::Value &state_json = GetMember(json_root, "state"); + //events_add_user_info(state_json, room); + + const rapidjson::Value &start_json = GetMember(json_root, "start"); + const rapidjson::Value &end_json = GetMember(json_root, "end"); + + const rapidjson::Value &event_json = GetMember(json_root, "event"); + const rapidjson::Value &events_before_json = GetMember(json_root, "events_before"); + const rapidjson::Value &events_after_json = GetMember(json_root, "events_after"); + + if(start_json.IsString()) + before_token.assign(start_json.GetString(), start_json.GetStringLength()); + + if(end_json.IsString()) + after_token.assign(end_json.GetString(), end_json.GetStringLength()); + + message = parse_message_event(event_json, room); + + if(events_before_json.IsArray()) { + for(const rapidjson::Value &event_item_json : events_before_json.GetArray()) { + std::shared_ptr<Message> new_message = parse_message_event(event_item_json, room); + if(new_message) + before_messages.push_back(std::move(new_message)); + } + } + + if(events_after_json.IsArray()) { + for(const rapidjson::Value &event_item_json : events_after_json.GetArray()) { + std::shared_ptr<Message> new_message = parse_message_event(event_item_json, room); + if(new_message) + after_messages.push_back(std::move(new_message)); + } + } + + return PluginResult::OK; + } + + void Matrix::clear_previous_messages_token(RoomData *room) { + room->set_prev_batch(""); + } + static const char* file_get_filename(const std::string &filepath) { size_t index = filepath.rfind('/'); if(index == std::string::npos) |