From 40e0f8f5d8c3e480f01a2d71b6a493247adcb77f Mon Sep 17 00:00:00 2001 From: dec05eba Date: Mon, 21 Sep 2020 03:49:17 +0200 Subject: Initial matrix support --- src/QuickMedia.cpp | 442 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 388 insertions(+), 54 deletions(-) (limited to 'src/QuickMedia.cpp') diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 956370f..1d0518f 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -7,6 +7,7 @@ #include "../plugins/Fourchan.hpp" #include "../plugins/Dmenu.hpp" #include "../plugins/NyaaSi.hpp" +#include "../plugins/Matrix.hpp" #include "../include/Scale.hpp" #include "../include/Program.h" #include "../include/VideoPlayer.hpp" @@ -41,15 +42,15 @@ static const sf::Color tab_selected_color(0, 85, 119); static const sf::Color tab_unselected_color(43, 45, 47); // Prevent writing to broken pipe from exiting the program -static void sigpipe_handler(int unused) { +static void sigpipe_handler(int) { } -static int x_error_handler(Display *display, XErrorEvent *event) { +static int x_error_handler(Display*, XErrorEvent*) { return 0; } -static int x_io_error_handler(Display *display) { +static int x_io_error_handler(Display*) { return 0; } @@ -275,7 +276,7 @@ namespace QuickMedia { current_plugin = nullptr; std::string plugin_logo_path; - std::string search_placeholder = "Search..."; + std::string search_placeholder; for(int i = 1; i < argc; ++i) { if(!current_plugin) { @@ -302,6 +303,9 @@ namespace QuickMedia { plugin_logo_path = resources_root + "images/nyaa_si_logo.png"; } else if(strcmp(argv[i], "dmenu") == 0) { current_plugin = new Dmenu(); + } else if(strcmp(argv[i], "matrix") == 0) { + current_plugin = new Matrix(); + plugin_logo_path = resources_root + "images/matrix_logo.png"; } else { fprintf(stderr, "Invalid plugin %s\n", argv[i]); usage(); @@ -327,6 +331,12 @@ namespace QuickMedia { } } + if(!search_placeholder.empty() && current_plugin->name == "dmenu") { + fprintf(stderr, "Option -p is only valid with dmenu\n"); + usage(); + return -1; + } + if(use_tor && !is_program_executable_by_name("torsocks")) { fprintf(stderr, "torsocks needs to be installed (and accessible from PATH environment variable) when using the --tor option\n"); return -2; @@ -389,6 +399,20 @@ namespace QuickMedia { plugin_logo.setSmooth(true); } + if(current_plugin->name == "matrix") { + Matrix *matrix = static_cast(current_plugin); + if(matrix->load_and_verify_cached_session() == PluginResult::OK) { + current_page = Page::CHAT; + } else { + fprintf(stderr, "Failed to load session cache, redirecting to login page\n"); + current_page = Page::CHAT_LOGIN; + } + search_placeholder = "Send a message..."; + } + + if(search_placeholder.empty()) + search_placeholder = "Search..."; + search_bar = std::make_unique(font, &plugin_logo, search_placeholder); search_bar->text_autosearch_delay = current_plugin->get_search_delay(); @@ -459,10 +483,15 @@ namespace QuickMedia { body->clear_thumbnails(); break; } - default: - fprintf(stderr, "Page not implemented: %d\n", current_page); - window.close(); + case Page::CHAT_LOGIN: { + chat_login_page(); + break; + } + case Page::CHAT: { + body->draw_thumbnails = true; + chat_page(); break; + } } } @@ -490,9 +519,9 @@ namespace QuickMedia { search_bar->clear(); } } - } else if(handle_searchbar && event.type == sf::Event::TextEntered) { - search_bar->onTextEntered(event.text.unicode); } else if(handle_searchbar) { + if(event.type == sf::Event::TextEntered) + search_bar->onTextEntered(event.text.unicode); search_bar->on_event(event); } } @@ -505,41 +534,6 @@ namespace QuickMedia { return base64_url::decode(data); } - static bool read_file_as_json(const Path &filepath, Json::Value &result) { - std::string file_content; - if(file_get_content(filepath, file_content) != 0) { - fprintf(stderr, "Failed to get content of file: %s\n", filepath.data.c_str()); - return false; - } - - Json::CharReaderBuilder json_builder; - std::unique_ptr json_reader(json_builder.newCharReader()); - std::string json_errors; - if(!json_reader->parse(file_content.data(), file_content.data() + file_content.size(), &result, &json_errors)) { - fprintf(stderr, "Failed to read file %s as json, error: %s\n", filepath.data.c_str(), json_errors.c_str()); - return false; - } - - return true; - } - - static bool save_json_to_file_atomic(const Path &path, const Json::Value &json) { - Path tmp_path = path; - tmp_path.append(".tmp"); - - Json::StreamWriterBuilder json_builder; - if(file_overwrite(tmp_path, Json::writeString(json_builder, json)) != 0) - return false; - - // Rename is atomic under posix! - if(rename(tmp_path.data.c_str(), path.data.c_str()) != 0) { - perror("save_json_to_file_atomic rename"); - return false; - } - - return true; - } - enum class SearchSuggestionTab { ALL, HISTORY, @@ -861,7 +855,8 @@ namespace QuickMedia { } else if(next_page == Page::VIDEO_CONTENT) { watched_videos.clear(); if(content_url.empty()) - next_page = Page::SEARCH_RESULT; + //next_page = Page::SEARCH_RESULT; + next_page = Page::SEARCH_SUGGESTION; else { page_stack.push(Page::SEARCH_SUGGESTION); } @@ -902,7 +897,7 @@ namespace QuickMedia { std::vector tabs; int selected_tab = 0; - auto login_submit_callback = [this, &tabs, &selected_tab](const std::string &text) -> bool { + auto login_submit_callback = [this, &tabs, &selected_tab](const std::string&) -> bool { if(!tabs[selected_tab].body) { std::string username = tabs[selected_tab].login_tab->username->get_text(); std::string password = tabs[selected_tab].login_tab->password->get_text(); @@ -970,7 +965,7 @@ namespace QuickMedia { typing = false; }; - search_bar->onTextSubmitCallback = [this, &tabs, &selected_tab, &typing](const std::string &text) -> bool { + search_bar->onTextSubmitCallback = [this, &tabs, &selected_tab, &typing](const std::string&) -> bool { if(current_plugin->name != "dmenu") { if(typing || tabs[selected_tab].body->no_items_visible()) return false; @@ -988,7 +983,11 @@ namespace QuickMedia { }); } else { */ - PluginResult front_page_result = current_plugin->get_front_page(body->items); + if(current_plugin->get_front_page(body->items) != PluginResult::OK) { + show_notification("QuickMedia", "Failed to get front page", Urgency::CRITICAL); + current_page = Page::EXIT; + return; + } body->clamp_selection(); /*}*/ @@ -1556,7 +1555,10 @@ namespace QuickMedia { } if(video_player_window && XCheckTypedWindowEvent(disp, video_player_window, KeyPress, &xev)/* && xev.xkey.subwindow == video_player_window*/) { + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wdeprecated-declarations" KeySym pressed_keysym = XKeycodeToKeysym(disp, xev.xkey.keycode, 0); + #pragma GCC diagnostic pop bool pressing_ctrl = (CLEANMASK(xev.xkey.state) == ControlMask); if(pressed_keysym == XK_Escape) { current_page = previous_page; @@ -1722,7 +1724,7 @@ namespace QuickMedia { tabs[selected_tab].body->clamp_selection(); }; - search_bar->onTextSubmitCallback = [this, &tabs, &selected_tab, &json_chapters](const std::string &text) -> bool { + search_bar->onTextSubmitCallback = [this, &tabs, &selected_tab, &json_chapters](const std::string&) -> bool { if(tabs[selected_tab].type == EpisodeListTabType::CHAPTERS) { BodyItem *selected_item = body->get_selected(); if(!selected_item) @@ -1741,7 +1743,7 @@ namespace QuickMedia { } }; - auto download_create_page = [manga](std::string url) { + auto download_creator_page = [manga](std::string url) { BodyItems body_items; if(manga->get_creators_manga_list(url, body_items) != PluginResult::OK) show_notification("Manga", "Failed to download authors page", Urgency::CRITICAL); @@ -1762,7 +1764,7 @@ namespace QuickMedia { tab.body = new Body(this, &font, &bold_font); tab.body->draw_thumbnails = true; tab.creator = &creator; - tab.creator_page_download_future = std::async(std::launch::async, download_create_page, creator.url); + tab.creator_page_download_future = std::async(std::launch::async, download_creator_page, creator.url); tab.text = sf::Text(creator.name, font, tab_text_size); tabs.push_back(std::move(tab)); } @@ -2323,7 +2325,7 @@ namespace QuickMedia { } }; - search_bar->onTextSubmitCallback = [this](const std::string &text) -> bool { + search_bar->onTextSubmitCallback = [this](const std::string&) -> bool { BodyItem *selected_item = body->get_selected(); if(!selected_item) return false; @@ -2400,7 +2402,7 @@ namespace QuickMedia { // TODO: Have an option for the search bar to be multi-line. search_bar->onTextUpdateCallback = nullptr; - search_bar->onTextSubmitCallback = [this](const std::string &text) -> bool { + search_bar->onTextSubmitCallback = [this](const std::string&) -> bool { if(current_plugin->name == "nyaa.si") { BodyItem *selected_item = body->get_selected(); if(selected_item && strncmp(selected_item->url.c_str(), "magnet:?", 8) == 0) { @@ -2456,7 +2458,7 @@ namespace QuickMedia { body->select_first_item(); }; - search_bar->onTextSubmitCallback = [this](const std::string &text) -> bool { + search_bar->onTextSubmitCallback = [this](const std::string&) -> bool { BodyItem *selected_item = body->get_selected(); if(!selected_item) return false; @@ -2922,4 +2924,336 @@ namespace QuickMedia { // so you dont have to retype a post that was in the middle of being posted when returning. search_bar->clear(); } + + // TODO: Provide a way to logout + void Program::chat_login_page() { + assert(current_plugin->name == "matrix"); + + SearchBar login_input(font, nullptr, "Username"); + SearchBar password_input(font, nullptr, "Password", true); + SearchBar homeserver_input(font, nullptr, "Homeserver"); + + sf::Text status_text("", font, 18); + + const int num_inputs = 3; + SearchBar *inputs[num_inputs] = { &login_input, &password_input, &homeserver_input }; + int focused_input = 0; + + auto text_submit_callback = [this, inputs, &status_text](const sf::String&) -> bool { + Matrix *matrix = static_cast(current_plugin); + for(int i = 0; i < num_inputs; ++i) { + if(inputs[i]->get_text().empty()) { + status_text.setString("All fields need to be filled in"); + return false; + } + } + + std::string err_msg; + // TODO: Make asynchronous + if(matrix->login(inputs[0]->get_text(), inputs[1]->get_text(), inputs[2]->get_text(), err_msg) == PluginResult::OK) { + current_page = Page::CHAT; + } else { + status_text.setString("Failed to login, error: " + err_msg); + } + return false; + }; + + for(int i = 0; i < num_inputs; ++i) { + inputs[i]->caret_visible = false; + inputs[i]->onTextSubmitCallback = text_submit_callback; + } + inputs[focused_input]->caret_visible = true; + + sf::Vector2f body_pos; + sf::Vector2f body_size; + bool redraw = true; + sf::Event event; + + while (current_page == Page::CHAT_LOGIN) { + while (window.pollEvent(event)) { + base_event_handler(event, Page::EXIT, false, false, false); + if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) { + redraw = true; + } else if(event.type == sf::Event::TextEntered) { + inputs[focused_input]->onTextEntered(event.text.unicode); + } else if(event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Tab) { + for(int i = 0; i < num_inputs; ++i) { + inputs[i]->caret_visible = false; + } + focused_input = (focused_input + 1) % num_inputs; + inputs[focused_input]->caret_visible = true; + } + inputs[focused_input]->on_event(event); + } + + if(redraw) { + redraw = false; + search_bar->onWindowResize(window_size); + get_body_dimensions(window_size, search_bar.get(), body_pos, body_size); + } + + window.clear(back_color); + body->draw(window, body_pos, body_size); + float y = 0.0f; + for(int i = 0; i < num_inputs; ++i) { + inputs[i]->set_vertical_position(y); + inputs[i]->update(); + inputs[i]->draw(window); + y += inputs[i]->getBottomWithoutShadow(); + } + status_text.setPosition(0.0f, y + 10.0f); + window.draw(status_text); + window.display(); + } + } + + enum class ChatTabType { + MESSAGES, + ROOMS + }; + + struct ChatTab { + ChatTabType type; + std::unique_ptr body; + std::future future; + sf::Text text; + }; + + void Program::chat_page() { + assert(current_plugin->name == "matrix"); + Matrix *matrix = static_cast(current_plugin); + + std::vector tabs; + int selected_tab = 0; + size_t room_message_index = 0; + + ChatTab messages_tab; + messages_tab.type = ChatTabType::MESSAGES; + messages_tab.body = std::make_unique(this, &font, &bold_font); + messages_tab.body->draw_thumbnails = true; + messages_tab.text = sf::Text("Messages", font, tab_text_size); + tabs.push_back(std::move(messages_tab)); + + ChatTab rooms_tab; + rooms_tab.type = ChatTabType::ROOMS; + rooms_tab.body = std::make_unique(this, &font, &bold_font); + rooms_tab.body->draw_thumbnails = true; + rooms_tab.text = sf::Text("Rooms", font, tab_text_size); + tabs.push_back(std::move(rooms_tab)); + + const int MESSAGES_TAB_INDEX = 0; + const int ROOMS_TAB_INDEX = 1; + + tabs[MESSAGES_TAB_INDEX].body->clear_items(); + /* + if(matrix->get_cached_sync(tabs[MESSAGES_TAB_INDEX].body->items) != PluginResult::OK) { + fprintf(stderr, "Failed to get matrix cached sync\n"); + } else { + fprintf(stderr, "Loaded matrix sync from cache, num items: %zu\n", tabs[MESSAGES_TAB_INDEX].body->items.size()); + } + */ + if(matrix->sync() != PluginResult::OK) { + show_notification("QuickMedia", "Intial matrix sync failed", Urgency::CRITICAL); + current_page = Page::EXIT; + return; + } + + if(matrix->get_joined_rooms(tabs[ROOMS_TAB_INDEX].body->items) != PluginResult::OK) { + show_notification("QuickMedia", "Failed to get a list of joined rooms", Urgency::CRITICAL); + current_page = Page::EXIT; + return; + } + + // TODO: the initial room to view should be the last viewed room when closing QuickMedia. + // The room id should be saved in a file when changing viewed room. + std::string current_room_id; + if(!tabs[ROOMS_TAB_INDEX].body->items.empty()) + current_room_id = tabs[ROOMS_TAB_INDEX].body->items[0]->get_title(); + + // TODO: Allow empty initial room (if the user hasn't joined any room yet) + assert(!current_room_id.empty()); + + // TODO: Filer for rooms and settings + search_bar->onTextUpdateCallback = nullptr; + + search_bar->onTextSubmitCallback = [matrix, &tabs, &selected_tab, &room_message_index, ¤t_room_id](const std::string &text) -> bool { + if(tabs[selected_tab].type == ChatTabType::MESSAGES) { + if(text.empty()) + return false; + + // TODO: Make asynchronous + if(matrix->post_message(current_room_id, text) != PluginResult::OK) { + show_notification("QuickMedia", "Failed to post matrix message", Urgency::CRITICAL); + return false; + } + return true; + } else if(tabs[selected_tab].type == ChatTabType::ROOMS) { + BodyItem *selected_item = tabs[selected_tab].body->get_selected(); + if(selected_item) { + // TODO: Change to selected_item->url once rooms have a display name + current_room_id = selected_item->get_title(); + selected_tab = MESSAGES_TAB_INDEX; + room_message_index = 0; + tabs[MESSAGES_TAB_INDEX].body->clear_items(); + + size_t num_new_messages = 0; + BodyItems new_items; + // TODO: Make asynchronous + if(matrix->get_room_messages(current_room_id, 0, new_items, num_new_messages) == PluginResult::OK) { + room_message_index += num_new_messages; + tabs[MESSAGES_TAB_INDEX].body->append_items(std::move(new_items)); + if(!tabs[MESSAGES_TAB_INDEX].body->items.empty() && num_new_messages > 0) + tabs[MESSAGES_TAB_INDEX].body->set_selected_item(tabs[MESSAGES_TAB_INDEX].body->items.size() - 1); + } else { + std::string err_msg = "Failed to get messages in room: " + current_room_id; + show_notification("QuickMedia", err_msg, Urgency::CRITICAL); + } + return true; + } + } + return false; + }; + + struct SyncFutureResult { + BodyItems body_items; + size_t num_new_messages; + }; + + std::future sync_future; + bool sync_running = false; + std::string sync_future_room_id; + sf::Clock sync_timer; + sf::Int32 sync_min_time_ms = 0; // Sync immediately the first time + + const float tab_spacer_height = 0.0f; + sf::Vector2f body_pos; + sf::Vector2f body_size; + bool redraw = true; + sf::Event event; + + sf::RectangleShape tab_drop_shadow; + tab_drop_shadow.setFillColor(sf::Color(23, 25, 27)); + + while (current_page == Page::CHAT) { + while (window.pollEvent(event)) { + base_event_handler(event, Page::EXIT, false, false); + if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) { + redraw = true; + } else if(event.type == sf::Event::KeyPressed) { + if(event.key.code == sf::Keyboard::Up) { + tabs[selected_tab].body->select_previous_item(); + } else if(event.key.code == sf::Keyboard::Down) { + tabs[selected_tab].body->select_next_item(); + } else if(event.key.code == sf::Keyboard::Escape) { + current_page = Page::EXIT; + body->clear_items(); + body->reset_selected(); + search_bar->clear(); + } else if(event.key.code == sf::Keyboard::Left) { + tabs[selected_tab].body->filter_search_fuzzy(""); + tabs[selected_tab].body->clamp_selection(); + selected_tab = std::max(0, selected_tab - 1); + search_bar->clear(); + } else if(event.key.code == sf::Keyboard::Right) { + tabs[selected_tab].body->filter_search_fuzzy(""); + tabs[selected_tab].body->clamp_selection(); + selected_tab = std::min((int)tabs.size() - 1, selected_tab + 1); + search_bar->clear(); + } + } + } + + if(redraw) { + redraw = false; + search_bar->onWindowResize(window_size); + search_bar->set_vertical_position(window_size.y - search_bar->getBottomWithoutShadow()); + + float body_padding_horizontal = 25.0f; + float body_padding_vertical = 25.0f; + float body_width = window_size.x - body_padding_horizontal * 2.0f; + if(body_width <= 480.0f) { + body_width = window_size.x; + body_padding_horizontal = 0.0f; + body_padding_vertical = 10.0f; + } + + float search_bottom = search_bar->getBottomWithoutShadow(); + body_pos = sf::Vector2f(body_padding_horizontal, body_padding_vertical + tab_height); + body_size = sf::Vector2f(body_width, window_size.y - search_bottom - body_padding_vertical - tab_height); + //get_body_dimensions(window_size, search_bar.get(), body_pos, body_size, true); + } + + if(!sync_running && sync_timer.getElapsedTime().asMilliseconds() >= sync_min_time_ms) { + fprintf(stderr, "Time since last sync: %d ms\n", sync_timer.getElapsedTime().asMilliseconds()); + // TODO: Ignore matrix->sync() call the first time, its already called above for the first time + sync_min_time_ms = 3000; + sync_running = true; + sync_timer.restart(); + sync_future_room_id = current_room_id; + sync_future = std::async(std::launch::async, [this, &sync_future_room_id, room_message_index]() { + Matrix *matrix = static_cast(current_plugin); + + SyncFutureResult result; + result.num_new_messages = 0; + if(matrix->sync() == PluginResult::OK) { + fprintf(stderr, "Synced matrix\n"); + if(matrix->get_room_messages(sync_future_room_id, room_message_index, result.body_items, result.num_new_messages) != PluginResult::OK) { + fprintf(stderr, "Failed to get new matrix messages in room: %s\n", sync_future_room_id.c_str()); + } + } else { + fprintf(stderr, "Failed to sync matrix\n"); + } + + return result; + }); + } + + if(sync_future.valid() && sync_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + SyncFutureResult sync_future_result = sync_future.get(); + // Ignore finished sync if it happened in another room. When we navigate back to the room we will get the messages again + if(sync_future_room_id == current_room_id) { + room_message_index += sync_future_result.num_new_messages; + tabs[MESSAGES_TAB_INDEX].body->append_items(std::move(sync_future_result.body_items)); + if(!tabs[MESSAGES_TAB_INDEX].body->items.empty() && sync_future_result.num_new_messages > 0) + tabs[MESSAGES_TAB_INDEX].body->set_selected_item(tabs[MESSAGES_TAB_INDEX].body->items.size() - 1); + } + sync_running = false; + } + + search_bar->update(); + + window.clear(back_color); + + const float width_per_tab = window_size.x / tabs.size(); + sf::RectangleShape tab_background(sf::Vector2f(std::floor(width_per_tab), tab_height)); + + float tab_vertical_offset = 0.0f; + tabs[selected_tab].body->draw(window, body_pos, body_size); + const float tab_y = tab_spacer_height + std::floor(tab_vertical_offset + tab_height * 0.5f - (tab_text_size + 5.0f) * 0.5f); + + int i = 0; + for(ChatTab &tab : tabs) { + if(i == selected_tab) + tab_background.setFillColor(tab_selected_color); + else + tab_background.setFillColor(tab_unselected_color); + + tab_background.setPosition(std::floor(i * width_per_tab), tab_spacer_height + std::floor(tab_vertical_offset)); + window.draw(tab_background); + const float center = (i * width_per_tab) + (width_per_tab * 0.5f); + tab.text.setPosition(std::floor(center - tab.text.getLocalBounds().width * 0.5f), tab_y); + window.draw(tab.text); + ++i; + } + + tab_drop_shadow.setSize(sf::Vector2f(window_size.x, 5.0f)); + tab_drop_shadow.setPosition(0.0f, std::floor(tab_vertical_offset + tab_height)); + window.draw(tab_drop_shadow); + + search_bar->draw(window, false); + window.display(); + } + + exit(0); // Ignore futures and quit immediately + } } -- cgit v1.2.3