From 16ca6e63f4fd1b407c826a5574dc20b3f9e71675 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Tue, 3 Nov 2020 01:07:57 +0100 Subject: Matrix: sync with filter, lazy member fetch (reducing sync time from 35 sec with huge server to 3 seconds) and cached fetch to 150ms). Properly show notifications for older messages. Reduce memory usage from 120mb to 13mb --- TODO | 4 +- matrix-requests-todo/m.room.tombstone.json | 15 + plugins/Matrix.hpp | 40 ++- src/AsyncImageLoader.cpp | 10 +- src/QuickMedia.cpp | 69 +++- src/plugins/Matrix.cpp | 518 ++++++++++++++++++++--------- 6 files changed, 470 insertions(+), 186 deletions(-) create mode 100644 matrix-requests-todo/m.room.tombstone.json diff --git a/TODO b/TODO index 62a861a..7022594 100644 --- a/TODO +++ b/TODO @@ -136,4 +136,6 @@ Get user displayname, avatar, room name, etc updates from /sync and update them Test if glScissor doesn't break loading of embedded body item. Handle matrix token being invalidated while running. Update upload limit if its updated on the server (can it be updated while the server is running?). -Apply search filter when updating rooms (including when switching from cache to server response sync data). \ No newline at end of file +Apply search filter when updating rooms (including when switching from cache to server response sync data). +Editing a reply removes reply formatting (both in body and formatted_body). Element also does this when you edit a reply twice. This breaks element mobile that is unable to display replied-to messages without correct formatting (doesn't fetch the replied-to message). +Implement m.room.tombstone \ No newline at end of file diff --git a/matrix-requests-todo/m.room.tombstone.json b/matrix-requests-todo/m.room.tombstone.json new file mode 100644 index 0000000..f67395d --- /dev/null +++ b/matrix-requests-todo/m.room.tombstone.json @@ -0,0 +1,15 @@ +{ + "content": { + "body": "This room has been replaced", + "replacement_room": "!newroom:example.org" + }, + "event_id": "$143273582443PhrSn:example.org", + "origin_server_ts": 1432735824653, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "sender": "@example:example.org", + "state_key": "", + "type": "m.room.tombstone", + "unsigned": { + "age": 1234 + } +} \ No newline at end of file diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index 903215b..cde502d 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -99,7 +99,6 @@ namespace QuickMedia { // These 4 variables are set by QuickMedia, not the matrix plugin bool last_message_read = true; time_t last_read_message_timestamp = 0; - bool has_unread_mention = false; void *userdata = nullptr; // Pointer to BodyItem. Note: this has to be valid as long as the room is valid // These are messages fetched with |Matrix::get_message_by_id|. Needed to show replies, when replying to old message not part of /sync. @@ -110,10 +109,12 @@ namespace QuickMedia { size_t messages_read_index = 0; bool pinned_events_updated = false; - size_t index; + std::atomic_int unread_notification_count = 0; + + size_t index = 0; private: - std::mutex user_mutex; - std::mutex room_mutex; + std::recursive_mutex user_mutex; + std::recursive_mutex room_mutex; std::string name; std::string avatar_url; @@ -188,6 +189,8 @@ namespace QuickMedia { virtual void add_invite(const std::string &room_id, const Invite &invite) = 0; virtual void remove_invite(const std::string &room_id) = 0; + virtual void add_unread_notification(RoomData *room, std::string event_id, std::string sender, std::string body) = 0; + virtual void update(MatrixPageType page_type) { (void)page_type; } virtual void clear_data() = 0; @@ -212,6 +215,8 @@ namespace QuickMedia { void add_invite(const std::string &room_id, const Invite &invite) override; void remove_invite(const std::string &room_id) override; + void add_unread_notification(RoomData *room, std::string event_id, std::string sender, std::string body) override; + void update(MatrixPageType page_type) override; void clear_data() override; @@ -222,6 +227,7 @@ namespace QuickMedia { MatrixRoomTagsPage *room_tags_page; MatrixInvitesPage *invites_page; private: + void update_room_description(RoomData *room, bool is_initial_sync); void update_pending_room_messages(MatrixPageType page_type); private: struct RoomMessagesData { @@ -230,10 +236,18 @@ namespace QuickMedia { bool sync_is_cache; }; + struct Notification { + std::string event_id; + std::string sender; + std::string body; + }; + std::map> room_body_item_by_room; std::mutex room_body_items_mutex; std::map pending_room_messages; std::mutex pending_room_messages_mutex; + + std::unordered_map> unread_notifications; }; class MatrixRoomsPage : public Page { @@ -253,6 +267,7 @@ namespace QuickMedia { void set_current_chat_page(MatrixChatPage *chat_page); void clear_data(); + void sort_rooms(); MatrixQuickMedia *matrix_delegate = nullptr; private: @@ -264,6 +279,7 @@ namespace QuickMedia { MatrixRoomTagsPage *room_tags_page = nullptr; MatrixChatPage *current_chat_page = nullptr; bool clear_data_on_update = false; + bool sort_on_update = false; }; class MatrixRoomTagsPage : public Page { @@ -282,6 +298,7 @@ namespace QuickMedia { void set_current_rooms_page(MatrixRoomsPage *rooms_page); void clear_data(); + void sort_rooms(); MatrixQuickMedia *matrix_delegate = nullptr; private: @@ -425,18 +442,20 @@ namespace QuickMedia { bool use_tor = false; private: - PluginResult parse_sync_response(const rapidjson::Document &root, MatrixDelegate *delegate); + PluginResult parse_sync_response(const rapidjson::Document &root); + PluginResult parse_notifications(const rapidjson::Value ¬ifications_json); PluginResult parse_sync_account_data(const rapidjson::Value &account_data_json, std::optional> &dm_rooms); - PluginResult parse_sync_room_data(const rapidjson::Value &rooms_json, MatrixDelegate *delegate); + PluginResult parse_sync_room_data(const rapidjson::Value &rooms_json); PluginResult get_previous_room_messages(RoomData *room_data); void events_add_user_info(const rapidjson::Value &events_json, RoomData *room_data); void events_add_user_read_markers(const rapidjson::Value &events_json, RoomData *room_data); - void events_add_messages(const rapidjson::Value &events_json, RoomData *room_data, MessageDirection message_dir, MatrixDelegate *delegate, bool has_unread_notifications); + void events_set_user_read_marker(const rapidjson::Value &events_json, RoomData *room_data, std::shared_ptr &me); + void events_add_messages(const rapidjson::Value &events_json, RoomData *room_data, MessageDirection message_dir, bool has_unread_notifications); void events_set_room_name(const rapidjson::Value &events_json, RoomData *room_data); void events_add_pinned_events(const rapidjson::Value &events_json, RoomData *room_data); - void events_add_room_to_tags(const rapidjson::Value &events_json, RoomData *room_data, MatrixDelegate *delegate); - void add_invites(const rapidjson::Value &invite_json, MatrixDelegate *delegate); - void remove_rooms(const rapidjson::Value &leave_json, MatrixDelegate *delegate); + void events_add_room_to_tags(const rapidjson::Value &events_json, RoomData *room_data); + void add_invites(const rapidjson::Value &invite_json); + void remove_rooms(const rapidjson::Value &leave_json); std::shared_ptr parse_message_event(const rapidjson::Value &event_item_json, RoomData *room_data); PluginResult upload_file(RoomData *room, const std::string &filepath, UploadInfo &file_info, UploadInfo &thumbnail_info, std::string &err_msg); void add_room(std::unique_ptr room); @@ -463,6 +482,7 @@ namespace QuickMedia { std::mutex invite_mutex; std::thread sync_thread; + std::thread notification_thread; bool sync_running = false; bool sync_failed = false; bool sync_is_cache = false; diff --git a/src/AsyncImageLoader.cpp b/src/AsyncImageLoader.cpp index d3aa287..136bd5b 100644 --- a/src/AsyncImageLoader.cpp +++ b/src/AsyncImageLoader.cpp @@ -1,5 +1,6 @@ #include "../include/AsyncImageLoader.hpp" #include "../include/DownloadUtils.hpp" +#include "../include/Program.hpp" #include "../include/ImageUtils.hpp" #include "../include/Scale.hpp" #include "../include/SfmlFixes.hpp" @@ -130,13 +131,16 @@ namespace QuickMedia { AsyncImageLoader::~AsyncImageLoader() { image_load_queue.close(); - if(load_image_thread.joinable()) + if(load_image_thread.joinable()) { + program_kill_in_thread(load_image_thread.get_id()); load_image_thread.join(); + } - // TODO: Find a way to kill the threads instead. We need to do this right now because creating a new thread before the last one has died causes a crash for(size_t i = 0; i < NUM_IMAGE_LOAD_THREADS; ++i) { - if(download_image_thread[i].joinable()) + if(download_image_thread[i].joinable()) { + program_kill_in_thread(download_image_thread[i].get_id()); download_image_thread[i].join(); + } } } diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index ad9cc7f..318aba8 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -1328,6 +1328,12 @@ namespace QuickMedia { load_sprite.setPosition(body_pos.x + body_size.x * 0.5f, body_pos.y + body_size.y * 0.5f); load_sprite.setRotation(load_sprite_timer.getElapsedTime().asSeconds() * 400.0); window.draw(load_sprite); + std::string err_msg; + if(matrix->did_initial_sync_fail(err_msg)) { + show_notification("QuickMedia", "Initial matrix sync failed, error: " + err_msg, Urgency::CRITICAL); + window.close(); + goto page_end; + } } window.display(); @@ -3050,7 +3056,7 @@ namespace QuickMedia { body_item->userdata = (void*)message; // Note: message has to be valid as long as body_item is used! if(message->related_event_type == RelatedEventType::REDACTION || message->related_event_type == RelatedEventType::EDIT) body_item->visible = false; - if(message->mentions_me || (me && (message_contains_user_mention(message->body, me->display_name) || message_contains_user_mention(message->body, me->user_id)))) + if(me && (message_contains_user_mention(message->body, me->display_name) || message_contains_user_mention(message->body, me->user_id))) body_item->set_description_color(sf::Color(255, 100, 100)); return body_item; } @@ -3234,6 +3240,7 @@ namespace QuickMedia { Messages all_messages; matrix->get_all_synced_room_messages(current_room, all_messages); + size_t num_messages_in_room = all_messages.size(); tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps(messages_to_body_items(all_messages, matrix->get_me(current_room).get())); modify_related_messages_in_current_room(all_messages); tabs[MESSAGES_TAB_INDEX].body->select_last_item(); @@ -3337,7 +3344,6 @@ namespace QuickMedia { }; AsyncTask previous_messages_future; - RoomData *previous_messages_future_room = nullptr; //const int num_fetch_message_threads = 4; AsyncTask> fetch_message_future; @@ -3421,6 +3427,16 @@ namespace QuickMedia { sf::Vertex gradient_points[4]; double gradient_inc = 0; + bool initial_prev_messages_fetch = true; + if(num_messages_in_room < 10) { + previous_messages_future = [this, ¤t_room]() { + Messages messages; + if(matrix->get_previous_room_messages(current_room, messages) != PluginResult::OK) + fprintf(stderr, "Failed to get previous matrix messages in room: %s\n", current_room->id.c_str()); + return messages; + }; + } + sf::RectangleShape more_messages_below_rect; more_messages_below_rect.setFillColor(sf::Color(128, 50, 50)); @@ -3562,13 +3578,15 @@ namespace QuickMedia { previous_messages_future.cancel(); fetch_message_future.cancel(); typing_state_queue.close(); - program_kill_in_thread(typing_state_thread.get_id()); - if(typing_state_thread.joinable()) + if(typing_state_thread.joinable()) { + program_kill_in_thread(typing_state_thread.get_id()); typing_state_thread.join(); + } post_task_queue.close(); - program_kill_in_thread(post_thread.get_id()); - if(post_thread.joinable()) + if(post_thread.joinable()) { + program_kill_in_thread(post_thread.get_id()); post_thread.join(); + } unreferenced_event_by_room.clear(); @@ -3616,11 +3634,10 @@ namespace QuickMedia { } if(hit_top && !previous_messages_future.valid() && selected_tab == MESSAGES_TAB_INDEX && current_room) { gradient_inc = 0; - previous_messages_future_room = current_room; - previous_messages_future = [this, &previous_messages_future_room]() { + previous_messages_future = [this, ¤t_room]() { Messages messages; - if(matrix->get_previous_room_messages(previous_messages_future_room, messages) != PluginResult::OK) - fprintf(stderr, "Failed to get previous matrix messages in room: %s\n", previous_messages_future_room->id.c_str()); + if(matrix->get_previous_room_messages(current_room, messages) != PluginResult::OK) + fprintf(stderr, "Failed to get previous matrix messages in room: %s\n", current_room->id.c_str()); return messages; }; } @@ -3977,7 +3994,7 @@ namespace QuickMedia { fprintf(stderr, "Finished fetching older messages, num new messages: %zu\n", new_messages.size()); // Ignore finished fetch of messages if it happened in another room. When we navigate back to the room we will get the messages again size_t num_new_messages = new_messages.size(); - if(num_new_messages > 0 && previous_messages_future_room == current_room) { + if(num_new_messages > 0) { BodyItem *selected_item = tabs[MESSAGES_TAB_INDEX].body->get_selected(); BodyItems new_body_items = messages_to_body_items(new_messages, matrix->get_me(current_room).get()); size_t num_new_body_items = new_body_items.size(); @@ -3990,6 +4007,13 @@ namespace QuickMedia { modify_related_messages_in_current_room(new_messages); resolve_unreferenced_events_with_body_items(tabs[MESSAGES_TAB_INDEX].body->items.data(), num_new_body_items); } + if(initial_prev_messages_fetch) { + initial_prev_messages_fetch = false; + // XXX: Hack to scroll up while keeping the selected item (usually the last one) visible + int selected_item = tabs[MESSAGES_TAB_INDEX].body->get_selected_item(); + tabs[MESSAGES_TAB_INDEX].body->select_first_item(); + tabs[MESSAGES_TAB_INDEX].body->set_selected_item(selected_item, false); + } } if(fetch_message_future.ready()) { @@ -4118,14 +4142,19 @@ namespace QuickMedia { std::string room_desc = current_room_body_item->get_description(); if(strncmp(room_desc.c_str(), "Unread: ", 8) == 0) room_desc = room_desc.substr(8); - if(room_desc.size() >= 25 && strncmp(room_desc.c_str() + room_desc.size() - 25, "\n** You were mentioned **", 25) == 0) - room_desc = room_desc.substr(0, room_desc.size() - 25); + size_t last_line_start = room_desc.rfind('\n'); + if(last_line_start != std::string::npos && last_line_start != room_desc.size()) { + ++last_line_start; + size_t last_line_size = room_desc.size() - last_line_start; + if(last_line_size >= 23 && memcmp(&room_desc[last_line_start], "** ", 3) == 0 && memcmp(&room_desc[room_desc.size() - 20], "unread mention(s) **", 20) == 0) + room_desc.erase(room_desc.begin() + last_line_start - 1, room_desc.end()); + } current_room_body_item->set_description(std::move(room_desc)); // TODO: Show a line like nheko instead for unread messages, or something else current_room_body_item->set_title_color(sf::Color::White); current_room->last_message_read = true; // TODO: Maybe set this instead when the mention is visible on the screen? - current_room->has_unread_mention = false; + current_room->unread_notification_count = 0; } else { window.draw(more_messages_below_rect); } @@ -4159,6 +4188,12 @@ namespace QuickMedia { load_sprite.setPosition(body_pos.x + body_size.x * 0.5f, body_pos.y + body_size.y * 0.5f); load_sprite.setRotation(load_sprite_timer.getElapsedTime().asSeconds() * 400.0); window.draw(load_sprite); + std::string err_msg; + if(matrix->did_initial_sync_fail(err_msg)) { + show_notification("QuickMedia", "Initial matrix sync failed, error: " + err_msg, Urgency::CRITICAL); + window.close(); + goto chat_page_end; + } } window.display(); @@ -4167,8 +4202,14 @@ namespace QuickMedia { matrix_chat_page->should_clear_data = false; cleanup_tasks(); + std::string err_msg; while(!matrix->is_initial_sync_finished()) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); + if(matrix->did_initial_sync_fail(err_msg)) { + show_notification("QuickMedia", "Initial matrix sync failed, error: " + err_msg, Urgency::CRITICAL); + window.close(); + goto chat_page_end; + } } current_room = matrix->get_room_by_id(current_room->id); diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index b7f911f..fd05399 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -17,7 +17,6 @@ // Show images/videos inline. // TODO: Verify if buffer of size 512 is enough for endpoints // Remove older messages (outside screen) to save memory. Reload them when the selected body item is the top/bottom one. -// TODO: Use lazy load filter for /sync (filter=0, required GET first to check if its available). If we use filter for sync then we also need to modify Matrix::get_message_by_id to parse state, etc. static const char* SERVICE_NAME = "matrix"; static const char* OTHERS_ROOM_TAG = "tld.name.others"; @@ -82,7 +81,7 @@ namespace QuickMedia { } std::shared_ptr RoomData::get_user_by_id(const std::string &user_id) { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); auto user_it = user_info_by_user_id.find(user_id); if(user_it == user_info_by_user_id.end()) return nullptr; @@ -90,22 +89,22 @@ namespace QuickMedia { } void RoomData::add_user(std::shared_ptr user) { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); user_info_by_user_id.insert(std::make_pair(user->user_id, user)); } void RoomData::set_user_read_marker(std::shared_ptr &user, const std::string &event_id) { - std::lock_guard lock(user_mutex); + std::lock_guard lock(user_mutex); user->read_marker_event_id = event_id; } std::string RoomData::get_user_read_marker(std::shared_ptr &user) { - std::lock_guard lock(user_mutex); + std::lock_guard lock(user_mutex); return user->read_marker_event_id; } void RoomData::prepend_messages_reverse(const std::vector> &new_messages) { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); for(auto it = new_messages.begin(); it != new_messages.end(); ++it) { if(message_by_event_id.find((*it)->event_id) == message_by_event_id.end()) { message_by_event_id.insert(std::make_pair((*it)->event_id, *it)); @@ -115,7 +114,7 @@ namespace QuickMedia { } void RoomData::append_messages(const std::vector> &new_messages) { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); for(auto it = new_messages.begin(); it != new_messages.end(); ++it) { if(message_by_event_id.find((*it)->event_id) == message_by_event_id.end()) { message_by_event_id.insert(std::make_pair((*it)->event_id, *it)); @@ -125,7 +124,7 @@ namespace QuickMedia { } std::shared_ptr RoomData::get_message_by_id(const std::string &id) { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); auto message_it = message_by_event_id.find(id); if(message_it == message_by_event_id.end()) return nullptr; @@ -133,7 +132,7 @@ namespace QuickMedia { } std::vector> RoomData::get_users_excluding_me(const std::string &my_user_id) { - std::lock_guard lock(user_mutex); + std::lock_guard lock(user_mutex); std::vector> users_excluding_me; for(auto &[user_id, user] : user_info_by_user_id) { if(user->user_id != my_user_id) { @@ -160,52 +159,52 @@ namespace QuickMedia { } bool RoomData::has_prev_batch() { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); return !prev_batch.empty(); } void RoomData::set_prev_batch(const std::string &new_prev_batch) { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); prev_batch = new_prev_batch; } std::string RoomData::get_prev_batch() { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); return prev_batch; } bool RoomData::has_name() { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); return !name.empty(); } void RoomData::set_name(const std::string &new_name) { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); name = new_name; } std::string RoomData::get_name() { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); return name; } bool RoomData::has_avatar_url() { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); return !avatar_url.empty(); } void RoomData::set_avatar_url(const std::string &new_avatar_url) { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); avatar_url = new_avatar_url; } std::string RoomData::get_avatar_url() { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); return avatar_url; } void RoomData::set_pinned_events(std::vector new_pinned_events) { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); pinned_events = std::move(new_pinned_events); pinned_events_updated = true; } @@ -215,10 +214,11 @@ namespace QuickMedia { } void RoomData::clear_data() { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); fetched_messages_by_event_id.clear(); user_info_by_user_id.clear(); messages.clear(); + messages_read_index = 0; message_by_event_id.clear(); pinned_events.clear(); tags.clear(); @@ -292,6 +292,15 @@ namespace QuickMedia { invites_page->remove_body_item_by_room_id(room_id); } + void MatrixQuickMedia::add_unread_notification(RoomData *room, std::string event_id, std::string sender, std::string body) { + std::lock_guard lock(room_body_items_mutex); + Notification notification; + notification.event_id = std::move(event_id); + notification.sender = std::move(sender); + notification.body = std::move(body); + unread_notifications[room].push_back(std::move(notification)); + } + static int find_top_body_position_for_unread_room(const BodyItems &room_body_items, BodyItem *item_to_swap) { for(int i = 0; i < (int)room_body_items.size(); ++i) { const auto &body_item = room_body_items[i]; @@ -304,7 +313,7 @@ namespace QuickMedia { static int find_top_body_position_for_mentioned_room(const BodyItems &room_body_items, BodyItem *item_to_swap) { for(int i = 0; i < (int)room_body_items.size(); ++i) { const auto &body_item = room_body_items[i]; - if(!static_cast(body_item->userdata)->has_unread_mention || body_item.get() == item_to_swap) + if(static_cast(body_item->userdata)->unread_notification_count == 0 || body_item.get() == item_to_swap) return i; } return -1; @@ -314,14 +323,30 @@ namespace QuickMedia { std::sort(room_body_items.begin(), room_body_items.end(), [](const std::shared_ptr &body_item1, const std::shared_ptr &body_item2) { RoomData *room1 = static_cast(body_item1->userdata); RoomData *room2 = static_cast(body_item2->userdata); - int room1_focus_sum = (int)room1->has_unread_mention + (int)!room1->last_message_read; - int room2_focus_sum = (int)room2->has_unread_mention + (int)!room2->last_message_read; + int room1_focus_sum = (int)(room1->unread_notification_count > 0) + (int)!room1->last_message_read; + int room2_focus_sum = (int)(room2->unread_notification_count > 0) + (int)!room2->last_message_read; return room1_focus_sum > room2_focus_sum; }); } void MatrixQuickMedia::update(MatrixPageType page_type) { update_pending_room_messages(page_type); + std::lock_guard room_body_lock(room_body_items_mutex); + bool is_window_focused = program->is_window_focused(); + RoomData *current_room = program->get_current_chat_room(); + for(auto &it : unread_notifications) { + if(!it.second.empty() && (!is_window_focused || it.first != current_room || page_type == MatrixPageType::ROOM_LIST)) { + for(auto &unread_notification : it.second) { + show_notification("QuickMedia matrix - " + unread_notification.sender + " (" + it.first->get_name() + ")", unread_notification.body); + } + } + update_room_description(it.first, false); + } + //if(!unread_notifications.empty()) { + // rooms_page->sort_rooms(); + // room_tags_page->sort_rooms(); + //} + unread_notifications.clear(); } void MatrixQuickMedia::clear_data() { @@ -332,6 +357,62 @@ namespace QuickMedia { rooms_page->clear_data(); room_tags_page->clear_data(); invites_page->clear_data(); + unread_notifications.clear(); + } + + void MatrixQuickMedia::update_room_description(RoomData *room, bool is_initial_sync) { + room->acquire_room_lock(); + const Messages &messages = room->get_messages_thread_unsafe(); + + time_t read_marker_message_timestamp = 0; + std::shared_ptr me = matrix->get_me(room); + if(me) { + auto read_marker_message = room->get_message_by_id(room->get_user_read_marker(me)); + if(read_marker_message) + read_marker_message_timestamp = read_marker_message->timestamp; + } + + // TODO: this wont always work because we dont display all types of messages from server, such as "joined", "left", "kicked", "banned", "changed avatar", "changed display name", etc. + // TODO: Binary search? + Message *last_unread_message = nullptr; + for(auto it = messages.rbegin(), end = messages.rend(); it != end; ++it) { + if((*it)->related_event_type != RelatedEventType::EDIT && (*it)->related_event_type != RelatedEventType::REDACTION && (*it)->timestamp > read_marker_message_timestamp) { + last_unread_message = (*it).get(); + break; + } + } + if(!last_unread_message && !messages.empty() && messages.back()->timestamp > read_marker_message_timestamp) + last_unread_message = messages.back().get(); + + BodyItem *room_body_item = static_cast(room->userdata); + assert(room_body_item); + + if(last_unread_message) { + std::string room_desc = "Unread: " + matrix->message_get_author_displayname(last_unread_message) + ": " + extract_first_line_elipses(last_unread_message->body, 150); + int unread_notification_count = room->unread_notification_count; + if(unread_notification_count > 0) + room_desc += "\n** " + std::to_string(unread_notification_count) + " unread mention(s) **"; // TODO: Better notification? + room_body_item->set_description(std::move(room_desc)); + room_body_item->set_title_color(sf::Color(255, 100, 100)); + room->last_message_read = false; + + rooms_page->move_room_to_top(room); + room_tags_page->move_room_to_top(room); + } else if(is_initial_sync) { + Message *last_message = nullptr; + for(auto it = messages.rbegin(), end = messages.rend(); it != end; ++it) { + if((*it)->related_event_type != RelatedEventType::EDIT && (*it)->related_event_type != RelatedEventType::REDACTION) { + last_message = (*it).get(); + break; + } + } + if(last_message && !messages.empty()) + last_message = messages.back().get(); + if(last_message) + room_body_item->set_description(matrix->message_get_author_displayname(last_message) + ": " + extract_first_line_elipses(last_message->body, 150)); + } + + room->release_room_lock(); } void MatrixQuickMedia::update_pending_room_messages(MatrixPageType page_type) { @@ -349,7 +430,6 @@ namespace QuickMedia { if(!it.second.sync_is_cache) { for(auto &message : messages) { if(message->mentions_me) { - room->has_unread_mention = true; // TODO: What if the message or username begins with "-"? also make the notification image be the avatar of the user if(!is_window_focused || room != current_room || is_initial_sync || page_type == MatrixPageType::ROOM_LIST) show_notification("QuickMedia matrix - " + matrix->message_get_author_displayname(message.get()) + " (" + room->get_name() + ")", message->body); @@ -357,48 +437,7 @@ namespace QuickMedia { } } - std::shared_ptr me = matrix->get_me(room); - time_t read_marker_message_timestamp = 0; - if(me) { - auto read_marker_message = room->get_message_by_id(room->get_user_read_marker(me)); - if(read_marker_message) - read_marker_message_timestamp = read_marker_message->timestamp; - } - - // TODO: this wont always work because we dont display all types of messages from server, such as "joined", "left", "kicked", "banned", "changed avatar", "changed display name", etc. - // TODO: Binary search? - Message *last_unread_message = nullptr; - for(auto it = messages.rbegin(), end = messages.rend(); it != end; ++it) { - if((*it)->related_event_type != RelatedEventType::EDIT && (*it)->related_event_type != RelatedEventType::REDACTION && (*it)->timestamp > read_marker_message_timestamp) { - last_unread_message = (*it).get(); - break; - } - } - - BodyItem *room_body_item = static_cast(room->userdata); - assert(room_body_item); - - if(last_unread_message) { - std::string room_desc = "Unread: " + matrix->message_get_author_displayname(last_unread_message) + ": " + extract_first_line_elipses(last_unread_message->body, 150); - if(room->has_unread_mention) - room_desc += "\n** You were mentioned **"; // TODO: Better notification? - room_body_item->set_description(std::move(room_desc)); - room_body_item->set_title_color(sf::Color(255, 100, 100)); - room->last_message_read = false; - - rooms_page->move_room_to_top(room); - room_tags_page->move_room_to_top(room); - } else if(is_initial_sync) { - Message *last_message = nullptr; - for(auto it = messages.rbegin(), end = messages.rend(); it != end; ++it) { - if((*it)->related_event_type != RelatedEventType::EDIT && (*it)->related_event_type != RelatedEventType::REDACTION) { - last_message = (*it).get(); - break; - } - } - if(last_message) - room_body_item->set_description(matrix->message_get_author_displayname(last_message) + ": " + extract_first_line_elipses(last_message->body, 150)); - } + update_room_description(room, is_initial_sync); } pending_room_messages.clear(); } @@ -443,6 +482,10 @@ namespace QuickMedia { body->clamp_selection(); body->append_items(std::move(room_body_items)); } + if(sort_on_update) { + sort_on_update = false; + sort_room_body_items(body->items); + } matrix_delegate->update(MatrixPageType::ROOM_LIST); } @@ -460,7 +503,7 @@ namespace QuickMedia { if(room_body_index != -1) { std::shared_ptr body_item = body->items[room_body_index]; int body_swap_index = -1; - if(room->has_unread_mention) + if(room->unread_notification_count > 0) body_swap_index = find_top_body_position_for_mentioned_room(body->items, body_item.get()); else if(!room->last_message_read) body_swap_index = find_top_body_position_for_unread_room(body->items, body_item.get()); @@ -493,6 +536,10 @@ namespace QuickMedia { current_chat_page->should_clear_data = true; } + void MatrixRoomsPage::sort_rooms() { + sort_on_update = true; + } + PluginResult MatrixRoomTagsPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { (void)title; std::lock_guard lock(mutex); @@ -607,6 +654,12 @@ namespace QuickMedia { current_rooms_page->clear_data(); } + void MatrixRoomTagsPage::sort_rooms() { + std::lock_guard lock(mutex); + if(current_rooms_page) + current_rooms_page->sort_rooms(); + } + MatrixInvitesPage::MatrixInvitesPage(Program *program, Matrix *matrix, Body *body) : Page(program), matrix(matrix), body(body) { } @@ -775,7 +828,7 @@ namespace QuickMedia { sync_is_cache = false; sync_running = true; - sync_thread = std::thread([this, delegate, matrix_cache_dir]() { + sync_thread = std::thread([this, matrix_cache_dir]() { sync_is_cache = true; FILE *sync_cache_file = fopen(matrix_cache_dir.data.c_str(), "rb"); if(sync_cache_file) { @@ -786,13 +839,24 @@ namespace QuickMedia { rapidjson::ParseResult parse_result = doc.ParseStream(is); if(parse_result.IsError()) break; - if(parse_sync_response(doc, delegate) != PluginResult::OK) + if(parse_sync_response(doc) != PluginResult::OK) fprintf(stderr, "Failed to parse cached sync response\n"); } fclose(sync_cache_file); } sync_is_cache = false; + // Filter with account data. TODO: Test if this is needed for encrypted chats + // {"presence":{"limit":0,"types":[""]},"account_data":{"not_types":["im.vector.setting.breadcrumbs","m.push_rules","im.vector.setting.allowed_widgets","io.element.recent_emoji"]},"room":{"state":{"limit":1,"not_types":["m.room.related_groups","m.room.power_levels","m.room.join_rules","m.room.history_visibility"],"lazy_load_members":true},"timeline":{"limit":3,"lazy_load_members":true},"ephemeral":{"limit":0,"types":[""],"lazy_load_members":true},"account_data":{"limit":1,"types":["m.fully_read"],"lazy_load_members":true}}} + // Filter without account data + const char *filter = "{\"presence\":{\"limit\":0,\"types\":[\"\"]},\"account_data\":{\"limit\":0,\"types\":[\"\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"limit\":3,\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"limit\":1,\"types\":[\"m.fully_read\",\"m.tag\"],\"lazy_load_members\":true}}}"; + const std::string filter_encoded = url_param_encode(filter); + + std::vector additional_args = { + { "-H", "Authorization: Bearer " + access_token }, + { "-m", "35" } + }; + const rapidjson::Value *next_batch_json; PluginResult result; bool initial_sync = true; @@ -802,40 +866,43 @@ namespace QuickMedia { { "-m", "35" } }; - char url[512]; + char url[1024]; if(next_batch.empty()) - snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=0", homeserver.c_str()); + snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?filter=%s&timeout=0", homeserver.c_str(), filter_encoded.c_str()); else - snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=30000&since=%s", homeserver.c_str(), next_batch.c_str()); + snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?filter=%s&timeout=30000&since=%s", homeserver.c_str(), filter_encoded.c_str(), next_batch.c_str()); rapidjson::Document json_root; std::string err_msg; - DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true, &err_msg); + DownloadResult download_result = download_json(json_root, url, additional_args, true, &err_msg); if(download_result != DownloadResult::OK) { fprintf(stderr, "/sync failed\n"); - if(initial_sync && json_root.IsObject()) { - const rapidjson::Value &errcode_json = GetMember(json_root, "errcode"); - if(errcode_json.IsString()) { - for(const char *sync_fail_error_code : sync_fail_error_codes) { - if(strcmp(errcode_json.GetString(), sync_fail_error_code) == 0) { - sync_fail_reason = sync_fail_error_code; - const rapidjson::Value &error_json = GetMember(json_root, "error"); - if(error_json.IsString()) - sync_fail_reason = error_json.GetString(); - sync_failed = true; - sync_running = false; - break; - } + goto sync_end; + } + + if(initial_sync && json_root.IsObject()) { + const rapidjson::Value &errcode_json = GetMember(json_root, "errcode"); + if(errcode_json.IsString()) { + for(const char *sync_fail_error_code : sync_fail_error_codes) { + if(strcmp(errcode_json.GetString(), sync_fail_error_code) == 0) { + sync_fail_reason = sync_fail_error_code; + const rapidjson::Value &error_json = GetMember(json_root, "error"); + if(error_json.IsString()) + sync_fail_reason = error_json.GetString(); + sync_failed = true; + sync_running = false; + break; } } + fprintf(stderr, "/sync failed\n"); + goto sync_end; } - goto sync_end; } if(next_batch.empty()) clear_sync_cache_for_new_sync(); - result = parse_sync_response(json_root, delegate); + result = parse_sync_response(json_root); if(result != PluginResult::OK) { fprintf(stderr, "Failed to parse sync response\n"); goto sync_end; @@ -850,22 +917,44 @@ namespace QuickMedia { fprintf(stderr, "Matrix: missing next batch\n"); } + if(initial_sync) { + notification_thread = std::thread([this]() { + std::vector additional_args = { + { "-H", "Authorization: Bearer " + access_token } + }; + + // TODO: Instead of guessing notification limit with 100, accumulate rooms unread_notifications count and use that as the limit + // (and take into account that notification response may have notifications after call to sync above). + char url[512]; + snprintf(url, sizeof(url), "%s/_matrix/client/r0/notifications?limit=100", homeserver.c_str()); + + rapidjson::Document json_root; + DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true); + if(download_result != DownloadResult::OK || !json_root.IsObject()) { + fprintf(stderr, "Fetching notifications failed!\n"); + return; + } + + const rapidjson::Value ¬ification_json = GetMember(json_root, "notifications"); + parse_notifications(notification_json); + }); + } + sync_end: if(sync_running) - std::this_thread::sleep_for(std::chrono::seconds(1)); - - if(!json_root.IsObject()) - continue; + std::this_thread::sleep_for(std::chrono::milliseconds(500)); // TODO: Circulate file FILE *sync_cache_file = fopen(matrix_cache_dir.data.c_str(), initial_sync ? "wb" : "ab"); initial_sync = false; if(sync_cache_file) { - char buffer[4096]; - rapidjson::FileWriteStream file_write_stream(sync_cache_file, buffer, sizeof(buffer)); - rapidjson::Writer writer(file_write_stream); - remove_unused_sync_data_fields(json_root); - json_root.Accept(writer); + if(json_root.IsObject()) { + char buffer[4096]; + rapidjson::FileWriteStream file_write_stream(sync_cache_file, buffer, sizeof(buffer)); + rapidjson::Writer writer(file_write_stream); + remove_unused_sync_data_fields(json_root); + json_root.Accept(writer); + } fclose(sync_cache_file); } } @@ -874,9 +963,17 @@ namespace QuickMedia { void Matrix::stop_sync() { sync_running = false; - program_kill_in_thread(sync_thread.get_id()); - if(sync_thread.joinable()) + + if(sync_thread.joinable()) { + program_kill_in_thread(sync_thread.get_id()); sync_thread.join(); + } + + if(notification_thread.joinable()) { + program_kill_in_thread(notification_thread.get_id()); + notification_thread.join(); + } + delegate = nullptr; sync_failed = false; sync_fail_reason.clear(); @@ -903,6 +1000,7 @@ namespace QuickMedia { room->messages_read_index = room_messages.size(); } else { fprintf(stderr, "Unexpected behavior!!!! get_room_sync_data said read index is %zu but we only have %zu messages\n", room->messages_read_index, room_messages.size()); + room->messages_read_index = room_messages.size(); } if(room->pinned_events_updated) { sync_data.pinned_events = room->get_pinned_events_unsafe(); @@ -938,22 +1036,75 @@ namespace QuickMedia { size_t num_new_messages = num_messages_after - num_messages_before; messages.insert(messages.end(), room->get_messages_thread_unsafe().begin(), room->get_messages_thread_unsafe().begin() + num_new_messages); room->messages_read_index += num_new_messages; + assert(room->messages_read_index <= room->get_messages_thread_unsafe().size()); room->release_room_lock(); return PluginResult::OK; } - PluginResult Matrix::parse_sync_response(const rapidjson::Document &root, MatrixDelegate *delegate) { + PluginResult Matrix::parse_sync_response(const rapidjson::Document &root) { if(!root.IsObject()) return PluginResult::ERR; - const rapidjson::Value &account_data_json = GetMember(root, "account_data"); - std::optional> dm_rooms; - parse_sync_account_data(account_data_json, dm_rooms); + //const rapidjson::Value &account_data_json = GetMember(root, "account_data"); + //std::optional> dm_rooms; + //parse_sync_account_data(account_data_json, dm_rooms); // TODO: Include "Direct messages" as a tag using |dm_rooms| above const rapidjson::Value &rooms_json = GetMember(root, "rooms"); - parse_sync_room_data(rooms_json, delegate); + parse_sync_room_data(rooms_json); + + return PluginResult::OK; + } + + PluginResult Matrix::parse_notifications(const rapidjson::Value ¬ifications_json) { + if(!notifications_json.IsArray()) + return PluginResult::ERR; + for(const rapidjson::Value ¬ification_json : notifications_json.GetArray()) { + if(!notification_json.IsObject()) + continue; + + const rapidjson::Value &read_json = GetMember(notification_json, "read"); + if(!read_json.IsBool() || read_json.GetBool()) + continue; + + const rapidjson::Value &room_id_json = GetMember(notification_json, "room_id"); + if(!room_id_json.IsString()) + continue; + + const rapidjson::Value &event_json = GetMember(notification_json, "event"); + if(!event_json.IsObject()) + continue; + + const rapidjson::Value &event_id_json = GetMember(event_json, "event_id"); + if(!event_id_json.IsString()) + continue; + + const rapidjson::Value &sender_json = GetMember(event_json, "sender"); + if(!sender_json.IsString()) + continue; + + const rapidjson::Value &content_json = GetMember(event_json, "content"); + if(!content_json.IsObject()) + continue; + + const rapidjson::Value &body_json = GetMember(content_json, "body"); + if(!body_json.IsString()) + continue; + + std::string room_id(room_id_json.GetString(), room_id_json.GetStringLength()); + RoomData *room = get_room_by_id(room_id); + if(!room) { + fprintf(stderr, "Warning: got notification in unknown room %s\n", room_id.c_str()); + continue; + } + room->unread_notification_count++; + + std::string event_id(event_id_json.GetString(), event_id_json.GetStringLength()); + std::string sender(sender_json.GetString(), sender_json.GetStringLength()); + std::string body(body_json.GetString(), body_json.GetStringLength()); + delegate->add_unread_notification(room, std::move(event_id), std::move(sender), std::move(body)); + } return PluginResult::OK; } @@ -999,7 +1150,7 @@ namespace QuickMedia { return PluginResult::OK; } - PluginResult Matrix::parse_sync_room_data(const rapidjson::Value &rooms_json, MatrixDelegate *delegate) { + PluginResult Matrix::parse_sync_room_data(const rapidjson::Value &rooms_json) { if(!rooms_json.IsObject()) return PluginResult::OK; @@ -1033,7 +1184,7 @@ namespace QuickMedia { events_add_pinned_events(events_json, room); } - const rapidjson::Value &ephemeral_json = GetMember(it.value, "ephemeral"); + const rapidjson::Value &account_data_json = GetMember(it.value, "account_data"); const rapidjson::Value &timeline_json = GetMember(it.value, "timeline"); if(timeline_json.IsObject()) { @@ -1047,27 +1198,38 @@ namespace QuickMedia { // TODO: Use /_matrix/client/r0/notifications ? or remove this and always look for displayname/user_id in messages bool has_unread_notifications = false; const rapidjson::Value &unread_notification_json = GetMember(it.value, "unread_notifications"); - if(unread_notification_json.IsObject()) { + if(unread_notification_json.IsObject() && is_initial_sync_finished()) { const rapidjson::Value &highlight_count_json = GetMember(unread_notification_json, "highlight_count"); - if(highlight_count_json.IsNumber() && highlight_count_json.GetInt64() > 0) + if(highlight_count_json.IsNumber() && highlight_count_json.GetInt64() > 0) { + room->unread_notification_count = highlight_count_json.GetInt64(); has_unread_notifications = true; + } } const rapidjson::Value &events_json = GetMember(timeline_json, "events"); events_add_user_info(events_json, room); events_set_room_name(events_json, room); - // We want to do this before adding messages to know if a message that mentions us is a new mention - if(ephemeral_json.IsObject()) { - const rapidjson::Value &events_json = GetMember(ephemeral_json, "events"); - events_add_user_read_markers(events_json, room); + + if(account_data_json.IsObject()) { + const rapidjson::Value &events_json = GetMember(account_data_json, "events"); + auto me = get_me(room); + events_set_user_read_marker(events_json, room, me); } - events_add_messages(events_json, room, MessageDirection::AFTER, delegate, has_unread_notifications); + + if(is_new_room) + delegate->join_room(room); + + events_add_messages(events_json, room, MessageDirection::AFTER, has_unread_notifications); events_add_pinned_events(events_json, room); } else { - if(ephemeral_json.IsObject()) { - const rapidjson::Value &events_json = GetMember(ephemeral_json, "events"); - events_add_user_read_markers(events_json, room); + if(account_data_json.IsObject()) { + const rapidjson::Value &events_json = GetMember(account_data_json, "events"); + auto me = get_me(room); + events_set_user_read_marker(events_json, room, me); } + + if(is_new_room) + delegate->join_room(room); } if(remove_invite(room_id_str)) { @@ -1075,13 +1237,9 @@ namespace QuickMedia { delegate->remove_invite(room_id_str); } - if(is_new_room) - delegate->join_room(room); - - const rapidjson::Value &account_data_json = GetMember(it.value, "account_data"); if(account_data_json.IsObject()) { const rapidjson::Value &events_json = GetMember(account_data_json, "events"); - events_add_room_to_tags(events_json, room, delegate); + events_add_room_to_tags(events_json, room); } if(is_new_room) { @@ -1097,10 +1255,10 @@ namespace QuickMedia { } const rapidjson::Value &leave_json = GetMember(rooms_json, "leave"); - remove_rooms(leave_json, delegate); + remove_rooms(leave_json); const rapidjson::Value &invite_json = GetMember(rooms_json, "invite"); - add_invites(invite_json, delegate); + add_invites(invite_json); return PluginResult::OK; } @@ -1158,7 +1316,7 @@ namespace QuickMedia { if(strncmp(user_info->avatar_url.c_str(), "mxc://", 6) == 0) user_info->avatar_url.erase(user_info->avatar_url.begin(), user_info->avatar_url.begin() + 6); if(!user_info->avatar_url.empty()) - user_info->avatar_url = homeserver + "/_matrix/media/r0/thumbnail/" + user_info->avatar_url + "?width=32&height=32&method=crop"; + user_info->avatar_url = homeserver + "/_matrix/media/r0/thumbnail/" + user_info->avatar_url + "?width=32&height=32&method=crop"; // TODO: Remove the constant strings around to reduce memory usage (6.3mb) user_info->display_name = display_name_json.IsString() ? display_name_json.GetString() : sender_json_str; user_info->display_name_color = user_id_to_color(sender_json_str); @@ -1217,6 +1375,31 @@ namespace QuickMedia { } } + void Matrix::events_set_user_read_marker(const rapidjson::Value &events_json, RoomData *room_data, std::shared_ptr &me) { + assert(me); // TODO: Remove read marker from user and set it for the room instead. We need that in the matrix pages also + if(!events_json.IsArray() || !me) + return; + + for(const rapidjson::Value &event_json : events_json.GetArray()) { + if(!event_json.IsObject()) + continue; + + const rapidjson::Value &type_json = GetMember(event_json, "type"); + if(!type_json.IsString() || strcmp(type_json.GetString(), "m.fully_read") != 0) + continue; + + const rapidjson::Value &content_json = GetMember(event_json, "content"); + if(!content_json.IsObject()) + continue; + + const rapidjson::Value &event_id_json = GetMember(content_json, "event_id"); + if(!event_id_json.IsString()) + continue; + + room_data->set_user_read_marker(me, std::string(event_id_json.GetString(), event_id_json.GetStringLength())); + } + } + static std::string message_content_extract_thumbnail_url(const rapidjson::Value &content_json, const std::string &homeserver) { const rapidjson::Value &info_json = GetMember(content_json, "info"); if(info_json.IsObject()) { @@ -1323,7 +1506,7 @@ namespace QuickMedia { return false; } - void Matrix::events_add_messages(const rapidjson::Value &events_json, RoomData *room_data, MessageDirection message_dir, MatrixDelegate *delegate, bool has_unread_notifications) { + void Matrix::events_add_messages(const rapidjson::Value &events_json, RoomData *room_data, MessageDirection message_dir, bool has_unread_notifications) { if(!events_json.IsArray()) return; @@ -1346,18 +1529,20 @@ namespace QuickMedia { room_data->append_messages(new_messages); } - time_t read_marker_message_timestamp = 0; - if(me) { - auto read_marker_message = room_data->get_message_by_id(room_data->get_user_read_marker(me)); - if(read_marker_message) - read_marker_message_timestamp = read_marker_message->timestamp; - } + if(is_initial_sync_finished()) { + time_t read_marker_message_timestamp = 0; + if(me) { + auto read_marker_message = room_data->get_message_by_id(room_data->get_user_read_marker(me)); + if(read_marker_message) + read_marker_message_timestamp = read_marker_message->timestamp; + } - for(auto &message : new_messages) { - // TODO: Is @room ok? shouldn't we also check if the user has permission to do @room? (only when notifications are limited to @mentions) - // TODO: Is comparing against read marker timestamp ok enough? - if(has_unread_notifications && me && message->timestamp > read_marker_message_timestamp) - message->mentions_me = message_contains_user_mention(message->body, me->display_name) || message_contains_user_mention(message->body, me->user_id) || message_contains_user_mention(message->body, "@room"); + for(auto &message : new_messages) { + // TODO: Is @room ok? shouldn't we also check if the user has permission to do @room? (only when notifications are limited to @mentions) + // TODO: Is comparing against read marker timestamp ok enough? + if(has_unread_notifications && me && message->timestamp > read_marker_message_timestamp) + message->mentions_me = message_contains_user_mention(message->body, me->display_name) || message_contains_user_mention(message->body, me->user_id) || message_contains_user_mention(message->body, "@room"); + } } if(delegate) @@ -1426,7 +1611,7 @@ namespace QuickMedia { content_json = &new_content_json; const rapidjson::Value &content_type = GetMember(*content_json, "msgtype"); - if(!content_type.IsString() || strcmp(type_json.GetString(), "m.room.redaction") == 0) { + if(strcmp(type_json.GetString(), "m.room.redaction") == 0) { auto message = std::make_shared(); message->type = MessageType::REDACTION; message->user = user; @@ -1460,7 +1645,7 @@ namespace QuickMedia { // TODO: Also show joins, leave, invites, bans, kicks, mutes, etc - if(strcmp(content_type.GetString(), "m.text") == 0) { + if(!content_type.IsString() || strcmp(content_type.GetString(), "m.text") == 0) { message->type = MessageType::TEXT; } else if(strcmp(content_type.GetString(), "m.image") == 0) { const rapidjson::Value &url_json = GetMember(*content_json, "url"); @@ -1665,7 +1850,7 @@ namespace QuickMedia { room_data->set_pinned_events(std::move(pinned_events)); } - void Matrix::events_add_room_to_tags(const rapidjson::Value &events_json, RoomData *room_data, MatrixDelegate *delegate) { + void Matrix::events_add_room_to_tags(const rapidjson::Value &events_json, RoomData *room_data) { if(!events_json.IsArray()) return; @@ -1730,7 +1915,7 @@ namespace QuickMedia { } } - void Matrix::add_invites(const rapidjson::Value &invite_json, MatrixDelegate *delegate) { + void Matrix::add_invites(const rapidjson::Value &invite_json) { if(!invite_json.IsObject()) return; @@ -1801,7 +1986,7 @@ namespace QuickMedia { } } - void Matrix::remove_rooms(const rapidjson::Value &leave_json, MatrixDelegate *delegate) { + void Matrix::remove_rooms(const rapidjson::Value &leave_json) { if(!leave_json.IsObject()) return; @@ -1917,12 +2102,12 @@ namespace QuickMedia { if(!json_root.IsObject()) return PluginResult::ERR; - //const rapidjson::Value &state_json = GetMember(json_root, "state"); - //events_add_user_info(state_json, room_data); - //events_set_room_name(state_json, room_data); + const rapidjson::Value &state_json = GetMember(json_root, "state"); + events_add_user_info(state_json, room_data); + events_set_room_name(state_json, room_data); const rapidjson::Value &chunk_json = GetMember(json_root, "chunk"); - events_add_messages(chunk_json, room_data, MessageDirection::BEFORE, nullptr, false); + events_add_messages(chunk_json, room_data, MessageDirection::BEFORE, false); const rapidjson::Value &end_json = GetMember(json_root, "end"); if(!end_json.IsString()) { @@ -2154,7 +2339,6 @@ namespace QuickMedia { PluginResult Matrix::post_reply(RoomData *room, const std::string &body, void *relates_to) { // TODO: Store shared_ptr instead of raw pointer... Message *relates_to_message_raw = (Message*)relates_to; - std::shared_ptr relates_to_message_shared = room->get_message_by_id(relates_to_message_raw->event_id); char random_characters[18]; if(!generate_random_characters(random_characters, sizeof(random_characters))) @@ -2163,7 +2347,7 @@ namespace QuickMedia { std::string random_readable_chars = random_characters_to_readable_string(random_characters, sizeof(random_characters)); rapidjson::Document in_reply_to_json(rapidjson::kObjectType); - in_reply_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_shared->event_id.c_str()), in_reply_to_json.GetAllocator()); + in_reply_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_raw->event_id.c_str()), in_reply_to_json.GetAllocator()); rapidjson::Document relates_to_json(rapidjson::kObjectType); relates_to_json.AddMember("m.in_reply_to", std::move(in_reply_to_json), relates_to_json.GetAllocator()); @@ -2209,7 +2393,6 @@ namespace QuickMedia { PluginResult Matrix::post_edit(RoomData *room, const std::string &body, void *relates_to) { Message *relates_to_message_raw = (Message*)relates_to; - std::shared_ptr relates_to_message_shared = room->get_message_by_id(relates_to_message_raw->event_id); char random_characters[18]; if(!generate_random_characters(random_characters, sizeof(random_characters))) @@ -2246,7 +2429,7 @@ namespace QuickMedia { } rapidjson::Document relates_to_json(rapidjson::kObjectType); - relates_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_shared->event_id.c_str()), relates_to_json.GetAllocator()); + relates_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_raw->event_id.c_str()), relates_to_json.GetAllocator()); relates_to_json.AddMember("rel_type", "m.replace", relates_to_json.GetAllocator()); std::string body_edit_str = " * " + body; @@ -2300,29 +2483,47 @@ namespace QuickMedia { auto fetched_message_it = room->fetched_messages_by_event_id.find(event_id); if(fetched_message_it != room->fetched_messages_by_event_id.end()) return fetched_message_it->second; + + rapidjson::Document request_data(rapidjson::kObjectType); + request_data.AddMember("lazy_load_members", true, request_data.GetAllocator()); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + request_data.Accept(writer); std::vector additional_args = { { "-H", "Authorization: Bearer " + access_token } }; + std::string filter = url_param_encode(buffer.GetString()); + char url[512]; - snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/event/%s", homeserver.c_str(), room->id.c_str(), event_id.c_str()); + snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/context/%s?limit=0&filter=%s", homeserver.c_str(), room->id.c_str(), event_id.c_str(), filter.c_str()); std::string err_msg; rapidjson::Document json_root; DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true, &err_msg); if(download_result != DownloadResult::OK) return nullptr; - if(json_root.IsObject()) { - const rapidjson::Value &error_json = GetMember(json_root, "error"); - if(error_json.IsString()) { - fprintf(stderr, "Matrix::get_message_by_id, error: %s\n", error_json.GetString()); - room->fetched_messages_by_event_id.insert(std::make_pair(event_id, nullptr)); - return nullptr; - } + if(!json_root.IsObject()) { + fprintf(stderr, "Failed to get message by id %s, error: %s\n", event_id.c_str(), err_msg.c_str()); + room->fetched_messages_by_event_id.insert(std::make_pair(event_id, nullptr)); + return nullptr; } - std::shared_ptr new_message = parse_message_event(json_root, room); + const rapidjson::Value &error_json = GetMember(json_root, "error"); + if(error_json.IsString()) { + fprintf(stderr, "Matrix::get_message_by_id, error: %s\n", error_json.GetString()); + room->fetched_messages_by_event_id.insert(std::make_pair(event_id, nullptr)); + return nullptr; + } + + const rapidjson::Value &state_json = GetMember(json_root, "state"); + events_add_user_info(state_json, room); + events_set_room_name(state_json, room); + + const rapidjson::Value &event_json = GetMember(json_root, "event"); + std::shared_ptr new_message = parse_message_event(event_json, room); room->fetched_messages_by_event_id.insert(std::make_pair(event_id, new_message)); return new_message; } @@ -2443,6 +2644,7 @@ namespace QuickMedia { } PluginResult Matrix::login(const std::string &username, const std::string &password, const std::string &homeserver, std::string &err_msg) { + assert(!sync_running); rapidjson::Document identifier_json(rapidjson::kObjectType); identifier_json.AddMember("type", "m.id.user", identifier_json.GetAllocator()); // TODO: What if the server doesn't support this login type? redirect to sso web page etc identifier_json.AddMember("user", rapidjson::StringRef(username.c_str()), identifier_json.GetAllocator()); -- cgit v1.2.3