From d123c41cd3ad4f0d55ae134be69e7ffd144dbb74 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Tue, 18 May 2021 22:05:19 +0200 Subject: Add mention autocomplete --- 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 +++++++++++++++++++++++++++++-------- 7 files changed, 512 insertions(+), 133 deletions(-) (limited to 'src') 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