From e19a29c7e51860144f02d7e7b08ac5e430e1f78f Mon Sep 17 00:00:00 2001 From: dec05eba Date: Mon, 7 Nov 2022 22:21:52 +0100 Subject: Support images in text, add custom emoji to matrix --- TODO | 8 +- depends/html-parser | 2 +- depends/html-search | 2 +- depends/mglpp | 2 +- include/Text.hpp | 7 +- include/types.hpp | 10 - plugins/Matrix.hpp | 71 +++++- src/Body.cpp | 6 +- src/QuickMedia.cpp | 49 ++-- src/Tabs.cpp | 1 + src/Text.cpp | 138 +++++++++--- src/main.cpp | 4 +- src/plugins/Matrix.cpp | 595 +++++++++++++++++++++++++++++++++++++++++-------- 13 files changed, 719 insertions(+), 176 deletions(-) delete mode 100644 include/types.hpp diff --git a/TODO b/TODO index 50571d4..ed04741 100644 --- a/TODO +++ b/TODO @@ -5,7 +5,7 @@ Animate page navigation. Add support for special formatting for posts by admins on imageboards. For image boards, track (You)'s and show notification when somebody replies to your post. Go to next chapter when reaching the end of the chapter in image endless mode. -Make code blocks on matrix and 4chan use monospace and have a background of a different color. +Make code blocks on matrix and 4chan have a background of a different color. Allow deleting watch history with delete key (and show confirmation). Add navigation to nyaa.si submitter torrents. Create a large texture and add downloaded images to it. This will save memory usage because sfml has to use power of two textures (and so does opengl internally) for textures, so if you have multiple textures they will use more memory than one large texture with the same texture data. @@ -83,7 +83,6 @@ Improve /sync by not removing cached data on initial sync, and also always appen then add a gap between old messages from before sync and after sync so we can fetch the messages between the old messages and new messages and remove the gap when the fetched messages contains any of the old messages. After the sync, ignored users messages should be removed from the cache and messages list. Also take into consideration unignoring users. Fetching of previous messages should also be saved in the /sync file and messages fetched with get_message_by_id, which would cache embedded items and pinned messages; also cache users. If manga page fails to download then show "failed to download image" as text and bind F5 to refresh (retry download). -Use #include #include -#include "types.hpp" -#include namespace mgl { class Font; @@ -143,7 +141,7 @@ namespace QuickMedia bool single_line_edit = false; private: - enum class CaretMoveDirection : u8 + enum class CaretMoveDirection : uint8_t { NONE, UP, @@ -172,7 +170,10 @@ namespace QuickMedia float font_get_real_height(mgl::Font *font); float get_text_quad_left_side(const VertexRef &vertex_ref) const; float get_text_quad_right_side(const VertexRef &vertex_ref) const; + float get_text_quad_top_side(const VertexRef &vertex_ref) const; float get_text_quad_bottom_side(const VertexRef &vertex_ref) const; + float get_text_quad_height(const VertexRef &vertex_ref) const; + void move_vertex_lines_by_largest_items(int vertices_linear_end); // If the index is past the end, then the caret offset is the right side of the last character, rather than the left side float get_caret_offset_by_caret_index(int index) const; VertexRef& get_vertex_ref_clamp(int index); diff --git a/include/types.hpp b/include/types.hpp deleted file mode 100644 index dc2a016..0000000 --- a/include/types.hpp +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include - -typedef uint8_t u8; -typedef uint16_t u16; -typedef uint32_t u32; -typedef uint64_t u64; -typedef intptr_t isize; -typedef uintptr_t usize; \ No newline at end of file diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index 152c292..3613709 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -20,9 +20,12 @@ namespace QuickMedia { static const int AUTHOR_MAX_LENGTH = 48; + class Matrix; + std::string extract_first_line_remove_newline_elipses(const std::string &str, size_t max_length); mgl::Color user_id_to_color(const std::string &user_id); - std::string formatted_text_to_qm_text(const char *str, size_t size, bool allow_formatted_text); + std::string formatted_text_to_qm_text(Matrix *matrix, const char *str, size_t size, bool allow_formatted_text); + std::string message_to_qm_text(Matrix *matrix, const Message *message, bool allow_formatted_text = true); struct TimestampedDisplayData { std::string data; @@ -233,7 +236,7 @@ namespace QuickMedia { using Rooms = std::vector; - bool message_contains_user_mention(const Message *message, const std::string &username, const std::string &user_id); + bool message_contains_user_mention(Matrix *matrix, const Message *message, const std::string &username, const std::string &user_id); bool message_contains_user_mention(const BodyItem *body_item, const std::string &username, const std::string &user_id); bool message_is_timeline(Message *message); void body_set_selected_item_by_url(Body *body, const std::string &url); @@ -450,6 +453,50 @@ namespace QuickMedia { Matrix *matrix; }; + class MatrixCustomEmojiPage : public LazyFetchPage { + public: + MatrixCustomEmojiPage(Program *program, Matrix *matrix) : LazyFetchPage(program), matrix(matrix) {} + const char* get_title() const override { return "Custom emoji"; } + PluginResult submit(const SubmitArgs &args, std::vector &result_tabs) override; + PluginResult lazy_fetch(BodyItems &result_items) override; + bool is_ready() override; + private: + Matrix *matrix; + }; + + class MatrixCustomEmojiRenameSelectPage : public LazyFetchPage { + public: + MatrixCustomEmojiRenameSelectPage(Program *program, Matrix *matrix) : LazyFetchPage(program), matrix(matrix) {} + const char* get_title() const override { return "Select emoji to rename"; } + PluginResult submit(const SubmitArgs &args, std::vector &result_tabs) override; + PluginResult lazy_fetch(BodyItems &result_items) override; + bool submit_is_async() const override { return false; } + bool reload_on_page_change() override { return true; } + private: + Matrix *matrix; + }; + + class MatrixCustomEmojiRenamePage : public Page { + public: + MatrixCustomEmojiRenamePage(Program *program, Matrix *matrix, std::string emoji_key) : Page(program), matrix(matrix), emoji_key(std::move(emoji_key)) {} + const char* get_title() const override { return "Enter a new name for the emoji"; } + PluginResult submit(const SubmitArgs &args, std::vector &result_tabs) override; + bool allow_submit_no_selection() const override { return true; } + private: + Matrix *matrix; + std::string emoji_key; + }; + + class MatrixCustomEmojiDeletePage : public Page { + public: + MatrixCustomEmojiDeletePage(Program *program, Matrix *matrix, Body *body) : Page(program), matrix(matrix), body(body) {} + const char* get_title() const override { return "Select emoji to delete"; } + PluginResult submit(const SubmitArgs &args, std::vector &result_tabs) override; + private: + Matrix *matrix; + Body *body; + }; + // Only play one video. TODO: Play all videos in room, as related videos? class MatrixVideoPage : public VideoPage { public: @@ -559,12 +606,17 @@ namespace QuickMedia { std::string room_id; }; + struct CustomEmoji { + std::string url; + mgl::vec2i size; + }; + class Matrix { public: // TODO: Make this return the Matrix object instead, to force users to call start_sync bool start_sync(MatrixDelegate *delegate, bool &cached); void stop_sync(); - bool is_initial_sync_finished() const; + bool is_initial_sync_finished(); // Returns true if initial sync failed, and |err_msg| is set to the error reason in that case bool did_initial_sync_fail(std::string &err_msg); bool has_finished_fetching_notifications() const; @@ -590,6 +642,11 @@ namespace QuickMedia { // If filename is empty then the filename is extracted from filepath PluginResult post_file(RoomData *room, const std::string &filepath, std::string filename, std::string &event_id_response, std::string &err_msg, void *relates_to = nullptr); + PluginResult upload_custom_emoji(const std::string &filepath, const std::string &key, std::string &mxc_url, std::string &err_msg); + bool delete_custom_emoji(const std::string &key); + bool rename_custom_emoji(const std::string &key, const std::string &new_key); + bool does_custom_emoji_with_name_exist(const std::string &name); + std::unordered_map get_custom_emojis(); PluginResult login(const std::string &username, const std::string &password, const std::string &homeserver, std::string &err_msg); PluginResult logout(); @@ -636,6 +693,8 @@ namespace QuickMedia { RoomData* get_room_by_id(const std::string &id); void update_room_users(RoomData *room); + std::string get_media_url(const std::string &mxc_id); + void append_system_message(RoomData *room_data, std::shared_ptr message); std::string body_to_formatted_body(RoomData *room, const std::string &body); void on_exit_room(RoomData *room); @@ -657,7 +716,7 @@ namespace QuickMedia { PluginResult parse_sync_response(const rapidjson::Document &root, bool is_additional_messages_sync, bool initial_sync); PluginResult parse_notifications(const rapidjson::Value ¬ifications_json, std::function callback_func); - PluginResult parse_sync_account_data(const rapidjson::Value &account_data_json, std::optional> &dm_rooms); + PluginResult parse_sync_account_data(const rapidjson::Value &account_data_json); PluginResult parse_sync_room_data(const rapidjson::Value &rooms_json, bool is_additional_messages_sync, bool initial_sync); PluginResult get_previous_room_messages(RoomData *room_data, bool latest_messages, size_t &num_new_messages, bool *reached_end = nullptr); void events_add_user_info(const rapidjson::Value &events_json, RoomData *room_data, int64_t timestamp); @@ -673,7 +732,7 @@ namespace QuickMedia { void remove_rooms(const rapidjson::Value &leave_json); PluginResult get_pinned_events(RoomData *room, std::vector &pinned_events); std::shared_ptr parse_message_event(const rapidjson::Value &event_item_json, RoomData *room_data); - PluginResult upload_file(RoomData *room, const std::string &filepath, std::string filename, UploadInfo &file_info, UploadInfo &thumbnail_info, std::string &err_msg, bool upload_thumbnail = true); + PluginResult upload_file(const std::string &filepath, std::string filename, UploadInfo &file_info, UploadInfo &thumbnail_info, std::string &err_msg, bool upload_thumbnail = true); void add_room(std::unique_ptr room); void remove_room(const std::string &room_id); // Returns false if an invite to the room already exists @@ -701,6 +760,7 @@ namespace QuickMedia { std::string next_batch; std::string next_notifications_token; std::mutex next_batch_mutex; + bool initial_sync_finished = false; std::unordered_map invites; std::mutex invite_mutex; @@ -724,5 +784,6 @@ namespace QuickMedia { std::vector> invite_rooms; std::unordered_set my_events_transaction_ids; + std::unordered_map custom_emoji_by_key; }; } \ No newline at end of file diff --git a/src/Body.cpp b/src/Body.cpp index 5bc4f2a..7411a79 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -59,7 +59,7 @@ namespace QuickMedia { body_spacing[BODY_THEME_MINIMAL].body_padding_vertical = std::floor(10.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MINIMAL].reaction_background_padding_x = std::floor(7.0f * get_config().scale * get_config().spacing_scale); - body_spacing[BODY_THEME_MINIMAL].reaction_background_padding_y = std::floor(3.0f * get_config().scale * get_config().spacing_scale); + body_spacing[BODY_THEME_MINIMAL].reaction_background_padding_y = std::floor(5.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MINIMAL].reaction_spacing_x = std::floor(5.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MINIMAL].reaction_padding_y = std::floor(7.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MINIMAL].embedded_item_font_size = std::floor(get_config().body.embedded_load_font_size * get_config().scale * get_config().font_scale); @@ -76,7 +76,7 @@ namespace QuickMedia { body_spacing[BODY_THEME_MODERN_SPACIOUS].body_padding_vertical = std::floor(20.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MODERN_SPACIOUS].reaction_background_padding_x = std::floor(7.0f * get_config().scale * get_config().spacing_scale); - body_spacing[BODY_THEME_MODERN_SPACIOUS].reaction_background_padding_y = std::floor(3.0f * get_config().scale * get_config().spacing_scale); + body_spacing[BODY_THEME_MODERN_SPACIOUS].reaction_background_padding_y = std::floor(5.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MODERN_SPACIOUS].reaction_spacing_x = std::floor(5.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MODERN_SPACIOUS].reaction_padding_y = std::floor(7.0f * get_config().scale * get_config().spacing_scale); body_spacing[BODY_THEME_MODERN_SPACIOUS].embedded_item_font_size = std::floor(get_config().body.embedded_load_font_size * get_config().scale * get_config().font_scale); @@ -1519,7 +1519,7 @@ namespace QuickMedia { } if(reaction.text) { - reaction.text->set_position(reaction_background.get_position() + mgl::vec2f(body_spacing[body_theme].reaction_background_padding_x, - 4.0f + body_spacing[body_theme].reaction_background_padding_y)); + reaction.text->set_position(reaction_background.get_position() + mgl::vec2f(body_spacing[body_theme].reaction_background_padding_x, -6.0f + body_spacing[body_theme].reaction_background_padding_y)); reaction_background.draw(window); reaction.text->draw(window); diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index c528056..fc3cba6 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -71,7 +71,6 @@ extern "C" { static int FPS_IDLE; static const double IDLE_TIMEOUT_SEC = 2.0; static const mgl::vec2i AVATAR_THUMBNAIL_SIZE(std::floor(32), std::floor(32)); -static const float more_items_height = 2.0f; static const int FPS_SYNC_TO_VSYNC = 0; static const std::pair valid_plugins[] = { @@ -5319,17 +5318,10 @@ namespace QuickMedia { return load_cached_related_embedded_item(body_item, message, me.get(), current_room->get_user_display_name(me), me->user_id, message_body_items); } - static std::string message_to_qm_text(Message *message) { - if(message->body_is_formatted) - return formatted_text_to_qm_text(message->body.c_str(), message->body.size(), true); - else - return message->body; - } - - static std::shared_ptr message_to_body_item(RoomData *room, Message *message, const std::string &my_display_name, const std::string &my_user_id) { + static std::shared_ptr message_to_body_item(Matrix *matrix, RoomData *room, Message *message, const std::string &my_display_name, const std::string &my_user_id) { auto body_item = BodyItem::create(""); body_item->set_author(extract_first_line_remove_newline_elipses(room->get_user_display_name(message->user), AUTHOR_MAX_LENGTH)); - body_item->set_description(strip(message_to_qm_text(message))); + body_item->set_description(strip(message_to_qm_text(matrix, message))); body_item->set_timestamp(message->timestamp); if(!message->thumbnail_url.empty()) { body_item->thumbnail_url = message->thumbnail_url; @@ -5364,10 +5356,10 @@ namespace QuickMedia { return body_item; } - static BodyItems messages_to_body_items(RoomData *room, const Messages &messages, const std::string &my_display_name, const std::string &my_user_id) { + static BodyItems messages_to_body_items(Matrix *matrix, RoomData *room, const Messages &messages, const std::string &my_display_name, const std::string &my_user_id) { BodyItems result_items(messages.size()); for(size_t i = 0; i < messages.size(); ++i) { - result_items[i] = message_to_body_item(room, messages[i].get(), my_display_name, my_user_id); + result_items[i] = message_to_body_item(matrix, room, messages[i].get(), my_display_name, my_user_id); } return result_items; } @@ -5659,10 +5651,10 @@ namespace QuickMedia { // TODO: Properly check reply message objects for mention of user instead of message data, but only when synapse fixes that notifications // are not triggered by reply to a message with our display name/user id. Message *edited_message_ref = static_cast(body_item->userdata); - std::string qm_formatted_text = message_to_qm_text(message.get()); + std::string qm_formatted_text = message_to_qm_text(matrix, message.get()); body_item->set_description(std::move(qm_formatted_text)); - if(message->user != me && message_contains_user_mention(message.get(), my_display_name, me->user_id)) + if(message->user != me && message_contains_user_mention(matrix, message.get(), my_display_name, me->user_id)) body_item->set_description_color(get_theme().attention_alert_text_color, true); else body_item->set_description_color(get_theme().text_color); @@ -5700,10 +5692,10 @@ namespace QuickMedia { // TODO: Properly check reply message objects for mention of user instead of message data, but only when synapse fixes that notifications // are not triggered by reply to a message with our display name/user id. Message *edited_message_ref = static_cast(body_item->userdata); - std::string qm_formatted_text = formatted_text_to_qm_text(message->body.c_str(), message->body.size(), true); + std::string qm_formatted_text = formatted_text_to_qm_text(matrix, message->body.c_str(), message->body.size(), true); body_item->set_description(std::move(qm_formatted_text)); - if(message->user != me && message_contains_user_mention(message.get(), my_display_name, me->user_id)) + if(message->user != me && message_contains_user_mention(matrix, message.get(), my_display_name, me->user_id)) body_item->set_description_color(get_theme().attention_alert_text_color, true); else body_item->set_description_color(get_theme().text_color); @@ -5832,7 +5824,7 @@ namespace QuickMedia { fetched_messages_set.insert(message->event_id); } auto me = matrix->get_me(current_room); - auto new_body_items = messages_to_body_items(current_room, all_messages, current_room->get_user_display_name(me), me->user_id); + auto new_body_items = messages_to_body_items(matrix, current_room, all_messages, current_room->get_user_display_name(me), me->user_id); messages_load_cached_related_embedded_item(new_body_items, tabs[MESSAGES_TAB_INDEX].body->get_items(), me, current_room); tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps(std::move(new_body_items)); modify_related_messages_in_current_room(all_messages); @@ -6143,7 +6135,7 @@ namespace QuickMedia { message->type = MessageType::REACTION; message->related_event_type = RelatedEventType::REACTION; message->related_event_id = static_cast(related_to_message)->event_id; - auto body_item = message_to_body_item(current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id); + auto body_item = message_to_body_item(matrix, current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id); load_cached_related_embedded_item(body_item.get(), message.get(), me, current_room, tabs[MESSAGES_TAB_INDEX].body->get_items()); tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps({body_item}); Messages messages; @@ -6158,7 +6150,7 @@ namespace QuickMedia { return provisional_message; }); } else { - auto body_item = message_to_body_item(current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id); + auto body_item = message_to_body_item(matrix, current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id); body_item->set_description_color(get_theme().provisional_message_color); load_cached_related_embedded_item(body_item.get(), message.get(), me, current_room, tabs[MESSAGES_TAB_INDEX].body->get_items()); tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps({body_item}); @@ -6183,7 +6175,7 @@ namespace QuickMedia { void *related_to_message = currently_operating_on_item->userdata; message->related_event_type = RelatedEventType::REPLY; message->related_event_id = static_cast(related_to_message)->event_id; - auto body_item = message_to_body_item(current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id); + auto body_item = message_to_body_item(matrix, current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id); body_item->set_description_color(get_theme().provisional_message_color); load_cached_related_embedded_item(body_item.get(), message.get(), me, current_room, tabs[MESSAGES_TAB_INDEX].body->get_items()); tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps({body_item}); @@ -6212,13 +6204,13 @@ namespace QuickMedia { auto body_item = find_body_item_by_event_id(tabs[MESSAGES_TAB_INDEX].body->get_items().data(), tabs[MESSAGES_TAB_INDEX].body->get_items().size(), message->related_event_id, &body_item_index); if(body_item) { const std::string formatted_text = matrix->body_to_formatted_body(current_room, text); - std::string qm_formatted_text = formatted_text_to_qm_text(formatted_text.c_str(), formatted_text.size(), true); + std::string qm_formatted_text = formatted_text_to_qm_text(matrix, formatted_text.c_str(), formatted_text.size(), true); auto body_item_shared_ptr = tabs[MESSAGES_TAB_INDEX].body->get_item_by_index(body_item_index); body_item_shared_ptr->set_description(std::move(qm_formatted_text)); body_item_shared_ptr->set_description_color(get_theme().provisional_message_color); - auto edit_body_item = message_to_body_item(current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id); + auto edit_body_item = message_to_body_item(matrix, current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id); edit_body_item->visible = false; load_cached_related_embedded_item(edit_body_item.get(), message.get(), me, current_room, tabs[MESSAGES_TAB_INDEX].body->get_items()); tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps({edit_body_item}); @@ -6466,7 +6458,7 @@ namespace QuickMedia { } all_messages.insert(all_messages.end(), message_list->begin(), message_list->end()); - auto new_body_items = messages_to_body_items(current_room, *message_list, current_room->get_user_display_name(me), me->user_id); + auto new_body_items = messages_to_body_items(matrix, current_room, *message_list, current_room->get_user_display_name(me), me->user_id); messages_load_cached_related_embedded_item(new_body_items, tabs[MESSAGES_TAB_INDEX].body->get_items(), me, current_room); tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps(std::move(new_body_items)); modify_related_messages_in_current_room(*message_list); @@ -6536,7 +6528,7 @@ namespace QuickMedia { } }; - auto add_new_messages_to_current_room = [&me, &tabs, &ui_tabs, ¤t_room, MESSAGES_TAB_INDEX, &after_token](Messages &messages) { + auto add_new_messages_to_current_room = [this, &me, &tabs, &ui_tabs, ¤t_room, MESSAGES_TAB_INDEX, &after_token](Messages &messages) { if(messages.empty()) return; @@ -6555,7 +6547,7 @@ namespace QuickMedia { if(!after_token.empty()) scroll_to_end = false; - auto new_body_items = messages_to_body_items(current_room, messages, current_room->get_user_display_name(me), me->user_id); + auto new_body_items = messages_to_body_items(matrix, current_room, messages, current_room->get_user_display_name(me), me->user_id); messages_load_cached_related_embedded_item(new_body_items, tabs[MESSAGES_TAB_INDEX].body->get_items(), me, current_room); tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps(std::move(new_body_items)); if(scroll_to_end) @@ -7489,7 +7481,7 @@ namespace QuickMedia { if(fetch_message_tab == PINNED_TAB_INDEX) { PinnedEventData *event_data = static_cast(fetch_body_item->userdata); if(fetch_message_result.message) { - *fetch_body_item = *message_to_body_item(current_room, fetch_message_result.message.get(), current_room->get_user_display_name(me), me->user_id); + *fetch_body_item = *message_to_body_item(matrix, current_room, fetch_message_result.message.get(), current_room->get_user_display_name(me), me->user_id); event_data->status = FetchStatus::FINISHED_LOADING; event_data->message = fetch_message_result.message.get(); fetch_body_item->userdata = event_data; @@ -7499,7 +7491,7 @@ namespace QuickMedia { } } else if(fetch_message_tab == MESSAGES_TAB_INDEX) { if(fetch_message_result.message) { - fetch_body_item->embedded_item = message_to_body_item(current_room, fetch_message_result.message.get(), current_room->get_user_display_name(me), me->user_id); + fetch_body_item->embedded_item = message_to_body_item(matrix, current_room, fetch_message_result.message.get(), current_room->get_user_display_name(me), me->user_id); fetch_body_item->embedded_item_status = FetchStatus::FINISHED_LOADING; if(fetch_message_result.message->user == me) fetch_body_item->set_description_color(get_theme().attention_alert_text_color, true); @@ -7846,6 +7838,9 @@ namespace QuickMedia { auto matrix_room_directory_page = std::make_unique(this, matrix); auto settings_body = create_body(); + auto custom_emoji_body_item = BodyItem::create("Custom emoji"); + custom_emoji_body_item->url = "emoji"; + settings_body->append_item(std::move(custom_emoji_body_item)); auto join_body_item = BodyItem::create("Join room"); join_body_item->url = "join"; settings_body->append_item(std::move(join_body_item)); diff --git a/src/Tabs.cpp b/src/Tabs.cpp index a5c371a..41554fb 100644 --- a/src/Tabs.cpp +++ b/src/Tabs.cpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace QuickMedia { static float floor(float v) { diff --git a/src/Text.cpp b/src/Text.cpp index 3ecf24c..c43944b 100644 --- a/src/Text.cpp +++ b/src/Text.cpp @@ -4,10 +4,12 @@ #include "../include/Theme.hpp" #include "../include/AsyncImageLoader.hpp" #include "../include/StringUtils.hpp" +#include "../include/Scale.hpp" #include "../generated/Emoji.hpp" #include #include #include +#include #include #include #include @@ -45,6 +47,8 @@ namespace QuickMedia static const uint8_t FORMATTED_TEXT_START = '\x02'; static const uint8_t FORMATTED_TEXT_END = '\x03'; + static const mgl::vec2i MAX_IMAGE_SIZE(300, 300); + enum class FormattedTextType : uint8_t { TEXT, IMAGE @@ -92,6 +96,7 @@ namespace QuickMedia setString(std::move(_str)); } + // TODO: Validate |str|. Turn |str| into a valid utf-8 string void Text::setString(std::string str) { //if(str != this->str) @@ -119,6 +124,7 @@ namespace QuickMedia dirtyText = true; } + // TODO: Alt text. Helpful when copying the text. Or do we want to copy the url instead? // static std::string Text::formatted_image(const std::string &url, bool local, mgl::vec2i size) { const uint32_t str_size = url.size(); @@ -455,6 +461,7 @@ namespace QuickMedia return size; image_url.assign(str + offset, text_size); + image_size = clamp_to_size(image_size, MAX_IMAGE_SIZE); return std::min(offset + text_size + 1, size); // + 1 for FORMATTED_TEXT_END } @@ -518,7 +525,8 @@ namespace QuickMedia text_elements.push_back(std::move(text_element)); } else { text_element.text_num_bytes = 1; - text_elements.push_back(std::move(text_element)); + if(!text_element.url.empty()) + text_elements.push_back(std::move(text_element)); } } } else if(match_emoji_sequence((const unsigned char*)str.data() + index, size - index, emoji_sequence, emoji_sequence_length, emoji_byte_length)) { @@ -585,10 +593,18 @@ namespace QuickMedia return vertices[vertex_ref.vertices_index][vertex_ref.index + 5].position.x; } + float Text::get_text_quad_top_side(const VertexRef &vertex_ref) const { + return vertices[vertex_ref.vertices_index][vertex_ref.index + 1].position.y; + } + float Text::get_text_quad_bottom_side(const VertexRef &vertex_ref) const { return vertices[vertex_ref.vertices_index][vertex_ref.index + 4].position.y; } + float Text::get_text_quad_height(const VertexRef &vertex_ref) const { + return get_text_quad_bottom_side(vertex_ref) - get_text_quad_top_side(vertex_ref); + } + float Text::get_caret_offset_by_caret_index(int index) const { const int num_vertices = vertices_linear.size(); if(num_vertices == 0) @@ -636,6 +652,69 @@ namespace QuickMedia static mgl::vec2f vec2f_floor(mgl::vec2f value) { return mgl::vec2f((int)value.x, (int)value.y); } + + void Text::move_vertex_lines_by_largest_items(int vertices_linear_end) { + if(vertices_linear.empty() || vertices_linear_end == 0) + return; + + mgl::Font *latin_font; + if(bold_font) + latin_font = FontLoader::get_font(FontLoader::FontType::LATIN_BOLD, characterSize); + else + latin_font = FontLoader::get_font(FontLoader::FontType::LATIN, characterSize); + + const float vspace = font_get_real_height(latin_font); + const float vertex_height = get_text_quad_height(vertices_linear[0]); + float vertex_max_height = std::max(vertex_height, vspace); + float vertex_second_max_height = vspace; + int current_line = vertices_linear[0].line; + int current_line_vertices_linear_start = 0; + float move_y = 0.0f; + + for(int i = 0; i < vertices_linear_end; ++i) { + VertexRef &vertex_ref = vertices_linear[i]; + mgl::Vertex *vertex = &vertices[vertex_ref.vertices_index][vertex_ref.index]; + for(int v = 0; v < 6; ++v) { + vertex[v].position.y += move_y; + } + + if(vertices_linear[i].line != current_line) { + const float vertices_move_down_offset = vertex_max_height - vertex_second_max_height; + if(vertex_max_height > vspace/* && vertex_max_height - vertex_min_height > 2.0f*/) { + for(int j = current_line_vertices_linear_start; j <= i; ++j) { + VertexRef &vertex_ref = vertices_linear[j]; + mgl::Vertex *vertex = &vertices[vertex_ref.vertices_index][vertex_ref.index]; + for(int v = 0; v < 6; ++v) { + vertex[v].position.y += vertices_move_down_offset; + } + } + move_y += vertices_move_down_offset; + } + + vertex_max_height = vspace; + current_line = vertices_linear[i].line; + current_line_vertices_linear_start = i; + } + + const float vertex_height = std::max(get_text_quad_height(vertex_ref), vspace); + if(vertex_height > vertex_max_height) { + vertex_second_max_height = vertex_max_height; + vertex_max_height = vertex_height; + } + } + + const float vertices_move_down_offset = vertex_max_height - vertex_second_max_height; + if(vertex_max_height > vspace/* && vertex_max_height - vertex_min_height > 2.0f*/) { + // TODO: current_line_vertices_linear_start vs vertices_linear_end + for(int j = current_line_vertices_linear_start; j < vertices_linear_end; ++j) { + VertexRef &vertex_ref = vertices_linear[j]; + mgl::Vertex *vertex = &vertices[vertex_ref.vertices_index][vertex_ref.index]; + for(int v = 0; v < 6; ++v) { + vertex[v].position.y += vertices_move_down_offset; + } + } + } + } void Text::updateGeometry(bool update_even_if_not_dirty) { if(dirtyText) { @@ -668,8 +747,8 @@ namespace QuickMedia latin_font = FontLoader::get_font(FontLoader::FontType::LATIN, characterSize); const float latin_font_width = latin_font->get_glyph(' ').advance; - const float hspace_latin = latin_font_width + characterSpacing; const float vspace = font_get_real_height(latin_font); + const float hspace_latin = latin_font_width + characterSpacing; const float emoji_spacing = 2.0f; int hspace_monospace = 0; @@ -686,7 +765,7 @@ namespace QuickMedia mgl::vec2f glyphPos; uint32_t prevCodePoint = 0; // TODO: Only do this if dirtyText (then the Text object shouldn't be reset in Body. There should be a cleanup function in text instead) - for(usize textElementIndex = 0; textElementIndex < textElements.size(); ++textElementIndex) + for(int textElementIndex = 0; textElementIndex < (int)textElements.size(); ++textElementIndex) { TextElement &textElement = textElements[textElementIndex]; @@ -714,11 +793,11 @@ namespace QuickMedia if(prevCodePoint != 0) glyphPos.x += emoji_spacing; - const float font_height_offset = 0.0f;//floor(vspace * 0.6f); - mgl::vec2f vertexTopLeft(glyphPos.x, glyphPos.y + font_height_offset - textElement.size.y * 0.5f); - mgl::vec2f vertexTopRight(glyphPos.x + textElement.size.x, glyphPos.y + font_height_offset - textElement.size.y * 0.5f); - mgl::vec2f vertexBottomLeft(glyphPos.x, glyphPos.y + font_height_offset + textElement.size.y * 0.5f); - mgl::vec2f vertexBottomRight(glyphPos.x + textElement.size.x, glyphPos.y + font_height_offset + textElement.size.y * 0.5f); + const float font_height_offset = vspace; + mgl::vec2f vertexTopLeft(glyphPos.x, glyphPos.y + font_height_offset - textElement.size.y); + mgl::vec2f vertexTopRight(glyphPos.x + textElement.size.x, glyphPos.y + font_height_offset - textElement.size.y); + mgl::vec2f vertexBottomLeft(glyphPos.x, glyphPos.y + font_height_offset); + mgl::vec2f vertexBottomRight(glyphPos.x + textElement.size.x, glyphPos.y + font_height_offset); vertexTopLeft = vec2f_floor(vertexTopLeft); vertexTopRight = vec2f_floor(vertexTopRight); @@ -1018,6 +1097,7 @@ namespace QuickMedia } } vertices_linear_done:; + move_vertex_lines_by_largest_items(vertices_linear_index); // TODO: Optimize for(TextElement &textElement : textElements) { @@ -1052,19 +1132,19 @@ namespace QuickMedia boundingBox.size.y = 0.0f; for(VertexRef &vertex_ref : vertices_linear) { boundingBox.size.x = std::max(boundingBox.size.x, get_text_quad_right_side(vertex_ref)); - //boundingBox.size.y = std::max(boundingBox.size.y, get_text_quad_bottom_side(vertex_ref)); + boundingBox.size.y = std::max(boundingBox.size.y, get_text_quad_bottom_side(vertex_ref)); } - boundingBox.size.y = num_lines * line_height; + //boundingBox.size.y = num_lines * line_height; //boundingBox.size.y = text_offset_y; // TODO: - //if(vertices_linear.empty()) - // boundingBox.size.y = line_height; + if(vertices_linear.empty()) + boundingBox.size.y = line_height; - //if(editable) - // boundingBox.size.y = num_lines * line_height; + if(editable) + boundingBox.size.y = num_lines * line_height; // TODO: Clear |vertices| somehow even with editable text for(size_t i = 0; i < FONT_ARRAY_SIZE; ++i) { @@ -1458,16 +1538,21 @@ namespace QuickMedia target.draw(vertex_buffers[FONT_INDEX_EMOJI]); }*/ + // TODO: Use rounded rectangle for fallback image + // TODO: Use a new vector with only the image data instead of this. // TODO: Sprite mgl::Sprite sprite; - mgl::Rectangle fallback_emoji(mgl::vec2f(vspace, vspace)); - fallback_emoji.set_color(get_theme().shade_color); + mgl::Rectangle fallback_image(mgl::vec2f(vspace, vspace)); + fallback_image.set_color(get_theme().image_loading_background_color); for(const TextElement &textElement : textElements) { if(textElement.text_type == TextElement::TextType::EMOJI) { - if(textElement.pos.to_vec2f().y + vspace > boundingBox.size.y) + // TODO: + if(textElement.pos.to_vec2f().y + vspace > boundingBox.size.y + 10.0f) { + fprintf(stderr, "bounding box y: %f\n", boundingBox.size.y); continue; + } auto emoji_data = AsyncImageLoader::get_instance().get_thumbnail(textElement.url, textElement.local, { (int)vspace, (int)vspace }); if(emoji_data->loading_state == LoadingState::FINISHED_LOADING) { @@ -1480,18 +1565,17 @@ namespace QuickMedia if(emoji_data->loading_state == LoadingState::APPLIED_TO_TEXTURE && emoji_data->texture.get_size().x > 0) { sprite.set_texture(&emoji_data->texture); sprite.set_position(pos + textElement.pos.to_vec2f()); - sprite.set_size(textElement.size.to_vec2f()); + //sprite.set_size(textElement.size.to_vec2f()); target.draw(sprite); } else { - fallback_emoji.set_position(pos + textElement.pos.to_vec2f()); - target.draw(fallback_emoji); + fallback_image.set_position(pos + textElement.pos.to_vec2f()); + target.draw(fallback_image); } } } // TODO: Use a new vector with only the image data instead of this. // TODO: Sprite - #if 0 for(const TextElement &textElement : textElements) { if(textElement.type == TextElement::Type::IMAGE) { auto thumbnail_data = AsyncImageLoader::get_instance().get_thumbnail(textElement.url, textElement.local, textElement.size); @@ -1502,18 +1586,22 @@ namespace QuickMedia thumbnail_data->loading_state = LoadingState::APPLIED_TO_TEXTURE; } - if(thumbnail_data->loading_state == LoadingState::APPLIED_TO_TEXTURE) { - if(textElement.pos.to_vec2f().y + thumbnail_data->texture->get_size().y > boundingBox.size.y) + if(thumbnail_data->loading_state == LoadingState::APPLIED_TO_TEXTURE && thumbnail_data->texture.get_size().x > 0) { + // TODO: + if(textElement.pos.to_vec2f().y + thumbnail_data->texture.get_size().y > boundingBox.size.y + 10.0f) continue; sprite.set_texture(&thumbnail_data->texture); sprite.set_position(pos + textElement.pos.to_vec2f()); - sprite.set_size(textElement.size.to_vec2f()); + //sprite.set_size(textElement.size.to_vec2f()); target.draw(sprite); + } else { + fallback_image.set_size(textElement.size.to_vec2f()); + fallback_image.set_position(pos + textElement.pos.to_vec2f()); + target.draw(fallback_image); } } } - #endif if(!editable) return true; pos.y -= floor(vspace*1.25f); diff --git a/src/main.cpp b/src/main.cpp index 52cb374..ef0568b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,9 +1,7 @@ #include "../include/QuickMedia.hpp" -#include -#include +#include int main(int argc, char **argv) { - XInitThreads(); setlocale(LC_ALL, "C"); // Sigh... stupid C QuickMedia::Program program; return program.run(argc, argv); diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index f79b10c..28b4823 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -9,6 +9,7 @@ #include "../../include/AsyncImageLoader.hpp" #include "../../include/Config.hpp" #include "../../include/Theme.hpp" +#include "../../include/Scale.hpp" #include #include #include @@ -27,14 +28,15 @@ namespace QuickMedia { static const mgl::vec2i thumbnail_max_size(600, 337); + static const mgl::vec2i custom_emoji_max_size(64, 64); static const char* SERVICE_NAME = "matrix"; static const char* OTHERS_ROOM_TAG = "tld.name.others"; // Filter without account data. TODO: We include pinned events but limit events to 1. That means if the last event is a pin, // then we cant see room message preview. TODO: Fix this somehow. // TODO: What about state events in initial sync in timeline? such as user display name change. - static const char* INITIAL_FILTER = "{\"presence\":{\"limit\":0,\"types\":[\"\"]},\"account_data\":{\"limit\":0,\"types\":[\"\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"types\":[\"m.room.message\"],\"limit\":1,\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"limit\":1,\"types\":[\"m.fully_read\",\"m.tag\",\"qm.last_read_message_timestamp\"],\"lazy_load_members\":true}}}"; - static const char* ADDITIONAL_MESSAGES_FILTER = "{\"presence\":{\"limit\":0,\"types\":[\"\"]},\"account_data\":{\"limit\":0,\"types\":[\"\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"limit\":20,\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true}}}"; - static const char* CONTINUE_FILTER = "{\"presence\":{\"limit\":0,\"types\":[\"\"]},\"account_data\":{\"limit\":0,\"types\":[\"\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"types\":[\"m.fully_read\",\"m.tag\",\"qm.last_read_message_timestamp\"],\"lazy_load_members\":true}}}"; + static const char* INITIAL_FILTER = "{\"presence\":{\"limit\":0,\"types\":[\"\"]},\"account_data\":{\"types\":[\"qm.emoji\",\"m.direct\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"types\":[\"m.room.message\"],\"limit\":1,\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"limit\":1,\"types\":[\"m.fully_read\",\"m.tag\",\"qm.last_read_message_timestamp\"],\"lazy_load_members\":true}}}"; + static const char* ADDITIONAL_MESSAGES_FILTER = "{\"presence\":{\"types\":[\"\"]},\"account_data\":{\"limit\":0,\"types\":[\"\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"limit\":20,\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true}}}"; + static const char* CONTINUE_FILTER = "{\"presence\":{\"limit\":0,\"types\":[\"\"]},\"account_data\":{\"types\":[\"qm.emoji\",\"m.direct\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"types\":[\"m.fully_read\",\"m.tag\",\"qm.last_read_message_timestamp\"],\"lazy_load_members\":true}}}"; static std::string capitalize(const std::string &str) { if(str.size() >= 1) @@ -49,6 +51,8 @@ namespace QuickMedia { 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, "direct") == 0) + return "Direct messages"; else if(strcmp(tag.c_str() + 2, "lowpriority") == 0) return "Low priority"; else if(strcmp(tag.c_str() + 2, "server_notice") == 0) @@ -117,7 +121,7 @@ namespace QuickMedia { return colors[color_hash_code(user_id) % num_colors]; } - static std::string remove_reply_formatting(const std::string &str) { + static std::string remove_reply_formatting(Matrix *matrix, const std::string &str) { if(strncmp(str.c_str(), "> <@", 4) == 0) { size_t index = str.find("> ", 4); if(index != std::string::npos) { @@ -126,12 +130,12 @@ namespace QuickMedia { return str.substr(msg_begin + 2); } } else { - return formatted_text_to_qm_text(str.c_str(), str.size(), false); + return formatted_text_to_qm_text(matrix, str.c_str(), str.size(), false); } return str; } - static std::string remove_reply_formatting(const Message *message, bool keep_formatted = false) { + static std::string remove_reply_formatting(Matrix *matrix, const Message *message, bool keep_formatted = false) { if(!message->body_is_formatted && strncmp(message->body.c_str(), "> <@", 4) == 0) { size_t index = message->body.find("> ", 4); if(index != std::string::npos) { @@ -144,7 +148,7 @@ namespace QuickMedia { if(keep_formatted) return message->body; else - return formatted_text_to_qm_text(message->body.c_str(), message->body.size(), false); + return formatted_text_to_qm_text(matrix, message->body.c_str(), message->body.size(), false); } } @@ -464,7 +468,7 @@ namespace QuickMedia { if(!sync_is_cache && message_dir == MessageDirection::AFTER) { for(auto &message : messages) { if(message->notification_mentions_me) { - std::string body = remove_reply_formatting(message.get()); + std::string body = remove_reply_formatting(matrix, message->body); bool read = 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) && message->related_event_type != RelatedEventType::EDIT && message->related_event_type != RelatedEventType::REDACTION) { @@ -629,15 +633,15 @@ namespace QuickMedia { return nullptr; } - static std::string message_to_qm_text(const Message *message, bool allow_formatted_text = true) { + std::string message_to_qm_text(Matrix *matrix, const Message *message, bool allow_formatted_text) { if(message->body_is_formatted) - return formatted_text_to_qm_text(message->body.c_str(), message->body.size(), allow_formatted_text); + return formatted_text_to_qm_text(matrix, message->body.c_str(), message->body.size(), allow_formatted_text); else return message->body; } - static std::string message_to_room_description_text(Message *message) { - std::string body = strip(message_to_qm_text(message)); + static std::string message_to_room_description_text(Matrix *matrix, Message *message) { + std::string body = strip(formatted_text_to_qm_text(matrix, message->body.c_str(), message->body.size(), true)); if(message->type == MessageType::REACTION) return "Reacted with: " + body; else if(message->related_event_type == RelatedEventType::REPLY) @@ -704,7 +708,7 @@ namespace QuickMedia { room_desc += "Unread: "; if(last_unread_message) - room_desc += extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(last_unread_message), AUTHOR_MAX_LENGTH) + ": " + message_to_room_description_text(last_unread_message); + room_desc += extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(last_unread_message), AUTHOR_MAX_LENGTH) + ": " + message_to_room_description_text(matrix, last_unread_message); int unread_notification_count = room->unread_notification_count; if(unread_notification_count > 0 && set_room_as_unread) { @@ -724,7 +728,7 @@ namespace QuickMedia { rooms_page->move_room_to_top(room); room_tags_page->move_room_to_top(room); } else if(last_new_message) { - room->body_item->set_description(extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(last_new_message.get()), AUTHOR_MAX_LENGTH) + ": " + message_to_room_description_text(last_new_message.get())); + room->body_item->set_description(extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(last_new_message.get()), AUTHOR_MAX_LENGTH) + ": " + message_to_room_description_text(matrix, last_new_message.get())); room->body_item->set_description_color(get_theme().faded_text_color); room->body_item->set_description_max_lines(3); @@ -974,6 +978,9 @@ namespace QuickMedia { matrix->logout(); program->set_go_to_previous_page(); return PluginResult::OK; + } else if(args.url == "emoji") { + result_tabs.push_back(Tab{create_body(), std::make_unique(program, matrix), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + return PluginResult::OK; } else { return PluginResult::ERR; } @@ -991,9 +998,170 @@ namespace QuickMedia { } else { show_notification("QuickMedia", "Failed to join " + args.title, Urgency::CRITICAL); } + + return PluginResult::OK; + } + + static const char* file_get_filename(const std::string &filepath) { + size_t index = filepath.rfind('/'); + if(index == std::string::npos) + return filepath.c_str(); + return filepath.c_str() + index + 1; + } + + static bool generate_random_characters(char *buffer, int buffer_size) { + int fd = open("/dev/urandom", O_RDONLY); + if(fd == -1) { + perror("/dev/urandom"); + return false; + } + + if(read(fd, buffer, buffer_size) < buffer_size) { + fprintf(stderr, "Failed to read %d bytes from /dev/urandom\n", buffer_size); + close(fd); + return false; + } + + close(fd); + return true; + } + + static std::string random_characters_to_readable_string(const char *buffer, int buffer_size) { + std::ostringstream result; + result << std::hex; + for(int i = 0; i < buffer_size; ++i) + result << (int)(unsigned char)buffer[i]; + return result.str(); + } + + PluginResult MatrixCustomEmojiPage::submit(const SubmitArgs &args, std::vector &result_tabs) { + if(args.url == "add") { + auto submit_handler = [this](FileManagerPage*, const std::filesystem::path &filepath) { + program->run_task_with_loading_screen([this, filepath] { + std::string key = filepath.filename().string(); + if(!key.empty()) { + size_t ext_index = key.rfind('.'); + if(ext_index != std::string::npos) + key = key.substr(0, ext_index); + } + + if(key.empty()) { + char random_characters[10]; + if(!generate_random_characters(random_characters, sizeof(random_characters))) { + show_notification("QuickMedia", "Failed to generate random string", Urgency::CRITICAL); + return false; + } + key = random_characters_to_readable_string(random_characters, sizeof(random_characters)); + } + + if(matrix->does_custom_emoji_with_name_exist(key)) { + show_notification("QuickMedia", "Failed to upload custom emoji. You already have a custom emoji with the name " + key, Urgency::CRITICAL); + return false; + } + + std::string mxc_url; + std::string err_msg; + if(matrix->upload_custom_emoji(filepath, key, mxc_url, err_msg) != PluginResult::OK) { + show_notification("QuickMedia", "Failed to upload custom emoji, error: " + err_msg, Urgency::CRITICAL); + return false; + } + + return true; + }); + return std::vector{}; + }; + + auto file_manager_body = create_body(); + auto file_manager_page = std::make_unique(program, FILE_MANAGER_MIME_TYPE_IMAGE, std::move(submit_handler)); + file_manager_page->set_current_directory(get_home_dir().data); + BodyItems body_items; + file_manager_page->get_files_in_directory(body_items); + file_manager_body->set_items(std::move(body_items)); + + result_tabs.push_back(Tab{std::move(file_manager_body), std::move(file_manager_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + return PluginResult::OK; + } else if(args.url == "rename") { + result_tabs.push_back(Tab{create_body(false, true), std::make_unique(program, matrix), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + return PluginResult::OK; + } else if(args.url == "delete") { + auto body = create_body(false, true); + BodyItems body_items; + for(auto &emoji : matrix->get_custom_emojis()) { + auto emoji_item = BodyItem::create(":" + emoji.first + ":"); + emoji_item->url = emoji.first; + emoji_item->thumbnail_url = matrix->get_media_url(emoji.second.url); + emoji_item->thumbnail_size = emoji.second.size; + body_items.push_back(std::move(emoji_item)); + } + body->set_items(std::move(body_items)); + + Body *body_p = body.get(); + result_tabs.push_back(Tab{std::move(body), std::make_unique(program, matrix, body_p), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + return PluginResult::OK; + } else { + return PluginResult::ERR; + } + } + + PluginResult MatrixCustomEmojiPage::lazy_fetch(BodyItems &result_items) { + auto add_emoji_item = BodyItem::create("Add emoji"); + add_emoji_item->url = "add"; + result_items.push_back(std::move(add_emoji_item)); + + auto rename_emoji_item = BodyItem::create("Rename emoji"); + rename_emoji_item->url = "rename"; + result_items.push_back(std::move(rename_emoji_item)); + + auto delete_emoji_item = BodyItem::create("Delete emoji"); + delete_emoji_item->set_title_color(mgl::Color(255, 45, 47)); + delete_emoji_item->url = "delete"; + result_items.push_back(std::move(delete_emoji_item)); + + return PluginResult::OK; + } + + bool MatrixCustomEmojiPage::is_ready() { + return matrix->is_initial_sync_finished(); + } + + PluginResult MatrixCustomEmojiRenameSelectPage::submit(const SubmitArgs &args, std::vector &result_tabs) { + result_tabs.push_back(Tab{create_body(), std::make_unique(program, matrix, args.url), create_search_bar("Enter a new name for the emoji...", SEARCH_DELAY_FILTER)}); + return PluginResult::OK; + } + + PluginResult MatrixCustomEmojiRenameSelectPage::lazy_fetch(BodyItems &result_items) { + for(auto &emoji : matrix->get_custom_emojis()) { + auto emoji_item = BodyItem::create(":" + emoji.first + ":"); + emoji_item->url = emoji.first; + emoji_item->thumbnail_url = matrix->get_media_url(emoji.second.url); + emoji_item->thumbnail_size = emoji.second.size; + result_items.push_back(std::move(emoji_item)); + } return PluginResult::OK; } + PluginResult MatrixCustomEmojiRenamePage::submit(const SubmitArgs &args, std::vector&) { + if(matrix->rename_custom_emoji(emoji_key, args.title)) { + program->set_go_to_previous_page(); + return PluginResult::OK; + } else { + show_notification("QuickMedia", "Failed to rename emoji " + emoji_key + " to " + args.title, Urgency::CRITICAL); + return PluginResult::OK; + } + } + + PluginResult MatrixCustomEmojiDeletePage::submit(const SubmitArgs &args, std::vector&) { + if(matrix->delete_custom_emoji(args.url)) { + body->erase_item([&args](std::shared_ptr &item) { + return item->url == args.url; + }); + return PluginResult::OK; + } else { + show_notification("QuickMedia", "Failed to delete emoji: " + args.url, Urgency::CRITICAL); + return PluginResult::OK; + } + } + MatrixChatPage::MatrixChatPage(Program *program, std::string room_id, MatrixRoomsPage *rooms_page, std::string jump_to_event_id) : Page(program), room_id(std::move(room_id)), rooms_page(rooms_page), jump_to_event_id(std::move(jump_to_event_id)) { @@ -1533,19 +1701,21 @@ namespace QuickMedia { notification_thread.join(); } + std::lock_guard lock(room_data_mutex); delegate = nullptr; sync_failed = false; sync_fail_reason.clear(); - next_batch.clear(); + set_next_batch(""); next_notifications_token.clear(); invites.clear(); filter_cached.reset(); my_events_transaction_ids.clear(); finished_fetching_notifications = false; + custom_emoji_by_key.clear(); } - bool Matrix::is_initial_sync_finished() const { - return !next_batch.empty(); + bool Matrix::is_initial_sync_finished() { + return initial_sync_finished; } bool Matrix::did_initial_sync_fail(std::string &err_msg) { @@ -1705,14 +1875,12 @@ namespace QuickMedia { if(!root.IsObject()) return PluginResult::ERR; - //const rapidjson::Value &account_data_json = GetMember(root, "account_data"); - //std::optional> dm_rooms; - //parse_sync_account_data(account_data_json, dm_rooms); - // TODO: Include "Direct messages" as a tag using |dm_rooms| above - const rapidjson::Value &rooms_json = GetMember(root, "rooms"); parse_sync_room_data(rooms_json, is_additional_messages_sync, initial_sync); + const rapidjson::Value &account_data_json = GetMember(root, "account_data"); + parse_sync_account_data(account_data_json); + return PluginResult::OK; } @@ -1771,7 +1939,7 @@ namespace QuickMedia { notification.room = room; notification.event_id = std::move(event_id); notification.sender_user_id.assign(sender_json.GetString(), sender_json.GetStringLength()); - notification.body = remove_reply_formatting(body_json.GetString()); + notification.body = remove_reply_formatting(this, body_json.GetString()); notification.timestamp = timestamp; notification.read = read_json.GetBool(); callback_func(notification); @@ -1781,7 +1949,7 @@ namespace QuickMedia { return PluginResult::OK; } - PluginResult Matrix::parse_sync_account_data(const rapidjson::Value &account_data_json, std::optional> &dm_rooms) { + PluginResult Matrix::parse_sync_account_data(const rapidjson::Value &account_data_json) { if(!account_data_json.IsObject()) return PluginResult::OK; @@ -1789,36 +1957,74 @@ namespace QuickMedia { if(!events_json.IsArray()) return PluginResult::OK; - bool has_direct_rooms = false; - std::set dm_rooms_tmp; for(const rapidjson::Value &event_item_json : events_json.GetArray()) { if(!event_item_json.IsObject()) continue; const rapidjson::Value &type_json = GetMember(event_item_json, "type"); - if(!type_json.IsString() || strcmp(type_json.GetString(), "m.direct") != 0) + if(!type_json.IsString()) 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; + if(strcmp(type_json.GetString(), "m.direct") == 0) { + for(auto const &it : content_json.GetObject()) { + if(!it.name.IsString()) + continue; - for(const rapidjson::Value &room_id_json : it.value.GetArray()) { - if(!room_id_json.IsString()) + if(!it.value.IsArray()) continue; - - dm_rooms_tmp.insert(std::string(room_id_json.GetString(), room_id_json.GetStringLength())); + + for(const rapidjson::Value &room_id_json : it.value.GetArray()) { + if(!room_id_json.IsString()) + continue; + + RoomData *room = get_room_by_id(std::string(room_id_json.GetString(), room_id_json.GetStringLength())); + if(!room) { + fprintf(stderr, "Warning: got m.direct for room %s that we haven't created yet\n", room_id_json.GetString()); + continue; + } + + auto user = get_user_by_id(room, std::string(it.name.GetString(), it.name.GetStringLength()), nullptr, false); + if(!user) { + fprintf(stderr, "Warning: got m.direct for user %s that doesn't exist in the room %s yet\n", it.name.GetString(), room_id_json.GetString()); + continue; + } + + room->acquire_room_lock(); + std::set &room_tags = room->get_tags_thread_unsafe(); + auto room_tag_it = room_tags.find("m.direct"); + if(room_tag_it == room_tags.end()) { + room_tags.insert("m.direct"); + ui_thread_tasks.push([this, room]{ delegate->room_add_tag(room, "m.direct"); }); + } + room->release_room_lock(); + } } - } - } + } else if(strcmp(type_json.GetString(), "qm.emoji") == 0) { + std::lock_guard lock(room_data_mutex); + for(auto const &emoji_json : content_json.GetObject()) { + if(!emoji_json.name.IsString() || !emoji_json.value.IsObject()) + continue; - if(has_direct_rooms) - dm_rooms = std::move(dm_rooms_tmp); + const rapidjson::Value &url_json = GetMember(emoji_json.value, "url"); + const rapidjson::Value &width_json = GetMember(emoji_json.value, "width"); + const rapidjson::Value &height_json = GetMember(emoji_json.value, "height"); + if(!url_json.IsString()) + continue; + + CustomEmoji custom_emoji; + custom_emoji.url = url_json.GetString(); + if(width_json.IsInt() && height_json.IsInt()) { + custom_emoji.size.x = width_json.GetInt(); + custom_emoji.size.y = height_json.GetInt(); + } + custom_emoji_by_key[emoji_json.name.GetString()] = std::move(custom_emoji); + } + } + } return PluginResult::OK; } @@ -1986,6 +2192,20 @@ namespace QuickMedia { item_timestamp = origin_server_ts.GetInt64(); } + const rapidjson::Value &is_direct_json = GetMember(content_json, "is_direct"); + if(is_direct_json.IsBool() && is_direct_json.GetBool()) { + room_data->acquire_room_lock(); + std::set &room_tags = room_data->get_tags_thread_unsafe(); + + auto room_tag_it = room_tags.find("m.direct"); + if(room_tag_it == room_tags.end()) { + room_tags.insert("m.direct"); + ui_thread_tasks.push([this, room_data]{ delegate->room_add_tag(room_data, "m.direct"); }); + } + + room_data->release_room_lock(); + } + parse_user_info(content_json, sender_json->GetString(), room_data, item_timestamp); } } @@ -2003,7 +2223,7 @@ namespace QuickMedia { return media_url.substr(start, end - start); } - static std::string get_thumbnail_url(const std::string &homeserver, const std::string &mxc_id) { + static std::string get_avatar_thumbnail_url(const std::string &homeserver, const std::string &mxc_id) { if(mxc_id.empty()) return ""; @@ -2011,6 +2231,10 @@ namespace QuickMedia { return homeserver + "/_matrix/media/r0/thumbnail/" + mxc_id + "?width=" + size + "&height=" + size + "&method=crop"; } + std::string Matrix::get_media_url(const std::string &mxc_id) { + return homeserver + "/_matrix/media/r0/download/" + thumbnail_url_extract_media_id(mxc_id); + } + std::shared_ptr Matrix::parse_user_info(const rapidjson::Value &json, const std::string &user_id, RoomData *room_data, int64_t timestamp) { assert(json.IsObject()); std::string avatar_url_str; @@ -2023,7 +2247,7 @@ namespace QuickMedia { std::string display_name = display_name_json.IsString() ? display_name_json.GetString() : user_id; std::string avatar_url = thumbnail_url_extract_media_id(avatar_url_str); if(!avatar_url.empty()) - avatar_url = get_thumbnail_url(homeserver, avatar_url); // TODO: Remove the constant strings around to reduce memory usage (6.3mb) + avatar_url = get_avatar_thumbnail_url(homeserver, avatar_url); // TODO: Remove the constant strings around to reduce memory usage (6.3mb) //auto user_info = std::make_shared(room_data, user_id, std::move(display_name), std::move(avatar_url)); // Overwrites user data //room_data->add_user(user_info); @@ -2195,8 +2419,8 @@ namespace QuickMedia { return false; } - bool message_contains_user_mention(const Message *message, const std::string &username, const std::string &user_id) { - const std::string formatted_text = message_to_qm_text(message, false); + bool message_contains_user_mention(Matrix *matrix, const Message *message, const std::string &username, const std::string &user_id) { + const std::string formatted_text = message_to_qm_text(matrix, message, false); return message_contains_user_mention(formatted_text, username) || message_contains_user_mention(formatted_text, user_id); } @@ -2263,7 +2487,7 @@ namespace QuickMedia { // TODO: Is @room ok? shouldn't we also check if the user has permission to do @room? (only when notifications are limited to @mentions) // TODO: Is comparing against read marker timestamp ok enough? if(me && message->timestamp > read_marker_message_timestamp) { - std::string message_str = message_to_qm_text(message.get(), false); + std::string message_str = message_to_qm_text(this, message.get(), false); message->notification_mentions_me = message_contains_user_mention(message_str, my_display_name) || message_contains_user_mention(message_str, me->user_id) || message_contains_user_mention(message_str, "@room"); } } @@ -2359,7 +2583,11 @@ namespace QuickMedia { bool allow_formatted_text = false; bool inside_source_highlight = false; bool supports_syntax_highlight = false; + bool inside_img_tag = false; + std::string_view img_src; + mgl::vec2i img_size; mgl::Color font_color = mgl::Color(255, 255, 255, 255); + Matrix *matrix = nullptr; }; static int accumulate_string(char *data, int size, void *userdata) { @@ -2384,6 +2612,10 @@ namespace QuickMedia { else if(html_parser->tag_name.size == 4 && memcmp(html_parser->tag_name.data, "code", 4) == 0) { parse_userdata.inside_code_tag = true; parse_userdata.code_tag_language = std::string_view(); + } else if(html_parser->tag_name.size == 3 && memcmp(html_parser->tag_name.data, "img", 3) == 0) { + parse_userdata.inside_img_tag = true; + parse_userdata.img_src = std::string_view(); + parse_userdata.img_size = { 0, 0 }; } break; } @@ -2397,6 +2629,17 @@ namespace QuickMedia { parse_userdata.mx_reply_depth = std::max(0, parse_userdata.mx_reply_depth - 1); } else if(html_parser->tag_name.size == 4 && memcmp(html_parser->tag_name.data, "code", 4) == 0) { parse_userdata.inside_code_tag = false; + } else if(html_parser->tag_name.size == 3 && memcmp(html_parser->tag_name.data, "img", 3) == 0) { + if(parse_userdata.matrix && parse_userdata.inside_img_tag && parse_userdata.img_src.size() > 0) { + std::string image_url(parse_userdata.img_src); + html_unescape_sequences(image_url); + mgl::vec2i img_size = parse_userdata.img_size; + // TODO: Better solution when size not given? + if(img_size.x == 0 || img_size.y == 0) + img_size = custom_emoji_max_size; + parse_userdata.result += Text::formatted_image(parse_userdata.matrix->get_media_url(image_url), false, img_size); + } + parse_userdata.inside_img_tag = false; } break; } @@ -2407,6 +2650,20 @@ namespace QuickMedia { } else if(parse_userdata.inside_code_tag && html_parser->attribute_key.size == 5 && memcmp(html_parser->attribute_key.data, "class", 5) == 0) { if(html_parser->attribute_value.size > 9 && memcmp(html_parser->attribute_value.data, "language-", 9) == 0) parse_userdata.code_tag_language = std::string_view(html_parser->attribute_value.data + 9, html_parser->attribute_value.size - 9); + } else if(parse_userdata.allow_formatted_text && parse_userdata.inside_img_tag) { + if(html_parser->attribute_key.size == 3 && memcmp(html_parser->attribute_key.data, "src", 3) == 0) { + parse_userdata.img_src = std::string_view(html_parser->attribute_value.data, html_parser->attribute_value.size); + } else if(html_parser->attribute_key.size == 5 && memcmp(html_parser->attribute_key.data, "width", 5) == 0) { + const std::string width(html_parser->attribute_value.data, html_parser->attribute_value.size); + parse_userdata.img_size.x = atoi(width.c_str()); + } else if(html_parser->attribute_key.size == 6 && memcmp(html_parser->attribute_key.data, "height", 6) == 0) { + const std::string height(html_parser->attribute_value.data, html_parser->attribute_value.size); + parse_userdata.img_size.y = atoi(height.c_str()); + } + } else if(!parse_userdata.allow_formatted_text && parse_userdata.inside_img_tag && html_parser->attribute_key.size == 3 && memcmp(html_parser->attribute_key.data, "alt", 3) == 0) { + std::string text_to_add(html_parser->attribute_value.data, html_parser->attribute_value.size); + html_unescape_sequences(text_to_add); + parse_userdata.result += std::move(text_to_add); } break; } @@ -2455,10 +2712,11 @@ namespace QuickMedia { return 0; } - std::string formatted_text_to_qm_text(const char *str, size_t size, bool allow_formatted_text) { + std::string formatted_text_to_qm_text(Matrix *matrix, const char *str, size_t size, bool allow_formatted_text) { FormattedTextParseUserdata parse_userdata; parse_userdata.allow_formatted_text = allow_formatted_text; parse_userdata.supports_syntax_highlight = is_program_executable_by_name("source-highlight"); + parse_userdata.matrix = matrix; html_parser_parse(str, size, formattext_text_parser_callback, &parse_userdata); return std::move(parse_userdata.result); } @@ -2648,7 +2906,7 @@ namespace QuickMedia { body = user_display_name + " changed his profile picture"; std::string new_avatar_url_str = thumbnail_url_extract_media_id(new_avatar_url_json.GetString()); if(!new_avatar_url_str.empty()) - new_avatar_url_str = get_thumbnail_url(homeserver, new_avatar_url_str); // TODO: Remove the constant strings around to reduce memory usage (6.3mb) + new_avatar_url_str = get_avatar_thumbnail_url(homeserver, new_avatar_url_str); // TODO: Remove the constant strings around to reduce memory usage (6.3mb) new_avatar_url = new_avatar_url_str; update_user_display_info = room_data->set_user_avatar_url(user, std::move(new_avatar_url_str), timestamp); } else if((!new_avatar_url_json.IsString() || new_avatar_url_json.GetStringLength() == 0) && prev_avatar_url_json.IsString()) { @@ -2989,7 +3247,7 @@ namespace QuickMedia { if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) continue; - update_room_avatar_url |= room_data->set_avatar_url(get_thumbnail_url(homeserver, thumbnail_url_extract_media_id(url_json.GetString())), item_timestamp); + update_room_avatar_url |= room_data->set_avatar_url(get_avatar_thumbnail_url(homeserver, thumbnail_url_extract_media_id(url_json.GetString())), item_timestamp); room_data->avatar_is_fallback = false; } else if(strcmp(type_json.GetString(), "m.room.topic") == 0) { const rapidjson::Value &content_json = GetMember(event_item_json, "content"); @@ -3144,7 +3402,10 @@ namespace QuickMedia { ui_thread_tasks.push([this, room_data]{ delegate->room_add_tag(room_data, OTHERS_ROOM_TAG); }); } + const bool contains_direct_messaging = room_tags.find("m.direct") != room_tags.end(); room_tags = std::move(new_tags); + if(contains_direct_messaging) + room_tags.insert("m.direct"); room_data->release_room_lock(); } } @@ -3362,31 +3623,6 @@ namespace QuickMedia { return PluginResult::OK; } - static bool generate_random_characters(char *buffer, int buffer_size) { - int fd = open("/dev/urandom", O_RDONLY); - if(fd == -1) { - perror("/dev/urandom"); - return false; - } - - if(read(fd, buffer, buffer_size) < buffer_size) { - fprintf(stderr, "Failed to read %d bytes from /dev/urandom\n", buffer_size); - close(fd); - return false; - } - - close(fd); - return true; - } - - static std::string random_characters_to_readable_string(const char *buffer, int buffer_size) { - std::ostringstream result; - result << std::hex; - for(int i = 0; i < buffer_size; ++i) - result << (int)(unsigned char)buffer[i]; - return result.str(); - } - std::string create_transaction_id() { char random_characters[18]; if(!generate_random_characters(random_characters, sizeof(random_characters))) @@ -3505,11 +3741,29 @@ namespace QuickMedia { } } + static void replace_emoji_references_with_formatted_images(std::string &str, const std::unordered_map &custom_emojis) { + for(const auto &it : custom_emojis) { + std::string keybind = ":" + it.first + ":"; + std::string url = it.second.url; + html_escape_sequences(url); + std::string width = std::to_string(it.second.size.x); + std::string height = std::to_string(it.second.size.y); + std::string tag = "\"""; + string_replace_all(str, keybind, tag); + } + } + std::string Matrix::body_to_formatted_body(RoomData *room, const std::string &body) { + std::unordered_map custom_emojis_copy; + { + std::lock_guard lock(room_data_mutex); + custom_emojis_copy = custom_emoji_by_key; + } + std::string formatted_body; bool is_inside_code_block = false; bool is_first_line = true; - string_split(body, '\n', [this, room, &formatted_body, &is_inside_code_block, &is_first_line](const char *str, size_t size){ + string_split(body, '\n', [this, room, &formatted_body, &is_inside_code_block, &is_first_line, &custom_emojis_copy](const char *str, size_t size){ if(!is_first_line) { if(is_inside_code_block) formatted_body += '\n'; @@ -3533,6 +3787,9 @@ namespace QuickMedia { } is_first_line = true; } else { + if(!is_inside_code_block) + replace_emoji_references_with_formatted_images(line_str, custom_emojis_copy); + if(!is_inside_code_block && size > 0 && str[0] == '>') { formatted_body += ""; formatted_body_add_line(room, formatted_body, line_str); @@ -3653,17 +3910,17 @@ namespace QuickMedia { return result; } - static std::string get_reply_message(const Message *message, bool keep_formatted = false) { + static std::string get_reply_message(Matrix *matrix, const Message *message, bool keep_formatted = false) { std::string related_to_body; switch(message->type) { case MessageType::TEXT: { if(message->related_event_type != RelatedEventType::NONE) { - related_to_body = remove_reply_formatting(message, keep_formatted); + related_to_body = remove_reply_formatting(matrix, message, keep_formatted); } else { if(keep_formatted && message->body_is_formatted) related_to_body = message->body; else - related_to_body = message_to_qm_text(message, false); + related_to_body = message_to_qm_text(matrix, message, false); } break; } @@ -3683,15 +3940,15 @@ namespace QuickMedia { if(keep_formatted && message->body_is_formatted) related_to_body = message->body; else - related_to_body = message_to_qm_text(message, false); + related_to_body = message_to_qm_text(matrix, message, false); break; } } return related_to_body; } - static std::string create_body_for_message_reply(const Message *message, const std::string &body) { - return "> <" + message->user->user_id + "> " + block_quote(get_reply_message(message)) + "\n\n" + body; + static std::string create_body_for_message_reply(Matrix *matrix, const Message *message, const std::string &body) { + return "> <" + message->user->user_id + "> " + block_quote(get_reply_message(matrix, message)) + "\n\n" + body; } static std::string extract_homeserver_from_room_id(const std::string &room_id) { @@ -3703,7 +3960,7 @@ namespace QuickMedia { std::string Matrix::create_formatted_body_for_message_reply(RoomData *room, const Message *message, const std::string &body) { std::string formatted_body = body_to_formatted_body(room, body); - std::string related_to_body = get_reply_message(message, true); + std::string related_to_body = get_reply_message(this, message, true); if(!message->body_is_formatted) html_escape_sequences(related_to_body); // TODO: Add keybind to navigate to the reply message, which would also depend on this formatting. @@ -3746,7 +4003,7 @@ namespace QuickMedia { 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()); - std::string message_reply_body = create_body_for_message_reply(related_to_text_message, body); // Yes, the reply is to the edited message but the event_id reference is to the original message... + std::string message_reply_body = create_body_for_message_reply(this, related_to_text_message, body); // Yes, the reply is to the edited message but the event_id reference is to the original message... std::string formatted_message_reply_body = create_formatted_body_for_message_reply(room, related_to_text_message, body); rapidjson::Document request_data(rapidjson::kObjectType); @@ -4079,11 +4336,162 @@ namespace QuickMedia { room->set_prev_batch(""); } - static const char* file_get_filename(const std::string &filepath) { - size_t index = filepath.rfind('/'); - if(index == std::string::npos) - return filepath.c_str(); - return filepath.c_str() + index + 1; + PluginResult Matrix::upload_custom_emoji(const std::string &filepath, const std::string &key, std::string &mxc_url, std::string &err_msg) { + UploadInfo file_info; + UploadInfo thumbnail_info; + // TODO: Do not create and upload thumbnail + PluginResult upload_file_result = upload_file(filepath, "", file_info, thumbnail_info, err_msg); + if(upload_file_result != PluginResult::OK) + return upload_file_result; + + mxc_url = std::move(file_info.content_uri); + + rapidjson::Document request_data(rapidjson::kObjectType); + { + std::lock_guard lock(room_data_mutex); + for(const auto &it : custom_emoji_by_key) { + rapidjson::Document emoji_obj(rapidjson::kObjectType); + emoji_obj.AddMember("url", rapidjson::Value(it.second.url.c_str(), request_data.GetAllocator()).Move(), request_data.GetAllocator()); + emoji_obj.AddMember("width", it.second.size.x, request_data.GetAllocator()); + emoji_obj.AddMember("height", it.second.size.y, request_data.GetAllocator()); + request_data.AddMember(rapidjson::Value(it.first.c_str(), request_data.GetAllocator()).Move(), std::move(emoji_obj), request_data.GetAllocator()); + } + } + + CustomEmoji custom_emoji; + custom_emoji.url = mxc_url; + rapidjson::Document emoji_obj(rapidjson::kObjectType); + emoji_obj.AddMember("url", rapidjson::Value(mxc_url.c_str(), request_data.GetAllocator()).Move(), request_data.GetAllocator()); + if(file_info.dimensions) { + custom_emoji.size = clamp_to_size(mgl::vec2i(file_info.dimensions->width, file_info.dimensions->height), custom_emoji_max_size); + emoji_obj.AddMember("width", custom_emoji.size.x, request_data.GetAllocator()); + emoji_obj.AddMember("height", custom_emoji.size.y, request_data.GetAllocator()); + } + request_data.AddMember(rapidjson::Value(key.c_str(), request_data.GetAllocator()).Move(), std::move(emoji_obj), request_data.GetAllocator()); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + request_data.Accept(writer); + + std::vector additional_args = { + { "-X", "PUT" }, + { "-H", "content-type: application/json" }, + { "-H", "Authorization: Bearer " + access_token }, + { "--data-binary", buffer.GetString() } + }; + + std::string server_response; + DownloadResult download_result = download_to_string(homeserver + "/_matrix/client/r0/user/" + my_user_id + "/account_data/qm.emoji", server_response, std::move(additional_args), true); + if(download_result != DownloadResult::OK) + return download_result_to_plugin_result(download_result); + + std::lock_guard lock(room_data_mutex); + custom_emoji_by_key[key] = std::move(custom_emoji); + return PluginResult::OK; + } + + bool Matrix::delete_custom_emoji(const std::string &key) { + rapidjson::Document request_data(rapidjson::kObjectType); + { + std::lock_guard lock(room_data_mutex); + auto it = custom_emoji_by_key.find(key); + if(it == custom_emoji_by_key.end()) + return false; + + for(const auto &it : custom_emoji_by_key) { + if(it.first == key) + continue; + + rapidjson::Document emoji_obj(rapidjson::kObjectType); + emoji_obj.AddMember("url", rapidjson::Value(it.second.url.c_str(), request_data.GetAllocator()).Move(), request_data.GetAllocator()); + emoji_obj.AddMember("width", it.second.size.x, request_data.GetAllocator()); + emoji_obj.AddMember("height", it.second.size.y, request_data.GetAllocator()); + request_data.AddMember(rapidjson::Value(it.first.c_str(), request_data.GetAllocator()).Move(), std::move(emoji_obj), request_data.GetAllocator()); + } + } + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + request_data.Accept(writer); + + std::vector additional_args = { + { "-X", "PUT" }, + { "-H", "content-type: application/json" }, + { "-H", "Authorization: Bearer " + access_token }, + { "--data-binary", buffer.GetString() } + }; + + std::string server_response; + DownloadResult download_result = download_to_string(homeserver + "/_matrix/client/r0/user/" + my_user_id + "/account_data/qm.emoji", server_response, std::move(additional_args), true); + if(download_result != DownloadResult::OK) + return false; + + std::lock_guard lock(room_data_mutex); + auto it = custom_emoji_by_key.find(key); + if(it != custom_emoji_by_key.end()) + custom_emoji_by_key.erase(it); + + return true; + } + + bool Matrix::rename_custom_emoji(const std::string &key, const std::string &new_key) { + rapidjson::Document request_data(rapidjson::kObjectType); + { + std::lock_guard lock(room_data_mutex); + auto custom_emoji_list_copy = custom_emoji_by_key; + auto it = custom_emoji_list_copy.find(key); + if(it == custom_emoji_list_copy.end()) + return false; + + auto custom_emoji_copy = it->second; + custom_emoji_list_copy.erase(it); + custom_emoji_list_copy[new_key] = std::move(custom_emoji_copy); + for(const auto &it : custom_emoji_list_copy) { + rapidjson::Document emoji_obj(rapidjson::kObjectType); + emoji_obj.AddMember("url", rapidjson::Value(it.second.url.c_str(), request_data.GetAllocator()).Move(), request_data.GetAllocator()); + emoji_obj.AddMember("width", it.second.size.x, request_data.GetAllocator()); + emoji_obj.AddMember("height", it.second.size.y, request_data.GetAllocator()); + request_data.AddMember(rapidjson::Value(it.first.c_str(), request_data.GetAllocator()).Move(), std::move(emoji_obj), request_data.GetAllocator()); + } + } + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + request_data.Accept(writer); + + std::vector additional_args = { + { "-X", "PUT" }, + { "-H", "content-type: application/json" }, + { "-H", "Authorization: Bearer " + access_token }, + { "--data-binary", buffer.GetString() } + }; + + std::string server_response; + DownloadResult download_result = download_to_string(homeserver + "/_matrix/client/r0/user/" + my_user_id + "/account_data/qm.emoji", server_response, std::move(additional_args), true); + if(download_result != DownloadResult::OK) + return false; + + std::lock_guard lock(room_data_mutex); + auto it = custom_emoji_by_key.find(key); + if(it != custom_emoji_by_key.end()) { + auto custom_emoji_copy = it->second; + custom_emoji_by_key.erase(it); + custom_emoji_by_key[new_key] = std::move(custom_emoji_copy); + } + + return true; + } + + bool Matrix::does_custom_emoji_with_name_exist(const std::string &name) { + assert(is_initial_sync_finished()); + std::lock_guard lock(room_data_mutex); + return custom_emoji_by_key.find(name) != custom_emoji_by_key.end(); + } + + std::unordered_map Matrix::get_custom_emojis() { + assert(is_initial_sync_finished()); + std::lock_guard lock(room_data_mutex); + return custom_emoji_by_key; } PluginResult Matrix::post_file(RoomData *room, const std::string &filepath, std::string filename, std::string &event_id_response, std::string &err_msg, void *relates_to) { @@ -4092,7 +4500,7 @@ namespace QuickMedia { UploadInfo file_info; UploadInfo thumbnail_info; - PluginResult upload_file_result = upload_file(room, filepath, filename, file_info, thumbnail_info, err_msg); + PluginResult upload_file_result = upload_file(filepath, filename, file_info, thumbnail_info, err_msg); if(upload_file_result != PluginResult::OK) return upload_file_result; @@ -4107,7 +4515,7 @@ namespace QuickMedia { return post_message(room, filename, event_id_response, file_info_opt, thumbnail_info_opt); } - PluginResult Matrix::upload_file(RoomData *room, const std::string &filepath, std::string filename, UploadInfo &file_info, UploadInfo &thumbnail_info, std::string &err_msg, bool upload_thumbnail) { + PluginResult Matrix::upload_file(const std::string &filepath, std::string filename, UploadInfo &file_info, UploadInfo &thumbnail_info, std::string &err_msg, bool upload_thumbnail) { FileAnalyzer file_analyzer; if(!file_analyzer.load_file(filepath.c_str(), true)) { err_msg = "Failed to load " + filepath; @@ -4149,7 +4557,7 @@ namespace QuickMedia { if(video_get_middle_frame(file_analyzer, tmp_filename, thumbnail_max_size.x, thumbnail_max_size.y)) { UploadInfo upload_info_ignored; // Ignore because it wont be set anyways. Thumbnails dont have thumbnails. - PluginResult upload_thumbnail_result = upload_file(room, tmp_filename, thumbnail_filename.data, thumbnail_info, upload_info_ignored, err_msg, false); + PluginResult upload_thumbnail_result = upload_file(tmp_filename, thumbnail_filename.data, thumbnail_info, upload_info_ignored, err_msg, false); if(upload_thumbnail_result != PluginResult::OK) { close(tmp_file); remove(tmp_filename); @@ -4177,7 +4585,7 @@ namespace QuickMedia { thumbnail_filename = thumbnail_filename.filename_no_ext() + ".thumb" + thumbnail_filename.ext(); UploadInfo upload_info_ignored; // Ignore because it wont be set anyways. Thumbnails dont have thumbnails. - PluginResult upload_thumbnail_result = upload_file(room, thumbnail_path, thumbnail_filename.data, thumbnail_info, upload_info_ignored, err_msg, false); + PluginResult upload_thumbnail_result = upload_file(thumbnail_path, thumbnail_filename.data, thumbnail_info, upload_info_ignored, err_msg, false); if(upload_thumbnail_result != PluginResult::OK) { close(tmp_file); remove(tmp_filename); @@ -4811,7 +5219,7 @@ namespace QuickMedia { if(avatar_url_json.IsString()) { std::string avatar_url = thumbnail_url_extract_media_id(avatar_url_json.GetString()); if(!avatar_url.empty()) - avatar_url = get_thumbnail_url(homeserver, avatar_url); + avatar_url = get_avatar_thumbnail_url(homeserver, avatar_url); if(!avatar_url.empty()) room_body_item->thumbnail_url = std::move(avatar_url); @@ -4883,7 +5291,7 @@ namespace QuickMedia { if(avatar_url_json.IsString()) { std::string avatar_url = thumbnail_url_extract_media_id(std::string(avatar_url_json.GetString(), avatar_url_json.GetStringLength())); if(!avatar_url.empty()) - avatar_url = get_thumbnail_url(homeserver, avatar_url); + avatar_url = get_avatar_thumbnail_url(homeserver, avatar_url); body_item->thumbnail_url = std::move(avatar_url); } body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; @@ -5037,6 +5445,7 @@ namespace QuickMedia { void Matrix::set_next_batch(std::string new_next_batch) { std::lock_guard lock(next_batch_mutex); next_batch = std::move(new_next_batch); + initial_sync_finished = !next_batch.empty(); } std::string Matrix::get_next_batch() { @@ -5129,7 +5538,7 @@ namespace QuickMedia { if(avatar_url_json.IsString()) avatar_url = std::string(avatar_url_json.GetString(), avatar_url_json.GetStringLength()); if(!avatar_url.empty()) - avatar_url = get_thumbnail_url(homeserver, thumbnail_url_extract_media_id(avatar_url)); // TODO: Remove the constant strings around to reduce memory usage (6.3mb) + avatar_url = get_avatar_thumbnail_url(homeserver, thumbnail_url_extract_media_id(avatar_url)); // TODO: Remove the constant strings around to reduce memory usage (6.3mb) room->set_user_avatar_url(user, avatar_url, 0); room->set_user_display_name(user, display_name, 0); -- cgit v1.2.3