diff options
-rw-r--r-- | TODO | 5 | ||||
-rw-r--r-- | include/Body.hpp | 6 | ||||
-rw-r--r-- | include/SearchBar.hpp | 1 | ||||
-rw-r--r-- | plugins/Matrix.hpp | 48 | ||||
-rw-r--r-- | plugins/Page.hpp | 2 | ||||
-rw-r--r-- | src/Body.cpp | 51 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 128 | ||||
-rw-r--r-- | src/SearchBar.cpp | 4 | ||||
-rw-r--r-- | src/plugins/Matrix.cpp | 212 |
9 files changed, 363 insertions, 94 deletions
@@ -127,4 +127,7 @@ Set curl download limits everywhere (when saving to file, downloading to json, e In the downloader if we already have the url in thumbnail/video cache, then copy it to the destination instead of redownloading it. This would also fix downloading images when viewing a manga page. Update timestamp of messages posted with matrix (and move them to the correct place in the timeline) when we receive the message from the server. This is needed when the localtime is messed up (for example when rebooting from windows into linux). Remove dependency on imagemagick and create a function that forks the processes and creates a thumbnail from an input filepath and output filepath. That new process can use sf::Image to load and save the images. -Youtube now requires signing in to view age restricted content. For such videos quickmedia should fallback to invidious.
\ No newline at end of file +Youtube now requires signing in to view age restricted content. For such videos quickmedia should fallback to invidious. +Notification race condition when fetching the first notifications page and receiving a notification immediately after the first sync? we might end up with a duplicate notification. +Submit on notifications item in matrix should jump to the message in the room. +Notifications should load their replied-to-message.
\ No newline at end of file diff --git a/include/Body.hpp b/include/Body.hpp index 0b0fca4..e74d459 100644 --- a/include/Body.hpp +++ b/include/Body.hpp @@ -198,9 +198,9 @@ namespace QuickMedia { int get_index_by_body_item(BodyItem *body_item); void select_first_item(); - void select_last_item(); + void select_last_item(bool reset_prev_select = false); void clear_items(); - void prepend_items(BodyItems new_items); + void prepend_items_reverse(BodyItems new_items); void append_items(BodyItems new_items); void insert_item_by_timestamp(std::shared_ptr<BodyItem> body_item); void insert_items_by_timestamps(BodyItems new_items); @@ -240,6 +240,8 @@ namespace QuickMedia { void set_page_scroll(float scroll); float get_page_scroll() const { return page_scroll; } + // This is the item we can see the start of + bool is_first_item_fully_visible() const { return first_item_fully_visible; } // This is the item we can see the end of bool is_last_item_fully_visible() const { return last_item_fully_visible; } bool is_body_full_with_items() const { return items_cut_off; } diff --git a/include/SearchBar.hpp b/include/SearchBar.hpp index 4fc19e9..027f534 100644 --- a/include/SearchBar.hpp +++ b/include/SearchBar.hpp @@ -40,6 +40,7 @@ namespace QuickMedia { float getBottomWithoutShadow() const; std::string get_text() const; + bool is_empty() const; TextUpdateCallback onTextUpdateCallback; TextSubmitCallback onTextSubmitCallback; diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index bff89b2..bcb4058 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -227,6 +227,15 @@ namespace QuickMedia { BANNED }; + struct MatrixNotification { + RoomData *room; + std::string event_id; + std::string sender_user_id; + std::string body; // Without reply formatting + time_t timestamp; // The timestamp in milliseconds or 0 + bool read; + }; + // All of methods in this class are called in the main (ui) thread class MatrixDelegate { public: @@ -244,7 +253,7 @@ namespace QuickMedia { virtual void add_invite(const std::string &room_id, const Invite &invite) = 0; virtual void remove_invite(const std::string &room_id) = 0; - virtual void add_unread_notification(RoomData *room, std::string event_id, std::string sender, std::string body) = 0; + virtual void add_unread_notification(MatrixNotification notification) = 0; virtual void add_user(MatrixEventUserInfo user_info) = 0; virtual void remove_user(MatrixEventUserInfo user_info) = 0; @@ -258,12 +267,13 @@ namespace QuickMedia { class MatrixRoomTagsPage; class MatrixInvitesPage; class MatrixChatPage; + class MatrixNotificationsPage; using UsersByRoom = std::unordered_multimap<RoomData*, MatrixEventUserInfo>; class MatrixQuickMedia : public MatrixDelegate { public: - MatrixQuickMedia(Program *program, Matrix *matrix, MatrixRoomsPage *rooms_page, MatrixRoomTagsPage *room_tags_page, MatrixInvitesPage *invites_page); + MatrixQuickMedia(Program *program, Matrix *matrix, MatrixRoomsPage *rooms_page, MatrixRoomTagsPage *room_tags_page, MatrixInvitesPage *invites_page, MatrixNotificationsPage *notifications_page); void join_room(RoomData *room) override; void leave_room(RoomData *room, LeaveType leave_type, const std::string &reason) override; @@ -274,13 +284,15 @@ namespace QuickMedia { void add_invite(const std::string &room_id, const Invite &invite) override; void remove_invite(const std::string &room_id) override; - void add_unread_notification(RoomData *room, std::string event_id, std::string sender, std::string body) override; + void add_unread_notification(MatrixNotification notification) override; void add_user(MatrixEventUserInfo user_info) override; void remove_user(MatrixEventUserInfo user_info) override; void set_user_info(MatrixEventUserInfo user_info) override; void for_each_user_in_room(RoomData *room, std::function<void(const MatrixEventUserInfo&)> callback); + void set_room_as_read(RoomData *room); + void clear_data() override; Program *program; @@ -289,6 +301,7 @@ namespace QuickMedia { MatrixRoomsPage *rooms_page; MatrixRoomTagsPage *room_tags_page; MatrixInvitesPage *invites_page; + MatrixNotificationsPage *notifications_page; private: void update_room_description(RoomData *room, const Messages &new_messages, bool is_initial_sync, bool sync_is_cache); private: @@ -315,6 +328,8 @@ namespace QuickMedia { void set_current_chat_page(MatrixChatPage *chat_page); + void set_room_as_read(RoomData *room); + void clear_data(); MatrixQuickMedia *matrix_delegate = nullptr; @@ -419,6 +434,8 @@ namespace QuickMedia { void set_current_room(RoomData *room, Body *users_body); size_t get_num_users_in_current_room() const; + void set_room_as_read(RoomData *room); + const std::string room_id; MatrixRoomsPage *rooms_page = nullptr; bool should_clear_data = false; @@ -458,14 +475,21 @@ namespace QuickMedia { int current_page; }; - class MatrixNotificationsPage : public Page { + class MatrixNotificationsPage : public LazyFetchPage { public: - MatrixNotificationsPage(Program *program, Body *notifications_body) : Page(program), notifications_body(notifications_body) {} - const char* get_title() const override { return "Notifications (0)"; } + MatrixNotificationsPage(Program *program, Matrix *matrix, Body *notifications_body); + const char* get_title() const override { return "Notifications"; } PluginResult submit(const std::string&, const std::string&, std::vector<Tab>&) override { return PluginResult::OK; } + PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override; + PluginResult lazy_fetch(BodyItems &result_items) override; + bool is_ready() override; + + void add_notification(MatrixNotification notification); + void set_room_as_read(RoomData *room); private: + Matrix *matrix; Body *notifications_body; }; @@ -477,11 +501,14 @@ namespace QuickMedia { bool is_initial_sync_finished() const; // Returns true if initial sync failed, and |err_msg| is set to the error reason in that case bool did_initial_sync_fail(std::string &err_msg); + bool has_finished_fetching_notifications() const; void get_room_sync_data(RoomData *room, SyncData &sync_data); 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_previous_notifications(std::function<void(const MatrixNotification&)> callback_func); + void get_cached_notifications(std::function<void(const MatrixNotification&)> callback_func); // |url| should only be set when uploading media. // TODO: Make api better. @@ -546,7 +573,7 @@ namespace QuickMedia { PluginResult set_qm_last_read_message_timestamp(RoomData *room, int64_t timestamp); PluginResult parse_sync_response(const rapidjson::Document &root, bool is_additional_messages_sync, bool initial_sync); - PluginResult parse_notifications(const rapidjson::Value ¬ifications_json); + 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); @@ -571,6 +598,8 @@ namespace QuickMedia { bool remove_invite(const std::string &room_id); void set_next_batch(std::string new_next_batch); std::string get_next_batch(); + void set_next_notifications_token(std::string new_next_token); + std::string get_next_notifications_token(); void clear_sync_cache_for_new_sync(); std::shared_ptr<UserInfo> get_user_by_id(RoomData *room, const std::string &user_id, bool *is_new_user = nullptr, bool create_if_not_found = true); std::string get_filter_cached(); @@ -585,11 +614,15 @@ namespace QuickMedia { std::string homeserver_domain; std::optional<int> upload_limit; std::string next_batch; + std::string next_notifications_token; std::mutex next_batch_mutex; std::unordered_map<std::string, Invite> invites; std::mutex invite_mutex; + std::vector<MatrixNotification> notifications; + std::mutex notifications_mutex; + std::thread sync_thread; std::thread sync_additional_messages_thread; std::thread notification_thread; @@ -597,6 +630,7 @@ namespace QuickMedia { bool sync_running = false; bool sync_failed = false; bool sync_is_cache = false; + bool finished_fetching_notifications = false; std::string sync_fail_reason; MatrixDelegate *delegate = nullptr; std::optional<std::string> filter_cached; diff --git a/plugins/Page.hpp b/plugins/Page.hpp index be6eb76..db27fae 100644 --- a/plugins/Page.hpp +++ b/plugins/Page.hpp @@ -51,6 +51,8 @@ namespace QuickMedia { virtual bool is_lazy_fetch_page() const { return false; } // Note: If submit is done without any selection, then the search term is sent as the |title|, not |url|. Submit will only be sent if the input text is not empty or if an item is selected virtual bool allow_submit_no_selection() const { return false; } + // This is used to delay loading of the page. For example if the page relies on an external factor to start loading + virtual bool is_ready() { return true; } // This is called both when first navigating to page and when going back to page virtual void on_navigate_to_page(Body *body) { (void)body; } diff --git a/src/Body.cpp b/src/Body.cpp index 1b1d35a..5a23694 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -288,11 +288,12 @@ namespace QuickMedia { //item_background.set_position(sf::Vector2f(body_pos.x, item_background_target_pos_y)); } - void Body::select_last_item() { + void Body::select_last_item(bool reset_prev_select) { int new_selected_item = std::max(0, (int)items.size() - 1); selected_scrolled = 0.0f; selected_item = new_selected_item; - //prev_selected_item = selected_item; + if(reset_prev_select) + prev_selected_item = selected_item; //page_scroll = 0.0f; clamp_selection(); clamp_selected_item_to_body_count = 1; @@ -312,8 +313,8 @@ namespace QuickMedia { //item_background.set_position(sf::Vector2f(body_pos.x, item_background_target_pos_y)); } - void Body::prepend_items(BodyItems new_items) { - items.insert(items.begin(), std::make_move_iterator(new_items.begin()), std::make_move_iterator(new_items.end())); + void Body::prepend_items_reverse(BodyItems new_items) { + items.insert(items.begin(), std::make_move_iterator(new_items.rbegin()), std::make_move_iterator(new_items.rend())); items_set_dirty(); } @@ -1006,30 +1007,32 @@ namespace QuickMedia { if(body_item->dirty_timestamp) { body_item->dirty_timestamp = false; - //time_t time_now = time(NULL); - //struct tm *now_tm = localtime(&time_now); + if(body_item->get_timestamp() != 0) { + //time_t time_now = time(NULL); + //struct tm *now_tm = localtime(&time_now); - time_t message_timestamp = body_item->get_timestamp() / 1000; - struct tm message_tm; - localtime_r(&message_timestamp, &message_tm); + time_t message_timestamp = body_item->get_timestamp() / 1000; + struct tm message_tm; + localtime_r(&message_timestamp, &message_tm); - //bool is_same_year = message_tm->tm_year == now_tm->tm_year; - - char time_str[128] = {0}; - /* - if(is_same_year) - strftime(time_str, sizeof(time_str) - 1, "%a %b %d %H:%M:%S", message_tm); - else - strftime(time_str, sizeof(time_str) - 1, "%a %b %d %H:%M:%S %Y", message_tm); - */ - strftime(time_str, sizeof(time_str) - 1, "%a %b %d %H:%M", &message_tm); + //bool is_same_year = message_tm->tm_year == now_tm->tm_year; + + char time_str[128] = {0}; + /* + if(is_same_year) + strftime(time_str, sizeof(time_str) - 1, "%a %b %d %H:%M:%S", message_tm); + else + strftime(time_str, sizeof(time_str) - 1, "%a %b %d %H:%M:%S %Y", message_tm); + */ + strftime(time_str, sizeof(time_str) - 1, "%a %b %d %H:%M", &message_tm); - if(body_item->timestamp_text) - body_item->timestamp_text->setString(time_str); - else - body_item->timestamp_text = std::make_unique<sf::Text>(time_str, *FontLoader::get_font(FontLoader::FontType::LATIN), std::floor(10 * get_ui_scale())); + if(body_item->timestamp_text) + body_item->timestamp_text->setString(time_str); + else + body_item->timestamp_text = std::make_unique<sf::Text>(time_str, *FontLoader::get_font(FontLoader::FontType::LATIN), std::floor(10 * get_ui_scale())); - body_item->timestamp_text->setFillColor(sf::Color(185, 190, 198, 100)); + body_item->timestamp_text->setFillColor(sf::Color(185, 190, 198, 100)); + } } } diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index a9956d3..13361c6 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -1482,6 +1482,15 @@ namespace QuickMedia { window.draw(tab_associated_data.search_result_text); } + if(!tabs[selected_tab].page->is_ready()) { + sf::Text loading_text("Loading...", *FontLoader::get_font(FontLoader::FontType::LATIN), std::floor(30 * get_ui_scale())); + auto text_bounds = loading_text.getLocalBounds(); + loading_text.setPosition( + std::floor(body_pos.x + body_size.x * 0.5f - text_bounds.width * 0.5f), + std::floor(body_pos.y + body_size.y * 0.5f - text_bounds.height * 0.5f)); + window.draw(loading_text); + } + if(matrix) matrix->update(); @@ -1518,6 +1527,8 @@ namespace QuickMedia { bool redraw = true; for(Tab &tab : tabs) { + if(tab.body->attach_side == AttachSide::BOTTOM) + tab.body->select_last_item(); tab.body->thumbnail_max_size = tab.page->get_thumbnail_max_size(); tab.page->on_navigate_to_page(tab.body.get()); } @@ -1591,9 +1602,9 @@ namespace QuickMedia { tabs[selected_tab].search_bar->clear(); tabs[selected_tab].search_bar->onTextUpdateCallback(""); } else { - int selected_item_index = tabs[selected_tab].body->get_selected_item(); - tabs[selected_tab].body->select_first_item(); - tabs[selected_tab].body->set_selected_item(selected_item_index, false); + //int selected_item_index = tabs[selected_tab].body->get_selected_item(); + //tabs[selected_tab].body->select_first_item(); + //tabs[selected_tab].body->set_selected_item(selected_item_index, false); } } @@ -1716,13 +1727,14 @@ namespace QuickMedia { hide_virtual_keyboard(); }; - std::function<void()> on_bottom_reached = [&ui_tabs, &tabs, &tab_associated_data, &gradient_inc] { + std::function<void()> on_reached_end = [&ui_tabs, &tabs, &tab_associated_data, &gradient_inc] { const int selected_tab = ui_tabs.get_selected(); if(tab_associated_data[selected_tab].fetch_status == FetchStatus::NONE && !tab_associated_data[selected_tab].fetching_next_page_running && !tab_associated_data[selected_tab].fetching_next_page_failed - && !tabs[selected_tab].body->items.empty() - && tabs[selected_tab].page + && (!tabs[selected_tab].search_bar || tabs[selected_tab].search_bar->is_empty()) + && tabs[selected_tab].body->get_num_visible_items() > 0 + && tabs[selected_tab].page->is_ready() && (!tabs[selected_tab].page->is_lazy_fetch_page() || tab_associated_data[selected_tab].lazy_fetch_finished)) { gradient_inc = 0.0; @@ -1746,7 +1758,10 @@ namespace QuickMedia { submit_handler(body_item->get_title()); }; - tab.body->on_bottom_reached = on_bottom_reached; + if(tab.body->attach_side == AttachSide::TOP) + tab.body->on_bottom_reached = on_reached_end; + else if(tab.body->attach_side == AttachSide::BOTTOM) + tab.body->on_top_reached = on_reached_end; TabAssociatedData &associated_data = tab_associated_data[i]; if(tab.search_bar) { @@ -1762,7 +1777,10 @@ namespace QuickMedia { associated_data.search_text_updated = true; } else { tabs[i].body->filter_search_fuzzy(text); - tabs[i].body->select_first_item(); + if(tabs[i].body->attach_side == AttachSide::TOP) + tabs[i].body->select_first_item(); + else if(tabs[i].body->attach_side == AttachSide::BOTTOM) + tabs[i].body->select_last_item(); } associated_data.typing = false; }; @@ -1816,7 +1834,7 @@ namespace QuickMedia { } } else if(event.key.code == sf::Keyboard::T && event.key.control) { BodyItem *selected_item = tabs[selected_tab].body->get_selected(); - if(selected_item && tabs[selected_tab].page && tabs[selected_tab].page->is_trackable()) { + if(selected_item && tabs[selected_tab].page->is_trackable()) { TrackablePage *trackable_page = dynamic_cast<TrackablePage*>(tabs[selected_tab].page.get()); run_task_with_loading_screen([trackable_page, selected_item](){ return trackable_page->track(selected_item->get_title()) == TrackResult::OK; @@ -1857,17 +1875,31 @@ namespace QuickMedia { // TODO: Dont show tabs if there is only one tab get_body_dimensions(window_size, tabs[selected_tab].search_bar.get(), body_pos, body_size, true); - gradient_points[0].position.x = 0.0f; - gradient_points[0].position.y = window_size.y - gradient_height; + if(tabs[selected_tab].body->attach_side == AttachSide::TOP) { + gradient_points[0].position.x = 0.0f; + gradient_points[0].position.y = window_size.y - gradient_height; - gradient_points[1].position.x = window_size.x; - gradient_points[1].position.y = window_size.y - gradient_height; + gradient_points[1].position.x = window_size.x; + gradient_points[1].position.y = window_size.y - gradient_height; - gradient_points[2].position.x = window_size.x; - gradient_points[2].position.y = window_size.y; + gradient_points[2].position.x = window_size.x; + gradient_points[2].position.y = window_size.y; + + gradient_points[3].position.x = 0.0f; + gradient_points[3].position.y = window_size.y; + } else if(tabs[selected_tab].body->attach_side == AttachSide::BOTTOM) { + gradient_points[0].position.x = 0.0f; + gradient_points[0].position.y = body_pos.y; - gradient_points[3].position.x = 0.0f; - gradient_points[3].position.y = window_size.y; + gradient_points[1].position.x = window_size.x; + gradient_points[1].position.y = body_pos.y; + + gradient_points[2].position.x = window_size.x; + gradient_points[2].position.y = body_pos.y + gradient_height; + + gradient_points[3].position.x = 0.0f; + gradient_points[3].position.y = body_pos.y + gradient_height; + } } if(tab_associated_data[selected_tab].fetching_next_page_running) { @@ -1875,15 +1907,22 @@ namespace QuickMedia { gradient_inc += (frame_time_ms * 0.5); sf::Color bottom_color = interpolate_colors(back_color, sf::Color(175, 180, 188), progress); - gradient_points[0].color = back_color; - gradient_points[1].color = back_color; - gradient_points[2].color = bottom_color; - gradient_points[3].color = bottom_color; + if(tabs[selected_tab].body->attach_side == AttachSide::TOP) { + gradient_points[0].color = back_color; + gradient_points[1].color = back_color; + gradient_points[2].color = bottom_color; + gradient_points[3].color = bottom_color; + } else if(tabs[selected_tab].body->attach_side == AttachSide::BOTTOM) { + gradient_points[0].color = bottom_color; + gradient_points[1].color = bottom_color; + gradient_points[2].color = back_color; + gradient_points[3].color = back_color; + } } if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->update(); - if(tabs[selected_tab].page->is_lazy_fetch_page() && tab_associated_data[selected_tab].fetch_status == FetchStatus::NONE && !tab_associated_data[selected_tab].lazy_fetch_finished) { + if(tabs[selected_tab].page->is_ready() && tabs[selected_tab].page->is_lazy_fetch_page() && tab_associated_data[selected_tab].fetch_status == FetchStatus::NONE && !tab_associated_data[selected_tab].lazy_fetch_finished) { tab_associated_data[selected_tab].fetch_status = FetchStatus::LOADING; tab_associated_data[selected_tab].fetch_type = FetchType::LAZY; tab_associated_data[selected_tab].search_result_text.setString("Loading..."); @@ -1897,18 +1936,33 @@ namespace QuickMedia { for(size_t i = 0; i < tabs.size(); ++i) { TabAssociatedData &associated_data = tab_associated_data[i]; + if(!tabs[i].page->is_ready()) + continue; if(associated_data.fetching_next_page_running && associated_data.next_page_future.ready()) { + const bool body_was_empty = tabs[i].body->items.empty(); BodyItems new_body_items = associated_data.next_page_future.get(); fprintf(stderr, "Finished fetching page %d, num new items: %zu\n", associated_data.fetched_page + 1, new_body_items.size()); + int prev_selected_item = tabs[i].body->get_selected_item(); size_t num_new_messages = new_body_items.size(); if(num_new_messages > 0) { - tabs[i].body->append_items(std::move(new_body_items)); + if(tabs[i].body->attach_side == AttachSide::TOP) + tabs[i].body->append_items(std::move(new_body_items)); + else if(tabs[i].body->attach_side == AttachSide::BOTTOM) + tabs[i].body->prepend_items_reverse(std::move(new_body_items)); associated_data.fetched_page++; } else { associated_data.fetching_next_page_failed = true; } associated_data.fetching_next_page_running = false; + + if(tabs[i].body->attach_side == AttachSide::BOTTOM) { + if(body_was_empty) { + tabs[i].body->select_last_item(); + } else { + tabs[i].body->set_selected_item(prev_selected_item + num_new_messages, true); + } + } } if(associated_data.search_text_updated && associated_data.fetch_status == FetchStatus::NONE && !associated_data.fetching_next_page_running) { @@ -1929,7 +1983,12 @@ namespace QuickMedia { if(!associated_data.search_text_updated) { FetchResult fetch_result = associated_data.fetch_future.get(); tabs[i].body->items = std::move(fetch_result.body_items); - tabs[i].body->select_first_item(); + if(tabs[i].body->attach_side == AttachSide::TOP) { + tabs[i].body->select_first_item(); + } else if(tabs[i].body->attach_side == AttachSide::BOTTOM) { + std::reverse(tabs[i].body->items.begin(), tabs[i].body->items.end()); + tabs[i].body->select_last_item(); + } associated_data.fetched_page = 0; associated_data.fetching_next_page_failed = false; if(fetch_result.result != PluginResult::OK) @@ -1949,6 +2008,10 @@ namespace QuickMedia { FetchResult fetch_result = associated_data.fetch_future.get(); tabs[i].body->items = std::move(fetch_result.body_items); if(tabs[i].search_bar) tabs[i].body->filter_search_fuzzy(tabs[i].search_bar->get_text()); + if(tabs[i].body->attach_side == AttachSide::BOTTOM) { + std::reverse(tabs[i].body->items.begin(), tabs[i].body->items.end()); + tabs[i].body->select_last_item(); + } LazyFetchPage *lazy_fetch_page = static_cast<LazyFetchPage*>(tabs[i].page.get()); if(fetch_result.result != PluginResult::OK) associated_data.search_result_text.setString("Failed to fetch page!"); @@ -1984,8 +2047,12 @@ namespace QuickMedia { AsyncImageLoader::get_instance().update(); window.display(); - if(!tabs[selected_tab].body->items.empty() && tabs[selected_tab].body->is_last_item_fully_visible()) - on_bottom_reached(); + if(!tabs[selected_tab].body->items.empty()) { + if(tabs[selected_tab].body->attach_side == AttachSide::TOP && tabs[selected_tab].body->is_last_item_fully_visible()) + on_reached_end(); + else if(tabs[selected_tab].body->attach_side == AttachSide::BOTTOM && tabs[selected_tab].body->is_first_item_fully_visible()) + on_reached_end(); + } if(go_to_previous_page) { go_to_previous_page = false; @@ -3373,7 +3440,7 @@ namespace QuickMedia { //thread_body->clamp_selection(); //thread_body->set_page_scroll(0.0f); int prev_sel = thread_body->get_selected_item(); - thread_body->select_last_item(); + thread_body->select_first_item(); thread_body->set_selected_item(prev_sel, false); } else if(event.key.code == sf::Keyboard::BackSpace && !comment_navigation_stack.empty()) { size_t previous_selected = comment_navigation_stack.top(); @@ -5692,6 +5759,8 @@ namespace QuickMedia { // TODO: Maybe set this instead when the mention is visible on the screen? current_room->unread_notification_count = 0; + matrix_chat_page->set_room_as_read(current_room); + Message *read_message = last_visible_timeline_message; if(read_message->replaced_by) read_message = read_message->replaced_by.get(); @@ -5825,7 +5894,8 @@ namespace QuickMedia { exit(exit_code); auto notifications_body = create_body(); - auto matrix_notifications_page = std::make_unique<MatrixNotificationsPage>(this, notifications_body.get()); + //notifications_body->attach_side = AttachSide::BOTTOM; + auto matrix_notifications_page = std::make_unique<MatrixNotificationsPage>(this, matrix, notifications_body.get()); auto rooms_tags_body = create_body(); auto matrix_rooms_tag_page = std::make_unique<MatrixRoomTagsPage>(this, rooms_tags_body.get()); @@ -5854,7 +5924,7 @@ namespace QuickMedia { add_body_item_unique_title(room_directory_body->items, "jupiterbroadcasting.com"); auto matrix_room_directory_page = std::make_unique<MatrixRoomDirectoryPage>(this, matrix); - MatrixQuickMedia matrix_handler(this, matrix, matrix_rooms_page.get(), matrix_rooms_tag_page.get(), matrix_invites_page.get()); + MatrixQuickMedia matrix_handler(this, matrix, matrix_rooms_page.get(), matrix_rooms_tag_page.get(), matrix_invites_page.get(), matrix_notifications_page.get()); bool sync_cached = false; if(!matrix->start_sync(&matrix_handler, sync_cached)) { show_notification("QuickMedia", "Failed to start sync", Urgency::CRITICAL); diff --git a/src/SearchBar.cpp b/src/SearchBar.cpp index a7e3890..c0a8aad 100644 --- a/src/SearchBar.cpp +++ b/src/SearchBar.cpp @@ -358,4 +358,8 @@ namespace QuickMedia { return ""; return text.getString(); } + + bool SearchBar::is_empty() const { + return show_placeholder; + } }
\ No newline at end of file diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index e015ada..7992772 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -348,8 +348,8 @@ namespace QuickMedia { //body_item.reset(); } - MatrixQuickMedia::MatrixQuickMedia(Program *program, Matrix *matrix, MatrixRoomsPage *rooms_page, MatrixRoomTagsPage *room_tags_page, MatrixInvitesPage *invites_page) : - program(program), matrix(matrix), chat_page(nullptr), rooms_page(rooms_page), room_tags_page(room_tags_page), invites_page(invites_page) + MatrixQuickMedia::MatrixQuickMedia(Program *program, Matrix *matrix, MatrixRoomsPage *rooms_page, MatrixRoomTagsPage *room_tags_page, MatrixInvitesPage *invites_page, MatrixNotificationsPage *notifications_page) : + program(program), matrix(matrix), chat_page(nullptr), rooms_page(rooms_page), room_tags_page(room_tags_page), invites_page(invites_page), notifications_page(notifications_page) { rooms_page->matrix_delegate = this; room_tags_page->matrix_delegate = this; @@ -405,7 +405,17 @@ namespace QuickMedia { if(message->notification_mentions_me) { // TODO: What if the message or username begins with "-"? also make the notification image be the avatar of the user if((!is_window_focused || room != current_room) && message->related_event_type != RelatedEventType::EDIT && message->related_event_type != RelatedEventType::REDACTION) { - show_notification("QuickMedia matrix - " + extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(message.get()), AUTHOR_MAX_LENGTH) + " (" + room->get_name() + ")", message->body); + std::string body = remove_reply_formatting(message->body); + show_notification("QuickMedia matrix - " + extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(message.get()), AUTHOR_MAX_LENGTH) + " (" + room->get_name() + ")", body); + + MatrixNotification notification; + notification.room = room; + notification.event_id = message->event_id; + notification.sender_user_id = message->user->user_id; + notification.body = std::move(body); + notification.timestamp = message->timestamp; + notification.read = false; + notifications_page->add_notification(std::move(notification)); } } } @@ -433,8 +443,8 @@ namespace QuickMedia { invites_page->remove_body_item_by_room_id(room_id); } - void MatrixQuickMedia::add_unread_notification(RoomData *room, std::string, std::string sender, std::string body) { - show_notification("QuickMedia matrix - " + sender + " (" + room->get_name() + ")", body); + void MatrixQuickMedia::add_unread_notification(MatrixNotification notification) { + show_notification("QuickMedia matrix - " + notification.sender_user_id + " (" + notification.room->get_name() + ")", notification.body); } static UsersByRoom::iterator find_user_data_by_id(UsersByRoom &users_by_room, const MatrixEventUserInfo &user_info) { @@ -481,6 +491,10 @@ namespace QuickMedia { } } + void MatrixQuickMedia::set_room_as_read(RoomData *room) { + notifications_page->set_room_as_read(room); + } + static void sort_room_body_items(std::vector<std::shared_ptr<BodyItem>> &room_body_items) { std::sort(room_body_items.begin(), room_body_items.end(), [](const std::shared_ptr<BodyItem> &body_item1, const std::shared_ptr<BodyItem> &body_item2) { RoomData *room1 = static_cast<RoomData*>(body_item1->userdata); @@ -695,6 +709,10 @@ namespace QuickMedia { current_chat_page = chat_page; } + void MatrixRoomsPage::set_room_as_read(RoomData *room) { + matrix_delegate->set_room_as_read(room); + } + void MatrixRoomsPage::clear_data() { body->clear_items(); if(current_chat_page) @@ -966,6 +984,10 @@ namespace QuickMedia { return users_body ? users_body->items.size() : 0; } + void MatrixChatPage::set_room_as_read(RoomData *room) { + rooms_page->set_room_as_read(room); + } + PluginResult MatrixRoomDirectoryPage::submit(const std::string &title, const std::string&, std::vector<Tab> &result_tabs) { std::string server_name = title; @@ -1011,6 +1033,77 @@ namespace QuickMedia { return PluginResult::OK; } + class NotificationsExtraData : public BodyItemExtra { + public: + RoomData *room; + bool read; + }; + + static std::shared_ptr<BodyItem> notification_to_body_item(Body *body, const MatrixNotification ¬ification) { + auto body_item = BodyItem::create(""); + body_item->set_author(notification.room->get_name()); + body_item->set_description(notification.sender_user_id + ":\n" + notification.body); + body_item->set_timestamp(notification.timestamp); + body_item->url = notification.event_id; + + if(!notification.read) { + body_item->set_author_color(sf::Color(255, 100, 100)); + body_item->set_description_color(sf::Color(255, 100, 100)); + } + + body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; + body_item->thumbnail_size = sf::Vector2i(32, 32); + body_item->thumbnail_url = notification.room->get_avatar_url(); + + auto extra_data = std::make_shared<NotificationsExtraData>(); + extra_data->room = notification.room; + extra_data->read = notification.read; + body_item->extra = std::move(extra_data); + + body->apply_search_filter_for_item(body_item.get()); + return body_item; + } + + MatrixNotificationsPage::MatrixNotificationsPage(Program *program, Matrix *matrix, Body *notifications_body) : + LazyFetchPage(program), matrix(matrix), notifications_body(notifications_body) {} + + PluginResult MatrixNotificationsPage::get_page(const std::string&, int, BodyItems &result_items) { + return matrix->get_previous_notifications([this, &result_items](const MatrixNotification ¬ification) { + result_items.push_back(notification_to_body_item(notifications_body, notification)); + }); + } + + PluginResult MatrixNotificationsPage::lazy_fetch(BodyItems &result_items) { + matrix->get_cached_notifications([this, &result_items](const MatrixNotification ¬ification) { + result_items.push_back(notification_to_body_item(notifications_body, notification)); + }); + return PluginResult::OK; + } + + bool MatrixNotificationsPage::is_ready() { + return matrix->has_finished_fetching_notifications(); + } + + void MatrixNotificationsPage::add_notification(MatrixNotification notification) { + //int prev_selected_item = notifications_body->get_selected_item(); + //notifications_body->items.push_back(notification_to_body_item(notifications_body, notification)); + //notifications_body->set_selected_item(prev_selected_item - 1); + int prev_selected_item = notifications_body->get_selected_item(); + notifications_body->items.insert(notifications_body->items.begin(), notification_to_body_item(notifications_body, notification)); + notifications_body->set_selected_item(prev_selected_item + 1, false); + } + + void MatrixNotificationsPage::set_room_as_read(RoomData *room) { + for(auto &body_item : notifications_body->items) { + NotificationsExtraData *extra_data = static_cast<NotificationsExtraData*>(body_item->extra.get()); + if(!extra_data->read && extra_data->room == room) { + extra_data->read = true; + body_item->set_author_color(sf::Color::White); + body_item->set_description_color(sf::Color::White); + } + } + } + static std::array<const char*, 7> sync_fail_error_codes = { "M_FORBIDDEN", "M_UNKNOWN_TOKEN", @@ -1184,24 +1277,17 @@ namespace QuickMedia { if(initial_sync) { notification_thread = std::thread([this]() { - std::vector<CommandArg> additional_args = { - { "-H", "Authorization: Bearer " + access_token } - }; - - // TODO: Instead of guessing notification limit with 100, accumulate rooms unread_notifications count and use that as the limit - // (and take into account that notification response may have notifications after call to sync above). - char url[512]; - snprintf(url, sizeof(url), "%s/_matrix/client/r0/notifications?limit=100&only=highlight", homeserver.c_str()); - - rapidjson::Document json_root; - DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true); - if(download_result != DownloadResult::OK || !json_root.IsObject()) { - fprintf(stderr, "Fetching notifications failed!\n"); - return; - } + get_previous_notifications([this](const MatrixNotification ¬ification) { + if(notification.read) + return; + + MatrixDelegate *delegate = this->delegate; + ui_thread_tasks.push([delegate, notification] { + delegate->add_unread_notification(std::move(notification)); + }); + }); - const rapidjson::Value ¬ification_json = GetMember(json_root, "notifications"); - parse_notifications(notification_json); + finished_fetching_notifications = true; { std::vector<CommandArg> additional_args = { @@ -1288,10 +1374,12 @@ namespace QuickMedia { delegate = nullptr; sync_failed = false; sync_fail_reason.clear(); - set_next_batch(""); + next_batch.clear(); + next_notifications_token.clear(); invites.clear(); filter_cached.reset(); my_events_transaction_ids.clear(); + finished_fetching_notifications = false; } bool Matrix::is_initial_sync_finished() const { @@ -1307,6 +1395,10 @@ namespace QuickMedia { } } + bool Matrix::has_finished_fetching_notifications() const { + return finished_fetching_notifications; + } + void Matrix::get_room_sync_data(RoomData *room, SyncData &sync_data) { room->acquire_room_lock(); auto &room_messages = room->get_messages_thread_unsafe(); @@ -1356,6 +1448,45 @@ namespace QuickMedia { return PluginResult::OK; } + PluginResult Matrix::get_previous_notifications(std::function<void(const MatrixNotification&)> callback_func) { + std::vector<CommandArg> additional_args = { + { "-H", "Authorization: Bearer " + access_token } + }; + + std::string from = get_next_notifications_token(); + + // TODO: Instead of guessing notification limit with 100, accumulate rooms unread_notifications count and use that as the limit + // (and take into account that notification response may have notifications after call to sync above). + char url[512]; + if(from.empty()) + snprintf(url, sizeof(url), "%s/_matrix/client/r0/notifications?limit=100&only=highlight", homeserver.c_str()); + else + snprintf(url, sizeof(url), "%s/_matrix/client/r0/notifications?limit=100&only=highlight&from=%s", homeserver.c_str(), from.c_str()); + + rapidjson::Document json_root; + DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true); + if(download_result != DownloadResult::OK || !json_root.IsObject()) { + fprintf(stderr, "Fetching notifications failed!\n"); + return PluginResult::ERR; + } + + const rapidjson::Value ¬ification_json = GetMember(json_root, "notifications"); + parse_notifications(notification_json, std::move(callback_func)); + + const rapidjson::Value &next_token_json = GetMember(json_root, "next_token"); + if(next_token_json.IsString()) + set_next_notifications_token(next_token_json.GetString()); + + return PluginResult::OK; + } + + void Matrix::get_cached_notifications(std::function<void(const MatrixNotification&)> callback_func) { + std::lock_guard<std::mutex> lock(notifications_mutex); + for(const auto ¬ification : notifications) { + callback_func(notification); + } + } + PluginResult Matrix::parse_sync_response(const rapidjson::Document &root, bool is_additional_messages_sync, bool initial_sync) { if(!root.IsObject()) return PluginResult::ERR; @@ -1371,22 +1502,28 @@ namespace QuickMedia { return PluginResult::OK; } - PluginResult Matrix::parse_notifications(const rapidjson::Value ¬ifications_json) { + PluginResult Matrix::parse_notifications(const rapidjson::Value ¬ifications_json, std::function<void(const MatrixNotification&)> callback_func) { if(!notifications_json.IsArray()) return PluginResult::ERR; + std::lock_guard<std::mutex> lock(notifications_mutex); for(const rapidjson::Value ¬ification_json : notifications_json.GetArray()) { if(!notification_json.IsObject()) continue; const rapidjson::Value &read_json = GetMember(notification_json, "read"); - if(!read_json.IsBool() || read_json.GetBool()) + if(!read_json.IsBool()) continue; const rapidjson::Value &room_id_json = GetMember(notification_json, "room_id"); if(!room_id_json.IsString()) continue; + time_t timestamp = 0; + const rapidjson::Value &ts_json = GetMember(notification_json, "ts"); + if(ts_json.IsInt64()) + timestamp = ts_json.GetInt64(); + const rapidjson::Value &event_json = GetMember(notification_json, "event"); if(!event_json.IsObject()) continue; @@ -1414,12 +1551,15 @@ namespace QuickMedia { continue; } - std::string event_id(event_id_json.GetString(), event_id_json.GetStringLength()); - std::string sender(sender_json.GetString(), sender_json.GetStringLength()); - std::string body(body_json.GetString(), body_json.GetStringLength()); - ui_thread_tasks.push([this, room, event_id{std::move(event_id)}, sender{std::move(sender)}, body{std::move(body)}] { - delegate->add_unread_notification(room, std::move(event_id), std::move(sender), std::move(body)); - }); + MatrixNotification notification; + notification.room = room; + notification.event_id.assign(event_id_json.GetString(), event_id_json.GetStringLength()); + notification.sender_user_id.assign(sender_json.GetString(), sender_json.GetStringLength()); + notification.body = remove_reply_formatting(body_json.GetString()); + notification.timestamp = timestamp; + notification.read = read_json.GetBool(); + callback_func(notification); + notifications.push_back(std::move(notification)); } return PluginResult::OK; } @@ -3967,6 +4107,16 @@ namespace QuickMedia { return next_batch; } + void Matrix::set_next_notifications_token(std::string new_next_token) { + std::lock_guard<std::mutex> lock(next_batch_mutex); + next_notifications_token = std::move(new_next_token); + } + + std::string Matrix::get_next_notifications_token() { + std::lock_guard<std::mutex> lock(next_batch_mutex); + return next_notifications_token; + } + void Matrix::clear_sync_cache_for_new_sync() { std::lock_guard<std::recursive_mutex> room_data_lock(room_data_mutex); std::lock_guard<std::mutex> invites_lock(invite_mutex); |