From d123c41cd3ad4f0d55ae134be69e7ffd144dbb74 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Tue, 18 May 2021 22:05:19 +0200 Subject: Add mention autocomplete --- README.md | 5 +- TODO | 9 +- include/Body.hpp | 8 +- include/Entry.hpp | 3 + include/Text.hpp | 3 + plugins/Matrix.hpp | 64 ++++++++-- src/Body.cpp | 13 ++ src/Entry.cpp | 8 ++ src/QuickMedia.cpp | 321 +++++++++++++++++++++++++++++++++++++++---------- src/SearchBar.cpp | 6 +- src/Tabs.cpp | 8 +- src/Text.cpp | 45 ++++--- src/plugins/Matrix.cpp | 244 +++++++++++++++++++++++++++++-------- 13 files changed, 588 insertions(+), 149 deletions(-) diff --git a/README.md b/README.md index 6058e2e..7e208c5 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,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 `ESC` or `Backspace` to close the video.\ -Press `Ctrl + D` to clear the search input text.\ +Press `Ctrl + D` to clear the input text.\ 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 video comments, related videos or video channel when watching a video (if supported).\ @@ -53,7 +53,7 @@ Press `Enter` to view image/video attached to matrix message, or to view the url Press `I` 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 `Ctrl + 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 + D` to delete a message on matrix.\ Press `Ctrl + C` to copy the text of the selected item to the clipboard.\ Press `U` in matrix or in a 4chan thread to bring up the file manager to choose a file to upload.\ Press `Ctrl + V` to upload media to room in matrix if the clipboard contains a valid absolute filepath.\ @@ -63,6 +63,7 @@ Press `Ctrl+Alt+Arrow up` / `Ctrl+Alt+Arrow down` or `Ctrl+Alt+K` / `Ctrl+Alt+J` Press `Ctrl + S` to save the displaying image/video/audio (does currently not work for manga pages).\ Press `Ctrl + Enter` to submit text, ignoring the selected item (when saving a file or selecting a server for matrix room directory).\ Press `Ctrl + Enter` to save the file to the selected directory with the selected name, when downloading a file. +Text input in 4chan/matrix can be navigated with the arrow keys and if ctrl is pressed then the caret moves word by word. 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 diff --git a/TODO b/TODO index 5f112e2..ebfaa93 100644 --- a/TODO +++ b/TODO @@ -22,7 +22,7 @@ Scrolling in images still messes up the |current| page sometimes, need a way to Show filename at the bottom when viewing an image/video on 4chan. Use https://github.com/simdjson/simdjson as a json library in other parts than matrix. Sanitize check: do not allow pasting more than 2gb of text. -Implement mentions in matrix with an autofill list, like on element. Also do the same with / commands. +Add autocomplete for matrix commands. Add option to disable autosearch and search when pressing enter instead or something? this would be needed for mobile phones where typing is slow. Render view to a rendertexture and render that instead of redrawing every time every time. Provide a way to specify when notifications should be received (using matrix api) and also read the notification config from matrix. Also provide a way to disable notifications globally. @@ -80,8 +80,7 @@ Disable message input in matrix when muted. Preview rooms? Handle matrix token being invalidated while running. Update upload limit if its updated on the server. -Editing a reply removes reply formatting (both in body and formatted_body). Element also does this when you edit a reply twice. This breaks element mobile that is unable to display replied-to messages without correct formatting (doesn't fetch the replied-to message). -This also removes the mentioned name which breaks mention for reply. +Editing a reply removes reply formatting (both in body and formatted_body). Element also does this when you edit a reply twice. This breaks element mobile that is unable to display replied-to messages without correct formatting (doesn't fetch the replied-to message). This also removes the mentioned name which breaks mention for reply. Implement m.room.tombstone. Show a marker when a room uses encryption. Scroll tabs if there are more than 3 tab items and show arrow on left/right side when there are more items to see. @@ -125,4 +124,6 @@ Add client side 4chan file size limit (note, webm has different limit than image Add client side 4chan max comment chars limit. Dynamically fetch 4chan api key, if it ever changes in the future. Same for youtube. Set curl download limits everywhere (when saving to file, downloading to json, etc...). -In the downloader if we already have the url in thumbnail/video cache, then copy it to the destination instead of redownloading it. This would also fix downloading images when viewing a manga page. \ No newline at end of file +In the downloader if we already have the url in thumbnail/video cache, then copy it to the destination instead of redownloading it. This would also fix downloading images when viewing a manga page. +Update timestamp of messages posted with matrix (and move them to the correct place in the timeline) when we receive the message from the server. This is needed when the localtime is messed up (for example when rebooting from windows into linux). +Remove dependency on imagemagick and create a function that forks the processes and creates a thumbnail from an input filepath and output filepath. That new process can use sf::Image to load and save the images. \ No newline at end of file diff --git a/include/Body.hpp b/include/Body.hpp index 84d0f14..9798992 100644 --- a/include/Body.hpp +++ b/include/Body.hpp @@ -232,7 +232,6 @@ namespace QuickMedia { // TODO: Highlight the part of the text that matches the search. void filter_search_fuzzy(const std::string &text); - void filter_search_fuzzy_item(const std::string &text, BodyItem *body_item); bool no_items_visible() const; @@ -246,6 +245,9 @@ namespace QuickMedia { bool is_body_full_with_items() const { return items_cut_off; } int get_num_visible_items() const { return num_visible_items; }; + // Call this once after adding new items + void items_set_dirty(); + sf::Text progress_text; sf::Text replies_text; sf::Text embedded_item_load_text; @@ -265,6 +267,7 @@ namespace QuickMedia { std::function on_top_reached = nullptr; std::function on_bottom_reached = nullptr; private: + void filter_search_fuzzy_item(const std::string &text, BodyItem *body_item); 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, bool merge_with_previous = false); void update_dirty_state(BodyItem *body_item, float width); void clear_body_item_cache(BodyItem *body_item); @@ -325,5 +328,8 @@ namespace QuickMedia { bool render_selected_item_bg = true; float item_background_target_pos_y = 0.0f; float item_background_target_height = 0.0f; + // TODO: Instead of using this, add functions for modifying |items| and apply the filter on those new items + bool items_dirty = false; + std::string current_filter; }; } \ No newline at end of file diff --git a/include/Entry.hpp b/include/Entry.hpp index 581dc14..22680b5 100644 --- a/include/Entry.hpp +++ b/include/Entry.hpp @@ -30,6 +30,9 @@ namespace QuickMedia { void move_caret_to_end(); void append_text(std::string str); + void replace(size_t start_index, size_t length, const sf::String &insert_str); + int get_caret_index() const; + bool is_editable() const; float get_height(); const sf::String& get_text() const; diff --git a/include/Text.hpp b/include/Text.hpp index 989edbb..5749072 100644 --- a/include/Text.hpp +++ b/include/Text.hpp @@ -82,6 +82,9 @@ namespace QuickMedia void setCharacterSize(unsigned int characterSize); unsigned int getCharacterSize() const; + void replace(size_t start_index, size_t length, const sf::String &insert_str); + int getCaretIndex() const; + void setFillColor(sf::Color color); void setLineSpacing(float lineSpacing); diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index 25b2633..0e96fd7 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -19,12 +19,7 @@ namespace QuickMedia { std::string remove_reply_formatting(const std::string &str); std::string message_get_body_remove_formatting(Message *message); std::string extract_first_line_remove_newline_elipses(const std::string &str, size_t max_length); - - enum class UserResolveState { - NOT_RESOLVED, - RESOLVING, - RESOLVED - }; + sf::Color user_id_to_color(const std::string &user_id); struct UserInfo { friend struct RoomData; @@ -34,7 +29,6 @@ namespace QuickMedia { RoomData *room; const sf::Color display_name_color; const std::string user_id; - UserResolveState resolve_state; private: std::string display_name; std::string avatar_url; @@ -63,9 +57,42 @@ namespace QuickMedia { REACTION }; + struct MatrixEventUserInfo { + std::string user_id; + std::optional display_name; + std::optional avatar_url; + }; + class MatrixEvent { public: + enum class Type { + ADD_USER, + REMOVE_USER, + USER_INFO + }; + + MatrixEvent(Type type) : type(type) {} virtual ~MatrixEvent() = default; + + const Type type; + }; + + class MatrixAddUserEvent : public MatrixEvent { + public: + MatrixAddUserEvent(MatrixEventUserInfo user_info) : MatrixEvent(Type::ADD_USER), user_info(std::move(user_info)) {} + const MatrixEventUserInfo user_info; + }; + + class MatrixRemoveUserEvent : public MatrixEvent { + public: + MatrixRemoveUserEvent(MatrixEventUserInfo user_info) : MatrixEvent(Type::REMOVE_USER), user_info(std::move(user_info)) {} + const MatrixEventUserInfo user_info; + }; + + class MatrixUserInfoEvent : public MatrixEvent { + public: + MatrixUserInfoEvent(MatrixEventUserInfo user_info) : MatrixEvent(Type::USER_INFO), user_info(std::move(user_info)) {} + const MatrixEventUserInfo user_info; }; struct Message { @@ -108,6 +135,7 @@ namespace QuickMedia { std::shared_ptr get_message_by_id(const std::string &id); + std::vector> get_users(); std::vector> get_users_excluding_me(const std::string &my_user_id); void acquire_room_lock(); @@ -543,9 +571,24 @@ namespace QuickMedia { std::shared_ptr get_message_by_id(RoomData *room, const std::string &event_id); RoomData* get_room_by_id(const std::string &id); - void update_user_with_latest_state(RoomData *room, const std::string &user_id); void update_room_users(RoomData *room); + + // Only one room can have event queue enabled at once + void enable_event_queue(RoomData *room); + // Also clears the event queue + void disable_event_queue(); + + // Has to be called in the main thread. + // Returns nullptr if there are no new events. + std::unique_ptr pop_event(); private: + // No-op if sync is cache or if |room| is not the currently enabled event queue room + void trigger_event(RoomData *room, std::unique_ptr event); + + void replace_mentions(RoomData *room, std::string &text); + std::string body_to_formatted_body(RoomData *room, const std::string &body); + std::string create_formatted_body_for_message_reply(RoomData *room, const Message *message, const std::string &body); + PluginResult set_qm_last_read_message_timestamp(RoomData *room, int64_t timestamp); PluginResult parse_sync_response(const rapidjson::Document &root, bool is_additional_messages_sync, bool initial_sync); @@ -575,9 +618,12 @@ namespace QuickMedia { void set_next_batch(std::string new_next_batch); std::string get_next_batch(); void clear_sync_cache_for_new_sync(); - std::shared_ptr get_user_by_id(RoomData *room, const std::string &user_id); + std::shared_ptr get_user_by_id(RoomData *room, const std::string &user_id, bool *is_new_user = nullptr, bool create_if_not_found = true); std::string get_filter_cached(); private: + std::deque> event_queue; + std::mutex event_queue_mutex; + RoomData *current_event_queue_room = nullptr; std::vector> rooms; std::unordered_map room_data_by_id; // value is an index into |rooms| std::recursive_mutex room_data_mutex; diff --git a/src/Body.cpp b/src/Body.cpp index 2e267c5..4f2e816 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -484,6 +484,11 @@ namespace QuickMedia { // TODO: Use a render target for the whole body so all images can be put into one. // TODO: Load thumbnails with more than one thread. void Body::draw(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size, const Json::Value &content_progress) { + if(items_dirty) { + items_dirty = false; + filter_search_fuzzy(current_filter); + } + sf::Vector2f scissor_pos = pos; sf::Vector2f scissor_size = size; const float start_y = pos.y; @@ -1486,10 +1491,14 @@ namespace QuickMedia { } void Body::filter_search_fuzzy(const std::string &text) { + current_filter = text; + if(text.empty()) { for(auto &item : items) { item->visible = true; } + + select_first_item(); return; } @@ -1531,4 +1540,8 @@ namespace QuickMedia { page_scroll = scroll; clamp_selected_item_to_body_count = 1; } + + void Body::items_set_dirty() { + items_dirty = true; + } } diff --git a/src/Entry.cpp b/src/Entry.cpp index ba9718b..7e1b6d9 100644 --- a/src/Entry.cpp +++ b/src/Entry.cpp @@ -95,6 +95,14 @@ namespace QuickMedia { text.appendText(std::move(str)); } + void Entry::replace(size_t start_index, size_t length, const sf::String &insert_str) { + text.replace(start_index, length, insert_str); + } + + int Entry::get_caret_index() const { + return text.getCaretIndex(); + } + void Entry::set_position(const sf::Vector2f &pos) { background.set_position(pos); text.setPosition(pos + sf::Vector2f(background_margin_horizontal, background_margin_vertical)); diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 755f6df..28bd72f 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -1672,6 +1672,7 @@ namespace QuickMedia { while(window.isOpen()) { auto matrix_chat_page = std::make_unique(this, current_chat_room->id, rooms_page); bool move_room = chat_page(matrix_chat_page.get(), current_chat_room, tabs, selected_tab); + matrix_chat_page->messages_tab_visible = false; if(!move_room) break; @@ -3890,6 +3891,16 @@ namespace QuickMedia { return nullptr; } + // Returns |default_value| if the input items is empty + static size_t get_body_item_sorted_insert_position_by_author(BodyItems &body_items, const std::string &display_name, size_t default_value) { + for(size_t i = 0; i < body_items.size(); ++i) { + auto &body_item = body_items[i]; + if(strcasecmp(display_name.c_str(), body_item->get_author().c_str()) <= 0) + return i; + } + return default_value; + } + bool Program::chat_page(MatrixChatPage *matrix_chat_page, RoomData *current_room, std::vector &room_tabs, int room_selected_tab) { assert(current_room); assert(strcmp(plugin_name, "matrix") == 0); @@ -3918,17 +3929,17 @@ namespace QuickMedia { messages_tab.body->line_separator_color = sf::Color::Transparent; tabs.push_back(std::move(messages_tab)); - // ChatTab users_tab; - // users_tab.body = create_body(); - // users_tab.body->thumbnail_max_size = CHAT_MESSAGE_THUMBNAIL_MAX_SIZE; - // users_tab.body->attach_side = AttachSide::TOP; - // //users_tab.body->line_separator_color = sf::Color::Transparent; - // users_tab.text = sf::Text("Users", *FontLoader::get_font(FontLoader::FontType::LATIN), tab_text_size); - // tabs.push_back(std::move(users_tab)); + ChatTab users_tab; + users_tab.body = create_body(); + users_tab.body->thumbnail_max_size = CHAT_MESSAGE_THUMBNAIL_MAX_SIZE; + users_tab.body->attach_side = AttachSide::TOP; + users_tab.body->line_separator_color = sf::Color::Transparent; + tabs.push_back(std::move(users_tab)); - Tabs ui_tabs(&rounded_rectangle_shader, back_color); + Tabs ui_tabs(&rounded_rectangle_shader, sf::Color::Transparent); const int PINNED_TAB_INDEX = ui_tabs.add_tab("Pinned messages (0)"); const int MESSAGES_TAB_INDEX = ui_tabs.add_tab("Messages"); + const int USERS_TAB_INDEX = ui_tabs.add_tab("Users"); ui_tabs.set_selected(MESSAGES_TAB_INDEX); matrix_chat_page->chat_body = tabs[MESSAGES_TAB_INDEX].body.get(); @@ -3967,6 +3978,8 @@ namespace QuickMedia { tabs[selected_tab].body->clear_cache(); if(selected_tab == MESSAGES_TAB_INDEX) matrix_chat_page->messages_tab_visible = true; + else + matrix_chat_page->messages_tab_visible = false; read_marker_timer.restart(); redraw = true; if(typing) { @@ -4318,12 +4331,85 @@ namespace QuickMedia { } }; + struct Mention { + sf::Clock filter_timer; + bool visible = false; + bool filter_updated = false; + sf::String filter; + Body *users_tab_body = nullptr; + + void show() { + visible = true; + } + + void hide() { + visible = false; + filter_updated = false; + filter.clear(); + users_tab_body->filter_search_fuzzy(""); + } + + void handle_event(const sf::Event &event) { + if(visible) { + if(event.type == sf::Event::TextEntered) { + filter_timer.restart(); + if(event.text.unicode > 32) { + filter += event.text.unicode; + filter_updated = true; + } else if(event.text.unicode == 8) { // 8 = backspace + if(filter.getSize() == 0) { + hide(); + } else { + filter.erase(filter.getSize() - 1, 1); + filter_updated = true; + } + } else if(event.text.unicode == ' ' || event.text.unicode == '\t') { + hide(); + } + } else if(event.type == sf::Event::KeyPressed) { + if(event.key.code == sf::Keyboard::Up) { + users_tab_body->select_previous_item(); + } else if(event.key.code == sf::Keyboard::Down) { + users_tab_body->select_next_item(); + } else if(event.key.code == sf::Keyboard::Enter && event.key.shift) { + hide(); + } + } + } + + if(event.type == sf::Event::TextEntered && event.text.unicode == '@' && !visible) + show(); + } + + void update() { + if(visible && filter_updated && filter_timer.getElapsedTime().asMilliseconds() > 50) { + filter_updated = false; + // TODO: Use std::string instead of sf::String + auto u8 = filter.toUtf8(); + users_tab_body->filter_search_fuzzy(*(std::string*)&u8); + } + } + }; + + Mention mention; + mention.users_tab_body = tabs[USERS_TAB_INDEX].body.get(); + bool frame_skip_text_entry = false; - chat_input.on_submit_callback = [this, &frame_skip_text_entry, &tabs, &me, &chat_input, &ui_tabs, MESSAGES_TAB_INDEX, ¤t_room, &new_page, &chat_state, &pending_sent_replies, ¤tly_operating_on_item, &post_task_queue, &process_reactions](std::string text) mutable { - if(!current_room) + chat_input.on_submit_callback = [this, &frame_skip_text_entry, &mention, &tabs, &me, &chat_input, &ui_tabs, MESSAGES_TAB_INDEX, USERS_TAB_INDEX, ¤t_room, &new_page, &chat_state, &pending_sent_replies, ¤tly_operating_on_item, &post_task_queue, &process_reactions](std::string text) mutable { + if(mention.visible) { + BodyItem *selected_mention_item = tabs[USERS_TAB_INDEX].body->get_selected(); + if(selected_mention_item) { + std::string str_to_append = selected_mention_item->get_description(); + if(!str_to_append.empty()) + str_to_append.erase(0, 1); + str_to_append += ": "; + chat_input.replace(chat_input.get_caret_index() - mention.filter.getSize(), mention.filter.getSize(), sf::String::fromUtf8(str_to_append.begin(), str_to_append.end())); + mention.hide(); + } return false; - + } + frame_skip_text_entry = true; const int selected_tab = ui_tabs.get_selected(); @@ -4522,21 +4608,6 @@ namespace QuickMedia { if(!event_data) return; -#if 0 - if(event_data->message->user->resolve_state == UserResolveState::NOT_RESOLVED) { - fetch_message = event_data->message; - event_data->message->user->resolve_state = UserResolveState::RESOLVING; - std::string user_id = event_data->message->user->user_id; - fetch_message_future = [this, ¤t_room, user_id]() { - matrix->update_user_with_latest_state(current_room, user_id); - return FetchMessageResult{FetchMessageType::USER_UPDATE, nullptr}; - }; - return; - } else if(event_data->message->user->resolve_state == UserResolveState::RESOLVING) { - return; - } -#endif - // Fetch replied to message if(event_data->status == FetchStatus::FINISHED_LOADING && event_data->message) { if(event_data->message->related_event_id.empty() || (body_item->embedded_item_status != FetchStatus::NONE && body_item->embedded_item_status != FetchStatus::QUEUED_LOADING)) @@ -4594,21 +4665,6 @@ namespace QuickMedia { if(!message) return; -#if 0 - if(message->user->resolve_state == UserResolveState::NOT_RESOLVED) { - fetch_message = message; - message->user->resolve_state = UserResolveState::RESOLVING; - std::string user_id = message->user->user_id; - fetch_message_future = AsyncTask([this, ¤t_room, user_id]() { - matrix->update_user_with_latest_state(current_room, user_id); - return FetchMessageResult{FetchMessageType::USER_UPDATE, nullptr}; - }); - return; - } else if(message->user->resolve_state == UserResolveState::RESOLVING) { - return; - } -#endif - if(message_is_timeline(message) && (!last_visible_timeline_message || message->timestamp > last_visible_timeline_message->timestamp)) last_visible_timeline_message = message; @@ -4890,7 +4946,7 @@ namespace QuickMedia { } }; - auto cleanup_tasks = [&set_read_marker_future, &fetch_message_future, &fetch_users_future, &typing_state_queue, &typing_state_thread, &post_task_queue, &provisional_message_queue, &fetched_messages_set, &sent_messages, &pending_sent_replies, &post_thread, &tabs, PINNED_TAB_INDEX]() { + auto cleanup_tasks = [&set_read_marker_future, &fetch_message_future, &fetch_users_future, &typing_state_queue, &typing_state_thread, &post_task_queue, &provisional_message_queue, &fetched_messages_set, &sent_messages, &pending_sent_replies, &post_thread, &tabs, MESSAGES_TAB_INDEX, PINNED_TAB_INDEX, USERS_TAB_INDEX]() { set_read_marker_future.cancel(); fetch_message_future.cancel(); fetch_users_future.cancel(); @@ -4912,15 +4968,102 @@ namespace QuickMedia { //unreferenced_event_by_room.clear(); if(!tabs.empty()) { + tabs[MESSAGES_TAB_INDEX].body->clear_items(); for(auto &body_item : tabs[PINNED_TAB_INDEX].body->items) { delete (PinnedEventData*)body_item->userdata; } tabs[PINNED_TAB_INDEX].body->clear_items(); + tabs[USERS_TAB_INDEX].body->clear_items(); } //tabs.clear(); }; + auto on_add_user_event = [&ui_tabs, &tabs, USERS_TAB_INDEX](MatrixAddUserEvent *event) { + // Ignore if the user already exists in the room + // TODO: Remove the need for this + for(auto &body_item : tabs[USERS_TAB_INDEX].body->items) { + if(body_item->url == event->user_info.user_id) + return; + } + + std::string display_name = event->user_info.display_name.value_or(event->user_info.user_id); + size_t insert_position = get_body_item_sorted_insert_position_by_author(tabs[USERS_TAB_INDEX].body->items, display_name, 0); + + auto body_item = BodyItem::create(""); + body_item->url = event->user_info.user_id; + body_item->set_author(std::move(display_name)); + body_item->set_author_color(user_id_to_color(event->user_info.user_id)); + body_item->set_description(event->user_info.user_id); + body_item->set_description_color(sf::Color(179, 179, 179)); + if(event->user_info.avatar_url) + body_item->thumbnail_url = event->user_info.avatar_url.value(); + body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; + body_item->thumbnail_size = AVATAR_THUMBNAIL_SIZE; + tabs[USERS_TAB_INDEX].body->items.insert(tabs[USERS_TAB_INDEX].body->items.begin() + insert_position, std::move(body_item)); + tabs[USERS_TAB_INDEX].body->items_set_dirty(); + + ui_tabs.set_text(USERS_TAB_INDEX, "Users (" + std::to_string(tabs[USERS_TAB_INDEX].body->items.size()) + ")"); + }; + + // TODO: Actually trigger this when a user leaves the room. Also remove the user from the room in the matrix plugin + auto on_remove_user_event = [&ui_tabs, &tabs, USERS_TAB_INDEX](MatrixRemoveUserEvent *event) { + for(auto it = tabs[USERS_TAB_INDEX].body->items.begin(), end = tabs[USERS_TAB_INDEX].body->items.end(); it != end; ++it) { + if((*it)->url == event->user_info.user_id) { + tabs[USERS_TAB_INDEX].body->items.erase(it); + ui_tabs.set_text(USERS_TAB_INDEX, "Users (" + std::to_string(tabs[USERS_TAB_INDEX].body->items.size()) + ")"); + return; + } + } + }; + + auto on_user_info_event = [&tabs, USERS_TAB_INDEX](MatrixUserInfoEvent *event) { + for(auto it = tabs[USERS_TAB_INDEX].body->items.begin(), end = tabs[USERS_TAB_INDEX].body->items.end(); it != end; ++it) { + if((*it)->url == event->user_info.user_id) { + if(event->user_info.avatar_url) + (*it)->thumbnail_url = event->user_info.avatar_url.value(); + + if(event->user_info.display_name) { + std::string display_name; + if(event->user_info.display_name.value().empty()) + display_name = event->user_info.user_id; + else + display_name = event->user_info.display_name.value(); + + (*it)->set_author(std::move(display_name)); + + auto user_body_item = *it; + tabs[USERS_TAB_INDEX].body->items.erase(it); + + // TODO: extract_first_line_remove_newline_elipses(room->get_user_display_name(message->user), AUTHOR_MAX_LENGTH), + // But that should be done in Text because we need author to be 100% the same as in the input to reorder users + size_t insert_position = get_body_item_sorted_insert_position_by_author(tabs[USERS_TAB_INDEX].body->items, user_body_item->get_author(), 0); + tabs[USERS_TAB_INDEX].body->items.insert(tabs[USERS_TAB_INDEX].body->items.begin() + insert_position, std::move(user_body_item)); + tabs[USERS_TAB_INDEX].body->items_set_dirty(); + } + + return; + } + } + }; + + matrix->enable_event_queue(current_room); + { + auto users_in_room = current_room->get_users(); + for(auto &user : users_in_room) { + std::string display_name = current_room->get_user_display_name(user); + std::string avatar_url = current_room->get_user_avatar_url(user); + + MatrixEventUserInfo user_info; + user_info.user_id = user->user_id; + user_info.display_name = std::move(display_name); + user_info.avatar_url = std::move(avatar_url); + + MatrixAddUserEvent add_user_event(std::move(user_info)); + on_add_user_event(&add_user_event); + } + } + // TODO: Remove this once synapse bug has been resolved where /sync does not include user info for new messages when using message filter that limits number of messages for initial sync, // and then only call this when viewing the users tab for the first time. // Note that this is not needed when new users join the room, as those will be included in the sync timeline (with membership events) @@ -4959,6 +5102,9 @@ namespace QuickMedia { tabs[i].body->on_top_reached = on_top_reached; } + const float body_padding_horizontal = 10.0f; + const float body_padding_vertical = 10.0f; + while (current_page == PageType::CHAT && window.isOpen() && !move_room) { sf::Int32 frame_time_ms = frame_timer.restart().asMilliseconds(); while (window.pollEvent(event)) { @@ -4977,8 +5123,12 @@ namespace QuickMedia { base_event_handler(event, PageType::EXIT, tabs[selected_tab].body.get(), nullptr, false, false); event_idle_handler(event); - if(!frame_skip_text_entry) - chat_input.process_event(event); + if(!frame_skip_text_entry) { + if(!mention.visible || event.type != sf::Event::KeyPressed || (event.key.code != sf::Keyboard::Up && event.key.code != sf::Keyboard::Down && event.key.code != sf::Keyboard::Left && event.key.code != sf::Keyboard::Right)) + chat_input.process_event(event); + if(chat_input.is_editable()) + mention.handle_event(event); + } if(draw_room_list) { if(room_tabs[room_selected_tab].body->on_event(window, event, false)) @@ -5150,7 +5300,7 @@ namespace QuickMedia { } } - if(event.key.control && event.key.code == sf::Keyboard::D) { + if(event.key.control && event.key.code == sf::Keyboard::D && !chat_input.is_editable()) { frame_skip_text_entry = true; BodyItem *selected = tabs[selected_tab].body->get_selected(); if(selected) { @@ -5203,14 +5353,18 @@ namespace QuickMedia { typing = true; } } else if(event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Escape) { - chat_input.set_editable(false); - chat_input.set_text(""); - chat_state = ChatState::NAVIGATING; - currently_operating_on_item = nullptr; - if(typing && current_room) { - fprintf(stderr, "Stopped typing\n"); - typing = false; - typing_state_queue.push(false); + if(mention.visible) { + mention.hide(); + } else { + chat_input.set_editable(false); + chat_input.set_text(""); + chat_state = ChatState::NAVIGATING; + currently_operating_on_item = nullptr; + if(typing && current_room) { + fprintf(stderr, "Stopped typing\n"); + typing = false; + typing_state_queue.push(false); + } } } } @@ -5219,6 +5373,8 @@ namespace QuickMedia { update_idle_state(); handle_window_close(); + mention.update(); + matrix_chat_page->update(); switch(new_page) { @@ -5251,6 +5407,7 @@ namespace QuickMedia { break; } case PageType::CHAT_LOGIN: { + matrix->disable_event_queue(); previous_messages_future.cancel(); cleanup_tasks(); tabs.clear(); @@ -5308,11 +5465,11 @@ namespace QuickMedia { const int selected_tab = ui_tabs.get_selected(); float room_name_padding_y = 0.0f; - if(selected_tab == MESSAGES_TAB_INDEX || selected_tab == PINNED_TAB_INDEX) + if(selected_tab == MESSAGES_TAB_INDEX || selected_tab == PINNED_TAB_INDEX || selected_tab == USERS_TAB_INDEX) room_name_padding_y = room_name_total_height; 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) + if(selected_tab != MESSAGES_TAB_INDEX) chat_input_height_full = 0.0f; const float chat_height = chat_input.get_height(); @@ -5324,14 +5481,12 @@ namespace QuickMedia { if(redraw) { redraw = false; - if(selected_tab == MESSAGES_TAB_INDEX || selected_tab == PINNED_TAB_INDEX) { + if(selected_tab == MESSAGES_TAB_INDEX || selected_tab == PINNED_TAB_INDEX || selected_tab == USERS_TAB_INDEX) { tab_vertical_offset = std::floor(10.0f * get_ui_scale()); } tab_shade_height = std::floor(tab_vertical_offset) + Tabs::get_height() + room_name_padding_y; - float body_padding_horizontal = 10.0f; - float body_padding_vertical = std::floor(10.0f); float body_width = window_size.x - body_padding_horizontal * 2.0f; /*if(body_width <= 480.0f) { body_width = window_size.x; @@ -5377,6 +5532,23 @@ namespace QuickMedia { } } + std::unique_ptr matrix_event; + while((matrix_event = matrix->pop_event()) != nullptr) { + if(matrix_event) { + switch(matrix_event->type) { + case MatrixEvent::Type::ADD_USER: + on_add_user_event(static_cast(matrix_event.get())); + break; + case MatrixEvent::Type::REMOVE_USER: + on_remove_user_event(static_cast(matrix_event.get())); + break; + case MatrixEvent::Type::USER_INFO: + on_user_info_event(static_cast(matrix_event.get())); + break; + } + } + } + sync_data.messages.clear(); sync_data.pinned_events = std::nullopt; matrix->get_room_sync_data(current_room, sync_data); @@ -5462,15 +5634,27 @@ namespace QuickMedia { window.clear(back_color); - if(chat_state == ChatState::URL_SELECTION) + if(chat_state == ChatState::URL_SELECTION) { url_selection_body.draw(window, body_pos, body_size); - else + } else { tabs[selected_tab].body->draw(window, body_pos, body_size); + if(selected_tab == MESSAGES_TAB_INDEX && mention.visible) { + const float user_mention_body_height = std::floor(300.0f * get_ui_scale()); + sf::RectangleShape user_mention_background(sf::Vector2f(body_size.x + body_padding_vertical*2.0f, user_mention_body_height)); + user_mention_background.setPosition(sf::Vector2f(body_pos.x - body_padding_vertical, body_pos.y + body_size.y - user_mention_body_height)); + user_mention_background.setFillColor(sf::Color(33, 37, 44)); + + window.draw(user_mention_background); + tabs[USERS_TAB_INDEX].body->draw(window, + user_mention_background.getPosition() + sf::Vector2f(body_padding_vertical, body_padding_vertical), + user_mention_background.getSize() - sf::Vector2f(body_padding_vertical*2.0f, body_padding_vertical)); + } + } //tab_shade.setSize(sf::Vector2f(window_size.x, tab_shade_height)); //window.draw(tab_shade); - if(selected_tab == MESSAGES_TAB_INDEX || selected_tab == PINNED_TAB_INDEX) { + if(selected_tab == MESSAGES_TAB_INDEX || selected_tab == PINNED_TAB_INDEX || selected_tab == USERS_TAB_INDEX) { float room_name_text_offset_x = 0.0f; if(room_avatar_sprite.getTexture() && room_avatar_sprite.getTexture()->getNativeHandle() != 0) { auto room_avatar_texture_size = room_avatar_sprite.getTexture()->getSize(); @@ -5608,6 +5792,13 @@ namespace QuickMedia { if(matrix && !matrix->is_initial_sync_finished()) { std::string err_msg; if(matrix->did_initial_sync_fail(err_msg)) { + matrix->disable_event_queue(); + previous_messages_future.cancel(); + cleanup_tasks(); + tabs.clear(); + unreferenced_events.clear(); + unresolved_reactions.clear(); + all_messages.clear(); show_notification("QuickMedia", "Initial matrix sync failed, error: " + err_msg, Urgency::CRITICAL); matrix->logout(); current_page = PageType::CHAT_LOGIN; @@ -5631,6 +5822,13 @@ namespace QuickMedia { while(!matrix->is_initial_sync_finished()) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); if(matrix->did_initial_sync_fail(err_msg)) { + matrix->disable_event_queue(); + previous_messages_future.cancel(); + cleanup_tasks(); + tabs.clear(); + unreferenced_events.clear(); + unresolved_reactions.clear(); + all_messages.clear(); show_notification("QuickMedia", "Initial matrix sync failed, error: " + err_msg, Urgency::CRITICAL); matrix->logout(); current_page = PageType::CHAT_LOGIN; @@ -5675,6 +5873,7 @@ namespace QuickMedia { } chat_page_end: + matrix->disable_event_queue(); previous_messages_future.cancel(); cleanup_tasks(); window.setTitle("QuickMedia - matrix"); diff --git a/src/SearchBar.cpp b/src/SearchBar.cpp index 7d7f72d..a7e3890 100644 --- a/src/SearchBar.cpp +++ b/src/SearchBar.cpp @@ -23,8 +23,8 @@ namespace QuickMedia { onTextSubmitCallback(nullptr), onTextBeginTypingCallback(nullptr), onAutocompleteRequestCallback(nullptr), - text_autosearch_delay(0), - autocomplete_search_delay(0), + text_autosearch_delay(50), + autocomplete_search_delay(250), caret_visible(true), text(placeholder, *FontLoader::get_font(FontLoader::FontType::LATIN), std::floor(16 * get_ui_scale())), autocomplete_text("", *FontLoader::get_font(FontLoader::FontType::LATIN), std::floor(16 * get_ui_scale())), @@ -112,7 +112,7 @@ namespace QuickMedia { backspace_pressed = false; if(event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::V && event.key.control) { auto clipboard = sf::Clipboard::getString().toUtf8(); - append_text(std::string(clipboard.begin(), clipboard.end())); + append_text(*(std::string*)&clipboard); } if(event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::D && event.key.control) { diff --git a/src/Tabs.cpp b/src/Tabs.cpp index 01e5d2d..b671262 100644 --- a/src/Tabs.cpp +++ b/src/Tabs.cpp @@ -72,9 +72,11 @@ namespace QuickMedia { tab_background_width = std::floor(width_per_tab - tab_margin_x*2.0f); background.set_size(sf::Vector2f(tab_background_width, tab_height)); - shade.setSize(sf::Vector2f(width, get_shade_height())); - shade.setPosition(std::floor(pos.x), std::floor(pos.y)); - window.draw(shade); + if(shade_color != sf::Color::Transparent) { + shade.setSize(sf::Vector2f(width, get_shade_height())); + shade.setPosition(std::floor(pos.x), std::floor(pos.y)); + window.draw(shade); + } float scroll_fixed = scroll + (tab_offset * width_per_tab); diff --git a/src/Text.cpp b/src/Text.cpp index 23dfb5b..3790c85 100644 --- a/src/Text.cpp +++ b/src/Text.cpp @@ -129,6 +129,21 @@ namespace QuickMedia { return characterSize; } + + void Text::replace(size_t start_index, size_t length, const sf::String &insert_str) { + int string_diff = (int)insert_str.getSize() - (int)length; + str.replace(start_index, length, insert_str); + dirty = true; + dirtyText = true; + if(caretIndex >= (int)start_index) { + caretIndex += string_diff; + dirtyCaret = true; + } + } + + int Text::getCaretIndex() const { + return caretIndex; + } void Text::setFillColor(sf::Color color) { @@ -800,17 +815,21 @@ namespace QuickMedia } else if(event.key.code == sf::Keyboard::BackSpace && caretIndex > 0) { - auto strBefore = str.substring(0, caretIndex - 1); - auto strAfter = str.substring(caretIndex); + str.erase(caretIndex - 1, 1); --caretIndex; - setString(strBefore + strAfter); + dirty = true; + dirtyText = true; dirtyCaret = true; } else if(event.key.code == sf::Keyboard::Delete && !caretAtEnd) { - auto strBefore = str.substring(0, caretIndex); - auto strAfter = str.substring(caretIndex + 1); - setString(strBefore + strAfter); + str.erase(caretIndex, 1); + dirty = true; + dirtyText = true; + } + else if(event.key.code == sf::Keyboard::D && event.key.control) + { + setString(""); } else if(event.key.code == sf::Keyboard::Up) { @@ -835,11 +854,7 @@ namespace QuickMedia if(caretAtEnd) str += '\n'; else - { - auto strBefore = str.substring(0, caretIndex); - auto strAfter = str.substring(caretIndex); - str = strBefore + '\n' + strAfter; - } + str.insert(caretIndex, '\n'); ++caretIndex; dirty = true; @@ -858,7 +873,7 @@ namespace QuickMedia { stringToAdd = sf::Clipboard::getString(); } - else if(event.text.unicode >= 32 || (event.text.unicode == 9 && !single_line_edit)) // 9 == tab + else if(event.text.unicode >= 32 || (event.text.unicode == '\t' && !single_line_edit)) stringToAdd = event.text.unicode; else return; @@ -866,11 +881,7 @@ namespace QuickMedia if(caretAtEnd) str += stringToAdd; else - { - auto strBefore = str.substring(0, caretIndex); - auto strAfter = str.substring(caretIndex); - str = strBefore + stringToAdd + strAfter; - } + str.insert(caretIndex, stringToAdd); caretIndex += stringToAdd.getSize(); dirty = true; diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 1d471fc..db31303 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -99,7 +99,7 @@ namespace QuickMedia { return std::abs(hash); } - static sf::Color user_id_to_color(const std::string &user_id) { + sf::Color user_id_to_color(const std::string &user_id) { const int num_colors = 8; const sf::Color colors[num_colors] = { sf::Color(54, 139, 214), @@ -115,13 +115,13 @@ namespace QuickMedia { } UserInfo::UserInfo(RoomData *room, std::string user_id) : - room(room), display_name_color(user_id_to_color(user_id)), user_id(user_id), resolve_state(UserResolveState::NOT_RESOLVED) + room(room), display_name_color(user_id_to_color(user_id)), user_id(user_id) { display_name = user_id; } UserInfo::UserInfo(RoomData *room, std::string user_id, std::string display_name, std::string avatar_url) : - room(room), display_name_color(user_id_to_color(user_id)), user_id(std::move(user_id)), resolve_state(UserResolveState::RESOLVED), display_name(std::move(display_name)), avatar_url(std::move(avatar_url)) { + room(room), display_name_color(user_id_to_color(user_id)), user_id(std::move(user_id)), display_name(std::move(display_name)), avatar_url(std::move(avatar_url)) { } @@ -168,13 +168,11 @@ namespace QuickMedia { user->display_name = std::move(display_name); if(user->display_name.empty()) user->display_name = user->user_id; - user->resolve_state = UserResolveState::RESOLVED; } void RoomData::set_user_avatar_url(std::shared_ptr &user, std::string avatar_url) { std::lock_guard lock(user_mutex); user->avatar_url = std::move(avatar_url); - user->resolve_state = UserResolveState::RESOLVED; } size_t RoomData::prepend_messages_reverse(const std::vector> &new_messages) { @@ -219,6 +217,16 @@ namespace QuickMedia { return message_it->second; } + std::vector> RoomData::get_users() { + std::lock_guard lock(user_mutex); + std::vector> users(user_info_by_user_id.size()); + size_t i = 0; + for(auto &[user_id, user] : user_info_by_user_id) { + users[i++] = user; + } + return users; + } + std::vector> RoomData::get_users_excluding_me(const std::string &my_user_id) { std::lock_guard lock(user_mutex); std::vector> users_excluding_me; @@ -1697,9 +1705,24 @@ namespace QuickMedia { //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); - auto user_info = get_user_by_id(room_data, user_id); - room_data->set_user_display_name(user_info, std::move(display_name)); - room_data->set_user_avatar_url(user_info, std::move(avatar_url)); + bool is_new_user; + auto user_info = get_user_by_id(room_data, user_id, &is_new_user); + room_data->set_user_display_name(user_info, display_name); + room_data->set_user_avatar_url(user_info, avatar_url); + + MatrixEventUserInfo event_user_info; + event_user_info.user_id = user_id; + event_user_info.display_name = display_name; + event_user_info.avatar_url = avatar_url; + + if(is_new_user) { + auto event = std::make_unique(std::move(event_user_info)); + trigger_event(room_data, std::move(event)); + } else { + auto event = std::make_unique(std::move(event_user_info)); + trigger_event(room_data, std::move(event)); + } + return user_info; } @@ -1945,16 +1968,28 @@ namespace QuickMedia { if(!content_json->IsObject()) return nullptr; - auto user = get_user_by_id(room_data, sender_json_str); - if(!user) { - // Note: this is important because otherwise replying and such is broken - fprintf(stderr, "Warning: skipping unknown user: %s\n", sender_json_str.c_str()); - return nullptr; + bool is_new_user; + auto user = get_user_by_id(room_data, sender_json_str, &is_new_user); + + if(is_new_user) { + MatrixEventUserInfo user_info; + user_info.user_id = user->user_id; + auto event = std::make_unique(std::move(user_info)); + trigger_event(room_data, std::move(event)); } auto user_sender = user; - if(sent_by_somebody_else) - user_sender = get_user_by_id(room_data, sender_json_orig->GetString()); + if(sent_by_somebody_else) { + bool is_new_user; + user_sender = get_user_by_id(room_data, sender_json_orig->GetString(), &is_new_user); + + if(is_new_user) { + MatrixEventUserInfo user_info; + user_info.user_id = user_sender->user_id; + auto event = std::make_unique(std::move(user_info)); + trigger_event(room_data, std::move(event)); + } + } time_t timestamp = 0; const rapidjson::Value &origin_server_ts = GetMember(event_item_json, "origin_server_ts"); @@ -2053,6 +2088,10 @@ namespace QuickMedia { const rapidjson::Value &new_displayname_json = GetMember(*content_json, "displayname"); const rapidjson::Value &new_avatar_url_json = GetMember(*content_json, "avatar_url"); const rapidjson::Value &prev_membership_json = GetMember(prev_content_json, "membership"); + + std::optional new_display_name; + std::optional new_avatar_url; + if(prev_membership_json.IsString() && strcmp(prev_membership_json.GetString(), "leave") == 0) { body = user_display_name + " joined the room"; } else if(new_displayname_json.IsString() && new_displayname_json.GetStringLength() > 0 && (!prev_displayname_json.IsString() || strcmp(new_displayname_json.GetString(), prev_displayname_json.GetString()) != 0)) { @@ -2063,22 +2102,36 @@ namespace QuickMedia { else prev_displayname_str = sender_json_str; body = extract_first_line_remove_newline_elipses(prev_displayname_str, AUTHOR_MAX_LENGTH) + " changed their display name to " + extract_first_line_remove_newline_elipses(new_displayname_str, AUTHOR_MAX_LENGTH); + new_display_name = new_displayname_str; room_data->set_user_display_name(user, std::move(new_displayname_str)); } else if((!new_displayname_json.IsString() || new_displayname_json.GetStringLength() == 0) && prev_displayname_json.IsString()) { body = user_display_name + " removed their display name"; + new_display_name = ""; room_data->set_user_display_name(user, ""); } else if(new_avatar_url_json.IsString() && new_avatar_url_json.GetStringLength() > 0 && (!prev_avatar_url_json.IsString() || strcmp(new_avatar_url_json.GetString(), prev_avatar_url_json.GetString()) != 0)) { body = user_display_name + " changed their 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 = new_avatar_url_str; room_data->set_user_avatar_url(user, std::move(new_avatar_url_str)); } else if((!new_avatar_url_json.IsString() || new_avatar_url_json.GetStringLength() == 0) && prev_avatar_url_json.IsString()) { body = user_display_name + " removed their profile picture"; + new_avatar_url = ""; room_data->set_user_avatar_url(user, ""); } else { body = user_display_name + " joined the room"; } + + if(new_display_name || new_avatar_url) { + MatrixEventUserInfo user_info; + user_info.user_id = user->user_id; + user_info.display_name = std::move(new_display_name); + user_info.avatar_url = std::move(new_avatar_url); + + auto event = std::make_unique(std::move(user_info)); + trigger_event(room_data, std::move(event)); + } } else { body = user_display_name + " joined the room"; } @@ -2483,6 +2536,8 @@ namespace QuickMedia { const rapidjson::Value &membership_json = GetMember(content_json, "membership"); if(membership_json.IsString() && strcmp(membership_json.GetString(), "invite") == 0) { + // TODO: Check this this room should be saved in the rooms list, which might be needed if the server doesn't give a non-invite events + // for the same data (user display name update, etc) Invite invite; RoomData invite_room; events_add_user_info(events_json, &invite_room); @@ -2490,10 +2545,6 @@ namespace QuickMedia { std::string sender_json_str(sender_json.GetString(), sender_json.GetStringLength()); auto invited_by = get_user_by_id(&invite_room, sender_json_str); - if(!invited_by) { - fprintf(stderr, "Invited by unknown user. Bug in homeserver?\n"); - break; - } set_room_info_to_users_if_empty(&invite_room, sender_json_str); @@ -2721,11 +2772,68 @@ namespace QuickMedia { } } - static std::string body_to_formatted_body(const std::string &body) { + void Matrix::replace_mentions(RoomData *room, std::string &text) { + size_t index = 0; + while(index < text.size()) { + index = text.find('@', index); + if(index == std::string::npos) + return; + + bool is_valid_user_id = false; + bool user_id_finished = false; + size_t user_id_start = index; + size_t user_id_end = 0; + index += 1; + for(size_t i = index; i < text.size() && !user_id_finished; ++i) { + char c = text[i]; + switch(c) { + case ':': { + if(is_valid_user_id) { + user_id_finished = true; + user_id_end = i; + index = i; + } + is_valid_user_id = true; + break; + } + case ' ': + case '\n': + case '\r': + case '\t': + case '@': { + user_id_finished = true; + user_id_end = i; + index = i; + break; + } + } + } + + if(user_id_end == 0) + user_id_end = text.size(); + + if(is_valid_user_id) { + std::string user_id = text.substr(user_id_start, user_id_end - user_id_start); + auto user = get_user_by_id(room, user_id, nullptr, false); + if(user) { + std::string user_id_escaped = user_id; + html_escape_sequences(user_id_escaped); + + std::string display_name_escaped = room->get_user_display_name(user); + html_escape_sequences(display_name_escaped); + + std::string mention_text = "" + display_name_escaped + ""; + text.replace(user_id_start, user_id.size(), mention_text); + } + } + } + } + + std::string Matrix::body_to_formatted_body(RoomData *room, const std::string &body) { std::string formatted_body; bool is_inside_code_block = false; bool is_first_line = true; - string_split(body, '\n', [&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](const char *str, size_t size){ if(!is_first_line) formatted_body += "
"; @@ -2747,13 +2855,16 @@ namespace QuickMedia { } else { if(!is_inside_code_block && size > 0 && str[0] == '>') { formatted_body += ""; + replace_mentions(room, line_str); formatted_body_add_line(formatted_body, line_str); formatted_body += ""; } else { - if(is_inside_code_block) + if(is_inside_code_block) { formatted_body += line_str; - else + } else { + replace_mentions(room, line_str); formatted_body_add_line(formatted_body, line_str); + } } is_first_line = false; } @@ -2772,7 +2883,7 @@ namespace QuickMedia { std::string formatted_body; if(!file_info) - formatted_body = body_to_formatted_body(body); + formatted_body = body_to_formatted_body(room, body); rapidjson::Document request_data(rapidjson::kObjectType); if(msgtype.empty()) @@ -2919,8 +3030,8 @@ namespace QuickMedia { return ""; } - static std::string create_formatted_body_for_message_reply(RoomData *room, const Message *message, const std::string &body) { - std::string formatted_body = body_to_formatted_body(body); + 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); html_escape_sequences(related_to_body); // TODO: Add keybind to navigate to the reply message, which would also depend on this formatting. @@ -3001,7 +3112,7 @@ namespace QuickMedia { return PluginResult::ERR; my_events_transaction_ids.insert(transaction_id); - std::string formatted_body = body_to_formatted_body(body); + std::string formatted_body = body_to_formatted_body(room, body); rapidjson::Document new_content_json(rapidjson::kObjectType); new_content_json.AddMember("msgtype", "m.text", new_content_json.GetAllocator()); @@ -3908,34 +4019,25 @@ namespace QuickMedia { delegate->clear_data(); } - std::shared_ptr Matrix::get_user_by_id(RoomData *room, const std::string &user_id) { + std::shared_ptr Matrix::get_user_by_id(RoomData *room, const std::string &user_id, bool *is_new_user, bool create_if_not_found) { auto user = room->get_user_by_id(user_id); - if(user) + if(user) { + if(is_new_user) + *is_new_user = false; return user; + } + + if(!create_if_not_found) + return nullptr; //fprintf(stderr, "Unknown user: %s, creating locally... synapse bug?\n", user_id.c_str()); auto user_info = std::make_shared(room, user_id); room->add_user(user_info); + if(is_new_user) + *is_new_user = true; return user_info; } - void Matrix::update_user_with_latest_state(RoomData *room, const std::string &user_id) { - char url[512]; - snprintf(url, sizeof(url), "%s/_matrix/client/r0/profile/%s", homeserver.c_str(), user_id.c_str()); - - rapidjson::Document json_root; - DownloadResult download_result = download_json(json_root, url, {}, true); - if(download_result != DownloadResult::OK || !json_root.IsObject()) { - fprintf(stderr, "Fetching profile for user %s failed!\n", user_id.c_str()); - auto user = get_user_by_id(room, user_id); - assert(user); - user->resolve_state = UserResolveState::RESOLVED; - return; - } - - parse_user_info(json_root, user_id, room); - } - void Matrix::update_room_users(RoomData *room) { #if 1 std::vector additional_args = { @@ -3964,7 +4066,8 @@ namespace QuickMedia { const rapidjson::Value &display_name_json = GetMember(joined_obj.value, "display_name"); const rapidjson::Value &displayname_json = GetMember(joined_obj.value, "displayname"); // Construct bug... std::string user_id(joined_obj.name.GetString(), joined_obj.name.GetStringLength()); - auto user = get_user_by_id(room, user_id); + bool is_new_user; + auto user = get_user_by_id(room, user_id, &is_new_user); assert(user); std::string display_name; @@ -3980,8 +4083,21 @@ namespace QuickMedia { 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) - room->set_user_avatar_url(user, std::move(avatar_url)); - room->set_user_display_name(user, std::move(display_name)); + room->set_user_avatar_url(user, avatar_url); + room->set_user_display_name(user, display_name); + + MatrixEventUserInfo user_info; + user_info.user_id = user_id; + user_info.display_name = display_name; + user_info.avatar_url = avatar_url; + + if(is_new_user) { + auto event = std::make_unique(std::move(user_info)); + trigger_event(room, std::move(event)); + } else { + auto event = std::make_unique(std::move(user_info)); + trigger_event(room, std::move(event)); + } } #else std::vector additional_args = { @@ -4051,4 +4167,34 @@ namespace QuickMedia { return INITIAL_FILTER; #endif } + + void Matrix::enable_event_queue(RoomData *room) { + std::lock_guard lock(event_queue_mutex); + assert(!current_event_queue_room); + current_event_queue_room = room; + } + + void Matrix::disable_event_queue() { + std::lock_guard lock(event_queue_mutex); + assert(current_event_queue_room); + current_event_queue_room = nullptr; + event_queue.clear(); + } + + std::unique_ptr Matrix::pop_event() { + std::lock_guard lock(event_queue_mutex); + if(!current_event_queue_room || event_queue.empty()) + return nullptr; + + auto event_data = std::move(event_queue.front()); + event_queue.pop_front(); + return event_data; + } + + void Matrix::trigger_event(RoomData *room, std::unique_ptr event) { + std::lock_guard lock(event_queue_mutex); + if(sync_is_cache || !current_event_queue_room || current_event_queue_room != room) + return; + event_queue.push_back(std::move(event)); + } } \ No newline at end of file -- cgit v1.2.3