diff options
author | dec05eba <dec05eba@protonmail.com> | 2020-10-26 09:48:25 +0100 |
---|---|---|
committer | dec05eba <dec05eba@protonmail.com> | 2020-10-29 04:21:15 +0100 |
commit | 620123fbd6c18dc48a25cc735565f6d8d85f8639 (patch) | |
tree | 1563c8d2867f80f7c5cf00c15c8a1b6612de9f67 | |
parent | 0d432776c13f7b7bfd94d8ea2a7a41be33f21c8d (diff) |
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...
-rw-r--r-- | README.md | 16 | ||||
-rw-r--r-- | TODO | 6 | ||||
-rw-r--r-- | images/dropdown.png | bin | 0 -> 601 bytes | |||
-rw-r--r-- | images/loading_icon.png | bin | 2766 -> 2934 bytes | |||
-rw-r--r-- | include/AsyncImageLoader.hpp | 10 | ||||
-rw-r--r-- | include/Body.hpp | 4 | ||||
-rw-r--r-- | include/Entry.hpp | 2 | ||||
-rw-r--r-- | include/MessageQueue.hpp | 50 | ||||
-rw-r--r-- | include/Path.hpp | 2 | ||||
-rw-r--r-- | include/QuickMedia.hpp | 18 | ||||
-rw-r--r-- | plugins/Matrix.hpp | 188 | ||||
-rw-r--r-- | plugins/Page.hpp | 6 | ||||
-rw-r--r-- | src/AsyncImageLoader.cpp | 36 | ||||
-rw-r--r-- | src/Body.cpp | 45 | ||||
-rw-r--r-- | src/NetUtils.cpp | 18 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 838 | ||||
-rw-r--r-- | src/plugins/Mangadex.cpp | 2 | ||||
-rw-r--r-- | src/plugins/Manganelo.cpp | 2 | ||||
-rw-r--r-- | src/plugins/Mangatown.cpp | 2 | ||||
-rw-r--r-- | src/plugins/Matrix.cpp | 644 | ||||
-rw-r--r-- | src/plugins/NyaaSi.cpp | 2 | ||||
-rw-r--r-- | src/plugins/Pornhub.cpp | 2 | ||||
-rw-r--r-- | src/plugins/Youtube.cpp | 2 |
23 files changed, 1173 insertions, 722 deletions
@@ -32,6 +32,7 @@ Press `Home` to scroll to the top or `End` to scroll to the bottom.\ Press `Enter` (aka `Return`) to select the item.\ Press `ESC` to go back to the previous menu.\ Press `Ctrl + F` to switch between window mode and fullscreen mode when watching a video.\ +Press `Space` to pause/unpause a video.\ Press `Ctrl + R` to show/hide related videos menu when watching a video.\ Press `Ctrl + T` when hovering over a manga chapter to start tracking manga after that chapter. This only works if AutoMedia is installed and accessible in PATH environment variable.\ Press `Backspace` to return to the preview item when reading replies in image board threads.\ @@ -40,7 +41,7 @@ Press `M` to begin writing a post to a thread (image boards), press `ESC` to can Press `1 to 9` or `Numpad 1 to 9` to select google captcha image when posting a comment on 4chan.\ Press `P` to preview the 4chan image of the selected row in full screen view, press `ESC` or `Backspace` to go back.\ Press `I` to switch between single image and scroll image view mode when reading manga.\ -Press `F` to fit image to window size when reading manga. Press `F` again to show original window size.\ +Press `F` to fit image to window size when reading manga. Press `F` again to show original image size.\ Press `Middle mouse button` to "autoscroll" in scrolling image view mode.\ Press `Tab` to switch between username/password field in login panel.\ Press `Ctrl + C` to copy the url of the currently playing video to the clipboard (with timestamp).\ @@ -50,14 +51,13 @@ Press `M` to begin writing a message in a matrix room, press `ESC` to cancel.\ Press `R` to reply to a message on matrix, press `ESC` to cancel.\ Press `E` to edit a message on matrix, press `ESC` to cancel. Currently only works for your own messages.\ Press `D` to delete a message on matrix. Currently deleting a message only deletes the event, so if you delete an edit then the original message wont be deleted.\ -Press `Ctrl + V` to upload media to room in matrix, if the clipboard contains a path to an absolute filepath. +Press `Ctrl + V` to upload media to room in matrix if the clipboard contains a valid absolute filepath. In matrix you can select a message with enter to open the url in the message (or if there are multiple urls then a menu will appear for selecting which to open). ## Matrix commands -`/upload` to upload an image. TODO: Support regular files and videos.\ -`/logout` to logout. -## Video controls -Press `space` to pause/unpause video. `Double-click` video to fullscreen or leave fullscreen. +`/upload` to upload an image.\ +`/logout` to logout.\ +`/me` to send a message of type "m.emote". # Mangadex To search for manga with mangadex, you need to be logged into mangadex in your browser and copy the `mangadex_rememberme_token` cookie from developer tools and store it in `$HOME/.config/quickmedia/credentials/mangadex.json` under the key `rememberme_token`. Here is an example what the file should look like: @@ -72,14 +72,14 @@ See project.conf \[dependencies]. ## Runtime ### Required `curl` is required for network requests.\ -`noto-fonts` and `noto-fonts-cjk` is required for alphanumerical and japanese characters. +`noto-fonts` and `noto-fonts-cjk` is required for latin and japanese characters. ### Optional `mpv` is required for playing videos. This is not required if you dont plan on playing videos.\ `youtube-dl` needs to be installed to play videos from youtube.\ `notify-send` needs to be installed to show notifications (on Linux and other systems that uses d-bus notification system).\ `torsocks` needs to be installed when using the `--tor` option.\ [automedia](https://git.dec05eba.com/AutoMedia/) needs to be installed when tracking manga with `Ctrl + T`.\ -`waifu2x-ncnn-vulkan` needs to be installed when using the `--upscale-images` option.\ +`waifu2x-ncnn-vulkan` needs to be installed when using the `--upscale-images` or `--upscale-images-always` option.\ `xdg-utils` which provides `xdg-open` needs to be installed when downloading torrents with `nyaa.si` plugin.\ `ffmpeg (and ffprobe)` to upload videos with thumbnails on matrix. # Screenshots @@ -90,7 +90,6 @@ Allow choosing which translation/scanlation to use on mangadex. Right now it use Add file upload to 4chan. Retry download if it fails, at least 3 times (observed to be needed for mangadex images). Readd autocomplete, but make it better with a proper list. Also readd 4chan login page. -Fix logout/login in matrix. Currently it doesn't work because data is cleared while sync is in progress, leading to the first sync sometimes being with previous data... Modify sfml to use GL_COMPRESSED_LUMINANCE and other texture compression modes (in sf::Texture). This reduces memory usage by half. Decrease memory usage even further (mostly in matrix /sync when part of large rooms) by using rapidjson SAX style API to stream json string into SAX style parsing. Sometimes we fail to get images in mangadex, most common reason being that the manga is licensed and we can't view the manga on mangadex. QuickMedia should implement mangaplus and redirect us to mangaplus plugin to view the manga, or simply show that we cant view the manga because its licensed. @@ -120,4 +119,7 @@ Implement our own encryption for matrix. This is also needed to make forwarded m Modify matrix sync to download and parse json but not handle it, and then add a function to handle the json. This would allow us to remove all the mutex code if we would call that new method from the main thread. Room list in matrix ignores edited messages, which it should for unread messages but it should show the edited message if the edited message is the last message in the room. For messages that mention us we only want a notification for the last edited version to show as a notification. -Fetch replies/pinned message using multiple threads.
\ No newline at end of file +Fetch replies/pinned message using multiple threads. +Replying to edited message shows incorrect body in matrix. +Show in room tags list when there is a message in any of the rooms in the tag. +Apply current search filter when adding new rooms to the room list.
\ No newline at end of file diff --git a/images/dropdown.png b/images/dropdown.png Binary files differnew file mode 100644 index 0000000..71c5eb7 --- /dev/null +++ b/images/dropdown.png diff --git a/images/loading_icon.png b/images/loading_icon.png Binary files differindex 6c11fbc..dab9e00 100644 --- a/images/loading_icon.png +++ b/images/loading_icon.png diff --git a/include/AsyncImageLoader.hpp b/include/AsyncImageLoader.hpp index 3189565..c1c2e11 100644 --- a/include/AsyncImageLoader.hpp +++ b/include/AsyncImageLoader.hpp @@ -1,16 +1,13 @@ #pragma once #include "../include/Storage.hpp" +#include "../include/MessageQueue.hpp" #include <SFML/System/Vector2.hpp> #include <SFML/Graphics/Texture.hpp> #include <SFML/System/Clock.hpp> #include <string> -#include <vector> #include <memory> #include <thread> -#include <mutex> -#include <condition_variable> -#include <deque> namespace QuickMedia { enum class LoadingState { @@ -55,9 +52,6 @@ namespace QuickMedia { // TODO: Use curl single-threaded multi-download feature instead std::thread download_image_thread[NUM_IMAGE_LOAD_THREADS]; std::thread load_image_thread; - std::mutex load_image_mutex; - std::condition_variable load_image_cv; - std::deque<ThumbnailLoadData> images_to_load; - bool running = true; + MessageQueue<ThumbnailLoadData> image_load_queue; }; }
\ No newline at end of file diff --git a/include/Body.hpp b/include/Body.hpp index f3498c7..9cdcd7b 100644 --- a/include/Body.hpp +++ b/include/Body.hpp @@ -180,7 +180,7 @@ namespace QuickMedia { // because of Text::setMaxWidth void draw_item(sf::RenderWindow &window, BodyItem *item, sf::Vector2f pos, sf::Vector2f size, bool include_embedded_item = true); - float get_item_height(BodyItem *item, bool load_texture = true, bool include_embedded_item = true); + float get_item_height(BodyItem *item, float width, bool load_texture = true, bool include_embedded_item = true); float get_spacing_y() const; static bool string_find_case_insensitive(const std::string &str, const std::string &substr); @@ -216,7 +216,7 @@ namespace QuickMedia { sf::Shader *thumbnail_mask_shader; private: void draw_item(sf::RenderWindow &window, BodyItem *item, const sf::Vector2f &pos, const sf::Vector2f &size, const float item_height, const int item_index, const Json::Value &content_progress, bool include_embedded_item = true); - void update_dirty_state(BodyItem *body_item, sf::Vector2f size); + void update_dirty_state(BodyItem *body_item, float width); void clear_body_item_cache(BodyItem *body_item); sf::Vector2i get_item_thumbnail_size(BodyItem *item) const; private: diff --git a/include/Entry.hpp b/include/Entry.hpp index 23535e4..27c3517 100644 --- a/include/Entry.hpp +++ b/include/Entry.hpp @@ -13,7 +13,7 @@ namespace sf { namespace QuickMedia { // Return true to clear the text - using OnEntrySubmit = std::function<bool(const std::string& text)>; + using OnEntrySubmit = std::function<bool(std::string text)>; class Entry { public: diff --git a/include/MessageQueue.hpp b/include/MessageQueue.hpp new file mode 100644 index 0000000..174a227 --- /dev/null +++ b/include/MessageQueue.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include <deque> +#include <mutex> +#include <condition_variable> +#include <optional> + +namespace QuickMedia { + template <typename T> + class MessageQueue { + public: + MessageQueue() : running(true) { + + } + + void push(T data) { + std::unique_lock<std::mutex> lock(mutex); + data_queue.push_back(std::move(data)); + cv.notify_one(); + } + + std::optional<T> pop_wait() { + if(!running) + return std::nullopt; + std::unique_lock<std::mutex> lock(mutex); + while(data_queue.empty() && running) cv.wait(lock); + if(!running) + return std::nullopt; + T data = std::move(data_queue.front()); + data_queue.pop_front(); + return data; + } + + void close() { + std::unique_lock<std::mutex> lock(mutex); + running = false; + cv.notify_one(); + } + + void clear() { + std::unique_lock<std::mutex> lock(mutex); + data_queue.clear(); + } + private: + std::deque<T> data_queue; + std::mutex mutex; + std::condition_variable cv; + bool running; + }; +}
\ No newline at end of file diff --git a/include/Path.hpp b/include/Path.hpp index d26f605..46c8dee 100644 --- a/include/Path.hpp +++ b/include/Path.hpp @@ -24,7 +24,7 @@ namespace QuickMedia { const char* filename() const { size_t index = data.rfind('/'); if(index == std::string::npos) - return "/"; + return data.c_str(); return data.c_str() + index + 1; } diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index 45c499a..bdbafef 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -5,6 +5,7 @@ #include "Page.hpp" #include "Storage.hpp" #include "Tab.hpp" +#include "MessageQueue.hpp" #include <vector> #include <memory> #include <SFML/Graphics/Font.hpp> @@ -14,10 +15,7 @@ #include <unordered_set> #include <future> #include <thread> -#include <mutex> -#include <condition_variable> #include <stack> -#include <deque> #include <X11/Xlib.h> #include <X11/Xatom.h> @@ -26,6 +24,8 @@ namespace QuickMedia { class FileManager; class MangaImagesPage; class ImageBoardThreadPage; + class RoomData; + class MatrixChatPage; enum class ImageViewMode { SINGLE, @@ -50,16 +50,19 @@ namespace QuickMedia { bool load_manga_content_storage(const char *service_name, const std::string &manga_title, const std::string &manga_id); void select_file(const std::string &filepath); + + bool is_window_focused(); + RoomData* get_current_chat_room(); private: void base_event_handler(sf::Event &event, PageType previous_page, Body *body, SearchBar *search_bar, bool handle_key_press = true, bool handle_searchbar = true); - void page_loop(std::vector<Tab> tabs); + void page_loop(std::vector<Tab> &tabs); void video_content_page(Page *page, std::string video_url, std::string video_title); // Returns -1 to go to previous chapter, 0 to stay on same chapter and 1 to go to next chapter int image_page(MangaImagesPage *images_page, Body *chapters_body); void image_continuous_page(MangaImagesPage *images_page); void image_board_thread_page(ImageBoardThreadPage *thread_page, Body *thread_body); void chat_login_page(); - void chat_page(); + void chat_page(MatrixChatPage *chat_page, RoomData *current_room); enum class LoadImageResult { OK, @@ -110,10 +113,8 @@ namespace QuickMedia { std::future<std::string> autocomplete_future; std::future<void> image_download_future; std::thread image_upscale_thead; - std::mutex image_upscale_mutex; - std::deque<CopyOp> images_to_upscale; + MessageQueue<CopyOp> images_to_upscale_queue; std::vector<char> image_upscale_status; - std::condition_variable image_upscale_cv; std::string downloading_chapter_url; bool image_download_cancel = false; int exit_code = 0; @@ -127,5 +128,6 @@ namespace QuickMedia { ImageViewMode image_view_mode = ImageViewMode::SINGLE; std::vector<std::string> selected_files; bool fit_image_to_window = false; + RoomData *current_chat_room = nullptr; }; }
\ No newline at end of file diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index f0ca4f5..281f5a8 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -5,24 +5,11 @@ #include "Page.hpp" #include <SFML/Graphics/Color.hpp> #include <unordered_map> +#include <set> #include <mutex> #include <rapidjson/fwd.h> namespace QuickMedia { - // Dummy, only play one video. TODO: Play all videos in room, as related videos? - class MatrixVideoPage : public Page { - public: - MatrixVideoPage(Program *program) : Page(program) {} - const char* get_title() const override { return ""; } - PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override { - (void)title; - (void)url; - (void)result_tabs; - return PluginResult::ERR; - } - PageTypez get_type() const override { return PageTypez::VIDEO; } - }; - struct RoomData; struct UserInfo { @@ -78,8 +65,6 @@ namespace QuickMedia { // Ignores duplicates void append_messages(const std::vector<std::shared_ptr<Message>> &new_messages); - void append_pinned_events(std::vector<std::string> new_pinned_events); - std::shared_ptr<Message> get_message_by_id(const std::string &id); std::vector<std::shared_ptr<UserInfo>> get_users_excluding_me(const std::string &my_user_id); @@ -96,12 +81,16 @@ namespace QuickMedia { bool has_name(); void set_name(const std::string &new_name); + // TODO: Remove this std::string get_name(); bool has_avatar_url(); void set_avatar_url(const std::string &new_avatar_url); std::string get_avatar_url(); + void set_pinned_events(std::vector<std::string> new_pinned_events); + std::set<std::string>& get_tags_unsafe(); + std::string id; bool initial_fetch_finished = false; @@ -116,6 +105,9 @@ namespace QuickMedia { // TODO: Verify if replied to messages are also part of /sync; then this is not needed. std::unordered_map<std::string, std::shared_ptr<Message>> fetched_messages_by_event_id; + size_t messages_read_index = 0; + bool pinned_events_updated = false; + size_t index; private: std::mutex user_mutex; @@ -131,6 +123,7 @@ namespace QuickMedia { std::vector<std::shared_ptr<Message>> messages; std::unordered_map<std::string, std::shared_ptr<Message>> message_by_event_id; std::vector<std::string> pinned_events; + std::set<std::string> tags; }; enum class MessageDirection { @@ -150,25 +143,158 @@ namespace QuickMedia { struct SyncData { Messages messages; - std::vector<std::string> pinned_events; + std::optional<std::vector<std::string>> pinned_events; + std::optional<std::vector<std::string>> tags; }; - using RoomSyncData = std::unordered_map<RoomData*, SyncData>; using Rooms = std::vector<RoomData*>; bool message_contains_user_mention(const std::string &msg, const std::string &username); + enum class MatrixPageType { + ROOM_LIST, + CHAT + }; + + class MatrixDelegate { + public: + virtual ~MatrixDelegate() = default; + + virtual void room_create(RoomData *room) = 0; + // Note: calling |room| methods inside this function is not allowed + virtual void room_add_tag(RoomData *room, const std::string &tag) = 0; + // Note: calling |room| methods inside this function is not allowed + virtual void room_remove_tag(RoomData *room, const std::string &tag) = 0; + virtual void room_add_new_messages(RoomData *room, const Messages &messages, bool is_initial_sync) = 0; + + virtual void update(MatrixPageType page_type) { (void)page_type; } + }; + + class Matrix; + class MatrixRoomsPage; + class MatrixRoomTagsPage; + + class MatrixQuickMedia : public MatrixDelegate { + public: + MatrixQuickMedia(Program *program, Matrix *matrix, MatrixRoomsPage *rooms_page, MatrixRoomTagsPage *room_tags_page); + + void room_create(RoomData *room) override; + void room_add_tag(RoomData *room, const std::string &tag) override; + void room_remove_tag(RoomData *room, const std::string &tag) override; + void room_add_new_messages(RoomData *room, const Messages &messages, bool is_initial_sync) override; + + void update(MatrixPageType page_type) override; + + Program *program; + Matrix *matrix; + MatrixRoomsPage *rooms_page; + MatrixRoomTagsPage *room_tags_page; + private: + struct RoomMessagesData { + Messages messages; + bool is_initial_sync; + }; + + std::vector<std::shared_ptr<BodyItem>> room_body_items; + std::map<RoomData*, std::shared_ptr<BodyItem>> room_body_item_by_room; + std::map<RoomData*, RoomMessagesData> pending_room_messages; + std::mutex pending_room_messages_mutex; + }; + + class MatrixRoomsPage : public Page { + public: + MatrixRoomsPage(Program *program, Body *body, std::string title, MatrixRoomTagsPage *room_tags_page = nullptr); + ~MatrixRoomsPage() override; + + const char* get_title() const override { return title.c_str(); } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; + + void update() override; + void add_body_item(std::shared_ptr<BodyItem> body_item); + + void move_room_to_top(RoomData *room); + + MatrixQuickMedia *matrix_delegate = nullptr; + private: + std::mutex mutex; + std::vector<std::shared_ptr<BodyItem>> room_body_items; + Body *body; + std::string title; + MatrixRoomTagsPage *room_tags_page; + }; + + class MatrixRoomTagsPage : public Page { + public: + MatrixRoomTagsPage(Program *program, Body *body) : Page(program), body(body) {} + const char* get_title() const override { return "Tags"; } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; + + void update() override; + void add_room_body_item_to_tag(std::shared_ptr<BodyItem> body_item, const std::string &tag); + void remove_room_body_item_from_tag(std::shared_ptr<BodyItem> body_item, const std::string &tag); + + void move_room_to_top(RoomData *room); + + MatrixQuickMedia *matrix_delegate = nullptr; + MatrixRoomsPage *current_rooms_page = nullptr; + private: + struct TagData { + std::shared_ptr<BodyItem> tag_item; + std::vector<std::shared_ptr<BodyItem>> room_body_items; + }; + + std::mutex mutex; + Body *body; + std::map<std::string, TagData> tag_body_items_by_name; + std::map<std::string, std::vector<std::shared_ptr<BodyItem>>> add_room_body_items_by_tags; + std::map<std::string, std::vector<std::shared_ptr<BodyItem>>> remove_room_body_items_by_tags; + }; + + // Dummy, only play one video. TODO: Play all videos in room, as related videos? + class MatrixVideoPage : public Page { + public: + MatrixVideoPage(Program *program) : Page(program) {} + const char* get_title() const override { return ""; } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override { + (void)title; + (void)url; + (void)result_tabs; + return PluginResult::ERR; + } + PageTypez get_type() const override { return PageTypez::VIDEO; } + }; + + class MatrixChatPage : public Page { + public: + MatrixChatPage(Program *program, std::string room_id) : Page(program), room_id(std::move(room_id)) {} + const char* get_title() const override { return ""; } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override { + (void)title; + (void)url; + (void)result_tabs; + return PluginResult::ERR; + } + PageTypez get_type() const override { return PageTypez::CHAT; } + void update() override; + + const std::string room_id; + MatrixQuickMedia *matrix_delegate = nullptr; + }; + class Matrix { public: - PluginResult sync(RoomSyncData &room_sync_data); - void get_room_join_updates(Rooms &new_rooms); + void start_sync(MatrixDelegate *delegate); + void stop_sync(); + bool is_initial_sync_finished() const; + void get_room_sync_data(RoomData *room, SyncData &sync_data); + void get_all_synced_room_messages(RoomData *room, Messages &messages); void get_all_pinned_events(RoomData *room, std::vector<std::string> &events); PluginResult get_previous_room_messages(RoomData *room, Messages &messages); // |url| should only be set when uploading media. // TODO: Make api better. - PluginResult post_message(RoomData *room, const std::string &body, const std::optional<UploadInfo> &file_info, const std::optional<UploadInfo> &thumbnail_info); + PluginResult post_message(RoomData *room, const std::string &body, const std::optional<UploadInfo> &file_info, const std::optional<UploadInfo> &thumbnail_info, const std::string &msgtype = ""); // |relates_to| is from |BodyItem.userdata| and is of type |Message*| PluginResult post_reply(RoomData *room, const std::string &body, void *relates_to); // |relates_to| is from |BodyItem.userdata| and is of type |Message*| @@ -201,28 +327,27 @@ namespace QuickMedia { // Returns nullptr if message cant be found. Note: cached std::shared_ptr<Message> get_message_by_id(RoomData *room, const std::string &event_id); + RoomData* get_room_by_id(const std::string &id); + bool use_tor = false; private: - PluginResult sync_response_to_body_items(const rapidjson::Document &root, RoomSyncData &room_sync_data); + PluginResult parse_sync_response(const rapidjson::Document &root, MatrixDelegate *delegate); + PluginResult parse_sync_account_data(const rapidjson::Value &account_data_json, std::optional<std::set<std::string>> &dm_rooms); + PluginResult parse_sync_room_data(const rapidjson::Value &rooms_json, MatrixDelegate *delegate); PluginResult get_previous_room_messages(RoomData *room_data); void events_add_user_info(const rapidjson::Value &events_json, RoomData *room_data); void events_add_user_read_markers(const rapidjson::Value &events_json, RoomData *room_data); - void events_add_messages(const rapidjson::Value &events_json, RoomData *room_data, MessageDirection message_dir, RoomSyncData *room_sync_data, bool has_unread_notifications); + void events_add_messages(const rapidjson::Value &events_json, RoomData *room_data, MessageDirection message_dir, MatrixDelegate *delegate, bool has_unread_notifications); void events_set_room_name(const rapidjson::Value &events_json, RoomData *room_data); - void events_add_pinned_events(const rapidjson::Value &events_json, RoomData *room_data, RoomSyncData &room_sync_data); - void events_add_room_to_tags(const rapidjson::Value &events_json, RoomData *room_data); + void events_add_pinned_events(const rapidjson::Value &events_json, RoomData *room_data); + void events_add_room_to_tags(const rapidjson::Value &events_json, RoomData *room_data, MatrixDelegate *delegate); std::shared_ptr<Message> parse_message_event(const rapidjson::Value &event_item_json, RoomData *room_data); PluginResult upload_file(RoomData *room, const std::string &filepath, UploadInfo &file_info, UploadInfo &thumbnail_info, std::string &err_msg); - - std::shared_ptr<Message> get_edited_message_original_message(RoomData *room_data, std::shared_ptr<Message> message); - - RoomData* get_room_by_id(const std::string &id); void add_room(std::unique_ptr<RoomData> room); DownloadResult download_json(rapidjson::Document &result, const std::string &url, std::vector<CommandArg> additional_args, bool use_browser_useragent = false, std::string *err_msg = nullptr) const; private: std::vector<std::unique_ptr<RoomData>> rooms; std::unordered_map<std::string, size_t> room_data_by_id; // value is an index into |rooms| - std::map<std::string, std::vector<size_t>> rooms_by_tag_name; // value is an index into |rooms| size_t room_list_read_index = 0; std::mutex room_data_mutex; std::string user_id; @@ -231,5 +356,8 @@ namespace QuickMedia { std::string homeserver; std::optional<int> upload_limit; std::string next_batch; + + std::thread sync_thread; + bool sync_running = false; }; }
\ No newline at end of file diff --git a/plugins/Page.hpp b/plugins/Page.hpp index de80b4f..cc7dad6 100644 --- a/plugins/Page.hpp +++ b/plugins/Page.hpp @@ -14,7 +14,8 @@ namespace QuickMedia { REGULAR, MANGA_IMAGES, IMAGE_BOARD_THREAD, - VIDEO + VIDEO, + CHAT }; class Page { @@ -46,6 +47,9 @@ namespace QuickMedia { // This is called both when first navigating to page and when going back to page virtual void on_navigate_to_page() {}; + // Called periodically (every frame right now) if this page is the currently active one + virtual void update() {} + bool is_tor_enabled(); std::unique_ptr<Body> create_body(); std::unique_ptr<SearchBar> create_search_bar(const std::string &placeholder_text, int search_delay); 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<ThumbnailLoadData> thumbnail_load_data_opt; while(true) { - { - std::unique_lock<std::mutex> 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<sf::Image>(); 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<Text>(std::move(str), font, cjk_font, 16, size.x - 50 - image_padding_x * 2.0f); + body_item->title_text = std::make_unique<Text>(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<Text>(std::move(str), font, cjk_font, 14, size.x - 50 - image_padding_x * 2.0f); + body_item->description_text = std::make_unique<Text>(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<Text>(std::move(str), bold_font, cjk_font, 14, size.x - 50 - image_padding_x * 2.0f); + body_item->author_text = std::make_unique<Text>(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<CopyOp> copy_op_opt; while(true) { - { - std::unique_lock<std::mutex> 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<MatrixRoomsPage>(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<MatrixRoomTagsPage>(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<Body> Program::create_body() { - return std::make_unique<Body>(this, font.get(), bold_font.get(), cjk_font.get(), loading_icon); + auto body = std::make_unique<Body>(this, font.get(), bold_font.get(), cjk_font.get(), loading_icon); + body->thumbnail_mask_shader = &circle_mask_shader; + return body; } std::unique_ptr<SearchBar> 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<Tab> 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<Tab> &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<ImageBoardThreadPage*>(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<MatrixChatPage*>(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<TrackablePage*>(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<TrackablePage*>(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<LazyFetchPage*>(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<LazyFetchPage*>(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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<MatrixVideoPage>(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<Body>(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<RoomData*>(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<RoomData*>(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<UserInfo> 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<BodyItem*>(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<BodyItem> 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<std::string> &pinned_events) { + auto process_pinned_events = [&tabs](const std::optional<std::vector<std::string>> &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<std::string> 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<BodyItem*>(current_room->userdata)->get_title()); + room_avatar_thumbnail_data = std::make_shared<ThumbnailData>(); - 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<std::string> 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<BodyItem*>(current_room->userdata)->get_title()); - room_avatar_thumbnail_data = std::make_shared<ThumbnailData>(); - - 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<SyncFutureResult> sync_future; - bool sync_running = false; - sf::Clock sync_timer; - sf::Int32 sync_min_time_ms = 0; // Sync immediately the first time - std::future<Messages> 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<Message*>(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<Message*>(selected->userdata); + } else if(selected_tab == PINNED_TAB_INDEX && static_cast<PinnedEventData*>(selected->userdata)->status == FetchStatus::FINISHED_LOADING) { + selected_item_message = static_cast<PinnedEventData*>(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<BodyItem*>(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<std::string> 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<Message*>(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<Message*>(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<BodyItem> 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<BodyItem> 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<std::string> 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<BodyItem> 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<BodyItem> 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<Tab> &result_tabs) { - result_tabs.push_back(Tab{create_body(), std::make_unique<MangadexImagesPage>(program, content_title, title, url), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique<MangadexImagesPage>(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<Tab> &result_tabs) { - result_tabs.push_back(Tab{create_body(), std::make_unique<ManganeloImagesPage>(program, content_title, title, url), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique<ManganeloImagesPage>(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<Tab> &result_tabs) { - result_tabs.push_back(Tab{create_body(), std::make_unique<MangatownImagesPage>(program, content_title, title, url), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique<MangatownImagesPage>(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 <rapidjson/document.h> #include <rapidjson/writer.h> #include <rapidjson/stringbuffer.h> #include <fcntl.h> #include <unistd.h> +#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<UserInfo> RoomData::get_user_by_id(const std::string &user_id) { std::lock_guard<std::mutex> lock(room_mutex); @@ -75,11 +112,6 @@ namespace QuickMedia { } } - void RoomData::append_pinned_events(std::vector<std::string> new_pinned_events) { - std::lock_guard<std::mutex> lock(room_mutex); - pinned_events.insert(pinned_events.end(), new_pinned_events.begin(), new_pinned_events.end()); - } - std::shared_ptr<Message> RoomData::get_message_by_id(const std::string &id) { std::lock_guard<std::mutex> 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<CommandArg> additional_args = { - { "-H", "Authorization: Bearer " + access_token }, - { "-m", "35" } - }; + void RoomData::set_pinned_events(std::vector<std::string> new_pinned_events) { + std::lock_guard<std::mutex> 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<std::string>& 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<std::mutex> 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<RoomData*>(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<RoomData*>(body_item->userdata)->has_unread_mention || body_item.get() == item_to_swap) + return i; + } + return -1; + } + + static void sort_room_body_items(std::vector<std::shared_ptr<BodyItem>> &room_body_items) { + std::sort(room_body_items.begin(), room_body_items.end(), [](const std::shared_ptr<BodyItem> &body_item1, const std::shared_ptr<BodyItem> &body_item2) { + RoomData *room1 = static_cast<RoomData*>(body_item1->userdata); + RoomData *room2 = static_cast<RoomData*>(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<std::mutex> 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<UserInfo> 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<BodyItem*>(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<Tab> &result_tabs) { + (void)title; + auto chat_page = std::make_unique<MatrixChatPage>(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<std::mutex> 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<std::mutex> lock(mutex); + body->append_items(std::move(room_body_items)); + } + matrix_delegate->update(MatrixPageType::ROOM_LIST); + } + + void MatrixRoomsPage::add_body_item(std::shared_ptr<BodyItem> body_item) { + std::lock_guard<std::mutex> 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<std::mutex> lock(mutex); + BodyItem *room_body_item = static_cast<BodyItem*>(room->userdata); + int room_body_index = body->get_index_by_body_item(room_body_item); + if(room_body_index != -1) { + std::shared_ptr<BodyItem> 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<Tab> &result_tabs) { + (void)title; + std::lock_guard<std::mutex> 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<MatrixRoomsPage>(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<std::mutex> 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<BodyItem> body_item, const std::string &tag) { + std::lock_guard<std::mutex> lock(mutex); + add_room_body_items_by_tags[tag].push_back(body_item); + } + + void MatrixRoomTagsPage::remove_room_body_item_from_tag(std::shared_ptr<BodyItem> body_item, const std::string &tag) { + std::lock_guard<std::mutex> 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<CommandArg> 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<std::string> &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<std::set<std::string>> 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<std::set<std::string>> &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<std::string> 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<RoomData>(); 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<std::string> &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<std::shared_ptr<Message>> 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<Message> 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<std::string> 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<std::string> pinned_events; + bool has_tags = false; + std::set<std::string> 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<std::string> &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<UploadInfo> &file_info, const std::optional<UploadInfo> &thumbnail_info) { + PluginResult Matrix::post_message(RoomData *room, const std::string &body, const std::optional<UploadInfo> &file_info, const std::optional<UploadInfo> &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<Message> instead of raw pointer... Message *relates_to_message_raw = (Message*)relates_to; std::shared_ptr<Message> relates_to_message_shared = room->get_message_by_id(relates_to_message_raw->event_id); - std::shared_ptr<Message> 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<Message> relates_to_message_shared = room->get_message_by_id(relates_to_message_raw->event_id); - std::shared_ptr<Message> 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/<room_id>/context/<event_id> 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<Message> Matrix::get_edited_message_original_message(RoomData *room_data, std::shared_ptr<Message> 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<Message> Matrix::get_message_by_id(RoomData *room, const std::string &event_id) { std::shared_ptr<Message> 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("<tbody>"); if(tbody_begin == std::string::npos) - return SearchResult::ERR; + return SearchResult::OK; size_t tbody_end = website_data.find("</tbody>", 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<Tab> &result_tabs) { (void)title; (void)url; - result_tabs.push_back(Tab{create_body(), std::make_unique<PornhubVideoPage>(program), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique<PornhubVideoPage>(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<Tab> &result_tabs) { (void)title; (void)url; - result_tabs.push_back(Tab{create_body(), std::make_unique<YoutubeVideoPage>(program), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(program), nullptr}); return PluginResult::OK; } |