From 555f0e7e910b2231073734816727379e1276aa6c Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 29 Oct 2022 19:31:22 +0200 Subject: Matrix: add media reply with ctrl+u --- README.md | 1 + TODO | 6 +++-- include/FileAnalyzer.hpp | 2 +- plugins/Matrix.hpp | 4 +-- src/AsyncImageLoader.cpp | 2 +- src/FileAnalyzer.cpp | 2 +- src/QuickMedia.cpp | 34 +++++++++++++++++++++--- src/plugins/Matrix.cpp | 69 ++++++++++++++++++++++++++++++++++++++---------- 8 files changed, 96 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index f32229e..39f8083 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ Type text and then wait and QuickMedia will automatically search.\ `Ctrl+S`: Save the selected file.\ `Ctrl+V`: Uploads the file specified in the clipboard.\ `U`: Bring up the file manager and select a file to upload to the room, `Esc` to cancel.\ +`Ctrl+U`: Bring up the file manager and select a file to upload to the message you are replying to, `Esc` to cancel.\ `Arrow up`/`K`: Move up.\ `Arrow down`/`J`: Move down.\ `Alt+Up`/`Alt+K`: Select the room above the currently selected room.\ diff --git a/TODO b/TODO index 1f14ee5..d2294e9 100644 --- a/TODO +++ b/TODO @@ -237,7 +237,9 @@ Add option to use invidious, and the invidious front page. Add proper vim modal mode. Maybe switch focused part with tab? then also need to show which part is focused. Send clipboard content to clipboard manager when destroying the window, if we are the clipboard owner. Bypass compositor when fullscreening application. -Sort matrix events by timestamp (join/leave events and other things). +Sort matrix events by timestamp. This is needed to make name change and other similar things work properly, otherwise @ mention wont work as it may show an older name if a previous event is fetched and contains name change. Also applies to join/leave events and other things. Update room name, avatar, etc in gui when updated. Automatically cleanup old cache files. -Download manga pages in parallel. This helps downloading for certain websites such as mangakatana where a single page can take more than 2 seconds but loading 5 at once allows each page to load in 0.4 seconds. \ No newline at end of file +Download manga pages in parallel. This helps downloading for certain websites such as mangakatana where a single page can take more than 2 seconds but loading 5 at once allows each page to load in 0.4 seconds. +Allow pasting a file link (with or without file://) directly into matrix chat to upload a file (if the chat input is empty). This allows replying-with-media to work with ctrl+v. +Matrix image reply to image reply to text reply is a bit broken in the text formatting. \ No newline at end of file diff --git a/include/FileAnalyzer.hpp b/include/FileAnalyzer.hpp index 851a3e4..9f3dbb9 100644 --- a/include/FileAnalyzer.hpp +++ b/include/FileAnalyzer.hpp @@ -44,7 +44,7 @@ namespace QuickMedia { // Set |width| or |height| to 0 to disable scaling. // TODO: Make this async - bool video_get_first_frame(const FileAnalyzer &file, const char *destination_path, int width = 0, int height = 0); + bool video_get_middle_frame(const FileAnalyzer &file, const char *destination_path, int width = 0, int height = 0); class FileAnalyzer { public: diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index bf4ef94..b7d6d66 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -548,14 +548,14 @@ namespace QuickMedia { PluginResult post_message(RoomData *room, const std::string &body, std::string &event_id_response, const std::optional &file_info, const std::optional &thumbnail_info, const std::string &msgtype = ""); // |relates_to| is from |BodyItem.userdata| and is of type |Message*| // If |custom_transaction_id| is empty, then a new transaction id is generated - PluginResult post_reply(RoomData *room, const std::string &body, void *relates_to, std::string &event_id_response, const std::string &custom_transaction_id = ""); + PluginResult post_reply(RoomData *room, const std::string &body, void *relates_to, std::string &event_id_response, const std::string &custom_transaction_id = "", const std::optional &file_info = std::nullopt, const std::optional &thumbnail_info = std::nullopt); // |relates_to| is from |BodyItem.userdata| and is of type |Message*| PluginResult post_edit(RoomData *room, const std::string &body, void *relates_to, std::string &event_id_response); // |relates_to| is from |BodyItem.userdata| and is of type |Message*| PluginResult post_reaction(RoomData *room, const std::string &body, void *relates_to, std::string &event_id_response); // If filename is empty then the filename is extracted from filepath - PluginResult post_file(RoomData *room, const std::string &filepath, std::string filename, std::string &event_id_response, std::string &err_msg); + PluginResult post_file(RoomData *room, const std::string &filepath, std::string filename, std::string &event_id_response, std::string &err_msg, void *relates_to = nullptr); PluginResult login(const std::string &username, const std::string &password, const std::string &homeserver, std::string &err_msg); PluginResult logout(); diff --git a/src/AsyncImageLoader.cpp b/src/AsyncImageLoader.cpp index 09c01df..d12f9af 100644 --- a/src/AsyncImageLoader.cpp +++ b/src/AsyncImageLoader.cpp @@ -137,7 +137,7 @@ namespace QuickMedia { } if(is_content_type_video(file_analyzer.get_content_type())) { - if(file_analyzer.load_metadata() && video_get_first_frame(file_analyzer, thumbnail_path_resized.data.c_str(), resize_target_size.x, resize_target_size.y)) { + if(file_analyzer.load_metadata() && video_get_middle_frame(file_analyzer, thumbnail_path_resized.data.c_str(), resize_target_size.x, resize_target_size.y)) { thumbnail_data->loading_state = LoadingState::READY_TO_LOAD; } else { fprintf(stderr, "Failed to get video frame of %s\n", thumbnail_path.data.c_str()); diff --git a/src/FileAnalyzer.cpp b/src/FileAnalyzer.cpp index aeb88f7..0f312cf 100644 --- a/src/FileAnalyzer.cpp +++ b/src/FileAnalyzer.cpp @@ -127,7 +127,7 @@ namespace QuickMedia { return 0; } - bool video_get_first_frame(const FileAnalyzer &file, const char *destination_path, int width, int height) { + bool video_get_middle_frame(const FileAnalyzer &file, const char *destination_path, int width, int height) { Path destination_path_tmp = destination_path; destination_path_tmp.append(".tmp.jpg"); // TODO: .png, but the below code also needs to be changed for that diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 57c5489..13931d2 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -5864,15 +5864,19 @@ namespace QuickMedia { } }; - auto upload_file = [this, ¤t_room](const std::string &filepath, const std::string &filename) { - run_task_with_loading_screen([this, ¤t_room, filepath, filename]() { + auto upload_file = [this, ¤t_room, &tabs, &ui_tabs, &chat_state](const std::string &filepath, const std::string &filename) { + const int selected_tab = ui_tabs.get_selected(); + std::shared_ptr selected = tabs[selected_tab].body->get_selected_shared(); + void *message_to_reply_to = chat_state == ChatState::REPLYING ? selected->userdata : nullptr; + + run_task_with_loading_screen([this, ¤t_room, filepath, filename, message_to_reply_to]() { std::string filepath_mod = filepath; if(string_starts_with(filepath_mod, "file://")) filepath_mod.erase(filepath_mod.begin(), filepath_mod.begin() + 7); std::string event_id_response; std::string err_msg; - if(matrix->post_file(current_room, filepath_mod, filename, event_id_response, err_msg) == PluginResult::OK) { + if(matrix->post_file(current_room, filepath_mod, filename, event_id_response, err_msg, message_to_reply_to) == PluginResult::OK) { return true; } else { show_notification("QuickMedia", "Failed to upload media to room, error: " + err_msg, Urgency::CRITICAL); @@ -7102,6 +7106,14 @@ namespace QuickMedia { continue; launch_url(selected_item->get_title()); } + } else if(event.type == mgl::Event::KeyPressed && chat_state == ChatState::REPLYING) { + if(selected_tab == MESSAGES_TAB_INDEX) { + if(event.key.code == mgl::Keyboard::U && event.key.control) { + frame_skip_text_entry = true; + new_page = PageType::FILE_MANAGER; + chat_input.set_editable(false); + } + } } if((chat_state == ChatState::TYPING_MESSAGE || chat_state == ChatState::REPLYING || chat_state == ChatState::EDITING) && selected_tab == MESSAGES_TAB_INDEX && !frame_skip_text_entry) { @@ -7190,6 +7202,22 @@ namespace QuickMedia { } redraw = true; avatar_applied = false; + + if(selected_files.empty()) { + if(chat_state == ChatState::REPLYING) + chat_input.set_editable(true); + } else { + mention.hide(); + chat_input.set_editable(false); + chat_input.set_text(""); + chat_state = ChatState::NAVIGATING; + currently_operating_on_item = nullptr; + if(typing && current_room) { + fprintf(stderr, "Stopped typing\n"); + typing = false; + typing_state_queue.push(false); + } + } break; } case PageType::CHAT_LOGIN: { diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index b5948d2..13d42bb 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -3387,7 +3387,7 @@ namespace QuickMedia { "" + std::move(formatted_body); } - PluginResult Matrix::post_reply(RoomData *room, const std::string &body, void *relates_to, std::string &event_id_response, const std::string &custom_transaction_id) { + PluginResult Matrix::post_reply(RoomData *room, const std::string &body, void *relates_to, std::string &event_id_response, const std::string &custom_transaction_id, const std::optional &file_info, const std::optional &thumbnail_info) { Message *relates_to_message_raw = (Message*)relates_to; std::string transaction_id = custom_transaction_id; @@ -3397,7 +3397,8 @@ namespace QuickMedia { if(transaction_id.empty()) return PluginResult::ERR; - my_events_transaction_ids.insert(transaction_id); + if(!file_info) + my_events_transaction_ids.insert(transaction_id); rapidjson::Document in_reply_to_json(rapidjson::kObjectType); in_reply_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_raw->event_id.c_str()), in_reply_to_json.GetAllocator()); @@ -3409,12 +3410,43 @@ namespace QuickMedia { std::string formatted_message_reply_body = create_formatted_body_for_message_reply(room, relates_to_message_raw, body); rapidjson::Document request_data(rapidjson::kObjectType); - request_data.AddMember("msgtype", "m.text", request_data.GetAllocator()); // TODO: Allow image reply? element doesn't do that but we could! + request_data.AddMember("msgtype", rapidjson::StringRef(file_info ? content_type_to_message_type(file_info->content_type) : "m.text"), request_data.GetAllocator()); request_data.AddMember("body", rapidjson::StringRef(message_reply_body.c_str()), request_data.GetAllocator()); request_data.AddMember("format", "org.matrix.custom.html", request_data.GetAllocator()); request_data.AddMember("formatted_body", rapidjson::StringRef(formatted_message_reply_body.c_str()), request_data.GetAllocator()); request_data.AddMember("m.relates_to", std::move(relates_to_json), request_data.GetAllocator()); + // TODO: Add hashblur? + if(file_info) { + rapidjson::Value info_json(rapidjson::kObjectType); + info_json.AddMember("size", file_info->file_size, request_data.GetAllocator()); + info_json.AddMember("mimetype", rapidjson::StringRef(content_type_to_string(file_info->content_type)), request_data.GetAllocator()); + if(file_info->dimensions) { + info_json.AddMember("w", file_info->dimensions->width, request_data.GetAllocator()); + info_json.AddMember("h", file_info->dimensions->height, request_data.GetAllocator()); + } + if(file_info->duration_seconds) { + // TODO: Check for overflow? + info_json.AddMember("duration", (int)(file_info->duration_seconds.value() * 1000.0), request_data.GetAllocator()); + } + + if(thumbnail_info) { + rapidjson::Value thumbnail_info_json(rapidjson::kObjectType); + thumbnail_info_json.AddMember("size", thumbnail_info->file_size, request_data.GetAllocator()); + thumbnail_info_json.AddMember("mimetype", rapidjson::StringRef(content_type_to_string(thumbnail_info->content_type)), request_data.GetAllocator()); + if(thumbnail_info->dimensions) { + thumbnail_info_json.AddMember("w", thumbnail_info->dimensions->width, request_data.GetAllocator()); + thumbnail_info_json.AddMember("h", thumbnail_info->dimensions->height, request_data.GetAllocator()); + } + + info_json.AddMember("thumbnail_url", rapidjson::StringRef(thumbnail_info->content_uri.c_str()), request_data.GetAllocator()); + info_json.AddMember("thumbnail_info", std::move(thumbnail_info_json), request_data.GetAllocator()); + } + + request_data.AddMember("info", std::move(info_json), request_data.GetAllocator()); + request_data.AddMember("url", rapidjson::StringRef(file_info->content_uri.c_str()), request_data.GetAllocator()); + } + rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); request_data.Accept(writer); @@ -3714,7 +3746,10 @@ namespace QuickMedia { return filepath.c_str() + index + 1; } - PluginResult Matrix::post_file(RoomData *room, const std::string &filepath, std::string filename, std::string &event_id_response, std::string &err_msg) { + PluginResult Matrix::post_file(RoomData *room, const std::string &filepath, std::string filename, std::string &event_id_response, std::string &err_msg, void *relates_to) { + if(filename.empty()) + filename = file_get_filename(filepath); + UploadInfo file_info; UploadInfo thumbnail_info; PluginResult upload_file_result = upload_file(room, filepath, filename, file_info, thumbnail_info, err_msg); @@ -3726,10 +3761,10 @@ namespace QuickMedia { if(!thumbnail_info.content_uri.empty()) thumbnail_info_opt = std::move(thumbnail_info); - if(filename.empty()) - filename = file_get_filename(filepath); - - return post_message(room, filename, event_id_response, file_info_opt, thumbnail_info_opt); + if(relates_to) + return post_reply(room, filename, relates_to, event_id_response, "", file_info_opt, thumbnail_info_opt); + else + return post_message(room, filename, event_id_response, file_info_opt, thumbnail_info_opt); } PluginResult Matrix::upload_file(RoomData *room, const std::string &filepath, std::string filename, UploadInfo &file_info, UploadInfo &thumbnail_info, std::string &err_msg, bool upload_thumbnail) { @@ -3744,6 +3779,9 @@ namespace QuickMedia { file_info.dimensions = file_analyzer.get_dimensions(); file_info.duration_seconds = file_analyzer.get_duration_seconds(); + if(filename.empty()) + filename = file_get_filename(filepath); + int upload_limit; PluginResult config_result = get_config(&upload_limit); if(config_result != PluginResult::OK) { @@ -3766,9 +3804,12 @@ namespace QuickMedia { char tmp_filename[] = "/tmp/quickmedia_video_frame_XXXXXX"; int tmp_file = mkstemp(tmp_filename); if(tmp_file != -1) { - if(video_get_first_frame(file_analyzer, tmp_filename, thumbnail_max_size.x, thumbnail_max_size.y)) { + Path thumbnail_filename = filename; + thumbnail_filename = thumbnail_filename.filename_no_ext() + ".thumb.jpg"; // TODO: See video_get_middle_frame why this is jpg + + if(video_get_middle_frame(file_analyzer, tmp_filename, thumbnail_max_size.x, thumbnail_max_size.y)) { UploadInfo upload_info_ignored; // Ignore because it wont be set anyways. Thumbnails dont have thumbnails. - PluginResult upload_thumbnail_result = upload_file(room, tmp_filename, "", thumbnail_info, upload_info_ignored, err_msg, false); + PluginResult upload_thumbnail_result = upload_file(room, tmp_filename, thumbnail_filename.data, thumbnail_info, upload_info_ignored, err_msg, false); if(upload_thumbnail_result != PluginResult::OK) { close(tmp_file); remove(tmp_filename); @@ -3792,8 +3833,11 @@ namespace QuickMedia { else thumbnail_path = filepath; + Path thumbnail_filename = filename; + thumbnail_filename = thumbnail_filename.filename_no_ext() + ".thumb" + thumbnail_filename.ext(); + UploadInfo upload_info_ignored; // Ignore because it wont be set anyways. Thumbnails dont have thumbnails. - PluginResult upload_thumbnail_result = upload_file(room, thumbnail_path, "", thumbnail_info, upload_info_ignored, err_msg, false); + PluginResult upload_thumbnail_result = upload_file(room, thumbnail_path, thumbnail_filename.data, thumbnail_info, upload_info_ignored, err_msg, false); if(upload_thumbnail_result != PluginResult::OK) { close(tmp_file); remove(tmp_filename); @@ -3814,9 +3858,6 @@ namespace QuickMedia { { "--data-binary", "@" + filepath } }; - if(filename.empty()) - filename = file_get_filename(filepath); - std::string filename_escaped = url_param_encode(filename); char url[512]; -- cgit v1.2.3