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/QuickMedia.cpp | 321 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 260 insertions(+), 61 deletions(-) (limited to 'src/QuickMedia.cpp') 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"); -- cgit v1.2.3