aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2022-11-07 22:21:52 +0100
committerdec05eba <dec05eba@protonmail.com>2022-11-11 00:53:46 +0100
commite19a29c7e51860144f02d7e7b08ac5e430e1f78f (patch)
treef57adcf98b77a271b4df74a20e2389b73f495df7
parent5d2a7d977f9b0a1604e106f4e2b0c2c9b89c3235 (diff)
Support images in text, add custom emoji to matrix
-rw-r--r--TODO8
m---------depends/html-parser0
m---------depends/html-search0
m---------depends/mglpp0
-rw-r--r--include/Text.hpp7
-rw-r--r--include/types.hpp10
-rw-r--r--plugins/Matrix.hpp71
-rw-r--r--src/Body.cpp6
-rw-r--r--src/QuickMedia.cpp49
-rw-r--r--src/Tabs.cpp1
-rw-r--r--src/Text.cpp138
-rw-r--r--src/main.cpp4
-rw-r--r--src/plugins/Matrix.cpp595
13 files changed, 716 insertions, 173 deletions
diff --git a/TODO b/TODO
index 50571d4..ed04741 100644
--- a/TODO
+++ b/TODO
@@ -5,7 +5,7 @@ Animate page navigation.
Add support for special formatting for posts by admins on imageboards.
For image boards, track (You)'s and show notification when somebody replies to your post.
Go to next chapter when reaching the end of the chapter in image endless mode.
-Make code blocks on matrix and 4chan use monospace and have a background of a different color.
+Make code blocks on matrix and 4chan have a background of a different color.
Allow deleting watch history with delete key (and show confirmation).
Add navigation to nyaa.si submitter torrents.
Create a large texture and add downloaded images to it. This will save memory usage because sfml has to use power of two textures (and so does opengl internally) for textures, so if you have multiple textures they will use more memory than one large texture with the same texture data.
@@ -83,7 +83,6 @@ Improve /sync by not removing cached data on initial sync, and also always appen
then add a gap between old messages from before sync and after sync so we can fetch the messages between the old messages and new messages and remove the gap when the fetched messages contains any of the old messages. After the sync, ignored users messages should be removed from the cache and messages list. Also take into consideration unignoring users.
Fetching of previous messages should also be saved in the /sync file and messages fetched with get_message_by_id, which would cache embedded items and pinned messages; also cache users.
If manga page fails to download then show "failed to download image" as text and bind F5 to refresh (retry download).
-Use <img src to add custom emojis, and add setting for adding/removing custom emoji.
Create multiple BodyItem types. BodyItem has a lot of fields and most of them are not always used.
Remove display names from reactions if there are many reactions, and instead group them into: reaction (#number of this type of reaction); for example: 👍 2.
Make reaction and deleted message provisional.
@@ -225,7 +224,6 @@ Make youtube work with age restricted copy righted videos, such as https://www.y
Upload media once in 4chan, on comment enter and then re-use that after solving captcha; instead of reuploading video after each captcha retry. If possible...
Support migration from one manga service to another, automatically by selecting the manga to migrate from which service (or select all). Also do that in automedia. Give error if it's not possible to do automatically and show option to manually select the matching manga/chapter. Quickmedia should also be able to automatically migrate automedia in the same process or a separate option.
Instead of having an option to disable rounded corners, add an option to set corner radius where radius=0 means disabling the shader.
-Support proper emoji, maybe a different emoji per image.
Consider adding an option to use external sxiv, imv etc instead of mpv/quickmedia image viewer.
Fallback to playing videos/etc from origin homeserver in matrix if the file is larger than the users homeserver allows. This wont work with pantalaimon + encrypted rooms, so it should be delayed until quickmedia supports matrix encryption.
Fix 4chan posting! cloudflare broke shit. Then create external captcha solver program that solves captcha from captcha image.
@@ -246,4 +244,6 @@ Text images atlas.
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. \ No newline at end of file
+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
diff --git a/depends/html-parser b/depends/html-parser
-Subproject 66ec83b862ea2a8dbda1c1f3663af88a8d12d9b
+Subproject 684a3bc56d5c40ed3eb54ca263751fa4724e73f
diff --git a/depends/html-search b/depends/html-search
-Subproject 0dd5817eecb3a8db9338394e2c164119a92d4db
+Subproject 3dbbcc75199bd996163500beb39a6e902bc7ddc
diff --git a/depends/mglpp b/depends/mglpp
-Subproject 0108af496da089dbee70ce80d778dfb5c238460
+Subproject d04c98708fd46524c0861baf65e9e4ff62d4879
diff --git a/include/Text.hpp b/include/Text.hpp
index 02a6b62..245284f 100644
--- a/include/Text.hpp
+++ b/include/Text.hpp
@@ -10,8 +10,6 @@
#include <vector>
#include <string_view>
#include <array>
-#include "types.hpp"
-#include <assert.h>
namespace mgl {
class Font;
@@ -143,7 +141,7 @@ namespace QuickMedia
bool single_line_edit = false;
private:
- enum class CaretMoveDirection : u8
+ enum class CaretMoveDirection : uint8_t
{
NONE,
UP,
@@ -172,7 +170,10 @@ namespace QuickMedia
float font_get_real_height(mgl::Font *font);
float get_text_quad_left_side(const VertexRef &vertex_ref) const;
float get_text_quad_right_side(const VertexRef &vertex_ref) const;
+ float get_text_quad_top_side(const VertexRef &vertex_ref) const;
float get_text_quad_bottom_side(const VertexRef &vertex_ref) const;
+ float get_text_quad_height(const VertexRef &vertex_ref) const;
+ void move_vertex_lines_by_largest_items(int vertices_linear_end);
// If the index is past the end, then the caret offset is the right side of the last character, rather than the left side
float get_caret_offset_by_caret_index(int index) const;
VertexRef& get_vertex_ref_clamp(int index);
diff --git a/include/types.hpp b/include/types.hpp
deleted file mode 100644
index dc2a016..0000000
--- a/include/types.hpp
+++ /dev/null
@@ -1,10 +0,0 @@
-#pragma once
-
-#include <stdint.h>
-
-typedef uint8_t u8;
-typedef uint16_t u16;
-typedef uint32_t u32;
-typedef uint64_t u64;
-typedef intptr_t isize;
-typedef uintptr_t usize; \ No newline at end of file
diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp
index 152c292..3613709 100644
--- a/plugins/Matrix.hpp
+++ b/plugins/Matrix.hpp
@@ -20,9 +20,12 @@ namespace QuickMedia {
static const int AUTHOR_MAX_LENGTH = 48;
+ class Matrix;
+
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(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);
+ std::string message_to_qm_text(Matrix *matrix, const Message *message, bool allow_formatted_text = true);
struct TimestampedDisplayData {
std::string data;
@@ -233,7 +236,7 @@ namespace QuickMedia {
using Rooms = std::vector<RoomData*>;
- bool message_contains_user_mention(const Message *message, const std::string &username, const std::string &user_id);
+ bool message_contains_user_mention(Matrix *matrix, const Message *message, const std::string &username, const std::string &user_id);
bool message_contains_user_mention(const BodyItem *body_item, const std::string &username, const std::string &user_id);
bool message_is_timeline(Message *message);
void body_set_selected_item_by_url(Body *body, const std::string &url);
@@ -450,6 +453,50 @@ namespace QuickMedia {
Matrix *matrix;
};
+ class MatrixCustomEmojiPage : public LazyFetchPage {
+ public:
+ MatrixCustomEmojiPage(Program *program, Matrix *matrix) : LazyFetchPage(program), matrix(matrix) {}
+ const char* get_title() const override { return "Custom emoji"; }
+ PluginResult submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) override;
+ PluginResult lazy_fetch(BodyItems &result_items) override;
+ bool is_ready() override;
+ private:
+ Matrix *matrix;
+ };
+
+ class MatrixCustomEmojiRenameSelectPage : public LazyFetchPage {
+ public:
+ MatrixCustomEmojiRenameSelectPage(Program *program, Matrix *matrix) : LazyFetchPage(program), matrix(matrix) {}
+ const char* get_title() const override { return "Select emoji to rename"; }
+ PluginResult submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) override;
+ PluginResult lazy_fetch(BodyItems &result_items) override;
+ bool submit_is_async() const override { return false; }
+ bool reload_on_page_change() override { return true; }
+ private:
+ Matrix *matrix;
+ };
+
+ class MatrixCustomEmojiRenamePage : public Page {
+ public:
+ MatrixCustomEmojiRenamePage(Program *program, Matrix *matrix, std::string emoji_key) : Page(program), matrix(matrix), emoji_key(std::move(emoji_key)) {}
+ const char* get_title() const override { return "Enter a new name for the emoji"; }
+ PluginResult submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) override;
+ bool allow_submit_no_selection() const override { return true; }
+ private:
+ Matrix *matrix;
+ std::string emoji_key;
+ };
+
+ class MatrixCustomEmojiDeletePage : public Page {
+ public:
+ MatrixCustomEmojiDeletePage(Program *program, Matrix *matrix, Body *body) : Page(program), matrix(matrix), body(body) {}
+ const char* get_title() const override { return "Select emoji to delete"; }
+ PluginResult submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) override;
+ private:
+ Matrix *matrix;
+ Body *body;
+ };
+
// Only play one video. TODO: Play all videos in room, as related videos?
class MatrixVideoPage : public VideoPage {
public:
@@ -559,12 +606,17 @@ namespace QuickMedia {
std::string room_id;
};
+ struct CustomEmoji {
+ std::string url;
+ mgl::vec2i size;
+ };
+
class Matrix {
public:
// TODO: Make this return the Matrix object instead, to force users to call start_sync
bool start_sync(MatrixDelegate *delegate, bool &cached);
void stop_sync();
- bool is_initial_sync_finished() const;
+ bool is_initial_sync_finished();
// Returns true if initial sync failed, and |err_msg| is set to the error reason in that case
bool did_initial_sync_fail(std::string &err_msg);
bool has_finished_fetching_notifications() const;
@@ -590,6 +642,11 @@ namespace QuickMedia {
// If filename is empty then the filename is extracted from filepath
PluginResult post_file(RoomData *room, const std::string &filepath, std::string filename, std::string &event_id_response, std::string &err_msg, void *relates_to = nullptr);
+ PluginResult upload_custom_emoji(const std::string &filepath, const std::string &key, std::string &mxc_url, std::string &err_msg);
+ bool delete_custom_emoji(const std::string &key);
+ bool rename_custom_emoji(const std::string &key, const std::string &new_key);
+ bool does_custom_emoji_with_name_exist(const std::string &name);
+ std::unordered_map<std::string, CustomEmoji> get_custom_emojis();
PluginResult login(const std::string &username, const std::string &password, const std::string &homeserver, std::string &err_msg);
PluginResult logout();
@@ -636,6 +693,8 @@ namespace QuickMedia {
RoomData* get_room_by_id(const std::string &id);
void update_room_users(RoomData *room);
+ std::string get_media_url(const std::string &mxc_id);
+
void append_system_message(RoomData *room_data, std::shared_ptr<Message> message);
std::string body_to_formatted_body(RoomData *room, const std::string &body);
void on_exit_room(RoomData *room);
@@ -657,7 +716,7 @@ namespace QuickMedia {
PluginResult parse_sync_response(const rapidjson::Document &root, bool is_additional_messages_sync, bool initial_sync);
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, std::optional<std::set<std::string>> &dm_rooms);
+ 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);
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);
@@ -673,7 +732,7 @@ namespace QuickMedia {
void remove_rooms(const rapidjson::Value &leave_json);
PluginResult get_pinned_events(RoomData *room, std::vector<std::string> &pinned_events);
std::shared_ptr<Message> parse_message_event(const rapidjson::Value &event_item_json, RoomData *room_data);
- PluginResult upload_file(RoomData *room, const std::string &filepath, std::string filename, UploadInfo &file_info, UploadInfo &thumbnail_info, std::string &err_msg, bool upload_thumbnail = true);
+ PluginResult upload_file(const std::string &filepath, std::string filename, UploadInfo &file_info, UploadInfo &thumbnail_info, std::string &err_msg, bool upload_thumbnail = true);
void add_room(std::unique_ptr<RoomData> room);
void remove_room(const std::string &room_id);
// Returns false if an invite to the room already exists
@@ -701,6 +760,7 @@ namespace QuickMedia {
std::string next_batch;
std::string next_notifications_token;
std::mutex next_batch_mutex;
+ bool initial_sync_finished = false;
std::unordered_map<std::string, Invite> invites;
std::mutex invite_mutex;
@@ -724,5 +784,6 @@ namespace QuickMedia {
std::vector<std::unique_ptr<RoomData>> invite_rooms;
std::unordered_set<std::string> my_events_transaction_ids;
+ std::unordered_map<std::string, CustomEmoji> custom_emoji_by_key;
};
} \ No newline at end of file
diff --git a/src/Body.cpp b/src/Body.cpp
index 5bc4f2a..7411a79 100644
--- a/src/Body.cpp
+++ b/src/Body.cpp
@@ -59,7 +59,7 @@ namespace QuickMedia {
body_spacing[BODY_THEME_MINIMAL].body_padding_vertical = std::floor(10.0f * get_config().scale * get_config().spacing_scale);
body_spacing[BODY_THEME_MINIMAL].reaction_background_padding_x = std::floor(7.0f * get_config().scale * get_config().spacing_scale);
- body_spacing[BODY_THEME_MINIMAL].reaction_background_padding_y = std::floor(3.0f * get_config().scale * get_config().spacing_scale);
+ body_spacing[BODY_THEME_MINIMAL].reaction_background_padding_y = std::floor(5.0f * get_config().scale * get_config().spacing_scale);
body_spacing[BODY_THEME_MINIMAL].reaction_spacing_x = std::floor(5.0f * get_config().scale * get_config().spacing_scale);
body_spacing[BODY_THEME_MINIMAL].reaction_padding_y = std::floor(7.0f * get_config().scale * get_config().spacing_scale);
body_spacing[BODY_THEME_MINIMAL].embedded_item_font_size = std::floor(get_config().body.embedded_load_font_size * get_config().scale * get_config().font_scale);
@@ -76,7 +76,7 @@ namespace QuickMedia {
body_spacing[BODY_THEME_MODERN_SPACIOUS].body_padding_vertical = std::floor(20.0f * get_config().scale * get_config().spacing_scale);
body_spacing[BODY_THEME_MODERN_SPACIOUS].reaction_background_padding_x = std::floor(7.0f * get_config().scale * get_config().spacing_scale);
- body_spacing[BODY_THEME_MODERN_SPACIOUS].reaction_background_padding_y = std::floor(3.0f * get_config().scale * get_config().spacing_scale);
+ body_spacing[BODY_THEME_MODERN_SPACIOUS].reaction_background_padding_y = std::floor(5.0f * get_config().scale * get_config().spacing_scale);
body_spacing[BODY_THEME_MODERN_SPACIOUS].reaction_spacing_x = std::floor(5.0f * get_config().scale * get_config().spacing_scale);
body_spacing[BODY_THEME_MODERN_SPACIOUS].reaction_padding_y = std::floor(7.0f * get_config().scale * get_config().spacing_scale);
body_spacing[BODY_THEME_MODERN_SPACIOUS].embedded_item_font_size = std::floor(get_config().body.embedded_load_font_size * get_config().scale * get_config().font_scale);
@@ -1519,7 +1519,7 @@ namespace QuickMedia {
}
if(reaction.text) {
- reaction.text->set_position(reaction_background.get_position() + mgl::vec2f(body_spacing[body_theme].reaction_background_padding_x, - 4.0f + body_spacing[body_theme].reaction_background_padding_y));
+ reaction.text->set_position(reaction_background.get_position() + mgl::vec2f(body_spacing[body_theme].reaction_background_padding_x, -6.0f + body_spacing[body_theme].reaction_background_padding_y));
reaction_background.draw(window);
reaction.text->draw(window);
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index c528056..fc3cba6 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -71,7 +71,6 @@ extern "C" {
static int FPS_IDLE;
static const double IDLE_TIMEOUT_SEC = 2.0;
static const mgl::vec2i AVATAR_THUMBNAIL_SIZE(std::floor(32), std::floor(32));
-static const float more_items_height = 2.0f;
static const int FPS_SYNC_TO_VSYNC = 0;
static const std::pair<const char*, const char*> valid_plugins[] = {
@@ -5319,17 +5318,10 @@ namespace QuickMedia {
return load_cached_related_embedded_item(body_item, message, me.get(), current_room->get_user_display_name(me), me->user_id, message_body_items);
}
- static std::string message_to_qm_text(Message *message) {
- if(message->body_is_formatted)
- return formatted_text_to_qm_text(message->body.c_str(), message->body.size(), true);
- else
- return message->body;
- }
-
- static std::shared_ptr<BodyItem> message_to_body_item(RoomData *room, Message *message, const std::string &my_display_name, const std::string &my_user_id) {
+ static std::shared_ptr<BodyItem> message_to_body_item(Matrix *matrix, RoomData *room, Message *message, const std::string &my_display_name, const std::string &my_user_id) {
auto body_item = BodyItem::create("");
body_item->set_author(extract_first_line_remove_newline_elipses(room->get_user_display_name(message->user), AUTHOR_MAX_LENGTH));
- body_item->set_description(strip(message_to_qm_text(message)));
+ body_item->set_description(strip(message_to_qm_text(matrix, message)));
body_item->set_timestamp(message->timestamp);
if(!message->thumbnail_url.empty()) {
body_item->thumbnail_url = message->thumbnail_url;
@@ -5364,10 +5356,10 @@ namespace QuickMedia {
return body_item;
}
- static BodyItems messages_to_body_items(RoomData *room, const Messages &messages, const std::string &my_display_name, const std::string &my_user_id) {
+ static BodyItems messages_to_body_items(Matrix *matrix, RoomData *room, const Messages &messages, const std::string &my_display_name, const std::string &my_user_id) {
BodyItems result_items(messages.size());
for(size_t i = 0; i < messages.size(); ++i) {
- result_items[i] = message_to_body_item(room, messages[i].get(), my_display_name, my_user_id);
+ result_items[i] = message_to_body_item(matrix, room, messages[i].get(), my_display_name, my_user_id);
}
return result_items;
}
@@ -5659,10 +5651,10 @@ namespace QuickMedia {
// TODO: Properly check reply message objects for mention of user instead of message data, but only when synapse fixes that notifications
// are not triggered by reply to a message with our display name/user id.
Message *edited_message_ref = static_cast<Message*>(body_item->userdata);
- std::string qm_formatted_text = message_to_qm_text(message.get());
+ std::string qm_formatted_text = message_to_qm_text(matrix, message.get());
body_item->set_description(std::move(qm_formatted_text));
- if(message->user != me && message_contains_user_mention(message.get(), my_display_name, me->user_id))
+ if(message->user != me && message_contains_user_mention(matrix, message.get(), my_display_name, me->user_id))
body_item->set_description_color(get_theme().attention_alert_text_color, true);
else
body_item->set_description_color(get_theme().text_color);
@@ -5700,10 +5692,10 @@ namespace QuickMedia {
// TODO: Properly check reply message objects for mention of user instead of message data, but only when synapse fixes that notifications
// are not triggered by reply to a message with our display name/user id.
Message *edited_message_ref = static_cast<Message*>(body_item->userdata);
- std::string qm_formatted_text = formatted_text_to_qm_text(message->body.c_str(), message->body.size(), true);
+ std::string qm_formatted_text = formatted_text_to_qm_text(matrix, message->body.c_str(), message->body.size(), true);
body_item->set_description(std::move(qm_formatted_text));
- if(message->user != me && message_contains_user_mention(message.get(), my_display_name, me->user_id))
+ if(message->user != me && message_contains_user_mention(matrix, message.get(), my_display_name, me->user_id))
body_item->set_description_color(get_theme().attention_alert_text_color, true);
else
body_item->set_description_color(get_theme().text_color);
@@ -5832,7 +5824,7 @@ namespace QuickMedia {
fetched_messages_set.insert(message->event_id);
}
auto me = matrix->get_me(current_room);
- auto new_body_items = messages_to_body_items(current_room, all_messages, current_room->get_user_display_name(me), me->user_id);
+ auto new_body_items = messages_to_body_items(matrix, current_room, all_messages, current_room->get_user_display_name(me), me->user_id);
messages_load_cached_related_embedded_item(new_body_items, tabs[MESSAGES_TAB_INDEX].body->get_items(), me, current_room);
tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps(std::move(new_body_items));
modify_related_messages_in_current_room(all_messages);
@@ -6143,7 +6135,7 @@ namespace QuickMedia {
message->type = MessageType::REACTION;
message->related_event_type = RelatedEventType::REACTION;
message->related_event_id = static_cast<Message*>(related_to_message)->event_id;
- auto body_item = message_to_body_item(current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id);
+ auto body_item = message_to_body_item(matrix, current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id);
load_cached_related_embedded_item(body_item.get(), message.get(), me, current_room, tabs[MESSAGES_TAB_INDEX].body->get_items());
tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps({body_item});
Messages messages;
@@ -6158,7 +6150,7 @@ namespace QuickMedia {
return provisional_message;
});
} else {
- auto body_item = message_to_body_item(current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id);
+ auto body_item = message_to_body_item(matrix, current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id);
body_item->set_description_color(get_theme().provisional_message_color);
load_cached_related_embedded_item(body_item.get(), message.get(), me, current_room, tabs[MESSAGES_TAB_INDEX].body->get_items());
tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps({body_item});
@@ -6183,7 +6175,7 @@ namespace QuickMedia {
void *related_to_message = currently_operating_on_item->userdata;
message->related_event_type = RelatedEventType::REPLY;
message->related_event_id = static_cast<Message*>(related_to_message)->event_id;
- auto body_item = message_to_body_item(current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id);
+ auto body_item = message_to_body_item(matrix, current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id);
body_item->set_description_color(get_theme().provisional_message_color);
load_cached_related_embedded_item(body_item.get(), message.get(), me, current_room, tabs[MESSAGES_TAB_INDEX].body->get_items());
tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps({body_item});
@@ -6212,13 +6204,13 @@ namespace QuickMedia {
auto body_item = find_body_item_by_event_id(tabs[MESSAGES_TAB_INDEX].body->get_items().data(), tabs[MESSAGES_TAB_INDEX].body->get_items().size(), message->related_event_id, &body_item_index);
if(body_item) {
const std::string formatted_text = matrix->body_to_formatted_body(current_room, text);
- std::string qm_formatted_text = formatted_text_to_qm_text(formatted_text.c_str(), formatted_text.size(), true);
+ std::string qm_formatted_text = formatted_text_to_qm_text(matrix, formatted_text.c_str(), formatted_text.size(), true);
auto body_item_shared_ptr = tabs[MESSAGES_TAB_INDEX].body->get_item_by_index(body_item_index);
body_item_shared_ptr->set_description(std::move(qm_formatted_text));
body_item_shared_ptr->set_description_color(get_theme().provisional_message_color);
- auto edit_body_item = message_to_body_item(current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id);
+ auto edit_body_item = message_to_body_item(matrix, current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id);
edit_body_item->visible = false;
load_cached_related_embedded_item(edit_body_item.get(), message.get(), me, current_room, tabs[MESSAGES_TAB_INDEX].body->get_items());
tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps({edit_body_item});
@@ -6466,7 +6458,7 @@ namespace QuickMedia {
}
all_messages.insert(all_messages.end(), message_list->begin(), message_list->end());
- auto new_body_items = messages_to_body_items(current_room, *message_list, current_room->get_user_display_name(me), me->user_id);
+ auto new_body_items = messages_to_body_items(matrix, current_room, *message_list, current_room->get_user_display_name(me), me->user_id);
messages_load_cached_related_embedded_item(new_body_items, tabs[MESSAGES_TAB_INDEX].body->get_items(), me, current_room);
tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps(std::move(new_body_items));
modify_related_messages_in_current_room(*message_list);
@@ -6536,7 +6528,7 @@ namespace QuickMedia {
}
};
- auto add_new_messages_to_current_room = [&me, &tabs, &ui_tabs, &current_room, MESSAGES_TAB_INDEX, &after_token](Messages &messages) {
+ auto add_new_messages_to_current_room = [this, &me, &tabs, &ui_tabs, &current_room, MESSAGES_TAB_INDEX, &after_token](Messages &messages) {
if(messages.empty())
return;
@@ -6555,7 +6547,7 @@ namespace QuickMedia {
if(!after_token.empty())
scroll_to_end = false;
- auto new_body_items = messages_to_body_items(current_room, messages, current_room->get_user_display_name(me), me->user_id);
+ auto new_body_items = messages_to_body_items(matrix, current_room, messages, current_room->get_user_display_name(me), me->user_id);
messages_load_cached_related_embedded_item(new_body_items, tabs[MESSAGES_TAB_INDEX].body->get_items(), me, current_room);
tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps(std::move(new_body_items));
if(scroll_to_end)
@@ -7489,7 +7481,7 @@ namespace QuickMedia {
if(fetch_message_tab == PINNED_TAB_INDEX) {
PinnedEventData *event_data = static_cast<PinnedEventData*>(fetch_body_item->userdata);
if(fetch_message_result.message) {
- *fetch_body_item = *message_to_body_item(current_room, fetch_message_result.message.get(), current_room->get_user_display_name(me), me->user_id);
+ *fetch_body_item = *message_to_body_item(matrix, current_room, fetch_message_result.message.get(), current_room->get_user_display_name(me), me->user_id);
event_data->status = FetchStatus::FINISHED_LOADING;
event_data->message = fetch_message_result.message.get();
fetch_body_item->userdata = event_data;
@@ -7499,7 +7491,7 @@ namespace QuickMedia {
}
} else if(fetch_message_tab == MESSAGES_TAB_INDEX) {
if(fetch_message_result.message) {
- fetch_body_item->embedded_item = message_to_body_item(current_room, fetch_message_result.message.get(), current_room->get_user_display_name(me), me->user_id);
+ fetch_body_item->embedded_item = message_to_body_item(matrix, current_room, fetch_message_result.message.get(), current_room->get_user_display_name(me), me->user_id);
fetch_body_item->embedded_item_status = FetchStatus::FINISHED_LOADING;
if(fetch_message_result.message->user == me)
fetch_body_item->set_description_color(get_theme().attention_alert_text_color, true);
@@ -7846,6 +7838,9 @@ namespace QuickMedia {
auto matrix_room_directory_page = std::make_unique<MatrixRoomDirectoryPage>(this, matrix);
auto settings_body = create_body();
+ auto custom_emoji_body_item = BodyItem::create("Custom emoji");
+ custom_emoji_body_item->url = "emoji";
+ settings_body->append_item(std::move(custom_emoji_body_item));
auto join_body_item = BodyItem::create("Join room");
join_body_item->url = "join";
settings_body->append_item(std::move(join_body_item));
diff --git a/src/Tabs.cpp b/src/Tabs.cpp
index a5c371a..41554fb 100644
--- a/src/Tabs.cpp
+++ b/src/Tabs.cpp
@@ -8,6 +8,7 @@
#include <mglpp/window/Event.hpp>
#include <mglpp/window/Window.hpp>
#include <mglpp/graphics/Texture.hpp>
+#include <assert.h>
namespace QuickMedia {
static float floor(float v) {
diff --git a/src/Text.cpp b/src/Text.cpp
index 3ecf24c..c43944b 100644
--- a/src/Text.cpp
+++ b/src/Text.cpp
@@ -4,10 +4,12 @@
#include "../include/Theme.hpp"
#include "../include/AsyncImageLoader.hpp"
#include "../include/StringUtils.hpp"
+#include "../include/Scale.hpp"
#include "../generated/Emoji.hpp"
#include <string.h>
#include <stack>
#include <unistd.h>
+#include <assert.h>
#include <mglpp/graphics/Rectangle.hpp>
#include <mglpp/window/Event.hpp>
#include <mglpp/window/Window.hpp>
@@ -45,6 +47,8 @@ namespace QuickMedia
static const uint8_t FORMATTED_TEXT_START = '\x02';
static const uint8_t FORMATTED_TEXT_END = '\x03';
+ static const mgl::vec2i MAX_IMAGE_SIZE(300, 300);
+
enum class FormattedTextType : uint8_t {
TEXT,
IMAGE
@@ -92,6 +96,7 @@ namespace QuickMedia
setString(std::move(_str));
}
+ // TODO: Validate |str|. Turn |str| into a valid utf-8 string
void Text::setString(std::string str)
{
//if(str != this->str)
@@ -119,6 +124,7 @@ 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) {
const uint32_t str_size = url.size();
@@ -455,6 +461,7 @@ namespace QuickMedia
return size;
image_url.assign(str + offset, text_size);
+ image_size = clamp_to_size(image_size, MAX_IMAGE_SIZE);
return std::min(offset + text_size + 1, size); // + 1 for FORMATTED_TEXT_END
}
@@ -518,7 +525,8 @@ namespace QuickMedia
text_elements.push_back(std::move(text_element));
} else {
text_element.text_num_bytes = 1;
- text_elements.push_back(std::move(text_element));
+ if(!text_element.url.empty())
+ text_elements.push_back(std::move(text_element));
}
}
} else if(match_emoji_sequence((const unsigned char*)str.data() + index, size - index, emoji_sequence, emoji_sequence_length, emoji_byte_length)) {
@@ -585,10 +593,18 @@ namespace QuickMedia
return vertices[vertex_ref.vertices_index][vertex_ref.index + 5].position.x;
}
+ float Text::get_text_quad_top_side(const VertexRef &vertex_ref) const {
+ return vertices[vertex_ref.vertices_index][vertex_ref.index + 1].position.y;
+ }
+
float Text::get_text_quad_bottom_side(const VertexRef &vertex_ref) const {
return vertices[vertex_ref.vertices_index][vertex_ref.index + 4].position.y;
}
+ float Text::get_text_quad_height(const VertexRef &vertex_ref) const {
+ return get_text_quad_bottom_side(vertex_ref) - get_text_quad_top_side(vertex_ref);
+ }
+
float Text::get_caret_offset_by_caret_index(int index) const {
const int num_vertices = vertices_linear.size();
if(num_vertices == 0)
@@ -636,6 +652,69 @@ namespace QuickMedia
static mgl::vec2f vec2f_floor(mgl::vec2f value) {
return mgl::vec2f((int)value.x, (int)value.y);
}
+
+ void Text::move_vertex_lines_by_largest_items(int vertices_linear_end) {
+ if(vertices_linear.empty() || vertices_linear_end == 0)
+ return;
+
+ mgl::Font *latin_font;
+ if(bold_font)
+ latin_font = FontLoader::get_font(FontLoader::FontType::LATIN_BOLD, characterSize);
+ else
+ latin_font = FontLoader::get_font(FontLoader::FontType::LATIN, characterSize);
+
+ const float vspace = font_get_real_height(latin_font);
+ const float vertex_height = get_text_quad_height(vertices_linear[0]);
+ float vertex_max_height = std::max(vertex_height, vspace);
+ float vertex_second_max_height = vspace;
+ int current_line = vertices_linear[0].line;
+ int current_line_vertices_linear_start = 0;
+ float move_y = 0.0f;
+
+ for(int i = 0; i < vertices_linear_end; ++i) {
+ VertexRef &vertex_ref = vertices_linear[i];
+ mgl::Vertex *vertex = &vertices[vertex_ref.vertices_index][vertex_ref.index];
+ for(int v = 0; v < 6; ++v) {
+ vertex[v].position.y += move_y;
+ }
+
+ if(vertices_linear[i].line != current_line) {
+ const float vertices_move_down_offset = vertex_max_height - vertex_second_max_height;
+ if(vertex_max_height > vspace/* && vertex_max_height - vertex_min_height > 2.0f*/) {
+ for(int j = current_line_vertices_linear_start; j <= i; ++j) {
+ VertexRef &vertex_ref = vertices_linear[j];
+ mgl::Vertex *vertex = &vertices[vertex_ref.vertices_index][vertex_ref.index];
+ for(int v = 0; v < 6; ++v) {
+ vertex[v].position.y += vertices_move_down_offset;
+ }
+ }
+ move_y += vertices_move_down_offset;
+ }
+
+ vertex_max_height = vspace;
+ current_line = vertices_linear[i].line;
+ current_line_vertices_linear_start = i;
+ }
+
+ const float vertex_height = std::max(get_text_quad_height(vertex_ref), vspace);
+ if(vertex_height > vertex_max_height) {
+ vertex_second_max_height = vertex_max_height;
+ vertex_max_height = vertex_height;
+ }
+ }
+
+ const float vertices_move_down_offset = vertex_max_height - vertex_second_max_height;
+ if(vertex_max_height > vspace/* && vertex_max_height - vertex_min_height > 2.0f*/) {
+ // TODO: current_line_vertices_linear_start vs vertices_linear_end
+ for(int j = current_line_vertices_linear_start; j < vertices_linear_end; ++j) {
+ VertexRef &vertex_ref = vertices_linear[j];
+ mgl::Vertex *vertex = &vertices[vertex_ref.vertices_index][vertex_ref.index];
+ for(int v = 0; v < 6; ++v) {
+ vertex[v].position.y += vertices_move_down_offset;
+ }
+ }
+ }
+ }
void Text::updateGeometry(bool update_even_if_not_dirty) {
if(dirtyText) {
@@ -668,8 +747,8 @@ namespace QuickMedia
latin_font = FontLoader::get_font(FontLoader::FontType::LATIN, characterSize);
const float latin_font_width = latin_font->get_glyph(' ').advance;
- const float hspace_latin = latin_font_width + characterSpacing;
const float vspace = font_get_real_height(latin_font);
+ const float hspace_latin = latin_font_width + characterSpacing;
const float emoji_spacing = 2.0f;
int hspace_monospace = 0;
@@ -686,7 +765,7 @@ namespace QuickMedia
mgl::vec2f glyphPos;
uint32_t prevCodePoint = 0;
// TODO: Only do this if dirtyText (then the Text object shouldn't be reset in Body. There should be a cleanup function in text instead)
- for(usize textElementIndex = 0; textElementIndex < textElements.size(); ++textElementIndex)
+ for(int textElementIndex = 0; textElementIndex < (int)textElements.size(); ++textElementIndex)
{
TextElement &textElement = textElements[textElementIndex];
@@ -714,11 +793,11 @@ namespace QuickMedia
if(prevCodePoint != 0)
glyphPos.x += emoji_spacing;
- const float font_height_offset = 0.0f;//floor(vspace * 0.6f);
- mgl::vec2f vertexTopLeft(glyphPos.x, glyphPos.y + font_height_offset - textElement.size.y * 0.5f);
- mgl::vec2f vertexTopRight(glyphPos.x + textElement.size.x, glyphPos.y + font_height_offset - textElement.size.y * 0.5f);
- mgl::vec2f vertexBottomLeft(glyphPos.x, glyphPos.y + font_height_offset + textElement.size.y * 0.5f);
- mgl::vec2f vertexBottomRight(glyphPos.x + textElement.size.x, glyphPos.y + font_height_offset + textElement.size.y * 0.5f);
+ const float font_height_offset = vspace;
+ mgl::vec2f vertexTopLeft(glyphPos.x, glyphPos.y + font_height_offset - textElement.size.y);
+ mgl::vec2f vertexTopRight(glyphPos.x + textElement.size.x, glyphPos.y + font_height_offset - textElement.size.y);
+ mgl::vec2f vertexBottomLeft(glyphPos.x, glyphPos.y + font_height_offset);
+ mgl::vec2f vertexBottomRight(glyphPos.x + textElement.size.x, glyphPos.y + font_height_offset);
vertexTopLeft = vec2f_floor(vertexTopLeft);
vertexTopRight = vec2f_floor(vertexTopRight);
@@ -1018,6 +1097,7 @@ namespace QuickMedia
}
}
vertices_linear_done:;
+ move_vertex_lines_by_largest_items(vertices_linear_index);
// TODO: Optimize
for(TextElement &textElement : textElements) {
@@ -1052,19 +1132,19 @@ namespace QuickMedia
boundingBox.size.y = 0.0f;
for(VertexRef &vertex_ref : vertices_linear) {
boundingBox.size.x = std::max(boundingBox.size.x, get_text_quad_right_side(vertex_ref));
- //boundingBox.size.y = std::max(boundingBox.size.y, get_text_quad_bottom_side(vertex_ref));
+ boundingBox.size.y = std::max(boundingBox.size.y, get_text_quad_bottom_side(vertex_ref));
}
- boundingBox.size.y = num_lines * line_height;
+ //boundingBox.size.y = num_lines * line_height;
//boundingBox.size.y = text_offset_y;
// TODO:
- //if(vertices_linear.empty())
- // boundingBox.size.y = line_height;
+ if(vertices_linear.empty())
+ boundingBox.size.y = line_height;
- //if(editable)
- // boundingBox.size.y = num_lines * line_height;
+ if(editable)
+ boundingBox.size.y = num_lines * line_height;
// TODO: Clear |vertices| somehow even with editable text
for(size_t i = 0; i < FONT_ARRAY_SIZE; ++i) {
@@ -1458,16 +1538,21 @@ namespace QuickMedia
target.draw(vertex_buffers[FONT_INDEX_EMOJI]);
}*/
+ // TODO: Use rounded rectangle for fallback image
+
// TODO: Use a new vector with only the image data instead of this.
// TODO: Sprite
mgl::Sprite sprite;
- mgl::Rectangle fallback_emoji(mgl::vec2f(vspace, vspace));
- fallback_emoji.set_color(get_theme().shade_color);
+ mgl::Rectangle fallback_image(mgl::vec2f(vspace, vspace));
+ fallback_image.set_color(get_theme().image_loading_background_color);
for(const TextElement &textElement : textElements) {
if(textElement.text_type == TextElement::TextType::EMOJI) {
- if(textElement.pos.to_vec2f().y + vspace > boundingBox.size.y)
+ // TODO:
+ if(textElement.pos.to_vec2f().y + vspace > boundingBox.size.y + 10.0f) {
+ fprintf(stderr, "bounding box y: %f\n", boundingBox.size.y);
continue;
+ }
auto emoji_data = AsyncImageLoader::get_instance().get_thumbnail(textElement.url, textElement.local, { (int)vspace, (int)vspace });
if(emoji_data->loading_state == LoadingState::FINISHED_LOADING) {
@@ -1480,18 +1565,17 @@ namespace QuickMedia
if(emoji_data->loading_state == LoadingState::APPLIED_TO_TEXTURE && emoji_data->texture.get_size().x > 0) {
sprite.set_texture(&emoji_data->texture);
sprite.set_position(pos + textElement.pos.to_vec2f());
- sprite.set_size(textElement.size.to_vec2f());
+ //sprite.set_size(textElement.size.to_vec2f());
target.draw(sprite);
} else {
- fallback_emoji.set_position(pos + textElement.pos.to_vec2f());
- target.draw(fallback_emoji);
+ fallback_image.set_position(pos + textElement.pos.to_vec2f());
+ target.draw(fallback_image);
}
}
}
// TODO: Use a new vector with only the image data instead of this.
// TODO: Sprite
- #if 0
for(const TextElement &textElement : textElements) {
if(textElement.type == TextElement::Type::IMAGE) {
auto thumbnail_data = AsyncImageLoader::get_instance().get_thumbnail(textElement.url, textElement.local, textElement.size);
@@ -1502,18 +1586,22 @@ namespace QuickMedia
thumbnail_data->loading_state = LoadingState::APPLIED_TO_TEXTURE;
}
- if(thumbnail_data->loading_state == LoadingState::APPLIED_TO_TEXTURE) {
- if(textElement.pos.to_vec2f().y + thumbnail_data->texture->get_size().y > boundingBox.size.y)
+ if(thumbnail_data->loading_state == LoadingState::APPLIED_TO_TEXTURE && thumbnail_data->texture.get_size().x > 0) {
+ // TODO:
+ if(textElement.pos.to_vec2f().y + thumbnail_data->texture.get_size().y > boundingBox.size.y + 10.0f)
continue;
sprite.set_texture(&thumbnail_data->texture);
sprite.set_position(pos + textElement.pos.to_vec2f());
- sprite.set_size(textElement.size.to_vec2f());
+ //sprite.set_size(textElement.size.to_vec2f());
target.draw(sprite);
+ } else {
+ fallback_image.set_size(textElement.size.to_vec2f());
+ fallback_image.set_position(pos + textElement.pos.to_vec2f());
+ target.draw(fallback_image);
}
}
}
- #endif
if(!editable) return true;
pos.y -= floor(vspace*1.25f);
diff --git a/src/main.cpp b/src/main.cpp
index 52cb374..ef0568b 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,9 +1,7 @@
#include "../include/QuickMedia.hpp"
-#include <unistd.h>
-#include <X11/Xlib.h>
+#include <locale.h>
int main(int argc, char **argv) {
- XInitThreads();
setlocale(LC_ALL, "C"); // Sigh... stupid C
QuickMedia::Program program;
return program.run(argc, argv);
diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp
index f79b10c..28b4823 100644
--- a/src/plugins/Matrix.cpp
+++ b/src/plugins/Matrix.cpp
@@ -9,6 +9,7 @@
#include "../../include/AsyncImageLoader.hpp"
#include "../../include/Config.hpp"
#include "../../include/Theme.hpp"
+#include "../../include/Scale.hpp"
#include <rapidjson/document.h>
#include <rapidjson/writer.h>
#include <rapidjson/stringbuffer.h>
@@ -27,14 +28,15 @@
namespace QuickMedia {
static const mgl::vec2i thumbnail_max_size(600, 337);
+ static const mgl::vec2i custom_emoji_max_size(64, 64);
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,
// then we cant see room message preview. TODO: Fix this somehow.
// TODO: What about state events in initial sync in timeline? such as user display name change.
- static const char* INITIAL_FILTER = "{\"presence\":{\"limit\":0,\"types\":[\"\"]},\"account_data\":{\"limit\":0,\"types\":[\"\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"types\":[\"m.room.message\"],\"limit\":1,\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"limit\":1,\"types\":[\"m.fully_read\",\"m.tag\",\"qm.last_read_message_timestamp\"],\"lazy_load_members\":true}}}";
- static const char* ADDITIONAL_MESSAGES_FILTER = "{\"presence\":{\"limit\":0,\"types\":[\"\"]},\"account_data\":{\"limit\":0,\"types\":[\"\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"limit\":20,\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true}}}";
- static const char* CONTINUE_FILTER = "{\"presence\":{\"limit\":0,\"types\":[\"\"]},\"account_data\":{\"limit\":0,\"types\":[\"\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"types\":[\"m.fully_read\",\"m.tag\",\"qm.last_read_message_timestamp\"],\"lazy_load_members\":true}}}";
+ static const char* INITIAL_FILTER = "{\"presence\":{\"limit\":0,\"types\":[\"\"]},\"account_data\":{\"types\":[\"qm.emoji\",\"m.direct\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"types\":[\"m.room.message\"],\"limit\":1,\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"limit\":1,\"types\":[\"m.fully_read\",\"m.tag\",\"qm.last_read_message_timestamp\"],\"lazy_load_members\":true}}}";
+ static const char* ADDITIONAL_MESSAGES_FILTER = "{\"presence\":{\"types\":[\"\"]},\"account_data\":{\"limit\":0,\"types\":[\"\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"limit\":20,\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true}}}";
+ static const char* CONTINUE_FILTER = "{\"presence\":{\"limit\":0,\"types\":[\"\"]},\"account_data\":{\"types\":[\"qm.emoji\",\"m.direct\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"types\":[\"m.fully_read\",\"m.tag\",\"qm.last_read_message_timestamp\"],\"lazy_load_members\":true}}}";
static std::string capitalize(const std::string &str) {
if(str.size() >= 1)
@@ -49,6 +51,8 @@ namespace QuickMedia {
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, "direct") == 0)
+ return "Direct messages";
else if(strcmp(tag.c_str() + 2, "lowpriority") == 0)
return "Low priority";
else if(strcmp(tag.c_str() + 2, "server_notice") == 0)
@@ -117,7 +121,7 @@ namespace QuickMedia {
return colors[color_hash_code(user_id) % num_colors];
}
- static std::string remove_reply_formatting(const std::string &str) {
+ static std::string remove_reply_formatting(Matrix *matrix, const std::string &str) {
if(strncmp(str.c_str(), "> <@", 4) == 0) {
size_t index = str.find("> ", 4);
if(index != std::string::npos) {
@@ -126,12 +130,12 @@ namespace QuickMedia {
return str.substr(msg_begin + 2);
}
} else {
- return formatted_text_to_qm_text(str.c_str(), str.size(), false);
+ return formatted_text_to_qm_text(matrix, str.c_str(), str.size(), false);
}
return str;
}
- static std::string remove_reply_formatting(const Message *message, bool keep_formatted = false) {
+ static std::string remove_reply_formatting(Matrix *matrix, const Message *message, bool keep_formatted = false) {
if(!message->body_is_formatted && strncmp(message->body.c_str(), "> <@", 4) == 0) {
size_t index = message->body.find("> ", 4);
if(index != std::string::npos) {
@@ -144,7 +148,7 @@ namespace QuickMedia {
if(keep_formatted)
return message->body;
else
- return formatted_text_to_qm_text(message->body.c_str(), message->body.size(), false);
+ return formatted_text_to_qm_text(matrix, message->body.c_str(), message->body.size(), false);
}
}
@@ -464,7 +468,7 @@ namespace QuickMedia {
if(!sync_is_cache && message_dir == MessageDirection::AFTER) {
for(auto &message : messages) {
if(message->notification_mentions_me) {
- std::string body = remove_reply_formatting(message.get());
+ std::string body = remove_reply_formatting(matrix, message->body);
bool read = 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) && message->related_event_type != RelatedEventType::EDIT && message->related_event_type != RelatedEventType::REDACTION) {
@@ -629,15 +633,15 @@ namespace QuickMedia {
return nullptr;
}
- static std::string message_to_qm_text(const Message *message, bool allow_formatted_text = true) {
+ std::string message_to_qm_text(Matrix *matrix, const Message *message, bool allow_formatted_text) {
if(message->body_is_formatted)
- return formatted_text_to_qm_text(message->body.c_str(), message->body.size(), allow_formatted_text);
+ return formatted_text_to_qm_text(matrix, message->body.c_str(), message->body.size(), allow_formatted_text);
else
return message->body;
}
- static std::string message_to_room_description_text(Message *message) {
- std::string body = strip(message_to_qm_text(message));
+ 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));
if(message->type == MessageType::REACTION)
return "Reacted with: " + body;
else if(message->related_event_type == RelatedEventType::REPLY)
@@ -704,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(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);
int unread_notification_count = room->unread_notification_count;
if(unread_notification_count > 0 && set_room_as_unread) {
@@ -724,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(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()));
room->body_item->set_description_color(get_theme().faded_text_color);
room->body_item->set_description_max_lines(3);
@@ -974,6 +978,9 @@ namespace QuickMedia {
matrix->logout();
program->set_go_to_previous_page();
return PluginResult::OK;
+ } else if(args.url == "emoji") {
+ result_tabs.push_back(Tab{create_body(), std::make_unique<MatrixCustomEmojiPage>(program, matrix), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ return PluginResult::OK;
} else {
return PluginResult::ERR;
}
@@ -991,9 +998,170 @@ namespace QuickMedia {
} else {
show_notification("QuickMedia", "Failed to join " + args.title, Urgency::CRITICAL);
}
+
+ return PluginResult::OK;
+ }
+
+ static const char* file_get_filename(const std::string &filepath) {
+ size_t index = filepath.rfind('/');
+ if(index == std::string::npos)
+ return filepath.c_str();
+ return filepath.c_str() + index + 1;
+ }
+
+ static bool generate_random_characters(char *buffer, int buffer_size) {
+ int fd = open("/dev/urandom", O_RDONLY);
+ if(fd == -1) {
+ perror("/dev/urandom");
+ return false;
+ }
+
+ if(read(fd, buffer, buffer_size) < buffer_size) {
+ fprintf(stderr, "Failed to read %d bytes from /dev/urandom\n", buffer_size);
+ close(fd);
+ return false;
+ }
+
+ close(fd);
+ return true;
+ }
+
+ static std::string random_characters_to_readable_string(const char *buffer, int buffer_size) {
+ std::ostringstream result;
+ result << std::hex;
+ for(int i = 0; i < buffer_size; ++i)
+ result << (int)(unsigned char)buffer[i];
+ return result.str();
+ }
+
+ PluginResult MatrixCustomEmojiPage::submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) {
+ if(args.url == "add") {
+ auto submit_handler = [this](FileManagerPage*, const std::filesystem::path &filepath) {
+ program->run_task_with_loading_screen([this, filepath] {
+ std::string key = filepath.filename().string();
+ if(!key.empty()) {
+ size_t ext_index = key.rfind('.');
+ if(ext_index != std::string::npos)
+ key = key.substr(0, ext_index);
+ }
+
+ if(key.empty()) {
+ char random_characters[10];
+ if(!generate_random_characters(random_characters, sizeof(random_characters))) {
+ show_notification("QuickMedia", "Failed to generate random string", Urgency::CRITICAL);
+ return false;
+ }
+ key = random_characters_to_readable_string(random_characters, sizeof(random_characters));
+ }
+
+ if(matrix->does_custom_emoji_with_name_exist(key)) {
+ show_notification("QuickMedia", "Failed to upload custom emoji. You already have a custom emoji with the name " + key, Urgency::CRITICAL);
+ return false;
+ }
+
+ std::string mxc_url;
+ std::string err_msg;
+ if(matrix->upload_custom_emoji(filepath, key, mxc_url, err_msg) != PluginResult::OK) {
+ show_notification("QuickMedia", "Failed to upload custom emoji, error: " + err_msg, Urgency::CRITICAL);
+ return false;
+ }
+
+ return true;
+ });
+ return std::vector<Tab>{};
+ };
+
+ auto file_manager_body = create_body();
+ auto file_manager_page = std::make_unique<FileManagerPage>(program, FILE_MANAGER_MIME_TYPE_IMAGE, std::move(submit_handler));
+ file_manager_page->set_current_directory(get_home_dir().data);
+ BodyItems body_items;
+ file_manager_page->get_files_in_directory(body_items);
+ file_manager_body->set_items(std::move(body_items));
+
+ result_tabs.push_back(Tab{std::move(file_manager_body), std::move(file_manager_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ return PluginResult::OK;
+ } else if(args.url == "rename") {
+ result_tabs.push_back(Tab{create_body(false, true), std::make_unique<MatrixCustomEmojiRenameSelectPage>(program, matrix), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ return PluginResult::OK;
+ } else if(args.url == "delete") {
+ auto body = create_body(false, true);
+ BodyItems body_items;
+ for(auto &emoji : matrix->get_custom_emojis()) {
+ auto emoji_item = BodyItem::create(":" + emoji.first + ":");
+ emoji_item->url = emoji.first;
+ emoji_item->thumbnail_url = matrix->get_media_url(emoji.second.url);
+ emoji_item->thumbnail_size = emoji.second.size;
+ body_items.push_back(std::move(emoji_item));
+ }
+ body->set_items(std::move(body_items));
+
+ Body *body_p = body.get();
+ result_tabs.push_back(Tab{std::move(body), std::make_unique<MatrixCustomEmojiDeletePage>(program, matrix, body_p), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ return PluginResult::OK;
+ } else {
+ return PluginResult::ERR;
+ }
+ }
+
+ PluginResult MatrixCustomEmojiPage::lazy_fetch(BodyItems &result_items) {
+ auto add_emoji_item = BodyItem::create("Add emoji");
+ add_emoji_item->url = "add";
+ result_items.push_back(std::move(add_emoji_item));
+
+ auto rename_emoji_item = BodyItem::create("Rename emoji");
+ rename_emoji_item->url = "rename";
+ result_items.push_back(std::move(rename_emoji_item));
+
+ auto delete_emoji_item = BodyItem::create("Delete emoji");
+ delete_emoji_item->set_title_color(mgl::Color(255, 45, 47));
+ delete_emoji_item->url = "delete";
+ result_items.push_back(std::move(delete_emoji_item));
+
+ return PluginResult::OK;
+ }
+
+ bool MatrixCustomEmojiPage::is_ready() {
+ return matrix->is_initial_sync_finished();
+ }
+
+ PluginResult MatrixCustomEmojiRenameSelectPage::submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) {
+ result_tabs.push_back(Tab{create_body(), std::make_unique<MatrixCustomEmojiRenamePage>(program, matrix, args.url), create_search_bar("Enter a new name for the emoji...", SEARCH_DELAY_FILTER)});
+ return PluginResult::OK;
+ }
+
+ PluginResult MatrixCustomEmojiRenameSelectPage::lazy_fetch(BodyItems &result_items) {
+ for(auto &emoji : matrix->get_custom_emojis()) {
+ auto emoji_item = BodyItem::create(":" + emoji.first + ":");
+ emoji_item->url = emoji.first;
+ emoji_item->thumbnail_url = matrix->get_media_url(emoji.second.url);
+ emoji_item->thumbnail_size = emoji.second.size;
+ result_items.push_back(std::move(emoji_item));
+ }
return PluginResult::OK;
}
+ PluginResult MatrixCustomEmojiRenamePage::submit(const SubmitArgs &args, std::vector<Tab>&) {
+ if(matrix->rename_custom_emoji(emoji_key, args.title)) {
+ program->set_go_to_previous_page();
+ return PluginResult::OK;
+ } else {
+ show_notification("QuickMedia", "Failed to rename emoji " + emoji_key + " to " + args.title, Urgency::CRITICAL);
+ return PluginResult::OK;
+ }
+ }
+
+ PluginResult MatrixCustomEmojiDeletePage::submit(const SubmitArgs &args, std::vector<Tab>&) {
+ if(matrix->delete_custom_emoji(args.url)) {
+ body->erase_item([&args](std::shared_ptr<BodyItem> &item) {
+ return item->url == args.url;
+ });
+ return PluginResult::OK;
+ } else {
+ show_notification("QuickMedia", "Failed to delete emoji: " + args.url, Urgency::CRITICAL);
+ return PluginResult::OK;
+ }
+ }
+
MatrixChatPage::MatrixChatPage(Program *program, std::string room_id, MatrixRoomsPage *rooms_page, std::string jump_to_event_id) :
Page(program), room_id(std::move(room_id)), rooms_page(rooms_page), jump_to_event_id(std::move(jump_to_event_id))
{
@@ -1533,19 +1701,21 @@ namespace QuickMedia {
notification_thread.join();
}
+ std::lock_guard<std::recursive_mutex> lock(room_data_mutex);
delegate = nullptr;
sync_failed = false;
sync_fail_reason.clear();
- next_batch.clear();
+ set_next_batch("");
next_notifications_token.clear();
invites.clear();
filter_cached.reset();
my_events_transaction_ids.clear();
finished_fetching_notifications = false;
+ custom_emoji_by_key.clear();
}
- bool Matrix::is_initial_sync_finished() const {
- return !next_batch.empty();
+ bool Matrix::is_initial_sync_finished() {
+ return initial_sync_finished;
}
bool Matrix::did_initial_sync_fail(std::string &err_msg) {
@@ -1705,14 +1875,12 @@ namespace QuickMedia {
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, is_additional_messages_sync, initial_sync);
+ const rapidjson::Value &account_data_json = GetMember(root, "account_data");
+ parse_sync_account_data(account_data_json);
+
return PluginResult::OK;
}
@@ -1771,7 +1939,7 @@ namespace QuickMedia {
notification.room = room;
notification.event_id = std::move(event_id);
notification.sender_user_id.assign(sender_json.GetString(), sender_json.GetStringLength());
- notification.body = remove_reply_formatting(body_json.GetString());
+ notification.body = remove_reply_formatting(this, body_json.GetString());
notification.timestamp = timestamp;
notification.read = read_json.GetBool();
callback_func(notification);
@@ -1781,7 +1949,7 @@ namespace QuickMedia {
return PluginResult::OK;
}
- PluginResult Matrix::parse_sync_account_data(const rapidjson::Value &account_data_json, std::optional<std::set<std::string>> &dm_rooms) {
+ PluginResult Matrix::parse_sync_account_data(const rapidjson::Value &account_data_json) {
if(!account_data_json.IsObject())
return PluginResult::OK;
@@ -1789,36 +1957,74 @@ namespace QuickMedia {
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)
+ if(!type_json.IsString())
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;
+ if(strcmp(type_json.GetString(), "m.direct") == 0) {
+ for(auto const &it : content_json.GetObject()) {
+ if(!it.name.IsString())
+ continue;
- for(const rapidjson::Value &room_id_json : it.value.GetArray()) {
- if(!room_id_json.IsString())
+ if(!it.value.IsArray())
continue;
-
- dm_rooms_tmp.insert(std::string(room_id_json.GetString(), room_id_json.GetStringLength()));
+
+ for(const rapidjson::Value &room_id_json : it.value.GetArray()) {
+ if(!room_id_json.IsString())
+ continue;
+
+ RoomData *room = get_room_by_id(std::string(room_id_json.GetString(), room_id_json.GetStringLength()));
+ if(!room) {
+ fprintf(stderr, "Warning: got m.direct for room %s that we haven't created yet\n", room_id_json.GetString());
+ continue;
+ }
+
+ auto user = get_user_by_id(room, std::string(it.name.GetString(), it.name.GetStringLength()), nullptr, false);
+ if(!user) {
+ fprintf(stderr, "Warning: got m.direct for user %s that doesn't exist in the room %s yet\n", it.name.GetString(), room_id_json.GetString());
+ continue;
+ }
+
+ room->acquire_room_lock();
+ std::set<std::string> &room_tags = room->get_tags_thread_unsafe();
+ auto room_tag_it = room_tags.find("m.direct");
+ if(room_tag_it == room_tags.end()) {
+ room_tags.insert("m.direct");
+ ui_thread_tasks.push([this, room]{ delegate->room_add_tag(room, "m.direct"); });
+ }
+ room->release_room_lock();
+ }
}
- }
- }
+ } 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;
- if(has_direct_rooms)
- dm_rooms = std::move(dm_rooms_tmp);
+ 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);
+ }
+ }
+ }
return PluginResult::OK;
}
@@ -1986,6 +2192,20 @@ namespace QuickMedia {
item_timestamp = origin_server_ts.GetInt64();
}
+ const rapidjson::Value &is_direct_json = GetMember(content_json, "is_direct");
+ if(is_direct_json.IsBool() && is_direct_json.GetBool()) {
+ room_data->acquire_room_lock();
+ std::set<std::string> &room_tags = room_data->get_tags_thread_unsafe();
+
+ auto room_tag_it = room_tags.find("m.direct");
+ if(room_tag_it == room_tags.end()) {
+ room_tags.insert("m.direct");
+ ui_thread_tasks.push([this, room_data]{ delegate->room_add_tag(room_data, "m.direct"); });
+ }
+
+ room_data->release_room_lock();
+ }
+
parse_user_info(content_json, sender_json->GetString(), room_data, item_timestamp);
}
}
@@ -2003,7 +2223,7 @@ namespace QuickMedia {
return media_url.substr(start, end - start);
}
- static std::string get_thumbnail_url(const std::string &homeserver, const std::string &mxc_id) {
+ static std::string get_avatar_thumbnail_url(const std::string &homeserver, const std::string &mxc_id) {
if(mxc_id.empty())
return "";
@@ -2011,6 +2231,10 @@ namespace QuickMedia {
return homeserver + "/_matrix/media/r0/thumbnail/" + mxc_id + "?width=" + size + "&height=" + size + "&method=crop";
}
+ std::string Matrix::get_media_url(const std::string &mxc_id) {
+ return homeserver + "/_matrix/media/r0/download/" + thumbnail_url_extract_media_id(mxc_id);
+ }
+
std::shared_ptr<UserInfo> Matrix::parse_user_info(const rapidjson::Value &json, const std::string &user_id, RoomData *room_data, int64_t timestamp) {
assert(json.IsObject());
std::string avatar_url_str;
@@ -2023,7 +2247,7 @@ namespace QuickMedia {
std::string display_name = display_name_json.IsString() ? display_name_json.GetString() : user_id;
std::string avatar_url = thumbnail_url_extract_media_id(avatar_url_str);
if(!avatar_url.empty())
- avatar_url = get_thumbnail_url(homeserver, avatar_url); // TODO: Remove the constant strings around to reduce memory usage (6.3mb)
+ avatar_url = get_avatar_thumbnail_url(homeserver, avatar_url); // TODO: Remove the constant strings around to reduce memory usage (6.3mb)
//auto user_info = std::make_shared<UserInfo>(room_data, user_id, std::move(display_name), std::move(avatar_url));
// Overwrites user data
//room_data->add_user(user_info);
@@ -2195,8 +2419,8 @@ namespace QuickMedia {
return false;
}
- bool message_contains_user_mention(const Message *message, const std::string &username, const std::string &user_id) {
- const std::string formatted_text = message_to_qm_text(message, false);
+ bool message_contains_user_mention(Matrix *matrix, const Message *message, const std::string &username, const std::string &user_id) {
+ const std::string formatted_text = message_to_qm_text(matrix, message, false);
return message_contains_user_mention(formatted_text, username) || message_contains_user_mention(formatted_text, user_id);
}
@@ -2263,7 +2487,7 @@ namespace QuickMedia {
// TODO: Is @room ok? shouldn't we also check if the user has permission to do @room? (only when notifications are limited to @mentions)
// TODO: Is comparing against read marker timestamp ok enough?
if(me && message->timestamp > read_marker_message_timestamp) {
- std::string message_str = message_to_qm_text(message.get(), false);
+ std::string message_str = message_to_qm_text(this, message.get(), false);
message->notification_mentions_me = message_contains_user_mention(message_str, my_display_name) || message_contains_user_mention(message_str, me->user_id) || message_contains_user_mention(message_str, "@room");
}
}
@@ -2359,7 +2583,11 @@ namespace QuickMedia {
bool allow_formatted_text = false;
bool inside_source_highlight = false;
bool supports_syntax_highlight = false;
+ bool inside_img_tag = false;
+ std::string_view img_src;
+ mgl::vec2i img_size;
mgl::Color font_color = mgl::Color(255, 255, 255, 255);
+ Matrix *matrix = nullptr;
};
static int accumulate_string(char *data, int size, void *userdata) {
@@ -2384,6 +2612,10 @@ namespace QuickMedia {
else if(html_parser->tag_name.size == 4 && memcmp(html_parser->tag_name.data, "code", 4) == 0) {
parse_userdata.inside_code_tag = true;
parse_userdata.code_tag_language = std::string_view();
+ } 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_size = { 0, 0 };
}
break;
}
@@ -2397,6 +2629,17 @@ namespace QuickMedia {
parse_userdata.mx_reply_depth = std::max(0, parse_userdata.mx_reply_depth - 1);
} else if(html_parser->tag_name.size == 4 && memcmp(html_parser->tag_name.data, "code", 4) == 0) {
parse_userdata.inside_code_tag = false;
+ } else if(html_parser->tag_name.size == 3 && memcmp(html_parser->tag_name.data, "img", 3) == 0) {
+ if(parse_userdata.matrix && parse_userdata.inside_img_tag && parse_userdata.img_src.size() > 0) {
+ std::string image_url(parse_userdata.img_src);
+ html_unescape_sequences(image_url);
+ mgl::vec2i img_size = parse_userdata.img_size;
+ // 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);
+ }
+ parse_userdata.inside_img_tag = false;
}
break;
}
@@ -2407,6 +2650,20 @@ namespace QuickMedia {
} else if(parse_userdata.inside_code_tag && html_parser->attribute_key.size == 5 && memcmp(html_parser->attribute_key.data, "class", 5) == 0) {
if(html_parser->attribute_value.size > 9 && memcmp(html_parser->attribute_value.data, "language-", 9) == 0)
parse_userdata.code_tag_language = std::string_view(html_parser->attribute_value.data + 9, html_parser->attribute_value.size - 9);
+ } else if(parse_userdata.allow_formatted_text && parse_userdata.inside_img_tag) {
+ if(html_parser->attribute_key.size == 3 && memcmp(html_parser->attribute_key.data, "src", 3) == 0) {
+ parse_userdata.img_src = std::string_view(html_parser->attribute_value.data, html_parser->attribute_value.size);
+ } else if(html_parser->attribute_key.size == 5 && memcmp(html_parser->attribute_key.data, "width", 5) == 0) {
+ const std::string width(html_parser->attribute_value.data, html_parser->attribute_value.size);
+ parse_userdata.img_size.x = atoi(width.c_str());
+ } 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(!parse_userdata.allow_formatted_text && parse_userdata.inside_img_tag && 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);
+ html_unescape_sequences(text_to_add);
+ parse_userdata.result += std::move(text_to_add);
}
break;
}
@@ -2455,10 +2712,11 @@ namespace QuickMedia {
return 0;
}
- std::string formatted_text_to_qm_text(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) {
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;
html_parser_parse(str, size, formattext_text_parser_callback, &parse_userdata);
return std::move(parse_userdata.result);
}
@@ -2648,7 +2906,7 @@ namespace QuickMedia {
body = user_display_name + " changed his profile picture";
std::string new_avatar_url_str = thumbnail_url_extract_media_id(new_avatar_url_json.GetString());
if(!new_avatar_url_str.empty())
- new_avatar_url_str = get_thumbnail_url(homeserver, new_avatar_url_str); // TODO: Remove the constant strings around to reduce memory usage (6.3mb)
+ new_avatar_url_str = get_avatar_thumbnail_url(homeserver, new_avatar_url_str); // TODO: Remove the constant strings around to reduce memory usage (6.3mb)
new_avatar_url = new_avatar_url_str;
update_user_display_info = room_data->set_user_avatar_url(user, std::move(new_avatar_url_str), timestamp);
} else if((!new_avatar_url_json.IsString() || new_avatar_url_json.GetStringLength() == 0) && prev_avatar_url_json.IsString()) {
@@ -2989,7 +3247,7 @@ namespace QuickMedia {
if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0)
continue;
- update_room_avatar_url |= room_data->set_avatar_url(get_thumbnail_url(homeserver, thumbnail_url_extract_media_id(url_json.GetString())), item_timestamp);
+ update_room_avatar_url |= room_data->set_avatar_url(get_avatar_thumbnail_url(homeserver, thumbnail_url_extract_media_id(url_json.GetString())), item_timestamp);
room_data->avatar_is_fallback = false;
} else if(strcmp(type_json.GetString(), "m.room.topic") == 0) {
const rapidjson::Value &content_json = GetMember(event_item_json, "content");
@@ -3144,7 +3402,10 @@ namespace QuickMedia {
ui_thread_tasks.push([this, room_data]{ delegate->room_add_tag(room_data, OTHERS_ROOM_TAG); });
}
+ const bool contains_direct_messaging = room_tags.find("m.direct") != room_tags.end();
room_tags = std::move(new_tags);
+ if(contains_direct_messaging)
+ room_tags.insert("m.direct");
room_data->release_room_lock();
}
}
@@ -3362,31 +3623,6 @@ namespace QuickMedia {
return PluginResult::OK;
}
- static bool generate_random_characters(char *buffer, int buffer_size) {
- int fd = open("/dev/urandom", O_RDONLY);
- if(fd == -1) {
- perror("/dev/urandom");
- return false;
- }
-
- if(read(fd, buffer, buffer_size) < buffer_size) {
- fprintf(stderr, "Failed to read %d bytes from /dev/urandom\n", buffer_size);
- close(fd);
- return false;
- }
-
- close(fd);
- return true;
- }
-
- static std::string random_characters_to_readable_string(const char *buffer, int buffer_size) {
- std::ostringstream result;
- result << std::hex;
- for(int i = 0; i < buffer_size; ++i)
- result << (int)(unsigned char)buffer[i];
- return result.str();
- }
-
std::string create_transaction_id() {
char random_characters[18];
if(!generate_random_characters(random_characters, sizeof(random_characters)))
@@ -3505,11 +3741,29 @@ namespace QuickMedia {
}
}
+ static void replace_emoji_references_with_formatted_images(std::string &str, const std::unordered_map<std::string, CustomEmoji> &custom_emojis) {
+ for(const auto &it : custom_emojis) {
+ std::string keybind = ":" + it.first + ":";
+ std::string url = it.second.url;
+ html_escape_sequences(url);
+ std::string width = std::to_string(it.second.size.x);
+ std::string height = std::to_string(it.second.size.y);
+ std::string tag = "<img src=\"" + url + "\" alt=\"" + keybind + "\" width=\"" + width + "\" height=\"" + height + "\" vertical-align=\"middle\" />";
+ string_replace_all(str, keybind, tag);
+ }
+ }
+
std::string Matrix::body_to_formatted_body(RoomData *room, const std::string &body) {
+ std::unordered_map<std::string, CustomEmoji> custom_emojis_copy;
+ {
+ std::lock_guard<std::recursive_mutex> lock(room_data_mutex);
+ custom_emojis_copy = custom_emoji_by_key;
+ }
+
std::string formatted_body;
bool is_inside_code_block = false;
bool is_first_line = true;
- string_split(body, '\n', [this, room, &formatted_body, &is_inside_code_block, &is_first_line](const char *str, size_t size){
+ string_split(body, '\n', [this, room, &formatted_body, &is_inside_code_block, &is_first_line, &custom_emojis_copy](const char *str, size_t size){
if(!is_first_line) {
if(is_inside_code_block)
formatted_body += '\n';
@@ -3533,6 +3787,9 @@ namespace QuickMedia {
}
is_first_line = true;
} else {
+ if(!is_inside_code_block)
+ replace_emoji_references_with_formatted_images(line_str, custom_emojis_copy);
+
if(!is_inside_code_block && size > 0 && str[0] == '>') {
formatted_body += "<font color=\"#789922\">";
formatted_body_add_line(room, formatted_body, line_str);
@@ -3653,17 +3910,17 @@ namespace QuickMedia {
return result;
}
- static std::string get_reply_message(const Message *message, bool keep_formatted = false) {
+ static std::string get_reply_message(Matrix *matrix, const Message *message, bool keep_formatted = false) {
std::string related_to_body;
switch(message->type) {
case MessageType::TEXT: {
if(message->related_event_type != RelatedEventType::NONE) {
- related_to_body = remove_reply_formatting(message, keep_formatted);
+ related_to_body = remove_reply_formatting(matrix, message, keep_formatted);
} else {
if(keep_formatted && message->body_is_formatted)
related_to_body = message->body;
else
- related_to_body = message_to_qm_text(message, false);
+ related_to_body = message_to_qm_text(matrix, message, false);
}
break;
}
@@ -3683,15 +3940,15 @@ namespace QuickMedia {
if(keep_formatted && message->body_is_formatted)
related_to_body = message->body;
else
- related_to_body = message_to_qm_text(message, false);
+ related_to_body = message_to_qm_text(matrix, message, false);
break;
}
}
return related_to_body;
}
- static std::string create_body_for_message_reply(const Message *message, const std::string &body) {
- return "> <" + message->user->user_id + "> " + block_quote(get_reply_message(message)) + "\n\n" + body;
+ static std::string create_body_for_message_reply(Matrix *matrix, const Message *message, const std::string &body) {
+ return "> <" + message->user->user_id + "> " + block_quote(get_reply_message(matrix, message)) + "\n\n" + body;
}
static std::string extract_homeserver_from_room_id(const std::string &room_id) {
@@ -3703,7 +3960,7 @@ namespace QuickMedia {
std::string Matrix::create_formatted_body_for_message_reply(RoomData *room, const Message *message, const std::string &body) {
std::string formatted_body = body_to_formatted_body(room, body);
- std::string related_to_body = get_reply_message(message, true);
+ std::string related_to_body = get_reply_message(this, message, true);
if(!message->body_is_formatted)
html_escape_sequences(related_to_body);
// TODO: Add keybind to navigate to the reply message, which would also depend on this formatting.
@@ -3746,7 +4003,7 @@ namespace QuickMedia {
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());
- std::string message_reply_body = create_body_for_message_reply(related_to_text_message, body); // Yes, the reply is to the edited message but the event_id reference is to the original message...
+ std::string message_reply_body = create_body_for_message_reply(this, related_to_text_message, body); // Yes, the reply is to the edited message but the event_id reference is to the original message...
std::string formatted_message_reply_body = create_formatted_body_for_message_reply(room, related_to_text_message, body);
rapidjson::Document request_data(rapidjson::kObjectType);
@@ -4079,11 +4336,162 @@ namespace QuickMedia {
room->set_prev_batch("");
}
- static const char* file_get_filename(const std::string &filepath) {
- size_t index = filepath.rfind('/');
- if(index == std::string::npos)
- return filepath.c_str();
- return filepath.c_str() + index + 1;
+ PluginResult Matrix::upload_custom_emoji(const std::string &filepath, const std::string &key, std::string &mxc_url, std::string &err_msg) {
+ UploadInfo file_info;
+ UploadInfo thumbnail_info;
+ // TODO: Do not create and upload thumbnail
+ PluginResult upload_file_result = upload_file(filepath, "", file_info, thumbnail_info, err_msg);
+ if(upload_file_result != PluginResult::OK)
+ return upload_file_result;
+
+ mxc_url = std::move(file_info.content_uri);
+
+ rapidjson::Document request_data(rapidjson::kObjectType);
+ {
+ std::lock_guard<std::recursive_mutex> lock(room_data_mutex);
+ for(const auto &it : custom_emoji_by_key) {
+ rapidjson::Document emoji_obj(rapidjson::kObjectType);
+ emoji_obj.AddMember("url", rapidjson::Value(it.second.url.c_str(), request_data.GetAllocator()).Move(), request_data.GetAllocator());
+ emoji_obj.AddMember("width", it.second.size.x, request_data.GetAllocator());
+ emoji_obj.AddMember("height", it.second.size.y, request_data.GetAllocator());
+ request_data.AddMember(rapidjson::Value(it.first.c_str(), request_data.GetAllocator()).Move(), std::move(emoji_obj), request_data.GetAllocator());
+ }
+ }
+
+ CustomEmoji custom_emoji;
+ custom_emoji.url = mxc_url;
+ rapidjson::Document emoji_obj(rapidjson::kObjectType);
+ emoji_obj.AddMember("url", rapidjson::Value(mxc_url.c_str(), request_data.GetAllocator()).Move(), request_data.GetAllocator());
+ if(file_info.dimensions) {
+ custom_emoji.size = clamp_to_size(mgl::vec2i(file_info.dimensions->width, file_info.dimensions->height), custom_emoji_max_size);
+ emoji_obj.AddMember("width", custom_emoji.size.x, request_data.GetAllocator());
+ emoji_obj.AddMember("height", custom_emoji.size.y, request_data.GetAllocator());
+ }
+ request_data.AddMember(rapidjson::Value(key.c_str(), request_data.GetAllocator()).Move(), std::move(emoji_obj), request_data.GetAllocator());
+
+ rapidjson::StringBuffer buffer;
+ rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
+ request_data.Accept(writer);
+
+ std::vector<CommandArg> additional_args = {
+ { "-X", "PUT" },
+ { "-H", "content-type: application/json" },
+ { "-H", "Authorization: Bearer " + access_token },
+ { "--data-binary", buffer.GetString() }
+ };
+
+ std::string server_response;
+ DownloadResult download_result = download_to_string(homeserver + "/_matrix/client/r0/user/" + my_user_id + "/account_data/qm.emoji", server_response, std::move(additional_args), true);
+ if(download_result != DownloadResult::OK)
+ return download_result_to_plugin_result(download_result);
+
+ std::lock_guard<std::recursive_mutex> lock(room_data_mutex);
+ custom_emoji_by_key[key] = std::move(custom_emoji);
+ return PluginResult::OK;
+ }
+
+ bool Matrix::delete_custom_emoji(const std::string &key) {
+ rapidjson::Document request_data(rapidjson::kObjectType);
+ {
+ 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())
+ return false;
+
+ for(const auto &it : custom_emoji_by_key) {
+ if(it.first == key)
+ continue;
+
+ rapidjson::Document emoji_obj(rapidjson::kObjectType);
+ emoji_obj.AddMember("url", rapidjson::Value(it.second.url.c_str(), request_data.GetAllocator()).Move(), request_data.GetAllocator());
+ emoji_obj.AddMember("width", it.second.size.x, request_data.GetAllocator());
+ emoji_obj.AddMember("height", it.second.size.y, request_data.GetAllocator());
+ request_data.AddMember(rapidjson::Value(it.first.c_str(), request_data.GetAllocator()).Move(), std::move(emoji_obj), request_data.GetAllocator());
+ }
+ }
+
+ rapidjson::StringBuffer buffer;
+ rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
+ request_data.Accept(writer);
+
+ std::vector<CommandArg> additional_args = {
+ { "-X", "PUT" },
+ { "-H", "content-type: application/json" },
+ { "-H", "Authorization: Bearer " + access_token },
+ { "--data-binary", buffer.GetString() }
+ };
+
+ std::string server_response;
+ DownloadResult download_result = download_to_string(homeserver + "/_matrix/client/r0/user/" + my_user_id + "/account_data/qm.emoji", server_response, std::move(additional_args), true);
+ if(download_result != DownloadResult::OK)
+ return false;
+
+ 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())
+ custom_emoji_by_key.erase(it);
+
+ return true;
+ }
+
+ bool Matrix::rename_custom_emoji(const std::string &key, const std::string &new_key) {
+ rapidjson::Document request_data(rapidjson::kObjectType);
+ {
+ std::lock_guard<std::recursive_mutex> lock(room_data_mutex);
+ auto custom_emoji_list_copy = custom_emoji_by_key;
+ auto it = custom_emoji_list_copy.find(key);
+ if(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);
+ for(const auto &it : custom_emoji_list_copy) {
+ rapidjson::Document emoji_obj(rapidjson::kObjectType);
+ emoji_obj.AddMember("url", rapidjson::Value(it.second.url.c_str(), request_data.GetAllocator()).Move(), request_data.GetAllocator());
+ emoji_obj.AddMember("width", it.second.size.x, request_data.GetAllocator());
+ emoji_obj.AddMember("height", it.second.size.y, request_data.GetAllocator());
+ request_data.AddMember(rapidjson::Value(it.first.c_str(), request_data.GetAllocator()).Move(), std::move(emoji_obj), request_data.GetAllocator());
+ }
+ }
+
+ rapidjson::StringBuffer buffer;
+ rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
+ request_data.Accept(writer);
+
+ std::vector<CommandArg> additional_args = {
+ { "-X", "PUT" },
+ { "-H", "content-type: application/json" },
+ { "-H", "Authorization: Bearer " + access_token },
+ { "--data-binary", buffer.GetString() }
+ };
+
+ std::string server_response;
+ DownloadResult download_result = download_to_string(homeserver + "/_matrix/client/r0/user/" + my_user_id + "/account_data/qm.emoji", server_response, std::move(additional_args), true);
+ if(download_result != DownloadResult::OK)
+ return false;
+
+ 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()) {
+ auto custom_emoji_copy = it->second;
+ custom_emoji_by_key.erase(it);
+ custom_emoji_by_key[new_key] = std::move(custom_emoji_copy);
+ }
+
+ return true;
+ }
+
+ bool Matrix::does_custom_emoji_with_name_exist(const std::string &name) {
+ assert(is_initial_sync_finished());
+ std::lock_guard<std::recursive_mutex> lock(room_data_mutex);
+ return custom_emoji_by_key.find(name) != custom_emoji_by_key.end();
+ }
+
+ std::unordered_map<std::string, CustomEmoji> Matrix::get_custom_emojis() {
+ assert(is_initial_sync_finished());
+ std::lock_guard<std::recursive_mutex> lock(room_data_mutex);
+ return custom_emoji_by_key;
}
PluginResult Matrix::post_file(RoomData *room, const std::string &filepath, std::string filename, std::string &event_id_response, std::string &err_msg, void *relates_to) {
@@ -4092,7 +4500,7 @@ namespace QuickMedia {
UploadInfo file_info;
UploadInfo thumbnail_info;
- PluginResult upload_file_result = upload_file(room, filepath, filename, file_info, thumbnail_info, err_msg);
+ PluginResult upload_file_result = upload_file(filepath, filename, file_info, thumbnail_info, err_msg);
if(upload_file_result != PluginResult::OK)
return upload_file_result;
@@ -4107,7 +4515,7 @@ namespace QuickMedia {
return post_message(room, filename, event_id_response, file_info_opt, thumbnail_info_opt);
}
- PluginResult Matrix::upload_file(RoomData *room, const std::string &filepath, std::string filename, UploadInfo &file_info, UploadInfo &thumbnail_info, std::string &err_msg, bool upload_thumbnail) {
+ PluginResult Matrix::upload_file(const std::string &filepath, std::string filename, UploadInfo &file_info, UploadInfo &thumbnail_info, std::string &err_msg, bool upload_thumbnail) {
FileAnalyzer file_analyzer;
if(!file_analyzer.load_file(filepath.c_str(), true)) {
err_msg = "Failed to load " + filepath;
@@ -4149,7 +4557,7 @@ namespace QuickMedia {
if(video_get_middle_frame(file_analyzer, tmp_filename, thumbnail_max_size.x, thumbnail_max_size.y)) {
UploadInfo upload_info_ignored; // Ignore because it wont be set anyways. Thumbnails dont have thumbnails.
- PluginResult upload_thumbnail_result = upload_file(room, tmp_filename, thumbnail_filename.data, thumbnail_info, upload_info_ignored, err_msg, false);
+ PluginResult upload_thumbnail_result = upload_file(tmp_filename, thumbnail_filename.data, thumbnail_info, upload_info_ignored, err_msg, false);
if(upload_thumbnail_result != PluginResult::OK) {
close(tmp_file);
remove(tmp_filename);
@@ -4177,7 +4585,7 @@ namespace QuickMedia {
thumbnail_filename = thumbnail_filename.filename_no_ext() + ".thumb" + thumbnail_filename.ext();
UploadInfo upload_info_ignored; // Ignore because it wont be set anyways. Thumbnails dont have thumbnails.
- PluginResult upload_thumbnail_result = upload_file(room, thumbnail_path, thumbnail_filename.data, thumbnail_info, upload_info_ignored, err_msg, false);
+ PluginResult upload_thumbnail_result = upload_file(thumbnail_path, thumbnail_filename.data, thumbnail_info, upload_info_ignored, err_msg, false);
if(upload_thumbnail_result != PluginResult::OK) {
close(tmp_file);
remove(tmp_filename);
@@ -4811,7 +5219,7 @@ namespace QuickMedia {
if(avatar_url_json.IsString()) {
std::string avatar_url = thumbnail_url_extract_media_id(avatar_url_json.GetString());
if(!avatar_url.empty())
- avatar_url = get_thumbnail_url(homeserver, avatar_url);
+ avatar_url = get_avatar_thumbnail_url(homeserver, avatar_url);
if(!avatar_url.empty())
room_body_item->thumbnail_url = std::move(avatar_url);
@@ -4883,7 +5291,7 @@ namespace QuickMedia {
if(avatar_url_json.IsString()) {
std::string avatar_url = thumbnail_url_extract_media_id(std::string(avatar_url_json.GetString(), avatar_url_json.GetStringLength()));
if(!avatar_url.empty())
- avatar_url = get_thumbnail_url(homeserver, avatar_url);
+ avatar_url = get_avatar_thumbnail_url(homeserver, avatar_url);
body_item->thumbnail_url = std::move(avatar_url);
}
body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE;
@@ -5037,6 +5445,7 @@ namespace QuickMedia {
void Matrix::set_next_batch(std::string new_next_batch) {
std::lock_guard<std::mutex> lock(next_batch_mutex);
next_batch = std::move(new_next_batch);
+ initial_sync_finished = !next_batch.empty();
}
std::string Matrix::get_next_batch() {
@@ -5129,7 +5538,7 @@ namespace QuickMedia {
if(avatar_url_json.IsString())
avatar_url = std::string(avatar_url_json.GetString(), avatar_url_json.GetStringLength());
if(!avatar_url.empty())
- avatar_url = get_thumbnail_url(homeserver, thumbnail_url_extract_media_id(avatar_url)); // TODO: Remove the constant strings around to reduce memory usage (6.3mb)
+ avatar_url = get_avatar_thumbnail_url(homeserver, thumbnail_url_extract_media_id(avatar_url)); // TODO: Remove the constant strings around to reduce memory usage (6.3mb)
room->set_user_avatar_url(user, avatar_url, 0);
room->set_user_display_name(user, display_name, 0);