aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2020-10-26 09:48:25 +0100
committerdec05eba <dec05eba@protonmail.com>2020-10-29 04:21:15 +0100
commit620123fbd6c18dc48a25cc735565f6d8d85f8639 (patch)
tree1563c8d2867f80f7c5cf00c15c8a1b6612de9f67 /src
parent0d432776c13f7b7bfd94d8ea2a7a41be33f21c8d (diff)
Matrix: add room tags
Fix pinned events that are added after starting QuickMedia (before this change it adds all elements again to the list). Add /me command. Other fixes...
Diffstat (limited to 'src')
-rw-r--r--src/AsyncImageLoader.cpp36
-rw-r--r--src/Body.cpp45
-rw-r--r--src/NetUtils.cpp18
-rw-r--r--src/QuickMedia.cpp838
-rw-r--r--src/plugins/Mangadex.cpp2
-rw-r--r--src/plugins/Manganelo.cpp2
-rw-r--r--src/plugins/Mangatown.cpp2
-rw-r--r--src/plugins/Matrix.cpp644
-rw-r--r--src/plugins/NyaaSi.cpp2
-rw-r--r--src/plugins/Pornhub.cpp2
-rw-r--r--src/plugins/Youtube.cpp2
11 files changed, 932 insertions, 661 deletions
diff --git a/src/AsyncImageLoader.cpp b/src/AsyncImageLoader.cpp
index 98c7fee..d3aa287 100644
--- a/src/AsyncImageLoader.cpp
+++ b/src/AsyncImageLoader.cpp
@@ -28,8 +28,8 @@ namespace QuickMedia {
int scaled_y_start = ((float)y / (float)destination_size.y) * source_size.y;
int scaled_x_end = ((float)(x + 1) / (float)destination_size.x) * source_size.x;
int scaled_y_end = ((float)(y + 1) / (float)destination_size.y) * source_size.y;
- if(scaled_x_end > (int)source_size.x) scaled_x_end = source_size.x;
- if(scaled_y_end > (int)source_size.y) scaled_y_end = source_size.y;
+ if(scaled_x_end > (int)source_size.x - 1) scaled_x_end = source_size.x - 1;
+ if(scaled_y_end > (int)source_size.y - 1) scaled_y_end = source_size.y - 1;
//float scaled_x = x * width_ratio;
//float scaled_y = y * height_ratio;
@@ -99,16 +99,13 @@ namespace QuickMedia {
}
load_image_thread = std::thread([this]{
- ThumbnailLoadData thumbnail_load_data;
+ std::optional<ThumbnailLoadData> thumbnail_load_data_opt;
while(true) {
- {
- std::unique_lock<std::mutex> lock(load_image_mutex);
- while(images_to_load.empty() && running) load_image_cv.wait(lock);
- if(!running)
- break;
- thumbnail_load_data = images_to_load.front();
- images_to_load.pop_front();
- }
+ thumbnail_load_data_opt = image_load_queue.pop_wait();
+ if(!thumbnail_load_data_opt)
+ break;
+
+ ThumbnailLoadData &thumbnail_load_data = thumbnail_load_data_opt.value();
thumbnail_load_data.thumbnail_data->image = std::make_unique<sf::Image>();
if(load_image_from_file(*thumbnail_load_data.thumbnail_data->image, thumbnail_load_data.thumbnail_path.data)) {
@@ -132,12 +129,9 @@ namespace QuickMedia {
}
AsyncImageLoader::~AsyncImageLoader() {
- running = false;
- {
- std::unique_lock<std::mutex> lock(load_image_mutex);
- load_image_cv.notify_one();
- }
- load_image_thread.join();
+ image_load_queue.close();
+ if(load_image_thread.joinable())
+ 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) {
@@ -161,15 +155,11 @@ namespace QuickMedia {
Path thumbnail_path = get_cache_dir().join("thumbnails").join(sha256.getHash());
if(get_file_type(thumbnail_path) == FileType::REGULAR) {
thumbnail_data->loading_state = LoadingState::LOADING;
- std::unique_lock<std::mutex> lock(load_image_mutex);
- images_to_load.push_back({ url, thumbnail_path, local, thumbnail_data, resize_target_size });
- load_image_cv.notify_one();
+ image_load_queue.push({ url, thumbnail_path, local, thumbnail_data, resize_target_size });
return;
} else if(local && get_file_type(url) == FileType::REGULAR) {
thumbnail_data->loading_state = LoadingState::LOADING;
- std::unique_lock<std::mutex> lock(load_image_mutex);
- images_to_load.push_back({ url, thumbnail_path, true, thumbnail_data, resize_target_size });
- load_image_cv.notify_one();
+ image_load_queue.push({ url, thumbnail_path, true, thumbnail_data, resize_target_size });
return;
}
diff --git a/src/Body.cpp b/src/Body.cpp
index 0af6407..1ea5be2 100644
--- a/src/Body.cpp
+++ b/src/Body.cpp
@@ -187,7 +187,7 @@ namespace QuickMedia {
}
void Body::set_selected_item(int item) {
- assert(item >= 0 && item < (int)items.size());
+ //assert(item >= 0 && item < (int)items.size());
selected_item = item;
prev_selected_item = selected_item;
clamp_selection();
@@ -361,7 +361,7 @@ namespace QuickMedia {
int i = prev_selected_item;
while(num_items_scrolled < selected_int_diff_abs && i < num_items) {
if(items[i]->visible) {
- page_scroll += (get_item_height(items[i].get(), selected_int_diff_abs < 50) + spacing_y);
+ page_scroll += (get_item_height(items[i].get(), size.x, selected_int_diff_abs < 50) + spacing_y);
}
++num_items_scrolled;
++i;
@@ -372,7 +372,7 @@ namespace QuickMedia {
int i = prev_selected_item - 1;
while(num_items_scrolled < selected_int_diff_abs && i >= 0) {
if(items[i]->visible) {
- page_scroll -= (get_item_height(items[i].get(), selected_int_diff_abs < 50) + spacing_y);
+ page_scroll -= (get_item_height(items[i].get(), size.x, selected_int_diff_abs < 50) + spacing_y);
}
++num_items_scrolled;
--i;
@@ -380,8 +380,7 @@ namespace QuickMedia {
prev_selected_item = selected_item;
}
- update_dirty_state(items[selected_item].get(), size);
- float selected_item_height = get_item_height(items[selected_item].get()) + spacing_y;
+ float selected_item_height = get_item_height(items[selected_item].get(), size.x) + spacing_y;
if(page_scroll > size.y - selected_item_height) {
page_scroll = size.y - selected_item_height;
} else if(page_scroll < 0.0f) {
@@ -400,10 +399,8 @@ namespace QuickMedia {
if(!item->visible)
continue;
- update_dirty_state(item.get(), size);
item->last_drawn_time = elapsed_time_sec;
-
- float item_height = get_item_height(item.get());
+ float item_height = get_item_height(item.get(), size.x);
prev_pos.y -= (item_height + spacing_y);
if(prev_pos.y + item_height + spacing_y <= start_y)
@@ -430,9 +427,8 @@ namespace QuickMedia {
break;
}
- update_dirty_state(item.get(), size);
item->last_drawn_time = elapsed_time_sec;
- float item_height = get_item_height(item.get());
+ float item_height = get_item_height(item.get(), size.x);
// This is needed here rather than above the loop, since update_dirty_text cant be called inside scissor because it corrupts the text for some reason
glEnable(GL_SCISSOR_TEST);
@@ -469,7 +465,7 @@ namespace QuickMedia {
}
}
- void Body::update_dirty_state(BodyItem *body_item, sf::Vector2f size) {
+ void Body::update_dirty_state(BodyItem *body_item, float width) {
if(body_item->dirty) {
body_item->dirty = false;
// TODO: Find a way to optimize fromUtf8
@@ -477,7 +473,7 @@ namespace QuickMedia {
if(body_item->title_text)
body_item->title_text->setString(std::move(str));
else
- body_item->title_text = std::make_unique<Text>(std::move(str), font, cjk_font, 16, size.x - 50 - image_padding_x * 2.0f);
+ body_item->title_text = std::make_unique<Text>(std::move(str), font, cjk_font, 16, width - 50 - image_padding_x * 2.0f);
body_item->title_text->setFillColor(body_item->get_title_color());
body_item->title_text->updateGeometry();
}
@@ -488,7 +484,7 @@ namespace QuickMedia {
if(body_item->description_text)
body_item->description_text->setString(std::move(str));
else
- body_item->description_text = std::make_unique<Text>(std::move(str), font, cjk_font, 14, size.x - 50 - image_padding_x * 2.0f);
+ body_item->description_text = std::make_unique<Text>(std::move(str), font, cjk_font, 14, width - 50 - image_padding_x * 2.0f);
body_item->description_text->setFillColor(body_item->get_description_color());
body_item->description_text->updateGeometry();
}
@@ -499,7 +495,7 @@ namespace QuickMedia {
if(body_item->author_text)
body_item->author_text->setString(std::move(str));
else
- body_item->author_text = std::make_unique<Text>(std::move(str), bold_font, cjk_font, 14, size.x - 50 - image_padding_x * 2.0f);
+ body_item->author_text = std::make_unique<Text>(std::move(str), bold_font, cjk_font, 14, width - 50 - image_padding_x * 2.0f);
body_item->author_text->setFillColor(body_item->get_author_color());
body_item->author_text->updateGeometry();
}
@@ -566,7 +562,7 @@ namespace QuickMedia {
}
void Body::draw_item(sf::RenderWindow &window, BodyItem *item, sf::Vector2f pos, sf::Vector2f size, bool include_embedded_item) {
- update_dirty_state(item, size);
+ update_dirty_state(item, size.x);
item->last_drawn_time = draw_timer.getElapsedTime().asMilliseconds();
sf::Vector2u window_size = window.getSize();
glEnable(GL_SCISSOR_TEST);
@@ -622,7 +618,7 @@ namespace QuickMedia {
}
float text_offset_x = padding_x;
- if(draw_thumbnails && !item->thumbnail_url.empty()) {
+ if(draw_thumbnails && item_thumbnail) {
double elapsed_time_thumbnail = 0.0;
if(item_thumbnail->loading_state == LoadingState::APPLIED_TO_TEXTURE)
elapsed_time_thumbnail = item_thumbnail->texture_applied_time.getElapsedTime().asSeconds(); //thumbnail_fade_duration_sec
@@ -678,7 +674,7 @@ namespace QuickMedia {
auto new_loading_icon_size = clamp_to_size(loading_icon_size, content_size);
loading_icon.setPosition(item_pos + sf::Vector2f(image_padding_x, padding_y) + (content_size * 0.5f));
loading_icon.setScale(get_ratio(loading_icon_size, new_loading_icon_size));
- loading_icon.setRotation(-elapsed_time_sec * 400.0);
+ loading_icon.setRotation(elapsed_time_sec * 400.0);
loading_icon.setColor(sf::Color(255, 255, 255, fallback_fade_alpha));
window.draw(loading_icon);
@@ -710,8 +706,9 @@ namespace QuickMedia {
}
if(include_embedded_item && item->embedded_item_status != FetchStatus::NONE) {
- float embedded_item_height = item->embedded_item ? get_item_height(item->embedded_item.get(), true, false) : (embedded_item_load_text.getLocalBounds().height + embedded_item_padding_y * 2.0f);
const float border_width = 4.0f;
+ const float embedded_item_width = std::floor(size.x - text_offset_x - border_width - padding_x);
+ float embedded_item_height = item->embedded_item ? get_item_height(item->embedded_item.get(), embedded_item_width, true, false) : (embedded_item_load_text.getLocalBounds().height + embedded_item_padding_y * 2.0f);
sf::RectangleShape border_left(sf::Vector2f(border_width, std::floor(embedded_item_height)));
border_left.setFillColor(sf::Color::White);
border_left.setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + 4.0f));
@@ -719,7 +716,7 @@ namespace QuickMedia {
if(item->embedded_item) {
sf::Vector2f embedded_item_pos(std::floor(item_pos.x + text_offset_x + border_width + padding_x), std::floor(item_pos.y + embedded_item_padding_y + 4.0f));
- sf::Vector2f embedded_item_size(std::floor(size.x - text_offset_x - border_width - padding_x), embedded_item_height);
+ sf::Vector2f embedded_item_size(embedded_item_width, embedded_item_height);
draw_item(window, item->embedded_item.get(), embedded_item_pos, embedded_item_size, false);
} else {
embedded_item_load_text.setString(embedded_item_status_to_string(item->embedded_item_status));
@@ -770,7 +767,9 @@ namespace QuickMedia {
}
}
- float Body::get_item_height(BodyItem *item, bool load_texture, bool include_embedded_item) {
+ float Body::get_item_height(BodyItem *item, float width, bool load_texture, bool include_embedded_item) {
+ if(load_texture)
+ update_dirty_state(item, width);
float item_height = 0.0f;
if(item->title_text) {
item_height += item->title_text->getHeight() - 2.0f;
@@ -780,7 +779,7 @@ namespace QuickMedia {
}
if(include_embedded_item && item->embedded_item_status != FetchStatus::NONE) {
if(item->embedded_item)
- item_height += (get_item_height(item->embedded_item.get(), load_texture, false) + 4.0f + embedded_item_padding_y * 2.0f);
+ item_height += (get_item_height(item->embedded_item.get(), width, load_texture, false) + 4.0f + embedded_item_padding_y * 2.0f);
else
item_height += (embedded_item_load_text.getLocalBounds().height + 4.0f + embedded_item_padding_y * 2.0f);
}
@@ -802,7 +801,7 @@ namespace QuickMedia {
item_thumbnail = item_thumbnail_it->second;
}
- if(load_texture) {
+ if(load_texture && item_thumbnail) {
item_thumbnail->referenced = true;
if(!item->thumbnail_url.empty() && item_thumbnail->loading_state == LoadingState::NOT_LOADED)
@@ -864,6 +863,8 @@ namespace QuickMedia {
body_item->visible = string_find_case_insensitive(body_item->get_title(), text);
if(!body_item->visible && !body_item->get_description().empty())
body_item->visible = string_find_case_insensitive(body_item->get_description(), text);
+ if(!body_item->visible && !body_item->get_author().empty())
+ body_item->visible = string_find_case_insensitive(body_item->get_author(), text);
}
bool Body::no_items_visible() const {
diff --git a/src/NetUtils.cpp b/src/NetUtils.cpp
index 4d5a940..f8b118b 100644
--- a/src/NetUtils.cpp
+++ b/src/NetUtils.cpp
@@ -45,13 +45,21 @@ namespace QuickMedia {
}
}
+ static bool is_alpha(char c) {
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+ }
+
+ static bool is_digit(char c) {
+ return c >= '0' && c <= '9';
+ }
+
std::string url_param_encode(const std::string &param) {
std::ostringstream result;
result.fill('0');
result << std::hex;
for(char c : param) {
- if(isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
+ if(is_alpha(c) || is_digit(c) || c == '-' || c == '_' || c == '.' || c == '~') {
result << c;
} else {
result << std::uppercase;
@@ -62,14 +70,6 @@ namespace QuickMedia {
return result.str();
}
- static bool is_alpha(char c) {
- return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
- }
-
- static bool is_digit(char c) {
- return c >= '0' && c <= '9';
- }
-
static bool is_url_character(char c) {
switch(c) {
case '%':
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index 054b3ed..c4532cd 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -392,6 +392,9 @@ namespace QuickMedia {
}
Program::~Program() {
+ images_to_upscale_queue.close();
+ if(image_upscale_thead.joinable())
+ image_upscale_thead.join();
if(matrix)
delete matrix;
if(disp)
@@ -509,14 +512,13 @@ namespace QuickMedia {
}
image_upscale_thead = std::thread([this]{
- CopyOp copy_op;
+ std::optional<CopyOp> copy_op_opt;
while(true) {
- {
- std::unique_lock<std::mutex> lock(image_upscale_mutex);
- while(images_to_upscale.empty()) image_upscale_cv.wait(lock);
- copy_op = images_to_upscale.front();
- images_to_upscale.pop_front();
- }
+ copy_op_opt = images_to_upscale_queue.pop_wait();
+ if(!copy_op_opt)
+ break;
+
+ CopyOp &copy_op = copy_op_opt.value();
Path tmp_file = copy_op.source;
tmp_file.append(".tmp.png");
@@ -538,7 +540,6 @@ namespace QuickMedia {
file_overwrite(copy_op.destination.data.c_str(), "1");
}
});
- image_upscale_thead.detach();
}
if(strcmp(plugin_name, "file-manager") != 0 && start_dir) {
@@ -615,14 +616,16 @@ namespace QuickMedia {
}
if(!tabs.empty()) {
- page_loop(std::move(tabs));
+ page_loop(tabs);
return exit_code;
}
if(matrix) {
matrix->use_tor = use_tor;
{
- auto window_size = window.getSize();
+ auto window_size_u = window.getSize();
+ window_size.x = window_size_u.x;
+ window_size.y = window_size_u.y;
sf::Text loading_text("Loading...", *font.get(), 24);
loading_text.setPosition(window_size.x * 0.5f - loading_text.getLocalBounds().width * 0.5f, window_size.y * 0.5f - loading_text.getLocalBounds().height * 0.5f);
window.clear(back_color);
@@ -634,21 +637,56 @@ namespace QuickMedia {
} else {
fprintf(stderr, "Failed to load session cache, redirecting to login page\n");
current_page = PageType::CHAT_LOGIN;
+ chat_login_page();
}
- while(window.isOpen()) {
- switch(current_page) {
- case PageType::CHAT_LOGIN:
- chat_login_page();
- break;
- case PageType::CHAT:
- chat_page();
- break;
- default:
+ if(!window.isOpen())
+ return exit_code;
+
+ auto rooms_body = create_body();
+ rooms_body->thumbnail_mask_shader = &circle_mask_shader;
+ auto matrix_rooms_page = std::make_unique<MatrixRoomsPage>(this, rooms_body.get(), "All rooms");
+
+ auto rooms_tags_body = create_body();
+ rooms_tags_body->thumbnail_mask_shader = &circle_mask_shader;
+ auto matrix_rooms_tag_page = std::make_unique<MatrixRoomTagsPage>(this, rooms_tags_body.get());
+
+ MatrixQuickMedia matrix_handler(this, matrix, matrix_rooms_page.get(), matrix_rooms_tag_page.get());
+ matrix->start_sync(&matrix_handler);
+
+ tabs.push_back(Tab{std::move(rooms_body), std::move(matrix_rooms_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ tabs.push_back(Tab{std::move(rooms_tags_body), std::move(matrix_rooms_tag_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+
+ sf::Sprite load_sprite(loading_icon);
+ sf::Vector2u loading_icon_size = loading_icon.getSize();
+ load_sprite.setOrigin(loading_icon_size.x * 0.5f, loading_icon_size.y * 0.5f);
+
+ sf::Clock timer;
+ sf::Event event;
+ while(window.isOpen() && !matrix->is_initial_sync_finished()) {
+ while(window.pollEvent(event)) {
+ if(event.type == sf::Event::Closed)
window.close();
- break;
+ else if(event.type == sf::Event::Resized) {
+ window_size.x = event.size.width;
+ window_size.y = event.size.height;
+ sf::FloatRect visible_area(0, 0, window_size.x, window_size.y);
+ window.setView(sf::View(visible_area));
+ }
}
+ window.clear(back_color);
+ load_sprite.setPosition(window_size.x * 0.5f - loading_icon_size.x * 0.5f, window_size.y * 0.5f - loading_icon_size.y * 0.5f);
+ load_sprite.setRotation(timer.getElapsedTime().asSeconds() * 400.0);
+ window.draw(load_sprite);
+ window.display();
+ }
+
+ while(window.isOpen()) {
+ page_loop(tabs);
}
+
+ exit(exit_code); // Exit immediately without waiting for anything to finish
+ //matrix->stop_sync();
}
return exit_code;
@@ -896,7 +934,9 @@ namespace QuickMedia {
}
std::unique_ptr<Body> Program::create_body() {
- return std::make_unique<Body>(this, font.get(), bold_font.get(), cjk_font.get(), loading_icon);
+ auto body = std::make_unique<Body>(this, font.get(), bold_font.get(), cjk_font.get(), loading_icon);
+ body->thumbnail_mask_shader = &circle_mask_shader;
+ return body;
}
std::unique_ptr<SearchBar> Program::create_search_bar(const std::string &placeholder, int search_delay) {
@@ -926,7 +966,15 @@ namespace QuickMedia {
selected_files.push_back(filepath);
}
- void Program::page_loop(std::vector<Tab> tabs) {
+ bool Program::is_window_focused() {
+ return window.hasFocus();
+ }
+
+ RoomData* Program::get_current_chat_room() {
+ return current_chat_room;
+ }
+
+ void Program::page_loop(std::vector<Tab> &tabs) {
if(tabs.empty()) {
show_notification("QuickMedia", "No tabs provided!", Urgency::CRITICAL);
return;
@@ -1047,24 +1095,27 @@ namespace QuickMedia {
}
}
window.setKeyRepeatEnabled(true);
- redraw = true;
} else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::IMAGE_BOARD_THREAD) {
current_page = PageType::IMAGE_BOARD_THREAD;
image_board_thread_page(static_cast<ImageBoardThreadPage*>(new_tabs[0].page.get()), new_tabs[0].body.get());
- redraw = true;
} else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::VIDEO) {
current_page = PageType::VIDEO_CONTENT;
video_content_page(new_tabs[0].page.get(), selected_item->url, selected_item->get_title());
- redraw = true;
+ } else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::CHAT) {
+ current_page = PageType::CHAT;
+ current_chat_room = matrix->get_room_by_id(selected_item->url);
+ chat_page(static_cast<MatrixChatPage*>(new_tabs[0].page.get()), current_chat_room);
+ current_chat_room = nullptr;
} else {
- page_loop(std::move(new_tabs));
- tabs[selected_tab].page->on_navigate_to_page();
- if(content_storage_json.isObject()) {
- const Json::Value &chapters_json = content_storage_json["chapters"];
- if(chapters_json.isObject())
- json_chapters = &chapters_json;
- }
+ page_loop(new_tabs);
+ }
+ tabs[selected_tab].page->on_navigate_to_page();
+ if(content_storage_json.isObject()) {
+ const Json::Value &chapters_json = content_storage_json["chapters"];
+ if(chapters_json.isObject())
+ json_chapters = &chapters_json;
}
+ redraw = true;
} else {
// TODO: Show the exact cause of error (get error message from curl).
// TODO: Make asynchronous
@@ -1118,9 +1169,6 @@ namespace QuickMedia {
while (window.isOpen() && loop_running) {
sf::Int32 frame_time_ms = frame_timer.restart().asMilliseconds();
- Tab &current_tab = tabs[selected_tab];
- TabAssociatedData &current_tab_associated_data = tab_associated_data[selected_tab];
-
while (window.pollEvent(event)) {
if (event.type == sf::Event::Closed) {
window.close();
@@ -1131,10 +1179,10 @@ namespace QuickMedia {
window.setView(sf::View(visible_area));
}
- if(current_tab.search_bar) {
+ if(tabs[selected_tab].search_bar) {
if(event.type == sf::Event::TextEntered)
- current_tab.search_bar->onTextEntered(event.text.unicode);
- current_tab.search_bar->on_event(event);
+ tabs[selected_tab].search_bar->onTextEntered(event.text.unicode);
+ tabs[selected_tab].search_bar->on_event(event);
}
if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus)
@@ -1144,26 +1192,26 @@ namespace QuickMedia {
bool hit_bottom = false;
switch(event.key.code) {
case sf::Keyboard::Down:
- hit_bottom = !current_tab.body->select_next_item();
+ hit_bottom = !tabs[selected_tab].body->select_next_item();
break;
case sf::Keyboard::PageDown:
- hit_bottom = !current_tab.body->select_next_page();
+ hit_bottom = !tabs[selected_tab].body->select_next_page();
break;
case sf::Keyboard::End:
- current_tab.body->select_last_item();
+ tabs[selected_tab].body->select_last_item();
hit_bottom = true;
break;
default:
hit_bottom = false;
break;
}
- if(hit_bottom && current_tab_associated_data.fetch_status == FetchStatus::NONE && !current_tab_associated_data.fetching_next_page_running && current_tab.page) {
+ if(hit_bottom && tab_associated_data[selected_tab].fetch_status == FetchStatus::NONE && !tab_associated_data[selected_tab].fetching_next_page_running && tabs[selected_tab].page) {
gradient_inc = 0.0;
- current_tab_associated_data.fetching_next_page_running = true;
- int next_page = current_tab_associated_data.fetched_page + 1;
- Page *page = current_tab.page.get();
- std::string update_search_text = current_tab_associated_data.update_search_text;
- current_tab_associated_data.next_page_future = std::async(std::launch::async, [update_search_text, next_page, page]() {
+ tab_associated_data[selected_tab].fetching_next_page_running = true;
+ int next_page = tab_associated_data[selected_tab].fetched_page + 1;
+ Page *page = tabs[selected_tab].page.get();
+ std::string update_search_text = tab_associated_data[selected_tab].update_search_text;
+ tab_associated_data[selected_tab].next_page_future = std::async(std::launch::async, [update_search_text, next_page, page]() {
BodyItems result_items;
if(page->get_page(update_search_text, next_page, result_items) != PluginResult::OK)
fprintf(stderr, "Failed to get next page (page %d)\n", next_page);
@@ -1171,33 +1219,33 @@ namespace QuickMedia {
});
}
} else if(event.key.code == sf::Keyboard::Up) {
- current_tab.body->select_previous_item();
+ tabs[selected_tab].body->select_previous_item();
} else if(event.key.code == sf::Keyboard::PageUp) {
- current_tab.body->select_previous_page();
+ tabs[selected_tab].body->select_previous_page();
} else if(event.key.code == sf::Keyboard::Home) {
- current_tab.body->select_first_item();
+ tabs[selected_tab].body->select_first_item();
} else if(event.key.code == sf::Keyboard::Escape) {
goto page_end;
} else if(event.key.code == sf::Keyboard::Left) {
if(selected_tab > 0) {
- current_tab.body->clear_cache();
+ tabs[selected_tab].body->clear_cache();
--selected_tab;
redraw = true;
}
} else if(event.key.code == sf::Keyboard::Right) {
if(selected_tab < (int)tabs.size() - 1) {
- current_tab.body->clear_cache();
+ tabs[selected_tab].body->clear_cache();
++selected_tab;
redraw = true;
}
} else if(event.key.code == sf::Keyboard::Tab) {
- if(current_tab.search_bar) current_tab.search_bar->set_to_autocomplete();
+ if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->set_to_autocomplete();
} else if(event.key.code == sf::Keyboard::Enter) {
- if(!current_tab.search_bar) submit_handler();
+ if(!tabs[selected_tab].search_bar) submit_handler();
} else if(event.key.code == sf::Keyboard::T && event.key.control) {
- BodyItem *selected_item = current_tab.body->get_selected();
- if(selected_item && current_tab.page && current_tab.page->is_trackable()) {
- TrackablePage *trackable_page = static_cast<TrackablePage*>(current_tab.page.get());
+ BodyItem *selected_item = tabs[selected_tab].body->get_selected();
+ if(selected_item && tabs[selected_tab].page && tabs[selected_tab].page->is_trackable()) {
+ TrackablePage *trackable_page = static_cast<TrackablePage*>(tabs[selected_tab].page.get());
TrackResult track_result = trackable_page->track(selected_item->get_title());
// TODO: Show proper error message when this fails. For example if we are already tracking the manga
if(track_result == TrackResult::OK) {
@@ -1212,9 +1260,9 @@ namespace QuickMedia {
if(redraw) {
redraw = false;
- if(current_tab.search_bar) current_tab.search_bar->onWindowResize(window_size);
+ if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->onWindowResize(window_size);
// TODO: Dont show tabs if there is only one tab
- get_body_dimensions(window_size, current_tab.search_bar.get(), body_pos, body_size, true);
+ get_body_dimensions(window_size, tabs[selected_tab].search_bar.get(), body_pos, body_size, true);
gradient_points[0].position.x = 0.0f;
gradient_points[0].position.y = window_size.y - gradient_height;
@@ -1229,14 +1277,14 @@ namespace QuickMedia {
gradient_points[3].position.y = window_size.y;
}
- if(current_tab.search_bar) current_tab.search_bar->update();
+ if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->update();
- if(current_tab.page->is_lazy_fetch_page() && current_tab_associated_data.fetch_status == FetchStatus::NONE && !current_tab_associated_data.lazy_fetch_finished) {
- current_tab_associated_data.fetch_status = FetchStatus::LOADING;
- current_tab_associated_data.fetch_type = FetchType::LAZY;
- current_tab_associated_data.search_result_text.setString("Fetching page...");
- LazyFetchPage *lazy_fetch_page = static_cast<LazyFetchPage*>(current_tab.page.get());
- current_tab_associated_data.fetch_future = std::async(std::launch::async, [lazy_fetch_page]() {
+ if(tabs[selected_tab].page->is_lazy_fetch_page() && tab_associated_data[selected_tab].fetch_status == FetchStatus::NONE && !tab_associated_data[selected_tab].lazy_fetch_finished) {
+ tab_associated_data[selected_tab].fetch_status = FetchStatus::LOADING;
+ tab_associated_data[selected_tab].fetch_type = FetchType::LAZY;
+ tab_associated_data[selected_tab].search_result_text.setString("Fetching page...");
+ LazyFetchPage *lazy_fetch_page = static_cast<LazyFetchPage*>(tabs[selected_tab].page.get());
+ tab_associated_data[selected_tab].fetch_future = std::async(std::launch::async, [lazy_fetch_page]() {
FetchResult fetch_result;
fetch_result.result = lazy_fetch_page->lazy_fetch(fetch_result.body_items);
return fetch_result;
@@ -1246,6 +1294,8 @@ namespace QuickMedia {
for(size_t i = 0; i < tabs.size(); ++i) {
TabAssociatedData &associated_data = tab_associated_data[i];
+ tabs[i].page->update();
+
if(associated_data.fetching_next_page_running && is_future_ready(associated_data.next_page_future)) {
BodyItems new_body_items = associated_data.next_page_future.get();
fprintf(stderr, "Finished fetching page %d, num new messages: %zu\n", associated_data.fetched_page + 1, new_body_items.size());
@@ -1304,18 +1354,18 @@ namespace QuickMedia {
}
window.clear(back_color);
- if(current_tab.search_bar) current_tab.search_bar->draw(window, false);
+ if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->draw(window, false);
{
float shade_extra_height = 0.0f;
- if(!current_tab.search_bar)
+ if(!tabs[selected_tab].search_bar)
shade_extra_height = 10.0f;
const float width_per_tab = window_size.x / tabs.size();
tab_background.setSize(sf::Vector2f(std::floor(width_per_tab - tab_margin_x * 2.0f), tab_height));
- float tab_vertical_offset = current_tab.search_bar ? current_tab.search_bar->getBottomWithoutShadow() : 0.0f;
- current_tab.body->draw(window, body_pos, body_size, *json_chapters);
+ float tab_vertical_offset = tabs[selected_tab].search_bar ? tabs[selected_tab].search_bar->getBottomWithoutShadow() : 0.0f;
+ tabs[selected_tab].body->draw(window, body_pos, body_size, *json_chapters);
const float tab_y = tab_spacer_height + std::floor(tab_vertical_offset + tab_height * 0.5f - (tab_text_size + 5.0f) * 0.5f) + shade_extra_height;
tab_shade.setPosition(0.0f, tab_spacer_height + std::floor(tab_vertical_offset));
@@ -1338,7 +1388,7 @@ namespace QuickMedia {
}
}
- if(current_tab_associated_data.fetching_next_page_running) {
+ if(tab_associated_data[selected_tab].fetching_next_page_running) {
double progress = 0.5 + std::sin(std::fmod(gradient_inc, 360.0) * 0.017453292519943295 - 1.5707963267948966*0.5) * 0.5;
gradient_inc += (frame_time_ms * 0.5);
sf::Color bottom_color = interpolate_colors(back_color, sf::Color(175, 180, 188), progress);
@@ -1350,12 +1400,12 @@ namespace QuickMedia {
window.draw(gradient_points, 4, sf::Quads); // Note: sf::Quads doesn't work with egl
}
- if(!current_tab_associated_data.search_result_text.getString().isEmpty()) {
- auto search_result_text_bounds = current_tab_associated_data.search_result_text.getLocalBounds();
- current_tab_associated_data.search_result_text.setPosition(
+ if(!tab_associated_data[selected_tab].search_result_text.getString().isEmpty()) {
+ auto search_result_text_bounds = tab_associated_data[selected_tab].search_result_text.getLocalBounds();
+ tab_associated_data[selected_tab].search_result_text.setPosition(
std::floor(body_pos.x + body_size.x * 0.5f - search_result_text_bounds.width * 0.5f),
std::floor(body_pos.y + body_size.y * 0.5f - search_result_text_bounds.height * 0.5f));
- window.draw(current_tab_associated_data.search_result_text);
+ window.draw(tab_associated_data[selected_tab].search_result_text);
}
window.display();
@@ -1785,7 +1835,7 @@ namespace QuickMedia {
if(!video_loaded) {
window.clear(back_color);
load_sprite.setPosition(window_size.x * 0.5f - loading_icon_size.x * 0.5f, window_size.y * 0.5f - loading_icon_size.y * 0.5f);
- load_sprite.setRotation(-time_watched_timer.getElapsedTime().asSeconds() * 400.0);
+ load_sprite.setRotation(time_watched_timer.getElapsedTime().asSeconds() * 400.0);
window.draw(load_sprite);
window.display();
continue;
@@ -1998,9 +2048,7 @@ namespace QuickMedia {
CopyOp copy_op;
copy_op.source = image_filepath_tmp;
copy_op.destination = image_filepath;
- std::unique_lock<std::mutex> lock(image_upscale_mutex);
- images_to_upscale.push_back(std::move(copy_op));
- image_upscale_cv.notify_one();
+ images_to_upscale_queue.push(std::move(copy_op));
} else {
fprintf(stderr, "Info: not upscaling %s because the file is already large on your monitor (screen height: %d, image height: %d)\n", image_filepath_tmp.data.c_str(), screen_height, image_height);
image_upscale_status[image_index] = 1;
@@ -2014,9 +2062,7 @@ namespace QuickMedia {
CopyOp copy_op;
copy_op.source = image_filepath_tmp;
copy_op.destination = image_filepath;
- std::unique_lock<std::mutex> lock(image_upscale_mutex);
- images_to_upscale.push_back(std::move(copy_op));
- image_upscale_cv.notify_one();
+ images_to_upscale_queue.push(std::move(copy_op));
}
if(rename_immediately) {
@@ -2238,8 +2284,7 @@ namespace QuickMedia {
image_download_future.get();
image_download_cancel = false;
}
- std::unique_lock<std::mutex> lock(image_upscale_mutex);
- images_to_upscale.clear();
+ images_to_upscale_queue.clear();
image_upscale_status.clear();
}
return page_navigation;
@@ -2328,8 +2373,7 @@ namespace QuickMedia {
image_download_future.get();
image_download_cancel = false;
}
- std::unique_lock<std::mutex> lock(image_upscale_mutex);
- images_to_upscale.clear();
+ images_to_upscale_queue.clear();
image_upscale_status.clear();
}
}
@@ -2459,12 +2503,12 @@ namespace QuickMedia {
}
};
- comment_input.on_submit_callback = [&post_comment_future, &navigation_stage, &request_new_google_captcha_challenge, &comment_to_post, &captcha_post_id, &captcha_solved_time, &post_comment, &thread_page](const std::string &text) -> bool {
+ comment_input.on_submit_callback = [&post_comment_future, &navigation_stage, &request_new_google_captcha_challenge, &comment_to_post, &captcha_post_id, &captcha_solved_time, &post_comment, &thread_page](std::string text) -> bool {
if(text.empty())
return false;
assert(navigation_stage == NavigationStage::REPLYING);
- comment_to_post = text;
+ comment_to_post = std::move(text);
if(!captcha_post_id.empty() && captcha_solved_time.getElapsedTime().asSeconds() < 120) {
post_comment_future = std::async(std::launch::async, [&post_comment]() -> bool {
post_comment();
@@ -2909,19 +2953,6 @@ namespace QuickMedia {
sf::Text text;
};
- static std::string extract_first_line(const std::string &str, size_t max_length) {
- size_t index = str.find('\n');
- if(index == std::string::npos) {
- if(str.size() > max_length)
- return str.substr(0, max_length) + " (...)";
- return str;
- } else if(index == 0) {
- return "";
- } else {
- return str.substr(0, std::min(index, max_length)) + " (...)";
- }
- }
-
static std::string remove_reply_formatting(const std::string &str) {
if(strncmp(str.c_str(), "> <@", 4) == 0) {
size_t index = str.find("> ", 4);
@@ -2982,9 +3013,10 @@ namespace QuickMedia {
struct PinnedEventData {
std::string event_id;
FetchStatus status = FetchStatus::NONE;
+ Message *message = nullptr;
};
- void Program::chat_page() {
+ void Program::chat_page(MatrixChatPage *chat_page, RoomData *current_room) {
assert(strcmp(plugin_name, "matrix") == 0);
auto video_page = std::make_unique<MatrixVideoPage>(this);
@@ -2996,7 +3028,7 @@ namespace QuickMedia {
pinned_tab.body->thumbnail_max_size = CHAT_MESSAGE_THUMBNAIL_MAX_SIZE;
pinned_tab.body->thumbnail_mask_shader = &circle_mask_shader;
//pinned_tab.body->line_separator_color = sf::Color::Transparent;
- pinned_tab.text = sf::Text("Pinned", *font, tab_text_size);
+ pinned_tab.text = sf::Text("Pinned messages", *font, tab_text_size);
tabs.push_back(std::move(pinned_tab));
ChatTab messages_tab;
@@ -3007,127 +3039,12 @@ namespace QuickMedia {
messages_tab.text = sf::Text("Messages", *font, tab_text_size);
tabs.push_back(std::move(messages_tab));
- ChatTab rooms_tab;
- rooms_tab.body = std::make_unique<Body>(this, font.get(), bold_font.get(), cjk_font.get(), loading_icon);
- //rooms_tab.body->line_separator_color = sf::Color::Transparent;
- rooms_tab.body->thumbnail_mask_shader = &circle_mask_shader;
- rooms_tab.text = sf::Text("Rooms", *font, tab_text_size);
- tabs.push_back(std::move(rooms_tab));
-
const int PINNED_TAB_INDEX = 0;
const int MESSAGES_TAB_INDEX = 1;
- const int ROOMS_TAB_INDEX = 2;
int selected_tab = MESSAGES_TAB_INDEX;
-
- // This is needed to get initial data, with joined rooms etc. TODO: Remove this once its cached
- // and allow asynchronous update of rooms
- bool synced = false;
- RoomData *current_room = nullptr;
bool is_window_focused = window.hasFocus();
- // Returns -1 if no rooms or no unread rooms
- auto find_top_body_position_for_unread_room = [&tabs](BodyItem *item_to_swap, int start_index) {
- for(int i = start_index; i < (int)tabs[ROOMS_TAB_INDEX].body->items.size(); ++i) {
- const auto &body_item = tabs[ROOMS_TAB_INDEX].body->items[i];
- if(static_cast<RoomData*>(body_item->userdata)->last_message_read || body_item.get() == item_to_swap)
- return i;
- }
- return -1;
- };
-
- // Returns -1 if no rooms or all rooms have unread mentions
- auto find_top_body_position_for_mentioned_room = [&tabs](BodyItem *item_to_swap, int start_index) {
- for(int i = start_index; i < (int)tabs[ROOMS_TAB_INDEX].body->items.size(); ++i) {
- const auto &body_item = tabs[ROOMS_TAB_INDEX].body->items[i];
- if(!static_cast<RoomData*>(body_item->userdata)->has_unread_mention || body_item.get() == item_to_swap)
- return i;
- }
- return -1;
- };
-
- auto process_new_room_messages =
- [this, &selected_tab, &current_room, &is_window_focused, &tabs, &find_top_body_position_for_unread_room, &find_top_body_position_for_mentioned_room]
- (RoomSyncData &room_sync_data, bool is_first_sync) mutable
- {
- for(auto &[room, sync_data] : room_sync_data) {
- for(auto &message : sync_data.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_first_sync || selected_tab == ROOMS_TAB_INDEX)
- show_notification("QuickMedia matrix - " + matrix->message_get_author_displayname(message.get()) + " (" + room->get_name() + ")", message->body);
- }
- }
- }
-
- for(auto &[room, sync_data] : room_sync_data) {
- if(sync_data.messages.empty())
- continue;
-
- std::shared_ptr<UserInfo> 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 &message : sync_data.messages) {
- if(message->related_event_type != RelatedEventType::EDIT && message->related_event_type != RelatedEventType::REDACTION && message->timestamp > read_marker_message_timestamp)
- last_unread_message = message.get();
- }
-
- if(!last_unread_message && !is_first_sync)
- continue;
-
- BodyItem *room_body_item = static_cast<BodyItem*>(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(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;
-
- // Swap order of rooms in body list to put rooms with mentions at the top and then unread messages and then all the other rooms
- // TODO: Optimize with hash map instead of linear search? or cache the index
- Body *rooms_body = tabs[ROOMS_TAB_INDEX].body.get();
- int room_body_index = rooms_body->get_index_by_body_item(room_body_item);
- if(room_body_index != -1) {
- std::shared_ptr<BodyItem> body_item = rooms_body->items[room_body_index];
- int body_swap_index = -1;
- if(room->has_unread_mention)
- body_swap_index = find_top_body_position_for_mentioned_room(body_item.get(), 0);
- else if(!room->last_message_read)
- body_swap_index = find_top_body_position_for_unread_room(body_item.get(), 0);
- if(body_swap_index != -1 && body_swap_index != room_body_index) {
- rooms_body->items.erase(rooms_body->items.begin() + room_body_index);
- if(body_swap_index < room_body_index)
- rooms_body->items.insert(rooms_body->items.begin() + body_swap_index, std::move(body_item));
- else
- rooms_body->items.insert(rooms_body->items.begin() + (body_swap_index - 1), std::move(body_item));
- }
- }
- } else if(is_first_sync) {
- Message *last_unread_message = nullptr;
- for(auto it = sync_data.messages.rbegin(), end = sync_data.messages.rend(); it != end; ++it) {
- if((*it)->related_event_type != RelatedEventType::EDIT && (*it)->related_event_type != RelatedEventType::REDACTION) {
- last_unread_message = (*it).get();
- break;
- }
- }
- if(last_unread_message)
- room_body_item->set_description(matrix->message_get_author_displayname(last_unread_message) + ": " + extract_first_line(last_unread_message->body, 150));
- }
- }
- };
-
enum class ChatState {
NAVIGATING,
TYPING_MESSAGE,
@@ -3197,6 +3114,8 @@ namespace QuickMedia {
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)
set_body_as_deleted(message.get(), body_item.get());
it = unreferenced_events.erase(it);
@@ -3211,6 +3130,9 @@ namespace QuickMedia {
// TODO: Optimize with hash map?
auto modify_related_messages_in_current_room = [&set_body_as_deleted, &unreferenced_event_by_room, &current_room, &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) {
@@ -3219,6 +3141,8 @@ namespace QuickMedia {
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)
set_body_as_deleted(message.get(), body_item.get());
} else {
@@ -3228,9 +3152,16 @@ namespace QuickMedia {
}
};
- auto process_new_pinned_events = [&tabs](const std::vector<std::string> &pinned_events) {
+ auto process_pinned_events = [&tabs](const std::optional<std::vector<std::string>> &pinned_events) {
+ if(!pinned_events || pinned_events->empty())
+ return;
+
+ bool empty_before = tabs[PINNED_TAB_INDEX].body->items.empty();
+ int selected_before = tabs[PINNED_TAB_INDEX].body->get_selected_item();
+ tabs[PINNED_TAB_INDEX].body->items.clear();
+
// TODO: Add message to rooms messages when there are new pinned events
- for(const std::string &event : pinned_events) {
+ for(const std::string &event : pinned_events.value()) {
auto body = BodyItem::create("");
body->set_description("Loading message...");
PinnedEventData *event_data = new PinnedEventData();
@@ -3239,62 +3170,37 @@ namespace QuickMedia {
body->userdata = event_data;
tabs[PINNED_TAB_INDEX].body->items.push_back(std::move(body));
}
- };
- SearchBar room_search_bar(*font, &plugin_logo, "Search...");
- room_search_bar.autocomplete_search_delay = SEARCH_DELAY_FILTER;
- room_search_bar.onTextUpdateCallback = [&tabs](const std::string &text) {
- tabs[ROOMS_TAB_INDEX].body->filter_search_fuzzy(text);
- //tabs[ROOMS_TAB_INDEX].body->select_first_item();
+ if(empty_before)
+ tabs[PINNED_TAB_INDEX].body->select_last_item();
+ else
+ tabs[PINNED_TAB_INDEX].body->set_selected_item(selected_before);
};
- room_search_bar.onTextSubmitCallback =
- [this, &tabs, &selected_tab, &current_room, &room_name_text,
- &modify_related_messages_in_current_room, &process_new_pinned_events, &room_avatar_thumbnail_data,
- &read_marker_timeout_ms, &redraw, &room_search_bar]
- (const std::string&)
- {
- BodyItem *selected_item = tabs[ROOMS_TAB_INDEX].body->get_selected();
- if(!selected_item)
- return;
+ Body url_selection_body(this, font.get(), bold_font.get(), cjk_font.get(), loading_icon);
+
+ Messages all_messages;
+ matrix->get_all_synced_room_messages(current_room, all_messages);
+ 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();
- tabs[ROOMS_TAB_INDEX].body->clear_cache();
+ std::vector<std::string> pinned_events;
+ matrix->get_all_pinned_events(current_room, pinned_events);
+ process_pinned_events(pinned_events);
+ tabs[PINNED_TAB_INDEX].body->select_last_item();
- current_room = (RoomData*)selected_item->userdata;
- assert(current_room);
- selected_tab = MESSAGES_TAB_INDEX;
- tabs[MESSAGES_TAB_INDEX].body->clear_items();
+ room_name_text.setString(static_cast<BodyItem*>(current_room->userdata)->get_title());
+ room_avatar_thumbnail_data = std::make_shared<ThumbnailData>();
- for(auto &body_item : tabs[PINNED_TAB_INDEX].body->items) {
- delete((PinnedEventData*)body_item->userdata);
- }
- tabs[PINNED_TAB_INDEX].body->clear_items();
-
- Messages all_messages;
- matrix->get_all_synced_room_messages(current_room, all_messages);
- 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();
-
- std::vector<std::string> pinned_events;
- matrix->get_all_pinned_events(current_room, pinned_events);
- process_new_pinned_events(pinned_events);
- tabs[PINNED_TAB_INDEX].body->select_last_item();
-
- room_name_text.setString(static_cast<BodyItem*>(current_room->userdata)->get_title());
- room_avatar_thumbnail_data = std::make_shared<ThumbnailData>();
-
- read_marker_timeout_ms = 0;
- redraw = true;
- room_search_bar.clear();
- tabs[ROOMS_TAB_INDEX].body->filter_search_fuzzy("");
- };
+ read_marker_timeout_ms = 0;
+ redraw = true;
Entry chat_input("Press m to begin writing a message...", font.get(), cjk_font.get());
chat_input.draw_background = false;
chat_input.set_editable(false);
- chat_input.on_submit_callback = [this, &tabs, &chat_input, &selected_tab, &current_room, &new_page, &chat_state, &currently_operating_on_item](const std::string &text) mutable {
+ chat_input.on_submit_callback = [this, &tabs, &chat_input, &selected_tab, &current_room, &new_page, &chat_state, &currently_operating_on_item](std::string text) mutable {
if(!current_room)
return false;
@@ -3302,27 +3208,28 @@ namespace QuickMedia {
if(text.empty())
return false;
+ std::string msgtype;
if(chat_state == ChatState::TYPING_MESSAGE && text[0] == '/') {
- std::string command = strip(text);
- if(command == "/upload") {
+ if(text == "/upload") {
new_page = PageType::FILE_MANAGER;
chat_input.set_editable(false);
chat_state = ChatState::NAVIGATING;
return true;
- } else if(command == "/logout") {
- new_page = PageType::CHAT_LOGIN;
- chat_input.set_editable(false);
- chat_state = ChatState::NAVIGATING;
+ } else if(text == "/logout") {
+ show_notification("QuickMedia", "/logout command is temporary disabled. Delete " + get_storage_dir().join("matrix").join("session.json").data + " and restart QuickMedia to logout", Urgency::CRITICAL);
return true;
+ } else if(strncmp(text.c_str(), "/me ", 4) == 0) {
+ msgtype = "m.emote";
+ text.erase(text.begin(), text.begin() + 4);
} else {
- fprintf(stderr, "Error: invalid command: %s, expected /upload\n", command.c_str());
+ fprintf(stderr, "Error: invalid command: %s, expected /upload, /logout or /me\n", text.c_str());
return false;
}
}
if(chat_state == ChatState::TYPING_MESSAGE) {
// TODO: Make asynchronous
- if(matrix->post_message(current_room, text, std::nullopt, std::nullopt) == PluginResult::OK) {
+ if(matrix->post_message(current_room, text, std::nullopt, std::nullopt, msgtype) == PluginResult::OK) {
chat_input.set_editable(false);
chat_state = ChatState::NAVIGATING;
if(tabs[MESSAGES_TAB_INDEX].body->is_last_item_fully_visible())
@@ -3361,16 +3268,6 @@ namespace QuickMedia {
return false;
};
- struct SyncFutureResult {
- Rooms rooms;
- RoomSyncData room_sync_data;
- };
-
- std::future<SyncFutureResult> sync_future;
- bool sync_running = false;
- sf::Clock sync_timer;
- sf::Int32 sync_min_time_ms = 0; // Sync immediately the first time
-
std::future<Messages> previous_messages_future;
bool fetching_previous_messages_running = false;
RoomData *previous_messages_future_room = nullptr;
@@ -3397,6 +3294,7 @@ namespace QuickMedia {
if(related_body_item) {
*body_item = *related_body_item;
event_data->status = FetchStatus::FINISHED_LOADING;
+ event_data->message = static_cast<Message*>(related_body_item->userdata);
body_item->userdata = event_data;
return;
}
@@ -3405,7 +3303,7 @@ namespace QuickMedia {
std::string message_event_id = event_data->event_id;
fetch_future_room = current_room;
fetch_body_item = body_item;
- body_item->embedded_item_status = FetchStatus::LOADING;
+ event_data->status = FetchStatus::LOADING;
fetch_message_tab = PINNED_TAB_INDEX;
// TODO: Check if the message is already cached before calling async? is this needed? is async creation expensive?
fetch_message_future = std::async(std::launch::async, [this, &fetch_future_room, message_event_id]() {
@@ -3488,8 +3386,6 @@ namespace QuickMedia {
const float chat_input_padding_x = 10.0f;
const float chat_input_padding_y = 10.0f;
- Body url_selection_body(this, font.get(), bold_font.get(), cjk_font.get(), loading_icon);
-
auto launch_url = [this, &video_page, &redraw](const std::string &url) mutable {
if(url.empty())
return;
@@ -3520,6 +3416,9 @@ namespace QuickMedia {
};
auto add_new_messages_to_current_room = [this, &tabs, &selected_tab, &current_room](Messages &messages) {
+ if(messages.empty())
+ return;
+
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)
@@ -3536,48 +3435,68 @@ namespace QuickMedia {
}
};
- auto add_new_rooms = [&tabs, &current_room, &room_search_bar, &room_name_text](Rooms &rooms) {
- if(rooms.empty())
- return;
+ auto display_url_or_image = [this, &selected_tab, &redraw, &video_page, &launch_url, &chat_state, &url_selection_body](BodyItem *selected) {
+ if(!selected)
+ return false;
- std::string search_filter_text = room_search_bar.get_text();
-
- for(size_t i = 0; i < rooms.size(); ++i) {
- auto &room = rooms[i];
- std::string room_name = room->get_name();
- if(room_name.empty())
- room_name = room->id;
-
- auto body_item = BodyItem::create(std::move(room_name));
- body_item->thumbnail_url = room->get_avatar_url();
- body_item->userdata = room; // Note: this has to be valid as long as the room list is valid!
- body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE;
- body_item->thumbnail_size = sf::Vector2i(32, 32);
- tabs[ROOMS_TAB_INDEX].body->filter_search_fuzzy_item(search_filter_text, body_item.get());
- tabs[ROOMS_TAB_INDEX].body->items.push_back(body_item);
- room->userdata = body_item.get();
- }
+ Message *selected_item_message = nullptr;
+ if(selected_tab == MESSAGES_TAB_INDEX) {
+ selected_item_message = static_cast<Message*>(selected->userdata);
+ } else if(selected_tab == PINNED_TAB_INDEX && static_cast<PinnedEventData*>(selected->userdata)->status == FetchStatus::FINISHED_LOADING) {
+ selected_item_message = static_cast<PinnedEventData*>(selected->userdata)->message;
+ }
+
+ if(selected_item_message) {
+ MessageType message_type = selected_item_message->type;
+ std::string *selected_url = &selected->url;
+ if(!selected_url->empty()) {
+ if(message_type == MessageType::VIDEO || message_type == MessageType::IMAGE || message_type == MessageType::AUDIO) {
+ page_stack.push(PageType::CHAT);
+ watched_videos.clear();
+ current_page = PageType::VIDEO_CONTENT;
+ bool is_audio = (message_type == MessageType::AUDIO);
+ bool prev_no_video = no_video;
+ no_video = is_audio;
+ // TODO: Add title
+ video_content_page(video_page.get(), *selected_url, "No title");
+ no_video = prev_no_video;
+ redraw = true;
+ return true;
+ }
- if(current_room)
- return;
+ launch_url(*selected_url);
+ return true;
+ }
+ }
- current_room = rooms[0];
- room_name_text.setString(static_cast<BodyItem*>(current_room->userdata)->get_title());
+ // TODO: If content type is a file, show file-manager prompt where it should be saved and asynchronously save it instead
+ std::vector<std::string> urls;
+ extract_urls(selected->get_description(), urls);
+ if(urls.size() == 1) {
+ launch_url(urls[0]);
+ return true;
+ } else if(urls.size() > 1) {
+ chat_state = ChatState::URL_SELECTION;
+ url_selection_body.clear_items();
+ for(const std::string &url : urls) {
+ auto body_item = BodyItem::create(url);
+ url_selection_body.items.push_back(std::move(body_item));
+ }
+ return true;
+ }
+ return false;
};
float tab_shade_height = 0.0f;
+ bool frame_skip_text_entry = false;
+
+ SyncData sync_data;
while (current_page == PageType::CHAT) {
sf::Int32 frame_time_ms = frame_timer.restart().asMilliseconds();
while (window.pollEvent(event)) {
base_event_handler(event, PageType::EXIT, tabs[selected_tab].body.get(), nullptr, false, false);
- if(selected_tab == ROOMS_TAB_INDEX) {
- if(event.type == sf::Event::TextEntered)
- room_search_bar.onTextEntered(event.text.unicode);
- room_search_bar.on_event(event);
- }
-
if(event.type == sf::Event::GainedFocus) {
is_window_focused = true;
redraw = true;
@@ -3620,7 +3539,7 @@ namespace QuickMedia {
tabs[selected_tab].body->select_next_page();
} else if(event.key.code == sf::Keyboard::End) {
tabs[selected_tab].body->select_last_item();
- } else if((event.key.code == sf::Keyboard::Left) && synced && selected_tab > 0) {
+ } else if((event.key.code == sf::Keyboard::Left) && selected_tab > 0) {
tabs[selected_tab].body->clear_cache();
--selected_tab;
read_marker_timer.restart();
@@ -3630,7 +3549,7 @@ namespace QuickMedia {
typing = false;
typing_futures.push_back(std::async(typing_async_func, false, current_room));
}
- } else if((event.key.code == sf::Keyboard::Right) && synced && selected_tab < (int)tabs.size() - 1) {
+ } else if((event.key.code == sf::Keyboard::Right) && selected_tab < (int)tabs.size() - 1) {
tabs[selected_tab].body->clear_cache();
++selected_tab;
read_marker_timer.restart();
@@ -3640,50 +3559,93 @@ namespace QuickMedia {
typing = false;
typing_futures.push_back(std::async(typing_async_func, false, current_room));
}
+ } else if(event.key.code == sf::Keyboard::Escape) {
+ goto chat_page_end;
}
- if(selected_tab == MESSAGES_TAB_INDEX && event.key.code == sf::Keyboard::Enter) {
+ if((selected_tab == MESSAGES_TAB_INDEX || selected_tab == PINNED_TAB_INDEX) && event.key.code == sf::Keyboard::Enter) {
BodyItem *selected = tabs[selected_tab].body->get_selected();
if(selected) {
- MessageType message_type = static_cast<Message*>(selected->userdata)->type;
- std::string selected_url = selected->url;
- if(selected_url.empty() && selected->embedded_item) {
- selected_url = selected->embedded_item->url;
- message_type = static_cast<Message*>(selected->embedded_item->userdata)->type;
+ if(!display_url_or_image(selected))
+ display_url_or_image(selected->embedded_item.get());
+ }
+ }
+
+ if(selected_tab == MESSAGES_TAB_INDEX && current_room) {
+ if(event.key.code == sf::Keyboard::U) {
+ frame_skip_text_entry = true;
+ new_page = PageType::FILE_MANAGER;
+ chat_input.set_editable(false);
+ }
+
+ if(event.key.code == sf::Keyboard::M) {
+ frame_skip_text_entry = true;
+ chat_input.set_editable(true);
+ chat_state = ChatState::TYPING_MESSAGE;
+ }
+
+ if(event.key.control && event.key.code == sf::Keyboard::V) {
+ frame_skip_text_entry = true;
+ // TODO: Make asynchronous.
+ // TODO: Upload multiple files.
+ std::string err_msg;
+ if(matrix->post_file(current_room, sf::Clipboard::getString(), err_msg) != PluginResult::OK) {
+ std::string desc = "Failed to upload media to room, error: " + err_msg;
+ show_notification("QuickMedia", desc.c_str(), Urgency::CRITICAL);
}
- if(!selected_url.empty()) {
- if(message_type == MessageType::VIDEO || message_type == MessageType::IMAGE || message_type == MessageType::AUDIO) {
- page_stack.push(PageType::CHAT);
- watched_videos.clear();
- current_page = PageType::VIDEO_CONTENT;
- bool is_audio = (message_type == MessageType::AUDIO);
- bool prev_no_video = no_video;
- no_video = is_audio;
- // TODO: Add title
- video_content_page(video_page.get(), selected_url, "No title");
- no_video = prev_no_video;
- redraw = true;
- continue;
- }
+ }
+
+ if(event.key.code == sf::Keyboard::R) {
+ frame_skip_text_entry = true;
+ std::shared_ptr<BodyItem> selected = tabs[selected_tab].body->get_selected_shared();
+ if(selected) {
+ 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
+ show_notification("QuickMedia", "No message selected for replying");
+ }
+ }
- launch_url(selected_url);
- continue;
+ if(event.key.code == sf::Keyboard::E) {
+ frame_skip_text_entry = true;
+ std::shared_ptr<BodyItem> selected = tabs[selected_tab].body->get_selected_shared();
+ if(selected) {
+ 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");
+ } 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");
+ } else {
+ chat_state = ChatState::EDITING;
+ currently_operating_on_item = selected;
+ chat_input.set_editable(true);
+ chat_input.set_text(selected->get_description()); // TODO: Description? it may change in the future, in which case this should be edited
+ chat_input.move_caret_to_end();
+ replying_to_text.setString("Editing message:");
+ }
+ } else {
+ // TODO: Show inline notification
+ show_notification("QuickMedia", "No message selected for editing");
}
+ }
- // TODO: If content type is a file, show file-manager prompt where it should be saved and asynchronously save it instead
- std::vector<std::string> urls;
- extract_urls(selected->get_description(), urls);
- if(selected->embedded_item)
- extract_urls(selected->embedded_item->get_description(), urls);
- if(urls.size() == 1) {
- launch_url(urls[0]);
- } else if(urls.size() > 1) {
- chat_state = ChatState::URL_SELECTION;
- url_selection_body.clear_items();
- for(const std::string &url : urls) {
- auto body_item = BodyItem::create(url);
- url_selection_body.items.push_back(std::move(body_item));
+ if(event.key.code == sf::Keyboard::D) {
+ frame_skip_text_entry = true;
+ BodyItem *selected = tabs[selected_tab].body->get_selected();
+ if(selected) {
+ // TODO: Make asynchronous
+ std::string err_msg;
+ if(matrix->delete_message(current_room, selected->userdata, err_msg) != PluginResult::OK) {
+ // TODO: Show inline notification
+ show_notification("QuickMedia", "Failed to delete message, reason: " + err_msg, Urgency::CRITICAL);
}
+ } else {
+ // TODO: Show inline notification
+ show_notification("QuickMedia", "No message selected for deletion");
}
}
}
@@ -3719,80 +3681,10 @@ namespace QuickMedia {
continue;
launch_url(selected_item->get_title());
}
- } else if(event.type == sf::Event::KeyReleased && chat_state == ChatState::NAVIGATING && selected_tab == MESSAGES_TAB_INDEX && current_room) {
- if(event.key.code == sf::Keyboard::U) {
- new_page = PageType::FILE_MANAGER;
- chat_input.set_editable(false);
- }
-
- if(event.key.code == sf::Keyboard::M) {
- chat_input.set_editable(true);
- chat_state = ChatState::TYPING_MESSAGE;
- }
-
- if(event.key.control && event.key.code == sf::Keyboard::V) {
- // TODO: Make asynchronous.
- // TODO: Upload multiple files.
- std::string err_msg;
- if(matrix->post_file(current_room, sf::Clipboard::getString(), err_msg) != PluginResult::OK) {
- std::string desc = "Failed to upload media to room, error: " + err_msg;
- show_notification("QuickMedia", desc.c_str(), Urgency::CRITICAL);
- }
- }
-
- if(event.key.code == sf::Keyboard::R) {
- std::shared_ptr<BodyItem> selected = tabs[selected_tab].body->get_selected_shared();
- if(selected) {
- 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
- show_notification("QuickMedia", "No message selected for replying");
- }
- }
-
- if(event.key.code == sf::Keyboard::E) {
- std::shared_ptr<BodyItem> selected = tabs[selected_tab].body->get_selected_shared();
- if(selected) {
- 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");
- } 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");
- } else {
- chat_state = ChatState::EDITING;
- currently_operating_on_item = selected;
- chat_input.set_editable(true);
- chat_input.set_text(selected->get_description()); // TODO: Description? it may change in the future, in which case this should be edited
- chat_input.move_caret_to_end();
- replying_to_text.setString("Editing message:");
- }
- } else {
- // TODO: Show inline notification
- show_notification("QuickMedia", "No message selected for editing");
- }
- }
-
- if(event.key.code == sf::Keyboard::D) {
- BodyItem *selected = tabs[selected_tab].body->get_selected();
- if(selected) {
- // TODO: Make asynchronous
- std::string err_msg;
- if(matrix->delete_message(current_room, selected->userdata, err_msg) != PluginResult::OK) {
- // TODO: Show inline notification
- show_notification("QuickMedia", "Failed to delete message, reason: " + err_msg, Urgency::CRITICAL);
- }
- } else {
- // TODO: Show inline notification
- show_notification("QuickMedia", "No message selected for deletion");
- }
- }
}
- if((chat_state == ChatState::TYPING_MESSAGE || chat_state == ChatState::REPLYING || chat_state == ChatState::EDITING) && selected_tab == MESSAGES_TAB_INDEX) {
+ if((chat_state == ChatState::TYPING_MESSAGE || chat_state == ChatState::REPLYING || chat_state == ChatState::EDITING) && selected_tab == MESSAGES_TAB_INDEX && !frame_skip_text_entry) {
+ frame_skip_text_entry = false;
if(event.type == sf::Event::TextEntered) {
//chat_input.onTextEntered(event.text.unicode);
// TODO: Also show typing event when ctrl+v pasting?
@@ -3819,6 +3711,9 @@ namespace QuickMedia {
chat_input.process_event(event);
}
}
+ frame_skip_text_entry = false;
+
+ chat_page->update();
switch(new_page) {
case PageType::FILE_MANAGER: {
@@ -3833,7 +3728,7 @@ namespace QuickMedia {
file_manager_tabs.push_back(Tab{std::move(file_manager_body), std::move(file_manager_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
selected_files.clear();
- page_loop(std::move(file_manager_tabs));
+ page_loop(file_manager_tabs);
if(selected_files.empty()) {
fprintf(stderr, "No files selected!\n");
@@ -3854,19 +3749,7 @@ namespace QuickMedia {
break;
}
case PageType::CHAT_LOGIN: {
- new_page = PageType::CHAT;
- matrix->logout();
- tabs[MESSAGES_TAB_INDEX].body->clear_cache();
- // TODO: Instead of doing this, exit this current function and navigate to chat login page instead.
- // This doesn't currently work because at the end of this function there are futures that need to wait
- // and one of them is /sync, which has a timeout of 30 seconds. That timeout has to be killed somehow.
- //delete current_plugin;
- //current_plugin = new Matrix();
- current_page = PageType::CHAT_LOGIN;
- chat_login_page();
- if(current_page == PageType::CHAT)
- chat_page();
- exit(0);
+ abort();
break;
}
default:
@@ -3918,8 +3801,6 @@ namespace QuickMedia {
float room_name_padding_y = 0.0f;
if(selected_tab == MESSAGES_TAB_INDEX || selected_tab == PINNED_TAB_INDEX)
room_name_padding_y = room_name_total_height;
- else if(selected_tab == ROOMS_TAB_INDEX)
- room_name_padding_y = room_search_bar.getBottomWithoutShadow();
chat_input_height_full = chat_input.get_height() + chat_input_padding_y * 2.0f;
if(selected_tab != MESSAGES_TAB_INDEX || selected_tab == PINNED_TAB_INDEX)
@@ -3933,17 +3814,12 @@ namespace QuickMedia {
if(redraw) {
redraw = false;
- room_search_bar.onWindowResize(window_size);
float room_name_padding_y = 0.0f;
float padding_bottom = 0.0f;
if(selected_tab == MESSAGES_TAB_INDEX || selected_tab == PINNED_TAB_INDEX) {
room_name_padding_y = 10.0f + room_name_total_height;
tab_vertical_offset = 10.0f;
- } else if(selected_tab == ROOMS_TAB_INDEX) {
- room_name_padding_y = room_search_bar.getBottomWithoutShadow();
- tab_vertical_offset = 0.0f;
- padding_bottom = 10.0f;
}
tab_shade_height = tab_spacer_height + std::floor(tab_vertical_offset) + tab_height + room_name_padding_y + padding_bottom;
@@ -3971,42 +3847,11 @@ 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));
}
- room_search_bar.update();
-
- if(!sync_running && sync_timer.getElapsedTime().asMilliseconds() >= sync_min_time_ms) {
- fprintf(stderr, "Time since last sync: %d ms\n", sync_timer.getElapsedTime().asMilliseconds());
- sync_min_time_ms = 1000;
- sync_running = true;
- sync_timer.restart();
- sync_future = std::async(std::launch::async, [this]() {
- SyncFutureResult result;
- if(matrix->sync(result.room_sync_data) == PluginResult::OK) {
- fprintf(stderr, "Synced matrix\n");
- matrix->get_room_join_updates(result.rooms);
- } else {
- fprintf(stderr, "Failed to sync matrix\n");
- }
-
- return result;
- });
- }
-
- if(is_future_ready(sync_future)) {
- SyncFutureResult sync_result = sync_future.get();
-
- add_new_rooms(sync_result.rooms);
-
- auto room_messages_it = sync_result.room_sync_data.find(current_room);
- if(room_messages_it != sync_result.room_sync_data.end()) {
- add_new_messages_to_current_room(room_messages_it->second.messages);
- modify_related_messages_in_current_room(room_messages_it->second.messages);
- process_new_pinned_events(room_messages_it->second.pinned_events);
- }
-
- process_new_room_messages(sync_result.room_sync_data, !synced);
- sync_running = false;
- synced = true;
- }
+ sync_data.messages.clear();
+ matrix->get_room_sync_data(current_room, sync_data);
+ add_new_messages_to_current_room(sync_data.messages);
+ modify_related_messages_in_current_room(sync_data.messages);
+ process_pinned_events(sync_data.pinned_events);
if(is_future_ready(set_read_marker_future)) {
set_read_marker_future.get();
@@ -4045,6 +3890,7 @@ namespace QuickMedia {
if(message) {
*fetch_body_item = *message_to_body_item(message.get(), matrix->get_me(current_room).get());
event_data->status = FetchStatus::FINISHED_LOADING;
+ event_data->message = message.get();
fetch_body_item->userdata = event_data;
} else {
fetch_body_item->set_description("Failed to load message!");
@@ -4091,8 +3937,6 @@ namespace QuickMedia {
}
room_name_text.setPosition(body_pos.x + room_name_text_offset_x, room_name_text_padding_y + 4.0f);
window.draw(room_name_text);
- } else if(selected_tab == ROOMS_TAB_INDEX) {
- room_search_bar.draw(window, false);
}
gradient_points[0].position.x = 0.0f;
@@ -4136,7 +3980,7 @@ namespace QuickMedia {
const float margin = 5.0f;
const float replying_to_text_height = replying_to_text.getLocalBounds().height + margin;
- const float item_height = std::min(body_size.y - replying_to_text_height - margin, tabs[MESSAGES_TAB_INDEX].body->get_item_height(currently_operating_on_item.get()) + margin);
+ const float item_height = std::min(body_size.y - replying_to_text_height - margin, tabs[MESSAGES_TAB_INDEX].body->get_item_height(currently_operating_on_item.get(), body_size.x) + margin);
sf::RectangleShape overlay(sf::Vector2f(window_size.x, window_size.y - tab_shade_height - chat_input_height_full));
overlay.setPosition(0.0f, tab_shade_height);
@@ -4176,13 +4020,6 @@ 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(selected_tab == MESSAGES_TAB_INDEX && current_room) {
BodyItem *last_visible_item = tabs[selected_tab].body->get_last_fully_visible_item();
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) {
@@ -4210,6 +4047,21 @@ namespace QuickMedia {
window.display();
}
- exit(0); // Ignore futures and quit immediately
+ chat_page_end:
+ // TODO: Cancel these instead
+ if(set_read_marker_future.valid())
+ set_read_marker_future.get();
+ if(previous_messages_future.valid())
+ previous_messages_future.get();
+ if(fetch_message_future.valid())
+ fetch_message_future.get();
+ for(auto &typing_future : typing_futures) {
+ if(typing_future.valid())
+ typing_future.get();
+ }
+
+ for(auto &body_item : tabs[PINNED_TAB_INDEX].body->items) {
+ delete (PinnedEventData*)body_item->userdata;
+ }
}
}
diff --git a/src/plugins/Mangadex.cpp b/src/plugins/Mangadex.cpp
index a52788d..a8318e8 100644
--- a/src/plugins/Mangadex.cpp
+++ b/src/plugins/Mangadex.cpp
@@ -210,7 +210,7 @@ namespace QuickMedia {
}
PluginResult MangadexChaptersPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) {
- result_tabs.push_back(Tab{create_body(), std::make_unique<MangadexImagesPage>(program, content_title, title, url), nullptr});
+ result_tabs.push_back(Tab{nullptr, std::make_unique<MangadexImagesPage>(program, content_title, title, url), nullptr});
return PluginResult::OK;
}
diff --git a/src/plugins/Manganelo.cpp b/src/plugins/Manganelo.cpp
index 7f0a2f9..f87081c 100644
--- a/src/plugins/Manganelo.cpp
+++ b/src/plugins/Manganelo.cpp
@@ -121,7 +121,7 @@ namespace QuickMedia {
}
PluginResult ManganeloChaptersPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) {
- result_tabs.push_back(Tab{create_body(), std::make_unique<ManganeloImagesPage>(program, content_title, title, url), nullptr});
+ result_tabs.push_back(Tab{nullptr, std::make_unique<ManganeloImagesPage>(program, content_title, title, url), nullptr});
return PluginResult::OK;
}
diff --git a/src/plugins/Mangatown.cpp b/src/plugins/Mangatown.cpp
index 89bf447..1d4d71a 100644
--- a/src/plugins/Mangatown.cpp
+++ b/src/plugins/Mangatown.cpp
@@ -110,7 +110,7 @@ namespace QuickMedia {
}
PluginResult MangatownChaptersPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) {
- result_tabs.push_back(Tab{create_body(), std::make_unique<MangatownImagesPage>(program, content_title, title, url), nullptr});
+ result_tabs.push_back(Tab{nullptr, std::make_unique<MangatownImagesPage>(program, content_title, title, url), nullptr});
return PluginResult::OK;
}
diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp
index 99d6bed..ede0821 100644
--- a/src/plugins/Matrix.cpp
+++ b/src/plugins/Matrix.cpp
@@ -2,26 +2,22 @@
#include "../../include/Storage.hpp"
#include "../../include/StringUtils.hpp"
#include "../../include/NetUtils.hpp"
+#include "../../include/Notification.hpp"
#include <rapidjson/document.h>
#include <rapidjson/writer.h>
#include <rapidjson/stringbuffer.h>
#include <fcntl.h>
#include <unistd.h>
+#include "../../include/QuickMedia.hpp"
// TODO: Update avatar/display name when its changed in the room/globally.
-// Send read receipt to server and receive notifications in /sync and show the notifications.
-// Delete messages.
-// Edit messages.
// Show images/videos inline.
// TODO: Verify if buffer of size 512 is enough for endpoints
-// TODO: POST /_matrix/client/r0/rooms/{roomId}/read_markers after 5 seconds of receiving a message when the client is focused
-// to mark messages as read
-// When reaching top/bottom message, show older/newer messages.
// Remove older messages (outside screen) to save memory. Reload them when the selected body item is the top/bottom one.
-
-// TODO: Verify if this class really is thread-safe (for example room data fields, user fields, message fields; etc that are updated in /sync)
+// 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";
static rapidjson::Value nullValue(rapidjson::kNullType);
static const rapidjson::Value& GetMember(const rapidjson::Value &obj, const char *key) {
@@ -31,6 +27,47 @@ static const rapidjson::Value& GetMember(const rapidjson::Value &obj, const char
return nullValue;
}
+static std::string capitalize(const std::string &str) {
+ if(str.size() >= 1)
+ return (char)std::toupper(str[0]) + str.substr(1);
+ else
+ return "";
+}
+
+// TODO: According to spec: "Any tag in the tld.name.* form but not matching the namespace of the current client should be ignored",
+// should we follow this?
+static std::string tag_get_name(const std::string &tag) {
+ if(tag.size() >= 2 && memcmp(tag.data(), "m.", 2) == 0) {
+ if(strcmp(tag.c_str() + 2, "favourite") == 0)
+ return "Favorites";
+ else if(strcmp(tag.c_str() + 2, "lowpriority") == 0)
+ return "Low priority";
+ else if(strcmp(tag.c_str() + 2, "server_notice") == 0)
+ return "Server notice";
+ else
+ return capitalize(tag.substr(2));
+ } else if(tag.size() >= 2 && memcmp(tag.data(), "u.", 2) == 0) {
+ return capitalize(tag.substr(2));
+ } else if(tag.size() >= 9 && memcmp(tag.data(), "tld.name.", 9) == 0) {
+ return capitalize(tag.substr(9));
+ } else {
+ return "";
+ }
+}
+
+static std::string extract_first_line_elipses(const std::string &str, size_t max_length) {
+ size_t index = str.find('\n');
+ if(index == std::string::npos) {
+ if(str.size() > max_length)
+ return str.substr(0, max_length) + " (...)";
+ return str;
+ } else if(index == 0) {
+ return "";
+ } else {
+ return str.substr(0, std::min(index, max_length)) + " (...)";
+ }
+}
+
namespace QuickMedia {
std::shared_ptr<UserInfo> RoomData::get_user_by_id(const std::string &user_id) {
std::lock_guard<std::mutex> lock(room_mutex);
@@ -75,11 +112,6 @@ namespace QuickMedia {
}
}
- void RoomData::append_pinned_events(std::vector<std::string> new_pinned_events) {
- std::lock_guard<std::mutex> lock(room_mutex);
- pinned_events.insert(pinned_events.end(), new_pinned_events.begin(), new_pinned_events.end());
- }
-
std::shared_ptr<Message> RoomData::get_message_by_id(const std::string &id) {
std::lock_guard<std::mutex> lock(room_mutex);
auto message_it = message_by_event_id.find(id);
@@ -160,57 +192,368 @@ namespace QuickMedia {
return avatar_url;
}
- PluginResult Matrix::sync(RoomSyncData &room_sync_data) {
- std::vector<CommandArg> additional_args = {
- { "-H", "Authorization: Bearer " + access_token },
- { "-m", "35" }
- };
+ void RoomData::set_pinned_events(std::vector<std::string> new_pinned_events) {
+ std::lock_guard<std::mutex> lock(room_mutex);
+ pinned_events = std::move(new_pinned_events);
+ pinned_events_updated = true;
+ }
- char url[512];
- if(next_batch.empty())
- 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());
+ std::set<std::string>& RoomData::get_tags_unsafe() {
+ return tags;
+ }
- rapidjson::Document json_root;
- DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true);
- if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result);
+ MatrixQuickMedia::MatrixQuickMedia(Program *program, Matrix *matrix, MatrixRoomsPage *rooms_page, MatrixRoomTagsPage *room_tags_page) : program(program), matrix(matrix), rooms_page(rooms_page), room_tags_page(room_tags_page) {
+ rooms_page->matrix_delegate = this;
+ room_tags_page->matrix_delegate = this;
+ }
- PluginResult result = sync_response_to_body_items(json_root, room_sync_data);
- if(result != PluginResult::OK)
- return result;
+ void MatrixQuickMedia::room_create(RoomData *room) {
+ std::string room_name = room->get_name();
+ if(room_name.empty())
+ room_name = room->id;
- const rapidjson::Value &next_batch_json = GetMember(json_root, "next_batch");
- if(next_batch_json.IsString()) {
- next_batch = next_batch_json.GetString();
- fprintf(stderr, "Matrix: next batch: %s\n", next_batch.c_str());
- } else {
- fprintf(stderr, "Matrix: missing next batch\n");
+ auto body_item = BodyItem::create(std::move(room_name));
+ body_item->url = room->id;
+ body_item->thumbnail_url = room->get_avatar_url();
+ body_item->userdata = room; // Note: this has to be valid as long as the room list is valid!
+ body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE;
+ body_item->thumbnail_size = sf::Vector2i(32, 32);
+ room->userdata = body_item.get();
+ room_body_items.push_back(body_item);
+ rooms_page->add_body_item(body_item);
+ room_body_item_by_room[room] = body_item;
+ }
+
+ void MatrixQuickMedia::room_add_tag(RoomData *room, const std::string &tag) {
+ room_tags_page->add_room_body_item_to_tag(room_body_item_by_room[room], tag);
+ }
+
+ void MatrixQuickMedia::room_remove_tag(RoomData *room, const std::string &tag) {
+ room_tags_page->remove_room_body_item_from_tag(room_body_item_by_room[room], tag);
+ }
+
+ void MatrixQuickMedia::room_add_new_messages(RoomData *room, const Messages &messages, bool is_initial_sync) {
+ std::lock_guard<std::mutex> lock(pending_room_messages_mutex);
+ auto &room_messages_data = pending_room_messages[room];
+ room_messages_data.messages.insert(room_messages_data.messages.end(), messages.begin(), messages.end());
+ room_messages_data.is_initial_sync = is_initial_sync;
+ }
+
+ 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];
+ if(static_cast<RoomData*>(body_item->userdata)->last_message_read || body_item.get() == item_to_swap)
+ return i;
+ }
+ return -1;
+ }
+
+ 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<RoomData*>(body_item->userdata)->has_unread_mention || body_item.get() == item_to_swap)
+ return i;
+ }
+ return -1;
+ }
+
+ static void sort_room_body_items(std::vector<std::shared_ptr<BodyItem>> &room_body_items) {
+ std::sort(room_body_items.begin(), room_body_items.end(), [](const std::shared_ptr<BodyItem> &body_item1, const std::shared_ptr<BodyItem> &body_item2) {
+ RoomData *room1 = static_cast<RoomData*>(body_item1->userdata);
+ RoomData *room2 = static_cast<RoomData*>(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;
+ return room1_focus_sum > room2_focus_sum;
+ });
+ }
+
+ void MatrixQuickMedia::update(MatrixPageType page_type) {
+ std::lock_guard<std::mutex> lock(pending_room_messages_mutex);
+ bool is_window_focused = program->is_window_focused();
+ RoomData *current_room = program->get_current_chat_room();
+ for(auto &it : pending_room_messages) {
+ RoomData *room = it.first;
+ auto &messages = it.second.messages;
+ bool is_initial_sync = it.second.is_initial_sync;
+ //auto &room_body_item = room_body_item_by_room[room];
+ //std::string room_desc = matrix->message_get_author_displayname(it.second.back().get()) + ": " + extract_first_line_elipses(it.second.back()->body, 150);
+ //room_body_item->set_description(std::move(room_desc));
+
+ 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);
+ }
+ }
+
+ std::shared_ptr<UserInfo> 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<BodyItem*>(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));
+ }
}
+ pending_room_messages.clear();
+ }
+
+ MatrixRoomsPage::MatrixRoomsPage(Program *program, Body *body, std::string title, MatrixRoomTagsPage *room_tags_page) : Page(program), body(body), title(std::move(title)), room_tags_page(room_tags_page) {
+ if(room_tags_page)
+ room_tags_page->current_rooms_page = this;
+ }
+
+ MatrixRoomsPage::~MatrixRoomsPage() {
+ if(room_tags_page)
+ room_tags_page->current_rooms_page = nullptr;
+ }
+ PluginResult MatrixRoomsPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) {
+ (void)title;
+ auto chat_page = std::make_unique<MatrixChatPage>(program, url);
+ chat_page->matrix_delegate = matrix_delegate;
+ result_tabs.push_back(Tab{nullptr, std::move(chat_page), nullptr});
return PluginResult::OK;
}
- void Matrix::get_room_join_updates(Rooms &new_rooms) {
- std::lock_guard<std::mutex> lock(room_data_mutex);
- size_t num_new_rooms = rooms.size() - room_list_read_index;
- size_t new_rooms_prev_size = new_rooms.size();
- new_rooms.resize(new_rooms_prev_size + num_new_rooms);
- for(size_t i = new_rooms_prev_size; i < new_rooms.size(); ++i) {
- new_rooms[i] = rooms[room_list_read_index + i].get();
+ void MatrixRoomsPage::update() {
+ {
+ std::lock_guard<std::mutex> lock(mutex);
+ body->append_items(std::move(room_body_items));
+ }
+ matrix_delegate->update(MatrixPageType::ROOM_LIST);
+ }
+
+ void MatrixRoomsPage::add_body_item(std::shared_ptr<BodyItem> body_item) {
+ std::lock_guard<std::mutex> lock(mutex);
+ room_body_items.push_back(body_item);
+ }
+
+ void MatrixRoomsPage::move_room_to_top(RoomData *room) {
+ // Swap order of rooms in body list to put rooms with mentions at the top and then unread messages and then all the other rooms
+ // TODO: Optimize with hash map instead of linear search? or cache the index
+ std::lock_guard<std::mutex> lock(mutex);
+ BodyItem *room_body_item = static_cast<BodyItem*>(room->userdata);
+ int room_body_index = body->get_index_by_body_item(room_body_item);
+ if(room_body_index != -1) {
+ std::shared_ptr<BodyItem> body_item = body->items[room_body_index];
+ int body_swap_index = -1;
+ if(room->has_unread_mention)
+ 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());
+ if(body_swap_index != -1 && body_swap_index != room_body_index) {
+ body->items.erase(body->items.begin() + room_body_index);
+ if(body_swap_index < room_body_index)
+ body->items.insert(body->items.begin() + body_swap_index, std::move(body_item));
+ else
+ body->items.insert(body->items.begin() + (body_swap_index - 1), std::move(body_item));
+ }
+ }
+ }
+
+ PluginResult MatrixRoomTagsPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) {
+ (void)title;
+ std::lock_guard<std::mutex> lock(mutex);
+ auto body = create_body();
+ Body *body_ptr = body.get();
+ TagData &tag_data = tag_body_items_by_name[url];
+ body->items = tag_data.room_body_items;
+ sort_room_body_items(body->items);
+ auto rooms_page = std::make_unique<MatrixRoomsPage>(program, body_ptr, tag_data.tag_item->get_title(), this);
+ rooms_page->matrix_delegate = matrix_delegate;
+ result_tabs.push_back(Tab{std::move(body), std::move(rooms_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ return PluginResult::OK;
+ }
+
+ // TODO: Also add/remove body items to above body (in submit)
+ void MatrixRoomTagsPage::update() {
+ {
+ std::lock_guard<std::mutex> lock(mutex);
+ for(auto &it : remove_room_body_items_by_tags) {
+ auto tag_body_it = tag_body_items_by_name.find(it.first);
+ if(tag_body_it == tag_body_items_by_name.end())
+ continue;
+
+ for(auto &room_to_remove : it.second) {
+ auto room_body_item_it = std::find(tag_body_it->second.room_body_items.begin(), tag_body_it->second.room_body_items.end(), room_to_remove);
+ if(room_body_item_it != tag_body_it->second.room_body_items.end())
+ tag_body_it->second.room_body_items.erase(room_body_item_it);
+ }
+
+ if(tag_body_it->second.room_body_items.empty()) {
+ auto room_body_item_it = std::find(body->items.begin(), body->items.end(), tag_body_it->second.tag_item);
+ if(room_body_item_it != body->items.end())
+ body->items.erase(room_body_item_it);
+ tag_body_items_by_name.erase(tag_body_it);
+ }
+ }
+ remove_room_body_items_by_tags.clear();
+
+ for(auto &it : add_room_body_items_by_tags) {
+ TagData *tag_data;
+ auto tag_body_it = tag_body_items_by_name.find(it.first);
+ if(tag_body_it == tag_body_items_by_name.end()) {
+ std::string tag_name = tag_get_name(it.first);
+ if(!tag_name.empty()) {
+ auto tag_body_item = BodyItem::create(std::move(tag_name));
+ tag_body_item->url = it.first;
+ tag_body_items_by_name.insert(std::make_pair(it.first, TagData{tag_body_item, {}}));
+ // TODO: Sort by tag priority
+ body->items.push_back(tag_body_item);
+ tag_data = &tag_body_items_by_name[it.first];
+ tag_data->tag_item = tag_body_item;
+ }
+ } else {
+ tag_data = &tag_body_it->second;
+ }
+
+ for(auto &room_body_item : it.second) {
+ tag_data->room_body_items.push_back(room_body_item);
+ }
+ }
+ add_room_body_items_by_tags.clear();
+ }
+ matrix_delegate->update(MatrixPageType::ROOM_LIST);
+ }
+
+ void MatrixRoomTagsPage::add_room_body_item_to_tag(std::shared_ptr<BodyItem> body_item, const std::string &tag) {
+ std::lock_guard<std::mutex> lock(mutex);
+ add_room_body_items_by_tags[tag].push_back(body_item);
+ }
+
+ void MatrixRoomTagsPage::remove_room_body_item_from_tag(std::shared_ptr<BodyItem> body_item, const std::string &tag) {
+ std::lock_guard<std::mutex> lock(mutex);
+ remove_room_body_items_by_tags[tag].push_back(body_item);
+ }
+
+ void MatrixRoomTagsPage::move_room_to_top(RoomData *room) {
+ if(current_rooms_page)
+ current_rooms_page->move_room_to_top(room);
+ }
+
+ void MatrixChatPage::update() {
+ matrix_delegate->update(MatrixPageType::CHAT);
+ }
+
+ void Matrix::start_sync(MatrixDelegate *delegate) {
+ if(sync_running)
+ return;
+
+ sync_running = true;
+ sync_thread = std::thread([this, delegate]() {
+ const rapidjson::Value *next_batch_json;
+ PluginResult result;
+ while(sync_running) {
+ std::vector<CommandArg> additional_args = {
+ { "-H", "Authorization: Bearer " + access_token },
+ { "-m", "35" }
+ };
+
+ char url[512];
+ if(next_batch.empty())
+ 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());
+
+ rapidjson::Document json_root;
+ DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true);
+ if(download_result != DownloadResult::OK) {
+ fprintf(stderr, "Fetch response failed\n");
+ goto sync_end;
+ }
+
+ result = parse_sync_response(json_root, delegate);
+ if(result != PluginResult::OK) {
+ fprintf(stderr, "Failed to parse sync response\n");
+ goto sync_end;
+ }
+
+ next_batch_json = &GetMember(json_root, "next_batch");
+ if(next_batch_json->IsString()) {
+ next_batch = next_batch_json->GetString();
+ fprintf(stderr, "Matrix: next batch: %s\n", next_batch.c_str());
+ } else {
+ fprintf(stderr, "Matrix: missing next batch\n");
+ }
+
+ sync_end:
+ if(sync_running)
+ std::this_thread::sleep_for(std::chrono::seconds(1));
+ }
+ });
+ }
+
+ void Matrix::stop_sync() {
+ // TODO: Kill the running download in |sync_thread| instead of waiting until sync returns (which can be up to 30 seconds)
+ sync_running = false;
+ if(sync_thread.joinable())
+ sync_thread.join();
+ }
+
+ bool Matrix::is_initial_sync_finished() const {
+ return !next_batch.empty();
+ }
+
+ void Matrix::get_room_sync_data(RoomData *room, SyncData &sync_data) {
+ room->acquire_room_lock();
+ auto &room_messages = room->get_messages_thread_unsafe();
+ sync_data.messages.insert(sync_data.messages.end(), room_messages.begin() + room->messages_read_index, room_messages.end());
+ room->messages_read_index = room_messages.size();
+ if(room->pinned_events_updated) {
+ sync_data.pinned_events = room->get_pinned_events_unsafe();
+ room->pinned_events_updated = false;
}
- room_list_read_index += num_new_rooms;
+ room->release_room_lock();
}
void Matrix::get_all_synced_room_messages(RoomData *room, Messages &messages) {
room->acquire_room_lock();
messages = room->get_messages_thread_unsafe();
+ room->messages_read_index = messages.size();
room->release_room_lock();
}
void Matrix::get_all_pinned_events(RoomData *room, std::vector<std::string> &events) {
room->acquire_room_lock();
events = room->get_pinned_events_unsafe();
+ room->pinned_events_updated = false;
room->release_room_lock();
}
@@ -226,15 +569,69 @@ namespace QuickMedia {
size_t num_messages_after = room->get_messages_thread_unsafe().size();
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;
room->release_room_lock();
return PluginResult::OK;
}
- PluginResult Matrix::sync_response_to_body_items(const rapidjson::Document &root, RoomSyncData &room_sync_data) {
+ PluginResult Matrix::parse_sync_response(const rapidjson::Document &root, MatrixDelegate *delegate) {
if(!root.IsObject())
return PluginResult::ERR;
+ const rapidjson::Value &account_data_json = GetMember(root, "account_data");
+ std::optional<std::set<std::string>> 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);
+
+ return PluginResult::OK;
+ }
+
+ PluginResult Matrix::parse_sync_account_data(const rapidjson::Value &account_data_json, std::optional<std::set<std::string>> &dm_rooms) {
+ if(!account_data_json.IsObject())
+ return PluginResult::OK;
+
+ const rapidjson::Value &events_json = GetMember(account_data_json, "events");
+ if(!events_json.IsArray())
+ return PluginResult::OK;
+
+ bool has_direct_rooms = false;
+ std::set<std::string> dm_rooms_tmp;
+ for(const rapidjson::Value &event_item_json : events_json.GetArray()) {
+ if(!event_item_json.IsObject())
+ continue;
+
+ const rapidjson::Value &type_json = GetMember(event_item_json, "type");
+ if(!type_json.IsString() || strcmp(type_json.GetString(), "m.direct") != 0)
+ continue;
+
+ const rapidjson::Value &content_json = GetMember(event_item_json, "content");
+ if(!content_json.IsObject())
+ continue;
+
+ has_direct_rooms = true;
+ for(auto const &it : content_json.GetObject()) {
+ if(!it.value.IsArray())
+ continue;
+
+ for(const rapidjson::Value &room_id_json : it.value.GetArray()) {
+ if(!room_id_json.IsString())
+ continue;
+
+ dm_rooms_tmp.insert(std::string(room_id_json.GetString(), room_id_json.GetStringLength()));
+ }
+ }
+ }
+
+ if(has_direct_rooms)
+ dm_rooms = std::move(dm_rooms_tmp);
+
+ return PluginResult::OK;
+ }
+
+ PluginResult Matrix::parse_sync_room_data(const rapidjson::Value &rooms_json, MatrixDelegate *delegate) {
if(!rooms_json.IsObject())
return PluginResult::OK;
@@ -252,12 +649,14 @@ namespace QuickMedia {
std::string room_id_str = room_id.GetString();
+ bool is_new_room = false;
RoomData *room = get_room_by_id(room_id_str);
if(!room) {
auto new_room = std::make_unique<RoomData>();
new_room->id = room_id_str;
room = new_room.get();
add_room(std::move(new_room));
+ is_new_room = true;
}
const rapidjson::Value &state_json = GetMember(it.value, "state");
@@ -265,7 +664,7 @@ namespace QuickMedia {
const rapidjson::Value &events_json = GetMember(state_json, "events");
events_add_user_info(events_json, room);
events_set_room_name(events_json, room);
- events_add_pinned_events(events_json, room, room_sync_data);
+ events_add_pinned_events(events_json, room);
}
const rapidjson::Value &ephemeral_json = GetMember(it.value, "ephemeral");
@@ -296,7 +695,8 @@ namespace QuickMedia {
const rapidjson::Value &events_json = GetMember(ephemeral_json, "events");
events_add_user_read_markers(events_json, room);
}
- events_add_messages(events_json, room, MessageDirection::AFTER, &room_sync_data, has_unread_notifications);
+ events_add_messages(events_json, room, MessageDirection::AFTER, delegate, has_unread_notifications);
+ events_add_pinned_events(events_json, room);
} else {
if(ephemeral_json.IsObject()) {
const rapidjson::Value &events_json = GetMember(ephemeral_json, "events");
@@ -304,10 +704,23 @@ namespace QuickMedia {
}
}
+ if(is_new_room)
+ delegate->room_create(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);
+ events_add_room_to_tags(events_json, room, delegate);
+ }
+
+ if(is_new_room) {
+ room->acquire_room_lock();
+ std::set<std::string> &room_tags = room->get_tags_unsafe();
+ if(room_tags.empty()) {
+ room_tags.insert(OTHERS_ROOM_TAG);
+ delegate->room_add_tag(room, OTHERS_ROOM_TAG);
+ }
+ room->release_room_lock();
}
}
@@ -416,7 +829,7 @@ namespace QuickMedia {
auto user = room_data->get_user_by_id(user_id_json.GetString());
if(!user) {
- fprintf(stderr, "Receipt read receipt for unknown user: %s, ignoring...\n", user_id_json.GetString());
+ fprintf(stderr, "Read receipt for unknown user: %s, ignoring...\n", user_id_json.GetString());
continue;
}
@@ -492,16 +905,28 @@ namespace QuickMedia {
return false;
}
+ static size_t string_find_case_insensitive(const char *haystack, size_t index, size_t length, const std::string &needle) {
+ const char *haystack_end = haystack + length;
+ auto it = std::search(haystack + index, haystack_end, needle.begin(), needle.end(),
+ [](char c1, char c2) {
+ return std::toupper(c1) == std::toupper(c2);
+ });
+ if(it != haystack_end)
+ return it - haystack;
+ else
+ return std::string::npos;
+ }
+
// 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
bool message_contains_user_mention(const std::string &msg, const std::string &username) {
- if(msg.empty())
+ if(msg.empty() || username.empty())
return false;
size_t index = 0;
while(index < msg.size()) {
- size_t found_index = msg.find(username, index);
+ size_t found_index = string_find_case_insensitive(&msg[0], index, msg.size(), username);
if(found_index == std::string::npos)
- return false;
+ break;
char prev_char = ' ';
if(found_index > 0)
@@ -514,16 +939,17 @@ namespace QuickMedia {
if(is_username_seperating_character(prev_char) && is_username_seperating_character(next_char))
return true;
- index += username.size();
+ index = found_index + username.size();
}
return false;
}
- void Matrix::events_add_messages(const rapidjson::Value &events_json, RoomData *room_data, MessageDirection message_dir, RoomSyncData *room_sync_data, bool has_unread_notifications) {
+ void Matrix::events_add_messages(const rapidjson::Value &events_json, RoomData *room_data, MessageDirection message_dir, MatrixDelegate *delegate, bool has_unread_notifications) {
if(!events_json.IsArray())
return;
+ // TODO: Preallocate
std::vector<std::shared_ptr<Message>> new_messages;
auto me = get_me(room_data);
@@ -536,11 +962,6 @@ namespace QuickMedia {
if(new_messages.empty())
return;
- // TODO: Add directly to this instead when set? otherwise add to new_messages
- if(room_sync_data)
- (*room_sync_data)[room_data].messages = new_messages;
-
- // TODO: Loop and std::move instead? doesn't insert create copies?
if(message_dir == MessageDirection::BEFORE) {
room_data->prepend_messages_reverse(new_messages);
} else if(message_dir == MessageDirection::AFTER) {
@@ -560,6 +981,9 @@ namespace QuickMedia {
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)
+ delegate->room_add_new_messages(room_data, new_messages, next_batch.empty());
}
std::shared_ptr<Message> Matrix::parse_message_event(const rapidjson::Value &event_item_json, RoomData *room_data) {
@@ -706,6 +1130,9 @@ namespace QuickMedia {
message->type = MessageType::TEXT;
message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver);
message_content_extract_thumbnail_size(*content_json, message->thumbnail_size);
+ } else if(strcmp(content_type.GetString(), "m.server_notice") == 0) { // TODO: show server notices differently
+ message->type = MessageType::TEXT;
+ prefix = "* Server notice * ";
} else {
return nullptr;
}
@@ -825,10 +1252,11 @@ namespace QuickMedia {
}
}
- void Matrix::events_add_pinned_events(const rapidjson::Value &events_json, RoomData *room_data, RoomSyncData &room_sync_data) {
+ void Matrix::events_add_pinned_events(const rapidjson::Value &events_json, RoomData *room_data) {
if(!events_json.IsArray())
return;
+ bool has_pinned_events = false;
std::vector<std::string> pinned_events;
for(const rapidjson::Value &event_item_json : events_json.GetArray()) {
if(!event_item_json.IsObject())
@@ -846,34 +1274,25 @@ namespace QuickMedia {
if(!pinned_json.IsArray())
continue;
+ has_pinned_events = true;
+ pinned_events.clear();
for(const rapidjson::Value &pinned_item_json : pinned_json.GetArray()) {
if(!pinned_item_json.IsString())
continue;
-
pinned_events.push_back(std::string(pinned_item_json.GetString(), pinned_item_json.GetStringLength()));
}
}
- room_sync_data[room_data].pinned_events = pinned_events;
- room_data->append_pinned_events(std::move(pinned_events));
+ if(has_pinned_events)
+ room_data->set_pinned_events(std::move(pinned_events));
}
- // TODO: According to spec: "Any tag in the tld.name.* form but not matching the namespace of the current client should be ignored",
- // should we follow this?
- static const char* tag_get_name(const char *name, size_t size) {
- if(size >= 2 && (memcmp(name, "m.", 2) == 0 || memcmp(name, "u.", 2) == 0))
- return name + 2;
- else if(size >= 9 && memcmp(name, "tld.name.", 9) == 0)
- return name + 9;
- else
- return name;
- }
-
- void Matrix::events_add_room_to_tags(const rapidjson::Value &events_json, RoomData *room_data) {
+ void Matrix::events_add_room_to_tags(const rapidjson::Value &events_json, RoomData *room_data, MatrixDelegate *delegate) {
if(!events_json.IsArray())
return;
- std::vector<std::string> pinned_events;
+ bool has_tags = false;
+ std::set<std::string> new_tags;
for(const rapidjson::Value &event_item_json : events_json.GetArray()) {
if(!event_item_json.IsObject())
continue;
@@ -890,17 +1309,46 @@ namespace QuickMedia {
if(!tags_json.IsObject())
continue;
+ has_tags = true;
+ new_tags.clear();
for(auto const &tag_json : tags_json.GetObject()) {
if(!tag_json.name.IsString() || !tag_json.value.IsObject())
continue;
- const char *tag_name = tag_get_name(tag_json.name.GetString(), tag_json.name.GetStringLength());
- if(!tag_name)
- continue;
+ //const char *tag_name = tag_get_name(tag_json.name.GetString(), tag_json.name.GetStringLength());
+ //if(!tag_name)
+ // continue;
// TODO: Support tag order
- rooms_by_tag_name[tag_name].push_back(room_data->index);
+ new_tags.insert(std::string(tag_json.name.GetString(), tag_json.name.GetStringLength()));
+ }
+ }
+
+ // Adding/removing tags is done with PUT and DELETE, but tags is part of account_data that contains all of the tags.
+ // When we receive a list of tags its always the full list of tags
+ if(has_tags) {
+ room_data->acquire_room_lock();
+ std::set<std::string> &room_tags = room_data->get_tags_unsafe();
+
+ for(const std::string &room_tag : room_tags) {
+ auto it = new_tags.find(room_tag);
+ if(it == new_tags.end())
+ delegate->room_remove_tag(room_data, room_tag);
+ }
+
+ for(const std::string &new_tag : new_tags) {
+ auto it = room_tags.find(new_tag);
+ if(it == room_tags.end())
+ delegate->room_add_tag(room_data, new_tag);
}
+
+ if(new_tags.empty()) {
+ new_tags.insert(OTHERS_ROOM_TAG);
+ delegate->room_add_tag(room_data, OTHERS_ROOM_TAG);
+ }
+
+ room_tags = std::move(new_tags);
+ room_data->release_room_lock();
}
}
@@ -991,7 +1439,7 @@ namespace QuickMedia {
return "m.file";
}
- PluginResult Matrix::post_message(RoomData *room, const std::string &body, const std::optional<UploadInfo> &file_info, const std::optional<UploadInfo> &thumbnail_info) {
+ PluginResult Matrix::post_message(RoomData *room, const std::string &body, const std::optional<UploadInfo> &file_info, const std::optional<UploadInfo> &thumbnail_info, const std::string &msgtype) {
char random_characters[18];
if(!generate_random_characters(random_characters, sizeof(random_characters)))
return PluginResult::ERR;
@@ -1021,7 +1469,10 @@ namespace QuickMedia {
}
rapidjson::Document request_data(rapidjson::kObjectType);
- request_data.AddMember("msgtype", rapidjson::StringRef(file_info ? content_type_to_message_type(file_info->content_type) : "m.text"), request_data.GetAllocator());
+ if(msgtype.empty())
+ request_data.AddMember("msgtype", rapidjson::StringRef(file_info ? content_type_to_message_type(file_info->content_type) : "m.text"), request_data.GetAllocator());
+ else
+ request_data.AddMember("msgtype", rapidjson::StringRef(msgtype.c_str()), request_data.GetAllocator());
request_data.AddMember("body", rapidjson::StringRef(body.c_str()), request_data.GetAllocator());
if(contains_formatted_text) {
request_data.AddMember("format", "org.matrix.custom.html", request_data.GetAllocator());
@@ -1173,11 +1624,6 @@ namespace QuickMedia {
// TODO: Store shared_ptr<Message> instead of raw pointer...
Message *relates_to_message_raw = (Message*)relates_to;
std::shared_ptr<Message> relates_to_message_shared = room->get_message_by_id(relates_to_message_raw->event_id);
- std::shared_ptr<Message> relates_to_message_original = get_edited_message_original_message(room, relates_to_message_shared);
- if(!relates_to_message_original) {
- fprintf(stderr, "Failed to get the original message for message with event id: %s\n", relates_to_message_raw->event_id.c_str());
- return PluginResult::ERR;
- }
char random_characters[18];
if(!generate_random_characters(random_characters, sizeof(random_characters)))
@@ -1186,7 +1632,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_original->event_id.c_str()), in_reply_to_json.GetAllocator());
+ in_reply_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_shared->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());
@@ -1233,11 +1679,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<Message> relates_to_message_shared = room->get_message_by_id(relates_to_message_raw->event_id);
- std::shared_ptr<Message> relates_to_message_original = get_edited_message_original_message(room, relates_to_message_shared);
- if(!relates_to_message_original) {
- fprintf(stderr, "Failed to get the original message for message with event id: %s\n", relates_to_message_raw->event_id.c_str());
- return PluginResult::ERR;
- }
char random_characters[18];
if(!generate_random_characters(random_characters, sizeof(random_characters)))
@@ -1274,7 +1715,7 @@ namespace QuickMedia {
}
rapidjson::Document relates_to_json(rapidjson::kObjectType);
- relates_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_original->event_id.c_str()), relates_to_json.GetAllocator());
+ relates_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_shared->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;
@@ -1320,14 +1761,6 @@ namespace QuickMedia {
return PluginResult::OK;
}
- // TODO: Right now this recursively calls /rooms/<room_id>/context/<event_id> and trusts server to not make it recursive. To make this robust, check iteration count and do not trust server.
- // TODO: Optimize?
- std::shared_ptr<Message> Matrix::get_edited_message_original_message(RoomData *room_data, std::shared_ptr<Message> message) {
- if(!message || message->related_event_type != RelatedEventType::EDIT)
- return message;
- return get_edited_message_original_message(room_data, get_message_by_id(room_data, message->related_event_id));
- }
-
std::shared_ptr<Message> Matrix::get_message_by_id(RoomData *room, const std::string &event_id) {
std::shared_ptr<Message> existing_room_message = room->get_message_by_id(event_id);
if(existing_room_message)
@@ -1373,15 +1806,11 @@ namespace QuickMedia {
return new_message;
}
- // Returns empty string on error
static const char* file_get_filename(const std::string &filepath) {
size_t index = filepath.rfind('/');
if(index == std::string::npos)
- return "";
- const char *filename = filepath.c_str() + index + 1;
- if(filename[0] == '\0')
- return "";
- return filename;
+ return filepath.c_str();
+ return filepath.c_str() + index + 1;
}
PluginResult Matrix::post_file(RoomData *room, const std::string &filepath, std::string &err_msg) {
@@ -1581,7 +2010,6 @@ namespace QuickMedia {
rooms.clear();
room_list_read_index = 0;
room_data_by_id.clear();
- rooms_by_tag_name.clear();
user_id.clear();
username.clear();
access_token.clear();
diff --git a/src/plugins/NyaaSi.cpp b/src/plugins/NyaaSi.cpp
index 3fe6526..8b1efc7 100644
--- a/src/plugins/NyaaSi.cpp
+++ b/src/plugins/NyaaSi.cpp
@@ -51,7 +51,7 @@ namespace QuickMedia {
size_t tbody_begin = website_data.find("<tbody>");
if(tbody_begin == std::string::npos)
- return SearchResult::ERR;
+ return SearchResult::OK;
size_t tbody_end = website_data.find("</tbody>", tbody_begin + 7);
if(tbody_end == std::string::npos)
diff --git a/src/plugins/Pornhub.cpp b/src/plugins/Pornhub.cpp
index c0e3fa1..f527e76 100644
--- a/src/plugins/Pornhub.cpp
+++ b/src/plugins/Pornhub.cpp
@@ -141,7 +141,7 @@ namespace QuickMedia {
PluginResult PornhubSearchPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) {
(void)title;
(void)url;
- result_tabs.push_back(Tab{create_body(), std::make_unique<PornhubVideoPage>(program), nullptr});
+ result_tabs.push_back(Tab{nullptr, std::make_unique<PornhubVideoPage>(program), nullptr});
return PluginResult::OK;
}
diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp
index 12c156a..a157a8c 100644
--- a/src/plugins/Youtube.cpp
+++ b/src/plugins/Youtube.cpp
@@ -278,7 +278,7 @@ namespace QuickMedia {
PluginResult YoutubeSearchPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) {
(void)title;
(void)url;
- result_tabs.push_back(Tab{create_body(), std::make_unique<YoutubeVideoPage>(program), nullptr});
+ result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(program), nullptr});
return PluginResult::OK;
}