From e68ff632e2f54c705ae1d69033e58a8f2d1ca00c Mon Sep 17 00:00:00 2001 From: dec05eba Date: Mon, 16 Nov 2020 13:32:49 +0100 Subject: Matrix: show provisional messages as the message is being sent and received --- src/QuickMedia.cpp | 282 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 201 insertions(+), 81 deletions(-) (limited to 'src/QuickMedia.cpp') diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index c01ce59..5c97c2d 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -3199,41 +3199,50 @@ namespace QuickMedia { bool redraw = true; // TODO: Optimize with hash map? - auto find_body_item_by_event_id = [](std::shared_ptr *body_items, size_t num_body_items, const std::string &event_id) -> std::shared_ptr { + auto find_body_item_by_event_id = [](std::shared_ptr *body_items, size_t num_body_items, const std::string &event_id, size_t *index_result = nullptr) -> std::shared_ptr { for(size_t i = 0; i < num_body_items; ++i) { auto &body_item = body_items[i]; - if(static_cast(body_item->userdata)->event_id == event_id) + if(static_cast(body_item->userdata)->event_id == event_id) { + if(index_result) + *index_result = i; return body_item; + } } return nullptr; }; // TODO: What if these never end up referencing events? clean up automatically after a while? - std::unordered_map unreferenced_event_by_room; + Messages unreferenced_events; auto set_body_as_deleted = [¤t_room](Message *message, BodyItem *body_item) { body_item->embedded_item = nullptr; body_item->embedded_item_status = FetchStatus::NONE; + message->type = MessageType::REDACTION; + message->related_event_id.clear(); + message->related_event_type = RelatedEventType::NONE; body_item->thumbnail_url = current_room->get_user_avatar_url(message->user); body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; + body_item->set_description("Message deleted"); body_item->set_description_color(sf::Color::White); body_item->thumbnail_size = sf::Vector2i(32, 32); }; // TODO: Optimize with hash map? - auto resolve_unreferenced_events_with_body_items = [&set_body_as_deleted, &unreferenced_event_by_room, ¤t_room, &find_body_item_by_event_id](std::shared_ptr *body_items, size_t num_body_items) { - auto &unreferenced_events = unreferenced_event_by_room[current_room]; + auto resolve_unreferenced_events_with_body_items = [&set_body_as_deleted, &unreferenced_events, &find_body_item_by_event_id](std::shared_ptr *body_items, size_t num_body_items) { for(auto it = unreferenced_events.begin(); it != unreferenced_events.end(); ) { auto &message = *it; // TODO: Make redacted/edited events as (redacted)/(edited) in the body if(message->related_event_type == RelatedEventType::REDACTION || message->related_event_type == RelatedEventType::EDIT) { auto body_item = find_body_item_by_event_id(body_items, num_body_items, message->related_event_id); if(body_item) { - body_item->set_description(message_get_body_remove_formatting(message.get())); // TODO: Append the new message to the body item so the body item should have a list of edit events //body_item->userdata = message.get(); - if(message->related_event_type == RelatedEventType::REDACTION) + if(message->related_event_type == RelatedEventType::REDACTION) { set_body_as_deleted(message.get(), body_item.get()); + } else { + body_item->set_description(message_get_body_remove_formatting(message.get())); + body_item->set_description_color(sf::Color::White); + } it = unreferenced_events.erase(it); } else { ++it; @@ -3245,22 +3254,24 @@ namespace QuickMedia { }; // TODO: Optimize with hash map? - auto modify_related_messages_in_current_room = [&set_body_as_deleted, &unreferenced_event_by_room, ¤t_room, &find_body_item_by_event_id, &tabs](Messages &messages) { + auto modify_related_messages_in_current_room = [&set_body_as_deleted, &unreferenced_events, &find_body_item_by_event_id, &tabs](Messages &messages) { if(messages.empty()) return; - auto &unreferenced_events = unreferenced_event_by_room[current_room]; auto &body_items = tabs[MESSAGES_TAB_INDEX].body->items; for(auto &message : messages) { // TODO: Make redacted/edited events as (redacted)/(edited) in the body if(message->related_event_type == RelatedEventType::REDACTION || message->related_event_type == RelatedEventType::EDIT) { auto body_item = find_body_item_by_event_id(body_items.data(), body_items.size(), message->related_event_id); if(body_item) { - body_item->set_description(message_get_body_remove_formatting(message.get())); // TODO: Append the new message to the body item so the body item should have a list of edit events //body_item->userdata = message.get(); - if(message->related_event_type == RelatedEventType::REDACTION) + if(message->related_event_type == RelatedEventType::REDACTION) { set_body_as_deleted(message.get(), body_item.get()); + } else { + body_item->set_description(message_get_body_remove_formatting(message.get())); + body_item->set_description_color(sf::Color::White); + } } else { unreferenced_events.push_back(message); } @@ -3350,18 +3361,64 @@ namespace QuickMedia { chat_input.draw_background = false; chat_input.set_editable(false); - MessageQueue> post_task_queue; - auto post_thread_handler = [&post_task_queue]() { + struct ProvisionalMessage { + std::shared_ptr body_item; + std::shared_ptr message; + std::string event_id; + }; + + std::vector unresolved_provisional_messages; // |event_id| is always empty in this. Use |message->event_id| instead + std::optional provisional_message; + MessageQueue provisional_message_queue; + MessageQueue> post_task_queue; + auto post_thread_handler = [&provisional_message_queue, &post_task_queue]() { while(true) { - std::optional> post_task_opt = post_task_queue.pop_wait(); + std::optional> post_task_opt = post_task_queue.pop_wait(); if(!post_task_opt) break; - post_task_opt.value()(); + provisional_message_queue.push(post_task_opt.value()()); } }; std::thread post_thread(post_thread_handler); - chat_input.on_submit_callback = [this, &chat_input, &selected_tab, ¤t_room, &new_page, &chat_state, ¤tly_operating_on_item, &post_task_queue](std::string text) mutable { + auto resolve_provisional_messages = [&unresolved_provisional_messages](Messages &messages) { + if(messages.empty() || unresolved_provisional_messages.empty()) + return; + + for(auto it = unresolved_provisional_messages.begin(); it != unresolved_provisional_messages.end();) { + auto find_it = std::find_if(messages.cbegin(), messages.cend(), [&it](const std::shared_ptr &message) { + return message->event_id == it->message->event_id; + }); + if(find_it != messages.end()) { + it->body_item->set_description_color(sf::Color::White); + it->body_item->userdata = find_it->get(); // This is ok because the message is added to |all_messages| + it = unresolved_provisional_messages.erase(it); + messages.erase(find_it); + } else { + ++it; + } + } + }; + + auto upload_file = [this, &tabs, ¤t_room](const std::string &filepath) { + TaskResult post_file_result = run_task_with_loading_screen([this, ¤t_room, filepath]() { + std::string event_id_response; + std::string err_msg; + if(matrix->post_file(current_room, filepath, event_id_response, err_msg) == PluginResult::OK) { + return true; + } else { + show_notification("QuickMedia", "Failed to upload media to room, error: " + err_msg, Urgency::CRITICAL); + return false; + } + }); + + if(post_file_result == TaskResult::TRUE) { + if(tabs[MESSAGES_TAB_INDEX].body->is_last_item_fully_visible()) + tabs[MESSAGES_TAB_INDEX].body->select_last_item(); + } + }; + + chat_input.on_submit_callback = [this, &tabs, &me, &chat_input, &selected_tab, ¤t_room, &new_page, &chat_state, ¤tly_operating_on_item, &post_task_queue, &unreferenced_events, &find_body_item_by_event_id](std::string text) mutable { if(!current_room) return false; @@ -3400,34 +3457,82 @@ namespace QuickMedia { } } + auto message = std::make_shared(); + message->user = matrix->get_me(current_room); + message->body = text; + message->type = MessageType::TEXT; + message->timestamp = time(NULL) * 1000; + + const sf::Color provisional_message_color(171, 175, 180); + + int num_items = tabs[MESSAGES_TAB_INDEX].body->items.size(); + bool scroll_to_end = num_items == 0; + if(tabs[MESSAGES_TAB_INDEX].body->is_selected_item_last_visible_item() && selected_tab == MESSAGES_TAB_INDEX) + scroll_to_end = true; + if(chat_state == ChatState::TYPING_MESSAGE) { - post_task_queue.push([this, ¤t_room, text, msgtype]() { - if(matrix->post_message(current_room, text, std::nullopt, std::nullopt, msgtype) != PluginResult::OK) + auto body_item = message_to_body_item(current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id); + body_item->set_description_color(provisional_message_color); + tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps({body_item}); + post_task_queue.push([this, ¤t_room, text, msgtype, body_item, message]() { + ProvisionalMessage provisional_message; + provisional_message.body_item = body_item; + provisional_message.message = message; + if(matrix->post_message(current_room, text, provisional_message.event_id, std::nullopt, std::nullopt, msgtype) != PluginResult::OK) fprintf(stderr, "Failed to post matrix message\n"); + return provisional_message; }); chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; + if(scroll_to_end) + tabs[MESSAGES_TAB_INDEX].body->select_last_item(); return true; } else if(chat_state == ChatState::REPLYING) { void *related_to_message = currently_operating_on_item->userdata; - post_task_queue.push([this, ¤t_room, text, related_to_message]() { - if(matrix->post_reply(current_room, text, related_to_message) != PluginResult::OK) + message->related_event_type = RelatedEventType::REPLY; + message->related_event_id = static_cast(related_to_message)->event_id; + auto body_item = message_to_body_item(current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id); + body_item->set_description_color(provisional_message_color); + tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps({body_item}); + post_task_queue.push([this, ¤t_room, text, related_to_message, body_item, message]() { + ProvisionalMessage provisional_message; + provisional_message.body_item = body_item; + provisional_message.message = message; + if(matrix->post_reply(current_room, text, related_to_message, provisional_message.event_id) != PluginResult::OK) fprintf(stderr, "Failed to post matrix reply\n"); + return provisional_message; }); chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; currently_operating_on_item = nullptr; + if(scroll_to_end) + tabs[MESSAGES_TAB_INDEX].body->select_last_item(); return true; } else if(chat_state == ChatState::EDITING) { void *related_to_message = currently_operating_on_item->userdata; - post_task_queue.push([this, ¤t_room, text, related_to_message]() { - if(matrix->post_edit(current_room, text, related_to_message) != PluginResult::OK) - fprintf(stderr, "Failed to post matrix edit\n"); - }); - chat_input.set_editable(false); - chat_state = ChatState::NAVIGATING; - currently_operating_on_item = nullptr; - return true; + message->related_event_type = RelatedEventType::EDIT; + message->related_event_id = static_cast(related_to_message)->event_id; + auto body_item = find_body_item_by_event_id(tabs[MESSAGES_TAB_INDEX].body->items.data(), tabs[MESSAGES_TAB_INDEX].body->items.size(), message->related_event_id); + if(body_item) { + body_item->set_description(text); + body_item->set_description_color(provisional_message_color); + unreferenced_events.push_back(message); + post_task_queue.push([this, ¤t_room, text, related_to_message, message]() { + ProvisionalMessage provisional_message; + provisional_message.message = message; + if(matrix->post_edit(current_room, text, related_to_message, provisional_message.event_id) != PluginResult::OK) + fprintf(stderr, "Failed to post matrix edit\n"); + return provisional_message; + }); + + chat_input.set_editable(false); + chat_state = ChatState::NAVIGATING; + currently_operating_on_item = nullptr; + return true; + } else { + show_notification("QuickMedia", "Failed to edit message. Message refers to a non-existing message", Urgency::CRITICAL); + return false; + } } } return false; @@ -3567,10 +3672,10 @@ namespace QuickMedia { bool fetched_enough_messages = false; bool initial_prev_messages_fetch = true; - auto fetch_more_previous_messages_if_needed = [this, &all_messages, ¤t_room, &fetched_enough_messages, &previous_messages_future, &initial_prev_messages_fetch]() { + auto fetch_more_previous_messages_if_needed = [this, &tabs, ¤t_room, &fetched_enough_messages, &previous_messages_future, &initial_prev_messages_fetch]() { if(!fetched_enough_messages && !previous_messages_future.ready()) { bool fetch_latest_messages = !matrix->is_initial_sync_finished() && initial_prev_messages_fetch; - if(all_messages.size() < 10) { + if(!tabs[MESSAGES_TAB_INDEX].body->is_body_full_with_items()) { previous_messages_future = [this, ¤t_room, fetch_latest_messages]() { Messages messages; if(matrix->get_previous_room_messages(current_room, messages, fetch_latest_messages) != PluginResult::OK) @@ -3794,7 +3899,7 @@ namespace QuickMedia { } }; - auto cleanup_tasks = [&set_read_marker_future, &fetch_message_future, &typing_state_queue, &typing_state_thread, &post_task_queue, &post_thread, &tabs]() { + auto cleanup_tasks = [&set_read_marker_future, &fetch_message_future, &typing_state_queue, &typing_state_thread, &post_task_queue, &provisional_message_queue, &post_thread, &tabs]() { set_read_marker_future.cancel(); fetch_message_future.cancel(); typing_state_queue.close(); @@ -3807,6 +3912,7 @@ namespace QuickMedia { program_kill_in_thread(post_thread.get_id()); post_thread.join(); } + provisional_message_queue.clear(); //unreferenced_event_by_room.clear(); @@ -3940,20 +4046,7 @@ namespace QuickMedia { if(event.key.control && event.key.code == sf::Keyboard::V) { frame_skip_text_entry = true; // TODO: Upload multiple files. - std::string selected_file = sf::Clipboard::getString(); - TaskResult post_file_result = run_task_with_loading_screen([this, ¤t_room, selected_file]() { - std::string err_msg; - if(matrix->post_file(current_room, selected_file, err_msg) == PluginResult::OK) { - return true; - } else { - show_notification("QuickMedia", "Failed to upload media to room, error: " + err_msg, Urgency::CRITICAL); - return false; - } - }); - if(post_file_result == TaskResult::TRUE) { - if(tabs[MESSAGES_TAB_INDEX].body->is_last_item_fully_visible()) - tabs[MESSAGES_TAB_INDEX].body->select_last_item(); - } + upload_file(sf::Clipboard::getString()); } if(event.key.code == sf::Keyboard::R) { @@ -3961,10 +4054,15 @@ namespace QuickMedia { std::shared_ptr selected = tabs[selected_tab].body->get_selected_shared(); if(selected) { if(!is_state_message_type(static_cast(selected->userdata))) { - chat_state = ChatState::REPLYING; - currently_operating_on_item = selected; - chat_input.set_editable(true); - replying_to_text.setString("Replying to:"); + if(static_cast(selected->userdata)->event_id.empty()) { + // TODO: Show inline notification + show_notification("QuickMedia", "You can't reply to a message that hasn't been sent yet"); + } else { + chat_state = ChatState::REPLYING; + currently_operating_on_item = selected; + chat_input.set_editable(true); + replying_to_text.setString("Replying to:"); + } } } else { // TODO: Show inline notification @@ -3977,9 +4075,12 @@ namespace QuickMedia { std::shared_ptr selected = tabs[selected_tab].body->get_selected_shared(); if(selected) { if(!is_state_message_type(static_cast(selected->userdata))) { - if(!selected->url.empty()) { // cant edit messages that are image/video posts + if(static_cast(selected->userdata)->event_id.empty()) { + // TODO: Show inline notification + show_notification("QuickMedia", "You can't edit a message that hasn't been sent yet"); + } else if(!selected->url.empty()) { // cant edit messages that are image/video posts // TODO: Show inline notification - show_notification("QuickMedia", "You can only edit messages with no file attached to it"); + show_notification("QuickMedia", "You can't edit messages with files attached to them"); } else if(!matrix->was_message_posted_by_me(selected->userdata)) { // TODO: Show inline notification show_notification("QuickMedia", "You can't edit a message that was posted by somebody else"); @@ -4003,14 +4104,22 @@ namespace QuickMedia { BodyItem *selected = tabs[selected_tab].body->get_selected(); if(selected) { if(!is_state_message_type(static_cast(selected->userdata))) { - void *selected_message = selected->userdata; - post_task_queue.push([this, ¤t_room, selected_message]() { - std::string err_msg; - if(matrix->delete_message(current_room, selected_message, err_msg) != PluginResult::OK) { - // TODO: Show inline notification - fprintf(stderr, "Failed to delete message, reason: %s\n", err_msg.c_str()); - } - }); + if(static_cast(selected->userdata)->event_id.empty()) { + // TODO: Show inline notification + show_notification("QuickMedia", "You can't delete a message that hasn't been sent yet"); + } else { + set_body_as_deleted(static_cast(selected->userdata), selected); + void *selected_message = selected->userdata; + post_task_queue.push([this, ¤t_room, selected_message]() { + ProvisionalMessage provisional_message; + std::string err_msg; + if(matrix->delete_message(current_room, selected_message, err_msg) != PluginResult::OK) { + // TODO: Show inline notification + fprintf(stderr, "Failed to delete message, reason: %s\n", err_msg.c_str()); + } + return provisional_message; + }); + } } } else { // TODO: Show inline notification @@ -4106,19 +4215,7 @@ namespace QuickMedia { fprintf(stderr, "No files selected!\n"); } else { // TODO: Upload multiple files. - TaskResult post_file_result = run_task_with_loading_screen([this, ¤t_room]() { - std::string err_msg; - if(matrix->post_file(current_room, selected_files[0], err_msg) == PluginResult::OK) { - return true; - } else { - show_notification("QuickMedia", "Failed to upload media to room, error: " + err_msg, Urgency::CRITICAL); - return false; - } - }); - if(post_file_result == TaskResult::TRUE) { - if(tabs[MESSAGES_TAB_INDEX].body->is_last_item_fully_visible()) - tabs[MESSAGES_TAB_INDEX].body->select_last_item(); - } + upload_file(selected_files[0]); } redraw = true; } @@ -4128,7 +4225,7 @@ namespace QuickMedia { previous_messages_future.cancel(); cleanup_tasks(); tabs.clear(); - unreferenced_event_by_room.clear(); + unreferenced_events.clear(); all_messages.clear(); new_page = PageType::CHAT; matrix->stop_sync(); @@ -4223,10 +4320,25 @@ namespace QuickMedia { logo_sprite.setPosition(logo_padding_x, std::floor(window_size.y - chat_input_shade.getSize().y * 0.5f - logo_size.y * 0.5f)); } + while((provisional_message = provisional_message_queue.pop_if_available()) != std::nullopt) { + if(!provisional_message->body_item || !provisional_message->message) + continue; + + if(!provisional_message->event_id.empty()) { + provisional_message->message->event_id = std::move(provisional_message->event_id); + unresolved_provisional_messages.push_back(provisional_message.value()); + } else if(provisional_message->body_item) { + provisional_message->body_item->set_description("Failed to send: " + provisional_message->body_item->get_description()); + provisional_message->body_item->set_description_color(sf::Color::Red); + } + } + sync_data.messages.clear(); + sync_data.pinned_events = std::nullopt; matrix->get_room_sync_data(current_room, sync_data); if(!sync_data.messages.empty()) { all_messages.insert(all_messages.end(), sync_data.messages.begin(), sync_data.messages.end()); + resolve_provisional_messages(sync_data.messages); filter_existing_messages(sync_data.messages); } add_new_messages_to_current_room(sync_data.messages); @@ -4242,7 +4354,7 @@ namespace QuickMedia { if(previous_messages_future.ready()) { Messages new_messages = previous_messages_future.get(); all_messages.insert(all_messages.end(), new_messages.begin(), new_messages.end()); - if(new_messages.empty() || all_messages.size() >= 10) + if(new_messages.empty()) fetched_enough_messages = true; filter_existing_messages(new_messages); fprintf(stderr, "Finished fetching older messages, num new messages: %zu\n", new_messages.size()); @@ -4254,7 +4366,7 @@ namespace QuickMedia { BodyItem *selected_item = tabs[MESSAGES_TAB_INDEX].body->get_selected(); BodyItems new_body_items = messages_to_body_items(current_room, new_messages, current_room->get_user_display_name(me), me->user_id); size_t num_new_body_items = new_body_items.size(); - tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps(std::move(new_body_items)); + tabs[MESSAGES_TAB_INDEX].body->prepend_items(std::move(new_body_items)); // TODO: Insert by timestamp? then make sure num_new_body_items matches the first items blabla if(selected_item) { int selected_item_index = tabs[MESSAGES_TAB_INDEX].body->get_index_by_body_item(selected_item); if(selected_item_index != -1) @@ -4430,7 +4542,7 @@ namespace QuickMedia { if(is_window_focused && chat_state != ChatState::URL_SELECTION && current_room && last_visible_item && !setting_read_marker && read_marker_timer.getElapsedTime().asMilliseconds() >= read_marker_timeout_ms) { Message *message = (Message*)last_visible_item->userdata; // TODO: What if two messages have the same timestamp? - if(message->timestamp > current_room->last_read_message_timestamp) { + if(message && !message->event_id.empty() && message->timestamp > current_room->last_read_message_timestamp) { //read_marker_timeout_ms = read_marker_timeout_ms_default; current_room->last_read_message_timestamp = message->timestamp; // TODO: What if the message is no longer valid? @@ -4464,7 +4576,6 @@ namespace QuickMedia { if(matrix_chat_page->should_clear_data) { matrix_chat_page->should_clear_data = false; - cleanup_tasks(); std::string err_msg; while(!matrix->is_initial_sync_finished()) { @@ -4478,13 +4589,22 @@ namespace QuickMedia { current_room = matrix->get_room_by_id(current_room->id); if(current_room) { + all_messages.clear(); + tabs[MESSAGES_TAB_INDEX].body->clear_items(); + + matrix->get_all_synced_room_messages(current_room, all_messages); + for(auto &message : all_messages) { + fetched_messages_set.insert(message->event_id); + } + auto me = matrix->get_me(current_room); + resolve_provisional_messages(all_messages); + add_new_messages_to_current_room(all_messages); + modify_related_messages_in_current_room(all_messages); + std::vector pinned_events; matrix->get_all_pinned_events(current_room, pinned_events); process_pinned_events(std::move(pinned_events)); - typing_state_queue.restart(); - typing_state_thread = std::thread(typing_state_handler); - post_task_queue.restart(); - post_thread = std::thread(post_thread_handler); + fetch_more_previous_messages_if_needed(); } else { go_to_previous_page = true; -- cgit v1.2.3