From 620123fbd6c18dc48a25cc735565f6d8d85f8639 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Mon, 26 Oct 2020 09:48:25 +0100 Subject: Matrix: add room tags Fix pinned events that are added after starting QuickMedia (before this change it adds all elements again to the list). Add /me command. Other fixes... --- src/AsyncImageLoader.cpp | 36 +- src/Body.cpp | 45 +-- src/NetUtils.cpp | 18 +- src/QuickMedia.cpp | 838 +++++++++++++++++++--------------------------- src/plugins/Mangadex.cpp | 2 +- src/plugins/Manganelo.cpp | 2 +- src/plugins/Mangatown.cpp | 2 +- src/plugins/Matrix.cpp | 644 +++++++++++++++++++++++++++++------ src/plugins/NyaaSi.cpp | 2 +- src/plugins/Pornhub.cpp | 2 +- src/plugins/Youtube.cpp | 2 +- 11 files changed, 932 insertions(+), 661 deletions(-) (limited to 'src') diff --git a/src/AsyncImageLoader.cpp b/src/AsyncImageLoader.cpp index 98c7fee..d3aa287 100644 --- a/src/AsyncImageLoader.cpp +++ b/src/AsyncImageLoader.cpp @@ -28,8 +28,8 @@ namespace QuickMedia { int scaled_y_start = ((float)y / (float)destination_size.y) * source_size.y; int scaled_x_end = ((float)(x + 1) / (float)destination_size.x) * source_size.x; int scaled_y_end = ((float)(y + 1) / (float)destination_size.y) * source_size.y; - if(scaled_x_end > (int)source_size.x) scaled_x_end = source_size.x; - if(scaled_y_end > (int)source_size.y) scaled_y_end = source_size.y; + if(scaled_x_end > (int)source_size.x - 1) scaled_x_end = source_size.x - 1; + if(scaled_y_end > (int)source_size.y - 1) scaled_y_end = source_size.y - 1; //float scaled_x = x * width_ratio; //float scaled_y = y * height_ratio; @@ -99,16 +99,13 @@ namespace QuickMedia { } load_image_thread = std::thread([this]{ - ThumbnailLoadData thumbnail_load_data; + std::optional thumbnail_load_data_opt; while(true) { - { - std::unique_lock lock(load_image_mutex); - while(images_to_load.empty() && running) load_image_cv.wait(lock); - if(!running) - break; - thumbnail_load_data = images_to_load.front(); - images_to_load.pop_front(); - } + thumbnail_load_data_opt = image_load_queue.pop_wait(); + if(!thumbnail_load_data_opt) + break; + + ThumbnailLoadData &thumbnail_load_data = thumbnail_load_data_opt.value(); thumbnail_load_data.thumbnail_data->image = std::make_unique(); if(load_image_from_file(*thumbnail_load_data.thumbnail_data->image, thumbnail_load_data.thumbnail_path.data)) { @@ -132,12 +129,9 @@ namespace QuickMedia { } AsyncImageLoader::~AsyncImageLoader() { - running = false; - { - std::unique_lock lock(load_image_mutex); - load_image_cv.notify_one(); - } - load_image_thread.join(); + image_load_queue.close(); + if(load_image_thread.joinable()) + load_image_thread.join(); // TODO: Find a way to kill the threads instead. We need to do this right now because creating a new thread before the last one has died causes a crash for(size_t i = 0; i < NUM_IMAGE_LOAD_THREADS; ++i) { @@ -161,15 +155,11 @@ namespace QuickMedia { Path thumbnail_path = get_cache_dir().join("thumbnails").join(sha256.getHash()); if(get_file_type(thumbnail_path) == FileType::REGULAR) { thumbnail_data->loading_state = LoadingState::LOADING; - std::unique_lock lock(load_image_mutex); - images_to_load.push_back({ url, thumbnail_path, local, thumbnail_data, resize_target_size }); - load_image_cv.notify_one(); + image_load_queue.push({ url, thumbnail_path, local, thumbnail_data, resize_target_size }); return; } else if(local && get_file_type(url) == FileType::REGULAR) { thumbnail_data->loading_state = LoadingState::LOADING; - std::unique_lock lock(load_image_mutex); - images_to_load.push_back({ url, thumbnail_path, true, thumbnail_data, resize_target_size }); - load_image_cv.notify_one(); + image_load_queue.push({ url, thumbnail_path, true, thumbnail_data, resize_target_size }); return; } diff --git a/src/Body.cpp b/src/Body.cpp index 0af6407..1ea5be2 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -187,7 +187,7 @@ namespace QuickMedia { } void Body::set_selected_item(int item) { - assert(item >= 0 && item < (int)items.size()); + //assert(item >= 0 && item < (int)items.size()); selected_item = item; prev_selected_item = selected_item; clamp_selection(); @@ -361,7 +361,7 @@ namespace QuickMedia { int i = prev_selected_item; while(num_items_scrolled < selected_int_diff_abs && i < num_items) { if(items[i]->visible) { - page_scroll += (get_item_height(items[i].get(), selected_int_diff_abs < 50) + spacing_y); + page_scroll += (get_item_height(items[i].get(), size.x, selected_int_diff_abs < 50) + spacing_y); } ++num_items_scrolled; ++i; @@ -372,7 +372,7 @@ namespace QuickMedia { int i = prev_selected_item - 1; while(num_items_scrolled < selected_int_diff_abs && i >= 0) { if(items[i]->visible) { - page_scroll -= (get_item_height(items[i].get(), selected_int_diff_abs < 50) + spacing_y); + page_scroll -= (get_item_height(items[i].get(), size.x, selected_int_diff_abs < 50) + spacing_y); } ++num_items_scrolled; --i; @@ -380,8 +380,7 @@ namespace QuickMedia { prev_selected_item = selected_item; } - update_dirty_state(items[selected_item].get(), size); - float selected_item_height = get_item_height(items[selected_item].get()) + spacing_y; + float selected_item_height = get_item_height(items[selected_item].get(), size.x) + spacing_y; if(page_scroll > size.y - selected_item_height) { page_scroll = size.y - selected_item_height; } else if(page_scroll < 0.0f) { @@ -400,10 +399,8 @@ namespace QuickMedia { if(!item->visible) continue; - update_dirty_state(item.get(), size); item->last_drawn_time = elapsed_time_sec; - - float item_height = get_item_height(item.get()); + float item_height = get_item_height(item.get(), size.x); prev_pos.y -= (item_height + spacing_y); if(prev_pos.y + item_height + spacing_y <= start_y) @@ -430,9 +427,8 @@ namespace QuickMedia { break; } - update_dirty_state(item.get(), size); item->last_drawn_time = elapsed_time_sec; - float item_height = get_item_height(item.get()); + float item_height = get_item_height(item.get(), size.x); // This is needed here rather than above the loop, since update_dirty_text cant be called inside scissor because it corrupts the text for some reason glEnable(GL_SCISSOR_TEST); @@ -469,7 +465,7 @@ namespace QuickMedia { } } - void Body::update_dirty_state(BodyItem *body_item, sf::Vector2f size) { + void Body::update_dirty_state(BodyItem *body_item, float width) { if(body_item->dirty) { body_item->dirty = false; // TODO: Find a way to optimize fromUtf8 @@ -477,7 +473,7 @@ namespace QuickMedia { if(body_item->title_text) body_item->title_text->setString(std::move(str)); else - body_item->title_text = std::make_unique(std::move(str), font, cjk_font, 16, size.x - 50 - image_padding_x * 2.0f); + body_item->title_text = std::make_unique(std::move(str), font, cjk_font, 16, width - 50 - image_padding_x * 2.0f); body_item->title_text->setFillColor(body_item->get_title_color()); body_item->title_text->updateGeometry(); } @@ -488,7 +484,7 @@ namespace QuickMedia { if(body_item->description_text) body_item->description_text->setString(std::move(str)); else - body_item->description_text = std::make_unique(std::move(str), font, cjk_font, 14, size.x - 50 - image_padding_x * 2.0f); + body_item->description_text = std::make_unique(std::move(str), font, cjk_font, 14, width - 50 - image_padding_x * 2.0f); body_item->description_text->setFillColor(body_item->get_description_color()); body_item->description_text->updateGeometry(); } @@ -499,7 +495,7 @@ namespace QuickMedia { if(body_item->author_text) body_item->author_text->setString(std::move(str)); else - body_item->author_text = std::make_unique(std::move(str), bold_font, cjk_font, 14, size.x - 50 - image_padding_x * 2.0f); + body_item->author_text = std::make_unique(std::move(str), bold_font, cjk_font, 14, width - 50 - image_padding_x * 2.0f); body_item->author_text->setFillColor(body_item->get_author_color()); body_item->author_text->updateGeometry(); } @@ -566,7 +562,7 @@ namespace QuickMedia { } void Body::draw_item(sf::RenderWindow &window, BodyItem *item, sf::Vector2f pos, sf::Vector2f size, bool include_embedded_item) { - update_dirty_state(item, size); + update_dirty_state(item, size.x); item->last_drawn_time = draw_timer.getElapsedTime().asMilliseconds(); sf::Vector2u window_size = window.getSize(); glEnable(GL_SCISSOR_TEST); @@ -622,7 +618,7 @@ namespace QuickMedia { } float text_offset_x = padding_x; - if(draw_thumbnails && !item->thumbnail_url.empty()) { + if(draw_thumbnails && item_thumbnail) { double elapsed_time_thumbnail = 0.0; if(item_thumbnail->loading_state == LoadingState::APPLIED_TO_TEXTURE) elapsed_time_thumbnail = item_thumbnail->texture_applied_time.getElapsedTime().asSeconds(); //thumbnail_fade_duration_sec @@ -678,7 +674,7 @@ namespace QuickMedia { auto new_loading_icon_size = clamp_to_size(loading_icon_size, content_size); loading_icon.setPosition(item_pos + sf::Vector2f(image_padding_x, padding_y) + (content_size * 0.5f)); loading_icon.setScale(get_ratio(loading_icon_size, new_loading_icon_size)); - loading_icon.setRotation(-elapsed_time_sec * 400.0); + loading_icon.setRotation(elapsed_time_sec * 400.0); loading_icon.setColor(sf::Color(255, 255, 255, fallback_fade_alpha)); window.draw(loading_icon); @@ -710,8 +706,9 @@ namespace QuickMedia { } if(include_embedded_item && item->embedded_item_status != FetchStatus::NONE) { - float embedded_item_height = item->embedded_item ? get_item_height(item->embedded_item.get(), true, false) : (embedded_item_load_text.getLocalBounds().height + embedded_item_padding_y * 2.0f); const float border_width = 4.0f; + const float embedded_item_width = std::floor(size.x - text_offset_x - border_width - padding_x); + float embedded_item_height = item->embedded_item ? get_item_height(item->embedded_item.get(), embedded_item_width, true, false) : (embedded_item_load_text.getLocalBounds().height + embedded_item_padding_y * 2.0f); sf::RectangleShape border_left(sf::Vector2f(border_width, std::floor(embedded_item_height))); border_left.setFillColor(sf::Color::White); border_left.setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + 4.0f)); @@ -719,7 +716,7 @@ namespace QuickMedia { if(item->embedded_item) { sf::Vector2f embedded_item_pos(std::floor(item_pos.x + text_offset_x + border_width + padding_x), std::floor(item_pos.y + embedded_item_padding_y + 4.0f)); - sf::Vector2f embedded_item_size(std::floor(size.x - text_offset_x - border_width - padding_x), embedded_item_height); + sf::Vector2f embedded_item_size(embedded_item_width, embedded_item_height); draw_item(window, item->embedded_item.get(), embedded_item_pos, embedded_item_size, false); } else { embedded_item_load_text.setString(embedded_item_status_to_string(item->embedded_item_status)); @@ -770,7 +767,9 @@ namespace QuickMedia { } } - float Body::get_item_height(BodyItem *item, bool load_texture, bool include_embedded_item) { + float Body::get_item_height(BodyItem *item, float width, bool load_texture, bool include_embedded_item) { + if(load_texture) + update_dirty_state(item, width); float item_height = 0.0f; if(item->title_text) { item_height += item->title_text->getHeight() - 2.0f; @@ -780,7 +779,7 @@ namespace QuickMedia { } if(include_embedded_item && item->embedded_item_status != FetchStatus::NONE) { if(item->embedded_item) - item_height += (get_item_height(item->embedded_item.get(), load_texture, false) + 4.0f + embedded_item_padding_y * 2.0f); + item_height += (get_item_height(item->embedded_item.get(), width, load_texture, false) + 4.0f + embedded_item_padding_y * 2.0f); else item_height += (embedded_item_load_text.getLocalBounds().height + 4.0f + embedded_item_padding_y * 2.0f); } @@ -802,7 +801,7 @@ namespace QuickMedia { item_thumbnail = item_thumbnail_it->second; } - if(load_texture) { + if(load_texture && item_thumbnail) { item_thumbnail->referenced = true; if(!item->thumbnail_url.empty() && item_thumbnail->loading_state == LoadingState::NOT_LOADED) @@ -864,6 +863,8 @@ namespace QuickMedia { body_item->visible = string_find_case_insensitive(body_item->get_title(), text); if(!body_item->visible && !body_item->get_description().empty()) body_item->visible = string_find_case_insensitive(body_item->get_description(), text); + if(!body_item->visible && !body_item->get_author().empty()) + body_item->visible = string_find_case_insensitive(body_item->get_author(), text); } bool Body::no_items_visible() const { diff --git a/src/NetUtils.cpp b/src/NetUtils.cpp index 4d5a940..f8b118b 100644 --- a/src/NetUtils.cpp +++ b/src/NetUtils.cpp @@ -45,13 +45,21 @@ namespace QuickMedia { } } + static bool is_alpha(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + } + + static bool is_digit(char c) { + return c >= '0' && c <= '9'; + } + std::string url_param_encode(const std::string ¶m) { std::ostringstream result; result.fill('0'); result << std::hex; for(char c : param) { - if(isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') { + if(is_alpha(c) || is_digit(c) || c == '-' || c == '_' || c == '.' || c == '~') { result << c; } else { result << std::uppercase; @@ -62,14 +70,6 @@ namespace QuickMedia { return result.str(); } - static bool is_alpha(char c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); - } - - static bool is_digit(char c) { - return c >= '0' && c <= '9'; - } - static bool is_url_character(char c) { switch(c) { case '%': diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 054b3ed..c4532cd 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -392,6 +392,9 @@ namespace QuickMedia { } Program::~Program() { + images_to_upscale_queue.close(); + if(image_upscale_thead.joinable()) + image_upscale_thead.join(); if(matrix) delete matrix; if(disp) @@ -509,14 +512,13 @@ namespace QuickMedia { } image_upscale_thead = std::thread([this]{ - CopyOp copy_op; + std::optional copy_op_opt; while(true) { - { - std::unique_lock lock(image_upscale_mutex); - while(images_to_upscale.empty()) image_upscale_cv.wait(lock); - copy_op = images_to_upscale.front(); - images_to_upscale.pop_front(); - } + copy_op_opt = images_to_upscale_queue.pop_wait(); + if(!copy_op_opt) + break; + + CopyOp ©_op = copy_op_opt.value(); Path tmp_file = copy_op.source; tmp_file.append(".tmp.png"); @@ -538,7 +540,6 @@ namespace QuickMedia { file_overwrite(copy_op.destination.data.c_str(), "1"); } }); - image_upscale_thead.detach(); } if(strcmp(plugin_name, "file-manager") != 0 && start_dir) { @@ -615,14 +616,16 @@ namespace QuickMedia { } if(!tabs.empty()) { - page_loop(std::move(tabs)); + page_loop(tabs); return exit_code; } if(matrix) { matrix->use_tor = use_tor; { - auto window_size = window.getSize(); + auto window_size_u = window.getSize(); + window_size.x = window_size_u.x; + window_size.y = window_size_u.y; sf::Text loading_text("Loading...", *font.get(), 24); loading_text.setPosition(window_size.x * 0.5f - loading_text.getLocalBounds().width * 0.5f, window_size.y * 0.5f - loading_text.getLocalBounds().height * 0.5f); window.clear(back_color); @@ -634,21 +637,56 @@ namespace QuickMedia { } else { fprintf(stderr, "Failed to load session cache, redirecting to login page\n"); current_page = PageType::CHAT_LOGIN; + chat_login_page(); } - while(window.isOpen()) { - switch(current_page) { - case PageType::CHAT_LOGIN: - chat_login_page(); - break; - case PageType::CHAT: - chat_page(); - break; - default: + if(!window.isOpen()) + return exit_code; + + auto rooms_body = create_body(); + rooms_body->thumbnail_mask_shader = &circle_mask_shader; + auto matrix_rooms_page = std::make_unique(this, rooms_body.get(), "All rooms"); + + auto rooms_tags_body = create_body(); + rooms_tags_body->thumbnail_mask_shader = &circle_mask_shader; + auto matrix_rooms_tag_page = std::make_unique(this, rooms_tags_body.get()); + + MatrixQuickMedia matrix_handler(this, matrix, matrix_rooms_page.get(), matrix_rooms_tag_page.get()); + matrix->start_sync(&matrix_handler); + + tabs.push_back(Tab{std::move(rooms_body), std::move(matrix_rooms_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + tabs.push_back(Tab{std::move(rooms_tags_body), std::move(matrix_rooms_tag_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + + sf::Sprite load_sprite(loading_icon); + sf::Vector2u loading_icon_size = loading_icon.getSize(); + load_sprite.setOrigin(loading_icon_size.x * 0.5f, loading_icon_size.y * 0.5f); + + sf::Clock timer; + sf::Event event; + while(window.isOpen() && !matrix->is_initial_sync_finished()) { + while(window.pollEvent(event)) { + if(event.type == sf::Event::Closed) window.close(); - break; + else if(event.type == sf::Event::Resized) { + window_size.x = event.size.width; + window_size.y = event.size.height; + sf::FloatRect visible_area(0, 0, window_size.x, window_size.y); + window.setView(sf::View(visible_area)); + } } + window.clear(back_color); + load_sprite.setPosition(window_size.x * 0.5f - loading_icon_size.x * 0.5f, window_size.y * 0.5f - loading_icon_size.y * 0.5f); + load_sprite.setRotation(timer.getElapsedTime().asSeconds() * 400.0); + window.draw(load_sprite); + window.display(); + } + + while(window.isOpen()) { + page_loop(tabs); } + + exit(exit_code); // Exit immediately without waiting for anything to finish + //matrix->stop_sync(); } return exit_code; @@ -896,7 +934,9 @@ namespace QuickMedia { } std::unique_ptr Program::create_body() { - return std::make_unique(this, font.get(), bold_font.get(), cjk_font.get(), loading_icon); + auto body = std::make_unique(this, font.get(), bold_font.get(), cjk_font.get(), loading_icon); + body->thumbnail_mask_shader = &circle_mask_shader; + return body; } std::unique_ptr Program::create_search_bar(const std::string &placeholder, int search_delay) { @@ -926,7 +966,15 @@ namespace QuickMedia { selected_files.push_back(filepath); } - void Program::page_loop(std::vector tabs) { + bool Program::is_window_focused() { + return window.hasFocus(); + } + + RoomData* Program::get_current_chat_room() { + return current_chat_room; + } + + void Program::page_loop(std::vector &tabs) { if(tabs.empty()) { show_notification("QuickMedia", "No tabs provided!", Urgency::CRITICAL); return; @@ -1047,24 +1095,27 @@ namespace QuickMedia { } } window.setKeyRepeatEnabled(true); - redraw = true; } else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::IMAGE_BOARD_THREAD) { current_page = PageType::IMAGE_BOARD_THREAD; image_board_thread_page(static_cast(new_tabs[0].page.get()), new_tabs[0].body.get()); - redraw = true; } else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::VIDEO) { current_page = PageType::VIDEO_CONTENT; video_content_page(new_tabs[0].page.get(), selected_item->url, selected_item->get_title()); - redraw = true; + } else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::CHAT) { + current_page = PageType::CHAT; + current_chat_room = matrix->get_room_by_id(selected_item->url); + chat_page(static_cast(new_tabs[0].page.get()), current_chat_room); + current_chat_room = nullptr; } else { - page_loop(std::move(new_tabs)); - tabs[selected_tab].page->on_navigate_to_page(); - if(content_storage_json.isObject()) { - const Json::Value &chapters_json = content_storage_json["chapters"]; - if(chapters_json.isObject()) - json_chapters = &chapters_json; - } + page_loop(new_tabs); + } + tabs[selected_tab].page->on_navigate_to_page(); + if(content_storage_json.isObject()) { + const Json::Value &chapters_json = content_storage_json["chapters"]; + if(chapters_json.isObject()) + json_chapters = &chapters_json; } + redraw = true; } else { // TODO: Show the exact cause of error (get error message from curl). // TODO: Make asynchronous @@ -1118,9 +1169,6 @@ namespace QuickMedia { while (window.isOpen() && loop_running) { sf::Int32 frame_time_ms = frame_timer.restart().asMilliseconds(); - Tab ¤t_tab = tabs[selected_tab]; - TabAssociatedData ¤t_tab_associated_data = tab_associated_data[selected_tab]; - while (window.pollEvent(event)) { if (event.type == sf::Event::Closed) { window.close(); @@ -1131,10 +1179,10 @@ namespace QuickMedia { window.setView(sf::View(visible_area)); } - if(current_tab.search_bar) { + if(tabs[selected_tab].search_bar) { if(event.type == sf::Event::TextEntered) - current_tab.search_bar->onTextEntered(event.text.unicode); - current_tab.search_bar->on_event(event); + tabs[selected_tab].search_bar->onTextEntered(event.text.unicode); + tabs[selected_tab].search_bar->on_event(event); } if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) @@ -1144,26 +1192,26 @@ namespace QuickMedia { bool hit_bottom = false; switch(event.key.code) { case sf::Keyboard::Down: - hit_bottom = !current_tab.body->select_next_item(); + hit_bottom = !tabs[selected_tab].body->select_next_item(); break; case sf::Keyboard::PageDown: - hit_bottom = !current_tab.body->select_next_page(); + hit_bottom = !tabs[selected_tab].body->select_next_page(); break; case sf::Keyboard::End: - current_tab.body->select_last_item(); + tabs[selected_tab].body->select_last_item(); hit_bottom = true; break; default: hit_bottom = false; break; } - if(hit_bottom && current_tab_associated_data.fetch_status == FetchStatus::NONE && !current_tab_associated_data.fetching_next_page_running && current_tab.page) { + if(hit_bottom && tab_associated_data[selected_tab].fetch_status == FetchStatus::NONE && !tab_associated_data[selected_tab].fetching_next_page_running && tabs[selected_tab].page) { gradient_inc = 0.0; - current_tab_associated_data.fetching_next_page_running = true; - int next_page = current_tab_associated_data.fetched_page + 1; - Page *page = current_tab.page.get(); - std::string update_search_text = current_tab_associated_data.update_search_text; - current_tab_associated_data.next_page_future = std::async(std::launch::async, [update_search_text, next_page, page]() { + tab_associated_data[selected_tab].fetching_next_page_running = true; + int next_page = tab_associated_data[selected_tab].fetched_page + 1; + Page *page = tabs[selected_tab].page.get(); + std::string update_search_text = tab_associated_data[selected_tab].update_search_text; + tab_associated_data[selected_tab].next_page_future = std::async(std::launch::async, [update_search_text, next_page, page]() { BodyItems result_items; if(page->get_page(update_search_text, next_page, result_items) != PluginResult::OK) fprintf(stderr, "Failed to get next page (page %d)\n", next_page); @@ -1171,33 +1219,33 @@ namespace QuickMedia { }); } } else if(event.key.code == sf::Keyboard::Up) { - current_tab.body->select_previous_item(); + tabs[selected_tab].body->select_previous_item(); } else if(event.key.code == sf::Keyboard::PageUp) { - current_tab.body->select_previous_page(); + tabs[selected_tab].body->select_previous_page(); } else if(event.key.code == sf::Keyboard::Home) { - current_tab.body->select_first_item(); + tabs[selected_tab].body->select_first_item(); } else if(event.key.code == sf::Keyboard::Escape) { goto page_end; } else if(event.key.code == sf::Keyboard::Left) { if(selected_tab > 0) { - current_tab.body->clear_cache(); + tabs[selected_tab].body->clear_cache(); --selected_tab; redraw = true; } } else if(event.key.code == sf::Keyboard::Right) { if(selected_tab < (int)tabs.size() - 1) { - current_tab.body->clear_cache(); + tabs[selected_tab].body->clear_cache(); ++selected_tab; redraw = true; } } else if(event.key.code == sf::Keyboard::Tab) { - if(current_tab.search_bar) current_tab.search_bar->set_to_autocomplete(); + if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->set_to_autocomplete(); } else if(event.key.code == sf::Keyboard::Enter) { - if(!current_tab.search_bar) submit_handler(); + if(!tabs[selected_tab].search_bar) submit_handler(); } else if(event.key.code == sf::Keyboard::T && event.key.control) { - BodyItem *selected_item = current_tab.body->get_selected(); - if(selected_item && current_tab.page && current_tab.page->is_trackable()) { - TrackablePage *trackable_page = static_cast(current_tab.page.get()); + BodyItem *selected_item = tabs[selected_tab].body->get_selected(); + if(selected_item && tabs[selected_tab].page && tabs[selected_tab].page->is_trackable()) { + TrackablePage *trackable_page = static_cast(tabs[selected_tab].page.get()); TrackResult track_result = trackable_page->track(selected_item->get_title()); // TODO: Show proper error message when this fails. For example if we are already tracking the manga if(track_result == TrackResult::OK) { @@ -1212,9 +1260,9 @@ namespace QuickMedia { if(redraw) { redraw = false; - if(current_tab.search_bar) current_tab.search_bar->onWindowResize(window_size); + if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->onWindowResize(window_size); // TODO: Dont show tabs if there is only one tab - get_body_dimensions(window_size, current_tab.search_bar.get(), body_pos, body_size, true); + 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; @@ -1229,14 +1277,14 @@ namespace QuickMedia { gradient_points[3].position.y = window_size.y; } - if(current_tab.search_bar) current_tab.search_bar->update(); + if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->update(); - if(current_tab.page->is_lazy_fetch_page() && current_tab_associated_data.fetch_status == FetchStatus::NONE && !current_tab_associated_data.lazy_fetch_finished) { - current_tab_associated_data.fetch_status = FetchStatus::LOADING; - current_tab_associated_data.fetch_type = FetchType::LAZY; - current_tab_associated_data.search_result_text.setString("Fetching page..."); - LazyFetchPage *lazy_fetch_page = static_cast(current_tab.page.get()); - current_tab_associated_data.fetch_future = std::async(std::launch::async, [lazy_fetch_page]() { + 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) { + 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("Fetching page..."); + LazyFetchPage *lazy_fetch_page = static_cast(tabs[selected_tab].page.get()); + tab_associated_data[selected_tab].fetch_future = std::async(std::launch::async, [lazy_fetch_page]() { FetchResult fetch_result; fetch_result.result = lazy_fetch_page->lazy_fetch(fetch_result.body_items); return fetch_result; @@ -1246,6 +1294,8 @@ namespace QuickMedia { for(size_t i = 0; i < tabs.size(); ++i) { TabAssociatedData &associated_data = tab_associated_data[i]; + tabs[i].page->update(); + if(associated_data.fetching_next_page_running && is_future_ready(associated_data.next_page_future)) { BodyItems new_body_items = associated_data.next_page_future.get(); fprintf(stderr, "Finished fetching page %d, num new messages: %zu\n", associated_data.fetched_page + 1, new_body_items.size()); @@ -1304,18 +1354,18 @@ namespace QuickMedia { } window.clear(back_color); - if(current_tab.search_bar) current_tab.search_bar->draw(window, false); + if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->draw(window, false); { float shade_extra_height = 0.0f; - if(!current_tab.search_bar) + if(!tabs[selected_tab].search_bar) shade_extra_height = 10.0f; const float width_per_tab = window_size.x / tabs.size(); tab_background.setSize(sf::Vector2f(std::floor(width_per_tab - tab_margin_x * 2.0f), tab_height)); - float tab_vertical_offset = current_tab.search_bar ? current_tab.search_bar->getBottomWithoutShadow() : 0.0f; - current_tab.body->draw(window, body_pos, body_size, *json_chapters); + float tab_vertical_offset = tabs[selected_tab].search_bar ? tabs[selected_tab].search_bar->getBottomWithoutShadow() : 0.0f; + tabs[selected_tab].body->draw(window, body_pos, body_size, *json_chapters); const float tab_y = tab_spacer_height + std::floor(tab_vertical_offset + tab_height * 0.5f - (tab_text_size + 5.0f) * 0.5f) + shade_extra_height; tab_shade.setPosition(0.0f, tab_spacer_height + std::floor(tab_vertical_offset)); @@ -1338,7 +1388,7 @@ namespace QuickMedia { } } - if(current_tab_associated_data.fetching_next_page_running) { + if(tab_associated_data[selected_tab].fetching_next_page_running) { 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 bottom_color = interpolate_colors(back_color, sf::Color(175, 180, 188), progress); @@ -1350,12 +1400,12 @@ namespace QuickMedia { window.draw(gradient_points, 4, sf::Quads); // Note: sf::Quads doesn't work with egl } - if(!current_tab_associated_data.search_result_text.getString().isEmpty()) { - auto search_result_text_bounds = current_tab_associated_data.search_result_text.getLocalBounds(); - current_tab_associated_data.search_result_text.setPosition( + if(!tab_associated_data[selected_tab].search_result_text.getString().isEmpty()) { + auto search_result_text_bounds = tab_associated_data[selected_tab].search_result_text.getLocalBounds(); + tab_associated_data[selected_tab].search_result_text.setPosition( std::floor(body_pos.x + body_size.x * 0.5f - search_result_text_bounds.width * 0.5f), std::floor(body_pos.y + body_size.y * 0.5f - search_result_text_bounds.height * 0.5f)); - window.draw(current_tab_associated_data.search_result_text); + window.draw(tab_associated_data[selected_tab].search_result_text); } window.display(); @@ -1785,7 +1835,7 @@ namespace QuickMedia { if(!video_loaded) { window.clear(back_color); load_sprite.setPosition(window_size.x * 0.5f - loading_icon_size.x * 0.5f, window_size.y * 0.5f - loading_icon_size.y * 0.5f); - load_sprite.setRotation(-time_watched_timer.getElapsedTime().asSeconds() * 400.0); + load_sprite.setRotation(time_watched_timer.getElapsedTime().asSeconds() * 400.0); window.draw(load_sprite); window.display(); continue; @@ -1998,9 +2048,7 @@ namespace QuickMedia { CopyOp copy_op; copy_op.source = image_filepath_tmp; copy_op.destination = image_filepath; - std::unique_lock lock(image_upscale_mutex); - images_to_upscale.push_back(std::move(copy_op)); - image_upscale_cv.notify_one(); + images_to_upscale_queue.push(std::move(copy_op)); } else { fprintf(stderr, "Info: not upscaling %s because the file is already large on your monitor (screen height: %d, image height: %d)\n", image_filepath_tmp.data.c_str(), screen_height, image_height); image_upscale_status[image_index] = 1; @@ -2014,9 +2062,7 @@ namespace QuickMedia { CopyOp copy_op; copy_op.source = image_filepath_tmp; copy_op.destination = image_filepath; - std::unique_lock lock(image_upscale_mutex); - images_to_upscale.push_back(std::move(copy_op)); - image_upscale_cv.notify_one(); + images_to_upscale_queue.push(std::move(copy_op)); } if(rename_immediately) { @@ -2238,8 +2284,7 @@ namespace QuickMedia { image_download_future.get(); image_download_cancel = false; } - std::unique_lock lock(image_upscale_mutex); - images_to_upscale.clear(); + images_to_upscale_queue.clear(); image_upscale_status.clear(); } return page_navigation; @@ -2328,8 +2373,7 @@ namespace QuickMedia { image_download_future.get(); image_download_cancel = false; } - std::unique_lock lock(image_upscale_mutex); - images_to_upscale.clear(); + images_to_upscale_queue.clear(); image_upscale_status.clear(); } } @@ -2459,12 +2503,12 @@ namespace QuickMedia { } }; - comment_input.on_submit_callback = [&post_comment_future, &navigation_stage, &request_new_google_captcha_challenge, &comment_to_post, &captcha_post_id, &captcha_solved_time, &post_comment, &thread_page](const std::string &text) -> bool { + comment_input.on_submit_callback = [&post_comment_future, &navigation_stage, &request_new_google_captcha_challenge, &comment_to_post, &captcha_post_id, &captcha_solved_time, &post_comment, &thread_page](std::string text) -> bool { if(text.empty()) return false; assert(navigation_stage == NavigationStage::REPLYING); - comment_to_post = text; + comment_to_post = std::move(text); if(!captcha_post_id.empty() && captcha_solved_time.getElapsedTime().asSeconds() < 120) { post_comment_future = std::async(std::launch::async, [&post_comment]() -> bool { post_comment(); @@ -2909,19 +2953,6 @@ namespace QuickMedia { sf::Text text; }; - static std::string extract_first_line(const std::string &str, size_t max_length) { - size_t index = str.find('\n'); - if(index == std::string::npos) { - if(str.size() > max_length) - return str.substr(0, max_length) + " (...)"; - return str; - } else if(index == 0) { - return ""; - } else { - return str.substr(0, std::min(index, max_length)) + " (...)"; - } - } - static std::string remove_reply_formatting(const std::string &str) { if(strncmp(str.c_str(), "> <@", 4) == 0) { size_t index = str.find("> ", 4); @@ -2982,9 +3013,10 @@ namespace QuickMedia { struct PinnedEventData { std::string event_id; FetchStatus status = FetchStatus::NONE; + Message *message = nullptr; }; - void Program::chat_page() { + void Program::chat_page(MatrixChatPage *chat_page, RoomData *current_room) { assert(strcmp(plugin_name, "matrix") == 0); auto video_page = std::make_unique(this); @@ -2996,7 +3028,7 @@ namespace QuickMedia { pinned_tab.body->thumbnail_max_size = CHAT_MESSAGE_THUMBNAIL_MAX_SIZE; pinned_tab.body->thumbnail_mask_shader = &circle_mask_shader; //pinned_tab.body->line_separator_color = sf::Color::Transparent; - pinned_tab.text = sf::Text("Pinned", *font, tab_text_size); + pinned_tab.text = sf::Text("Pinned messages", *font, tab_text_size); tabs.push_back(std::move(pinned_tab)); ChatTab messages_tab; @@ -3007,127 +3039,12 @@ namespace QuickMedia { messages_tab.text = sf::Text("Messages", *font, tab_text_size); tabs.push_back(std::move(messages_tab)); - ChatTab rooms_tab; - rooms_tab.body = std::make_unique(this, font.get(), bold_font.get(), cjk_font.get(), loading_icon); - //rooms_tab.body->line_separator_color = sf::Color::Transparent; - rooms_tab.body->thumbnail_mask_shader = &circle_mask_shader; - rooms_tab.text = sf::Text("Rooms", *font, tab_text_size); - tabs.push_back(std::move(rooms_tab)); - const int PINNED_TAB_INDEX = 0; const int MESSAGES_TAB_INDEX = 1; - const int ROOMS_TAB_INDEX = 2; int selected_tab = MESSAGES_TAB_INDEX; - - // This is needed to get initial data, with joined rooms etc. TODO: Remove this once its cached - // and allow asynchronous update of rooms - bool synced = false; - RoomData *current_room = nullptr; bool is_window_focused = window.hasFocus(); - // Returns -1 if no rooms or no unread rooms - auto find_top_body_position_for_unread_room = [&tabs](BodyItem *item_to_swap, int start_index) { - for(int i = start_index; i < (int)tabs[ROOMS_TAB_INDEX].body->items.size(); ++i) { - const auto &body_item = tabs[ROOMS_TAB_INDEX].body->items[i]; - if(static_cast(body_item->userdata)->last_message_read || body_item.get() == item_to_swap) - return i; - } - return -1; - }; - - // Returns -1 if no rooms or all rooms have unread mentions - auto find_top_body_position_for_mentioned_room = [&tabs](BodyItem *item_to_swap, int start_index) { - for(int i = start_index; i < (int)tabs[ROOMS_TAB_INDEX].body->items.size(); ++i) { - const auto &body_item = tabs[ROOMS_TAB_INDEX].body->items[i]; - if(!static_cast(body_item->userdata)->has_unread_mention || body_item.get() == item_to_swap) - return i; - } - return -1; - }; - - auto process_new_room_messages = - [this, &selected_tab, ¤t_room, &is_window_focused, &tabs, &find_top_body_position_for_unread_room, &find_top_body_position_for_mentioned_room] - (RoomSyncData &room_sync_data, bool is_first_sync) mutable - { - for(auto &[room, sync_data] : room_sync_data) { - for(auto &message : sync_data.messages) { - if(message->mentions_me) { - room->has_unread_mention = true; - // 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 || is_first_sync || selected_tab == ROOMS_TAB_INDEX) - show_notification("QuickMedia matrix - " + matrix->message_get_author_displayname(message.get()) + " (" + room->get_name() + ")", message->body); - } - } - } - - for(auto &[room, sync_data] : room_sync_data) { - if(sync_data.messages.empty()) - continue; - - std::shared_ptr me = matrix->get_me(room); - time_t read_marker_message_timestamp = 0; - if(me) { - auto read_marker_message = room->get_message_by_id(room->get_user_read_marker(me)); - if(read_marker_message) - read_marker_message_timestamp = read_marker_message->timestamp; - } - - // TODO: this wont always work because we dont display all types of messages from server, such as "joined", "left", "kicked", "banned", "changed avatar", "changed display name", etc. - // TODO: Binary search? - Message *last_unread_message = nullptr; - for(auto &message : sync_data.messages) { - if(message->related_event_type != RelatedEventType::EDIT && message->related_event_type != RelatedEventType::REDACTION && message->timestamp > read_marker_message_timestamp) - last_unread_message = message.get(); - } - - if(!last_unread_message && !is_first_sync) - continue; - - BodyItem *room_body_item = static_cast(room->userdata); - assert(room_body_item); - - if(last_unread_message) { - std::string room_desc = "Unread: " + matrix->message_get_author_displayname(last_unread_message) + ": " + extract_first_line(last_unread_message->body, 150); - if(room->has_unread_mention) - room_desc += "\n** You were mentioned **"; // TODO: Better notification? - room_body_item->set_description(std::move(room_desc)); - room_body_item->set_title_color(sf::Color(255, 100, 100)); - room->last_message_read = false; - - // Swap order of rooms in body list to put rooms with mentions at the top and then unread messages and then all the other rooms - // TODO: Optimize with hash map instead of linear search? or cache the index - Body *rooms_body = tabs[ROOMS_TAB_INDEX].body.get(); - int room_body_index = rooms_body->get_index_by_body_item(room_body_item); - if(room_body_index != -1) { - std::shared_ptr body_item = rooms_body->items[room_body_index]; - int body_swap_index = -1; - if(room->has_unread_mention) - body_swap_index = find_top_body_position_for_mentioned_room(body_item.get(), 0); - else if(!room->last_message_read) - body_swap_index = find_top_body_position_for_unread_room(body_item.get(), 0); - if(body_swap_index != -1 && body_swap_index != room_body_index) { - rooms_body->items.erase(rooms_body->items.begin() + room_body_index); - if(body_swap_index < room_body_index) - rooms_body->items.insert(rooms_body->items.begin() + body_swap_index, std::move(body_item)); - else - rooms_body->items.insert(rooms_body->items.begin() + (body_swap_index - 1), std::move(body_item)); - } - } - } else if(is_first_sync) { - Message *last_unread_message = nullptr; - for(auto it = sync_data.messages.rbegin(), end = sync_data.messages.rend(); it != end; ++it) { - if((*it)->related_event_type != RelatedEventType::EDIT && (*it)->related_event_type != RelatedEventType::REDACTION) { - last_unread_message = (*it).get(); - break; - } - } - if(last_unread_message) - room_body_item->set_description(matrix->message_get_author_displayname(last_unread_message) + ": " + extract_first_line(last_unread_message->body, 150)); - } - } - }; - enum class ChatState { NAVIGATING, TYPING_MESSAGE, @@ -3197,6 +3114,8 @@ namespace QuickMedia { auto body_item = find_body_item_by_event_id(body_items, num_body_items, message->related_event_id); if(body_item) { body_item->set_description(message_get_body_remove_formatting(message.get())); + // TODO: Append the new message to the body item so the body item should have a list of edit events + //body_item->userdata = message.get(); if(message->related_event_type == RelatedEventType::REDACTION) set_body_as_deleted(message.get(), body_item.get()); it = unreferenced_events.erase(it); @@ -3211,6 +3130,9 @@ namespace QuickMedia { // TODO: Optimize with hash map? auto modify_related_messages_in_current_room = [&set_body_as_deleted, &unreferenced_event_by_room, ¤t_room, &find_body_item_by_event_id, &tabs](Messages &messages) { + if(messages.empty()) + return; + auto &unreferenced_events = unreferenced_event_by_room[current_room]; auto &body_items = tabs[MESSAGES_TAB_INDEX].body->items; for(auto &message : messages) { @@ -3219,6 +3141,8 @@ namespace QuickMedia { auto body_item = find_body_item_by_event_id(body_items.data(), body_items.size(), message->related_event_id); if(body_item) { body_item->set_description(message_get_body_remove_formatting(message.get())); + // TODO: Append the new message to the body item so the body item should have a list of edit events + //body_item->userdata = message.get(); if(message->related_event_type == RelatedEventType::REDACTION) set_body_as_deleted(message.get(), body_item.get()); } else { @@ -3228,9 +3152,16 @@ namespace QuickMedia { } }; - auto process_new_pinned_events = [&tabs](const std::vector &pinned_events) { + auto process_pinned_events = [&tabs](const std::optional> &pinned_events) { + if(!pinned_events || pinned_events->empty()) + return; + + bool empty_before = tabs[PINNED_TAB_INDEX].body->items.empty(); + int selected_before = tabs[PINNED_TAB_INDEX].body->get_selected_item(); + tabs[PINNED_TAB_INDEX].body->items.clear(); + // TODO: Add message to rooms messages when there are new pinned events - for(const std::string &event : pinned_events) { + for(const std::string &event : pinned_events.value()) { auto body = BodyItem::create(""); body->set_description("Loading message..."); PinnedEventData *event_data = new PinnedEventData(); @@ -3239,62 +3170,37 @@ namespace QuickMedia { body->userdata = event_data; tabs[PINNED_TAB_INDEX].body->items.push_back(std::move(body)); } - }; - SearchBar room_search_bar(*font, &plugin_logo, "Search..."); - room_search_bar.autocomplete_search_delay = SEARCH_DELAY_FILTER; - room_search_bar.onTextUpdateCallback = [&tabs](const std::string &text) { - tabs[ROOMS_TAB_INDEX].body->filter_search_fuzzy(text); - //tabs[ROOMS_TAB_INDEX].body->select_first_item(); + if(empty_before) + tabs[PINNED_TAB_INDEX].body->select_last_item(); + else + tabs[PINNED_TAB_INDEX].body->set_selected_item(selected_before); }; - room_search_bar.onTextSubmitCallback = - [this, &tabs, &selected_tab, ¤t_room, &room_name_text, - &modify_related_messages_in_current_room, &process_new_pinned_events, &room_avatar_thumbnail_data, - &read_marker_timeout_ms, &redraw, &room_search_bar] - (const std::string&) - { - BodyItem *selected_item = tabs[ROOMS_TAB_INDEX].body->get_selected(); - if(!selected_item) - return; + Body url_selection_body(this, font.get(), bold_font.get(), cjk_font.get(), loading_icon); + + Messages all_messages; + matrix->get_all_synced_room_messages(current_room, all_messages); + tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps(messages_to_body_items(all_messages, matrix->get_me(current_room).get())); + modify_related_messages_in_current_room(all_messages); + tabs[MESSAGES_TAB_INDEX].body->select_last_item(); - tabs[ROOMS_TAB_INDEX].body->clear_cache(); + std::vector pinned_events; + matrix->get_all_pinned_events(current_room, pinned_events); + process_pinned_events(pinned_events); + tabs[PINNED_TAB_INDEX].body->select_last_item(); - current_room = (RoomData*)selected_item->userdata; - assert(current_room); - selected_tab = MESSAGES_TAB_INDEX; - tabs[MESSAGES_TAB_INDEX].body->clear_items(); + room_name_text.setString(static_cast(current_room->userdata)->get_title()); + room_avatar_thumbnail_data = std::make_shared(); - for(auto &body_item : tabs[PINNED_TAB_INDEX].body->items) { - delete((PinnedEventData*)body_item->userdata); - } - tabs[PINNED_TAB_INDEX].body->clear_items(); - - Messages all_messages; - matrix->get_all_synced_room_messages(current_room, all_messages); - tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps(messages_to_body_items(all_messages, matrix->get_me(current_room).get())); - modify_related_messages_in_current_room(all_messages); - tabs[MESSAGES_TAB_INDEX].body->select_last_item(); - - std::vector pinned_events; - matrix->get_all_pinned_events(current_room, pinned_events); - process_new_pinned_events(pinned_events); - tabs[PINNED_TAB_INDEX].body->select_last_item(); - - room_name_text.setString(static_cast(current_room->userdata)->get_title()); - room_avatar_thumbnail_data = std::make_shared(); - - read_marker_timeout_ms = 0; - redraw = true; - room_search_bar.clear(); - tabs[ROOMS_TAB_INDEX].body->filter_search_fuzzy(""); - }; + read_marker_timeout_ms = 0; + redraw = true; Entry chat_input("Press m to begin writing a message...", font.get(), cjk_font.get()); chat_input.draw_background = false; chat_input.set_editable(false); - chat_input.on_submit_callback = [this, &tabs, &chat_input, &selected_tab, ¤t_room, &new_page, &chat_state, ¤tly_operating_on_item](const std::string &text) mutable { + chat_input.on_submit_callback = [this, &tabs, &chat_input, &selected_tab, ¤t_room, &new_page, &chat_state, ¤tly_operating_on_item](std::string text) mutable { if(!current_room) return false; @@ -3302,27 +3208,28 @@ namespace QuickMedia { if(text.empty()) return false; + std::string msgtype; if(chat_state == ChatState::TYPING_MESSAGE && text[0] == '/') { - std::string command = strip(text); - if(command == "/upload") { + if(text == "/upload") { new_page = PageType::FILE_MANAGER; chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; return true; - } else if(command == "/logout") { - new_page = PageType::CHAT_LOGIN; - chat_input.set_editable(false); - chat_state = ChatState::NAVIGATING; + } else if(text == "/logout") { + show_notification("QuickMedia", "/logout command is temporary disabled. Delete " + get_storage_dir().join("matrix").join("session.json").data + " and restart QuickMedia to logout", Urgency::CRITICAL); return true; + } else if(strncmp(text.c_str(), "/me ", 4) == 0) { + msgtype = "m.emote"; + text.erase(text.begin(), text.begin() + 4); } else { - fprintf(stderr, "Error: invalid command: %s, expected /upload\n", command.c_str()); + fprintf(stderr, "Error: invalid command: %s, expected /upload, /logout or /me\n", text.c_str()); return false; } } if(chat_state == ChatState::TYPING_MESSAGE) { // TODO: Make asynchronous - if(matrix->post_message(current_room, text, std::nullopt, std::nullopt) == PluginResult::OK) { + if(matrix->post_message(current_room, text, std::nullopt, std::nullopt, msgtype) == PluginResult::OK) { chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; if(tabs[MESSAGES_TAB_INDEX].body->is_last_item_fully_visible()) @@ -3361,16 +3268,6 @@ namespace QuickMedia { return false; }; - struct SyncFutureResult { - Rooms rooms; - RoomSyncData room_sync_data; - }; - - std::future sync_future; - bool sync_running = false; - 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; RoomData *previous_messages_future_room = nullptr; @@ -3397,6 +3294,7 @@ namespace QuickMedia { if(related_body_item) { *body_item = *related_body_item; event_data->status = FetchStatus::FINISHED_LOADING; + event_data->message = static_cast(related_body_item->userdata); body_item->userdata = event_data; return; } @@ -3405,7 +3303,7 @@ namespace QuickMedia { std::string message_event_id = event_data->event_id; fetch_future_room = current_room; fetch_body_item = body_item; - body_item->embedded_item_status = FetchStatus::LOADING; + event_data->status = FetchStatus::LOADING; fetch_message_tab = PINNED_TAB_INDEX; // TODO: Check if the message is already cached before calling async? is this needed? is async creation expensive? fetch_message_future = std::async(std::launch::async, [this, &fetch_future_room, message_event_id]() { @@ -3488,8 +3386,6 @@ namespace QuickMedia { const float chat_input_padding_x = 10.0f; const float chat_input_padding_y = 10.0f; - Body url_selection_body(this, font.get(), bold_font.get(), cjk_font.get(), loading_icon); - auto launch_url = [this, &video_page, &redraw](const std::string &url) mutable { if(url.empty()) return; @@ -3520,6 +3416,9 @@ namespace QuickMedia { }; auto add_new_messages_to_current_room = [this, &tabs, &selected_tab, ¤t_room](Messages &messages) { + if(messages.empty()) + return; + int num_items = tabs[MESSAGES_TAB_INDEX].body->items.size(); bool scroll_to_end = num_items == 0; if(tabs[MESSAGES_TAB_INDEX].body->is_selected_item_last_visible_item() && selected_tab == MESSAGES_TAB_INDEX) @@ -3536,48 +3435,68 @@ namespace QuickMedia { } }; - auto add_new_rooms = [&tabs, ¤t_room, &room_search_bar, &room_name_text](Rooms &rooms) { - if(rooms.empty()) - return; + auto display_url_or_image = [this, &selected_tab, &redraw, &video_page, &launch_url, &chat_state, &url_selection_body](BodyItem *selected) { + if(!selected) + return false; - std::string search_filter_text = room_search_bar.get_text(); - - for(size_t i = 0; i < rooms.size(); ++i) { - auto &room = rooms[i]; - std::string room_name = room->get_name(); - if(room_name.empty()) - room_name = room->id; - - auto body_item = BodyItem::create(std::move(room_name)); - body_item->thumbnail_url = room->get_avatar_url(); - body_item->userdata = room; // Note: this has to be valid as long as the room list is valid! - body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; - body_item->thumbnail_size = sf::Vector2i(32, 32); - tabs[ROOMS_TAB_INDEX].body->filter_search_fuzzy_item(search_filter_text, body_item.get()); - tabs[ROOMS_TAB_INDEX].body->items.push_back(body_item); - room->userdata = body_item.get(); - } + Message *selected_item_message = nullptr; + if(selected_tab == MESSAGES_TAB_INDEX) { + selected_item_message = static_cast(selected->userdata); + } else if(selected_tab == PINNED_TAB_INDEX && static_cast(selected->userdata)->status == FetchStatus::FINISHED_LOADING) { + selected_item_message = static_cast(selected->userdata)->message; + } + + if(selected_item_message) { + MessageType message_type = selected_item_message->type; + std::string *selected_url = &selected->url; + if(!selected_url->empty()) { + if(message_type == MessageType::VIDEO || message_type == MessageType::IMAGE || message_type == MessageType::AUDIO) { + page_stack.push(PageType::CHAT); + watched_videos.clear(); + current_page = PageType::VIDEO_CONTENT; + bool is_audio = (message_type == MessageType::AUDIO); + bool prev_no_video = no_video; + no_video = is_audio; + // TODO: Add title + video_content_page(video_page.get(), *selected_url, "No title"); + no_video = prev_no_video; + redraw = true; + return true; + } - if(current_room) - return; + launch_url(*selected_url); + return true; + } + } - current_room = rooms[0]; - room_name_text.setString(static_cast(current_room->userdata)->get_title()); + // TODO: If content type is a file, show file-manager prompt where it should be saved and asynchronously save it instead + std::vector urls; + extract_urls(selected->get_description(), urls); + if(urls.size() == 1) { + launch_url(urls[0]); + return true; + } else if(urls.size() > 1) { + chat_state = ChatState::URL_SELECTION; + url_selection_body.clear_items(); + for(const std::string &url : urls) { + auto body_item = BodyItem::create(url); + url_selection_body.items.push_back(std::move(body_item)); + } + return true; + } + return false; }; float tab_shade_height = 0.0f; + bool frame_skip_text_entry = false; + + SyncData sync_data; while (current_page == PageType::CHAT) { sf::Int32 frame_time_ms = frame_timer.restart().asMilliseconds(); while (window.pollEvent(event)) { base_event_handler(event, PageType::EXIT, tabs[selected_tab].body.get(), nullptr, false, false); - if(selected_tab == ROOMS_TAB_INDEX) { - if(event.type == sf::Event::TextEntered) - room_search_bar.onTextEntered(event.text.unicode); - room_search_bar.on_event(event); - } - if(event.type == sf::Event::GainedFocus) { is_window_focused = true; redraw = true; @@ -3620,7 +3539,7 @@ namespace QuickMedia { tabs[selected_tab].body->select_next_page(); } else if(event.key.code == sf::Keyboard::End) { tabs[selected_tab].body->select_last_item(); - } else if((event.key.code == sf::Keyboard::Left) && synced && selected_tab > 0) { + } else if((event.key.code == sf::Keyboard::Left) && selected_tab > 0) { tabs[selected_tab].body->clear_cache(); --selected_tab; read_marker_timer.restart(); @@ -3630,7 +3549,7 @@ namespace QuickMedia { typing = false; typing_futures.push_back(std::async(typing_async_func, false, current_room)); } - } else if((event.key.code == sf::Keyboard::Right) && synced && selected_tab < (int)tabs.size() - 1) { + } else if((event.key.code == sf::Keyboard::Right) && selected_tab < (int)tabs.size() - 1) { tabs[selected_tab].body->clear_cache(); ++selected_tab; read_marker_timer.restart(); @@ -3640,50 +3559,93 @@ namespace QuickMedia { typing = false; typing_futures.push_back(std::async(typing_async_func, false, current_room)); } + } else if(event.key.code == sf::Keyboard::Escape) { + goto chat_page_end; } - if(selected_tab == MESSAGES_TAB_INDEX && event.key.code == sf::Keyboard::Enter) { + if((selected_tab == MESSAGES_TAB_INDEX || selected_tab == PINNED_TAB_INDEX) && event.key.code == sf::Keyboard::Enter) { BodyItem *selected = tabs[selected_tab].body->get_selected(); if(selected) { - MessageType message_type = static_cast(selected->userdata)->type; - std::string selected_url = selected->url; - if(selected_url.empty() && selected->embedded_item) { - selected_url = selected->embedded_item->url; - message_type = static_cast(selected->embedded_item->userdata)->type; + if(!display_url_or_image(selected)) + display_url_or_image(selected->embedded_item.get()); + } + } + + if(selected_tab == MESSAGES_TAB_INDEX && current_room) { + if(event.key.code == sf::Keyboard::U) { + frame_skip_text_entry = true; + new_page = PageType::FILE_MANAGER; + chat_input.set_editable(false); + } + + if(event.key.code == sf::Keyboard::M) { + frame_skip_text_entry = true; + chat_input.set_editable(true); + chat_state = ChatState::TYPING_MESSAGE; + } + + if(event.key.control && event.key.code == sf::Keyboard::V) { + frame_skip_text_entry = true; + // TODO: Make asynchronous. + // TODO: Upload multiple files. + std::string err_msg; + if(matrix->post_file(current_room, sf::Clipboard::getString(), err_msg) != PluginResult::OK) { + std::string desc = "Failed to upload media to room, error: " + err_msg; + show_notification("QuickMedia", desc.c_str(), Urgency::CRITICAL); } - if(!selected_url.empty()) { - if(message_type == MessageType::VIDEO || message_type == MessageType::IMAGE || message_type == MessageType::AUDIO) { - page_stack.push(PageType::CHAT); - watched_videos.clear(); - current_page = PageType::VIDEO_CONTENT; - bool is_audio = (message_type == MessageType::AUDIO); - bool prev_no_video = no_video; - no_video = is_audio; - // TODO: Add title - video_content_page(video_page.get(), selected_url, "No title"); - no_video = prev_no_video; - redraw = true; - continue; - } + } + + if(event.key.code == sf::Keyboard::R) { + frame_skip_text_entry = true; + std::shared_ptr selected = tabs[selected_tab].body->get_selected_shared(); + if(selected) { + chat_state = ChatState::REPLYING; + currently_operating_on_item = selected; + chat_input.set_editable(true); + replying_to_text.setString("Replying to:"); + } else { + // TODO: Show inline notification + show_notification("QuickMedia", "No message selected for replying"); + } + } - launch_url(selected_url); - continue; + if(event.key.code == sf::Keyboard::E) { + frame_skip_text_entry = true; + std::shared_ptr selected = tabs[selected_tab].body->get_selected_shared(); + if(selected) { + if(!selected->url.empty()) { // cant edit messages that are image/video posts + // TODO: Show inline notification + show_notification("QuickMedia", "You can only edit messages with no file attached to it"); + } else if(!matrix->was_message_posted_by_me(selected->userdata)) { + // TODO: Show inline notification + show_notification("QuickMedia", "You can't edit a message that was posted by somebody else"); + } else { + chat_state = ChatState::EDITING; + currently_operating_on_item = selected; + chat_input.set_editable(true); + chat_input.set_text(selected->get_description()); // TODO: Description? it may change in the future, in which case this should be edited + chat_input.move_caret_to_end(); + replying_to_text.setString("Editing message:"); + } + } else { + // TODO: Show inline notification + show_notification("QuickMedia", "No message selected for editing"); } + } - // TODO: If content type is a file, show file-manager prompt where it should be saved and asynchronously save it instead - std::vector urls; - extract_urls(selected->get_description(), urls); - if(selected->embedded_item) - extract_urls(selected->embedded_item->get_description(), urls); - if(urls.size() == 1) { - launch_url(urls[0]); - } else if(urls.size() > 1) { - chat_state = ChatState::URL_SELECTION; - url_selection_body.clear_items(); - for(const std::string &url : urls) { - auto body_item = BodyItem::create(url); - url_selection_body.items.push_back(std::move(body_item)); + if(event.key.code == sf::Keyboard::D) { + frame_skip_text_entry = true; + BodyItem *selected = tabs[selected_tab].body->get_selected(); + if(selected) { + // TODO: Make asynchronous + std::string err_msg; + if(matrix->delete_message(current_room, selected->userdata, err_msg) != PluginResult::OK) { + // TODO: Show inline notification + show_notification("QuickMedia", "Failed to delete message, reason: " + err_msg, Urgency::CRITICAL); } + } else { + // TODO: Show inline notification + show_notification("QuickMedia", "No message selected for deletion"); } } } @@ -3719,80 +3681,10 @@ namespace QuickMedia { continue; launch_url(selected_item->get_title()); } - } else if(event.type == sf::Event::KeyReleased && chat_state == ChatState::NAVIGATING && selected_tab == MESSAGES_TAB_INDEX && current_room) { - if(event.key.code == sf::Keyboard::U) { - new_page = PageType::FILE_MANAGER; - chat_input.set_editable(false); - } - - if(event.key.code == sf::Keyboard::M) { - chat_input.set_editable(true); - chat_state = ChatState::TYPING_MESSAGE; - } - - if(event.key.control && event.key.code == sf::Keyboard::V) { - // TODO: Make asynchronous. - // TODO: Upload multiple files. - std::string err_msg; - if(matrix->post_file(current_room, sf::Clipboard::getString(), err_msg) != PluginResult::OK) { - std::string desc = "Failed to upload media to room, error: " + err_msg; - show_notification("QuickMedia", desc.c_str(), Urgency::CRITICAL); - } - } - - if(event.key.code == sf::Keyboard::R) { - std::shared_ptr selected = tabs[selected_tab].body->get_selected_shared(); - if(selected) { - chat_state = ChatState::REPLYING; - currently_operating_on_item = selected; - chat_input.set_editable(true); - replying_to_text.setString("Replying to:"); - } else { - // TODO: Show inline notification - show_notification("QuickMedia", "No message selected for replying"); - } - } - - if(event.key.code == sf::Keyboard::E) { - std::shared_ptr selected = tabs[selected_tab].body->get_selected_shared(); - if(selected) { - if(!selected->url.empty()) { // cant edit messages that are image/video posts - // TODO: Show inline notification - show_notification("QuickMedia", "You can only edit messages with no file attached to it"); - } else if(!matrix->was_message_posted_by_me(selected->userdata)) { - // TODO: Show inline notification - show_notification("QuickMedia", "You can't edit a message that was posted by somebody else"); - } else { - chat_state = ChatState::EDITING; - currently_operating_on_item = selected; - chat_input.set_editable(true); - chat_input.set_text(selected->get_description()); // TODO: Description? it may change in the future, in which case this should be edited - chat_input.move_caret_to_end(); - replying_to_text.setString("Editing message:"); - } - } else { - // TODO: Show inline notification - show_notification("QuickMedia", "No message selected for editing"); - } - } - - if(event.key.code == sf::Keyboard::D) { - BodyItem *selected = tabs[selected_tab].body->get_selected(); - if(selected) { - // TODO: Make asynchronous - std::string err_msg; - if(matrix->delete_message(current_room, selected->userdata, err_msg) != PluginResult::OK) { - // TODO: Show inline notification - show_notification("QuickMedia", "Failed to delete message, reason: " + err_msg, Urgency::CRITICAL); - } - } else { - // TODO: Show inline notification - show_notification("QuickMedia", "No message selected for deletion"); - } - } } - if((chat_state == ChatState::TYPING_MESSAGE || chat_state == ChatState::REPLYING || chat_state == ChatState::EDITING) && selected_tab == MESSAGES_TAB_INDEX) { + if((chat_state == ChatState::TYPING_MESSAGE || chat_state == ChatState::REPLYING || chat_state == ChatState::EDITING) && selected_tab == MESSAGES_TAB_INDEX && !frame_skip_text_entry) { + frame_skip_text_entry = false; if(event.type == sf::Event::TextEntered) { //chat_input.onTextEntered(event.text.unicode); // TODO: Also show typing event when ctrl+v pasting? @@ -3819,6 +3711,9 @@ namespace QuickMedia { chat_input.process_event(event); } } + frame_skip_text_entry = false; + + chat_page->update(); switch(new_page) { case PageType::FILE_MANAGER: { @@ -3833,7 +3728,7 @@ namespace QuickMedia { file_manager_tabs.push_back(Tab{std::move(file_manager_body), std::move(file_manager_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); selected_files.clear(); - page_loop(std::move(file_manager_tabs)); + page_loop(file_manager_tabs); if(selected_files.empty()) { fprintf(stderr, "No files selected!\n"); @@ -3854,19 +3749,7 @@ namespace QuickMedia { break; } case PageType::CHAT_LOGIN: { - new_page = PageType::CHAT; - matrix->logout(); - tabs[MESSAGES_TAB_INDEX].body->clear_cache(); - // TODO: Instead of doing this, exit this current function and navigate to chat login page instead. - // This doesn't currently work because at the end of this function there are futures that need to wait - // and one of them is /sync, which has a timeout of 30 seconds. That timeout has to be killed somehow. - //delete current_plugin; - //current_plugin = new Matrix(); - current_page = PageType::CHAT_LOGIN; - chat_login_page(); - if(current_page == PageType::CHAT) - chat_page(); - exit(0); + abort(); break; } default: @@ -3918,8 +3801,6 @@ namespace QuickMedia { float room_name_padding_y = 0.0f; if(selected_tab == MESSAGES_TAB_INDEX || selected_tab == PINNED_TAB_INDEX) room_name_padding_y = room_name_total_height; - else if(selected_tab == ROOMS_TAB_INDEX) - room_name_padding_y = room_search_bar.getBottomWithoutShadow(); chat_input_height_full = chat_input.get_height() + chat_input_padding_y * 2.0f; if(selected_tab != MESSAGES_TAB_INDEX || selected_tab == PINNED_TAB_INDEX) @@ -3933,17 +3814,12 @@ namespace QuickMedia { if(redraw) { redraw = false; - room_search_bar.onWindowResize(window_size); float room_name_padding_y = 0.0f; float padding_bottom = 0.0f; if(selected_tab == MESSAGES_TAB_INDEX || selected_tab == PINNED_TAB_INDEX) { room_name_padding_y = 10.0f + room_name_total_height; tab_vertical_offset = 10.0f; - } else if(selected_tab == ROOMS_TAB_INDEX) { - room_name_padding_y = room_search_bar.getBottomWithoutShadow(); - tab_vertical_offset = 0.0f; - padding_bottom = 10.0f; } tab_shade_height = tab_spacer_height + std::floor(tab_vertical_offset) + tab_height + room_name_padding_y + padding_bottom; @@ -3971,42 +3847,11 @@ namespace QuickMedia { logo_sprite.setPosition(logo_padding_x, std::floor(window_size.y - chat_input_shade.getSize().y * 0.5f - logo_size.y * 0.5f)); } - room_search_bar.update(); - - if(!sync_running && sync_timer.getElapsedTime().asMilliseconds() >= sync_min_time_ms) { - fprintf(stderr, "Time since last sync: %d ms\n", sync_timer.getElapsedTime().asMilliseconds()); - sync_min_time_ms = 1000; - sync_running = true; - sync_timer.restart(); - sync_future = std::async(std::launch::async, [this]() { - SyncFutureResult result; - if(matrix->sync(result.room_sync_data) == PluginResult::OK) { - fprintf(stderr, "Synced matrix\n"); - matrix->get_room_join_updates(result.rooms); - } else { - fprintf(stderr, "Failed to sync matrix\n"); - } - - return result; - }); - } - - if(is_future_ready(sync_future)) { - SyncFutureResult sync_result = sync_future.get(); - - add_new_rooms(sync_result.rooms); - - auto room_messages_it = sync_result.room_sync_data.find(current_room); - if(room_messages_it != sync_result.room_sync_data.end()) { - add_new_messages_to_current_room(room_messages_it->second.messages); - modify_related_messages_in_current_room(room_messages_it->second.messages); - process_new_pinned_events(room_messages_it->second.pinned_events); - } - - process_new_room_messages(sync_result.room_sync_data, !synced); - sync_running = false; - synced = true; - } + sync_data.messages.clear(); + matrix->get_room_sync_data(current_room, sync_data); + add_new_messages_to_current_room(sync_data.messages); + modify_related_messages_in_current_room(sync_data.messages); + process_pinned_events(sync_data.pinned_events); if(is_future_ready(set_read_marker_future)) { set_read_marker_future.get(); @@ -4045,6 +3890,7 @@ namespace QuickMedia { if(message) { *fetch_body_item = *message_to_body_item(message.get(), matrix->get_me(current_room).get()); event_data->status = FetchStatus::FINISHED_LOADING; + event_data->message = message.get(); fetch_body_item->userdata = event_data; } else { fetch_body_item->set_description("Failed to load message!"); @@ -4091,8 +3937,6 @@ namespace QuickMedia { } room_name_text.setPosition(body_pos.x + room_name_text_offset_x, room_name_text_padding_y + 4.0f); window.draw(room_name_text); - } else if(selected_tab == ROOMS_TAB_INDEX) { - room_search_bar.draw(window, false); } gradient_points[0].position.x = 0.0f; @@ -4136,7 +3980,7 @@ namespace QuickMedia { const float margin = 5.0f; const float replying_to_text_height = replying_to_text.getLocalBounds().height + margin; - const float item_height = std::min(body_size.y - replying_to_text_height - margin, tabs[MESSAGES_TAB_INDEX].body->get_item_height(currently_operating_on_item.get()) + margin); + const float item_height = std::min(body_size.y - replying_to_text_height - margin, tabs[MESSAGES_TAB_INDEX].body->get_item_height(currently_operating_on_item.get(), body_size.x) + margin); sf::RectangleShape overlay(sf::Vector2f(window_size.x, window_size.y - tab_shade_height - chat_input_height_full)); overlay.setPosition(0.0f, tab_shade_height); @@ -4176,13 +4020,6 @@ namespace QuickMedia { } } - // TODO: Cache /sync, then we wont only see loading text - if(!synced) { - sf::Text loading_text("Loading...", *font, 24); - loading_text.setPosition(body_pos.x + body_size.x * 0.5f - loading_text.getLocalBounds().width * 0.5f, body_pos.y + body_size.y * 0.5f - loading_text.getLocalBounds().height * 0.5f); - window.draw(loading_text); - } - if(selected_tab == MESSAGES_TAB_INDEX && current_room) { BodyItem *last_visible_item = tabs[selected_tab].body->get_last_fully_visible_item(); if(is_window_focused && chat_state != ChatState::URL_SELECTION && current_room && last_visible_item && !setting_read_marker && read_marker_timer.getElapsedTime().asMilliseconds() >= read_marker_timeout_ms) { @@ -4210,6 +4047,21 @@ namespace QuickMedia { window.display(); } - exit(0); // Ignore futures and quit immediately + chat_page_end: + // TODO: Cancel these instead + if(set_read_marker_future.valid()) + set_read_marker_future.get(); + if(previous_messages_future.valid()) + previous_messages_future.get(); + if(fetch_message_future.valid()) + fetch_message_future.get(); + for(auto &typing_future : typing_futures) { + if(typing_future.valid()) + typing_future.get(); + } + + for(auto &body_item : tabs[PINNED_TAB_INDEX].body->items) { + delete (PinnedEventData*)body_item->userdata; + } } } diff --git a/src/plugins/Mangadex.cpp b/src/plugins/Mangadex.cpp index a52788d..a8318e8 100644 --- a/src/plugins/Mangadex.cpp +++ b/src/plugins/Mangadex.cpp @@ -210,7 +210,7 @@ namespace QuickMedia { } PluginResult MangadexChaptersPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { - result_tabs.push_back(Tab{create_body(), std::make_unique(program, content_title, title, url), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique(program, content_title, title, url), nullptr}); return PluginResult::OK; } diff --git a/src/plugins/Manganelo.cpp b/src/plugins/Manganelo.cpp index 7f0a2f9..f87081c 100644 --- a/src/plugins/Manganelo.cpp +++ b/src/plugins/Manganelo.cpp @@ -121,7 +121,7 @@ namespace QuickMedia { } PluginResult ManganeloChaptersPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { - result_tabs.push_back(Tab{create_body(), std::make_unique(program, content_title, title, url), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique(program, content_title, title, url), nullptr}); return PluginResult::OK; } diff --git a/src/plugins/Mangatown.cpp b/src/plugins/Mangatown.cpp index 89bf447..1d4d71a 100644 --- a/src/plugins/Mangatown.cpp +++ b/src/plugins/Mangatown.cpp @@ -110,7 +110,7 @@ namespace QuickMedia { } PluginResult MangatownChaptersPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { - result_tabs.push_back(Tab{create_body(), std::make_unique(program, content_title, title, url), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique(program, content_title, title, url), nullptr}); return PluginResult::OK; } diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 99d6bed..ede0821 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -2,26 +2,22 @@ #include "../../include/Storage.hpp" #include "../../include/StringUtils.hpp" #include "../../include/NetUtils.hpp" +#include "../../include/Notification.hpp" #include #include #include #include #include +#include "../../include/QuickMedia.hpp" // TODO: Update avatar/display name when its changed in the room/globally. -// Send read receipt to server and receive notifications in /sync and show the notifications. -// Delete messages. -// Edit messages. // Show images/videos inline. // TODO: Verify if buffer of size 512 is enough for endpoints -// TODO: POST /_matrix/client/r0/rooms/{roomId}/read_markers after 5 seconds of receiving a message when the client is focused -// to mark messages as read -// When reaching top/bottom message, show older/newer messages. // Remove older messages (outside screen) to save memory. Reload them when the selected body item is the top/bottom one. - -// TODO: Verify if this class really is thread-safe (for example room data fields, user fields, message fields; etc that are updated in /sync) +// TODO: Use lazy load filter for /sync (filter=0, required GET first to check if its available). If we use filter for sync then we also need to modify Matrix::get_message_by_id to parse state, etc. static const char* SERVICE_NAME = "matrix"; +static const char* OTHERS_ROOM_TAG = "tld.name.others"; static rapidjson::Value nullValue(rapidjson::kNullType); static const rapidjson::Value& GetMember(const rapidjson::Value &obj, const char *key) { @@ -31,6 +27,47 @@ static const rapidjson::Value& GetMember(const rapidjson::Value &obj, const char return nullValue; } +static std::string capitalize(const std::string &str) { + if(str.size() >= 1) + return (char)std::toupper(str[0]) + str.substr(1); + else + return ""; +} + +// TODO: According to spec: "Any tag in the tld.name.* form but not matching the namespace of the current client should be ignored", +// should we follow this? +static std::string tag_get_name(const std::string &tag) { + if(tag.size() >= 2 && memcmp(tag.data(), "m.", 2) == 0) { + if(strcmp(tag.c_str() + 2, "favourite") == 0) + return "Favorites"; + else if(strcmp(tag.c_str() + 2, "lowpriority") == 0) + return "Low priority"; + else if(strcmp(tag.c_str() + 2, "server_notice") == 0) + return "Server notice"; + else + return capitalize(tag.substr(2)); + } else if(tag.size() >= 2 && memcmp(tag.data(), "u.", 2) == 0) { + return capitalize(tag.substr(2)); + } else if(tag.size() >= 9 && memcmp(tag.data(), "tld.name.", 9) == 0) { + return capitalize(tag.substr(9)); + } else { + return ""; + } +} + +static std::string extract_first_line_elipses(const std::string &str, size_t max_length) { + size_t index = str.find('\n'); + if(index == std::string::npos) { + if(str.size() > max_length) + return str.substr(0, max_length) + " (...)"; + return str; + } else if(index == 0) { + return ""; + } else { + return str.substr(0, std::min(index, max_length)) + " (...)"; + } +} + namespace QuickMedia { std::shared_ptr RoomData::get_user_by_id(const std::string &user_id) { std::lock_guard lock(room_mutex); @@ -75,11 +112,6 @@ namespace QuickMedia { } } - void RoomData::append_pinned_events(std::vector new_pinned_events) { - std::lock_guard lock(room_mutex); - pinned_events.insert(pinned_events.end(), new_pinned_events.begin(), new_pinned_events.end()); - } - std::shared_ptr RoomData::get_message_by_id(const std::string &id) { std::lock_guard lock(room_mutex); auto message_it = message_by_event_id.find(id); @@ -160,57 +192,368 @@ namespace QuickMedia { return avatar_url; } - PluginResult Matrix::sync(RoomSyncData &room_sync_data) { - std::vector additional_args = { - { "-H", "Authorization: Bearer " + access_token }, - { "-m", "35" } - }; + void RoomData::set_pinned_events(std::vector new_pinned_events) { + std::lock_guard lock(room_mutex); + pinned_events = std::move(new_pinned_events); + pinned_events_updated = true; + } - char url[512]; - if(next_batch.empty()) - snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=0", homeserver.c_str()); - else - snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=30000&since=%s", homeserver.c_str(), next_batch.c_str()); + std::set& RoomData::get_tags_unsafe() { + return tags; + } - 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); + MatrixQuickMedia::MatrixQuickMedia(Program *program, Matrix *matrix, MatrixRoomsPage *rooms_page, MatrixRoomTagsPage *room_tags_page) : program(program), matrix(matrix), rooms_page(rooms_page), room_tags_page(room_tags_page) { + rooms_page->matrix_delegate = this; + room_tags_page->matrix_delegate = this; + } - PluginResult result = sync_response_to_body_items(json_root, room_sync_data); - if(result != PluginResult::OK) - return result; + void MatrixQuickMedia::room_create(RoomData *room) { + std::string room_name = room->get_name(); + if(room_name.empty()) + room_name = room->id; - const rapidjson::Value &next_batch_json = GetMember(json_root, "next_batch"); - if(next_batch_json.IsString()) { - next_batch = next_batch_json.GetString(); - fprintf(stderr, "Matrix: next batch: %s\n", next_batch.c_str()); - } else { - fprintf(stderr, "Matrix: missing next batch\n"); + auto body_item = BodyItem::create(std::move(room_name)); + body_item->url = room->id; + body_item->thumbnail_url = room->get_avatar_url(); + body_item->userdata = room; // Note: this has to be valid as long as the room list is valid! + body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; + body_item->thumbnail_size = sf::Vector2i(32, 32); + room->userdata = body_item.get(); + room_body_items.push_back(body_item); + rooms_page->add_body_item(body_item); + room_body_item_by_room[room] = body_item; + } + + void MatrixQuickMedia::room_add_tag(RoomData *room, const std::string &tag) { + room_tags_page->add_room_body_item_to_tag(room_body_item_by_room[room], tag); + } + + void MatrixQuickMedia::room_remove_tag(RoomData *room, const std::string &tag) { + room_tags_page->remove_room_body_item_from_tag(room_body_item_by_room[room], tag); + } + + void MatrixQuickMedia::room_add_new_messages(RoomData *room, const Messages &messages, bool is_initial_sync) { + std::lock_guard lock(pending_room_messages_mutex); + auto &room_messages_data = pending_room_messages[room]; + room_messages_data.messages.insert(room_messages_data.messages.end(), messages.begin(), messages.end()); + room_messages_data.is_initial_sync = is_initial_sync; + } + + static int find_top_body_position_for_unread_room(const BodyItems &room_body_items, BodyItem *item_to_swap) { + for(int i = 0; i < (int)room_body_items.size(); ++i) { + const auto &body_item = room_body_items[i]; + if(static_cast(body_item->userdata)->last_message_read || body_item.get() == item_to_swap) + return i; + } + return -1; + } + + static int find_top_body_position_for_mentioned_room(const BodyItems &room_body_items, BodyItem *item_to_swap) { + for(int i = 0; i < (int)room_body_items.size(); ++i) { + const auto &body_item = room_body_items[i]; + if(!static_cast(body_item->userdata)->has_unread_mention || body_item.get() == item_to_swap) + return i; + } + return -1; + } + + static void sort_room_body_items(std::vector> &room_body_items) { + std::sort(room_body_items.begin(), room_body_items.end(), [](const std::shared_ptr &body_item1, const std::shared_ptr &body_item2) { + RoomData *room1 = static_cast(body_item1->userdata); + RoomData *room2 = static_cast(body_item2->userdata); + int room1_focus_sum = (int)room1->has_unread_mention + (int)!room1->last_message_read; + int room2_focus_sum = (int)room2->has_unread_mention + (int)!room2->last_message_read; + return room1_focus_sum > room2_focus_sum; + }); + } + + void MatrixQuickMedia::update(MatrixPageType page_type) { + std::lock_guard lock(pending_room_messages_mutex); + bool is_window_focused = program->is_window_focused(); + RoomData *current_room = program->get_current_chat_room(); + for(auto &it : pending_room_messages) { + RoomData *room = it.first; + auto &messages = it.second.messages; + bool is_initial_sync = it.second.is_initial_sync; + //auto &room_body_item = room_body_item_by_room[room]; + //std::string room_desc = matrix->message_get_author_displayname(it.second.back().get()) + ": " + extract_first_line_elipses(it.second.back()->body, 150); + //room_body_item->set_description(std::move(room_desc)); + + for(auto &message : messages) { + if(message->mentions_me) { + room->has_unread_mention = true; + // 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 || is_initial_sync || page_type == MatrixPageType::ROOM_LIST) + show_notification("QuickMedia matrix - " + matrix->message_get_author_displayname(message.get()) + " (" + room->get_name() + ")", message->body); + } + } + + std::shared_ptr me = matrix->get_me(room); + time_t read_marker_message_timestamp = 0; + if(me) { + auto read_marker_message = room->get_message_by_id(room->get_user_read_marker(me)); + if(read_marker_message) + read_marker_message_timestamp = read_marker_message->timestamp; + } + + // TODO: this wont always work because we dont display all types of messages from server, such as "joined", "left", "kicked", "banned", "changed avatar", "changed display name", etc. + // TODO: Binary search? + Message *last_unread_message = nullptr; + for(auto it = messages.rbegin(), end = messages.rend(); it != end; ++it) { + if((*it)->related_event_type != RelatedEventType::EDIT && (*it)->related_event_type != RelatedEventType::REDACTION && (*it)->timestamp > read_marker_message_timestamp) { + last_unread_message = (*it).get(); + break; + } + } + + BodyItem *room_body_item = static_cast(room->userdata); + assert(room_body_item); + + if(last_unread_message) { + std::string room_desc = "Unread: " + matrix->message_get_author_displayname(last_unread_message) + ": " + extract_first_line_elipses(last_unread_message->body, 150); + if(room->has_unread_mention) + room_desc += "\n** You were mentioned **"; // TODO: Better notification? + room_body_item->set_description(std::move(room_desc)); + room_body_item->set_title_color(sf::Color(255, 100, 100)); + room->last_message_read = false; + + rooms_page->move_room_to_top(room); + room_tags_page->move_room_to_top(room); + } else if(is_initial_sync) { + Message *last_message = nullptr; + for(auto it = messages.rbegin(), end = messages.rend(); it != end; ++it) { + if((*it)->related_event_type != RelatedEventType::EDIT && (*it)->related_event_type != RelatedEventType::REDACTION) { + last_message = (*it).get(); + break; + } + } + if(last_message) + room_body_item->set_description(matrix->message_get_author_displayname(last_message) + ": " + extract_first_line_elipses(last_message->body, 150)); + } } + pending_room_messages.clear(); + } + + MatrixRoomsPage::MatrixRoomsPage(Program *program, Body *body, std::string title, MatrixRoomTagsPage *room_tags_page) : Page(program), body(body), title(std::move(title)), room_tags_page(room_tags_page) { + if(room_tags_page) + room_tags_page->current_rooms_page = this; + } + + MatrixRoomsPage::~MatrixRoomsPage() { + if(room_tags_page) + room_tags_page->current_rooms_page = nullptr; + } + PluginResult MatrixRoomsPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { + (void)title; + auto chat_page = std::make_unique(program, url); + chat_page->matrix_delegate = matrix_delegate; + result_tabs.push_back(Tab{nullptr, std::move(chat_page), nullptr}); return PluginResult::OK; } - void Matrix::get_room_join_updates(Rooms &new_rooms) { - std::lock_guard lock(room_data_mutex); - size_t num_new_rooms = rooms.size() - room_list_read_index; - size_t new_rooms_prev_size = new_rooms.size(); - new_rooms.resize(new_rooms_prev_size + num_new_rooms); - for(size_t i = new_rooms_prev_size; i < new_rooms.size(); ++i) { - new_rooms[i] = rooms[room_list_read_index + i].get(); + void MatrixRoomsPage::update() { + { + std::lock_guard lock(mutex); + body->append_items(std::move(room_body_items)); + } + matrix_delegate->update(MatrixPageType::ROOM_LIST); + } + + void MatrixRoomsPage::add_body_item(std::shared_ptr body_item) { + std::lock_guard lock(mutex); + room_body_items.push_back(body_item); + } + + void MatrixRoomsPage::move_room_to_top(RoomData *room) { + // Swap order of rooms in body list to put rooms with mentions at the top and then unread messages and then all the other rooms + // TODO: Optimize with hash map instead of linear search? or cache the index + std::lock_guard lock(mutex); + BodyItem *room_body_item = static_cast(room->userdata); + int room_body_index = body->get_index_by_body_item(room_body_item); + if(room_body_index != -1) { + std::shared_ptr body_item = body->items[room_body_index]; + int body_swap_index = -1; + if(room->has_unread_mention) + body_swap_index = find_top_body_position_for_mentioned_room(body->items, body_item.get()); + else if(!room->last_message_read) + body_swap_index = find_top_body_position_for_unread_room(body->items, body_item.get()); + if(body_swap_index != -1 && body_swap_index != room_body_index) { + body->items.erase(body->items.begin() + room_body_index); + if(body_swap_index < room_body_index) + body->items.insert(body->items.begin() + body_swap_index, std::move(body_item)); + else + body->items.insert(body->items.begin() + (body_swap_index - 1), std::move(body_item)); + } + } + } + + PluginResult MatrixRoomTagsPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { + (void)title; + std::lock_guard lock(mutex); + auto body = create_body(); + Body *body_ptr = body.get(); + TagData &tag_data = tag_body_items_by_name[url]; + body->items = tag_data.room_body_items; + sort_room_body_items(body->items); + auto rooms_page = std::make_unique(program, body_ptr, tag_data.tag_item->get_title(), this); + rooms_page->matrix_delegate = matrix_delegate; + result_tabs.push_back(Tab{std::move(body), std::move(rooms_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + return PluginResult::OK; + } + + // TODO: Also add/remove body items to above body (in submit) + void MatrixRoomTagsPage::update() { + { + std::lock_guard lock(mutex); + for(auto &it : remove_room_body_items_by_tags) { + auto tag_body_it = tag_body_items_by_name.find(it.first); + if(tag_body_it == tag_body_items_by_name.end()) + continue; + + for(auto &room_to_remove : it.second) { + auto room_body_item_it = std::find(tag_body_it->second.room_body_items.begin(), tag_body_it->second.room_body_items.end(), room_to_remove); + if(room_body_item_it != tag_body_it->second.room_body_items.end()) + tag_body_it->second.room_body_items.erase(room_body_item_it); + } + + if(tag_body_it->second.room_body_items.empty()) { + auto room_body_item_it = std::find(body->items.begin(), body->items.end(), tag_body_it->second.tag_item); + if(room_body_item_it != body->items.end()) + body->items.erase(room_body_item_it); + tag_body_items_by_name.erase(tag_body_it); + } + } + remove_room_body_items_by_tags.clear(); + + for(auto &it : add_room_body_items_by_tags) { + TagData *tag_data; + auto tag_body_it = tag_body_items_by_name.find(it.first); + if(tag_body_it == tag_body_items_by_name.end()) { + std::string tag_name = tag_get_name(it.first); + if(!tag_name.empty()) { + auto tag_body_item = BodyItem::create(std::move(tag_name)); + tag_body_item->url = it.first; + tag_body_items_by_name.insert(std::make_pair(it.first, TagData{tag_body_item, {}})); + // TODO: Sort by tag priority + body->items.push_back(tag_body_item); + tag_data = &tag_body_items_by_name[it.first]; + tag_data->tag_item = tag_body_item; + } + } else { + tag_data = &tag_body_it->second; + } + + for(auto &room_body_item : it.second) { + tag_data->room_body_items.push_back(room_body_item); + } + } + add_room_body_items_by_tags.clear(); + } + matrix_delegate->update(MatrixPageType::ROOM_LIST); + } + + void MatrixRoomTagsPage::add_room_body_item_to_tag(std::shared_ptr body_item, const std::string &tag) { + std::lock_guard lock(mutex); + add_room_body_items_by_tags[tag].push_back(body_item); + } + + void MatrixRoomTagsPage::remove_room_body_item_from_tag(std::shared_ptr body_item, const std::string &tag) { + std::lock_guard lock(mutex); + remove_room_body_items_by_tags[tag].push_back(body_item); + } + + void MatrixRoomTagsPage::move_room_to_top(RoomData *room) { + if(current_rooms_page) + current_rooms_page->move_room_to_top(room); + } + + void MatrixChatPage::update() { + matrix_delegate->update(MatrixPageType::CHAT); + } + + void Matrix::start_sync(MatrixDelegate *delegate) { + if(sync_running) + return; + + sync_running = true; + sync_thread = std::thread([this, delegate]() { + const rapidjson::Value *next_batch_json; + PluginResult result; + while(sync_running) { + std::vector additional_args = { + { "-H", "Authorization: Bearer " + access_token }, + { "-m", "35" } + }; + + char url[512]; + if(next_batch.empty()) + snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=0", homeserver.c_str()); + else + snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=30000&since=%s", homeserver.c_str(), next_batch.c_str()); + + rapidjson::Document json_root; + DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true); + if(download_result != DownloadResult::OK) { + fprintf(stderr, "Fetch response failed\n"); + goto sync_end; + } + + result = parse_sync_response(json_root, delegate); + if(result != PluginResult::OK) { + fprintf(stderr, "Failed to parse sync response\n"); + goto sync_end; + } + + next_batch_json = &GetMember(json_root, "next_batch"); + if(next_batch_json->IsString()) { + next_batch = next_batch_json->GetString(); + fprintf(stderr, "Matrix: next batch: %s\n", next_batch.c_str()); + } else { + fprintf(stderr, "Matrix: missing next batch\n"); + } + + sync_end: + if(sync_running) + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + }); + } + + void Matrix::stop_sync() { + // TODO: Kill the running download in |sync_thread| instead of waiting until sync returns (which can be up to 30 seconds) + sync_running = false; + if(sync_thread.joinable()) + sync_thread.join(); + } + + bool Matrix::is_initial_sync_finished() const { + return !next_batch.empty(); + } + + void Matrix::get_room_sync_data(RoomData *room, SyncData &sync_data) { + room->acquire_room_lock(); + auto &room_messages = room->get_messages_thread_unsafe(); + sync_data.messages.insert(sync_data.messages.end(), room_messages.begin() + room->messages_read_index, room_messages.end()); + room->messages_read_index = room_messages.size(); + if(room->pinned_events_updated) { + sync_data.pinned_events = room->get_pinned_events_unsafe(); + room->pinned_events_updated = false; } - room_list_read_index += num_new_rooms; + room->release_room_lock(); } void Matrix::get_all_synced_room_messages(RoomData *room, Messages &messages) { room->acquire_room_lock(); messages = room->get_messages_thread_unsafe(); + room->messages_read_index = messages.size(); room->release_room_lock(); } void Matrix::get_all_pinned_events(RoomData *room, std::vector &events) { room->acquire_room_lock(); events = room->get_pinned_events_unsafe(); + room->pinned_events_updated = false; room->release_room_lock(); } @@ -226,15 +569,69 @@ namespace QuickMedia { size_t num_messages_after = room->get_messages_thread_unsafe().size(); size_t num_new_messages = num_messages_after - num_messages_before; messages.insert(messages.end(), room->get_messages_thread_unsafe().begin(), room->get_messages_thread_unsafe().begin() + num_new_messages); + room->messages_read_index += num_new_messages; room->release_room_lock(); return PluginResult::OK; } - PluginResult Matrix::sync_response_to_body_items(const rapidjson::Document &root, RoomSyncData &room_sync_data) { + PluginResult Matrix::parse_sync_response(const rapidjson::Document &root, MatrixDelegate *delegate) { if(!root.IsObject()) return PluginResult::ERR; + const rapidjson::Value &account_data_json = GetMember(root, "account_data"); + std::optional> dm_rooms; + parse_sync_account_data(account_data_json, dm_rooms); + // TODO: Include "Direct messages" as a tag using |dm_rooms| above + const rapidjson::Value &rooms_json = GetMember(root, "rooms"); + parse_sync_room_data(rooms_json, delegate); + + return PluginResult::OK; + } + + PluginResult Matrix::parse_sync_account_data(const rapidjson::Value &account_data_json, std::optional> &dm_rooms) { + if(!account_data_json.IsObject()) + return PluginResult::OK; + + const rapidjson::Value &events_json = GetMember(account_data_json, "events"); + if(!events_json.IsArray()) + return PluginResult::OK; + + bool has_direct_rooms = false; + std::set dm_rooms_tmp; + for(const rapidjson::Value &event_item_json : events_json.GetArray()) { + if(!event_item_json.IsObject()) + continue; + + const rapidjson::Value &type_json = GetMember(event_item_json, "type"); + if(!type_json.IsString() || strcmp(type_json.GetString(), "m.direct") != 0) + continue; + + const rapidjson::Value &content_json = GetMember(event_item_json, "content"); + if(!content_json.IsObject()) + continue; + + has_direct_rooms = true; + for(auto const &it : content_json.GetObject()) { + if(!it.value.IsArray()) + continue; + + for(const rapidjson::Value &room_id_json : it.value.GetArray()) { + if(!room_id_json.IsString()) + continue; + + dm_rooms_tmp.insert(std::string(room_id_json.GetString(), room_id_json.GetStringLength())); + } + } + } + + if(has_direct_rooms) + dm_rooms = std::move(dm_rooms_tmp); + + return PluginResult::OK; + } + + PluginResult Matrix::parse_sync_room_data(const rapidjson::Value &rooms_json, MatrixDelegate *delegate) { if(!rooms_json.IsObject()) return PluginResult::OK; @@ -252,12 +649,14 @@ namespace QuickMedia { std::string room_id_str = room_id.GetString(); + bool is_new_room = false; RoomData *room = get_room_by_id(room_id_str); if(!room) { auto new_room = std::make_unique(); new_room->id = room_id_str; room = new_room.get(); add_room(std::move(new_room)); + is_new_room = true; } const rapidjson::Value &state_json = GetMember(it.value, "state"); @@ -265,7 +664,7 @@ namespace QuickMedia { const rapidjson::Value &events_json = GetMember(state_json, "events"); events_add_user_info(events_json, room); events_set_room_name(events_json, room); - events_add_pinned_events(events_json, room, room_sync_data); + events_add_pinned_events(events_json, room); } const rapidjson::Value &ephemeral_json = GetMember(it.value, "ephemeral"); @@ -296,7 +695,8 @@ namespace QuickMedia { const rapidjson::Value &events_json = GetMember(ephemeral_json, "events"); events_add_user_read_markers(events_json, room); } - events_add_messages(events_json, room, MessageDirection::AFTER, &room_sync_data, has_unread_notifications); + events_add_messages(events_json, room, MessageDirection::AFTER, delegate, has_unread_notifications); + events_add_pinned_events(events_json, room); } else { if(ephemeral_json.IsObject()) { const rapidjson::Value &events_json = GetMember(ephemeral_json, "events"); @@ -304,10 +704,23 @@ namespace QuickMedia { } } + if(is_new_room) + delegate->room_create(room); + const rapidjson::Value &account_data_json = GetMember(it.value, "account_data"); if(account_data_json.IsObject()) { const rapidjson::Value &events_json = GetMember(account_data_json, "events"); - events_add_room_to_tags(events_json, room); + events_add_room_to_tags(events_json, room, delegate); + } + + if(is_new_room) { + room->acquire_room_lock(); + std::set &room_tags = room->get_tags_unsafe(); + if(room_tags.empty()) { + room_tags.insert(OTHERS_ROOM_TAG); + delegate->room_add_tag(room, OTHERS_ROOM_TAG); + } + room->release_room_lock(); } } @@ -416,7 +829,7 @@ namespace QuickMedia { auto user = room_data->get_user_by_id(user_id_json.GetString()); if(!user) { - fprintf(stderr, "Receipt read receipt for unknown user: %s, ignoring...\n", user_id_json.GetString()); + fprintf(stderr, "Read receipt for unknown user: %s, ignoring...\n", user_id_json.GetString()); continue; } @@ -492,16 +905,28 @@ namespace QuickMedia { return false; } + static size_t string_find_case_insensitive(const char *haystack, size_t index, size_t length, const std::string &needle) { + const char *haystack_end = haystack + length; + auto it = std::search(haystack + index, haystack_end, needle.begin(), needle.end(), + [](char c1, char c2) { + return std::toupper(c1) == std::toupper(c2); + }); + if(it != haystack_end) + return it - haystack; + else + return std::string::npos; + } + // TODO: Do not show notification if mention is a reply to somebody else that replies to me? also dont show notification everytime a mention is edited bool message_contains_user_mention(const std::string &msg, const std::string &username) { - if(msg.empty()) + if(msg.empty() || username.empty()) return false; size_t index = 0; while(index < msg.size()) { - size_t found_index = msg.find(username, index); + size_t found_index = string_find_case_insensitive(&msg[0], index, msg.size(), username); if(found_index == std::string::npos) - return false; + break; char prev_char = ' '; if(found_index > 0) @@ -514,16 +939,17 @@ namespace QuickMedia { if(is_username_seperating_character(prev_char) && is_username_seperating_character(next_char)) return true; - index += username.size(); + index = found_index + username.size(); } return false; } - void Matrix::events_add_messages(const rapidjson::Value &events_json, RoomData *room_data, MessageDirection message_dir, RoomSyncData *room_sync_data, bool has_unread_notifications) { + void Matrix::events_add_messages(const rapidjson::Value &events_json, RoomData *room_data, MessageDirection message_dir, MatrixDelegate *delegate, bool has_unread_notifications) { if(!events_json.IsArray()) return; + // TODO: Preallocate std::vector> new_messages; auto me = get_me(room_data); @@ -536,11 +962,6 @@ namespace QuickMedia { if(new_messages.empty()) return; - // TODO: Add directly to this instead when set? otherwise add to new_messages - if(room_sync_data) - (*room_sync_data)[room_data].messages = new_messages; - - // TODO: Loop and std::move instead? doesn't insert create copies? if(message_dir == MessageDirection::BEFORE) { room_data->prepend_messages_reverse(new_messages); } else if(message_dir == MessageDirection::AFTER) { @@ -560,6 +981,9 @@ namespace QuickMedia { if(has_unread_notifications && me && message->timestamp > read_marker_message_timestamp) message->mentions_me = message_contains_user_mention(message->body, me->display_name) || message_contains_user_mention(message->body, me->user_id) || message_contains_user_mention(message->body, "@room"); } + + if(delegate) + delegate->room_add_new_messages(room_data, new_messages, next_batch.empty()); } std::shared_ptr Matrix::parse_message_event(const rapidjson::Value &event_item_json, RoomData *room_data) { @@ -706,6 +1130,9 @@ namespace QuickMedia { message->type = MessageType::TEXT; message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver); message_content_extract_thumbnail_size(*content_json, message->thumbnail_size); + } else if(strcmp(content_type.GetString(), "m.server_notice") == 0) { // TODO: show server notices differently + message->type = MessageType::TEXT; + prefix = "* Server notice * "; } else { return nullptr; } @@ -825,10 +1252,11 @@ namespace QuickMedia { } } - void Matrix::events_add_pinned_events(const rapidjson::Value &events_json, RoomData *room_data, RoomSyncData &room_sync_data) { + void Matrix::events_add_pinned_events(const rapidjson::Value &events_json, RoomData *room_data) { if(!events_json.IsArray()) return; + bool has_pinned_events = false; std::vector pinned_events; for(const rapidjson::Value &event_item_json : events_json.GetArray()) { if(!event_item_json.IsObject()) @@ -846,34 +1274,25 @@ namespace QuickMedia { if(!pinned_json.IsArray()) continue; + has_pinned_events = true; + pinned_events.clear(); for(const rapidjson::Value &pinned_item_json : pinned_json.GetArray()) { if(!pinned_item_json.IsString()) continue; - pinned_events.push_back(std::string(pinned_item_json.GetString(), pinned_item_json.GetStringLength())); } } - room_sync_data[room_data].pinned_events = pinned_events; - room_data->append_pinned_events(std::move(pinned_events)); + if(has_pinned_events) + room_data->set_pinned_events(std::move(pinned_events)); } - // TODO: According to spec: "Any tag in the tld.name.* form but not matching the namespace of the current client should be ignored", - // should we follow this? - static const char* tag_get_name(const char *name, size_t size) { - if(size >= 2 && (memcmp(name, "m.", 2) == 0 || memcmp(name, "u.", 2) == 0)) - return name + 2; - else if(size >= 9 && memcmp(name, "tld.name.", 9) == 0) - return name + 9; - else - return name; - } - - void Matrix::events_add_room_to_tags(const rapidjson::Value &events_json, RoomData *room_data) { + void Matrix::events_add_room_to_tags(const rapidjson::Value &events_json, RoomData *room_data, MatrixDelegate *delegate) { if(!events_json.IsArray()) return; - std::vector pinned_events; + bool has_tags = false; + std::set new_tags; for(const rapidjson::Value &event_item_json : events_json.GetArray()) { if(!event_item_json.IsObject()) continue; @@ -890,17 +1309,46 @@ namespace QuickMedia { if(!tags_json.IsObject()) continue; + has_tags = true; + new_tags.clear(); for(auto const &tag_json : tags_json.GetObject()) { if(!tag_json.name.IsString() || !tag_json.value.IsObject()) continue; - const char *tag_name = tag_get_name(tag_json.name.GetString(), tag_json.name.GetStringLength()); - if(!tag_name) - continue; + //const char *tag_name = tag_get_name(tag_json.name.GetString(), tag_json.name.GetStringLength()); + //if(!tag_name) + // continue; // TODO: Support tag order - rooms_by_tag_name[tag_name].push_back(room_data->index); + new_tags.insert(std::string(tag_json.name.GetString(), tag_json.name.GetStringLength())); + } + } + + // Adding/removing tags is done with PUT and DELETE, but tags is part of account_data that contains all of the tags. + // When we receive a list of tags its always the full list of tags + if(has_tags) { + room_data->acquire_room_lock(); + std::set &room_tags = room_data->get_tags_unsafe(); + + for(const std::string &room_tag : room_tags) { + auto it = new_tags.find(room_tag); + if(it == new_tags.end()) + delegate->room_remove_tag(room_data, room_tag); + } + + for(const std::string &new_tag : new_tags) { + auto it = room_tags.find(new_tag); + if(it == room_tags.end()) + delegate->room_add_tag(room_data, new_tag); } + + if(new_tags.empty()) { + new_tags.insert(OTHERS_ROOM_TAG); + delegate->room_add_tag(room_data, OTHERS_ROOM_TAG); + } + + room_tags = std::move(new_tags); + room_data->release_room_lock(); } } @@ -991,7 +1439,7 @@ namespace QuickMedia { return "m.file"; } - PluginResult Matrix::post_message(RoomData *room, const std::string &body, const std::optional &file_info, const std::optional &thumbnail_info) { + PluginResult Matrix::post_message(RoomData *room, const std::string &body, const std::optional &file_info, const std::optional &thumbnail_info, const std::string &msgtype) { char random_characters[18]; if(!generate_random_characters(random_characters, sizeof(random_characters))) return PluginResult::ERR; @@ -1021,7 +1469,10 @@ namespace QuickMedia { } rapidjson::Document request_data(rapidjson::kObjectType); - request_data.AddMember("msgtype", rapidjson::StringRef(file_info ? content_type_to_message_type(file_info->content_type) : "m.text"), request_data.GetAllocator()); + if(msgtype.empty()) + request_data.AddMember("msgtype", rapidjson::StringRef(file_info ? content_type_to_message_type(file_info->content_type) : "m.text"), request_data.GetAllocator()); + else + request_data.AddMember("msgtype", rapidjson::StringRef(msgtype.c_str()), request_data.GetAllocator()); request_data.AddMember("body", rapidjson::StringRef(body.c_str()), request_data.GetAllocator()); if(contains_formatted_text) { request_data.AddMember("format", "org.matrix.custom.html", request_data.GetAllocator()); @@ -1173,11 +1624,6 @@ namespace QuickMedia { // TODO: Store shared_ptr instead of raw pointer... Message *relates_to_message_raw = (Message*)relates_to; std::shared_ptr relates_to_message_shared = room->get_message_by_id(relates_to_message_raw->event_id); - std::shared_ptr relates_to_message_original = get_edited_message_original_message(room, relates_to_message_shared); - if(!relates_to_message_original) { - fprintf(stderr, "Failed to get the original message for message with event id: %s\n", relates_to_message_raw->event_id.c_str()); - return PluginResult::ERR; - } char random_characters[18]; if(!generate_random_characters(random_characters, sizeof(random_characters))) @@ -1186,7 +1632,7 @@ namespace QuickMedia { std::string random_readable_chars = random_characters_to_readable_string(random_characters, sizeof(random_characters)); rapidjson::Document in_reply_to_json(rapidjson::kObjectType); - in_reply_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_original->event_id.c_str()), in_reply_to_json.GetAllocator()); + in_reply_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_shared->event_id.c_str()), in_reply_to_json.GetAllocator()); rapidjson::Document relates_to_json(rapidjson::kObjectType); relates_to_json.AddMember("m.in_reply_to", std::move(in_reply_to_json), relates_to_json.GetAllocator()); @@ -1233,11 +1679,6 @@ namespace QuickMedia { PluginResult Matrix::post_edit(RoomData *room, const std::string &body, void *relates_to) { Message *relates_to_message_raw = (Message*)relates_to; std::shared_ptr relates_to_message_shared = room->get_message_by_id(relates_to_message_raw->event_id); - std::shared_ptr relates_to_message_original = get_edited_message_original_message(room, relates_to_message_shared); - if(!relates_to_message_original) { - fprintf(stderr, "Failed to get the original message for message with event id: %s\n", relates_to_message_raw->event_id.c_str()); - return PluginResult::ERR; - } char random_characters[18]; if(!generate_random_characters(random_characters, sizeof(random_characters))) @@ -1274,7 +1715,7 @@ namespace QuickMedia { } rapidjson::Document relates_to_json(rapidjson::kObjectType); - relates_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_original->event_id.c_str()), relates_to_json.GetAllocator()); + relates_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_shared->event_id.c_str()), relates_to_json.GetAllocator()); relates_to_json.AddMember("rel_type", "m.replace", relates_to_json.GetAllocator()); std::string body_edit_str = " * " + body; @@ -1320,14 +1761,6 @@ namespace QuickMedia { return PluginResult::OK; } - // TODO: Right now this recursively calls /rooms//context/ and trusts server to not make it recursive. To make this robust, check iteration count and do not trust server. - // TODO: Optimize? - std::shared_ptr Matrix::get_edited_message_original_message(RoomData *room_data, std::shared_ptr message) { - if(!message || message->related_event_type != RelatedEventType::EDIT) - return message; - return get_edited_message_original_message(room_data, get_message_by_id(room_data, message->related_event_id)); - } - std::shared_ptr Matrix::get_message_by_id(RoomData *room, const std::string &event_id) { std::shared_ptr existing_room_message = room->get_message_by_id(event_id); if(existing_room_message) @@ -1373,15 +1806,11 @@ namespace QuickMedia { return new_message; } - // Returns empty string on error static const char* file_get_filename(const std::string &filepath) { size_t index = filepath.rfind('/'); if(index == std::string::npos) - return ""; - const char *filename = filepath.c_str() + index + 1; - if(filename[0] == '\0') - return ""; - return filename; + return filepath.c_str(); + return filepath.c_str() + index + 1; } PluginResult Matrix::post_file(RoomData *room, const std::string &filepath, std::string &err_msg) { @@ -1581,7 +2010,6 @@ namespace QuickMedia { rooms.clear(); room_list_read_index = 0; room_data_by_id.clear(); - rooms_by_tag_name.clear(); user_id.clear(); username.clear(); access_token.clear(); diff --git a/src/plugins/NyaaSi.cpp b/src/plugins/NyaaSi.cpp index 3fe6526..8b1efc7 100644 --- a/src/plugins/NyaaSi.cpp +++ b/src/plugins/NyaaSi.cpp @@ -51,7 +51,7 @@ namespace QuickMedia { size_t tbody_begin = website_data.find(""); if(tbody_begin == std::string::npos) - return SearchResult::ERR; + return SearchResult::OK; size_t tbody_end = website_data.find("", tbody_begin + 7); if(tbody_end == std::string::npos) diff --git a/src/plugins/Pornhub.cpp b/src/plugins/Pornhub.cpp index c0e3fa1..f527e76 100644 --- a/src/plugins/Pornhub.cpp +++ b/src/plugins/Pornhub.cpp @@ -141,7 +141,7 @@ namespace QuickMedia { PluginResult PornhubSearchPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { (void)title; (void)url; - result_tabs.push_back(Tab{create_body(), std::make_unique(program), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique(program), nullptr}); return PluginResult::OK; } diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index 12c156a..a157a8c 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -278,7 +278,7 @@ namespace QuickMedia { PluginResult YoutubeSearchPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { (void)title; (void)url; - result_tabs.push_back(Tab{create_body(), std::make_unique(program), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique(program), nullptr}); return PluginResult::OK; } -- cgit v1.2.3