aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2022-11-11 10:50:39 +0100
committerdec05eba <dec05eba@protonmail.com>2022-11-11 10:50:39 +0100
commitd274d3a6dfc0864ec6a44e7d6948c2d873eb6f76 (patch)
treea4308da3afd0e7c567c40f8a26ab1bdcbc58a898
parent25f6303ae40e9245f42545d120efa8b6f9be98d7 (diff)
Add image (custom emoji) alt text for copy-pasting, limit custom emoji size in room description, change max size to 32, 32, cache custom emoji locally
-rw-r--r--README.md1
-rw-r--r--TODO4
-rw-r--r--include/Text.hpp6
-rw-r--r--plugins/Matrix.hpp5
-rw-r--r--src/QuickMedia.cpp8
-rw-r--r--src/Text.cpp37
-rw-r--r--src/plugins/Matrix.cpp101
7 files changed, 117 insertions, 45 deletions
diff --git a/README.md b/README.md
index cc2af9c..03f638e 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,7 @@ Installing `lld` (the LLVM linker) can improve compile times.
`wget` needs to be installed for xxx plugins.
## Matrix
QuickMedia does currently not support encryption in matrix. If you want encryption then you can use [pantalaimon](https://github.com/matrix-org/pantalaimon/).\
+There are some issues with using pantalaimon though, for example pantalaimon breaks custom emoji.\
It's not recommended to run QuickMedia with matrix and element on the same account at the same time as read markers behave differently in the different clients so messages wont be properly marked as read.
## Controls
### General control
diff --git a/TODO b/TODO
index ed04741..c58db45 100644
--- a/TODO
+++ b/TODO
@@ -240,10 +240,10 @@ The formatting of replying to a message with an image in matrix is a bit weird.
Add ctrl+h to go back to the front page.
Async load textures (not just images). This can be done efficiently by using different opengl contexts in different threads and making the context current right before a heavy opengl operation. All threads need to set their opengl context often.
Downloading files should take into account the remove mime type if available. Fallback to file extension.
-Text images atlas.
+Text images atlas or vertex buffer with multiple active textures (grouped into 32 images to not go over active texture limit).
Do not render invalid unicode.
Use matrix "from" with proper cache.
Text editing should take into consideration FORMATTED_TEXT_START/FORMATTED_TEXT_END.
4chan code syntax highlight. 4chan doesn't say what language it is so we have to somehow guess the language.
Matrix autocomplete for emoji (and custom emoji).
-Cache emoji locally so they can be used (and autocompleted) before sync has finished. \ No newline at end of file
+Nicer custom emoji upload options - Selecting name, size, cropping. \ No newline at end of file
diff --git a/include/Text.hpp b/include/Text.hpp
index 245284f..89aba99 100644
--- a/include/Text.hpp
+++ b/include/Text.hpp
@@ -51,8 +51,9 @@ namespace QuickMedia
}
// If size is {0, 0} then the image is drawn at its original size
- void create_image(std::string url, bool local, mgl::vec2i size) {
+ void create_image(std::string url, bool local, mgl::vec2i size, std::string alt) {
this->url = std::move(url);
+ this->alt = std::move(alt);
this->local = local;
this->size = size;
type = Type::IMAGE;
@@ -68,6 +69,7 @@ namespace QuickMedia
// TODO: Remove these
std::string url;
+ std::string alt;
bool local = false;
mgl::vec2i pos;
mgl::vec2i size;
@@ -94,7 +96,7 @@ namespace QuickMedia
const std::string& getString() const;
void appendText(const std::string &str);
// size = {0, 0} = keep original image size
- static std::string formatted_image(const std::string &url, bool local, mgl::vec2i size);
+ static std::string formatted_image(const std::string &url, bool local, mgl::vec2i size, const std::string &alt);
// text_flags is bit-or of FormattedTextFlag
static std::string formatted_text(const std::string &text, mgl::Color color, uint8_t text_flags);
void insert_text_at_caret_position(const std::string &str);
diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp
index 3613709..66ca0a8 100644
--- a/plugins/Matrix.hpp
+++ b/plugins/Matrix.hpp
@@ -24,7 +24,8 @@ namespace QuickMedia {
std::string extract_first_line_remove_newline_elipses(const std::string &str, size_t max_length);
mgl::Color user_id_to_color(const std::string &user_id);
- std::string formatted_text_to_qm_text(Matrix *matrix, const char *str, size_t size, bool allow_formatted_text);
+ // |image_max_size| 0, 0 means no max size
+ std::string formatted_text_to_qm_text(Matrix *matrix, const char *str, size_t size, bool allow_formatted_text, mgl::vec2i image_max_size = mgl::vec2i(0, 0));
std::string message_to_qm_text(Matrix *matrix, const Message *message, bool allow_formatted_text = true);
struct TimestampedDisplayData {
@@ -718,6 +719,8 @@ namespace QuickMedia {
PluginResult parse_notifications(const rapidjson::Value &notifications_json, std::function<void(const MatrixNotification&)> callback_func);
PluginResult parse_sync_account_data(const rapidjson::Value &account_data_json);
PluginResult parse_sync_room_data(const rapidjson::Value &rooms_json, bool is_additional_messages_sync, bool initial_sync);
+ void parse_custom_emoji(const rapidjson::Value &custom_emoji_json);
+ void load_custom_emoji_from_cache();
PluginResult get_previous_room_messages(RoomData *room_data, bool latest_messages, size_t &num_new_messages, bool *reached_end = nullptr);
void events_add_user_info(const rapidjson::Value &events_json, RoomData *room_data, int64_t timestamp);
std::shared_ptr<UserInfo> parse_user_info(const rapidjson::Value &json, const std::string &user_id, RoomData *room_data, int64_t timestamp);
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index fc3cba6..433442a 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -7007,9 +7007,11 @@ namespace QuickMedia {
struct tm t;
localtime_r(&now, &t);
char filename[256] = {0};
- const int num_bytes_written = strftime(filename, sizeof(filename)-1, "Clipboard_%Y-%m-%d_%H-%M-%S", &t);
- if((int)sizeof(filename) - (num_bytes_written + file_ext.size()) >= 1)
- strcat(filename, file_ext.c_str());
+ if(tmp_file != -1) {
+ const int num_bytes_written = strftime(filename, sizeof(filename)-1, "Clipboard_%Y-%m-%d_%H-%M-%S", &t);
+ if((int)sizeof(filename) - (num_bytes_written + file_ext.size()) >= 1)
+ strcat(filename, file_ext.c_str());
+ }
upload_file(clipboard_text, filename);
}
diff --git a/src/Text.cpp b/src/Text.cpp
index c43944b..50db0ee 100644
--- a/src/Text.cpp
+++ b/src/Text.cpp
@@ -124,10 +124,11 @@ namespace QuickMedia
dirtyText = true;
}
- // TODO: Alt text. Helpful when copying the text. Or do we want to copy the url instead?
// static
- std::string Text::formatted_image(const std::string &url, bool local, mgl::vec2i size) {
+ std::string Text::formatted_image(const std::string &url, bool local, mgl::vec2i size, const std::string &alt) {
const uint32_t str_size = url.size();
+ const uint32_t alt_str_size = alt.size();
+
std::string result;
result += FORMATTED_TEXT_START;
result += (uint8_t)FormattedTextType::IMAGE;
@@ -136,6 +137,8 @@ namespace QuickMedia
result.append((const char*)&local, sizeof(local));
result.append((const char*)&str_size, sizeof(str_size));
result.append(url);
+ result.append((const char*)&alt_str_size, sizeof(alt_str_size));
+ result.append(alt);
result += FORMATTED_TEXT_END;
return result;
}
@@ -182,6 +185,12 @@ namespace QuickMedia
for(auto &text_element : tmp_text_elements) {
if(text_element.type == TextElement::Type::TEXT)
result.append(text_element.text);
+ else if(text_element.type == TextElement::Type::IMAGE) {
+ if(!text_element.alt.empty())
+ result.append(text_element.alt);
+ else
+ result.append(text_element.url);
+ }
}
return result;
}
@@ -435,12 +444,13 @@ namespace QuickMedia
return std::min(5 + sizeof(uint32_t) + text_size + 1, size); // + 1 for FORMATTED_TEXT_END
}
- static size_t parse_formatted_image(const char *str, size_t size, std::string &image_url, bool &image_local, mgl::vec2i &image_size) {
+ static size_t parse_formatted_image(const char *str, size_t size, std::string &image_url, bool &image_local, mgl::vec2i &image_size, std::string &alt) {
image_url.clear();
image_local = true;
image_size = { 0, 0 };
+ alt.clear();
- if(size < sizeof(image_size.x) + sizeof(image_size.y) + sizeof(image_local) + sizeof(uint32_t))
+ if(size < sizeof(image_size.x) + sizeof(image_size.y) + sizeof(image_local) + sizeof(uint32_t) + sizeof(uint32_t))
return size;
size_t offset = 0;
@@ -456,13 +466,24 @@ namespace QuickMedia
uint32_t text_size;
memcpy(&text_size, str + offset, sizeof(text_size));
offset += sizeof(text_size);
+ const size_t image_url_offset = offset;
+
+ if(size < offset + text_size + sizeof(uint32_t))
+ return size;
+
+ offset += text_size;
+ uint32_t alt_size;
+ memcpy(&alt_size, str + offset, sizeof(alt_size));
+ offset += sizeof(alt_size);
+ const size_t alt_offset = offset;
- if(size < offset + text_size)
+ if(size < offset + alt_size)
return size;
- image_url.assign(str + offset, text_size);
+ image_url.assign(str + image_url_offset, text_size);
+ alt.assign(str + alt_offset, alt_size);
image_size = clamp_to_size(image_size, MAX_IMAGE_SIZE);
- return std::min(offset + text_size + 1, size); // + 1 for FORMATTED_TEXT_END
+ return std::min(offset + alt_size + 1, size); // + 1 for FORMATTED_TEXT_END
}
static size_t parse_formatted_text(const char *str, size_t size, TextElement &text_element) {
@@ -477,7 +498,7 @@ namespace QuickMedia
}
case FormattedTextType::IMAGE: {
text_element.type = TextElement::Type::IMAGE;
- return parse_formatted_image(str + 1, size - 1, text_element.url, text_element.local, text_element.size);
+ return parse_formatted_image(str + 1, size - 1, text_element.url, text_element.local, text_element.size, text_element.alt);
}
default:
break;
diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp
index fd5026a..e53cf71 100644
--- a/src/plugins/Matrix.cpp
+++ b/src/plugins/Matrix.cpp
@@ -28,7 +28,7 @@
namespace QuickMedia {
static const mgl::vec2i thumbnail_max_size(600, 337);
- static const mgl::vec2i custom_emoji_max_size(64, 64);
+ static const mgl::vec2i custom_emoji_max_size(32, 32);
static const char* SERVICE_NAME = "matrix";
static const char* OTHERS_ROOM_TAG = "tld.name.others";
// Filter without account data. TODO: We include pinned events but limit events to 1. That means if the last event is a pin,
@@ -640,8 +640,8 @@ namespace QuickMedia {
return message->body;
}
- static std::string message_to_room_description_text(Matrix *matrix, Message *message) {
- std::string body = strip(formatted_text_to_qm_text(matrix, message->body.c_str(), message->body.size(), true));
+ static std::string message_to_room_description_text(Matrix *matrix, Message *message, mgl::vec2i image_max_size = mgl::vec2i(0, 0)) {
+ std::string body = strip(formatted_text_to_qm_text(matrix, message->body.c_str(), message->body.size(), true, image_max_size));
if(message->type == MessageType::REACTION)
return "Reacted with: " + body;
else if(message->related_event_type == RelatedEventType::REPLY)
@@ -708,7 +708,7 @@ namespace QuickMedia {
room_desc += "Unread: ";
if(last_unread_message)
- room_desc += extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(last_unread_message), AUTHOR_MAX_LENGTH) + ": " + message_to_room_description_text(matrix, last_unread_message);
+ room_desc += extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(last_unread_message), AUTHOR_MAX_LENGTH) + ": " + message_to_room_description_text(matrix, last_unread_message, custom_emoji_max_size);
int unread_notification_count = room->unread_notification_count;
if(unread_notification_count > 0 && set_room_as_unread) {
@@ -728,7 +728,7 @@ namespace QuickMedia {
rooms_page->move_room_to_top(room);
room_tags_page->move_room_to_top(room);
} else if(last_new_message) {
- room->body_item->set_description(extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(last_new_message.get()), AUTHOR_MAX_LENGTH) + ": " + message_to_room_description_text(matrix, last_new_message.get()));
+ room->body_item->set_description(extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(last_new_message.get()), AUTHOR_MAX_LENGTH) + ": " + message_to_room_description_text(matrix, last_new_message.get(), custom_emoji_max_size));
room->body_item->set_description_color(get_theme().faded_text_color);
room->body_item->set_description_max_lines(3);
@@ -1503,12 +1503,12 @@ namespace QuickMedia {
sync_is_cache = false;
sync_running = true;
+ load_custom_emoji_from_cache();
+
sync_thread = std::thread([this, matrix_cache_dir]() {
sync_is_cache = true;
- bool cache_available = false;
FILE *sync_cache_file = fopen(matrix_cache_dir.data.c_str(), "rb");
if(sync_cache_file) {
- cache_available = true;
rapidjson::Document doc;
char read_buffer[8192];
rapidjson::FileReadStream is(sync_cache_file, read_buffer, sizeof(read_buffer));
@@ -1949,6 +1949,47 @@ namespace QuickMedia {
return PluginResult::OK;
}
+ void Matrix::parse_custom_emoji(const rapidjson::Value &custom_emoji_json) {
+ if(!custom_emoji_json.IsObject())
+ return;
+
+ std::lock_guard<std::recursive_mutex> lock(room_data_mutex);
+ custom_emoji_by_key.clear();
+ for(auto const &emoji_json : custom_emoji_json.GetObject()) {
+ if(!emoji_json.name.IsString() || !emoji_json.value.IsObject())
+ continue;
+
+ const rapidjson::Value &url_json = GetMember(emoji_json.value, "url");
+ const rapidjson::Value &width_json = GetMember(emoji_json.value, "width");
+ const rapidjson::Value &height_json = GetMember(emoji_json.value, "height");
+ if(!url_json.IsString())
+ continue;
+
+ CustomEmoji custom_emoji;
+ custom_emoji.url = url_json.GetString();
+ if(width_json.IsInt() && height_json.IsInt()) {
+ custom_emoji.size.x = width_json.GetInt();
+ custom_emoji.size.y = height_json.GetInt();
+ }
+ custom_emoji_by_key[emoji_json.name.GetString()] = std::move(custom_emoji);
+ }
+ }
+
+ void Matrix::load_custom_emoji_from_cache() {
+ std::string custom_emoji_file_content;
+ if(file_get_content(get_cache_dir().join("matrix").join("custom_emoji.json"), custom_emoji_file_content) != 0)
+ return;
+
+ rapidjson::Document json_root;
+ rapidjson::ParseResult parse_result = json_root.Parse(custom_emoji_file_content.c_str(), custom_emoji_file_content.size());
+ if(parse_result.IsError()) {
+ fprintf(stderr, "Warning: failed to parse custom_emoji.json, error: %d\n", parse_result.Code());
+ return;
+ }
+
+ parse_custom_emoji(json_root);
+ }
+
PluginResult Matrix::parse_sync_account_data(const rapidjson::Value &account_data_json) {
if(!account_data_json.IsObject())
return PluginResult::OK;
@@ -2004,26 +2045,12 @@ namespace QuickMedia {
}
}
} else if(strcmp(type_json.GetString(), "qm.emoji") == 0) {
- std::lock_guard<std::recursive_mutex> lock(room_data_mutex);
- for(auto const &emoji_json : content_json.GetObject()) {
- if(!emoji_json.name.IsString() || !emoji_json.value.IsObject())
- continue;
-
- const rapidjson::Value &url_json = GetMember(emoji_json.value, "url");
- const rapidjson::Value &width_json = GetMember(emoji_json.value, "width");
- const rapidjson::Value &height_json = GetMember(emoji_json.value, "height");
- if(!url_json.IsString())
- continue;
-
- CustomEmoji custom_emoji;
- custom_emoji.url = url_json.GetString();
- if(width_json.IsInt() && height_json.IsInt()) {
- custom_emoji.size.x = width_json.GetInt();
- custom_emoji.size.y = height_json.GetInt();
- }
- custom_emoji_by_key[emoji_json.name.GetString()] = std::move(custom_emoji);
- }
- }
+ parse_custom_emoji(content_json);
+ rapidjson::StringBuffer buffer;
+ rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
+ content_json.Accept(writer);
+ file_overwrite_atomic(get_cache_dir().join("matrix").join("custom_emoji.json"), std::string(buffer.GetString(), buffer.GetSize()));
+ }
}
return PluginResult::OK;
@@ -2585,9 +2612,11 @@ namespace QuickMedia {
bool supports_syntax_highlight = false;
bool inside_img_tag = false;
std::string_view img_src;
+ std::string_view img_alt;
mgl::vec2i img_size;
mgl::Color font_color = mgl::Color(255, 255, 255, 255);
Matrix *matrix = nullptr;
+ mgl::vec2i image_max_size;
};
static int accumulate_string(char *data, int size, void *userdata) {
@@ -2615,6 +2644,7 @@ namespace QuickMedia {
} else if(html_parser->tag_name.size == 3 && memcmp(html_parser->tag_name.data, "img", 3) == 0) {
parse_userdata.inside_img_tag = true;
parse_userdata.img_src = std::string_view();
+ parse_userdata.img_alt = std::string_view();
parse_userdata.img_size = { 0, 0 };
}
break;
@@ -2637,7 +2667,9 @@ namespace QuickMedia {
// TODO: Better solution when size not given?
if(img_size.x == 0 || img_size.y == 0)
img_size = custom_emoji_max_size;
- parse_userdata.result += Text::formatted_image(parse_userdata.matrix->get_media_url(image_url), false, img_size);
+ if(parse_userdata.image_max_size.x > 0 && parse_userdata.image_max_size.y > 0)
+ img_size = clamp_to_size(img_size, parse_userdata.image_max_size);
+ parse_userdata.result += Text::formatted_image(parse_userdata.matrix->get_media_url(image_url), false, img_size, std::string(parse_userdata.img_alt));
}
parse_userdata.inside_img_tag = false;
}
@@ -2659,6 +2691,8 @@ namespace QuickMedia {
} else if(html_parser->attribute_key.size == 6 && memcmp(html_parser->attribute_key.data, "height", 6) == 0) {
const std::string height(html_parser->attribute_value.data, html_parser->attribute_value.size);
parse_userdata.img_size.y = atoi(height.c_str());
+ } else if(html_parser->attribute_key.size == 3 && memcmp(html_parser->attribute_key.data, "alt", 3) == 0) {
+ parse_userdata.img_alt = std::string_view(html_parser->attribute_value.data, html_parser->attribute_value.size);
}
} else if(!parse_userdata.allow_formatted_text && parse_userdata.inside_img_tag && parse_userdata.mx_reply_depth == 0 && html_parser->attribute_key.size == 3 && memcmp(html_parser->attribute_key.data, "alt", 3) == 0) {
std::string text_to_add(html_parser->attribute_value.data, html_parser->attribute_value.size);
@@ -2712,11 +2746,12 @@ namespace QuickMedia {
return 0;
}
- std::string formatted_text_to_qm_text(Matrix *matrix, const char *str, size_t size, bool allow_formatted_text) {
+ std::string formatted_text_to_qm_text(Matrix *matrix, const char *str, size_t size, bool allow_formatted_text, mgl::vec2i image_max_size) {
FormattedTextParseUserdata parse_userdata;
parse_userdata.allow_formatted_text = allow_formatted_text;
parse_userdata.supports_syntax_highlight = is_program_executable_by_name("source-highlight");
parse_userdata.matrix = matrix;
+ parse_userdata.image_max_size = image_max_size;
html_parser_parse(str, size, formattext_text_parser_callback, &parse_userdata);
return std::move(parse_userdata.result);
}
@@ -4387,6 +4422,8 @@ namespace QuickMedia {
if(download_result != DownloadResult::OK)
return download_result_to_plugin_result(download_result);
+ // TODO: Is this even needed? (including in other /account_data/qm.emoji functions), wont we get qm.emoji update in sync?
+ file_overwrite_atomic(get_cache_dir().join("matrix").join("custom_emoji.json"), std::string(buffer.GetString(), buffer.GetSize()));
std::lock_guard<std::recursive_mutex> lock(room_data_mutex);
custom_emoji_by_key[key] = std::move(custom_emoji);
return PluginResult::OK;
@@ -4428,6 +4465,7 @@ namespace QuickMedia {
if(download_result != DownloadResult::OK)
return false;
+ file_overwrite_atomic(get_cache_dir().join("matrix").join("custom_emoji.json"), std::string(buffer.GetString(), buffer.GetSize()));
std::lock_guard<std::recursive_mutex> lock(room_data_mutex);
auto it = custom_emoji_by_key.find(key);
if(it != custom_emoji_by_key.end())
@@ -4445,6 +4483,10 @@ namespace QuickMedia {
if(it == custom_emoji_list_copy.end())
return false;
+ auto existing_new_it = custom_emoji_list_copy.find(new_key);
+ if(existing_new_it != custom_emoji_list_copy.end())
+ return false;
+
auto custom_emoji_copy = it->second;
custom_emoji_list_copy.erase(it);
custom_emoji_list_copy[new_key] = std::move(custom_emoji_copy);
@@ -4473,6 +4515,7 @@ namespace QuickMedia {
if(download_result != DownloadResult::OK)
return false;
+ file_overwrite_atomic(get_cache_dir().join("matrix").join("custom_emoji.json"), std::string(buffer.GetString(), buffer.GetSize()));
std::lock_guard<std::recursive_mutex> lock(room_data_mutex);
auto it = custom_emoji_by_key.find(key);
if(it != custom_emoji_by_key.end()) {