aboutsummaryrefslogtreecommitdiff
path: root/plugins/Matrix.hpp
blob: 23b0a8acafdcb93b890bc9b068f4af4e6a29fba7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
#pragma once

#include "../include/FileAnalyzer.hpp"
#include "../include/MessageQueue.hpp"
#include "Plugin.hpp"
#include "Page.hpp"
#include <mglpp/graphics/Color.hpp>
#include <unordered_map>
#include <unordered_set>
#include <map>
#include <set>
#include <mutex>
#include <atomic>
#include <thread>
#include <rapidjson/fwd.h>

namespace QuickMedia {
    struct RoomData;
    struct Message;

    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);
    // |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));
    // |image_max_size| 0, 0 means no max size
    std::string message_to_qm_text(Matrix *matrix, const Message *message, bool allow_formatted_text = true, mgl::vec2i image_max_size = mgl::vec2i(0, 0));
    std::string pantalaimon_url_to_homeserver_url(Matrix *matrix, const std::string &url);
    Message* get_latest_message_in_edit_chain(Message *message);
    bool matrix_gpg_encrypt_for_each_user_in_room(Matrix *matrix, RoomData *room, const std::string &my_gpg_user_id, const std::string &str, std::string &encrypted_str);
    // Returns empty string on error
    std::string extract_user_name_from_user_id(const std::string &user_id);
    // Returns empty string on error
    std::string extract_user_name_from_email(const std::string &email);
    std::vector<std::string> matrix_extract_room_ids(const std::string &str);
    std::string remove_reply_formatting(Matrix *matrix, const Message *message, bool keep_formatted = false);

    struct MatrixChatBodyDecryptJob {
        enum class DecryptState {
            NOT_DECRYPTED,
            DECRYPTING,
            DECRYPTED,
            FAILED_TO_DECRYPT
        };

        std::string text;
        DecryptState decrypt_state = DecryptState::NOT_DECRYPTED;
        bool cancel = false;
    };

    class MatrixChatBodyItemData : public BodyItemExtra {
    public:
        enum class DecryptState {
            NOT_DECRYPTED,
            DECRYPTING,
            DECRYPTED
        };

        MatrixChatBodyItemData(Matrix *matrix, std::string text_to_decrypt, RoomData *room = nullptr, uint64_t gpg_decrypt_message_id = 0) : matrix(matrix), text_to_decrypt(std::move(text_to_decrypt)), room(room), gpg_decrypt_message_id(gpg_decrypt_message_id) {}
        ~MatrixChatBodyItemData();
        void draw_overlay(mgl::Window&, const Widgets &widgets) override;

        DecryptState decrypt_state = DecryptState::NOT_DECRYPTED;
        std::shared_ptr<MatrixChatBodyDecryptJob> decrypt_job;
        Matrix *matrix = nullptr;
        std::string text_to_decrypt;
        RoomData *room = nullptr;
        uint64_t gpg_decrypt_message_id = 0;
    };

    // TODO: Remove. Not needed anymore.
    struct TimestampedDisplayData {
        std::string data;
        time_t timestamp = 0; // In milliseconds

        // Force update by settings |new_timestamp| to 0
        bool set_data_if_newer(std::string new_data, time_t new_timestamp);
    };

    struct UserInfo {
        friend struct RoomData;
        UserInfo(RoomData *room, std::string user_id);
        UserInfo(RoomData *room, std::string user_id, std::string display_name, std::string avatar_url, time_t update_timestamp_ms);

        RoomData *room;
        const mgl::Color display_name_color;
        const std::string user_id;
        int power_level = 0;
    private:
        TimestampedDisplayData display_name;
        TimestampedDisplayData avatar_url;
        std::string read_marker_event_id;
    };

    enum class MessageType {
        TEXT,
        IMAGE,
        VIDEO,
        AUDIO,
        FILE,
        REDACTION,
        REACTION,
        MEMBERSHIP,
        SYSTEM,
        UNIMPLEMENTED
    };

    bool is_visual_media_message_type(MessageType message_type);
    bool is_system_message_type(MessageType message_type);

    enum class RelatedEventType {
        NONE,
        REPLY,
        EDIT,
        REDACTION,
        REACTION
    };

    struct MatrixEventUserInfo {
        RoomData *room;
        std::string user_id;
        std::optional<std::string> display_name;
        std::optional<std::string> avatar_url;
    };

    struct MatrixEventRoomInfo {
        RoomData *room;
        std::optional<std::string> name;
        std::optional<std::string> topic;
        std::optional<std::string> avatar_url;
    };

    enum class MatrixEventType {
        ADD_USER,
        REMOVE_USER,
        USER_INFO,
        ROOM_INFO
    };

    struct Message {
        std::shared_ptr<UserInfo> user;
        std::string event_id;
        std::string body;
        std::string url;
        std::string thumbnail_url;
        std::string related_event_id;
        mgl::vec2i thumbnail_size; // Set to {0, 0} if not specified
        RelatedEventType related_event_type = RelatedEventType::NONE;
        bool notification_mentions_me = false;
        bool body_is_formatted = false;
        std::string transaction_id;
        time_t timestamp = 0; // In milliseconds
        MessageType type;
        Message *replaces = nullptr;
        std::shared_ptr<Message> replaced_by = nullptr;
        // TODO: Store body item ref here
    };

    using Messages = std::vector<std::shared_ptr<Message>>;

    struct RoomData {
        std::shared_ptr<UserInfo> get_user_by_id(const std::string &user_id);
        void add_user(std::shared_ptr<UserInfo> user);

        void set_user_read_marker(std::shared_ptr<UserInfo> &user, const std::string &event_id);
        std::string get_user_read_marker(const std::shared_ptr<UserInfo> &user);

        std::string get_user_display_name(const std::shared_ptr<UserInfo> &user);
        std::string get_user_avatar_url(const std::shared_ptr<UserInfo> &user);

        // Set to empty to remove (in which case the display name will be the user id)
        bool set_user_display_name(std::shared_ptr<UserInfo> &user, std::string display_name, time_t update_timestamp_ms);
        bool set_user_avatar_url(std::shared_ptr<UserInfo> &user, std::string avatar_url, time_t update_timestamp_ms);

        // Ignores duplicates, returns the number of added messages
        size_t prepend_messages_reverse(const Messages &new_messages);
        // Ignores duplicates, returns the number of added messages
        size_t append_messages(const Messages &new_messages);

        std::shared_ptr<Message> get_message_by_id(const std::string &id);

        std::vector<std::shared_ptr<UserInfo>> get_users();
        std::vector<std::shared_ptr<UserInfo>> get_users_excluding_me(const std::string &my_user_id);

        void acquire_room_lock();
        void release_room_lock();

        const Messages& get_messages_thread_unsafe() const;
        const std::vector<std::string>& get_pinned_events_thread_unsafe() const;
        void clear_messages();

        bool has_prev_batch();
        void set_prev_batch(const std::string &new_prev_batch);
        std::string get_prev_batch();

        bool has_name();
        bool set_name(const std::string &new_name, time_t update_timestamp_ms);
        // TODO: Remove this
        std::string get_name();
        bool set_topic(const std::string &new_topic, time_t update_timestamp_ms);
        std::string get_topic();

        bool has_avatar_url();
        bool set_avatar_url(const std::string &new_avatar_url, time_t update_timestamp_ms);
        std::string get_avatar_url();

        void set_pinned_events(std::vector<std::string> new_pinned_events);
        std::set<std::string>& get_tags_thread_unsafe();

        void clear_data();

        std::string id;

        // These 5 variables are set by QuickMedia, not the matrix plugin
        bool initial_prev_messages_fetch = true;
        bool last_message_read = true;
        bool users_fetched = false;
        time_t last_read_message_timestamp = 0;
        std::shared_ptr<BodyItem> body_item;
        int offset_to_latest_message_text = 0;

        // These are messages fetched with |Matrix::get_message_by_id|. Needed to show replies, when replying to old message not part of /sync.
        // The value is nullptr if the message is fetched and cached but the event if referenced an invalid message.
        // TODO: Verify if replied to messages are also part of /sync; then this is not needed.
        std::unordered_map<std::string, std::shared_ptr<Message>> fetched_messages_by_event_id;

        size_t messages_read_index = 0;
        bool pinned_events_updated = false;
        bool name_is_fallback = false;
        bool avatar_is_fallback = false;
        std::atomic_int64_t last_message_timestamp = 0;

        std::atomic_int64_t read_marker_event_timestamp = 0;

        int notification_power_level = 50;
        size_t index = 0;
        std::atomic<uint64_t> gpg_decrypt_message_id = 0;
    private:
        std::mutex user_mutex;
        std::recursive_mutex room_mutex;

        TimestampedDisplayData name;
        TimestampedDisplayData topic;
        TimestampedDisplayData avatar_url;
        std::string prev_batch;

        // Each room has its own list of user data, even if multiple rooms has the same user
        // because users can have different display names and avatars in different rooms.
        std::unordered_map<std::string, std::shared_ptr<UserInfo>> user_info_by_user_id;
        Messages messages;
        std::unordered_map<std::string, std::shared_ptr<Message>> message_by_event_id;
        std::vector<std::string> pinned_events;
        std::set<std::string> tags;
    };

    struct RoomExtraData {
        std::string chat_message;
        std::string editing_message_id;
    };

    struct Invite {
        std::string room_name;
        std::string room_avatar_url;
        std::shared_ptr<UserInfo> invited_by;
        time_t timestamp = 0; // In milliseconds
    };

    enum class MessageDirection {
        BEFORE,
        AFTER
    };

    struct UploadInfo {
        ContentType content_type;
        int64_t file_size = 0;
        std::optional<Dimensions> dimensions;
        std::optional<double> duration_seconds;
        std::string content_uri;
    };

    struct SyncData {
        Messages messages;
        std::optional<std::vector<std::string>> pinned_events;
    };

    using Rooms = std::vector<RoomData*>;

    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);
    std::string create_transaction_id();

    enum class MatrixPageType {
        ROOM_LIST,
        CHAT
    };

    enum class LeaveType {
        LEAVE,
        KICKED,
        BANNED
    };

    struct MatrixNotification {
        RoomData *room;
        std::string event_id;
        std::string sender_user_id;
        std::string body; // Without reply formatting
        time_t timestamp; // The timestamp in milliseconds or 0
        bool read;
    };

    // All of methods in this class are called in the main (ui) thread
    class MatrixDelegate {
    public:
        virtual ~MatrixDelegate() = default;

        virtual void join_room(RoomData *room) = 0;
        virtual void leave_room(RoomData *room, const std::string &event_id, LeaveType leave_type, const std::string &reason) = 0;

        // Note: calling |room| methods inside this function is not allowed
        virtual void room_add_tag(RoomData *room, const std::string &tag) = 0;
        // Note: calling |room| methods inside this function is not allowed
        virtual void room_remove_tag(RoomData *room, const std::string &tag) = 0;
        virtual void room_add_new_messages(RoomData *room, const Messages &messages, bool is_initial_sync, MessageDirection message_dir) = 0;
        virtual void room_clear_messages(RoomData *room) = 0;

        virtual void add_invite(const std::string &room_id, const Invite &invite) = 0;
        virtual void remove_invite(const std::string &room_id) = 0;

        virtual void add_unread_notification(MatrixNotification notification) = 0;

        virtual void add_user(MatrixEventUserInfo user_info) = 0;
        virtual void remove_user(MatrixEventUserInfo user_info) = 0;
        virtual void set_user_info(MatrixEventUserInfo user_info) = 0;
        virtual void set_room_info(MatrixEventRoomInfo room_info) = 0;

        virtual void set_room_as_read(RoomData *room) = 0;
    };

    class Matrix;
    class MatrixRoomsPage;
    class MatrixRoomTagsPage;
    class MatrixInvitesPage;
    class MatrixChatPage;
    class MatrixNotificationsPage;

    using UsersByRoom = std::unordered_map<RoomData*, std::unordered_map<std::string, MatrixEventUserInfo>>;

    class MatrixQuickMedia : public MatrixDelegate {
    public:
        MatrixQuickMedia(Program *program, Matrix *matrix, MatrixRoomsPage *rooms_page, MatrixRoomTagsPage *room_tags_page, MatrixInvitesPage *invites_page, MatrixNotificationsPage *notifications_page);

        void join_room(RoomData *room) override;
        void leave_room(RoomData *room, const std::string &event_id, LeaveType leave_type, const std::string &reason) override;
        void room_add_tag(RoomData *room, const std::string &tag) override;
        void room_remove_tag(RoomData *room, const std::string &tag) override;
        void room_add_new_messages(RoomData *room, const Messages &messages, bool is_initial_sync, MessageDirection message_dir) override;
        void room_clear_messages(RoomData *room) override;

        void add_invite(const std::string &room_id, const Invite &invite) override;
        void remove_invite(const std::string &room_id) override;

        void add_unread_notification(MatrixNotification notification) override;

        void add_user(MatrixEventUserInfo user_info) override;
        void remove_user(MatrixEventUserInfo user_info) override;
        void set_user_info(MatrixEventUserInfo user_info) override;
        void set_room_info(MatrixEventRoomInfo room_info) override;
        void for_each_user_in_room(RoomData *room, std::function<void(const MatrixEventUserInfo&)> callback);

        void set_room_as_read(RoomData *room) override;

        Program *program;
        Matrix *matrix;
        MatrixChatPage *chat_page;
        MatrixRoomsPage *rooms_page;
        MatrixRoomTagsPage *room_tags_page;
        MatrixInvitesPage *invites_page;
        MatrixNotificationsPage *notifications_page;
    private:
        void update_room_description(RoomData *room, const Messages &new_messages, bool is_initial_sync);
    private:
        std::map<RoomData*, std::shared_ptr<BodyItem>> room_body_item_by_room;
        std::map<RoomData*, std::shared_ptr<Message>> last_message_by_room;
        std::map<RoomData*, int> unread_mention_count_by_room;
        std::unordered_set<std::string> notifications_shown;
        UsersByRoom users_by_room;
    };

    class MatrixRoomsPage : public Page {
    public:
        MatrixRoomsPage(Program *program, Body *body, std::string title, MatrixRoomTagsPage *room_tags_page, SearchBar *search_bar);
        ~MatrixRoomsPage() override;

        const char* get_title() const override { return title.c_str(); }
        PluginResult submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) override;
        bool submit_is_async() const override { return false; }
        bool clear_search_after_submit() override { return true; }

        void add_body_item(std::shared_ptr<BodyItem> body_item);

        void move_room_to_top(RoomData *room);
        void remove_body_item_by_room_id(const std::string &room_id);

        void set_current_chat_page(MatrixChatPage *chat_page);

        void set_room_as_read(RoomData *room);

        void clear_search();

        MatrixQuickMedia *matrix_delegate = nullptr;
        Body *body = nullptr;
    private:
        std::string title;
        MatrixRoomTagsPage *room_tags_page = nullptr;
        MatrixChatPage *current_chat_page = nullptr;
        SearchBar *search_bar;
    };

    class MatrixRoomTagsPage : public Page {
    public:
        MatrixRoomTagsPage(Program *program, Body *body) : Page(program), body(body) {}
        const char* get_title() const override { return "Tags"; }
        PluginResult submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) override;
        bool submit_is_async() const override { return false; }
        bool clear_search_after_submit() override { return true; }

        void add_room_body_item_to_tag(std::shared_ptr<BodyItem> body_item, const std::string &tag);
        void remove_room_body_item_from_tag(std::shared_ptr<BodyItem> body_item, const std::string &tag);

        void move_room_to_top(RoomData *room);
        void remove_body_item_by_room_id(const std::string &room_id);

        void set_current_rooms_page(MatrixRoomsPage *rooms_page);

        MatrixQuickMedia *matrix_delegate = nullptr;
    private:
        struct TagData {
            std::shared_ptr<BodyItem> tag_item;
            std::vector<std::shared_ptr<BodyItem>> room_body_items;
        };

        Body *body;
        std::map<std::string, TagData> tag_body_items_by_name;
        MatrixRoomsPage *current_rooms_page = nullptr;
    };

    class MatrixInvitesPage : public Page {
    public:
        MatrixInvitesPage(Program *program, Matrix *matrix, Body *body);

        const char* get_title() const override { return title.c_str(); }
        PluginResult submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) override;
        bool submit_is_async() const override { return false; }
        bool clear_search_after_submit() override { return true; }

        void add_body_item(std::shared_ptr<BodyItem> body_item);
        void remove_body_item_by_room_id(const std::string &room_id);
    private:
        Matrix *matrix;
        Body *body;
        std::string title = "Invites (0)";
        size_t prev_invite_count = 0;
    };

    class MatrixInviteDetailsPage : public Page {
    public:
        MatrixInviteDetailsPage(Program *program, Matrix *matrix, MatrixInvitesPage *invites_page, std::string room_id, std::string title) :
            Page(program), matrix(matrix), invites_page(invites_page), room_id(std::move(room_id)), title(std::move(title)) {}
        const char* get_title() const override { return title.c_str(); }
        PluginResult submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) override;

        Matrix *matrix;
        MatrixInvitesPage *invites_page;
        const std::string room_id;
        const std::string title;
    };

    class MatrixSettingsPage : public Page {
    public:
        MatrixSettingsPage(Program *program, Matrix *matrix) : Page(program), matrix(matrix) {}
        const char* get_title() const override { return "Settings"; }
        PluginResult submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) override;
    private:
        Matrix *matrix;
    };

    class MatrixRoomInputPage : public Page {
    public:
        MatrixRoomInputPage(Program *program, Matrix *matrix) : Page(program), matrix(matrix) {}
        const char* get_title() const override { return "Enter the id of a room to join"; }
        PluginResult submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) override;
        bool allow_submit_no_selection() const override { return true; }
    private:
        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:
        MatrixVideoPage(Program *program, std::string filename) : VideoPage(program, ""), filename(std::move(filename)) {}
        const char* get_title() const override { return ""; }
        std::string get_filename() override { return filename; }
    private:
        std::string filename;
    };

    using MatrixRoomInfoUpdateCallback = std::function<void(const MatrixEventRoomInfo &room_info)>;

    class MatrixChatPage : public Page {
    public:
        MatrixChatPage(Program *program, std::string room_id, MatrixRoomsPage *rooms_page, std::string jump_to_event_id = "");
        ~MatrixChatPage();

        const char* get_title() const override { return ""; }
        PageTypez get_type() const override { return PageTypez::CHAT; }

        void add_user(MatrixEventUserInfo user_info);
        void remove_user(MatrixEventUserInfo user_info);
        void set_user_info(MatrixEventUserInfo user_info);
        void set_room_info(MatrixEventRoomInfo room_info);

        void set_current_room(RoomData *room, Body *users_body, MatrixRoomInfoUpdateCallback room_info_update_callback);
        size_t get_num_users_in_current_room() const;

        void set_room_as_read(RoomData *room);

        const std::string room_id;
        MatrixRoomsPage *rooms_page = nullptr;

        Body *chat_body = nullptr;
        bool messages_tab_visible = false;
        bool is_regular_navigation = true;

        const std::string jump_to_event_id;
    private:
        void add_user_to_body_by_user_info(const MatrixEventUserInfo &user_info);
    private:
        RoomData *current_room = nullptr;
        Body *users_body = nullptr;
        MatrixRoomInfoUpdateCallback room_info_update_callback;
        std::unordered_map<std::string, BodyItem*> user_body_item_by_user_id;
    };

    class MatrixRoomDirectoryPage : public Page {
    public:
        MatrixRoomDirectoryPage(Program *program, Matrix *matrix) : Page(program), matrix(matrix) {}
        const char* get_title() const override { return "Room directory"; }
        bool allow_submit_no_selection() const override { return true; }
        bool clear_search_after_submit() override { return true; }
        PluginResult submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) override;
    private:
        Matrix *matrix;
    };

    class MatrixServerRoomListPage : public LazyFetchPage {
    public:
        MatrixServerRoomListPage(Program *program, Matrix *matrix, const std::string &server_name) : LazyFetchPage(program), matrix(matrix), server_name(server_name), current_page(0) {}
        const char* get_title() const override { return "Select a room to join"; }
        bool search_is_filter() override { return false; }
        PluginResult lazy_fetch(BodyItems &result_items) override;
        PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override;
        SearchResult search(const std::string &str, BodyItems &result_items) override;
        PluginResult submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) override;
    private:
        Matrix *matrix;
        const std::string server_name;
        std::string next_batch;
        std::string search_term;
        int current_page;
    };

    class MatrixNotificationsPage : public LazyFetchPage {
    public:
        MatrixNotificationsPage(Program *program, Matrix *matrix, Body *notifications_body, MatrixRoomsPage *all_rooms_page);
        const char* get_title() const override { return "Notifications"; }
        PluginResult submit(const SubmitArgs &args, std::vector<Tab>&) override;
        PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override;
        PluginResult lazy_fetch(BodyItems &result_items) override;
        bool is_ready() override;
        bool submit_is_async() const override { return false; }
        bool clear_search_after_submit() override { return true; }

        void add_notification(MatrixNotification notification);
        void set_room_as_read(RoomData *room);
    private:
        bool has_fetched = false;
        Matrix *matrix;
        Body *notifications_body;
        MatrixRoomsPage *all_rooms_page;
        // room id[event_id[]]
        std::unordered_map<std::string, std::unordered_map<std::string, std::shared_ptr<BodyItem>>> room_notifications;
        // Notifications are here until the notifications has been fetched, so that page handler doesn't the notifications
        std::unordered_map<std::string, std::unordered_map<std::string, MatrixNotification>> pending_room_notifications;
    };

    class MatrixInviteUserPage : public Page {
    public:
        MatrixInviteUserPage(Program *program, Matrix *matrix, std::string room_id) : Page(program), matrix(matrix), room_id(std::move(room_id)) {}
        const char* get_title() const override { return "Invite user"; }
        bool search_is_filter() override { return false; }
        SearchResult search(const std::string &str, BodyItems &result_items) override;
        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 room_id;
    };

    struct CustomEmoji {
        std::string url;
        mgl::vec2i size;
    };

    class Matrix {
    public:
        Matrix(bool matrix_instance_already_running);
        // 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();
        // 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;
        void get_room_sync_data(RoomData *room, SyncData &sync_data);

        void get_all_synced_room_messages(RoomData *room, Messages &messages);
        void get_all_pinned_events(RoomData *room, std::vector<std::string> &events);
        PluginResult get_messages_in_direction(RoomData *room, const std::string &token, MessageDirection message_dir, Messages &messages, std::string &new_token);
        PluginResult get_previous_room_messages(RoomData *room, Messages &messages, bool latest_messages = false, bool *reached_end = nullptr);
        PluginResult get_previous_notifications(std::function<void(const MatrixNotification&)> callback_func);
        void get_cached_notifications(std::function<void(const MatrixNotification&)> callback_func);

        // |url| should only be set when uploading media.
        // TODO: Make api better.
        PluginResult post_message(RoomData *room, const std::string &body, std::string &event_id_response, const std::optional<UploadInfo> &file_info, const std::optional<UploadInfo> &thumbnail_info, const std::string &msgtype = "", const std::string &custom_transaction_id = "");
        // |relates_to| is from |BodyItem.userdata| and is of type |Message*|
        // If |custom_transaction_id| is empty, then a new transaction id is generated
        PluginResult post_reply(RoomData *room, const std::string &body, void *relates_to, std::string &event_id_response, const std::string &custom_transaction_id = "", const std::optional<UploadInfo> &file_info = std::nullopt, const std::optional<UploadInfo> &thumbnail_info = std::nullopt);
        // |relates_to| is from |BodyItem.userdata| and is of type |Message*|
        PluginResult post_edit(RoomData *room, const std::string &body, void *relates_to, std::string &event_id_response, const std::string &custom_transaction_id = "");
        // |relates_to| is from |BodyItem.userdata| and is of type |Message*|
        PluginResult post_reaction(RoomData *room, const std::string &body, void *relates_to, std::string &event_id_response, const std::string &custom_transaction_id = "");
        
        // 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();

        // |message| is from |BodyItem.userdata| and is of type |Message*|
        PluginResult delete_message(RoomData *room, void *message, std::string &err_msg);

        PluginResult pin_message(RoomData *room, const std::string &event_id);
        PluginResult unpin_message(RoomData *room, const std::string &event_id);

        PluginResult load_cached_session();

        PluginResult on_start_typing(RoomData *room);
        PluginResult on_stop_typing(RoomData *room);

        PluginResult set_read_marker(RoomData *room, const std::string &event_id, int64_t event_timestamp);

        PluginResult join_room(const std::string &room_id_or_name);
        PluginResult leave_room(const std::string &room_id);

        bool is_invite_silenced(const std::string &room_id, int64_t timestamp);
        void silence_invite(const std::string &room_id, int64_t timestamp);

        // If |since| is empty, then the first page is fetched
        PluginResult get_public_rooms(const std::string &server, const std::string &search_term, const std::string &since, BodyItems &rooms, std::string &next_batch);

        PluginResult search_user(const std::string &search_term, unsigned int limit, BodyItems &result_items);
        PluginResult invite_user(const std::string &room_id, const std::string &user_id);

        // |message| is from |BodyItem.userdata| and is of type |Message*|
        bool was_message_posted_by_me(void *message);

        std::string message_get_author_displayname(Message *message) const;

        // Cached
        PluginResult get_config(int64_t *upload_size);

        std::shared_ptr<UserInfo> get_me(RoomData *room);

        const std::string& get_homeserver_domain() const;
        std::string get_remote_homeserver_url() const;

        // Returns nullptr if message cant be found. Note: cached
        std::shared_ptr<Message> get_message_by_id(RoomData *room, const std::string &event_id);
        PluginResult get_message_context(RoomData *room, const std::string &event_id, std::shared_ptr<Message> &message, Messages &before_messages, Messages &after_messages, std::string &before_token, std::string &after_token);
        void clear_previous_messages_token(RoomData *room);

        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);

        RoomExtraData& get_room_extra_data(RoomData *room);

        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);

        void async_decrypt_message(std::shared_ptr<MatrixChatBodyDecryptJob> decrypt_job);

        MatrixDelegate* get_delegate();

        bool is_another_instance_running() const { return matrix_instance_already_running; }

        void mark_other_notification_as_read(const std::string &event_id);
        bool is_other_notification_read(const std::string &event_id) const;

        // Calls the |MatrixDelegate| pending events.
        // Should be called from the main (ui) thread
        void update();
    private:
        void trigger_event(RoomData *room, MatrixEventType type, MatrixEventUserInfo user_info);
        void trigger_event(RoomData *room, MatrixEventType type, MatrixEventRoomInfo room_info);

        void formatted_body_add_line(RoomData *room, std::string &formatted_body, const std::string &line_str, const std::unordered_map<std::string, CustomEmoji> &custom_emojis);
        void replace_mentions(RoomData *room, std::string &text);
        std::string create_formatted_body_for_message_reply(RoomData *room, const Message *message, const std::string &body);
        std::string create_formatted_body_for_message_edit(RoomData *room, const Message *replied_to_message, const std::string &body);

        PluginResult set_pinned_events(RoomData *room, const std::vector<std::string> &pinned_events, bool is_add);

        PluginResult set_qm_last_read_message_timestamp(RoomData *room, int64_t timestamp);
        bool load_qm_read_markers_from_account_data();

        PluginResult parse_sync_response(const rapidjson::Document &root, 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);
        PluginResult parse_sync_room_data(const rapidjson::Value &rooms_json, bool initial_sync);
        void add_new_invites();
        void parse_custom_emoji(const rapidjson::Value &custom_emoji_json);
        void load_custom_emoji_from_cache();
        void load_other_notifications();
        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);
        void events_set_user_read_marker(const rapidjson::Value &events_json, RoomData *room_data, std::shared_ptr<UserInfo> &me);
        // Returns the number of messages added
        size_t events_add_messages(const rapidjson::Value &events_json, RoomData *room_data, MessageDirection message_dir, bool has_unread_notifications);
        void events_set_room_info(const rapidjson::Value &events_json, RoomData *room_data, int64_t timestamp);
        void set_room_info_to_users_if_empty(RoomData *room, const std::string &room_creator_user_id);
        void events_add_pinned_events(const rapidjson::Value &events_json, RoomData *room_data);
        void events_add_room_to_tags(const rapidjson::Value &events_json, RoomData *room_data);
        void add_invites(const rapidjson::Value &invite_json);
        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(const std::string &filepath, std::string filename, UploadInfo &file_info, UploadInfo &thumbnail_info, std::string &err_msg, bool upload_thumbnail = true, bool bypass_proxy = false);
        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
        bool set_invite(const std::string &room_id, Invite invite);
        // Returns true if an invite for |room_id| exists
        bool remove_invite(const std::string &room_id);
        void set_next_batch(std::string new_next_batch, bool set_initial_sync);
        std::string get_next_batch();
        void set_next_notifications_token(std::string new_next_token);
        std::string get_next_notifications_token();
        std::shared_ptr<UserInfo> get_user_by_id(RoomData *room, const std::string &user_id, bool *is_new_user = nullptr, bool create_if_not_found = true);
        std::string get_filter_cached();
        void load_silenced_invites();
    private:
        MessageQueue<std::function<void()>> ui_thread_tasks;

        std::vector<std::unique_ptr<RoomData>> rooms;
        std::unordered_map<std::string, size_t> room_data_by_id; // value is an index into |rooms|
        std::recursive_mutex room_data_mutex;
        std::string my_user_id;
        std::string access_token;
        std::string homeserver;
        std::string homeserver_domain;
        std::string well_known_base_url;
        std::optional<int64_t> upload_limit;
        std::string next_batch;
        std::string next_notifications_token;
        std::mutex next_batch_mutex;
        bool initial_sync_finished = false;
        bool matrix_instance_already_running = false;

        std::unordered_map<std::string, Invite> invites;
        std::mutex invite_mutex;

        std::vector<MatrixNotification> notifications;
        std::unordered_set<std::string> notifications_by_event_id;
        std::mutex notifications_mutex;

        std::thread sync_thread;
        std::thread notification_thread;
        bool sync_running = false;
        bool sync_failed = false;
        bool finished_fetching_notifications = false;
        std::string sync_fail_reason;
        MatrixDelegate *delegate = nullptr;
        std::optional<std::string> filter_cached;

        std::vector<std::unique_ptr<RoomData>> invite_rooms;

        std::unordered_map<std::string, CustomEmoji> custom_emoji_by_key;
        std::unordered_set<std::string> silenced_invites;
        std::unordered_map<std::string, int64_t> qm_read_markers_by_room_cache;
        std::unordered_set<std::string> other_notifications_read;

        MessageQueue<std::shared_ptr<MatrixChatBodyDecryptJob>> decrypt_task;
        std::thread decrypt_thread;

        std::unordered_map<std::string, RoomExtraData> room_extra_data_by_id;
    };
}