aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2021-05-18 22:05:19 +0200
committerdec05eba <dec05eba@protonmail.com>2021-05-18 22:08:41 +0200
commitd123c41cd3ad4f0d55ae134be69e7ffd144dbb74 (patch)
treef46a8e6daea5b757f4b66363c64aea6269dd83a9 /src
parentf6a39afa8bfd869ba661799897ac37e7d1ff7c34 (diff)
Add mention autocomplete
Diffstat (limited to 'src')
-rw-r--r--src/Body.cpp13
-rw-r--r--src/Entry.cpp8
-rw-r--r--src/QuickMedia.cpp321
-rw-r--r--src/SearchBar.cpp6
-rw-r--r--src/Tabs.cpp8
-rw-r--r--src/Text.cpp45
-rw-r--r--src/plugins/Matrix.cpp244
7 files changed, 512 insertions, 133 deletions
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<MatrixChatPage>(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<Tab> &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, &current_room, &new_page, &chat_state, &pending_sent_replies, &currently_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, &current_room, &new_page, &chat_state, &pending_sent_replies, &currently_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, &current_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<FetchMessageResult>([this, &current_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<MatrixEvent> 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<MatrixAddUserEvent*>(matrix_event.get()));
+ break;
+ case MatrixEvent::Type::REMOVE_USER:
+ on_remove_user_event(static_cast<MatrixRemoveUserEvent*>(matrix_event.get()));
+ break;
+ case MatrixEvent::Type::USER_INFO:
+ on_user_info_event(static_cast<MatrixUserInfoEvent*>(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<UserInfo> &user, std::string avatar_url) {
std::lock_guard<std::recursive_mutex> lock(user_mutex);
user->avatar_url = std::move(avatar_url);
- user->resolve_state = UserResolveState::RESOLVED;
}
size_t RoomData::prepend_messages_reverse(const std::vector<std::shared_ptr<Message>> &new_messages) {
@@ -219,6 +217,16 @@ namespace QuickMedia {
return message_it->second;
}
+ std::vector<std::shared_ptr<UserInfo>> RoomData::get_users() {
+ std::lock_guard<std::recursive_mutex> lock(user_mutex);
+ std::vector<std::shared_ptr<UserInfo>> 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<std::shared_ptr<UserInfo>> RoomData::get_users_excluding_me(const std::string &my_user_id) {
std::lock_guard<std::recursive_mutex> lock(user_mutex);
std::vector<std::shared_ptr<UserInfo>> users_excluding_me;
@@ -1697,9 +1705,24 @@ namespace QuickMedia {
//auto user_info = std::make_shared<UserInfo>(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<MatrixAddUserEvent>(std::move(event_user_info));
+ trigger_event(room_data, std::move(event));
+ } else {
+ auto event = std::make_unique<MatrixUserInfoEvent>(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<MatrixAddUserEvent>(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<MatrixAddUserEvent>(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<std::string> new_display_name;
+ std::optional<std::string> 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<MatrixUserInfoEvent>(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 = "<a href=\"https://matrix.to/#/" + user_id_escaped + "\">" + display_name_escaped + "</a>";
+ 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 += "<br/>";
@@ -2747,13 +2855,16 @@ namespace QuickMedia {
} else {
if(!is_inside_code_block && size > 0 && str[0] == '>') {
formatted_body += "<font color=\"#789922\">";
+ replace_mentions(room, line_str);
formatted_body_add_line(formatted_body, line_str);
formatted_body += "</font>";
} 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<UserInfo> Matrix::get_user_by_id(RoomData *room, const std::string &user_id) {
+ std::shared_ptr<UserInfo> 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<UserInfo>(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<CommandArg> 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<MatrixAddUserEvent>(std::move(user_info));
+ trigger_event(room, std::move(event));
+ } else {
+ auto event = std::make_unique<MatrixUserInfoEvent>(std::move(user_info));
+ trigger_event(room, std::move(event));
+ }
}
#else
std::vector<CommandArg> additional_args = {
@@ -4051,4 +4167,34 @@ namespace QuickMedia {
return INITIAL_FILTER;
#endif
}
+
+ void Matrix::enable_event_queue(RoomData *room) {
+ std::lock_guard<std::mutex> lock(event_queue_mutex);
+ assert(!current_event_queue_room);
+ current_event_queue_room = room;
+ }
+
+ void Matrix::disable_event_queue() {
+ std::lock_guard<std::mutex> lock(event_queue_mutex);
+ assert(current_event_queue_room);
+ current_event_queue_room = nullptr;
+ event_queue.clear();
+ }
+
+ std::unique_ptr<MatrixEvent> Matrix::pop_event() {
+ std::lock_guard<std::mutex> 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<MatrixEvent> event) {
+ std::lock_guard<std::mutex> 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