aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2020-10-01 22:21:14 +0200
committerdec05eba <dec05eba@protonmail.com>2020-10-01 22:21:14 +0200
commitef1dd33682ae26b4af1343aaecf443e7cd883674 (patch)
treecf3c959f1cf9f8b6b401d384537685191ae3e4ba /src
parent30dbaeb2b175c1e67f57aba748ced1a2280fb56d (diff)
Matrix: implement mention/reply notifications
Diffstat (limited to 'src')
-rw-r--r--src/Body.cpp15
-rw-r--r--src/QuickMedia.cpp150
-rw-r--r--src/plugins/Matrix.cpp105
3 files changed, 221 insertions, 49 deletions
diff --git a/src/Body.cpp b/src/Body.cpp
index 843a1b1..673a095 100644
--- a/src/Body.cpp
+++ b/src/Body.cpp
@@ -75,7 +75,8 @@ namespace QuickMedia {
page_scroll(0.0f),
item_background(sf::Vector2f(1.0f, 1.0f), 10.0f, 10),
num_visible_items(0),
- last_item_fully_visible(true)
+ last_item_fully_visible(true),
+ last_fully_visible_item(-1)
{
progress_text.setFillColor(sf::Color::White);
replies_text.setFillColor(sf::Color(129, 162, 190));
@@ -238,6 +239,12 @@ namespace QuickMedia {
return items[selected_item];
}
+ BodyItem* Body::get_last_fully_visible_item() {
+ if(last_fully_visible_item < 0 || last_fully_visible_item >= (int)items.size() || !items[last_fully_visible_item]->visible)
+ return nullptr;
+ return items[last_fully_visible_item].get();
+ }
+
void Body::clamp_selection() {
int num_items = (int)items.size();
if(items.empty())
@@ -291,6 +298,7 @@ namespace QuickMedia {
item_background_shadow.setFillColor(line_seperator_color);
num_visible_items = 0;
last_item_fully_visible = true;
+ last_fully_visible_item = -1;
int num_items = items.size();
if(num_items == 0 || size.y <= 0.0f) {
@@ -413,6 +421,8 @@ namespace QuickMedia {
if((after_pos.y - start_y) + item_height + spacing_y > size.y)
last_item_fully_visible = false;
+ else
+ last_fully_visible_item = i;
if(after_pos.y - start_y >= size.y)
break;
@@ -422,6 +432,9 @@ namespace QuickMedia {
++num_visible_items;
}
+ if(last_fully_visible_item == -1)
+ last_fully_visible_item = selected_item;
+
glDisable(GL_SCISSOR_TEST);
for(auto it = item_thumbnail_textures.begin(); it != item_thumbnail_textures.end();) {
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index 909bdd7..da5453e 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -3331,50 +3331,58 @@ namespace QuickMedia {
*/
// This is needed to get initial data, with joined rooms etc. TODO: Remove this once its cached
// and allow asynchronous update of rooms
- RoomSyncMessages room_sync_messages;
- if(matrix->sync(room_sync_messages) != PluginResult::OK) {
- show_notification("QuickMedia", "Intial matrix sync failed", Urgency::CRITICAL);
- current_page = Page::EXIT;
- return;
- }
-
- if(matrix->get_joined_rooms(tabs[ROOMS_TAB_INDEX].body->items) != PluginResult::OK) {
- show_notification("QuickMedia", "Failed to get a list of joined rooms", Urgency::CRITICAL);
- current_page = Page::EXIT;
- return;
- }
+ bool synced = false;
struct RoomBodyData {
std::shared_ptr<BodyItem> body_item;
bool last_message_read;
+ time_t last_read_message_timestamp;
};
std::unordered_map<std::string, RoomBodyData> body_items_by_room_id;
- for(auto body_item : tabs[ROOMS_TAB_INDEX].body->items) {
- // TODO: Set |last_message_read| depending on read markers (either remote matrix read markers or locally saved ones)
- body_items_by_room_id[body_item->url] = { body_item, true };
- }
- for(auto &[room, messages] : room_sync_messages) {
- auto room_body_item_it = body_items_by_room_id.find(room->id);
- if(room_body_item_it != body_items_by_room_id.end() && !messages.empty()) {
- room_body_item_it->second.body_item->set_description(matrix->message_get_author_displayname(room, messages.back().get()) + ": " + extract_first_line(messages.back()->body));
+ auto process_new_room_messages = [matrix, &body_items_by_room_id](RoomSyncMessages &room_sync_messages, bool only_show_mentions) mutable {
+ for(auto &[room, messages] : room_sync_messages) {
+ bool was_mentioned = false;
+ for(auto &message : messages) {
+ if(message->mentions_me) {
+ was_mentioned = true;
+ message->mentions_me = false;
+ // TODO: What if the message or username begins with "-"? also make the notification image be the avatar of the user
+ std::string desc = "QuickMedia Matrix\n\n" + message->body;
+ show_notification(matrix->message_get_author_displayname(room, message.get()), desc.c_str());
+ }
+ }
+
+ auto room_body_item_it = body_items_by_room_id.find(room->id);
+ if(room_body_item_it == body_items_by_room_id.end())
+ continue;
+
+ if(only_show_mentions) {
+ std::string room_desc;
+ if(!messages.empty())
+ room_desc = matrix->message_get_author_displayname(room, messages.back().get()) + ": " + extract_first_line(messages.back()->body);
+ if(was_mentioned) {
+ room_desc += "\n** You were mentioned **"; // TODO: Better notification?
+ room_body_item_it->second.body_item->title_color = sf::Color(255, 100, 100);
+ room_body_item_it->second.last_message_read = false;
+ }
+ room_body_item_it->second.body_item->set_description(std::move(room_desc));
+ } else if(!messages.empty()) {
+ std::string room_desc = "Unread: " + matrix->message_get_author_displayname(room, messages.back().get()) + ": " + extract_first_line(messages.back()->body);
+ if(was_mentioned)
+ room_desc += "\n** You were mentioned **"; // TODO: Better notification?
+ room_body_item_it->second.body_item->set_description(std::move(room_desc));
+ room_body_item_it->second.body_item->title_color = sf::Color(255, 100, 100);
+ room_body_item_it->second.last_message_read = false;
+ }
}
- }
+ };
// TODO: the initial room to view should be the last viewed room when closing QuickMedia.
// The room id should be saved in a file when changing viewed room.
std::string current_room_id;
- if(!tabs[ROOMS_TAB_INDEX].body->items.empty())
- current_room_id = tabs[ROOMS_TAB_INDEX].body->items[0]->url;
-
- // TODO: Allow empty initial room (if the user hasn't joined any room yet).
- assert(!current_room_id.empty());
-
RoomBodyData *current_room_body_data = nullptr;
- auto room_body_item_it = body_items_by_room_id.find(current_room_id);
- if(room_body_item_it != body_items_by_room_id.end())
- current_room_body_data = &room_body_item_it->second;
// get_all_room_messages is not needed here because its done in the loop, where the initial timeout is 0ms
@@ -3461,6 +3469,7 @@ namespace QuickMedia {
struct SyncFutureResult {
BodyItems body_items;
+ BodyItems rooms_body_items;
RoomSyncMessages room_sync_messages;
};
@@ -3503,9 +3512,7 @@ namespace QuickMedia {
const float tab_vertical_offset = 10.0f;
sf::Text room_name_text("", *font, 18);
- if(current_room_body_data)
- room_name_text.setString(current_room_body_data->body_item->get_title());
- const float room_name_text_height = std::floor(room_name_text.getLocalBounds().height);
+ const float room_name_text_height = 20.0f;
const float room_name_text_padding_y = 10.0f;
const float room_name_total_height = room_name_text_height + room_name_text_padding_y * 2.0f;
const float room_avatar_height = 32.0f;
@@ -3534,6 +3541,9 @@ namespace QuickMedia {
Body url_selection_body(this, font.get(), bold_font.get(), cjk_font.get());
+ sf::Clock read_marker_timer;
+ const sf::Int32 read_marker_timeout_ms = 3000;
+
auto launch_url = [this, &redraw](const std::string &url) mutable {
if(url.empty())
return;
@@ -3608,6 +3618,7 @@ namespace QuickMedia {
} else if(event.key.code == sf::Keyboard::Left) {
tabs[selected_tab].body->clear_thumbnails();
selected_tab = std::max(0, selected_tab - 1);
+ read_marker_timer.restart();
if(typing) {
fprintf(stderr, "Stopped typing\n");
typing = false;
@@ -3616,6 +3627,7 @@ namespace QuickMedia {
} else if(event.key.code == sf::Keyboard::Right) {
tabs[selected_tab].body->clear_thumbnails();
selected_tab = std::min((int)tabs.size() - 1, selected_tab + 1);
+ read_marker_timer.restart();
if(typing) {
fprintf(stderr, "Stopped typing\n");
typing = false;
@@ -3907,6 +3919,8 @@ namespace QuickMedia {
}
chat_input_height_full = chat_input.get_height() + chat_input_padding_y * 2.0f;
+ if(selected_tab != MESSAGES_TAB_INDEX)
+ chat_input_height_full = 0.0f;
chat_input_shade.setSize(sf::Vector2f(window_size.x, chat_input_height_full));
chat_input_shade.setPosition(0.0f, window_size.y - chat_input_shade.getSize().y);
@@ -3927,12 +3941,21 @@ namespace QuickMedia {
sync_running = true;
sync_timer.restart();
sync_future_room_id = current_room_id;
- sync_future = std::async(std::launch::async, [this, &sync_future_room_id]() {
+ sync_future = std::async(std::launch::async, [this, &sync_future_room_id, synced]() {
Matrix *matrix = static_cast<Matrix*>(current_plugin);
SyncFutureResult result;
if(matrix->sync(result.room_sync_messages) == PluginResult::OK) {
fprintf(stderr, "Synced matrix\n");
+
+ if(!synced) {
+ if(matrix->get_joined_rooms(result.rooms_body_items) != PluginResult::OK) {
+ show_notification("QuickMedia", "Failed to get a list of joined rooms", Urgency::CRITICAL);
+ current_page = Page::EXIT;
+ return result;
+ }
+ }
+
if(matrix->get_new_room_messages(sync_future_room_id, result.body_items) != PluginResult::OK) {
fprintf(stderr, "Failed to get new matrix messages in room: %s\n", sync_future_room_id.c_str());
}
@@ -3954,23 +3977,39 @@ namespace QuickMedia {
if(scroll_to_end)
tabs[MESSAGES_TAB_INDEX].body->select_last_item();
}
- for(auto &[room, messages] : sync_result.room_sync_messages) {
- auto room_body_item_it = body_items_by_room_id.find(room->id);
- if(room_body_item_it != body_items_by_room_id.end() && !messages.empty()) {
- room_body_item_it->second.body_item->set_description("Unread: " + matrix->message_get_author_displayname(room, messages.back().get()) + ": " + extract_first_line(messages.back()->body));
- room_body_item_it->second.body_item->title_color = sf::Color(255, 100, 100);
- room_body_item_it->second.last_message_read = false;
+
+ if(!synced) {
+ tabs[ROOMS_TAB_INDEX].body->items = std::move(sync_result.rooms_body_items);
+
+ for(auto body_item : tabs[ROOMS_TAB_INDEX].body->items) {
+ // TODO: Set |last_message_read| depending on read markers (either remote matrix read markers or locally saved ones)
+ body_items_by_room_id[body_item->url] = { body_item, true, 0 };
}
+
+ // TODO: the initial room to view should be the last viewed room when closing QuickMedia.
+ // The room id should be saved in a file when changing viewed room.
+ if(!tabs[ROOMS_TAB_INDEX].body->items.empty())
+ current_room_id = tabs[ROOMS_TAB_INDEX].body->items[0]->url;
+
+ auto room_body_item_it = body_items_by_room_id.find(current_room_id);
+ if(room_body_item_it != body_items_by_room_id.end()) {
+ current_room_body_data = &room_body_item_it->second;
+ room_name_text.setString(current_room_body_data->body_item->get_title());
+ }
+ redraw = true;
}
+
+ process_new_room_messages(sync_result.room_sync_messages, !synced);
sync_running = false;
+ synced = true;
}
if(fetching_previous_messages_running && previous_messages_future.valid() && previous_messages_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
BodyItems new_body_items = previous_messages_future.get();
fprintf(stderr, "Finished fetching older messages, num new messages: %zu\n", new_body_items.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
- if(previous_messages_future_room_id == current_room_id) {
- size_t num_new_messages = new_body_items.size();
+ size_t num_new_messages = new_body_items.size();
+ if(previous_messages_future_room_id == current_room_id && num_new_messages > 0) {
int selected_item_index = tabs[MESSAGES_TAB_INDEX].body->get_selected_item();
tabs[MESSAGES_TAB_INDEX].body->prepend_items(std::move(new_body_items));
tabs[MESSAGES_TAB_INDEX].body->set_selected_item(selected_item_index + num_new_messages);
@@ -4073,8 +4112,12 @@ namespace QuickMedia {
if(tabs[selected_tab].type == ChatTabType::MESSAGES) {
if(tabs[selected_tab].body->is_last_item_fully_visible()) {
if(current_room_body_data && !current_room_body_data->last_message_read) {
- if(strncmp(current_room_body_data->body_item->get_description().c_str(), "Unread: ", 8) == 0)
- current_room_body_data->body_item->set_description(current_room_body_data->body_item->get_description().c_str() + 8);
+ std::string room_desc = current_room_body_data->body_item->get_description();
+ if(strncmp(room_desc.c_str(), "Unread: ", 8) == 0)
+ room_desc = room_desc.substr(8);
+ if(room_desc.size() >= 26 && strncmp(room_desc.c_str() + room_desc.size() - 26, "\n** You were mentioned **", 26) == 0)
+ room_desc = room_desc.substr(0, room_desc.size() - 26);
+ current_room_body_data->body_item->set_description(std::move(room_desc));
// TODO: Show a line like nheko instead for unread messages, or something else
current_room_body_data->body_item->title_color = sf::Color::White;
current_room_body_data->last_message_read = true;
@@ -4084,7 +4127,26 @@ namespace QuickMedia {
}
}
+ // TODO: Cache /sync, then we wont only see loading text
+ if(!synced) {
+ sf::Text loading_text("Loading...", *font, 24);
+ loading_text.setPosition(body_pos.x + body_size.x * 0.5f - loading_text.getLocalBounds().width * 0.5f, body_pos.y + body_size.y * 0.5f - loading_text.getLocalBounds().height * 0.5f);
+ window.draw(loading_text);
+ }
+
if(tabs[selected_tab].type == ChatTabType::MESSAGES) {
+ BodyItem *last_visible_item = tabs[selected_tab].body->get_last_fully_visible_item();
+ if(chat_state != ChatState::URL_SELECTION && current_room_body_data && last_visible_item && read_marker_timer.getElapsedTime().asMilliseconds() >= read_marker_timeout_ms) {
+ Message *message = (Message*)last_visible_item->userdata;
+ if(message->timestamp > current_room_body_data->last_read_message_timestamp) {
+ current_room_body_data->last_read_message_timestamp = message->timestamp;
+ read_marker_timer.restart();
+ if(matrix->set_read_marker(current_room_id, message) != PluginResult::OK) {
+ fprintf(stderr, "Warning: failed to set read marker to %s\n", message->event_id.c_str());
+ }
+ }
+ }
+
window.draw(chat_input_shade);
chat_input.draw(window); //chat_input.draw(window, false);
window.draw(logo_sprite);
diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp
index 6d89ff4..e182c11 100644
--- a/src/plugins/Matrix.cpp
+++ b/src/plugins/Matrix.cpp
@@ -54,7 +54,7 @@ namespace QuickMedia {
char url[512];
if(next_batch.empty())
- snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=0&full_state=true", homeserver.c_str());
+ snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=0", homeserver.c_str());
else
snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=30000&since=%s", homeserver.c_str(), next_batch.c_str());
@@ -312,9 +312,18 @@ namespace QuickMedia {
room_it->second->prev_batch = prev_batch_json.asString();
}
+ // TODO: Is there no better way to check for notifications? this is not robust...
+ bool has_unread_notifications = false;
+ const Json::Value &unread_notification_json = (*it)["unread_notifications"];
+ if(unread_notification_json.isObject()) {
+ const Json::Value &highlight_count_json = unread_notification_json["highlight_count"];
+ if(highlight_count_json.isNumeric() && highlight_count_json.asInt64() > 0)
+ has_unread_notifications = true;
+ }
+
const Json::Value &events_json = timeline_json["events"];
events_add_user_info(events_json, room_it->second.get());
- events_add_messages(events_json, room_it->second.get(), MessageDirection::AFTER, &room_messages);
+ events_add_messages(events_json, room_it->second.get(), MessageDirection::AFTER, &room_messages, has_unread_notifications);
events_set_room_name(events_json, room_it->second.get());
}
@@ -400,13 +409,65 @@ namespace QuickMedia {
return "";
}
- void Matrix::events_add_messages(const Json::Value &events_json, RoomData *room_data, MessageDirection message_dir, RoomSyncMessages *room_messages) {
+ // TODO: Is this really the proper way to check for username mentions?
+ static bool is_username_seperating_character(char c) {
+ switch(c) {
+ case ' ':
+ case '\n':
+ case '\t':
+ case '\v':
+ case '.':
+ case ',':
+ case '@':
+ case ':':
+ case '?':
+ case '!':
+ case '<':
+ case '>':
+ case '\0':
+ return true;
+ default:
+ return false;
+ }
+ return false;
+ }
+
+ // TODO: Do not show notification if mention is a reply to somebody else that replies to me? also dont show notification everytime a mention is edited
+ static bool message_contains_user_mention(const std::string &msg, const std::string &username) {
+ if(msg.empty())
+ return false;
+
+ size_t index = 0;
+ while(index < msg.size()) {
+ size_t found_index = msg.find(username, index);
+ if(found_index == std::string::npos)
+ return false;
+
+ char prev_char = ' ';
+ if(found_index > 0)
+ prev_char = msg[found_index - 1];
+
+ char next_char = '\0';
+ if(found_index + username.size() < msg.size() - 1)
+ next_char = msg[found_index + username.size()];
+
+ if(is_username_seperating_character(prev_char) && is_username_seperating_character(next_char))
+ return true;
+
+ index += username.size();
+ }
+
+ return false;
+ }
+
+ void Matrix::events_add_messages(const Json::Value &events_json, RoomData *room_data, MessageDirection message_dir, RoomSyncMessages *room_messages, bool has_unread_notifications) {
if(!events_json.isArray())
return;
std::vector<std::shared_ptr<Message>> *room_sync_messages = nullptr;
if(room_messages)
room_sync_messages = &(*room_messages)[room_data];
+
std::vector<std::shared_ptr<Message>> new_messages;
for(const Json::Value &event_item_json : events_json) {
@@ -448,6 +509,11 @@ namespace QuickMedia {
if(!body_json.isString())
continue;
+ time_t timestamp = 0;
+ const Json::Value &origin_server_ts = event_item_json["origin_server_ts"];
+ if(origin_server_ts.isNumeric())
+ timestamp = origin_server_ts.asInt64();
+
std::string replaces_event_id;
const Json::Value &relates_to_json = content_json["m.relates_to"];
if(relates_to_json.isObject()) {
@@ -485,6 +551,10 @@ namespace QuickMedia {
message->event_id = event_id_str;
message->body = body_json.asString();
message->replaces_event_id = std::move(replaces_event_id);
+ // TODO: Is @room ok? shouldn't we also check if the user has permission to do @room? (only when notifications are limited to @mentions)
+ if(has_unread_notifications && !username.empty())
+ message->mentions_me = message_contains_user_mention(message->body, username) || message_contains_user_mention(message->body, "@room");
+ message->timestamp = timestamp;
new_messages.push_back(message);
room_data->message_by_event_id[event_id_str] = message;
if(room_sync_messages)
@@ -631,7 +701,7 @@ namespace QuickMedia {
events_set_room_name(state_json, room_data);
const Json::Value &chunk_json = json_root["chunk"];
- events_add_messages(chunk_json, room_data, MessageDirection::BEFORE, nullptr);
+ events_add_messages(chunk_json, room_data, MessageDirection::BEFORE, nullptr, false);
const Json::Value &end_json = json_root["end"];
if(!end_json.isString()) {
@@ -1232,6 +1302,7 @@ namespace QuickMedia {
json_root["homeserver"] = homeserver;
this->user_id = user_id_json.asString();
+ this->username = extract_user_name_from_user_id(this->user_id);
this->access_token = access_token_json.asString();
this->homeserver = homeserver;
@@ -1266,6 +1337,7 @@ namespace QuickMedia {
// Make sure all fields are reset here!
room_data_by_id.clear();
user_id.clear();
+ username.clear();
access_token.clear();
homeserver.clear();
next_batch.clear();
@@ -1390,6 +1462,7 @@ namespace QuickMedia {
}
this->user_id = std::move(user_id);
+ this->username = extract_user_name_from_user_id(this->user_id);
this->access_token = std::move(access_token);
this->homeserver = std::move(homeserver);
return PluginResult::OK;
@@ -1440,6 +1513,30 @@ namespace QuickMedia {
return PluginResult::OK;
}
+ PluginResult Matrix::set_read_marker(const std::string &room_id, const Message *message) {
+ Json::Value request_data(Json::objectValue);
+ request_data["m.fully_read"] = message->event_id;
+ request_data["m.read"] = message->event_id;
+ request_data["m.hidden"] = false; // What is this for? element sends it but its not part of the documentation. Is it for hiding read receipt from other users? in that case, TODO: make it configurable
+
+ Json::StreamWriterBuilder builder;
+ builder["commentStyle"] = "None";
+ builder["indentation"] = "";
+
+ std::vector<CommandArg> additional_args = {
+ { "-X", "POST" },
+ { "-H", "content-type: application/json" },
+ { "--data-binary", Json::writeString(builder, std::move(request_data)) },
+ { "-H", "Authorization: Bearer " + access_token }
+ };
+
+ std::string server_response;
+ if(download_to_string(homeserver + "/_matrix/client/r0/rooms/" + room_id + "/read_markers", server_response, std::move(additional_args), use_tor, true) != DownloadResult::OK)
+ return PluginResult::NET_ERR;
+
+ return PluginResult::OK;
+ }
+
bool Matrix::was_message_posted_by_me(const std::string &room_id, void *message) const {
auto room_it = room_data_by_id.find(room_id);
if(room_it == room_data_by_id.end()) {