summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorJoseph Donofry <joedonofry@gmail.com>2020-02-28 19:10:39 -0500
committerGitHub <noreply@github.com>2020-02-28 19:10:39 -0500
commit30cb7c5b02bb8a21a8d7257cfe5a8bd3db891606 (patch)
treeddadd16c86600ce937b47798a9b3d5ff2fdec998 /src
parentMerge pull request #129 from nico202/master (diff)
parentMerge branch 'master' into 0.7.0-dev (diff)
downloadnheko-30cb7c5b02bb8a21a8d7257cfe5a8bd3db891606.tar.xz
Merge pull request #130 from Nheko-Reborn/0.7.0-dev
0.7.0 dev merge to master
Diffstat (limited to 'src')
-rw-r--r--src/AvatarProvider.cpp54
-rw-r--r--src/AvatarProvider.h12
-rw-r--r--src/Cache.cpp878
-rw-r--r--src/Cache.h886
-rw-r--r--src/CacheCryptoStructs.h67
-rw-r--r--src/CacheStructs.h91
-rw-r--r--src/Cache_p.h490
-rw-r--r--src/ChatPage.cpp616
-rw-r--r--src/ChatPage.h74
-rw-r--r--src/ColorImageProvider.cpp27
-rw-r--r--src/ColorImageProvider.h11
-rw-r--r--src/CommunitiesList.cpp23
-rw-r--r--src/CommunitiesList.h8
-rw-r--r--src/CommunitiesListItem.cpp3
-rw-r--r--src/CommunitiesListItem.h7
-rw-r--r--src/Config.h6
-rw-r--r--src/EventAccessors.cpp383
-rw-r--r--src/EventAccessors.h64
-rw-r--r--src/InviteeItem.h2
-rw-r--r--src/Logging.cpp48
-rw-r--r--src/Logging.h5
-rw-r--r--src/LoginPage.cpp45
-rw-r--r--src/LoginPage.h2
-rw-r--r--src/MainWindow.cpp43
-rw-r--r--src/MainWindow.h33
-rw-r--r--src/MatrixClient.cpp20
-rw-r--r--src/MatrixClient.h28
-rw-r--r--src/MxcImageProvider.cpp85
-rw-r--r--src/MxcImageProvider.h69
-rw-r--r--src/Olm.cpp50
-rw-r--r--src/Olm.h5
-rw-r--r--src/QuickSwitcher.cpp8
-rw-r--r--src/QuickSwitcher.h4
-rw-r--r--src/RegisterPage.cpp215
-rw-r--r--src/RegisterPage.h9
-rw-r--r--src/RoomInfoListItem.cpp110
-rw-r--r--src/RoomInfoListItem.h21
-rw-r--r--src/RoomList.cpp138
-rw-r--r--src/RoomList.h13
-rw-r--r--src/RunGuard.cpp84
-rw-r--r--src/RunGuard.h31
-rw-r--r--src/SideBarActions.cpp8
-rw-r--r--src/SideBarActions.h2
-rw-r--r--src/Splitter.cpp25
-rw-r--r--src/Splitter.h18
-rw-r--r--src/SuggestionsPopup.cpp296
-rw-r--r--src/SuggestionsPopup.h147
-rw-r--r--src/TextInputWidget.cpp150
-rw-r--r--src/TextInputWidget.h64
-rw-r--r--src/TopRoomBar.cpp47
-rw-r--r--src/TopRoomBar.h40
-rw-r--r--src/TrayIcon.cpp6
-rw-r--r--src/TrayIcon.h13
-rw-r--r--src/TypingDisplay.cpp76
-rw-r--r--src/TypingDisplay.h36
-rw-r--r--src/UserInfoWidget.cpp25
-rw-r--r--src/UserInfoWidget.h4
-rw-r--r--src/UserSettingsPage.cpp338
-rw-r--r--src/UserSettingsPage.h36
-rw-r--r--src/Utils.cpp318
-rw-r--r--src/Utils.h188
-rw-r--r--src/WelcomePage.cpp1
-rw-r--r--src/WelcomePage.h2
-rw-r--r--src/dialogs/CreateRoom.h2
-rw-r--r--src/dialogs/FallbackAuth.cpp69
-rw-r--r--src/dialogs/FallbackAuth.h26
-rw-r--r--src/dialogs/ImageOverlay.cpp22
-rw-r--r--src/dialogs/ImageOverlay.h1
-rw-r--r--src/dialogs/InviteUsers.cpp2
-rw-r--r--src/dialogs/MemberList.cpp24
-rw-r--r--src/dialogs/PreviewUploadOverlay.cpp23
-rw-r--r--src/dialogs/PreviewUploadOverlay.h2
-rw-r--r--src/dialogs/ReCaptcha.cpp5
-rw-r--r--src/dialogs/ReCaptcha.h1
-rw-r--r--src/dialogs/ReadReceipts.cpp24
-rw-r--r--src/dialogs/ReadReceipts.h2
-rw-r--r--src/dialogs/RoomSettings.cpp167
-rw-r--r--src/dialogs/RoomSettings.h12
-rw-r--r--src/dialogs/UserProfile.cpp38
-rw-r--r--src/dialogs/UserProfile.h2
-rw-r--r--src/emoji/Category.cpp12
-rw-r--r--src/emoji/Category.h7
-rw-r--r--src/emoji/ItemDelegate.cpp22
-rw-r--r--src/emoji/ItemDelegate.h2
-rw-r--r--src/emoji/Panel.cpp13
-rw-r--r--src/emoji/Provider.cpp6041
-rw-r--r--src/main.cpp46
-rw-r--r--src/notifications/ManagerLinux.cpp4
-rw-r--r--src/popups/PopupItem.cpp141
-rw-r--r--src/popups/PopupItem.h83
-rw-r--r--src/popups/ReplyPopup.cpp103
-rw-r--r--src/popups/ReplyPopup.h44
-rw-r--r--src/popups/SuggestionsPopup.cpp156
-rw-r--r--src/popups/SuggestionsPopup.h75
-rw-r--r--src/popups/UserMentions.cpp171
-rw-r--r--src/popups/UserMentions.h45
-rw-r--r--src/timeline/DelegateChooser.cpp138
-rw-r--r--src/timeline/DelegateChooser.h82
-rw-r--r--src/timeline/TimelineItem.cpp918
-rw-r--r--src/timeline/TimelineItem.h377
-rw-r--r--src/timeline/TimelineModel.cpp1570
-rw-r--r--src/timeline/TimelineModel.h265
-rw-r--r--src/timeline/TimelineView.cpp1583
-rw-r--r--src/timeline/TimelineView.h444
-rw-r--r--src/timeline/TimelineViewManager.cpp508
-rw-r--r--src/timeline/TimelineViewManager.h126
-rw-r--r--src/timeline/widgets/AudioItem.cpp230
-rw-r--r--src/timeline/widgets/AudioItem.h104
-rw-r--r--src/timeline/widgets/FileItem.cpp215
-rw-r--r--src/timeline/widgets/FileItem.h79
-rw-r--r--src/timeline/widgets/ImageItem.cpp264
-rw-r--r--src/timeline/widgets/ImageItem.h104
-rw-r--r--src/timeline/widgets/VideoItem.cpp65
-rw-r--r--src/timeline/widgets/VideoItem.h51
-rw-r--r--src/ui/Avatar.cpp70
-rw-r--r--src/ui/Avatar.h8
-rw-r--r--src/ui/Badge.h6
-rw-r--r--src/ui/DropShadow.cpp108
-rw-r--r--src/ui/DropShadow.h97
-rw-r--r--src/ui/FlatButton.cpp2
-rw-r--r--src/ui/FlatButton.h12
-rw-r--r--src/ui/FloatingButton.cpp1
-rw-r--r--src/ui/InfoMessage.cpp29
-rw-r--r--src/ui/LoadingIndicator.cpp5
-rw-r--r--src/ui/LoadingIndicator.h10
-rw-r--r--src/ui/OverlayWidget.cpp4
-rw-r--r--src/ui/OverlayWidget.h4
-rw-r--r--src/ui/Painter.h12
-rw-r--r--src/ui/RaisedButton.h6
-rw-r--r--src/ui/Ripple.cpp2
-rw-r--r--src/ui/Ripple.h4
-rw-r--r--src/ui/RippleOverlay.h2
-rw-r--r--src/ui/SnackBar.cpp2
-rw-r--r--src/ui/TextField.cpp27
-rw-r--r--src/ui/TextField.h6
-rw-r--r--src/ui/TextLabel.h2
-rw-r--r--src/ui/Theme.h2
-rw-r--r--src/ui/ToggleButton.cpp2
138 files changed, 12530 insertions, 9011 deletions
diff --git a/src/AvatarProvider.cpp b/src/AvatarProvider.cpp

index 57b61c75..d0556f85 100644 --- a/src/AvatarProvider.cpp +++ b/src/AvatarProvider.cpp
@@ -16,30 +16,37 @@ */ #include <QBuffer> +#include <QPixmapCache> #include <memory> +#include <unordered_map> #include "AvatarProvider.h" #include "Cache.h" #include "Logging.h" #include "MatrixClient.h" -namespace AvatarProvider { +static QPixmapCache avatar_cache; +namespace AvatarProvider { void -resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback callback) +resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback callback) { - const auto key = QString("%1 %2").arg(room_id).arg(user_id); - const auto avatarUrl = Cache::avatarUrl(room_id, user_id); + const auto cacheKey = QString("%1_size_%2").arg(avatarUrl).arg(size); - if (!Cache::AvatarUrls.contains(key) || !cache::client()) + if (avatarUrl.isEmpty()) return; - if (avatarUrl.isEmpty()) + QPixmap pixmap; + if (avatar_cache.find(cacheKey, &pixmap)) { + callback(pixmap); return; + } - auto data = cache::client()->image(avatarUrl); + auto data = cache::image(cacheKey); if (!data.isNull()) { - callback(QImage::fromData(data)); + pixmap.loadFromData(data); + avatar_cache.insert(cacheKey, pixmap); + callback(pixmap); return; } @@ -47,16 +54,22 @@ resolve(const QString &room_id, const QString &user_id, QObject *receiver, Avata QObject::connect(proxy.get(), &AvatarProxy::avatarDownloaded, receiver, - [callback](const QByteArray &data) { callback(QImage::fromData(data)); }); + [callback, cacheKey](const QByteArray &data) { + QPixmap pm; + pm.loadFromData(data); + avatar_cache.insert(cacheKey, pm); + callback(pm); + }); mtx::http::ThumbOpts opts; - opts.width = 256; - opts.height = 256; + opts.width = size; + opts.height = size; opts.mxc_url = avatarUrl.toStdString(); http::client()->get_thumbnail( opts, - [opts, proxy = std::move(proxy)](const std::string &res, mtx::http::RequestErr err) { + [opts, cacheKey, proxy = std::move(proxy)](const std::string &res, + mtx::http::RequestErr err) { if (err) { nhlog::net()->warn("failed to download avatar: {} - ({} {})", opts.mxc_url, @@ -65,10 +78,21 @@ resolve(const QString &room_id, const QString &user_id, QObject *receiver, Avata return; } - cache::client()->saveImage(opts.mxc_url, res); + cache::saveImage(cacheKey.toStdString(), res); - auto data = QByteArray(res.data(), res.size()); - emit proxy->avatarDownloaded(data); + emit proxy->avatarDownloaded(QByteArray(res.data(), res.size())); }); } + +void +resolve(const QString &room_id, + const QString &user_id, + int size, + QObject *receiver, + AvatarCallback callback) +{ + const auto avatarUrl = cache::avatarUrl(room_id, user_id); + + resolve(avatarUrl, size, receiver, callback); +} } diff --git a/src/AvatarProvider.h b/src/AvatarProvider.h
index 4b4e15e9..47ed028e 100644 --- a/src/AvatarProvider.h +++ b/src/AvatarProvider.h
@@ -17,7 +17,7 @@ #pragma once -#include <QImage> +#include <QPixmap> #include <functional> class AvatarProxy : public QObject @@ -28,9 +28,15 @@ signals: void avatarDownloaded(const QByteArray &data); }; -using AvatarCallback = std::function<void(QImage)>; +using AvatarCallback = std::function<void(QPixmap)>; namespace AvatarProvider { void -resolve(const QString &room_id, const QString &user_id, QObject *receiver, AvatarCallback cb); +resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback cb); +void +resolve(const QString &room_id, + const QString &user_id, + int size, + QObject *receiver, + AvatarCallback cb); } diff --git a/src/Cache.cpp b/src/Cache.cpp
index 81054ddc..0f33a276 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp
@@ -17,17 +17,21 @@ #include <limits> #include <stdexcept> +#include <variant> #include <QByteArray> +#include <QCoreApplication> #include <QFile> #include <QHash> +#include <QMap> #include <QSettings> #include <QStandardPaths> -#include <boost/variant.hpp> #include <mtx/responses/common.hpp> #include "Cache.h" +#include "Cache_p.h" +#include "Logging.h" #include "Utils.h" //! Should be changed when a breaking change occurs in the cache format. @@ -35,13 +39,13 @@ static const std::string CURRENT_CACHE_FORMAT_VERSION("2018.09.21"); static const std::string SECRET("secret"); -static const lmdb::val NEXT_BATCH_KEY("next_batch"); -static const lmdb::val OLM_ACCOUNT_KEY("olm_account"); -static const lmdb::val CACHE_FORMAT_VERSION_KEY("cache_format_version"); +static lmdb::val NEXT_BATCH_KEY("next_batch"); +static lmdb::val OLM_ACCOUNT_KEY("olm_account"); +static lmdb::val CACHE_FORMAT_VERSION_KEY("cache_format_version"); -constexpr size_t MAX_RESTORED_MESSAGES = 30; +constexpr size_t MAX_RESTORED_MESSAGES = 30'000; -constexpr auto DB_SIZE = 512UL * 1024UL * 1024UL; // 512 MB +constexpr auto DB_SIZE = 32ULL * 1024ULL * 1024ULL * 1024ULL; // 32 GB constexpr auto MAX_DBS = 8092UL; //! Cache databases and their format. @@ -76,32 +80,30 @@ constexpr auto OUTBOUND_MEGOLM_SESSIONS_DB("outbound_megolm_sessions"); using CachedReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>; using Receipts = std::map<std::string, std::map<std::string, uint64_t>>; +Q_DECLARE_METATYPE(SearchResult) +Q_DECLARE_METATYPE(std::vector<SearchResult>) +Q_DECLARE_METATYPE(RoomMember) +Q_DECLARE_METATYPE(mtx::responses::Timeline) +Q_DECLARE_METATYPE(RoomSearchResult) +Q_DECLARE_METATYPE(RoomInfo) + namespace { std::unique_ptr<Cache> instance_ = nullptr; } -namespace cache { -void -init(const QString &user_id) +int +numeric_key_comparison(const MDB_val *a, const MDB_val *b) { - qRegisterMetaType<SearchResult>(); - qRegisterMetaType<QVector<SearchResult>>(); - qRegisterMetaType<RoomMember>(); - qRegisterMetaType<RoomSearchResult>(); - qRegisterMetaType<RoomInfo>(); - qRegisterMetaType<QMap<QString, RoomInfo>>(); - qRegisterMetaType<std::map<QString, RoomInfo>>(); - qRegisterMetaType<std::map<QString, mtx::responses::Timeline>>(); + auto lhs = std::stoull(std::string((char *)a->mv_data, a->mv_size)); + auto rhs = std::stoull(std::string((char *)b->mv_data, b->mv_size)); - instance_ = std::make_unique<Cache>(user_id); -} + if (lhs < rhs) + return 1; + else if (lhs == rhs) + return 0; -Cache * -client() -{ - return instance_.get(); + return -1; } -} // namespace cache Cache::Cache(const QString &userId, QObject *parent) : QObject{parent} @@ -392,7 +394,7 @@ Cache::saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr txn.commit(); } -boost::optional<mtx::crypto::OlmSessionPtr> +std::optional<mtx::crypto::OlmSessionPtr> Cache::getOlmSession(const std::string &curve25519, const std::string &session_id) { using namespace mtx::crypto; @@ -410,7 +412,7 @@ Cache::getOlmSession(const std::string &curve25519, const std::string &session_i return unpickle<SessionObject>(data, SECRET); } - return boost::none; + return std::nullopt; } std::vector<std::string> @@ -913,13 +915,17 @@ Cache::calculateRoomReadStatus(const std::string &room_id) auto txn = lmdb::txn::begin(env_); // Get last event id on the room. - const auto last_event_id = getLastMessageInfo(txn, room_id).event_id; + const auto last_event_id = getLastEventId(txn, room_id); const auto localUser = utils::localUser().toStdString(); txn.commit(); + if (last_event_id.empty()) + return false; + // Retrieve all read receipts for that event. - const auto receipts = readReceipts(last_event_id, QString::fromStdString(room_id)); + const auto receipts = + readReceipts(QString::fromStdString(last_event_id), QString::fromStdString(room_id)); if (receipts.size() == 0) return true; @@ -958,13 +964,14 @@ Cache::saveState(const mtx::responses::Sync &res) updatedInfo.avatar_url = getRoomAvatarUrl(txn, statesdb, membersdb, QString::fromStdString(room.first)) .toStdString(); + updatedInfo.version = getRoomVersion(txn, statesdb).toStdString(); // Process the account_data associated with this room bool has_new_tags = false; for (const auto &evt : room.second.account_data.events) { // for now only fetch tag events - if (evt.type() == typeid(Event<account_data::Tag>)) { - auto tags_evt = boost::get<Event<account_data::Tag>>(evt); + if (std::holds_alternative<Event<account_data::Tag>>(evt)) { + auto tags_evt = std::get<Event<account_data::Tag>>(evt); has_new_tags = true; for (const auto &tag : tags_evt.content.tags) { updatedInfo.tags.push_back(tag.first); @@ -1045,19 +1052,17 @@ Cache::saveInvite(lmdb::txn &txn, using namespace mtx::events::state; for (const auto &e : room.invite_state) { - if (boost::get<StrippedEvent<Member>>(&e) != nullptr) { - auto msg = boost::get<StrippedEvent<Member>>(e); - - auto display_name = msg.content.display_name.empty() - ? msg.state_key - : msg.content.display_name; + if (auto msg = std::get_if<StrippedEvent<Member>>(&e)) { + auto display_name = msg->content.display_name.empty() + ? msg->state_key + : msg->content.display_name; - MemberInfo tmp{display_name, msg.content.avatar_url}; + MemberInfo tmp{display_name, msg->content.avatar_url}; lmdb::dbi_put( - txn, membersdb, lmdb::val(msg.state_key), lmdb::val(json(tmp).dump())); + txn, membersdb, lmdb::val(msg->state_key), lmdb::val(json(tmp).dump())); } else { - boost::apply_visitor( + std::visit( [&txn, &statesdb](auto msg) { bool res = lmdb::dbi_put(txn, statesdb, @@ -1065,8 +1070,8 @@ Cache::saveInvite(lmdb::txn &txn, lmdb::val(json(msg).dump())); if (!res) - std::cout << "couldn't save data" << json(msg).dump() - << '\n'; + nhlog::db()->warn("couldn't save data: {}", + json(msg).dump()); }, e); } @@ -1118,7 +1123,7 @@ Cache::roomsWithTagUpdates(const mtx::responses::Sync &res) for (const auto &room : res.rooms.join) { bool hasUpdates = false; for (const auto &evt : room.second.account_data.events) { - if (evt.type() == typeid(Event<account_data::Tag>)) { + if (std::holds_alternative<Event<account_data::Tag>>(evt)) { hasUpdates = true; } } @@ -1230,9 +1235,31 @@ Cache::roomMessages() return msgs; } +QMap<QString, mtx::responses::Notifications> +Cache::getTimelineMentions() +{ + // TODO: Should be read-only, but getMentionsDb will attempt to create a DB + // if it doesn't exist, throwing an error. + auto txn = lmdb::txn::begin(env_, nullptr); + + QMap<QString, mtx::responses::Notifications> notifs; + + auto room_ids = getRoomIds(txn); + + for (const auto &room_id : room_ids) { + auto roomNotifs = getTimelineMentionsForRoom(txn, room_id); + notifs[QString::fromStdString(room_id)] = roomNotifs; + } + + txn.commit(); + + return notifs; +} + mtx::responses::Timeline Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id) { + // TODO(nico): Limit the messages returned by this maybe? auto db = getMessagesDb(txn, room_id); mtx::responses::Timeline timeline; @@ -1300,6 +1327,31 @@ Cache::roomInfo(bool withInvites) return result; } +std::string +Cache::getLastEventId(lmdb::txn &txn, const std::string &room_id) +{ + auto db = getMessagesDb(txn, room_id); + + if (db.size(txn) == 0) + return {}; + + std::string timestamp, msg; + + auto cursor = lmdb::cursor::open(txn, db); + while (cursor.get(timestamp, msg, MDB_NEXT)) { + auto obj = json::parse(msg); + + if (obj.count("event") == 0) + continue; + + cursor.close(); + return obj["event"]["event_id"]; + } + cursor.close(); + + return {}; +} + DescInfo Cache::getLastMessageInfo(lmdb::txn &txn, const std::string &room_id) { @@ -1311,13 +1363,15 @@ Cache::getLastMessageInfo(lmdb::txn &txn, const std::string &room_id) std::string timestamp, msg; QSettings settings; - auto local_user = settings.value("auth/user_id").toString(); + const auto local_user = utils::localUser(); auto cursor = lmdb::cursor::open(txn, db); while (cursor.get(timestamp, msg, MDB_NEXT)) { auto obj = json::parse(msg); - if (obj.count("event") == 0) + if (obj.count("event") == 0 || !(obj["event"]["type"] == "m.room.message" || + obj["event"]["type"] == "m.sticker" || + obj["event"]["type"] == "m.room.encrypted")) continue; mtx::events::collections::TimelineEvent event; @@ -1481,7 +1535,7 @@ Cache::getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) return "Empty Room"; } -JoinRule +mtx::events::state::JoinRule Cache::getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb) { using namespace mtx::events; @@ -1493,14 +1547,14 @@ Cache::getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb) if (res) { try { - StateEvent<JoinRules> msg = + StateEvent<state::JoinRules> msg = json::parse(std::string(event.data(), event.size())); return msg.content.join_rule; } catch (const json::exception &e) { nhlog::db()->warn("failed to parse m.room.join_rule event: {}", e.what()); } } - return JoinRule::Knock; + return state::JoinRule::Knock; } bool @@ -1552,6 +1606,32 @@ Cache::getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb) } QString +Cache::getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + using namespace mtx::events; + using namespace mtx::events::state; + + lmdb::val event; + bool res = lmdb::dbi_get( + txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomCreate)), event); + + if (res) { + try { + StateEvent<Create> msg = + json::parse(std::string(event.data(), event.size())); + + if (!msg.content.room_version.empty()) + return QString::fromStdString(msg.content.room_version); + } catch (const json::exception &e) { + nhlog::db()->warn("failed to parse m.room.create event: {}", e.what()); + } + } + + nhlog::db()->warn("m.room.create event is missing room version, assuming version \"1\""); + return QString("1"); +} + +QString Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) { using namespace mtx::events; @@ -1779,10 +1859,7 @@ Cache::searchRooms(const std::string &query, std::uint8_t max_items) std::vector<RoomSearchResult> results; for (auto it = items.begin(); it != end; it++) { - results.push_back( - RoomSearchResult{it->second.first, - it->second.second, - QImage::fromData(image(txn, it->second.second.avatar_url))}); + results.push_back(RoomSearchResult{it->second.first, it->second.second}); } txn.commit(); @@ -1790,7 +1867,7 @@ Cache::searchRooms(const std::string &query, std::uint8_t max_items) return results; } -QVector<SearchResult> +std::vector<SearchResult> Cache::searchUsers(const std::string &room_id, const std::string &query, std::uint8_t max_items) { std::multimap<int, std::pair<std::string, std::string>> items; @@ -1813,7 +1890,7 @@ Cache::searchUsers(const std::string &room_id, const std::string &query, std::ui else if (items.size() > 0) std::advance(end, items.size()); - QVector<SearchResult> results; + std::vector<SearchResult> results; for (auto it = items.begin(); it != end; it++) { const auto user = it->second; results.push_back(SearchResult{QString::fromStdString(user.first), @@ -1889,10 +1966,7 @@ Cache::saveTimelineMessages(lmdb::txn &txn, using namespace mtx::events::state; for (const auto &e : res.events) { - if (isStateEvent(e)) - continue; - - if (boost::get<RedactionEvent<msg::Redaction>>(&e) != nullptr) + if (std::holds_alternative<RedactionEvent<msg::Redaction>>(e)) continue; json obj = json::object(); @@ -1907,6 +1981,88 @@ Cache::saveTimelineMessages(lmdb::txn &txn, } } +mtx::responses::Notifications +Cache::getTimelineMentionsForRoom(lmdb::txn &txn, const std::string &room_id) +{ + auto db = getMentionsDb(txn, room_id); + + if (db.size(txn) == 0) { + return mtx::responses::Notifications{}; + } + + mtx::responses::Notifications notif; + std::string event_id, msg; + + auto cursor = lmdb::cursor::open(txn, db); + + while (cursor.get(event_id, msg, MDB_NEXT)) { + auto obj = json::parse(msg); + + if (obj.count("event") == 0) + continue; + + mtx::responses::Notification notification; + mtx::responses::from_json(obj, notification); + + notif.notifications.push_back(notification); + } + cursor.close(); + + std::reverse(notif.notifications.begin(), notif.notifications.end()); + + return notif; +} + +//! Add all notifications containing a user mention to the db. +void +Cache::saveTimelineMentions(const mtx::responses::Notifications &res) +{ + QMap<std::string, QList<mtx::responses::Notification>> notifsByRoom; + + // Sort into room-specific 'buckets' + for (const auto &notif : res.notifications) { + json val = notif; + notifsByRoom[notif.room_id].push_back(notif); + } + + auto txn = lmdb::txn::begin(env_); + // Insert the entire set of mentions for each room at a time. + QMap<std::string, QList<mtx::responses::Notification>>::const_iterator it = + notifsByRoom.constBegin(); + auto end = notifsByRoom.constEnd(); + while (it != end) { + nhlog::db()->debug("Storing notifications for " + it.key()); + saveTimelineMentions(txn, it.key(), std::move(it.value())); + ++it; + } + + txn.commit(); +} + +void +Cache::saveTimelineMentions(lmdb::txn &txn, + const std::string &room_id, + const QList<mtx::responses::Notification> &res) +{ + auto db = getMentionsDb(txn, room_id); + + using namespace mtx::events; + using namespace mtx::events::state; + + for (const auto &notif : res) { + const auto event_id = utils::event_id(notif.event); + + // double check that we have the correct room_id... + if (room_id.compare(notif.room_id) != 0) { + return; + } + + json obj = notif; + + lmdb::dbi_put(txn, db, lmdb::val(event_id), lmdb::val(obj.dump())); + } +} + void Cache::markSentNotification(const std::string &event_id) { @@ -2059,7 +2215,6 @@ Cache::roomMembers(const std::string &room_id) QHash<QString, QString> Cache::DisplayNames; QHash<QString, QString> Cache::AvatarUrls; -QHash<QString, QString> Cache::UserColors; QString Cache::displayName(const QString &room_id, const QString &user_id) @@ -2091,16 +2246,6 @@ Cache::avatarUrl(const QString &room_id, const QString &user_id) return QString(); } -QString -Cache::userColor(const QString &user_id) -{ - if (UserColors.contains(user_id)) { - return UserColors[user_id]; - } - - return QString(); -} - void Cache::insertDisplayName(const QString &room_id, const QString &user_id, @@ -2132,19 +2277,604 @@ Cache::removeAvatarUrl(const QString &room_id, const QString &user_id) } void -Cache::insertUserColor(const QString &user_id, const QString &color_name) +to_json(json &j, const RoomInfo &info) +{ + j["name"] = info.name; + j["topic"] = info.topic; + j["avatar_url"] = info.avatar_url; + j["version"] = info.version; + j["is_invite"] = info.is_invite; + j["join_rule"] = info.join_rule; + j["guest_access"] = info.guest_access; + + if (info.member_count != 0) + j["member_count"] = info.member_count; + + if (info.tags.size() != 0) + j["tags"] = info.tags; +} + +void +from_json(const json &j, RoomInfo &info) { - UserColors.insert(user_id, color_name); + info.name = j.at("name"); + info.topic = j.at("topic"); + info.avatar_url = j.at("avatar_url"); + info.version = j.value( + "version", QCoreApplication::translate("RoomInfo", "no version stored").toStdString()); + info.is_invite = j.at("is_invite"); + info.join_rule = j.at("join_rule"); + info.guest_access = j.at("guest_access"); + + if (j.count("member_count")) + info.member_count = j.at("member_count"); + + if (j.count("tags")) + info.tags = j.at("tags").get<std::vector<std::string>>(); +} + +void +to_json(json &j, const ReadReceiptKey &key) +{ + j = json{{"event_id", key.event_id}, {"room_id", key.room_id}}; } void -Cache::removeUserColor(const QString &user_id) +from_json(const json &j, ReadReceiptKey &key) { - UserColors.remove(user_id); + key.event_id = j.at("event_id").get<std::string>(); + key.room_id = j.at("room_id").get<std::string>(); } void -Cache::clearUserColors() +to_json(json &j, const MemberInfo &info) { - UserColors.clear(); + j["name"] = info.name; + j["avatar_url"] = info.avatar_url; } + +void +from_json(const json &j, MemberInfo &info) +{ + info.name = j.at("name"); + info.avatar_url = j.at("avatar_url"); +} + +void +to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg) +{ + obj["session_id"] = msg.session_id; + obj["session_key"] = msg.session_key; + obj["message_index"] = msg.message_index; +} + +void +from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg) +{ + msg.session_id = obj.at("session_id"); + msg.session_key = obj.at("session_key"); + msg.message_index = obj.at("message_index"); +} + +void +to_json(nlohmann::json &obj, const DevicePublicKeys &msg) +{ + obj["ed25519"] = msg.ed25519; + obj["curve25519"] = msg.curve25519; +} + +void +from_json(const nlohmann::json &obj, DevicePublicKeys &msg) +{ + msg.ed25519 = obj.at("ed25519"); + msg.curve25519 = obj.at("curve25519"); +} + +void +to_json(nlohmann::json &obj, const MegolmSessionIndex &msg) +{ + obj["room_id"] = msg.room_id; + obj["session_id"] = msg.session_id; + obj["sender_key"] = msg.sender_key; +} + +void +from_json(const nlohmann::json &obj, MegolmSessionIndex &msg) +{ + msg.room_id = obj.at("room_id"); + msg.session_id = obj.at("session_id"); + msg.sender_key = obj.at("sender_key"); +} + +namespace cache { +void +init(const QString &user_id) +{ + qRegisterMetaType<SearchResult>(); + qRegisterMetaType<std::vector<SearchResult>>(); + qRegisterMetaType<RoomMember>(); + qRegisterMetaType<RoomSearchResult>(); + qRegisterMetaType<RoomInfo>(); + qRegisterMetaType<QMap<QString, RoomInfo>>(); + qRegisterMetaType<std::map<QString, RoomInfo>>(); + qRegisterMetaType<std::map<QString, mtx::responses::Timeline>>(); + + instance_ = std::make_unique<Cache>(user_id); +} + +Cache * +client() +{ + return instance_.get(); +} + +std::string +displayName(const std::string &room_id, const std::string &user_id) +{ + return instance_->displayName(room_id, user_id); +} + +QString +displayName(const QString &room_id, const QString &user_id) +{ + return instance_->displayName(room_id, user_id); +} +QString +avatarUrl(const QString &room_id, const QString &user_id) +{ + return instance_->avatarUrl(room_id, user_id); +} + +void +removeDisplayName(const QString &room_id, const QString &user_id) +{ + instance_->removeDisplayName(room_id, user_id); +} +void +removeAvatarUrl(const QString &room_id, const QString &user_id) +{ + instance_->removeAvatarUrl(room_id, user_id); +} + +void +insertDisplayName(const QString &room_id, const QString &user_id, const QString &display_name) +{ + instance_->insertDisplayName(room_id, user_id, display_name); +} +void +insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url) +{ + instance_->insertAvatarUrl(room_id, user_id, avatar_url); +} + +//! Load saved data for the display names & avatars. +void +populateMembers() +{ + instance_->populateMembers(); +} + +std::vector<std::string> +joinedRooms() +{ + return instance_->joinedRooms(); +} + +QMap<QString, RoomInfo> +roomInfo(bool withInvites) +{ + return instance_->roomInfo(withInvites); +} +std::map<QString, bool> +invites() +{ + return instance_->invites(); +} + +QString +getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb) +{ + return instance_->getRoomName(txn, statesdb, membersdb); +} +mtx::events::state::JoinRule +getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + return instance_->getRoomJoinRule(txn, statesdb); +} +bool +getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + return instance_->getRoomGuestAccess(txn, statesdb); +} +QString +getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + return instance_->getRoomTopic(txn, statesdb); +} +QString +getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb, const QString &room_id) +{ + return instance_->getRoomAvatarUrl(txn, statesdb, membersdb, room_id); +} + +QString +getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb) +{ + return instance_->getRoomVersion(txn, statesdb); +} + +std::vector<RoomMember> +getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len) +{ + return instance_->getMembers(room_id, startIndex, len); +} + +void +saveState(const mtx::responses::Sync &res) +{ + instance_->saveState(res); +} +bool +isInitialized() +{ + return instance_->isInitialized(); +} + +std::string +nextBatchToken() +{ + return instance_->nextBatchToken(); +} + +void +deleteData() +{ + instance_->deleteData(); +} + +void +removeInvite(lmdb::txn &txn, const std::string &room_id) +{ + instance_->removeInvite(txn, room_id); +} +void +removeInvite(const std::string &room_id) +{ + instance_->removeInvite(room_id); +} +void +removeRoom(lmdb::txn &txn, const std::string &roomid) +{ + instance_->removeRoom(txn, roomid); +} +void +removeRoom(const std::string &roomid) +{ + instance_->removeRoom(roomid); +} +void +removeRoom(const QString &roomid) +{ + instance_->removeRoom(roomid.toStdString()); +} +void +setup() +{ + instance_->setup(); +} + +bool +isFormatValid() +{ + return instance_->isFormatValid(); +} +void +setCurrentFormat() +{ + instance_->setCurrentFormat(); +} + +std::map<QString, mtx::responses::Timeline> +roomMessages() +{ + return instance_->roomMessages(); +} + +QMap<QString, mtx::responses::Notifications> +getTimelineMentions() +{ + return instance_->getTimelineMentions(); +} + +//! Retrieve all the user ids from a room. +std::vector<std::string> +roomMembers(const std::string &room_id) +{ + return instance_->roomMembers(room_id); +} + +//! Check if the given user has power leve greater than than +//! lowest power level of the given events. +bool +hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes, + const std::string &room_id, + const std::string &user_id) +{ + return instance_->hasEnoughPowerLevel(eventTypes, room_id, user_id); +} + +//! Retrieves the saved room avatar. +QImage +getRoomAvatar(const QString &id) +{ + return instance_->getRoomAvatar(id); +} +QImage +getRoomAvatar(const std::string &id) +{ + return instance_->getRoomAvatar(id); +} + +void +updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts) +{ + instance_->updateReadReceipt(txn, room_id, receipts); +} + +UserReceipts +readReceipts(const QString &event_id, const QString &room_id) +{ + return instance_->readReceipts(event_id, room_id); +} + +//! Filter the events that have at least one read receipt. +std::vector<QString> +filterReadEvents(const QString &room_id, + const std::vector<QString> &event_ids, + const std::string &excluded_user) +{ + return instance_->filterReadEvents(room_id, event_ids, excluded_user); +} +//! Add event for which we are expecting some read receipts. +void +addPendingReceipt(const QString &room_id, const QString &event_id) +{ + instance_->addPendingReceipt(room_id, event_id); +} +void +removePendingReceipt(lmdb::txn &txn, const std::string &room_id, const std::string &event_id) +{ + instance_->removePendingReceipt(txn, room_id, event_id); +} +void +notifyForReadReceipts(const std::string &room_id) +{ + instance_->notifyForReadReceipts(room_id); +} +std::vector<QString> +pendingReceiptsEvents(lmdb::txn &txn, const std::string &room_id) +{ + return instance_->pendingReceiptsEvents(txn, room_id); +} + +QByteArray +image(const QString &url) +{ + return instance_->image(url); +} +QByteArray +image(lmdb::txn &txn, const std::string &url) +{ + return instance_->image(txn, url); +} +void +saveImage(const std::string &url, const std::string &data) +{ + instance_->saveImage(url, data); +} +void +saveImage(const QString &url, const QByteArray &data) +{ + instance_->saveImage(url, data); +} + +RoomInfo +singleRoomInfo(const std::string &room_id) +{ + return instance_->singleRoomInfo(room_id); +} +std::vector<std::string> +roomsWithStateUpdates(const mtx::responses::Sync &res) +{ + return instance_->roomsWithStateUpdates(res); +} +std::vector<std::string> +roomsWithTagUpdates(const mtx::responses::Sync &res) +{ + return instance_->roomsWithTagUpdates(res); +} +std::map<QString, RoomInfo> +getRoomInfo(const std::vector<std::string> &rooms) +{ + return instance_->getRoomInfo(rooms); +} + +//! Calculates which the read status of a room. +//! Whether all the events in the timeline have been read. +bool +calculateRoomReadStatus(const std::string &room_id) +{ + return instance_->calculateRoomReadStatus(room_id); +} +void +calculateRoomReadStatus() +{ + instance_->calculateRoomReadStatus(); +} + +std::vector<SearchResult> +searchUsers(const std::string &room_id, const std::string &query, std::uint8_t max_items) +{ + return instance_->searchUsers(room_id, query, max_items); +} +std::vector<RoomSearchResult> +searchRooms(const std::string &query, std::uint8_t max_items) +{ + return instance_->searchRooms(query, max_items); +} + +void +markSentNotification(const std::string &event_id) +{ + instance_->markSentNotification(event_id); +} +//! Removes an event from the sent notifications. +void +removeReadNotification(const std::string &event_id) +{ + instance_->removeReadNotification(event_id); +} +//! Check if we have sent a desktop notification for the given event id. +bool +isNotificationSent(const std::string &event_id) +{ + return instance_->isNotificationSent(event_id); +} + +//! Add all notifications containing a user mention to the db. +void +saveTimelineMentions(const mtx::responses::Notifications &res) +{ + instance_->saveTimelineMentions(res); +} + +//! Remove old unused data. +void +deleteOldMessages() +{ + instance_->deleteOldMessages(); +} +void +deleteOldData() noexcept +{ + instance_->deleteOldData(); +} +//! Retrieve all saved room ids. +std::vector<std::string> +getRoomIds(lmdb::txn &txn) +{ + return instance_->getRoomIds(txn); +} + +//! Mark a room that uses e2e encryption. +void +setEncryptedRoom(lmdb::txn &txn, const std::string &room_id) +{ + instance_->setEncryptedRoom(txn, room_id); +} +bool +isRoomEncrypted(const std::string &room_id) +{ + return instance_->isRoomEncrypted(room_id); +} + +//! Check if a user is a member of the room. +bool +isRoomMember(const std::string &user_id, const std::string &room_id) +{ + return instance_->isRoomMember(user_id, room_id); +} + +// +// Outbound Megolm Sessions +// +void +saveOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data, + mtx::crypto::OutboundGroupSessionPtr session) +{ + instance_->saveOutboundMegolmSession(room_id, data, std::move(session)); +} +OutboundGroupSessionDataRef +getOutboundMegolmSession(const std::string &room_id) +{ + return instance_->getOutboundMegolmSession(room_id); +} +bool +outboundMegolmSessionExists(const std::string &room_id) noexcept +{ + return instance_->outboundMegolmSessionExists(room_id); +} +void +updateOutboundMegolmSession(const std::string &room_id, int message_index) +{ + instance_->updateOutboundMegolmSession(room_id, message_index); +} + +void +importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys) +{ + instance_->importSessionKeys(keys); +} +mtx::crypto::ExportedSessionKeys +exportSessionKeys() +{ + return instance_->exportSessionKeys(); +} + +// +// Inbound Megolm Sessions +// +void +saveInboundMegolmSession(const MegolmSessionIndex &index, + mtx::crypto::InboundGroupSessionPtr session) +{ + instance_->saveInboundMegolmSession(index, std::move(session)); +} +OlmInboundGroupSession * +getInboundMegolmSession(const MegolmSessionIndex &index) +{ + return instance_->getInboundMegolmSession(index); +} +bool +inboundMegolmSessionExists(const MegolmSessionIndex &index) +{ + return instance_->inboundMegolmSessionExists(index); +} + +// +// Olm Sessions +// +void +saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session) +{ + instance_->saveOlmSession(curve25519, std::move(session)); +} +std::vector<std::string> +getOlmSessions(const std::string &curve25519) +{ + return instance_->getOlmSessions(curve25519); +} +std::optional<mtx::crypto::OlmSessionPtr> +getOlmSession(const std::string &curve25519, const std::string &session_id) +{ + return instance_->getOlmSession(curve25519, session_id); +} + +void +saveOlmAccount(const std::string &pickled) +{ + instance_->saveOlmAccount(pickled); +} +std::string +restoreOlmAccount() +{ + return instance_->restoreOlmAccount(); +} + +void +restoreSessions() +{ + return instance_->restoreSessions(); +} +} // namespace cache diff --git a/src/Cache.h b/src/Cache.h
index b9cf0aeb..bb042ea9 100644 --- a/src/Cache.h +++ b/src/Cache.h
@@ -17,716 +17,280 @@ #pragma once -#include <boost/optional.hpp> - #include <QDateTime> #include <QDir> #include <QImage> #include <QString> +#if __has_include(<lmdbxx/lmdb++.h>) +#include <lmdbxx/lmdb++.h> +#else #include <lmdb++.h> -#include <mtx/events/join_rules.hpp> -#include <mtx/responses.hpp> -#include <mtxclient/crypto/client.hpp> -#include <mutex> -#include <nlohmann/json.hpp> - -#include "Logging.h" - -using mtx::events::state::JoinRule; - -struct RoomMember -{ - QString user_id; - QString display_name; - QImage avatar; -}; - -struct SearchResult -{ - QString user_id; - QString display_name; -}; - -static int -numeric_key_comparison(const MDB_val *a, const MDB_val *b) -{ - auto lhs = std::stoull(std::string((char *)a->mv_data, a->mv_size)); - auto rhs = std::stoull(std::string((char *)b->mv_data, b->mv_size)); +#endif - if (lhs < rhs) - return 1; - else if (lhs == rhs) - return 0; - - return -1; -} - -Q_DECLARE_METATYPE(SearchResult) -Q_DECLARE_METATYPE(QVector<SearchResult>) -Q_DECLARE_METATYPE(RoomMember) -Q_DECLARE_METATYPE(mtx::responses::Timeline) +#include <mtx/responses.hpp> -//! Used to uniquely identify a list of read receipts. -struct ReadReceiptKey -{ - std::string event_id; - std::string room_id; -}; +#include "CacheCryptoStructs.h" +#include "CacheStructs.h" -inline void -to_json(json &j, const ReadReceiptKey &key) -{ - j = json{{"event_id", key.event_id}, {"room_id", key.room_id}}; -} +namespace cache { +void +init(const QString &user_id); -inline void -from_json(const json &j, ReadReceiptKey &key) -{ - key.event_id = j.at("event_id").get<std::string>(); - key.room_id = j.at("room_id").get<std::string>(); -} +std::string +displayName(const std::string &room_id, const std::string &user_id); +QString +displayName(const QString &room_id, const QString &user_id); +QString +avatarUrl(const QString &room_id, const QString &user_id); -struct DescInfo -{ - QString event_id; - QString username; - QString userid; - QString body; - QString timestamp; - QDateTime datetime; -}; +void +removeDisplayName(const QString &room_id, const QString &user_id); +void +removeAvatarUrl(const QString &room_id, const QString &user_id); -//! UI info associated with a room. -struct RoomInfo -{ - //! The calculated name of the room. - std::string name; - //! The topic of the room. - std::string topic; - //! The calculated avatar url of the room. - std::string avatar_url; - //! Whether or not the room is an invite. - bool is_invite = false; - //! Total number of members in the room. - int16_t member_count = 0; - //! Who can access to the room. - JoinRule join_rule = JoinRule::Public; - bool guest_access = false; - //! Metadata describing the last message in the timeline. - DescInfo msgInfo; - //! The list of tags associated with this room - std::vector<std::string> tags; -}; +void +insertDisplayName(const QString &room_id, const QString &user_id, const QString &display_name); +void +insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url); -inline void -to_json(json &j, const RoomInfo &info) -{ - j["name"] = info.name; - j["topic"] = info.topic; - j["avatar_url"] = info.avatar_url; - j["is_invite"] = info.is_invite; - j["join_rule"] = info.join_rule; - j["guest_access"] = info.guest_access; +//! Load saved data for the display names & avatars. +void +populateMembers(); +std::vector<std::string> +joinedRooms(); - if (info.member_count != 0) - j["member_count"] = info.member_count; +QMap<QString, RoomInfo> +roomInfo(bool withInvites = true); +std::map<QString, bool> +invites(); - if (info.tags.size() != 0) - j["tags"] = info.tags; -} +//! Calculate & return the name of the room. +QString +getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); +//! Get room join rules +mtx::events::state::JoinRule +getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb); +bool +getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb); +//! Retrieve the topic of the room if any. +QString +getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); +//! Retrieve the room avatar's url if any. +QString +getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb, const QString &room_id); +//! Retrieve the version of the room if any. +QString +getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb); -inline void -from_json(const json &j, RoomInfo &info) -{ - info.name = j.at("name"); - info.topic = j.at("topic"); - info.avatar_url = j.at("avatar_url"); - info.is_invite = j.at("is_invite"); - info.join_rule = j.at("join_rule"); - info.guest_access = j.at("guest_access"); +//! Retrieve member info from a room. +std::vector<RoomMember> +getMembers(const std::string &room_id, std::size_t startIndex = 0, std::size_t len = 30); - if (j.count("member_count")) - info.member_count = j.at("member_count"); +void +saveState(const mtx::responses::Sync &res); +bool +isInitialized(); - if (j.count("tags")) - info.tags = j.at("tags").get<std::vector<std::string>>(); -} +std::string +nextBatchToken(); -//! Basic information per member; -struct MemberInfo -{ - std::string name; - std::string avatar_url; -}; +void +deleteData(); -inline void -to_json(json &j, const MemberInfo &info) -{ - j["name"] = info.name; - j["avatar_url"] = info.avatar_url; -} +void +removeInvite(lmdb::txn &txn, const std::string &room_id); +void +removeInvite(const std::string &room_id); +void +removeRoom(lmdb::txn &txn, const std::string &roomid); +void +removeRoom(const std::string &roomid); +void +removeRoom(const QString &roomid); +void +setup(); -inline void -from_json(const json &j, MemberInfo &info) -{ - info.name = j.at("name"); - info.avatar_url = j.at("avatar_url"); -} +bool +isFormatValid(); +void +setCurrentFormat(); -struct RoomSearchResult -{ - std::string room_id; - RoomInfo info; - QImage img; -}; +std::map<QString, mtx::responses::Timeline> +roomMessages(); -Q_DECLARE_METATYPE(RoomSearchResult) -Q_DECLARE_METATYPE(RoomInfo) +QMap<QString, mtx::responses::Notifications> +getTimelineMentions(); -// Extra information associated with an outbound megolm session. -struct OutboundGroupSessionData -{ - std::string session_id; - std::string session_key; - uint64_t message_index = 0; -}; +//! Retrieve all the user ids from a room. +std::vector<std::string> +roomMembers(const std::string &room_id); -inline void -to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg) -{ - obj["session_id"] = msg.session_id; - obj["session_key"] = msg.session_key; - obj["message_index"] = msg.message_index; -} +//! Check if the given user has power leve greater than than +//! lowest power level of the given events. +bool +hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes, + const std::string &room_id, + const std::string &user_id); -inline void -from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg) -{ - msg.session_id = obj.at("session_id"); - msg.session_key = obj.at("session_key"); - msg.message_index = obj.at("message_index"); -} +//! Retrieves the saved room avatar. +QImage +getRoomAvatar(const QString &id); +QImage +getRoomAvatar(const std::string &id); -struct OutboundGroupSessionDataRef -{ - OlmOutboundGroupSession *session; - OutboundGroupSessionData data; -}; +//! Adds a user to the read list for the given event. +//! +//! There should be only one user id present in a receipt list per room. +//! The user id should be removed from any other lists. +using Receipts = std::map<std::string, std::map<std::string, uint64_t>>; +void +updateReadReceipt(lmdb::txn &txn, const std::string &room_id, const Receipts &receipts); -struct DevicePublicKeys -{ - std::string ed25519; - std::string curve25519; -}; +//! Retrieve all the read receipts for the given event id and room. +//! +//! Returns a map of user ids and the time of the read receipt in milliseconds. +using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>; +UserReceipts +readReceipts(const QString &event_id, const QString &room_id); -inline void -to_json(nlohmann::json &obj, const DevicePublicKeys &msg) -{ - obj["ed25519"] = msg.ed25519; - obj["curve25519"] = msg.curve25519; -} +//! Filter the events that have at least one read receipt. +std::vector<QString> +filterReadEvents(const QString &room_id, + const std::vector<QString> &event_ids, + const std::string &excluded_user); +//! Add event for which we are expecting some read receipts. +void +addPendingReceipt(const QString &room_id, const QString &event_id); +void +removePendingReceipt(lmdb::txn &txn, const std::string &room_id, const std::string &event_id); +void +notifyForReadReceipts(const std::string &room_id); +std::vector<QString> +pendingReceiptsEvents(lmdb::txn &txn, const std::string &room_id); -inline void -from_json(const nlohmann::json &obj, DevicePublicKeys &msg) +QByteArray +image(const QString &url); +QByteArray +image(lmdb::txn &txn, const std::string &url); +inline QByteArray +image(const std::string &url) { - msg.ed25519 = obj.at("ed25519"); - msg.curve25519 = obj.at("curve25519"); + return image(QString::fromStdString(url)); } +void +saveImage(const std::string &url, const std::string &data); +void +saveImage(const QString &url, const QByteArray &data); -//! Represents a unique megolm session identifier. -struct MegolmSessionIndex -{ - //! The room in which this session exists. - std::string room_id; - //! The session_id of the megolm session. - std::string session_id; - //! The curve25519 public key of the sender. - std::string sender_key; -}; - -inline void -to_json(nlohmann::json &obj, const MegolmSessionIndex &msg) +RoomInfo +singleRoomInfo(const std::string &room_id); +std::vector<std::string> +roomsWithStateUpdates(const mtx::responses::Sync &res); +std::vector<std::string> +roomsWithTagUpdates(const mtx::responses::Sync &res); +std::map<QString, RoomInfo> +getRoomInfo(const std::vector<std::string> &rooms); +inline std::map<QString, RoomInfo> +roomUpdates(const mtx::responses::Sync &sync) { - obj["room_id"] = msg.room_id; - obj["session_id"] = msg.session_id; - obj["sender_key"] = msg.sender_key; + return getRoomInfo(roomsWithStateUpdates(sync)); } - -inline void -from_json(const nlohmann::json &obj, MegolmSessionIndex &msg) +inline std::map<QString, RoomInfo> +roomTagUpdates(const mtx::responses::Sync &sync) { - msg.room_id = obj.at("room_id"); - msg.session_id = obj.at("session_id"); - msg.sender_key = obj.at("sender_key"); + return getRoomInfo(roomsWithTagUpdates(sync)); } -struct OlmSessionStorage -{ - // Megolm sessions - std::map<std::string, mtx::crypto::InboundGroupSessionPtr> group_inbound_sessions; - std::map<std::string, mtx::crypto::OutboundGroupSessionPtr> group_outbound_sessions; - std::map<std::string, OutboundGroupSessionData> group_outbound_session_data; - - // Guards for accessing megolm sessions. - std::mutex group_outbound_mtx; - std::mutex group_inbound_mtx; -}; - -class Cache : public QObject -{ - Q_OBJECT - -public: - Cache(const QString &userId, QObject *parent = nullptr); - - static QHash<QString, QString> DisplayNames; - static QHash<QString, QString> AvatarUrls; - static QHash<QString, QString> UserColors; - - static std::string displayName(const std::string &room_id, const std::string &user_id); - static QString displayName(const QString &room_id, const QString &user_id); - static QString avatarUrl(const QString &room_id, const QString &user_id); - static QString userColor(const QString &user_id); - - static void removeDisplayName(const QString &room_id, const QString &user_id); - static void removeAvatarUrl(const QString &room_id, const QString &user_id); - static void removeUserColor(const QString &user_id); - - static void insertDisplayName(const QString &room_id, - const QString &user_id, - const QString &display_name); - static void insertAvatarUrl(const QString &room_id, - const QString &user_id, - const QString &avatar_url); - static void insertUserColor(const QString &user_id, const QString &color_name); - - static void clearUserColors(); - - //! Load saved data for the display names & avatars. - void populateMembers(); - std::vector<std::string> joinedRooms(); - - QMap<QString, RoomInfo> roomInfo(bool withInvites = true); - std::map<QString, bool> invites(); - - //! Calculate & return the name of the room. - QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); - //! Get room join rules - JoinRule getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb); - bool getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb); - //! Retrieve the topic of the room if any. - QString getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); - //! Retrieve the room avatar's url if any. - QString getRoomAvatarUrl(lmdb::txn &txn, - lmdb::dbi &statesdb, - lmdb::dbi &membersdb, - const QString &room_id); - - //! Retrieve member info from a room. - std::vector<RoomMember> getMembers(const std::string &room_id, - std::size_t startIndex = 0, - std::size_t len = 30); - - void saveState(const mtx::responses::Sync &res); - bool isInitialized() const; - - std::string nextBatchToken() const; - - void deleteData(); - - void removeInvite(lmdb::txn &txn, const std::string &room_id); - void removeInvite(const std::string &room_id); - void removeRoom(lmdb::txn &txn, const std::string &roomid); - void removeRoom(const std::string &roomid); - void removeRoom(const QString &roomid) { removeRoom(roomid.toStdString()); }; - void setup(); - - bool isFormatValid(); - void setCurrentFormat(); - - std::map<QString, mtx::responses::Timeline> roomMessages(); - - //! Retrieve all the user ids from a room. - std::vector<std::string> roomMembers(const std::string &room_id); - - //! Check if the given user has power leve greater than than - //! lowest power level of the given events. - bool hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes, - const std::string &room_id, - const std::string &user_id); - - //! Retrieves the saved room avatar. - QImage getRoomAvatar(const QString &id); - QImage getRoomAvatar(const std::string &id); - - //! Adds a user to the read list for the given event. - //! - //! There should be only one user id present in a receipt list per room. - //! The user id should be removed from any other lists. - using Receipts = std::map<std::string, std::map<std::string, uint64_t>>; - void updateReadReceipt(lmdb::txn &txn, - const std::string &room_id, - const Receipts &receipts); - - //! Retrieve all the read receipts for the given event id and room. - //! - //! Returns a map of user ids and the time of the read receipt in milliseconds. - using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>; - UserReceipts readReceipts(const QString &event_id, const QString &room_id); - - //! Filter the events that have at least one read receipt. - std::vector<QString> filterReadEvents(const QString &room_id, - const std::vector<QString> &event_ids, - const std::string &excluded_user); - //! Add event for which we are expecting some read receipts. - void addPendingReceipt(const QString &room_id, const QString &event_id); - void removePendingReceipt(lmdb::txn &txn, - const std::string &room_id, - const std::string &event_id); - void notifyForReadReceipts(const std::string &room_id); - std::vector<QString> pendingReceiptsEvents(lmdb::txn &txn, const std::string &room_id); - - QByteArray image(const QString &url) const; - QByteArray image(lmdb::txn &txn, const std::string &url) const; - QByteArray image(const std::string &url) const - { - return image(QString::fromStdString(url)); - } - void saveImage(const std::string &url, const std::string &data); - void saveImage(const QString &url, const QByteArray &data); - - RoomInfo singleRoomInfo(const std::string &room_id); - std::vector<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res); - std::vector<std::string> roomsWithTagUpdates(const mtx::responses::Sync &res); - std::map<QString, RoomInfo> getRoomInfo(const std::vector<std::string> &rooms); - std::map<QString, RoomInfo> roomUpdates(const mtx::responses::Sync &sync) - { - return getRoomInfo(roomsWithStateUpdates(sync)); - } - std::map<QString, RoomInfo> roomTagUpdates(const mtx::responses::Sync &sync) - { - return getRoomInfo(roomsWithTagUpdates(sync)); - } - - //! Calculates which the read status of a room. - //! Whether all the events in the timeline have been read. - bool calculateRoomReadStatus(const std::string &room_id); - void calculateRoomReadStatus(); - - QVector<SearchResult> searchUsers(const std::string &room_id, - const std::string &query, - std::uint8_t max_items = 5); - std::vector<RoomSearchResult> searchRooms(const std::string &query, - std::uint8_t max_items = 5); - - void markSentNotification(const std::string &event_id); - //! Removes an event from the sent notifications. - void removeReadNotification(const std::string &event_id); - //! Check if we have sent a desktop notification for the given event id. - bool isNotificationSent(const std::string &event_id); - - //! Remove old unused data. - void deleteOldMessages(); - void deleteOldData() noexcept; - //! Retrieve all saved room ids. - std::vector<std::string> getRoomIds(lmdb::txn &txn); - - //! Mark a room that uses e2e encryption. - void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id); - bool isRoomEncrypted(const std::string &room_id); - - //! Save the public keys for a device. - void saveDeviceKeys(const std::string &device_id); - void getDeviceKeys(const std::string &device_id); - - //! Save the device list for a user. - void setDeviceList(const std::string &user_id, const std::vector<std::string> &devices); - std::vector<std::string> getDeviceList(const std::string &user_id); - - //! Check if a user is a member of the room. - bool isRoomMember(const std::string &user_id, const std::string &room_id); - - // - // Outbound Megolm Sessions - // - void saveOutboundMegolmSession(const std::string &room_id, - const OutboundGroupSessionData &data, - mtx::crypto::OutboundGroupSessionPtr session); - OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); - bool outboundMegolmSessionExists(const std::string &room_id) noexcept; - void updateOutboundMegolmSession(const std::string &room_id, int message_index); - - void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys); - mtx::crypto::ExportedSessionKeys exportSessionKeys(); - - // - // Inbound Megolm Sessions - // - void saveInboundMegolmSession(const MegolmSessionIndex &index, - mtx::crypto::InboundGroupSessionPtr session); - OlmInboundGroupSession *getInboundMegolmSession(const MegolmSessionIndex &index); - bool inboundMegolmSessionExists(const MegolmSessionIndex &index); - - // - // Olm Sessions - // - void saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session); - std::vector<std::string> getOlmSessions(const std::string &curve25519); - boost::optional<mtx::crypto::OlmSessionPtr> getOlmSession(const std::string &curve25519, - const std::string &session_id); - - void saveOlmAccount(const std::string &pickled); - std::string restoreOlmAccount(); - - void restoreSessions(); - - OlmSessionStorage session_storage; - -signals: - void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); - void roomReadStatus(const std::map<QString, bool> &status); - -private: - //! Save an invited room. - void saveInvite(lmdb::txn &txn, - lmdb::dbi &statesdb, - lmdb::dbi &membersdb, - const mtx::responses::InvitedRoom &room); - - QString getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); - QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); - QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); - - DescInfo getLastMessageInfo(lmdb::txn &txn, const std::string &room_id); - void saveTimelineMessages(lmdb::txn &txn, - const std::string &room_id, - const mtx::responses::Timeline &res); - - mtx::responses::Timeline getTimelineMessages(lmdb::txn &txn, const std::string &room_id); - - //! Remove a room from the cache. - // void removeLeftRoom(lmdb::txn &txn, const std::string &room_id); - template<class T> - void saveStateEvents(lmdb::txn &txn, - const lmdb::dbi &statesdb, - const lmdb::dbi &membersdb, - const std::string &room_id, - const std::vector<T> &events) - { - for (const auto &e : events) - saveStateEvent(txn, statesdb, membersdb, room_id, e); - } - - template<class T> - void saveStateEvent(lmdb::txn &txn, - const lmdb::dbi &statesdb, - const lmdb::dbi &membersdb, - const std::string &room_id, - const T &event) - { - using namespace mtx::events; - using namespace mtx::events::state; - - if (boost::get<StateEvent<Member>>(&event) != nullptr) { - const auto e = boost::get<StateEvent<Member>>(event); - - switch (e.content.membership) { - // - // We only keep users with invite or join membership. - // - case Membership::Invite: - case Membership::Join: { - auto display_name = e.content.display_name.empty() - ? e.state_key - : e.content.display_name; - - // Lightweight representation of a member. - MemberInfo tmp{display_name, e.content.avatar_url}; - - lmdb::dbi_put(txn, - membersdb, - lmdb::val(e.state_key), - lmdb::val(json(tmp).dump())); - - insertDisplayName(QString::fromStdString(room_id), - QString::fromStdString(e.state_key), - QString::fromStdString(display_name)); - - insertAvatarUrl(QString::fromStdString(room_id), - QString::fromStdString(e.state_key), - QString::fromStdString(e.content.avatar_url)); - - break; - } - default: { - lmdb::dbi_del( - txn, membersdb, lmdb::val(e.state_key), lmdb::val("")); - - removeDisplayName(QString::fromStdString(room_id), - QString::fromStdString(e.state_key)); - removeAvatarUrl(QString::fromStdString(room_id), - QString::fromStdString(e.state_key)); - - break; - } - } - - return; - } else if (boost::get<StateEvent<Encryption>>(&event) != nullptr) { - setEncryptedRoom(txn, room_id); - return; - } - - if (!isStateEvent(event)) - return; - - boost::apply_visitor( - [&txn, &statesdb](auto e) { - lmdb::dbi_put( - txn, statesdb, lmdb::val(to_string(e.type)), lmdb::val(json(e).dump())); - }, - event); - } - - template<class T> - bool isStateEvent(const T &e) - { - using namespace mtx::events; - using namespace mtx::events::state; - - return boost::get<StateEvent<Aliases>>(&e) != nullptr || - boost::get<StateEvent<state::Avatar>>(&e) != nullptr || - boost::get<StateEvent<CanonicalAlias>>(&e) != nullptr || - boost::get<StateEvent<Create>>(&e) != nullptr || - boost::get<StateEvent<GuestAccess>>(&e) != nullptr || - boost::get<StateEvent<HistoryVisibility>>(&e) != nullptr || - boost::get<StateEvent<JoinRules>>(&e) != nullptr || - boost::get<StateEvent<Name>>(&e) != nullptr || - boost::get<StateEvent<Member>>(&e) != nullptr || - boost::get<StateEvent<PowerLevels>>(&e) != nullptr || - boost::get<StateEvent<Topic>>(&e) != nullptr; - } - - template<class T> - bool containsStateUpdates(const T &e) - { - using namespace mtx::events; - using namespace mtx::events::state; - - return boost::get<StateEvent<state::Avatar>>(&e) != nullptr || - boost::get<StateEvent<CanonicalAlias>>(&e) != nullptr || - boost::get<StateEvent<Name>>(&e) != nullptr || - boost::get<StateEvent<Member>>(&e) != nullptr || - boost::get<StateEvent<Topic>>(&e) != nullptr; - } - - bool containsStateUpdates(const mtx::events::collections::StrippedEvents &e) - { - using namespace mtx::events; - using namespace mtx::events::state; - - return boost::get<StrippedEvent<state::Avatar>>(&e) != nullptr || - boost::get<StrippedEvent<CanonicalAlias>>(&e) != nullptr || - boost::get<StrippedEvent<Name>>(&e) != nullptr || - boost::get<StrippedEvent<Member>>(&e) != nullptr || - boost::get<StrippedEvent<Topic>>(&e) != nullptr; - } - - void saveInvites(lmdb::txn &txn, - const std::map<std::string, mtx::responses::InvitedRoom> &rooms); - - //! Sends signals for the rooms that are removed. - void removeLeftRooms(lmdb::txn &txn, - const std::map<std::string, mtx::responses::LeftRoom> &rooms) - { - for (const auto &room : rooms) { - removeRoom(txn, room.first); - - // Clean up leftover invites. - removeInvite(txn, room.first); - } - } - - lmdb::dbi getPendingReceiptsDb(lmdb::txn &txn) - { - return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE); - } - - lmdb::dbi getMessagesDb(lmdb::txn &txn, const std::string &room_id) - { - auto db = - lmdb::dbi::open(txn, std::string(room_id + "/messages").c_str(), MDB_CREATE); - lmdb::dbi_set_compare(txn, db, numeric_key_comparison); - - return db; - } - - lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open( - txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE); - } - - lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open( - txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE); - } +//! Calculates which the read status of a room. +//! Whether all the events in the timeline have been read. +bool +calculateRoomReadStatus(const std::string &room_id); +void +calculateRoomReadStatus(); - lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE); - } +std::vector<SearchResult> +searchUsers(const std::string &room_id, const std::string &query, std::uint8_t max_items = 5); +std::vector<RoomSearchResult> +searchRooms(const std::string &query, std::uint8_t max_items = 5); - lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id) - { - return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE); - } +void +markSentNotification(const std::string &event_id); +//! Removes an event from the sent notifications. +void +removeReadNotification(const std::string &event_id); +//! Check if we have sent a desktop notification for the given event id. +bool +isNotificationSent(const std::string &event_id); - //! Retrieves or creates the database that stores the open OLM sessions between our device - //! and the given curve25519 key which represents another device. - //! - //! Each entry is a map from the session_id to the pickled representation of the session. - lmdb::dbi getOlmSessionsDb(lmdb::txn &txn, const std::string &curve25519_key) - { - return lmdb::dbi::open( - txn, std::string("olm_sessions/" + curve25519_key).c_str(), MDB_CREATE); - } +//! Add all notifications containing a user mention to the db. +void +saveTimelineMentions(const mtx::responses::Notifications &res); - QString getDisplayName(const mtx::events::StateEvent<mtx::events::state::Member> &event) - { - if (!event.content.display_name.empty()) - return QString::fromStdString(event.content.display_name); +//! Remove old unused data. +void +deleteOldMessages(); +void +deleteOldData() noexcept; +//! Retrieve all saved room ids. +std::vector<std::string> +getRoomIds(lmdb::txn &txn); - return QString::fromStdString(event.state_key); - } +//! Mark a room that uses e2e encryption. +void +setEncryptedRoom(lmdb::txn &txn, const std::string &room_id); +bool +isRoomEncrypted(const std::string &room_id); - void setNextBatchToken(lmdb::txn &txn, const std::string &token); - void setNextBatchToken(lmdb::txn &txn, const QString &token); +//! Check if a user is a member of the room. +bool +isRoomMember(const std::string &user_id, const std::string &room_id); - lmdb::env env_; - lmdb::dbi syncStateDb_; - lmdb::dbi roomsDb_; - lmdb::dbi invitesDb_; - lmdb::dbi mediaDb_; - lmdb::dbi readReceiptsDb_; - lmdb::dbi notificationsDb_; +// +// Outbound Megolm Sessions +// +void +saveOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data, + mtx::crypto::OutboundGroupSessionPtr session); +OutboundGroupSessionDataRef +getOutboundMegolmSession(const std::string &room_id); +bool +outboundMegolmSessionExists(const std::string &room_id) noexcept; +void +updateOutboundMegolmSession(const std::string &room_id, int message_index); - lmdb::dbi devicesDb_; - lmdb::dbi deviceKeysDb_; +void +importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys); +mtx::crypto::ExportedSessionKeys +exportSessionKeys(); - lmdb::dbi inboundMegolmSessionDb_; - lmdb::dbi outboundMegolmSessionDb_; +// +// Inbound Megolm Sessions +// +void +saveInboundMegolmSession(const MegolmSessionIndex &index, + mtx::crypto::InboundGroupSessionPtr session); +OlmInboundGroupSession * +getInboundMegolmSession(const MegolmSessionIndex &index); +bool +inboundMegolmSessionExists(const MegolmSessionIndex &index); - QString localUserId_; - QString cacheDirectory_; -}; +// +// Olm Sessions +// +void +saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session); +std::vector<std::string> +getOlmSessions(const std::string &curve25519); +std::optional<mtx::crypto::OlmSessionPtr> +getOlmSession(const std::string &curve25519, const std::string &session_id); -namespace cache { void -init(const QString &user_id); +saveOlmAccount(const std::string &pickled); +std::string +restoreOlmAccount(); -Cache * -client(); +void +restoreSessions(); } diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h new file mode 100644
index 00000000..14c9c86b --- /dev/null +++ b/src/CacheCryptoStructs.h
@@ -0,0 +1,67 @@ +#pragma once + +#include <map> +#include <mutex> + +//#include <nlohmann/json.hpp> + +#include <mtx/responses.hpp> +#include <mtxclient/crypto/client.hpp> + +// Extra information associated with an outbound megolm session. +struct OutboundGroupSessionData +{ + std::string session_id; + std::string session_key; + uint64_t message_index = 0; +}; + +void +to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg); +void +from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg); + +struct OutboundGroupSessionDataRef +{ + OlmOutboundGroupSession *session; + OutboundGroupSessionData data; +}; + +struct DevicePublicKeys +{ + std::string ed25519; + std::string curve25519; +}; + +void +to_json(nlohmann::json &obj, const DevicePublicKeys &msg); +void +from_json(const nlohmann::json &obj, DevicePublicKeys &msg); + +//! Represents a unique megolm session identifier. +struct MegolmSessionIndex +{ + //! The room in which this session exists. + std::string room_id; + //! The session_id of the megolm session. + std::string session_id; + //! The curve25519 public key of the sender. + std::string sender_key; +}; + +void +to_json(nlohmann::json &obj, const MegolmSessionIndex &msg); +void +from_json(const nlohmann::json &obj, MegolmSessionIndex &msg); + +struct OlmSessionStorage +{ + // Megolm sessions + std::map<std::string, mtx::crypto::InboundGroupSessionPtr> group_inbound_sessions; + std::map<std::string, mtx::crypto::OutboundGroupSessionPtr> group_outbound_sessions; + std::map<std::string, OutboundGroupSessionData> group_outbound_session_data; + + // Guards for accessing megolm sessions. + std::mutex group_outbound_mtx; + std::mutex group_inbound_mtx; +}; diff --git a/src/CacheStructs.h b/src/CacheStructs.h new file mode 100644
index 00000000..2051afc8 --- /dev/null +++ b/src/CacheStructs.h
@@ -0,0 +1,91 @@ +#pragma once + +#include <QDateTime> +#include <QImage> +#include <QString> + +#include <string> + +#include <mtx/events/join_rules.hpp> + +struct RoomMember +{ + QString user_id; + QString display_name; + QImage avatar; +}; + +struct SearchResult +{ + QString user_id; + QString display_name; +}; + +//! Used to uniquely identify a list of read receipts. +struct ReadReceiptKey +{ + std::string event_id; + std::string room_id; +}; + +void +to_json(nlohmann::json &j, const ReadReceiptKey &key); + +void +from_json(const nlohmann::json &j, ReadReceiptKey &key); + +struct DescInfo +{ + QString event_id; + QString userid; + QString body; + QString timestamp; + QDateTime datetime; +}; + +//! UI info associated with a room. +struct RoomInfo +{ + //! The calculated name of the room. + std::string name; + //! The topic of the room. + std::string topic; + //! The calculated avatar url of the room. + std::string avatar_url; + //! The calculated version of this room set at creation time. + std::string version; + //! Whether or not the room is an invite. + bool is_invite = false; + //! Total number of members in the room. + int16_t member_count = 0; + //! Who can access to the room. + mtx::events::state::JoinRule join_rule = mtx::events::state::JoinRule::Public; + bool guest_access = false; + //! Metadata describing the last message in the timeline. + DescInfo msgInfo; + //! The list of tags associated with this room + std::vector<std::string> tags; +}; + +void +to_json(nlohmann::json &j, const RoomInfo &info); +void +from_json(const nlohmann::json &j, RoomInfo &info); + +//! Basic information per member; +struct MemberInfo +{ + std::string name; + std::string avatar_url; +}; + +void +to_json(nlohmann::json &j, const MemberInfo &info); +void +from_json(const nlohmann::json &j, MemberInfo &info); + +struct RoomSearchResult +{ + std::string room_id; + RoomInfo info; +}; diff --git a/src/Cache_p.h b/src/Cache_p.h new file mode 100644
index 00000000..14ceafe8 --- /dev/null +++ b/src/Cache_p.h
@@ -0,0 +1,490 @@ +/* + * nheko Copyright (C) 2019 The nheko authors + * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <optional> + +#include <QDateTime> +#include <QDir> +#include <QImage> +#include <QString> + +#if __has_include(<lmdbxx/lmdb++.h>) +#include <lmdbxx/lmdb++.h> +#else +#include <lmdb++.h> +#endif +#include <nlohmann/json.hpp> + +#include <mtx/responses.hpp> +#include <mtxclient/crypto/client.hpp> + +#include "CacheCryptoStructs.h" +#include "CacheStructs.h" + +int +numeric_key_comparison(const MDB_val *a, const MDB_val *b); + +class Cache : public QObject +{ + Q_OBJECT + +public: + Cache(const QString &userId, QObject *parent = nullptr); + + static std::string displayName(const std::string &room_id, const std::string &user_id); + static QString displayName(const QString &room_id, const QString &user_id); + static QString avatarUrl(const QString &room_id, const QString &user_id); + + static void removeDisplayName(const QString &room_id, const QString &user_id); + static void removeAvatarUrl(const QString &room_id, const QString &user_id); + + static void insertDisplayName(const QString &room_id, + const QString &user_id, + const QString &display_name); + static void insertAvatarUrl(const QString &room_id, + const QString &user_id, + const QString &avatar_url); + + //! Load saved data for the display names & avatars. + void populateMembers(); + std::vector<std::string> joinedRooms(); + + QMap<QString, RoomInfo> roomInfo(bool withInvites = true); + std::map<QString, bool> invites(); + + //! Calculate & return the name of the room. + QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + //! Get room join rules + mtx::events::state::JoinRule getRoomJoinRule(lmdb::txn &txn, lmdb::dbi &statesdb); + bool getRoomGuestAccess(lmdb::txn &txn, lmdb::dbi &statesdb); + //! Retrieve the topic of the room if any. + QString getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); + //! Retrieve the room avatar's url if any. + QString getRoomAvatarUrl(lmdb::txn &txn, + lmdb::dbi &statesdb, + lmdb::dbi &membersdb, + const QString &room_id); + //! Retrieve the version of the room if any. + QString getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb); + + //! Retrieve member info from a room. + std::vector<RoomMember> getMembers(const std::string &room_id, + std::size_t startIndex = 0, + std::size_t len = 30); + + void saveState(const mtx::responses::Sync &res); + bool isInitialized() const; + + std::string nextBatchToken() const; + + void deleteData(); + + void removeInvite(lmdb::txn &txn, const std::string &room_id); + void removeInvite(const std::string &room_id); + void removeRoom(lmdb::txn &txn, const std::string &roomid); + void removeRoom(const std::string &roomid); + void setup(); + + bool isFormatValid(); + void setCurrentFormat(); + + std::map<QString, mtx::responses::Timeline> roomMessages(); + + QMap<QString, mtx::responses::Notifications> getTimelineMentions(); + + //! Retrieve all the user ids from a room. + std::vector<std::string> roomMembers(const std::string &room_id); + + //! Check if the given user has power leve greater than than + //! lowest power level of the given events. + bool hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes, + const std::string &room_id, + const std::string &user_id); + + //! Retrieves the saved room avatar. + QImage getRoomAvatar(const QString &id); + QImage getRoomAvatar(const std::string &id); + + //! Adds a user to the read list for the given event. + //! + //! There should be only one user id present in a receipt list per room. + //! The user id should be removed from any other lists. + using Receipts = std::map<std::string, std::map<std::string, uint64_t>>; + void updateReadReceipt(lmdb::txn &txn, + const std::string &room_id, + const Receipts &receipts); + + //! Retrieve all the read receipts for the given event id and room. + //! + //! Returns a map of user ids and the time of the read receipt in milliseconds. + using UserReceipts = std::multimap<uint64_t, std::string, std::greater<uint64_t>>; + UserReceipts readReceipts(const QString &event_id, const QString &room_id); + + //! Filter the events that have at least one read receipt. + std::vector<QString> filterReadEvents(const QString &room_id, + const std::vector<QString> &event_ids, + const std::string &excluded_user); + //! Add event for which we are expecting some read receipts. + void addPendingReceipt(const QString &room_id, const QString &event_id); + void removePendingReceipt(lmdb::txn &txn, + const std::string &room_id, + const std::string &event_id); + void notifyForReadReceipts(const std::string &room_id); + std::vector<QString> pendingReceiptsEvents(lmdb::txn &txn, const std::string &room_id); + + QByteArray image(const QString &url) const; + QByteArray image(lmdb::txn &txn, const std::string &url) const; + void saveImage(const std::string &url, const std::string &data); + void saveImage(const QString &url, const QByteArray &data); + + RoomInfo singleRoomInfo(const std::string &room_id); + std::vector<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res); + std::vector<std::string> roomsWithTagUpdates(const mtx::responses::Sync &res); + std::map<QString, RoomInfo> getRoomInfo(const std::vector<std::string> &rooms); + + //! Calculates which the read status of a room. + //! Whether all the events in the timeline have been read. + bool calculateRoomReadStatus(const std::string &room_id); + void calculateRoomReadStatus(); + + std::vector<SearchResult> searchUsers(const std::string &room_id, + const std::string &query, + std::uint8_t max_items = 5); + std::vector<RoomSearchResult> searchRooms(const std::string &query, + std::uint8_t max_items = 5); + + void markSentNotification(const std::string &event_id); + //! Removes an event from the sent notifications. + void removeReadNotification(const std::string &event_id); + //! Check if we have sent a desktop notification for the given event id. + bool isNotificationSent(const std::string &event_id); + + //! Add all notifications containing a user mention to the db. + void saveTimelineMentions(const mtx::responses::Notifications &res); + + //! Remove old unused data. + void deleteOldMessages(); + void deleteOldData() noexcept; + //! Retrieve all saved room ids. + std::vector<std::string> getRoomIds(lmdb::txn &txn); + + //! Mark a room that uses e2e encryption. + void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id); + bool isRoomEncrypted(const std::string &room_id); + + //! Check if a user is a member of the room. + bool isRoomMember(const std::string &user_id, const std::string &room_id); + + // + // Outbound Megolm Sessions + // + void saveOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data, + mtx::crypto::OutboundGroupSessionPtr session); + OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); + bool outboundMegolmSessionExists(const std::string &room_id) noexcept; + void updateOutboundMegolmSession(const std::string &room_id, int message_index); + + void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys); + mtx::crypto::ExportedSessionKeys exportSessionKeys(); + + // + // Inbound Megolm Sessions + // + void saveInboundMegolmSession(const MegolmSessionIndex &index, + mtx::crypto::InboundGroupSessionPtr session); + OlmInboundGroupSession *getInboundMegolmSession(const MegolmSessionIndex &index); + bool inboundMegolmSessionExists(const MegolmSessionIndex &index); + + // + // Olm Sessions + // + void saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session); + std::vector<std::string> getOlmSessions(const std::string &curve25519); + std::optional<mtx::crypto::OlmSessionPtr> getOlmSession(const std::string &curve25519, + const std::string &session_id); + + void saveOlmAccount(const std::string &pickled); + std::string restoreOlmAccount(); + + void restoreSessions(); + +signals: + void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); + void roomReadStatus(const std::map<QString, bool> &status); + +private: + //! Save an invited room. + void saveInvite(lmdb::txn &txn, + lmdb::dbi &statesdb, + lmdb::dbi &membersdb, + const mtx::responses::InvitedRoom &room); + + //! Add a notification containing a user mention to the db. + void saveTimelineMentions(lmdb::txn &txn, + const std::string &room_id, + const QList<mtx::responses::Notification> &res); + + //! Get timeline items that a user was mentions in for a given room + mtx::responses::Notifications getTimelineMentionsForRoom(lmdb::txn &txn, + const std::string &room_id); + + QString getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb); + QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); + + std::string getLastEventId(lmdb::txn &txn, const std::string &room_id); + DescInfo getLastMessageInfo(lmdb::txn &txn, const std::string &room_id); + void saveTimelineMessages(lmdb::txn &txn, + const std::string &room_id, + const mtx::responses::Timeline &res); + + mtx::responses::Timeline getTimelineMessages(lmdb::txn &txn, const std::string &room_id); + + //! Remove a room from the cache. + // void removeLeftRoom(lmdb::txn &txn, const std::string &room_id); + template<class T> + void saveStateEvents(lmdb::txn &txn, + const lmdb::dbi &statesdb, + const lmdb::dbi &membersdb, + const std::string &room_id, + const std::vector<T> &events) + { + for (const auto &e : events) + saveStateEvent(txn, statesdb, membersdb, room_id, e); + } + + template<class T> + void saveStateEvent(lmdb::txn &txn, + const lmdb::dbi &statesdb, + const lmdb::dbi &membersdb, + const std::string &room_id, + const T &event) + { + using namespace mtx::events; + using namespace mtx::events::state; + + if (auto e = std::get_if<StateEvent<Member>>(&event); e != nullptr) { + switch (e->content.membership) { + // + // We only keep users with invite or join membership. + // + case Membership::Invite: + case Membership::Join: { + auto display_name = e->content.display_name.empty() + ? e->state_key + : e->content.display_name; + + // Lightweight representation of a member. + MemberInfo tmp{display_name, e->content.avatar_url}; + + lmdb::dbi_put(txn, + membersdb, + lmdb::val(e->state_key), + lmdb::val(json(tmp).dump())); + + insertDisplayName(QString::fromStdString(room_id), + QString::fromStdString(e->state_key), + QString::fromStdString(display_name)); + + insertAvatarUrl(QString::fromStdString(room_id), + QString::fromStdString(e->state_key), + QString::fromStdString(e->content.avatar_url)); + + break; + } + default: { + lmdb::dbi_del( + txn, membersdb, lmdb::val(e->state_key), lmdb::val("")); + + removeDisplayName(QString::fromStdString(room_id), + QString::fromStdString(e->state_key)); + removeAvatarUrl(QString::fromStdString(room_id), + QString::fromStdString(e->state_key)); + + break; + } + } + + return; + } else if (std::holds_alternative<StateEvent<Encryption>>(event)) { + setEncryptedRoom(txn, room_id); + return; + } + + if (!isStateEvent(event)) + return; + + std::visit( + [&txn, &statesdb](auto e) { + lmdb::dbi_put( + txn, statesdb, lmdb::val(to_string(e.type)), lmdb::val(json(e).dump())); + }, + event); + } + + template<class T> + bool isStateEvent(const T &e) + { + using namespace mtx::events; + using namespace mtx::events::state; + + return std::holds_alternative<StateEvent<Aliases>>(e) || + std::holds_alternative<StateEvent<state::Avatar>>(e) || + std::holds_alternative<StateEvent<CanonicalAlias>>(e) || + std::holds_alternative<StateEvent<Create>>(e) || + std::holds_alternative<StateEvent<GuestAccess>>(e) || + std::holds_alternative<StateEvent<HistoryVisibility>>(e) || + std::holds_alternative<StateEvent<JoinRules>>(e) || + std::holds_alternative<StateEvent<Name>>(e) || + std::holds_alternative<StateEvent<Member>>(e) || + std::holds_alternative<StateEvent<PowerLevels>>(e) || + std::holds_alternative<StateEvent<Topic>>(e); + } + + template<class T> + bool containsStateUpdates(const T &e) + { + using namespace mtx::events; + using namespace mtx::events::state; + + return std::holds_alternative<StateEvent<state::Avatar>>(e) || + std::holds_alternative<StateEvent<CanonicalAlias>>(e) || + std::holds_alternative<StateEvent<Name>>(e) || + std::holds_alternative<StateEvent<Member>>(e) || + std::holds_alternative<StateEvent<Topic>>(e); + } + + bool containsStateUpdates(const mtx::events::collections::StrippedEvents &e) + { + using namespace mtx::events; + using namespace mtx::events::state; + + return std::holds_alternative<StrippedEvent<state::Avatar>>(e) || + std::holds_alternative<StrippedEvent<CanonicalAlias>>(e) || + std::holds_alternative<StrippedEvent<Name>>(e) || + std::holds_alternative<StrippedEvent<Member>>(e) || + std::holds_alternative<StrippedEvent<Topic>>(e); + } + + void saveInvites(lmdb::txn &txn, + const std::map<std::string, mtx::responses::InvitedRoom> &rooms); + + //! Sends signals for the rooms that are removed. + void removeLeftRooms(lmdb::txn &txn, + const std::map<std::string, mtx::responses::LeftRoom> &rooms) + { + for (const auto &room : rooms) { + removeRoom(txn, room.first); + + // Clean up leftover invites. + removeInvite(txn, room.first); + } + } + + lmdb::dbi getPendingReceiptsDb(lmdb::txn &txn) + { + return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE); + } + + lmdb::dbi getMessagesDb(lmdb::txn &txn, const std::string &room_id) + { + auto db = + lmdb::dbi::open(txn, std::string(room_id + "/messages").c_str(), MDB_CREATE); + lmdb::dbi_set_compare(txn, db, numeric_key_comparison); + + return db; + } + + lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open( + txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE); + } + + lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open( + txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE); + } + + lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE); + } + + lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE); + } + + lmdb::dbi getMentionsDb(lmdb::txn &txn, const std::string &room_id) + { + return lmdb::dbi::open(txn, std::string(room_id + "/mentions").c_str(), MDB_CREATE); + } + + //! Retrieves or creates the database that stores the open OLM sessions between our device + //! and the given curve25519 key which represents another device. + //! + //! Each entry is a map from the session_id to the pickled representation of the session. + lmdb::dbi getOlmSessionsDb(lmdb::txn &txn, const std::string &curve25519_key) + { + return lmdb::dbi::open( + txn, std::string("olm_sessions/" + curve25519_key).c_str(), MDB_CREATE); + } + + QString getDisplayName(const mtx::events::StateEvent<mtx::events::state::Member> &event) + { + if (!event.content.display_name.empty()) + return QString::fromStdString(event.content.display_name); + + return QString::fromStdString(event.state_key); + } + + void setNextBatchToken(lmdb::txn &txn, const std::string &token); + void setNextBatchToken(lmdb::txn &txn, const QString &token); + + lmdb::env env_; + lmdb::dbi syncStateDb_; + lmdb::dbi roomsDb_; + lmdb::dbi invitesDb_; + lmdb::dbi mediaDb_; + lmdb::dbi readReceiptsDb_; + lmdb::dbi notificationsDb_; + + lmdb::dbi devicesDb_; + lmdb::dbi deviceKeysDb_; + + lmdb::dbi inboundMegolmSessionDb_; + lmdb::dbi outboundMegolmSessionDb_; + + QString localUserId_; + QString cacheDirectory_; + + static QHash<QString, QString> DisplayNames; + static QHash<QString, QString> AvatarUrls; + + OlmSessionStorage session_storage; +}; + +namespace cache { +Cache * +client(); +} diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index dd23fb80..89bfd55a 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp
@@ -18,10 +18,12 @@ #include <QApplication> #include <QImageReader> #include <QSettings> +#include <QShortcut> #include <QtConcurrent> #include "AvatarProvider.h" #include "Cache.h" +#include "Cache_p.h" #include "ChatPage.h" #include "Logging.h" #include "MainWindow.h" @@ -33,7 +35,6 @@ #include "Splitter.h" #include "TextInputWidget.h" #include "TopRoomBar.h" -#include "TypingDisplay.h" #include "UserInfoWidget.h" #include "UserSettingsPage.h" #include "Utils.h" @@ -43,6 +44,7 @@ #include "notifications/Manager.h" #include "dialogs/ReadReceipts.h" +#include "popups/UserMentions.h" #include "timeline/TimelineViewManager.h" // TODO: Needs to be updated with an actual secret. @@ -53,6 +55,9 @@ constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000; constexpr int RETRY_TIMEOUT = 5'000; constexpr size_t MAX_ONETIME_KEYS = 50; +Q_DECLARE_METATYPE(std::optional<mtx::crypto::EncryptedFile>) +Q_DECLARE_METATYPE(std::optional<RelatedInfo>) + ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) : QWidget(parent) , isConnected_(true) @@ -61,6 +66,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) { setObjectName("chatPage"); + qRegisterMetaType<std::optional<mtx::crypto::EncryptedFile>>(); + qRegisterMetaType<std::optional<RelatedInfo>>(); + topLayout_ = new QHBoxLayout(this); topLayout_->setSpacing(0); topLayout_->setMargin(0); @@ -76,7 +84,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) // SideBar sideBar_ = new QFrame(this); sideBar_->setObjectName("sideBar"); - sideBar_->setMinimumWidth(utils::calculateSidebarSizes(QFont{}).normal); + sideBar_->setMinimumWidth(::splitter::calculateSidebarSizes(QFont{}).normal); sideBarLayout_ = new QVBoxLayout(sideBar_); sideBarLayout_->setSpacing(0); sideBarLayout_->setMargin(0); @@ -88,8 +96,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) connect(sidebarActions_, &SideBarActions::joinRoom, this, &ChatPage::joinRoom); connect(sidebarActions_, &SideBarActions::createRoom, this, &ChatPage::createRoom); - user_info_widget_ = new UserInfoWidget(sideBar_); - room_list_ = new RoomList(sideBar_); + user_info_widget_ = new UserInfoWidget(sideBar_); + user_mentions_popup_ = new popups::UserMentions(); + room_list_ = new RoomList(sideBar_); connect(room_list_, &RoomList::joinRoom, this, &ChatPage::joinRoom); sideBarLayout_->addWidget(user_info_widget_); @@ -108,15 +117,10 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) contentLayout_->setMargin(0); top_bar_ = new TopRoomBar(this); - view_manager_ = new TimelineViewManager(this); + view_manager_ = new TimelineViewManager(userSettings_, this); contentLayout_->addWidget(top_bar_); - contentLayout_->addWidget(view_manager_); - - connect(this, - &ChatPage::removeTimelineEvent, - view_manager_, - &TimelineViewManager::removeTimelineEvent); + contentLayout_->addWidget(view_manager_->getWidget()); // Splitter splitter->addWidget(sideBar_); @@ -126,11 +130,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) text_input_ = new TextInputWidget(this); contentLayout_->addWidget(text_input_); - typingDisplay_ = new TypingDisplay(content_); - typingDisplay_->hide(); - connect( - text_input_, &TextInputWidget::heightChanged, typingDisplay_, &TypingDisplay::setOffset); - typingRefresher_ = new QTimer(this); typingRefresher_->setInterval(TYPING_REFRESH_TIMEOUT); @@ -150,6 +149,41 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) trySync(); }); + connect( + new QShortcut(QKeySequence("Ctrl+Down"), this), &QShortcut::activated, this, [this]() { + if (isVisible()) + room_list_->nextRoom(); + }); + connect( + new QShortcut(QKeySequence("Ctrl+Up"), this), &QShortcut::activated, this, [this]() { + if (isVisible()) + room_list_->previousRoom(); + }); + + connect(top_bar_, &TopRoomBar::mentionsClicked, this, [this](const QPoint &mentionsPos) { + if (user_mentions_popup_->isVisible()) { + user_mentions_popup_->hide(); + } else { + showNotificationsDialog(mentionsPos); + http::client()->notifications( + 1000, + "", + "highlight", + [this, mentionsPos](const mtx::responses::Notifications &res, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn( + "failed to retrieve notifications: {} ({})", + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + emit highlightedNotifsRetrieved(std::move(res), mentionsPos); + }); + } + }); + connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL); connect(&connectivityTimer_, &QTimer::timeout, this, [=]() { if (http::client()->access_token().empty()) { @@ -186,30 +220,16 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) mtx::http::RequestErr err) { if (err) { emit showNotification( - QString("Failed to invite user: %1").arg(user)); + tr("Failed to invite user: %1").arg(user)); return; } - emit showNotification( - QString("Invited user: %1").arg(user)); + emit showNotification(tr("Invited user: %1").arg(user)); }); }); } }); - connect(room_list_, &RoomList::roomChanged, this, [this](const QString &roomid) { - QStringList users; - - if (!userSettings_->isTypingNotificationsEnabled()) { - typingDisplay_->setUsers(users); - return; - } - - if (typingUsers_.find(roomid) != typingUsers_.end()) - users = typingUsers_[roomid]; - - typingDisplay_->setUsers(users); - }); connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::stopTyping); connect(room_list_, &RoomList::roomChanged, this, &ChatPage::changeTopRoomInfo); connect(room_list_, &RoomList::roomChanged, splitter, &Splitter::showChatView); @@ -260,22 +280,31 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) SLOT(showUnreadMessageNotification(int))); connect(text_input_, - SIGNAL(sendTextMessage(const QString &)), + &TextInputWidget::sendTextMessage, view_manager_, - SLOT(queueTextMessage(const QString &))); + &TimelineViewManager::queueTextMessage); connect(text_input_, - SIGNAL(sendEmoteMessage(const QString &)), + &TextInputWidget::sendEmoteMessage, view_manager_, - SLOT(queueEmoteMessage(const QString &))); + &TimelineViewManager::queueEmoteMessage); connect(text_input_, &TextInputWidget::sendJoinRoomRequest, this, &ChatPage::joinRoom); + // invites and bans via quick command + connect(text_input_, &TextInputWidget::sendInviteRoomRequest, this, &ChatPage::inviteUser); + connect(text_input_, &TextInputWidget::sendKickRoomRequest, this, &ChatPage::kickUser); + connect(text_input_, &TextInputWidget::sendBanRoomRequest, this, &ChatPage::banUser); + connect(text_input_, &TextInputWidget::sendUnbanRoomRequest, this, &ChatPage::unbanUser); + connect( text_input_, - &TextInputWidget::uploadImage, + &TextInputWidget::uploadMedia, this, - [this](QSharedPointer<QIODevice> dev, const QString &fn) { + [this](QSharedPointer<QIODevice> dev, + QString mimeClass, + const QString &fn, + const std::optional<RelatedInfo> &related) { QMimeDatabase db; QMimeType mime = db.mimeTypeForData(dev.data()); @@ -285,205 +314,89 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) return; } - auto bin = dev->peek(dev->size()); - auto payload = std::string(bin.data(), bin.size()); - auto dimensions = QImageReader(dev.data()).size(); + auto bin = dev->peek(dev->size()); + auto payload = std::string(bin.data(), bin.size()); + std::optional<mtx::crypto::EncryptedFile> encryptedFile; + if (cache::isRoomEncrypted(current_room_.toStdString())) { + mtx::crypto::BinaryBuf buf; + std::tie(buf, encryptedFile) = mtx::crypto::encrypt_file(payload); + payload = mtx::crypto::to_string(buf); + } + + QSize dimensions; + if (mimeClass == "image") + dimensions = QImageReader(dev.data()).size(); http::client()->upload( payload, - mime.name().toStdString(), + encryptedFile ? "application/octet-stream" : mime.name().toStdString(), QFileInfo(fn).fileName().toStdString(), [this, room_id = current_room_, filename = fn, - mime = mime.name(), - size = payload.size(), - dimensions](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) { + encryptedFile, + mimeClass, + mime = mime.name(), + size = payload.size(), + dimensions, + related](const mtx::responses::ContentURI &res, mtx::http::RequestErr err) { if (err) { emit uploadFailed( - tr("Failed to upload image. Please try again.")); - nhlog::net()->warn("failed to upload image: {} {} ({})", + tr("Failed to upload media. Please try again.")); + nhlog::net()->warn("failed to upload media: {} {} ({})", err->matrix_error.error, to_string(err->matrix_error.errcode), static_cast<int>(err->status_code)); return; } - emit imageUploaded(room_id, + emit mediaUploaded(room_id, filename, + encryptedFile, QString::fromStdString(res.content_uri), + mimeClass, mime, size, - dimensions); + dimensions, + related); }); }); - connect(text_input_, - &TextInputWidget::uploadFile, - this, - [this](QSharedPointer<QIODevice> dev, const QString &fn) { - QMimeDatabase db; - QMimeType mime = db.mimeTypeForData(dev.data()); - - if (!dev->open(QIODevice::ReadOnly)) { - emit uploadFailed( - QString("Error while reading media: %1").arg(dev->errorString())); - return; - } - - auto bin = dev->readAll(); - auto payload = std::string(bin.data(), bin.size()); - - http::client()->upload( - payload, - mime.name().toStdString(), - QFileInfo(fn).fileName().toStdString(), - [this, - room_id = current_room_, - filename = fn, - mime = mime.name(), - size = payload.size()](const mtx::responses::ContentURI &res, - mtx::http::RequestErr err) { - if (err) { - emit uploadFailed( - tr("Failed to upload file. Please try again.")); - nhlog::net()->warn("failed to upload file: {} ({})", - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - emit fileUploaded(room_id, - filename, - QString::fromStdString(res.content_uri), - mime, - size); - }); - }); - - connect(text_input_, - &TextInputWidget::uploadAudio, - this, - [this](QSharedPointer<QIODevice> dev, const QString &fn) { - QMimeDatabase db; - QMimeType mime = db.mimeTypeForData(dev.data()); - - if (!dev->open(QIODevice::ReadOnly)) { - emit uploadFailed( - QString("Error while reading media: %1").arg(dev->errorString())); - return; - } - - auto bin = dev->readAll(); - auto payload = std::string(bin.data(), bin.size()); - - http::client()->upload( - payload, - mime.name().toStdString(), - QFileInfo(fn).fileName().toStdString(), - [this, - room_id = current_room_, - filename = fn, - mime = mime.name(), - size = payload.size()](const mtx::responses::ContentURI &res, - mtx::http::RequestErr err) { - if (err) { - emit uploadFailed( - tr("Failed to upload audio. Please try again.")); - nhlog::net()->warn("failed to upload audio: {} ({})", - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - emit audioUploaded(room_id, - filename, - QString::fromStdString(res.content_uri), - mime, - size); - }); - }); - connect(text_input_, - &TextInputWidget::uploadVideo, - this, - [this](QSharedPointer<QIODevice> dev, const QString &fn) { - QMimeDatabase db; - QMimeType mime = db.mimeTypeForData(dev.data()); - - if (!dev->open(QIODevice::ReadOnly)) { - emit uploadFailed( - QString("Error while reading media: %1").arg(dev->errorString())); - return; - } - - auto bin = dev->readAll(); - auto payload = std::string(bin.data(), bin.size()); - - http::client()->upload( - payload, - mime.name().toStdString(), - QFileInfo(fn).fileName().toStdString(), - [this, - room_id = current_room_, - filename = fn, - mime = mime.name(), - size = payload.size()](const mtx::responses::ContentURI &res, - mtx::http::RequestErr err) { - if (err) { - emit uploadFailed( - tr("Failed to upload video. Please try again.")); - nhlog::net()->warn("failed to upload video: {} ({})", - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - emit videoUploaded(room_id, - filename, - QString::fromStdString(res.content_uri), - mime, - size); - }); - }); - connect(this, &ChatPage::uploadFailed, this, [this](const QString &msg) { text_input_->hideUploadSpinner(); emit showNotification(msg); }); - connect(this, - &ChatPage::imageUploaded, - this, - [this](QString roomid, - QString filename, - QString url, - QString mime, - qint64 dsize, - QSize dimensions) { - text_input_->hideUploadSpinner(); - view_manager_->queueImageMessage( - roomid, filename, url, mime, dsize, dimensions); - }); - connect(this, - &ChatPage::fileUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueFileMessage(roomid, filename, url, mime, dsize); - }); - connect(this, - &ChatPage::audioUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueAudioMessage(roomid, filename, url, mime, dsize); - }); - connect(this, - &ChatPage::videoUploaded, - this, - [this](QString roomid, QString filename, QString url, QString mime, qint64 dsize) { - text_input_->hideUploadSpinner(); - view_manager_->queueVideoMessage(roomid, filename, url, mime, dsize); - }); + connect( + this, + &ChatPage::mediaUploaded, + this, + [this](QString roomid, + QString filename, + std::optional<mtx::crypto::EncryptedFile> encryptedFile, + QString url, + QString mimeClass, + QString mime, + qint64 dsize, + QSize dimensions, + const std::optional<RelatedInfo> &related) { + text_input_->hideUploadSpinner(); + + if (encryptedFile) + encryptedFile->url = url.toStdString(); + + if (mimeClass == "image") + view_manager_->queueImageMessage( + roomid, filename, encryptedFile, url, mime, dsize, dimensions, related); + else if (mimeClass == "audio") + view_manager_->queueAudioMessage( + roomid, filename, encryptedFile, url, mime, dsize, related); + else if (mimeClass == "video") + view_manager_->queueVideoMessage( + roomid, filename, encryptedFile, url, mime, dsize, related); + else + view_manager_->queueFileMessage( + roomid, filename, encryptedFile, url, mime, dsize, related); + }); connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar); @@ -492,6 +405,16 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom); connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendDesktopNotifications); + connect(this, + &ChatPage::highlightedNotifsRetrieved, + this, + [](const mtx::responses::Notifications &notif) { + try { + cache::saveTimelineMentions(notif); + } catch (const lmdb::error &e) { + nhlog::db()->error("failed to save mentions: {}", e.what()); + } + }); connect(communitiesList_, &CommunitiesList::communityChanged, @@ -525,26 +448,28 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) connect(this, &ChatPage::initializeViews, view_manager_, - [this](const mtx::responses::Rooms &rooms) { view_manager_->initialize(rooms); }); + [this](const mtx::responses::Rooms &rooms) { view_manager_->sync(rooms); }); connect(this, &ChatPage::initializeEmptyViews, view_manager_, &TimelineViewManager::initWithMessages); + connect(this, + &ChatPage::initializeMentions, + user_mentions_popup_, + &popups::UserMentions::initializeMentions); connect(this, &ChatPage::syncUI, this, [this](const mtx::responses::Rooms &rooms) { try { - room_list_->cleanupInvites(cache::client()->invites()); + room_list_->cleanupInvites(cache::invites()); } catch (const lmdb::error &e) { nhlog::db()->error("failed to retrieve invites: {}", e.what()); } - view_manager_->initialize(rooms); + view_manager_->sync(rooms); removeLeftRooms(rooms.leave); bool hasNotifications = false; for (const auto &room : rooms.join) { auto room_id = QString::fromStdString(room.first); - - updateTypingUsers(room_id, room.second.ephemeral.typing); updateRoomNotificationCount( room_id, room.second.unread_notifications.notification_count, @@ -557,6 +482,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) if (hasNotifications && userSettings_->hasDesktopNotifications()) http::client()->notifications( 5, + "", + "", [this](const mtx::responses::Notifications &res, mtx::http::RequestErr err) { if (err) { @@ -594,6 +521,13 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) connect(this, &ChatPage::dropToLoginPageCb, this, &ChatPage::dropToLoginPage); connect(this, &ChatPage::messageReply, text_input_, &TextInputWidget::addReply); + connect(this, &ChatPage::messageReply, this, [this](const RelatedInfo &related) { + view_manager_->updateReplyingEvent(QString::fromStdString(related.related_event)); + }); + connect(view_manager_, + &TimelineViewManager::replyClosed, + text_input_, + &TextInputWidget::closeReplyPopup); instance_ = this; } @@ -648,7 +582,7 @@ ChatPage::deleteConfigs() settings.remove(""); settings.endGroup(); - cache::client()->deleteData(); + cache::deleteData(); http::client()->clear(); } @@ -683,18 +617,18 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) connect( cache::client(), &Cache::roomReadStatus, room_list_, &RoomList::updateReadStatus); - const bool isInitialized = cache::client()->isInitialized(); - const bool isValid = cache::client()->isFormatValid(); + const bool isInitialized = cache::isInitialized(); + const bool isValid = cache::isFormatValid(); if (!isInitialized) { - cache::client()->setCurrentFormat(); + cache::setCurrentFormat(); } else if (isInitialized && !isValid) { // TODO: Deleting session data but keep using the // same device doesn't work. - cache::client()->deleteData(); + cache::deleteData(); cache::init(userid); - cache::client()->setCurrentFormat(); + cache::setCurrentFormat(); } else if (isInitialized) { loadStateFromCache(); return; @@ -702,7 +636,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) } catch (const lmdb::error &e) { nhlog::db()->critical("failure during boot: {}", e.what()); - cache::client()->deleteData(); + cache::deleteData(); nhlog::net()->info("falling back to initial sync"); } @@ -711,7 +645,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) // There isn't a saved olm account to restore. nhlog::crypto()->info("creating new olm account"); olm::client()->create_new_account(); - cache::client()->saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY)); + cache::saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY)); } catch (const lmdb::error &e) { nhlog::crypto()->critical("failed to save olm account {}", e.what()); emit dropToLoginPageCb(QString::fromStdString(e.what())); @@ -727,12 +661,12 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token) } void -ChatPage::updateTopBarAvatar(const QString &roomid, const QPixmap &img) +ChatPage::updateTopBarAvatar(const QString &roomid, const QString &img) { if (current_room_ != roomid) return; - top_bar_->updateRoomAvatar(img.toImage()); + top_bar_->updateRoomAvatar(img); } void @@ -744,7 +678,7 @@ ChatPage::changeTopRoomInfo(const QString &room_id) } try { - auto room_info = cache::client()->getRoomInfo({room_id.toStdString()}); + auto room_info = cache::getRoomInfo({room_id.toStdString()}); if (room_info.find(room_id) == room_info.end()) return; @@ -755,12 +689,12 @@ ChatPage::changeTopRoomInfo(const QString &room_id) top_bar_->updateRoomName(name); top_bar_->updateRoomTopic(QString::fromStdString(room_info[room_id].topic)); - auto img = cache::client()->getRoomAvatar(room_id); + auto img = cache::getRoomAvatar(room_id); if (img.isNull()) top_bar_->updateRoomAvatarFromName(name); else - top_bar_->updateRoomAvatar(img); + top_bar_->updateRoomAvatar(avatar_url); } catch (const lmdb::error &e) { nhlog::ui()->error("failed to change top bar room info: {}", e.what()); @@ -792,17 +726,17 @@ ChatPage::loadStateFromCache() QtConcurrent::run([this]() { try { - cache::client()->restoreSessions(); - olm::client()->load(cache::client()->restoreOlmAccount(), - STORAGE_SECRET_KEY); + cache::restoreSessions(); + olm::client()->load(cache::restoreOlmAccount(), STORAGE_SECRET_KEY); - cache::client()->populateMembers(); + cache::populateMembers(); - emit initializeEmptyViews(cache::client()->roomMessages()); - emit initializeRoomList(cache::client()->roomInfo()); - emit syncTags(cache::client()->roomInfo().toStdMap()); + emit initializeEmptyViews(cache::roomMessages()); + emit initializeRoomList(cache::roomInfo()); + emit initializeMentions(cache::getTimelineMentions()); + emit syncTags(cache::roomInfo().toStdMap()); - cache::client()->calculateRoomReadStatus(); + cache::calculateRoomReadStatus(); } catch (const mtx::crypto::olm_exception &e) { nhlog::crypto()->critical("failed to restore olm account: {}", e.what()); @@ -845,8 +779,8 @@ void ChatPage::removeRoom(const QString &room_id) { try { - cache::client()->removeRoom(room_id); - cache::client()->removeInvite(room_id.toStdString()); + cache::removeRoom(room_id); + cache::removeInvite(room_id.toStdString()); } catch (const lmdb::error &e) { nhlog::db()->critical("failure while removing room: {}", e.what()); // TODO: Notify the user. @@ -856,38 +790,6 @@ ChatPage::removeRoom(const QString &room_id) } void -ChatPage::updateTypingUsers(const QString &roomid, const std::vector<std::string> &user_ids) -{ - if (!userSettings_->isTypingNotificationsEnabled()) - return; - - typingUsers_[roomid] = generateTypingUsers(roomid, user_ids); - - if (current_room_ == roomid) - typingDisplay_->setUsers(typingUsers_[roomid]); -} - -QStringList -ChatPage::generateTypingUsers(const QString &room_id, const std::vector<std::string> &typing_users) -{ - QStringList users; - auto local_user = utils::localUser(); - - for (const auto &uid : typing_users) { - const auto remote_user = QString::fromStdString(uid); - - if (remote_user == local_user) - continue; - - users.append(Cache::displayName(room_id, remote_user)); - } - - users.sort(); - - return users; -} - -void ChatPage::removeLeftRooms(const std::map<std::string, mtx::responses::LeftRoom> &rooms) { for (auto it = rooms.cbegin(); it != rooms.cend(); ++it) { @@ -925,16 +827,16 @@ ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res) try { if (item.read) { - cache::client()->removeReadNotification(event_id); + cache::removeReadNotification(event_id); continue; } - if (!cache::client()->isNotificationSent(event_id)) { + if (!cache::isNotificationSent(event_id)) { const auto room_id = QString::fromStdString(item.room_id); const auto user_id = utils::event_sender(item.event); // We should only sent one notification per event. - cache::client()->markSentNotification(event_id); + cache::markSentNotification(event_id); // Don't send a notification when the current room is opened. if (isRoomActive(room_id)) @@ -943,11 +845,10 @@ ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res) notificationsManager.postNotification( room_id, QString::fromStdString(event_id), - QString::fromStdString( - cache::client()->singleRoomInfo(item.room_id).name), - Cache::displayName(room_id, user_id), + QString::fromStdString(cache::singleRoomInfo(item.room_id).name), + cache::displayName(room_id, user_id), utils::event_body(item.event), - cache::client()->getRoomAvatar(room_id)); + cache::getRoomAvatar(room_id)); } } catch (const lmdb::error &e) { nhlog::db()->warn("error while sending desktop notification: {}", e.what()); @@ -956,6 +857,18 @@ ChatPage::sendDesktopNotifications(const mtx::responses::Notifications &res) } void +ChatPage::showNotificationsDialog(const QPoint &widgetPos) +{ + auto notifDialog = user_mentions_popup_; + + notifDialog->setGeometry( + widgetPos.x() - (width() / 10), widgetPos.y() + 25, width() / 5, height() / 2); + + notifDialog->raise(); + notifDialog->showPopup(); +} + +void ChatPage::tryInitialSync() { nhlog::crypto()->info("ed25519 : {}", olm::client()->identity_keys().ed25519); @@ -1022,7 +935,7 @@ ChatPage::trySync() connectivityTimer_.start(); try { - opts.since = cache::client()->nextBatchToken(); + opts.since = cache::nextBatchToken(); } catch (const lmdb::error &e) { nhlog::db()->error("failed to retrieve next batch token: {}", e.what()); return; @@ -1075,22 +988,22 @@ ChatPage::trySync() // TODO: fine grained error handling try { - cache::client()->saveState(res); + cache::saveState(res); olm::handle_to_device_messages(res.to_device); emit syncUI(res.rooms); - auto updates = cache::client()->roomUpdates(res); + auto updates = cache::roomUpdates(res); emit syncTopBar(updates); emit syncRoomlist(updates); - emit syncTags(cache::client()->roomTagUpdates(res)); + emit syncTags(cache::roomTagUpdates(res)); - cache::client()->deleteOldData(); + cache::deleteOldData(); } catch (const lmdb::map_full_error &e) { nhlog::db()->error("lmdb is full: {}", e.what()); - cache::client()->deleteOldData(); + cache::deleteOldData(); } catch (const lmdb::error &e) { nhlog::db()->error("saving sync response: {}", e.what()); } @@ -1109,19 +1022,18 @@ ChatPage::joinRoom(const QString &room) room_id, [this, room_id](const nlohmann::json &, mtx::http::RequestErr err) { if (err) { emit showNotification( - QString("Failed to join room: %1") + tr("Failed to join room: %1") .arg(QString::fromStdString(err->matrix_error.error))); return; } - emit showNotification("You joined the room"); + emit tr("You joined the room"); // We remove any invites with the same room_id. try { - cache::client()->removeInvite(room_id); + cache::removeInvite(room_id); } catch (const lmdb::error &e) { - emit showNotification( - QString("Failed to remove invite: %1").arg(e.what())); + emit showNotification(tr("Failed to remove invite: %1").arg(e.what())); } }); } @@ -1144,8 +1056,8 @@ ChatPage::createRoom(const mtx::requests::CreateRoom &req) return; } - emit showNotification(QString("Room %1 created") - .arg(QString::fromStdString(res.room_id.to_string()))); + emit showNotification( + tr("Room %1 created").arg(QString::fromStdString(res.room_id.to_string()))); }); } @@ -1166,6 +1078,83 @@ ChatPage::leaveRoom(const QString &room_id) } void +ChatPage::inviteUser(QString userid, QString reason) +{ + http::client()->invite_user( + current_room_.toStdString(), + userid.toStdString(), + [this, userid, room = current_room_](const mtx::responses::Empty &, + mtx::http::RequestErr err) { + if (err) { + emit showNotification( + tr("Failed to invite %1 to %2: %3") + .arg(userid) + .arg(room) + .arg(QString::fromStdString(err->matrix_error.error))); + } else + emit showNotification(tr("Invited user: %1").arg(userid)); + }, + reason.trimmed().toStdString()); +} +void +ChatPage::kickUser(QString userid, QString reason) +{ + http::client()->kick_user( + current_room_.toStdString(), + userid.toStdString(), + [this, userid, room = current_room_](const mtx::responses::Empty &, + mtx::http::RequestErr err) { + if (err) { + emit showNotification( + tr("Failed to kick %1 to %2: %3") + .arg(userid) + .arg(room) + .arg(QString::fromStdString(err->matrix_error.error))); + } else + emit showNotification(tr("Kicked user: %1").arg(userid)); + }, + reason.trimmed().toStdString()); +} +void +ChatPage::banUser(QString userid, QString reason) +{ + http::client()->ban_user( + current_room_.toStdString(), + userid.toStdString(), + [this, userid, room = current_room_](const mtx::responses::Empty &, + mtx::http::RequestErr err) { + if (err) { + emit showNotification( + tr("Failed to ban %1 in %2: %3") + .arg(userid) + .arg(room) + .arg(QString::fromStdString(err->matrix_error.error))); + } else + emit showNotification(tr("Banned user: %1").arg(userid)); + }, + reason.trimmed().toStdString()); +} +void +ChatPage::unbanUser(QString userid, QString reason) +{ + http::client()->unban_user( + current_room_.toStdString(), + userid.toStdString(), + [this, userid, room = current_room_](const mtx::responses::Empty &, + mtx::http::RequestErr err) { + if (err) { + emit showNotification( + tr("Failed to unban %1 in %2: %3") + .arg(userid) + .arg(room) + .arg(QString::fromStdString(err->matrix_error.error))); + } else + emit showNotification(tr("Unbanned user: %1").arg(userid)); + }, + reason.trimmed().toStdString()); +} + +void ChatPage::sendTypingNotifications() { if (!userSettings_->isTypingNotificationsEnabled()) @@ -1183,6 +1172,8 @@ ChatPage::sendTypingNotifications() void ChatPage::initialSyncHandler(const mtx::responses::Sync &res, mtx::http::RequestErr err) { + // TODO: Initial Sync should include mentions as well... + if (err) { const auto error = QString::fromStdString(err->matrix_error.error); const auto msg = tr("Please try to login again: %1").arg(error); @@ -1214,15 +1205,16 @@ ChatPage::initialSyncHandler(const mtx::responses::Sync &res, mtx::http::Request nhlog::net()->info("initial sync completed"); try { - cache::client()->saveState(res); + cache::saveState(res); olm::handle_to_device_messages(res.to_device); emit initializeViews(std::move(res.rooms)); - emit initializeRoomList(cache::client()->roomInfo()); + emit initializeRoomList(cache::roomInfo()); + emit initializeMentions(cache::getTimelineMentions()); - cache::client()->calculateRoomReadStatus(); - emit syncTags(cache::client()->roomInfo().toStdMap()); + cache::calculateRoomReadStatus(); + emit syncTags(cache::roomInfo().toStdMap()); } catch (const lmdb::error &e) { nhlog::db()->error("failed to save state after initial sync: {}", e.what()); startInitialSync(); @@ -1274,37 +1266,7 @@ ChatPage::getProfileInfo() emit setUserDisplayName(QString::fromStdString(res.display_name)); - if (cache::client()) { - auto data = cache::client()->image(res.avatar_url); - if (!data.isNull()) { - emit setUserAvatar(QImage::fromData(data)); - return; - } - } - - if (res.avatar_url.empty()) - return; - - http::client()->download( - res.avatar_url, - [this, res](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to download user avatar: {} - {}", - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - - if (cache::client()) - cache::client()->saveImage(res.avatar_url, data); - - emit setUserAvatar( - QImage::fromData(QByteArray(data.data(), data.size()))); - }); + emit setUserAvatar(QString::fromStdString(res.avatar_url)); }); http::client()->joined_groups( @@ -1349,7 +1311,7 @@ ChatPage::timelineWidth() bool ChatPage::isSideBarExpanded() { - const auto sz = utils::calculateSidebarSizes(QFont{}); + const auto sz = splitter::calculateSidebarSizes(QFont{}); return sideBar_->size().width() > sz.normal; } diff --git a/src/ChatPage.h b/src/ChatPage.h
index 7d3b3273..8e2e9192 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h
@@ -18,19 +18,27 @@ #pragma once #include <atomic> -#include <boost/variant.hpp> +#include <optional> +#include <variant> + +#include <mtx/common.hpp> +#include <mtx/requests.hpp> +#include <mtx/responses.hpp> +#include <mtxclient/http/errors.hpp> #include <QFrame> #include <QHBoxLayout> #include <QMap> #include <QPixmap> +#include <QPoint> #include <QTimer> #include <QWidget> -#include "Cache.h" +#include "CacheStructs.h" #include "CommunitiesList.h" -#include "MatrixClient.h" +#include "Utils.h" #include "notifications/Manager.h" +#include "popups/UserMentions.h" class OverlayModal; class QuickSwitcher; @@ -40,7 +48,6 @@ class Splitter; class TextInputWidget; class TimelineViewManager; class TopRoomBar; -class TypingDisplay; class UserInfoWidget; class UserSettings; class NotificationsManager; @@ -49,12 +56,16 @@ constexpr int CONSENSUS_TIMEOUT = 1000; constexpr int SHOW_CONTENT_TIMEOUT = 3000; constexpr int TYPING_REFRESH_TIMEOUT = 10000; +namespace mtx::http { +using RequestErr = const std::optional<mtx::http::ClientError> &; +} + class ChatPage : public QWidget { Q_OBJECT public: - ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent = 0); + ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent = nullptr); // Initialize all the components of the UI. void bootstrap(QString userid, QString homeserver, QString token); @@ -79,36 +90,31 @@ public slots: void leaveRoom(const QString &room_id); void createRoom(const mtx::requests::CreateRoom &req); + void inviteUser(QString userid, QString reason); + void kickUser(QString userid, QString reason); + void banUser(QString userid, QString reason); + void unbanUser(QString userid, QString reason); + signals: void connectionLost(); void connectionRestored(); - void messageReply(const QString &username, const QString &msg); + void messageReply(const RelatedInfo &related); void notificationsRetrieved(const mtx::responses::Notifications &); + void highlightedNotifsRetrieved(const mtx::responses::Notifications &, + const QPoint widgetPos); void uploadFailed(const QString &msg); - void imageUploaded(const QString &roomid, + void mediaUploaded(const QString &roomid, const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &file, const QString &url, + const QString &mimeClass, const QString &mime, qint64 dsize, - const QSize &dimensions); - void fileUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - qint64 dsize); - void audioUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - qint64 dsize); - void videoUploaded(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - qint64 dsize); + const QSize &dimensions, + const std::optional<RelatedInfo> &related); void contentLoaded(); void closing(); @@ -119,11 +125,9 @@ signals: void showUserSettingsPage(); void showOverlayProgressBar(); - void removeTimelineEvent(const QString &room_id, const QString &event_id); - void ownProfileOk(); void setUserDisplayName(const QString &name); - void setUserAvatar(const QImage &avatar); + void setUserAvatar(const QString &avatar); void loggedOut(); void trySyncCb(); @@ -134,6 +138,7 @@ signals: void initializeRoomList(QMap<QString, RoomInfo>); void initializeViews(const mtx::responses::Rooms &rooms); void initializeEmptyViews(const std::map<QString, mtx::responses::Timeline> &msgs); + void initializeMentions(const QMap<QString, mtx::responses::Notifications> &notifs); void syncUI(const mtx::responses::Rooms &rooms); void syncRoomlist(const std::map<QString, RoomInfo> &updates); void syncTags(const std::map<QString, RoomInfo> &updates); @@ -152,7 +157,7 @@ signals: private slots: void showUnreadMessageNotification(int count); - void updateTopBarAvatar(const QString &roomid, const QPixmap &img); + void updateTopBarAvatar(const QString &roomid, const QString &img); void changeTopRoomInfo(const QString &room_id); void logout(); void removeRoom(const QString &room_id); @@ -186,8 +191,6 @@ private: using LeftRooms = std::map<std::string, mtx::responses::LeftRoom>; void removeLeftRooms(const LeftRooms &rooms); - void updateTypingUsers(const QString &roomid, const std::vector<std::string> &user_ids); - void loadStateFromCache(); void resetUI(); //! Decides whether or not to hide the group's sidebar. @@ -203,8 +206,7 @@ private: //! Send desktop notification for the received messages. void sendDesktopNotifications(const mtx::responses::Notifications &); - QStringList generateTypingUsers(const QString &room_id, - const std::vector<std::string> &typing_users); + void showNotificationsDialog(const QPoint &point); QHBoxLayout *topLayout_; Splitter *splitter; @@ -225,7 +227,6 @@ private: TopRoomBar *top_bar_; TextInputWidget *text_input_; - TypingDisplay *typingDisplay_; QTimer connectivityTimer_; std::atomic_bool isConnected_; @@ -235,8 +236,8 @@ private: UserInfoWidget *user_info_widget_; - // Keeps track of the users currently typing on each room. - std::map<QString, QList<QString>> typingUsers_; + popups::UserMentions *user_mentions_popup_; + QTimer *typingRefresher_; // Global user settings. @@ -254,9 +255,8 @@ ChatPage::getMemberships(const std::vector<Collection> &collection) const using Member = mtx::events::StateEvent<mtx::events::state::Member>; for (const auto &event : collection) { - if (boost::get<Member>(event) != nullptr) { - auto member = boost::get<Member>(event); - memberships.emplace(member.state_key, member); + if (auto member = std::get_if<Member>(event)) { + memberships.emplace(member->state_key, *member); } } diff --git a/src/ColorImageProvider.cpp b/src/ColorImageProvider.cpp new file mode 100644
index 00000000..c580c394 --- /dev/null +++ b/src/ColorImageProvider.cpp
@@ -0,0 +1,27 @@ +#include "ColorImageProvider.h" + +#include <QPainter> + +QPixmap +ColorImageProvider::requestPixmap(const QString &id, QSize *size, const QSize &) +{ + auto args = id.split('?'); + + QPixmap source(args[0]); + + if (size) + *size = QSize(source.width(), source.height()); + + if (args.size() < 2) + return source; + + QColor color(args[1]); + + QPixmap colorized = source; + QPainter painter(&colorized); + painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + painter.fillRect(colorized.rect(), color); + painter.end(); + + return colorized; +} diff --git a/src/ColorImageProvider.h b/src/ColorImageProvider.h new file mode 100644
index 00000000..21f36c12 --- /dev/null +++ b/src/ColorImageProvider.h
@@ -0,0 +1,11 @@ +#include <QQuickImageProvider> + +class ColorImageProvider : public QQuickImageProvider +{ +public: + ColorImageProvider() + : QQuickImageProvider(QQuickImageProvider::Pixmap) + {} + + QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override; +}; diff --git a/src/CommunitiesList.cpp b/src/CommunitiesList.cpp
index 6e46741b..bb57ca40 100644 --- a/src/CommunitiesList.cpp +++ b/src/CommunitiesList.cpp
@@ -2,7 +2,9 @@ #include "Cache.h" #include "Logging.h" #include "MatrixClient.h" -#include "Utils.h" +#include "Splitter.h" + +#include <mtx/responses/groups.hpp> #include <QLabel> @@ -14,13 +16,11 @@ CommunitiesList::CommunitiesList(QWidget *parent) sizePolicy.setVerticalStretch(1); setSizePolicy(sizePolicy); - setStyleSheet("border-style: none;"); - topLayout_ = new QVBoxLayout(this); topLayout_->setSpacing(0); topLayout_->setMargin(0); - const auto sideBarSizes = utils::calculateSidebarSizes(QFont{}); + const auto sideBarSizes = splitter::calculateSidebarSizes(QFont{}); setFixedWidth(sideBarSizes.groups); scrollArea_ = new QScrollArea(this); @@ -30,16 +30,14 @@ CommunitiesList::CommunitiesList(QWidget *parent) scrollArea_->setWidgetResizable(true); scrollArea_->setAlignment(Qt::AlignLeading | Qt::AlignTop | Qt::AlignVCenter); - scrollAreaContents_ = new QWidget(); - - contentsLayout_ = new QVBoxLayout(scrollAreaContents_); + contentsLayout_ = new QVBoxLayout(); contentsLayout_->setSpacing(0); contentsLayout_->setMargin(0); addGlobalItem(); contentsLayout_->addStretch(1); - scrollArea_->setWidget(scrollAreaContents_); + scrollArea_->setLayout(contentsLayout_); topLayout_->addWidget(scrollArea_); connect( @@ -185,7 +183,8 @@ void CommunitiesList::updateCommunityAvatar(const QString &community_id, const QPixmap &img) { if (!communityExists(community_id)) { - qWarning() << "Avatar update on nonexistent community" << community_id; + nhlog::ui()->warn("Avatar update on nonexistent community {}", + community_id.toStdString()); return; } @@ -196,7 +195,7 @@ void CommunitiesList::highlightSelectedCommunity(const QString &community_id) { if (!communityExists(community_id)) { - qDebug() << "CommunitiesList: clicked unknown community"; + nhlog::ui()->debug("CommunitiesList: clicked unknown community"); return; } @@ -215,7 +214,7 @@ CommunitiesList::highlightSelectedCommunity(const QString &community_id) void CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUrl) { - auto savedImgData = cache::client()->image(avatarUrl); + auto savedImgData = cache::image(avatarUrl); if (!savedImgData.isNull()) { QPixmap pix; pix.loadFromData(savedImgData); @@ -238,7 +237,7 @@ CommunitiesList::fetchCommunityAvatar(const QString &id, const QString &avatarUr return; } - cache::client()->saveImage(opts.mxc_url, res); + cache::saveImage(opts.mxc_url, res); auto data = QByteArray(res.data(), res.size()); diff --git a/src/CommunitiesList.h b/src/CommunitiesList.h
index b18df654..d3cbeeff 100644 --- a/src/CommunitiesList.h +++ b/src/CommunitiesList.h
@@ -4,10 +4,15 @@ #include <QSharedPointer> #include <QVBoxLayout> -#include "Cache.h" +#include "CacheStructs.h" #include "CommunitiesListItem.h" #include "ui/Theme.h" +namespace mtx::responses { +struct GroupProfile; +struct JoinedGroups; +} + class CommunitiesList : public QWidget { Q_OBJECT @@ -48,7 +53,6 @@ private: QVBoxLayout *topLayout_; QVBoxLayout *contentsLayout_; - QWidget *scrollAreaContents_; QScrollArea *scrollArea_; std::map<QString, QSharedPointer<CommunitiesListItem>> communities_; diff --git a/src/CommunitiesListItem.cpp b/src/CommunitiesListItem.cpp
index 324482d3..274271e5 100644 --- a/src/CommunitiesListItem.cpp +++ b/src/CommunitiesListItem.cpp
@@ -1,4 +1,7 @@ #include "CommunitiesListItem.h" + +#include <QMouseEvent> + #include "Utils.h" #include "ui/Painter.h" #include "ui/Ripple.h" diff --git a/src/CommunitiesListItem.h b/src/CommunitiesListItem.h
index d4d7e9c6..0cc5d60c 100644 --- a/src/CommunitiesListItem.h +++ b/src/CommunitiesListItem.h
@@ -1,17 +1,14 @@ #pragma once -#include <QDebug> -#include <QMouseEvent> -#include <QPainter> #include <QSharedPointer> #include <QWidget> -#include <mtx/responses/groups.hpp> - #include "Config.h" #include "ui/Theme.h" class RippleOverlay; +class QPainter; +class QMouseEvent; class CommunitiesListItem : public QWidget { diff --git a/src/Config.h b/src/Config.h
index e1271452..f99cf36b 100644 --- a/src/Config.h +++ b/src/Config.h
@@ -53,9 +53,9 @@ namespace strings { const QString url_html = "<a href=\"\\1\">\\1</a>"; const QRegularExpression url_regex( // match an URL, that is not quoted, i.e. - // vvvvvvv match quote via negative lookahead/lookbehind vvvvvv - // vvvv atomic match url -> fail if there is a " before or after vv - "(?<!\")(?>((www\\.(?!\\.)|[a-z][a-z0-9+.-]*://)[^\\s<>'\"]+[^!,\\.\\s<>'\"\\]\\)\\:]))(?!\")"); + // vvvvvv match quote via negative lookahead/lookbehind vv + // vvvv atomic match url -> fail if there is a " before or after vvv + R"((?<!")(?>((www\.(?!\.)|[a-z][a-z0-9+.-]*://)[^\s<>'"]+[^!,\.\s<>'"\]\)\:]))(?!"))"); } // Window geometry. diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp new file mode 100644
index 00000000..20cdb63c --- /dev/null +++ b/src/EventAccessors.cpp
@@ -0,0 +1,383 @@ +#include "EventAccessors.h" + +#include <type_traits> + +namespace { +struct nonesuch +{ + ~nonesuch() = delete; + nonesuch(nonesuch const &) = delete; + void operator=(nonesuch const &) = delete; +}; + +namespace detail { +template<class Default, class AlwaysVoid, template<class...> class Op, class... Args> +struct detector +{ + using value_t = std::false_type; + using type = Default; +}; + +template<class Default, template<class...> class Op, class... Args> +struct detector<Default, std::void_t<Op<Args...>>, Op, Args...> +{ + using value_t = std::true_type; + using type = Op<Args...>; +}; + +} // namespace detail + +template<template<class...> class Op, class... Args> +using is_detected = typename detail::detector<nonesuch, void, Op, Args...>::value_t; + +struct EventMsgType +{ + template<class E> + using msgtype_t = decltype(E::msgtype); + template<class T> + mtx::events::MessageType operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<msgtype_t, T>::value) + return mtx::events::getMessageType(e.content.msgtype); + return mtx::events::MessageType::Unknown; + } +}; + +struct EventRoomName +{ + template<class T> + std::string operator()(const T &e) + { + if constexpr (std::is_same_v<mtx::events::StateEvent<mtx::events::state::Name>, T>) + return e.content.name; + return ""; + } +}; + +struct EventRoomTopic +{ + template<class T> + std::string operator()(const T &e) + { + if constexpr (std::is_same_v<mtx::events::StateEvent<mtx::events::state::Topic>, T>) + return e.content.topic; + return ""; + } +}; + +struct EventBody +{ + template<class C> + using body_t = decltype(C::body); + template<class T> + std::string operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<body_t, T>::value) + return e.content.body; + return ""; + } +}; + +struct EventFormattedBody +{ + template<class C> + using formatted_body_t = decltype(C::formatted_body); + template<class T> + std::string operator()(const mtx::events::RoomEvent<T> &e) + { + if constexpr (is_detected<formatted_body_t, T>::value) + return e.content.formatted_body; + return ""; + } +}; + +struct EventFile +{ + template<class Content> + using file_t = decltype(Content::file); + template<class T> + std::optional<mtx::crypto::EncryptedFile> operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<file_t, T>::value) + return e.content.file; + return std::nullopt; + } +}; + +struct EventUrl +{ + template<class Content> + using url_t = decltype(Content::url); + template<class T> + std::string operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<url_t, T>::value) { + if (auto file = EventFile{}(e)) + return file->url; + return e.content.url; + } + return ""; + } +}; + +struct EventThumbnailUrl +{ + template<class Content> + using thumbnail_url_t = decltype(Content::info.thumbnail_url); + template<class T> + std::string operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<thumbnail_url_t, T>::value) { + return e.content.info.thumbnail_url; + } + return ""; + } +}; + +struct EventFilename +{ + template<class T> + std::string operator()(const mtx::events::Event<T> &) + { + return ""; + } + std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Audio> &e) + { + // body may be the original filename + return e.content.body; + } + std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Video> &e) + { + // body may be the original filename + return e.content.body; + } + std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::Image> &e) + { + // body may be the original filename + return e.content.body; + } + std::string operator()(const mtx::events::RoomEvent<mtx::events::msg::File> &e) + { + // body may be the original filename + if (!e.content.filename.empty()) + return e.content.filename; + return e.content.body; + } +}; + +struct EventMimeType +{ + template<class Content> + using mimetype_t = decltype(Content::info.mimetype); + template<class T> + std::string operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<mimetype_t, T>::value) { + return e.content.info.mimetype; + } + return ""; + } +}; + +struct EventFilesize +{ + template<class Content> + using filesize_t = decltype(Content::info.size); + template<class T> + int64_t operator()(const mtx::events::RoomEvent<T> &e) + { + if constexpr (is_detected<filesize_t, T>::value) { + return e.content.info.size; + } + return 0; + } +}; + +struct EventInReplyTo +{ + template<class Content> + using related_ev_id_t = decltype(Content::relates_to.in_reply_to.event_id); + template<class T> + std::string operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<related_ev_id_t, T>::value) { + return e.content.relates_to.in_reply_to.event_id; + } + return ""; + } +}; + +struct EventTransactionId +{ + template<class T> + std::string operator()(const mtx::events::RoomEvent<T> &e) + { + return e.unsigned_data.transaction_id; + } + template<class T> + std::string operator()(const mtx::events::Event<T> &e) + { + return e.unsigned_data.transaction_id; + } +}; + +struct EventMediaHeight +{ + template<class Content> + using h_t = decltype(Content::info.h); + template<class T> + uint64_t operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<h_t, T>::value) { + return e.content.info.h; + } + return -1; + } +}; + +struct EventMediaWidth +{ + template<class Content> + using w_t = decltype(Content::info.w); + template<class T> + uint64_t operator()(const mtx::events::Event<T> &e) + { + if constexpr (is_detected<w_t, T>::value) { + return e.content.info.w; + } + return -1; + } +}; + +template<class T> +double +eventPropHeight(const mtx::events::RoomEvent<T> &e) +{ + auto w = eventWidth(e); + if (w == 0) + w = 1; + + double prop = eventHeight(e) / (double)w; + + return prop > 0 ? prop : 1.; +} +} + +std::string +mtx::accessors::event_id(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit([](const auto e) { return e.event_id; }, event); +} +std::string +mtx::accessors::room_id(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit([](const auto e) { return e.room_id; }, event); +} + +std::string +mtx::accessors::sender(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit([](const auto e) { return e.sender; }, event); +} + +QDateTime +mtx::accessors::origin_server_ts(const mtx::events::collections::TimelineEvents &event) +{ + return QDateTime::fromMSecsSinceEpoch( + std::visit([](const auto e) { return e.origin_server_ts; }, event)); +} + +std::string +mtx::accessors::filename(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventFilename{}, event); +} + +mtx::events::MessageType +mtx::accessors::msg_type(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventMsgType{}, event); +} +std::string +mtx::accessors::room_name(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventRoomName{}, event); +} +std::string +mtx::accessors::room_topic(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventRoomTopic{}, event); +} + +std::string +mtx::accessors::body(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventBody{}, event); +} + +std::string +mtx::accessors::formatted_body(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventFormattedBody{}, event); +} + +QString +mtx::accessors::formattedBodyWithFallback(const mtx::events::collections::TimelineEvents &event) +{ + auto formatted = formatted_body(event); + if (!formatted.empty()) + return QString::fromStdString(formatted); + else + return QString::fromStdString(body(event)).toHtmlEscaped().replace("\n", "<br>"); +} + +std::optional<mtx::crypto::EncryptedFile> +mtx::accessors::file(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventFile{}, event); +} + +std::string +mtx::accessors::url(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventUrl{}, event); +} +std::string +mtx::accessors::thumbnail_url(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventThumbnailUrl{}, event); +} +std::string +mtx::accessors::mimetype(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventMimeType{}, event); +} +std::string +mtx::accessors::in_reply_to_event(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventInReplyTo{}, event); +} + +std::string +mtx::accessors::transaction_id(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventTransactionId{}, event); +} + +int64_t +mtx::accessors::filesize(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventFilesize{}, event); +} + +uint64_t +mtx::accessors::media_height(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventMediaHeight{}, event); +} + +uint64_t +mtx::accessors::media_width(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(EventMediaWidth{}, event); +} diff --git a/src/EventAccessors.h b/src/EventAccessors.h new file mode 100644
index 00000000..cf79f68f --- /dev/null +++ b/src/EventAccessors.h
@@ -0,0 +1,64 @@ +#pragma once + +#include <string> + +#include <QDateTime> +#include <QString> + +#include <mtx/events/collections.hpp> + +namespace mtx::accessors { +std::string +event_id(const mtx::events::collections::TimelineEvents &event); + +std::string +room_id(const mtx::events::collections::TimelineEvents &event); + +std::string +sender(const mtx::events::collections::TimelineEvents &event); + +QDateTime +origin_server_ts(const mtx::events::collections::TimelineEvents &event); + +std::string +filename(const mtx::events::collections::TimelineEvents &event); + +mtx::events::MessageType +msg_type(const mtx::events::collections::TimelineEvents &event); +std::string +room_name(const mtx::events::collections::TimelineEvents &event); +std::string +room_topic(const mtx::events::collections::TimelineEvents &event); + +std::string +body(const mtx::events::collections::TimelineEvents &event); + +std::string +formatted_body(const mtx::events::collections::TimelineEvents &event); + +QString +formattedBodyWithFallback(const mtx::events::collections::TimelineEvents &event); + +std::optional<mtx::crypto::EncryptedFile> +file(const mtx::events::collections::TimelineEvents &event); + +std::string +url(const mtx::events::collections::TimelineEvents &event); +std::string +thumbnail_url(const mtx::events::collections::TimelineEvents &event); +std::string +mimetype(const mtx::events::collections::TimelineEvents &event); +std::string +in_reply_to_event(const mtx::events::collections::TimelineEvents &event); +std::string +transaction_id(const mtx::events::collections::TimelineEvents &event); + +int64_t +filesize(const mtx::events::collections::TimelineEvents &event); + +uint64_t +media_height(const mtx::events::collections::TimelineEvents &event); + +uint64_t +media_width(const mtx::events::collections::TimelineEvents &event); +} diff --git a/src/InviteeItem.h b/src/InviteeItem.h
index 85ff7a63..582904b4 100644 --- a/src/InviteeItem.h +++ b/src/InviteeItem.h
@@ -3,7 +3,7 @@ #include <QLabel> #include <QWidget> -#include "mtx.hpp" +#include <mtx/identifiers.hpp> class QPushButton; diff --git a/src/Logging.cpp b/src/Logging.cpp
index 686274d8..5d64a630 100644 --- a/src/Logging.cpp +++ b/src/Logging.cpp
@@ -5,17 +5,56 @@ #include "spdlog/sinks/stdout_color_sinks.h" #include <iostream> +#include <QString> +#include <QtGlobal> + namespace { std::shared_ptr<spdlog::logger> db_logger = nullptr; std::shared_ptr<spdlog::logger> net_logger = nullptr; std::shared_ptr<spdlog::logger> crypto_logger = nullptr; std::shared_ptr<spdlog::logger> ui_logger = nullptr; +std::shared_ptr<spdlog::logger> qml_logger = nullptr; constexpr auto MAX_FILE_SIZE = 1024 * 1024 * 6; constexpr auto MAX_LOG_FILES = 3; + +void +qmlMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + std::string localMsg = msg.toStdString(); + const char *file = context.file ? context.file : ""; + const char *function = context.function ? context.function : ""; + + // Surpress binding wrning for now, as we can't set restore mode to keep compat with qt 5.10 + if (msg.endsWith( + "QML Binding: Not restoring previous value because restoreMode has not been set.This " + "behavior is deprecated.In Qt < 6.0 the default is Binding.RestoreBinding.In Qt >= " + "6.0 the default is Binding.RestoreBindingOrValue.")) + return; + + switch (type) { + case QtDebugMsg: + nhlog::qml()->debug("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtInfoMsg: + nhlog::qml()->info("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtWarningMsg: + nhlog::qml()->warn("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtCriticalMsg: + nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + case QtFatalMsg: + nhlog::qml()->critical("{} ({}:{}, {})", localMsg, file, context.line, function); + break; + } +} } namespace nhlog { +bool enable_debug_log_from_commandline = false; + void init(const std::string &file_path) { @@ -33,12 +72,15 @@ init(const std::string &file_path) db_logger = std::make_shared<spdlog::logger>("db", std::begin(sinks), std::end(sinks)); crypto_logger = std::make_shared<spdlog::logger>("crypto", std::begin(sinks), std::end(sinks)); + qml_logger = std::make_shared<spdlog::logger>("qml", std::begin(sinks), std::end(sinks)); if (nheko::enable_debug_log) { db_logger->set_level(spdlog::level::trace); ui_logger->set_level(spdlog::level::trace); crypto_logger->set_level(spdlog::level::trace); } + + qInstallMessageHandler(qmlMessageHandler); } std::shared_ptr<spdlog::logger> @@ -64,4 +106,10 @@ crypto() { return crypto_logger; } + +std::shared_ptr<spdlog::logger> +qml() +{ + return qml_logger; +} } diff --git a/src/Logging.h b/src/Logging.h
index 2feae60d..f572afae 100644 --- a/src/Logging.h +++ b/src/Logging.h
@@ -18,4 +18,9 @@ db(); std::shared_ptr<spdlog::logger> crypto(); + +std::shared_ptr<spdlog::logger> +qml(); + +extern bool enable_debug_log_from_commandline; } diff --git a/src/LoginPage.cpp b/src/LoginPage.cpp
index f702832f..20fb3888 100644 --- a/src/LoginPage.cpp +++ b/src/LoginPage.cpp
@@ -15,11 +15,14 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <QPainter> #include <QStyleOption> #include <mtx/identifiers.hpp> +#include <mtx/responses/login.hpp> #include "Config.h" +#include "Logging.h" #include "LoginPage.h" #include "MatrixClient.h" #include "ui/FlatButton.h" @@ -108,7 +111,7 @@ LoginPage::LoginPage(QWidget *parent) form_layout_->addLayout(matrixidLayout_); form_layout_->addWidget(password_input_); - form_layout_->addWidget(deviceName_, Qt::AlignHCenter, 0); + form_layout_->addWidget(deviceName_, Qt::AlignHCenter, nullptr); form_layout_->addLayout(serverLayout_); button_layout_ = new QHBoxLayout(); @@ -128,6 +131,7 @@ LoginPage::LoginPage(QWidget *parent) error_label_ = new QLabel(this); error_label_->setFont(font); + error_label_->setWordWrap(true); top_layout_->addLayout(top_bar_layout_); top_layout_->addStretch(1); @@ -186,7 +190,37 @@ LoginPage::onMatrixIdEntered() serverInput_->setText(homeServer); http::client()->set_server(user.hostname()); - checkHomeserverVersion(); + http::client()->well_known([this](const mtx::responses::WellKnown &res, + mtx::http::RequestErr err) { + if (err) { + using namespace boost::beast::http; + + if (err->status_code == status::not_found) { + nhlog::net()->info("Autodiscovery: No .well-known."); + checkHomeserverVersion(); + return; + } + + if (!err->parse_error.empty()) { + emit versionErrorCb( + tr("Autodiscovery failed. Received malformed response.")); + nhlog::net()->error( + "Autodiscovery failed. Received malformed response."); + return; + } + + emit versionErrorCb(tr("Autodiscovery failed. Unknown error when " + "requesting .well-known.")); + nhlog::net()->error("Autodiscovery failed. Unknown error when " + "requesting .well-known."); + return; + } + + nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + + "'"); + http::client()->set_server(res.homeserver.base_url); + checkHomeserverVersion(); + }); } } @@ -272,7 +306,6 @@ LoginPage::onLoginButtonClicked() if (password_input_->text().isEmpty()) return loginError(tr("Empty password")); - http::client()->set_server(serverInput_->text().toStdString()); http::client()->login( user.localpart(), password_input_->text().toStdString(), @@ -285,6 +318,12 @@ LoginPage::onLoginButtonClicked() return; } + if (res.well_known) { + http::client()->set_server(res.well_known->homeserver.base_url); + nhlog::net()->info("Login requested to user server: " + + res.well_known->homeserver.base_url); + } + emit loginOk(res); }); diff --git a/src/LoginPage.h b/src/LoginPage.h
index 99c249b1..4b84abfc 100644 --- a/src/LoginPage.h +++ b/src/LoginPage.h
@@ -38,7 +38,7 @@ class LoginPage : public QWidget Q_OBJECT public: - LoginPage(QWidget *parent = 0); + LoginPage(QWidget *parent = nullptr); void reset(); diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 7d9a8902..fb64f0fe 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp
@@ -23,6 +23,7 @@ #include <mtx/requests.hpp> +#include "Cache.h" #include "ChatPage.h" #include "Config.h" #include "Logging.h" @@ -30,6 +31,7 @@ #include "MainWindow.h" #include "MatrixClient.h" #include "RegisterPage.h" +#include "Splitter.h" #include "TrayIcon.h" #include "UserSettingsPage.h" #include "Utils.h" @@ -64,7 +66,7 @@ MainWindow::MainWindow(QWidget *parent) setFont(font); userSettings_ = QSharedPointer<UserSettings>(new UserSettings); - trayIcon_ = new TrayIcon(":/logos/nheko-32.png", this); + trayIcon_ = new TrayIcon(":/logos/nheko.svg", this); welcome_page_ = new WelcomePage(this); login_page_ = new LoginPage(this); @@ -113,9 +115,6 @@ MainWindow::MainWindow(QWidget *parent) connect( userSettingsPage_, SIGNAL(trayOptionChanged(bool)), trayIcon_, SLOT(setVisible(bool))); - connect(userSettingsPage_, &UserSettingsPage::themeChanged, this, []() { - Cache::clearUserColors(); - }); connect( userSettingsPage_, &UserSettingsPage::themeChanged, chat_page_, &ChatPage::themeChanged); connect(trayIcon_, @@ -190,7 +189,7 @@ MainWindow::resizeEvent(QResizeEvent *event) void MainWindow::adjustSideBars() { - const auto sz = utils::calculateSidebarSizes(QFont{}); + const auto sz = splitter::calculateSidebarSizes(QFont{}); const uint64_t timelineWidth = chat_page_->timelineWidth(); const uint64_t minAvailableWidth = sz.collapsePoint + sz.groups; @@ -444,7 +443,7 @@ MainWindow::openReadReceiptsDialog(const QString &event_id) const auto room_id = chat_page_->currentRoom(); try { - dialog->addUsers(cache::client()->readReceipts(event_id, room_id)); + dialog->addUsers(cache::readReceipts(event_id, room_id)); } catch (const lmdb::error &e) { nhlog::db()->warn("failed to retrieve read receipts for {} {}", event_id.toStdString(), @@ -507,4 +506,34 @@ MainWindow::loadJdenticonPlugin() nhlog::ui()->info("jdenticon plugin not found."); return false; -} \ No newline at end of file +} +void +MainWindow::showWelcomePage() +{ + removeOverlayProgressBar(); + pageStack_->addWidget(welcome_page_); + pageStack_->setCurrentWidget(welcome_page_); +} + +void +MainWindow::showLoginPage() +{ + if (modal_) + modal_->hide(); + + pageStack_->addWidget(login_page_); + pageStack_->setCurrentWidget(login_page_); +} + +void +MainWindow::showRegisterPage() +{ + pageStack_->addWidget(register_page_); + pageStack_->setCurrentWidget(register_page_); +} + +void +MainWindow::showUserSettingsPage() +{ + pageStack_->setCurrentWidget(userSettingsPage_); +} diff --git a/src/MainWindow.h b/src/MainWindow.h
index 1aadbf4d..e3e04698 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h
@@ -24,16 +24,17 @@ #include <QStackedWidget> #include <QSystemTrayIcon> -#include "LoginPage.h" -#include "RegisterPage.h" #include "UserSettingsPage.h" -#include "WelcomePage.h" #include "dialogs/UserProfile.h" #include "ui/OverlayModal.h" #include "jdenticoninterface.h" class ChatPage; +class RegisterPage; +class LoginPage; +class WelcomePage; + class LoadingIndicator; class OverlayModal; class SnackBar; @@ -62,7 +63,7 @@ class MainWindow : public QMainWindow Q_OBJECT public: - explicit MainWindow(QWidget *parent = 0); + explicit MainWindow(QWidget *parent = nullptr); static MainWindow *instance() { return instance_; }; void saveCurrentWindowSize(); @@ -97,32 +98,16 @@ private slots: void iconActivated(QSystemTrayIcon::ActivationReason reason); //! Show the welcome page in the main window. - void showWelcomePage() - { - removeOverlayProgressBar(); - pageStack_->addWidget(welcome_page_); - pageStack_->setCurrentWidget(welcome_page_); - } + void showWelcomePage(); //! Show the login page in the main window. - void showLoginPage() - { - if (modal_) - modal_->hide(); - - pageStack_->addWidget(login_page_); - pageStack_->setCurrentWidget(login_page_); - } + void showLoginPage(); //! Show the register page in the main window. - void showRegisterPage() - { - pageStack_->addWidget(register_page_); - pageStack_->setCurrentWidget(register_page_); - } + void showRegisterPage(); //! Show user settings page. - void showUserSettingsPage() { pageStack_->setCurrentWidget(userSettingsPage_); } + void showUserSettingsPage(); //! Show the chat page and start communicating with the given access token. void showChatPage(); diff --git a/src/MatrixClient.cpp b/src/MatrixClient.cpp
index 12d7ac91..b69ba480 100644 --- a/src/MatrixClient.cpp +++ b/src/MatrixClient.cpp
@@ -2,6 +2,26 @@ #include <memory> +#include <QMetaType> +#include <QObject> +#include <QString> + +#include "nlohmann/json.hpp" +#include <mtx/responses.hpp> + +Q_DECLARE_METATYPE(mtx::responses::Login) +Q_DECLARE_METATYPE(mtx::responses::Messages) +Q_DECLARE_METATYPE(mtx::responses::Notifications) +Q_DECLARE_METATYPE(mtx::responses::Rooms) +Q_DECLARE_METATYPE(mtx::responses::Sync) +Q_DECLARE_METATYPE(mtx::responses::JoinedGroups) +Q_DECLARE_METATYPE(mtx::responses::GroupProfile) + +Q_DECLARE_METATYPE(nlohmann::json) +Q_DECLARE_METATYPE(std::string) +Q_DECLARE_METATYPE(std::vector<std::string>) +Q_DECLARE_METATYPE(std::vector<QString>) + namespace { auto client_ = std::make_shared<mtx::http::Client>(); } diff --git a/src/MatrixClient.h b/src/MatrixClient.h
index 2af57267..4db51095 100644 --- a/src/MatrixClient.h +++ b/src/MatrixClient.h
@@ -1,35 +1,7 @@ #pragma once -#include <QMetaType> -#include <QObject> -#include <QString> - -#include "nlohmann/json.hpp" -#include <mtx/responses.hpp> #include <mtxclient/http/client.hpp> -Q_DECLARE_METATYPE(mtx::responses::Login) -Q_DECLARE_METATYPE(mtx::responses::Messages) -Q_DECLARE_METATYPE(mtx::responses::Notifications) -Q_DECLARE_METATYPE(mtx::responses::Rooms) -Q_DECLARE_METATYPE(mtx::responses::Sync) -Q_DECLARE_METATYPE(mtx::responses::JoinedGroups) -Q_DECLARE_METATYPE(mtx::responses::GroupProfile) -Q_DECLARE_METATYPE(std::string) -Q_DECLARE_METATYPE(nlohmann::json) -Q_DECLARE_METATYPE(std::vector<std::string>) -Q_DECLARE_METATYPE(std::vector<QString>) - -class MediaProxy : public QObject -{ - Q_OBJECT - -signals: - void imageDownloaded(const QPixmap &); - void imageSaved(const QString &, const QByteArray &); - void fileDownloaded(const QByteArray &); -}; - namespace http { mtx::http::Client * client(); diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp new file mode 100644
index 00000000..d04eab24 --- /dev/null +++ b/src/MxcImageProvider.cpp
@@ -0,0 +1,85 @@ +#include "MxcImageProvider.h" + +#include "Cache.h" +#include "Logging.h" +#include "MatrixClient.h" + +void +MxcImageResponse::run() +{ + if (m_requestedSize.isValid() && !m_encryptionInfo) { + QString fileName = QString("%1_%2x%3_crop") + .arg(m_id) + .arg(m_requestedSize.width()) + .arg(m_requestedSize.height()); + + auto data = cache::image(fileName); + if (!data.isNull() && m_image.loadFromData(data)) { + m_image = m_image.scaled(m_requestedSize, Qt::KeepAspectRatio); + m_image.setText("mxc url", "mxc://" + m_id); + emit finished(); + return; + } + + mtx::http::ThumbOpts opts; + opts.mxc_url = "mxc://" + m_id.toStdString(); + opts.width = m_requestedSize.width() > 0 ? m_requestedSize.width() : -1; + opts.height = m_requestedSize.height() > 0 ? m_requestedSize.height() : -1; + opts.method = "crop"; + http::client()->get_thumbnail( + opts, [this, fileName](const std::string &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("Failed to download image {}", + m_id.toStdString()); + m_error = "Failed download"; + emit finished(); + + return; + } + + auto data = QByteArray(res.data(), res.size()); + cache::saveImage(fileName, data); + m_image.loadFromData(data); + m_image.setText("mxc url", "mxc://" + m_id); + + emit finished(); + }); + } else { + auto data = cache::image(m_id); + if (!data.isNull() && m_image.loadFromData(data)) { + m_image.setText("mxc url", "mxc://" + m_id); + emit finished(); + return; + } + + http::client()->download( + "mxc://" + m_id.toStdString(), + [this](const std::string &res, + const std::string &, + const std::string &originalFilename, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("Failed to download image {}", + m_id.toStdString()); + m_error = "Failed download"; + emit finished(); + + return; + } + + auto temp = res; + if (m_encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, m_encryptionInfo.value())); + + auto data = QByteArray(temp.data(), temp.size()); + m_image.loadFromData(data); + m_image.setText("original filename", + QString::fromStdString(originalFilename)); + m_image.setText("mxc url", "mxc://" + m_id); + cache::saveImage(m_id, data); + + emit finished(); + }); + } +} diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h new file mode 100644
index 00000000..2c197a13 --- /dev/null +++ b/src/MxcImageProvider.h
@@ -0,0 +1,69 @@ +#pragma once + +#include <QQuickAsyncImageProvider> +#include <QQuickImageResponse> + +#include <QImage> +#include <QThreadPool> + +#include <mtx/common.hpp> + +#include <boost/optional.hpp> + +class MxcImageResponse + : public QQuickImageResponse + , public QRunnable +{ +public: + MxcImageResponse(const QString &id, + const QSize &requestedSize, + boost::optional<mtx::crypto::EncryptedFile> encryptionInfo) + : m_id(id) + , m_requestedSize(requestedSize) + , m_encryptionInfo(encryptionInfo) + { + setAutoDelete(false); + } + + QQuickTextureFactory *textureFactory() const override + { + return QQuickTextureFactory::textureFactoryForImage(m_image); + } + QString errorString() const override { return m_error; } + + void run() override; + + QString m_id, m_error; + QSize m_requestedSize; + QImage m_image; + boost::optional<mtx::crypto::EncryptedFile> m_encryptionInfo; +}; + +class MxcImageProvider + : public QObject + , public QQuickAsyncImageProvider +{ + Q_OBJECT +public slots: + QQuickImageResponse *requestImageResponse(const QString &id, + const QSize &requestedSize) override + { + boost::optional<mtx::crypto::EncryptedFile> info; + auto temp = infos.find("mxc://" + id); + if (temp != infos.end()) + info = *temp; + + MxcImageResponse *response = new MxcImageResponse(id, requestedSize, info); + pool.start(response); + return response; + } + + void addEncryptionInfo(mtx::crypto::EncryptedFile info) + { + infos.insert(QString::fromStdString(info.url), info); + } + +private: + QThreadPool pool; + QHash<QString, mtx::crypto::EncryptedFile> infos; +}; diff --git a/src/Olm.cpp b/src/Olm.cpp
index c1598570..78b16be7 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp
@@ -1,4 +1,4 @@ -#include <boost/variant.hpp> +#include <variant> #include "Olm.h" @@ -121,7 +121,7 @@ handle_pre_key_olm_message(const std::string &sender, // We also remove the one time key used to establish that // session so we'll have to update our copy of the account object. - cache::client()->saveOlmAccount(olm::client()->save("secret")); + cache::saveOlmAccount(olm::client()->save("secret")); } catch (const mtx::crypto::olm_exception &e) { nhlog::crypto()->critical( "failed to create inbound session with {}: {}", sender, e.what()); @@ -149,7 +149,7 @@ handle_pre_key_olm_message(const std::string &sender, nhlog::crypto()->debug("decrypted message: \n {}", plaintext.dump(2)); try { - cache::client()->saveOlmSession(sender_key, std::move(inbound_session)); + cache::saveOlmSession(sender_key, std::move(inbound_session)); } catch (const lmdb::error &e) { nhlog::db()->warn( "failed to save inbound olm session from {}: {}", sender, e.what()); @@ -159,15 +159,20 @@ handle_pre_key_olm_message(const std::string &sender, } mtx::events::msg::Encrypted -encrypt_group_message(const std::string &room_id, - const std::string &device_id, - const std::string &body) +encrypt_group_message(const std::string &room_id, const std::string &device_id, nlohmann::json body) { using namespace mtx::events; - // Always chech before for existence. - auto res = cache::client()->getOutboundMegolmSession(room_id); - auto payload = olm::client()->encrypt_group_message(res.session, body); + // relations shouldn't be encrypted... + mtx::common::RelatesTo relation; + if (body["content"].count("m.relates_to") != 0) { + relation = body["content"]["m.relates_to"]; + body["content"].erase("m.relates_to"); + } + + // Always check before for existence. + auto res = cache::getOutboundMegolmSession(room_id); + auto payload = olm::client()->encrypt_group_message(res.session, body.dump()); // Prepare the m.room.encrypted event. msg::Encrypted data; @@ -176,12 +181,13 @@ encrypt_group_message(const std::string &room_id, data.session_id = res.data.session_id; data.device_id = device_id; data.algorithm = MEGOLM_ALGO; + data.relates_to = relation; auto message_index = olm_outbound_group_session_message_index(res.session); nhlog::crypto()->info("next message_index {}", message_index); // We need to re-pickle the session after we send a message to save the new message_index. - cache::client()->updateOutboundMegolmSession(room_id, message_index); + cache::updateOutboundMegolmSession(room_id, message_index); return data; } @@ -189,13 +195,13 @@ encrypt_group_message(const std::string &room_id, nlohmann::json try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCipherContent &msg) { - auto session_ids = cache::client()->getOlmSessions(sender_key); + auto session_ids = cache::getOlmSessions(sender_key); nhlog::crypto()->info("attempt to decrypt message with {} known session_ids", session_ids.size()); for (const auto &id : session_ids) { - auto session = cache::client()->getOlmSession(sender_key, id); + auto session = cache::getOlmSession(sender_key, id); if (!session) continue; @@ -204,7 +210,7 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip try { text = olm::client()->decrypt_message(session->get(), msg.type, msg.body); - cache::client()->saveOlmSession(id, std::move(session.value())); + cache::saveOlmSession(id, std::move(session.value())); } catch (const mtx::crypto::olm_exception &e) { nhlog::crypto()->debug("failed to decrypt olm message ({}, {}) with {}: {}", msg.type, @@ -252,7 +258,7 @@ create_inbound_megolm_session(const std::string &sender, try { auto megolm_session = olm::client()->init_inbound_group_session(session_key); - cache::client()->saveInboundMegolmSession(index, std::move(megolm_session)); + cache::saveInboundMegolmSession(index, std::move(megolm_session)); } catch (const lmdb::error &e) { nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what()); return; @@ -268,7 +274,7 @@ void mark_keys_as_published() { olm::client()->mark_keys_as_published(); - cache::client()->saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY)); + cache::saveOlmAccount(olm::client()->save(STORAGE_SECRET_KEY)); } void @@ -289,14 +295,13 @@ request_keys(const std::string &room_id, const std::string &event_id) return; } - if (boost::get<EncryptedEvent<msg::Encrypted>>(&res) == nullptr) { + if (!std::holds_alternative<EncryptedEvent<msg::Encrypted>>(res)) { nhlog::net()->info( "retrieved event is not encrypted: {} from {}", event_id, room_id); return; } - olm::send_key_request_for(room_id, - boost::get<EncryptedEvent<msg::Encrypted>>(res)); + olm::send_key_request_for(room_id, std::get<EncryptedEvent<msg::Encrypted>>(res)); }); } @@ -356,13 +361,13 @@ handle_key_request_message(const mtx::events::msg::KeyRequest &req) } // Check if we have the keys for the requested session. - if (!cache::client()->outboundMegolmSessionExists(req.room_id)) { + if (!cache::outboundMegolmSessionExists(req.room_id)) { nhlog::crypto()->warn("requested session not found in room: {}", req.room_id); return; } // Check that the requested session_id and the one we have saved match. - const auto session = cache::client()->getOutboundMegolmSession(req.room_id); + const auto session = cache::getOutboundMegolmSession(req.room_id); if (req.session_id != session.data.session_id) { nhlog::crypto()->warn("session id of retrieved session doesn't match the request: " "requested({}), ours({})", @@ -371,7 +376,7 @@ handle_key_request_message(const mtx::events::msg::KeyRequest &req) return; } - if (!cache::client()->isRoomMember(req.sender, req.room_id)) { + if (!cache::isRoomMember(req.sender, req.room_id)) { nhlog::crypto()->warn( "user {} that requested the session key is not member of the room {}", req.sender, @@ -510,8 +515,7 @@ send_megolm_key_to_device(const std::string &user_id, device_msg = olm::client()->create_olm_encrypted_content( olm_session.get(), room_key, pks.curve25519); - cache::client()->saveOlmSession(pks.curve25519, - std::move(olm_session)); + cache::saveOlmSession(pks.curve25519, std::move(olm_session)); } catch (const json::exception &e) { nhlog::crypto()->warn("creating outbound session: {}", e.what()); diff --git a/src/Olm.h b/src/Olm.h
index ae4e0659..28521413 100644 --- a/src/Olm.h +++ b/src/Olm.h
@@ -3,7 +3,8 @@ #include <boost/optional.hpp> #include <memory> -#include <mtx.hpp> +#include <mtx/events.hpp> +#include <mtx/events/encrypted.hpp> #include <mtxclient/crypto/client.hpp> constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2"; @@ -62,7 +63,7 @@ handle_pre_key_olm_message(const std::string &sender, mtx::events::msg::Encrypted encrypt_group_message(const std::string &room_id, const std::string &device_id, - const std::string &body); + nlohmann::json body); void mark_keys_as_published(); diff --git a/src/QuickSwitcher.cpp b/src/QuickSwitcher.cpp
index eb79a427..05a9f431 100644 --- a/src/QuickSwitcher.cpp +++ b/src/QuickSwitcher.cpp
@@ -22,8 +22,11 @@ #include <QTimer> #include <QtConcurrent> +#include "Cache.h" #include "QuickSwitcher.h" -#include "SuggestionsPopup.h" +#include "popups/SuggestionsPopup.h" + +Q_DECLARE_METATYPE(std::vector<RoomSearchResult>) RoomSearchInput::RoomSearchInput(QWidget *parent) : TextField(parent) @@ -93,8 +96,7 @@ QuickSwitcher::QuickSwitcher(QWidget *parent) QtConcurrent::run([this, query = query.toLower()]() { try { - emit queryResults( - cache::client()->searchRooms(query.toStdString())); + emit queryResults(cache::searchRooms(query.toStdString())); } catch (const lmdb::error &e) { qWarning() << "room search failed:" << e.what(); } diff --git a/src/QuickSwitcher.h b/src/QuickSwitcher.h
index 24b9adfa..5bc31650 100644 --- a/src/QuickSwitcher.h +++ b/src/QuickSwitcher.h
@@ -22,11 +22,9 @@ #include <QVBoxLayout> #include <QWidget> -#include "SuggestionsPopup.h" +#include "popups/SuggestionsPopup.h" #include "ui/TextField.h" -Q_DECLARE_METATYPE(std::vector<RoomSearchResult>) - class RoomSearchInput : public TextField { Q_OBJECT diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp
index fdb0f43a..39a69a34 100644 --- a/src/RegisterPage.cpp +++ b/src/RegisterPage.cpp
@@ -15,9 +15,13 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <QMetaType> +#include <QPainter> #include <QStyleOption> #include <QTimer> +#include <mtx/responses/register.hpp> + #include "Config.h" #include "Logging.h" #include "MainWindow.h" @@ -27,11 +31,17 @@ #include "ui/RaisedButton.h" #include "ui/TextField.h" +#include "dialogs/FallbackAuth.h" #include "dialogs/ReCaptcha.h" +Q_DECLARE_METATYPE(mtx::user_interactive::Unauthorized) +Q_DECLARE_METATYPE(mtx::user_interactive::Auth) + RegisterPage::RegisterPage(QWidget *parent) : QWidget(parent) { + qRegisterMetaType<mtx::user_interactive::Unauthorized>(); + qRegisterMetaType<mtx::user_interactive::Auth>(); top_layout_ = new QVBoxLayout(); back_layout_ = new QHBoxLayout(); @@ -87,10 +97,10 @@ RegisterPage::RegisterPage(QWidget *parent) server_input_ = new TextField(); server_input_->setLabel(tr("Home Server")); - form_layout_->addWidget(username_input_, Qt::AlignHCenter, 0); - form_layout_->addWidget(password_input_, Qt::AlignHCenter, 0); - form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter, 0); - form_layout_->addWidget(server_input_, Qt::AlignHCenter, 0); + form_layout_->addWidget(username_input_, Qt::AlignHCenter, nullptr); + form_layout_->addWidget(password_input_, Qt::AlignHCenter, nullptr); + form_layout_->addWidget(password_confirmation_, Qt::AlignHCenter, nullptr); + form_layout_->addWidget(server_input_, Qt::AlignHCenter, nullptr); button_layout_ = new QHBoxLayout(); button_layout_->setSpacing(0); @@ -130,46 +140,139 @@ RegisterPage::RegisterPage(QWidget *parent) this, &RegisterPage::registrationFlow, this, - [this](const std::string &user, const std::string &pass, const std::string &session) { - emit errorOccurred(); + [this](const std::string &user, + const std::string &pass, + const mtx::user_interactive::Unauthorized &unauthorized) { + auto completed_stages = unauthorized.completed; + auto flows = unauthorized.flows; + auto session = unauthorized.session; + + nhlog::ui()->info("Completed stages: {}", completed_stages.size()); + + if (!completed_stages.empty()) + flows.erase(std::remove_if( + flows.begin(), + flows.end(), + [completed_stages](auto flow) { + if (completed_stages.size() > flow.stages.size()) + return true; + for (size_t f = 0; f < completed_stages.size(); f++) + if (completed_stages[f] != flow.stages[f]) + return true; + return false; + }), + flows.end()); + + if (flows.empty()) { + nhlog::net()->error("No available registration flows!"); + emit registerErrorCb(tr("No supported registration flows!")); + return; + } + + auto current_stage = flows.front().stages.at(completed_stages.size()); + + if (current_stage == mtx::user_interactive::auth_types::recaptcha) { + auto captchaDialog = + new dialogs::ReCaptcha(QString::fromStdString(session), this); + + connect(captchaDialog, + &dialogs::ReCaptcha::confirmation, + this, + [this, user, pass, session, captchaDialog]() { + captchaDialog->close(); + captchaDialog->deleteLater(); - auto captchaDialog = - new dialogs::ReCaptcha(QString::fromStdString(session), this); + emit registerAuth( + user, + pass, + mtx::user_interactive::Auth{ + session, mtx::user_interactive::auth::Fallback{}}); + }); + connect(captchaDialog, + &dialogs::ReCaptcha::cancel, + this, + &RegisterPage::errorOccurred); - connect(captchaDialog, - &dialogs::ReCaptcha::confirmation, - this, - [this, user, pass, session, captchaDialog]() { - captchaDialog->close(); - captchaDialog->deleteLater(); + QTimer::singleShot( + 1000, this, [captchaDialog]() { captchaDialog->show(); }); + } else if (current_stage == mtx::user_interactive::auth_types::dummy) { + emit registerAuth(user, + pass, + mtx::user_interactive::Auth{ + session, mtx::user_interactive::auth::Dummy{}}); + } else { + // use fallback + auto dialog = + new dialogs::FallbackAuth(QString::fromStdString(current_stage), + QString::fromStdString(session), + this); + + connect(dialog, + &dialogs::FallbackAuth::confirmation, + this, + [this, user, pass, session, dialog]() { + dialog->close(); + dialog->deleteLater(); + + emit registerAuth( + user, + pass, + mtx::user_interactive::Auth{ + session, mtx::user_interactive::auth::Fallback{}}); + }); + connect(dialog, + &dialogs::FallbackAuth::cancel, + this, + &RegisterPage::errorOccurred); + + dialog->show(); + } + }); + + connect( + this, + &RegisterPage::registerAuth, + this, + [this](const std::string &user, + const std::string &pass, + const mtx::user_interactive::Auth &auth) { + http::client()->registration( + user, + pass, + auth, + [this, user, pass](const mtx::responses::Register &res, + mtx::http::RequestErr err) { + if (!err) { + http::client()->set_user(res.user_id); + http::client()->set_access_token(res.access_token); - emit registering(); + emit registerOk(); + return; + } - http::client()->flow_response( - user, - pass, - session, - "m.login.recaptcha", - [this](const mtx::responses::Register &res, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to retrieve registration flows: {}", - err->matrix_error.error); - emit errorOccurred(); - emit registerErrorCb(QString::fromStdString( - err->matrix_error.error)); - return; - } + // The server requires registration flows. + if (err->status_code == boost::beast::http::status::unauthorized) { + if (err->matrix_error.unauthorized.session.empty()) { + nhlog::net()->warn( + "failed to retrieve registration flows: ({}) " + "{}", + static_cast<int>(err->status_code), + err->matrix_error.error); + emit registerErrorCb( + QString::fromStdString(err->matrix_error.error)); + return; + } - http::client()->set_user(res.user_id); - http::client()->set_access_token(res.access_token); + emit registrationFlow( + user, pass, err->matrix_error.unauthorized); + return; + } - emit registerOk(); - }); - }); + nhlog::net()->warn("failed to register: status_code ({})", + static_cast<int>(err->status_code)); - QTimer::singleShot(1000, this, [captchaDialog]() { captchaDialog->show(); }); + emit registerErrorCb(QString::fromStdString(err->matrix_error.error)); + }); }); setLayout(top_layout_); @@ -222,31 +325,27 @@ RegisterPage::onRegisterButtonClicked() // The server requires registration flows. if (err->status_code == boost::beast::http::status::unauthorized) { - http::client()->flow_register( - username, - password, - [this, username, password]( - const mtx::responses::RegistrationFlows &res, - mtx::http::RequestErr err) { - if (res.session.empty() && err) { - nhlog::net()->warn( - "failed to retrieve registration flows: ({}) " - "{}", - static_cast<int>(err->status_code), - err->matrix_error.error); - emit errorOccurred(); - emit registerErrorCb(QString::fromStdString( - err->matrix_error.error)); - return; - } + if (err->matrix_error.unauthorized.session.empty()) { + nhlog::net()->warn( + "failed to retrieve registration flows: ({}) " + "{}", + static_cast<int>(err->status_code), + err->matrix_error.error); + emit errorOccurred(); + emit registerErrorCb( + QString::fromStdString(err->matrix_error.error)); + return; + } - emit registrationFlow(username, password, res.session); - }); + emit registrationFlow( + username, password, err->matrix_error.unauthorized); return; } - nhlog::net()->warn("failed to register: status_code ({})", - static_cast<int>(err->status_code)); + nhlog::net()->warn( + "failed to register: status_code ({}), matrix_error({})", + static_cast<int>(err->status_code), + err->matrix_error.error); emit registerErrorCb(QString::fromStdString(err->matrix_error.error)); emit errorOccurred(); diff --git a/src/RegisterPage.h b/src/RegisterPage.h
index b05cf150..ebc24bb1 100644 --- a/src/RegisterPage.h +++ b/src/RegisterPage.h
@@ -21,6 +21,8 @@ #include <QLayout> #include <memory> +#include <mtx/user_interactive.hpp> + class FlatButton; class RaisedButton; class TextField; @@ -30,7 +32,7 @@ class RegisterPage : public QWidget Q_OBJECT public: - RegisterPage(QWidget *parent = 0); + RegisterPage(QWidget *parent = nullptr); protected: void paintEvent(QPaintEvent *event) override; @@ -43,7 +45,10 @@ signals: void registerErrorCb(const QString &msg); void registrationFlow(const std::string &user, const std::string &pass, - const std::string &session); + const mtx::user_interactive::Unauthorized &unauthorized); + void registerAuth(const std::string &user, + const std::string &pass, + const mtx::user_interactive::Auth &auth); private slots: void onBackButtonClicked(); diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp
index f17b383c..4c8535bf 100644 --- a/src/RoomInfoListItem.cpp +++ b/src/RoomInfoListItem.cpp
@@ -16,13 +16,16 @@ */ #include <QDateTime> -#include <QDebug> #include <QMouseEvent> #include <QPainter> +#include <QSettings> +#include <QtGlobal> +#include "AvatarProvider.h" #include "Cache.h" #include "Config.h" #include "RoomInfoListItem.h" +#include "Splitter.h" #include "Utils.h" #include "ui/Menu.h" #include "ui/Ripple.h" @@ -30,9 +33,6 @@ constexpr int MaxUnreadCountDisplayed = 99; -constexpr int IconSize = 44; -// constexpr int MaxHeight = IconSize + 2 * Padding; - struct WidgetMetrics { int maxHeight; @@ -62,7 +62,7 @@ getMetrics(const QFont &font) m.unreadLineOffset = m.padding - m.padding / 4; m.inviteBtnX = m.iconSize + 2 * m.padding; - m.inviteBtnX = m.iconSize / 2.0 + m.padding + m.padding / 3.0; + m.inviteBtnY = m.iconSize / 2.0 + m.padding + m.padding / 3.0; return m; } @@ -74,7 +74,8 @@ RoomInfoListItem::init(QWidget *parent) setMouseTracking(true); setAttribute(Qt::WA_Hover); - setFixedHeight(getMetrics(QFont{}).maxHeight); + auto wm = getMetrics(QFont{}); + setFixedHeight(wm.maxHeight); QPainterPath path; path.addRect(0, 0, parent->width(), height()); @@ -83,6 +84,10 @@ RoomInfoListItem::init(QWidget *parent) ripple_overlay_->setClipPath(path); ripple_overlay_->setClipping(true); + avatar_ = new Avatar(this, wm.iconSize); + avatar_->setLetter(utils::firstChar(roomName_)); + avatar_->move(wm.padding, wm.padding); + unreadCountFont_.setPointSizeF(unreadCountFont_.pointSizeF() * 0.8); unreadCountFont_.setBold(true); @@ -94,7 +99,7 @@ RoomInfoListItem::init(QWidget *parent) menu_->addAction(leaveRoom_); } -RoomInfoListItem::RoomInfoListItem(QString room_id, RoomInfo info, QWidget *parent) +RoomInfoListItem::RoomInfoListItem(QString room_id, const RoomInfo &info, QWidget *parent) : QWidget(parent) , roomType_{info.is_invite ? RoomType::Invited : RoomType::Joined} , roomId_(std::move(room_id)) @@ -104,18 +109,6 @@ RoomInfoListItem::RoomInfoListItem(QString room_id, RoomInfo info, QWidget *pare , unreadHighlightedMsgCount_(0) { init(parent); - - QString emptyEventId; - - // HACK - // We use fake message info with an old date to pin - // the invite events to the top. - // - // State events in invited rooms don't contain timestamp info, - // so we can't use them for sorting. - if (roomType_ == RoomType::Invited) - lastMsgInfo_ = { - emptyEventId, "-", "-", "-", "-", QDateTime::currentDateTime().addYears(10)}; } void @@ -125,7 +118,7 @@ RoomInfoListItem::resizeEvent(QResizeEvent *) QPainterPath path; path.addRect(0, 0, width(), height()); - const auto sidebarSizes = utils::calculateSidebarSizes(QFont{}); + const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{}); if (width() > sidebarSizes.small) setToolTip(""); @@ -167,12 +160,10 @@ RoomInfoListItem::paintEvent(QPaintEvent *event) subtitlePen.setColor(subtitleColor_); } - QRect avatarRegion(wm.padding, wm.padding, wm.iconSize, wm.iconSize); - // Description line with the default font. int bottom_y = wm.maxHeight - wm.padding - metrics.ascent() / 2; - const auto sidebarSizes = utils::calculateSidebarSizes(QFont{}); + const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{}); if (width() > sidebarSizes.small) { QFont headingFont; @@ -182,8 +173,12 @@ RoomInfoListItem::paintEvent(QPaintEvent *event) QFont tsFont; tsFont.setPointSizeF(tsFont.pointSizeF() * 0.9); +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) const int msgStampWidth = QFontMetrics(tsFont).width(lastMsgInfo_.timestamp) + 4; - +#else + const int msgStampWidth = + QFontMetrics(tsFont).horizontalAdvance(lastMsgInfo_.timestamp) + 4; +#endif // We use the full width of the widget if there is no unread msg bubble. const int bottomLineWidthLimit = (unreadMsgCount_ > 0) ? msgStampWidth : 0; @@ -201,30 +196,11 @@ RoomInfoListItem::paintEvent(QPaintEvent *event) p.setFont(QFont{}); p.setPen(subtitlePen); - // The limit is the space between the end of the avatar and the start of the - // timestamp. - int usernameLimit = - std::max(0, width() - 3 * wm.padding - msgStampWidth - wm.iconSize - 20); - auto userName = - metrics.elidedText(lastMsgInfo_.username, Qt::ElideRight, usernameLimit); - - p.setFont(QFont{}); - p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), userName); - - int nameWidth = QFontMetrics(QFont{}).width(userName); - - p.setFont(QFont{}); - - // The limit is the space between the end of the username and the start of - // the timestamp. - int descriptionLimit = - std::max(0, - width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize - - nameWidth - 5); + int descriptionLimit = std::max( + 0, width() - 3 * wm.padding - bottomLineWidthLimit - wm.iconSize); auto description = metrics.elidedText(lastMsgInfo_.body, Qt::ElideRight, descriptionLimit); - p.drawText(QPoint(2 * wm.padding + wm.iconSize + nameWidth, bottom_y), - description); + p.drawText(QPoint(2 * wm.padding + wm.iconSize, bottom_y), description); // We show the last message timestamp. p.save(); @@ -263,42 +239,17 @@ RoomInfoListItem::paintEvent(QPaintEvent *event) p.setPen(QPen(btnTextColor_)); p.setFont(QFont{}); - p.drawText(acceptBtnRegion_, Qt::AlignCenter, tr("Accept")); - p.drawText(declineBtnRegion_, Qt::AlignCenter, tr("Decline")); + p.drawText(acceptBtnRegion_, + Qt::AlignCenter, + metrics.elidedText(tr("Accept"), Qt::ElideRight, btnWidth)); + p.drawText(declineBtnRegion_, + Qt::AlignCenter, + metrics.elidedText(tr("Decline"), Qt::ElideRight, btnWidth)); } } p.setPen(Qt::NoPen); - // We using the first letter of room's name. - if (roomAvatar_.isNull()) { - QBrush brush; - brush.setStyle(Qt::SolidPattern); - brush.setColor(avatarBgColor()); - - p.setPen(Qt::NoPen); - p.setBrush(brush); - - p.drawEllipse(avatarRegion.center(), wm.iconSize / 2, wm.iconSize / 2); - - QFont bubbleFont; - bubbleFont.setPointSizeF(bubbleFont.pointSizeF() * 1.4); - p.setFont(bubbleFont); - p.setPen(avatarFgColor()); - p.setBrush(Qt::NoBrush); - p.drawText( - avatarRegion.translated(0, -1), Qt::AlignCenter, utils::firstChar(roomName())); - } else { - p.save(); - - QPainterPath path; - path.addEllipse(wm.padding, wm.padding, wm.iconSize, wm.iconSize); - p.setClipPath(path); - - p.drawPixmap(avatarRegion, roomAvatar_); - p.restore(); - } - if (unreadMsgCount_ > 0) { QBrush brush; brush.setStyle(Qt::SolidPattern); @@ -426,10 +377,9 @@ RoomInfoListItem::mousePressEvent(QMouseEvent *event) } void -RoomInfoListItem::setAvatar(const QImage &img) +RoomInfoListItem::setAvatar(const QString &avatar_url) { - roomAvatar_ = utils::scaleImageToPixmap(img, IconSize); - update(); + avatar_->setImage(avatar_url); } void diff --git a/src/RoomInfoListItem.h b/src/RoomInfoListItem.h
index 40c938c1..c1ee533d 100644 --- a/src/RoomInfoListItem.h +++ b/src/RoomInfoListItem.h
@@ -22,9 +22,11 @@ #include <QSharedPointer> #include <QWidget> -#include "Cache.h" #include <mtx/responses.hpp> +#include "CacheStructs.h" +#include "ui/Avatar.h" + class Menu; class RippleOverlay; @@ -37,9 +39,6 @@ class RoomInfoListItem : public QWidget QColor hoverBackgroundColor READ hoverBackgroundColor WRITE setHoverBackgroundColor) Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor) - Q_PROPERTY(QColor avatarBgColor READ avatarBgColor WRITE setAvatarBgColor) - Q_PROPERTY(QColor avatarFgColor READ avatarFgColor WRITE setAvatarFgColor) - Q_PROPERTY(QColor bubbleBgColor READ bubbleBgColor WRITE setBubbleBgColor) Q_PROPERTY(QColor bubbleFgColor READ bubbleFgColor WRITE setBubbleFgColor) @@ -64,7 +63,7 @@ class RoomInfoListItem : public QWidget Q_PROPERTY(QColor btnTextColor READ btnTextColor WRITE setBtnTextColor) public: - RoomInfoListItem(QString room_id, RoomInfo info, QWidget *parent = 0); + RoomInfoListItem(QString room_id, const RoomInfo &info, QWidget *parent = nullptr); void updateUnreadMessageCount(int count, int highlightedCount); void clearUnreadMessageCount() { updateUnreadMessageCount(0, 0); }; @@ -73,7 +72,7 @@ public: bool isPressed() const { return isPressed_; } int unreadMessageCount() const { return unreadMsgCount_; } - void setAvatar(const QImage &avatar_image); + void setAvatar(const QString &avatar_url); void setDescriptionMessage(const DescInfo &info); DescInfo lastMessageInfo() const { return lastMsgInfo_; } @@ -83,8 +82,6 @@ public: QColor hoverSubtitleColor() const { return hoverSubtitleColor_; } QColor hoverTimestampColor() const { return hoverTimestampColor_; } QColor backgroundColor() const { return backgroundColor_; } - QColor avatarBgColor() const { return avatarBgColor_; } - QColor avatarFgColor() const { return avatarFgColor_; } QColor highlightedTitleColor() const { return highlightedTitleColor_; } QColor highlightedSubtitleColor() const { return highlightedSubtitleColor_; } @@ -107,8 +104,6 @@ public: void setHoverTimestampColor(QColor &color) { hoverTimestampColor_ = color; } void setBackgroundColor(QColor &color) { backgroundColor_ = color; } void setTimestampColor(QColor &color) { timestampColor_ = color; } - void setAvatarFgColor(QColor &color) { avatarFgColor_ = color; } - void setAvatarBgColor(QColor &color) { avatarBgColor_ = color; } void setHighlightedTitleColor(QColor &color) { highlightedTitleColor_ = color; } void setHighlightedSubtitleColor(QColor &color) { highlightedSubtitleColor_ = color; } @@ -162,6 +157,7 @@ private: QString roomName() { return roomName_; } RippleOverlay *ripple_overlay_; + Avatar *avatar_; enum class RoomType { @@ -179,8 +175,6 @@ private: DescInfo lastMsgInfo_; - QPixmap roomAvatar_; - Menu *menu_; QAction *leaveRoom_; @@ -218,9 +212,6 @@ private: QColor highlightedTimestampColor_; QColor hoverTimestampColor_; - QColor avatarBgColor_; - QColor avatarFgColor_; - QColor bubbleBgColor_; QColor bubbleFgColor_; }; diff --git a/src/RoomList.cpp b/src/RoomList.cpp
index 1abf3533..6feb4f76 100644 --- a/src/RoomList.cpp +++ b/src/RoomList.cpp
@@ -15,18 +15,17 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#include <QApplication> -#include <QBuffer> +#include <limits> + #include <QObject> +#include <QPainter> +#include <QScroller> #include <QTimer> -#include "Cache.h" #include "Logging.h" #include "MainWindow.h" -#include "MatrixClient.h" #include "RoomInfoListItem.h" #include "RoomList.h" -#include "UserSettingsPage.h" #include "Utils.h" #include "ui/OverlayModal.h" @@ -43,6 +42,8 @@ RoomList::RoomList(QWidget *parent) scrollArea_->setWidgetResizable(true); scrollArea_->setAlignment(Qt::AlignLeading | Qt::AlignTop | Qt::AlignVCenter); + QScroller::grabGesture(scrollArea_, QScroller::TouchGesture); + // The scrollbar on macOS will hide itself when not active so it won't interfere // with the content. #if not defined(Q_OS_MAC) @@ -89,40 +90,7 @@ RoomList::updateAvatar(const QString &room_id, const QString &url) if (url.isEmpty()) return; - QByteArray savedImgData; - - if (cache::client()) - savedImgData = cache::client()->image(url); - - if (savedImgData.isEmpty()) { - mtx::http::ThumbOpts opts; - opts.mxc_url = url.toStdString(); - http::client()->get_thumbnail( - opts, [room_id, opts, this](const std::string &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to download room avatar: {} {} {}", - opts.mxc_url, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - - if (cache::client()) - cache::client()->saveImage(opts.mxc_url, res); - - auto data = QByteArray(res.data(), res.size()); - QPixmap pixmap; - pixmap.loadFromData(data); - - emit updateRoomAvatarCb(room_id, pixmap); - }); - } else { - QPixmap img; - img.loadFromData(savedImgData); - - updateRoomAvatar(room_id, img); - } + emit updateRoomAvatarCb(room_id, url); } void @@ -193,6 +161,8 @@ RoomList::initialize(const QMap<QString, RoomInfo> &info) if (rooms_.empty()) return; + sortRoomsByLastMessage(); + auto room = firstRoom(); if (room.second.isNull()) return; @@ -224,6 +194,9 @@ RoomList::sync(const std::map<QString, RoomInfo> &info) { for (const auto &room : info) updateRoom(room.first, room.second); + + if (!info.empty()) + sortRoomsByLastMessage(); } void @@ -252,7 +225,73 @@ RoomList::highlightSelectedRoom(const QString &room_id) } void -RoomList::updateRoomAvatar(const QString &roomid, const QPixmap &img) +RoomList::nextRoom() +{ + for (int ii = 0; ii < contentsLayout_->count() - 1; ++ii) { + auto room = qobject_cast<RoomInfoListItem *>(contentsLayout_->itemAt(ii)->widget()); + + if (!room) + continue; + + if (room->roomId() == selectedRoom_) { + auto nextRoom = qobject_cast<RoomInfoListItem *>( + contentsLayout_->itemAt(ii + 1)->widget()); + + // Not a room message. + if (!nextRoom || nextRoom->isInvite()) + return; + + emit roomChanged(nextRoom->roomId()); + if (!roomExists(nextRoom->roomId())) { + nhlog::ui()->warn("roomlist: clicked unknown room_id"); + return; + } + + room->setPressedState(false); + nextRoom->setPressedState(true); + + scrollArea_->ensureWidgetVisible(nextRoom); + selectedRoom_ = nextRoom->roomId(); + return; + } + } +} + +void +RoomList::previousRoom() +{ + for (int ii = 1; ii < contentsLayout_->count(); ++ii) { + auto room = qobject_cast<RoomInfoListItem *>(contentsLayout_->itemAt(ii)->widget()); + + if (!room) + continue; + + if (room->roomId() == selectedRoom_) { + auto nextRoom = qobject_cast<RoomInfoListItem *>( + contentsLayout_->itemAt(ii - 1)->widget()); + + // Not a room message. + if (!nextRoom || nextRoom->isInvite()) + return; + + emit roomChanged(nextRoom->roomId()); + if (!roomExists(nextRoom->roomId())) { + nhlog::ui()->warn("roomlist: clicked unknown room_id"); + return; + } + + room->setPressedState(false); + nextRoom->setPressedState(true); + + scrollArea_->ensureWidgetVisible(nextRoom); + selectedRoom_ = nextRoom->roomId(); + return; + } + } +} + +void +RoomList::updateRoomAvatar(const QString &roomid, const QString &img) { if (!roomExists(roomid)) { nhlog::ui()->warn("avatar update on non-existent room_id: {}", @@ -260,7 +299,7 @@ RoomList::updateRoomAvatar(const QString &roomid, const QPixmap &img) return; } - rooms_[roomid]->setAvatar(img.toImage()); + rooms_[roomid]->setAvatar(img); // Used to inform other widgets for the new image data. emit roomAvatarChanged(roomid, img); @@ -303,7 +342,9 @@ RoomList::sortRoomsByLastMessage() continue; // Not a room message. - if (room->lastMessageInfo().userid.isEmpty()) + if (room->isInvite()) + times.emplace(std::numeric_limits<uint64_t>::max(), room); + else if (room->lastMessageInfo().userid.isEmpty()) times.emplace(0, room); else times.emplace(room->lastMessageInfo().datetime.toMSecsSinceEpoch(), room); @@ -443,13 +484,16 @@ RoomList::addInvitedRoom(const QString &room_id, const RoomInfo &info) std::pair<QString, QSharedPointer<RoomInfoListItem>> RoomList::firstRoom() const { - auto firstRoom = rooms_.begin(); + for (int i = 0; i < contentsLayout_->count(); i++) { + auto item = qobject_cast<RoomInfoListItem *>(contentsLayout_->itemAt(i)->widget()); - while (firstRoom->second.isNull() && firstRoom != rooms_.end()) - firstRoom++; + if (item) { + return std::pair<QString, QSharedPointer<RoomInfoListItem>>( + item->roomId(), rooms_.at(item->roomId())); + } + } - return std::pair<QString, QSharedPointer<RoomInfoListItem>>(firstRoom->first, - firstRoom->second); + return {}; } void diff --git a/src/RoomList.h b/src/RoomList.h
index 155a969c..fef552c6 100644 --- a/src/RoomList.h +++ b/src/RoomList.h
@@ -17,15 +17,12 @@ #pragma once -#include <QMetaType> #include <QPushButton> #include <QScrollArea> #include <QSharedPointer> #include <QVBoxLayout> #include <QWidget> -#include <mtx.hpp> - class LeaveRoomDialog; class OverlayModal; class RoomInfoListItem; @@ -38,7 +35,7 @@ class RoomList : public QWidget Q_OBJECT public: - explicit RoomList(QWidget *parent = 0); + explicit RoomList(QWidget *parent = nullptr); void initialize(const QMap<QString, RoomInfo> &info); void sync(const std::map<QString, RoomInfo> &info); @@ -61,17 +58,19 @@ signals: void totalUnreadMessageCountUpdated(int count); void acceptInvite(const QString &room_id); void declineInvite(const QString &room_id); - void roomAvatarChanged(const QString &room_id, const QPixmap &img); + void roomAvatarChanged(const QString &room_id, const QString &img); void joinRoom(const QString &room_id); - void updateRoomAvatarCb(const QString &room_id, const QPixmap &img); + void updateRoomAvatarCb(const QString &room_id, const QString &img); public slots: - void updateRoomAvatar(const QString &roomid, const QPixmap &img); + void updateRoomAvatar(const QString &roomid, const QString &img); void highlightSelectedRoom(const QString &room_id); void updateUnreadMessageCount(const QString &roomid, int count, int highlightedCount); void updateRoomDescription(const QString &roomid, const DescInfo &info); void closeJoinRoomDialog(bool isJoining, QString roomAlias); void updateReadStatus(const std::map<QString, bool> &status); + void nextRoom(); + void previousRoom(); protected: void paintEvent(QPaintEvent *event) override; diff --git a/src/RunGuard.cpp b/src/RunGuard.cpp deleted file mode 100644
index 75833eb7..00000000 --- a/src/RunGuard.cpp +++ /dev/null
@@ -1,84 +0,0 @@ -#include "RunGuard.h" - -#include <QCryptographicHash> - -namespace { - -QString -generateKeyHash(const QString &key, const QString &salt) -{ - QByteArray data; - - data.append(key.toUtf8()); - data.append(salt.toUtf8()); - data = QCryptographicHash::hash(data, QCryptographicHash::Sha1).toHex(); - - return data; -} -} - -RunGuard::RunGuard(const QString &key) - : key(key) - , memLockKey(generateKeyHash(key, "_memLockKey")) - , sharedmemKey(generateKeyHash(key, "_sharedmemKey")) - , sharedMem(sharedmemKey) - , memLock(memLockKey, 1) -{ - memLock.acquire(); - { - // Fix for *nix: http://habrahabr.ru/post/173281/ - QSharedMemory fix(sharedmemKey); - fix.attach(); - } - - memLock.release(); -} - -RunGuard::~RunGuard() { release(); } - -bool -RunGuard::isAnotherRunning() -{ - if (sharedMem.isAttached()) - return false; - - memLock.acquire(); - const bool isRunning = sharedMem.attach(); - - if (isRunning) - sharedMem.detach(); - - memLock.release(); - - return isRunning; -} - -bool -RunGuard::tryToRun() -{ - // Extra check - if (isAnotherRunning()) - return false; - - memLock.acquire(); - const bool result = sharedMem.create(sizeof(quint64)); - memLock.release(); - - if (!result) { - release(); - return false; - } - - return true; -} - -void -RunGuard::release() -{ - memLock.acquire(); - - if (sharedMem.isAttached()) - sharedMem.detach(); - - memLock.release(); -} diff --git a/src/RunGuard.h b/src/RunGuard.h deleted file mode 100644
index f9a9641a..00000000 --- a/src/RunGuard.h +++ /dev/null
@@ -1,31 +0,0 @@ -#pragma once - -// -// Taken from -// https://stackoverflow.com/questions/5006547/qt-best-practice-for-a-single-instance-app-protection -// - -#include <QObject> -#include <QSharedMemory> -#include <QSystemSemaphore> - -class RunGuard -{ -public: - RunGuard(const QString &key); - ~RunGuard(); - - bool isAnotherRunning(); - bool tryToRun(); - void release(); - -private: - const QString key; - const QString memLockKey; - const QString sharedmemKey; - - QSharedMemory sharedMem; - QSystemSemaphore memLock; - - Q_DISABLE_COPY(RunGuard) -}; diff --git a/src/SideBarActions.cpp b/src/SideBarActions.cpp
index 2f447cd8..4934ec05 100644 --- a/src/SideBarActions.cpp +++ b/src/SideBarActions.cpp
@@ -1,15 +1,15 @@ -#include <QDebug> #include <QIcon> +#include <QPainter> +#include <QResizeEvent> #include <mtx/requests.hpp> #include "Config.h" #include "MainWindow.h" #include "SideBarActions.h" -#include "Utils.h" +#include "Splitter.h" #include "ui/FlatButton.h" #include "ui/Menu.h" -#include "ui/OverlayModal.h" SideBarActions::SideBarActions(QWidget *parent) : QWidget{parent} @@ -93,7 +93,7 @@ SideBarActions::resizeEvent(QResizeEvent *event) { Q_UNUSED(event); - const auto sidebarSizes = utils::calculateSidebarSizes(QFont{}); + const auto sidebarSizes = splitter::calculateSidebarSizes(QFont{}); if (width() <= sidebarSizes.small) { roomDirectory_->hide(); diff --git a/src/SideBarActions.h b/src/SideBarActions.h
index ce96cba8..662750b3 100644 --- a/src/SideBarActions.h +++ b/src/SideBarActions.h
@@ -2,7 +2,6 @@ #include <QAction> #include <QHBoxLayout> -#include <QResizeEvent> #include <QWidget> namespace mtx { @@ -13,6 +12,7 @@ struct CreateRoom; class Menu; class FlatButton; +class QResizeEvent; class SideBarActions : public QWidget { diff --git a/src/Splitter.cpp b/src/Splitter.cpp
index ddb1dc1c..04375853 100644 --- a/src/Splitter.cpp +++ b/src/Splitter.cpp
@@ -15,24 +15,19 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#include <QApplication> -#include <QDebug> -#include <QDesktopWidget> #include <QSettings> -#include <QShortcut> -#include "Config.h" +#include "Logging.h" #include "Splitter.h" constexpr auto MaxWidth = (1 << 24) - 1; Splitter::Splitter(QWidget *parent) : QSplitter(parent) - , sz_{utils::calculateSidebarSizes(QFont{})} + , sz_{splitter::calculateSidebarSizes(QFont{})} { connect(this, &QSplitter::splitterMoved, this, &Splitter::onSplitterMoved); setChildrenCollapsible(false); - setStyleSheet("QSplitter::handle { image: none; }"); } void @@ -80,7 +75,7 @@ Splitter::onSplitterMoved(int pos, int index) auto s = sizes(); if (s.count() < 2) { - qWarning() << "Splitter needs at least two children"; + nhlog::ui()->warn("Splitter needs at least two children"); return; } @@ -165,3 +160,17 @@ Splitter::showFullRoomList() left->show(); left->setMaximumWidth(MaxWidth); } + +splitter::SideBarSizes +splitter::calculateSidebarSizes(const QFont &f) +{ + const auto height = static_cast<double>(QFontMetrics{f}.lineSpacing()); + + SideBarSizes sz; + sz.small = std::ceil(3.8 * height); + sz.normal = std::ceil(16 * height); + sz.groups = std::ceil(3 * height); + sz.collapsePoint = 2 * sz.normal; + + return sz; +} diff --git a/src/Splitter.h b/src/Splitter.h
index 14d6773e..7bde89de 100644 --- a/src/Splitter.h +++ b/src/Splitter.h
@@ -17,15 +17,27 @@ #pragma once -#include "Utils.h" #include <QSplitter> +namespace splitter { +struct SideBarSizes +{ + int small; + int normal; + int groups; + int collapsePoint; +}; + +SideBarSizes +calculateSidebarSizes(const QFont &f); +} + class Splitter : public QSplitter { Q_OBJECT public: explicit Splitter(QWidget *parent = nullptr); - ~Splitter(); + ~Splitter() override; void restoreSizes(int fallback); @@ -45,5 +57,5 @@ private: int leftMoveCount_ = 0; int rightMoveCount_ = 0; - utils::SideBarSizes sz_; + splitter::SideBarSizes sz_; }; diff --git a/src/SuggestionsPopup.cpp b/src/SuggestionsPopup.cpp deleted file mode 100644
index 952d2ef3..00000000 --- a/src/SuggestionsPopup.cpp +++ /dev/null
@@ -1,296 +0,0 @@ -#include <QPaintEvent> -#include <QPainter> -#include <QStyleOption> - -#include "Config.h" -#include "SuggestionsPopup.h" -#include "Utils.h" -#include "ui/Avatar.h" -#include "ui/DropShadow.h" - -constexpr int PopupHMargin = 4; -constexpr int PopupItemMargin = 3; - -PopupItem::PopupItem(QWidget *parent) - : QWidget(parent) - , avatar_{new Avatar(this)} - , hovering_{false} -{ - setMouseTracking(true); - setAttribute(Qt::WA_Hover); - - topLayout_ = new QHBoxLayout(this); - topLayout_->setContentsMargins( - PopupHMargin, PopupItemMargin, PopupHMargin, PopupItemMargin); - - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); -} - -void -PopupItem::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); - - if (underMouse() || hovering_) - p.fillRect(rect(), hoverColor_); -} - -UserItem::UserItem(QWidget *parent, const QString &user_id) - : PopupItem(parent) - , userId_{user_id} -{ - auto displayName = Cache::displayName(ChatPage::instance()->currentRoom(), userId_); - - avatar_->setSize(conf::popup::avatar); - avatar_->setLetter(utils::firstChar(displayName)); - - // If it's a matrix id we use the second letter. - if (displayName.size() > 1 && displayName.at(0) == '@') - avatar_->setLetter(QChar(displayName.at(1))); - - userName_ = new QLabel(displayName, this); - - topLayout_->addWidget(avatar_); - topLayout_->addWidget(userName_, 1); - - resolveAvatar(user_id); -} - -void -UserItem::updateItem(const QString &user_id) -{ - userId_ = user_id; - - auto displayName = Cache::displayName(ChatPage::instance()->currentRoom(), userId_); - - // If it's a matrix id we use the second letter. - if (displayName.size() > 1 && displayName.at(0) == '@') - avatar_->setLetter(QChar(displayName.at(1))); - else - avatar_->setLetter(utils::firstChar(displayName)); - - userName_->setText(displayName); - resolveAvatar(user_id); -} - -void -UserItem::resolveAvatar(const QString &user_id) -{ - AvatarProvider::resolve( - ChatPage::instance()->currentRoom(), userId_, this, [this, user_id](const QImage &img) { - // The user on the widget when the avatar is resolved, - // might be different from the user that made the call. - if (user_id == userId_) - avatar_->setImage(img); - else - // We try to resolve the avatar again. - resolveAvatar(userId_); - }); -} - -void -UserItem::mousePressEvent(QMouseEvent *event) -{ - if (event->buttons() != Qt::RightButton) - emit clicked( - Cache::displayName(ChatPage::instance()->currentRoom(), selectedText())); - - QWidget::mousePressEvent(event); -} - -RoomItem::RoomItem(QWidget *parent, const RoomSearchResult &res) - : PopupItem(parent) - , roomId_{QString::fromStdString(res.room_id)} -{ - auto name = QFontMetrics(QFont()).elidedText( - QString::fromStdString(res.info.name), Qt::ElideRight, parentWidget()->width() - 10); - - avatar_->setSize(conf::popup::avatar + 6); - avatar_->setLetter(utils::firstChar(name)); - - roomName_ = new QLabel(name, this); - roomName_->setMargin(0); - - topLayout_->addWidget(avatar_); - topLayout_->addWidget(roomName_, 1); - - if (!res.img.isNull()) - avatar_->setImage(res.img); -} - -void -RoomItem::updateItem(const RoomSearchResult &result) -{ - roomId_ = QString::fromStdString(std::move(result.room_id)); - - auto name = - QFontMetrics(QFont()).elidedText(QString::fromStdString(std::move(result.info.name)), - Qt::ElideRight, - parentWidget()->width() - 10); - - roomName_->setText(name); - - if (!result.img.isNull()) - avatar_->setImage(result.img); - else - avatar_->setLetter(utils::firstChar(name)); -} - -void -RoomItem::mousePressEvent(QMouseEvent *event) -{ - if (event->buttons() != Qt::RightButton) - emit clicked(selectedText()); - - QWidget::mousePressEvent(event); -} - -SuggestionsPopup::SuggestionsPopup(QWidget *parent) - : QWidget(parent) -{ - setAttribute(Qt::WA_ShowWithoutActivating, true); - setWindowFlags(Qt::ToolTip | Qt::NoDropShadowWindowHint); - - layout_ = new QVBoxLayout(this); - layout_->setMargin(0); - layout_->setSpacing(0); -} - -void -SuggestionsPopup::addRooms(const std::vector<RoomSearchResult> &rooms) -{ - if (rooms.empty()) { - hide(); - return; - } - - const size_t layoutCount = layout_->count(); - const size_t roomCount = rooms.size(); - - // Remove the extra widgets from the layout. - if (roomCount < layoutCount) - removeLayoutItemsAfter(roomCount - 1); - - for (size_t i = 0; i < roomCount; ++i) { - auto item = layout_->itemAt(i); - - // Create a new widget if there isn't already one in that - // layout position. - if (!item) { - auto room = new RoomItem(this, rooms.at(i)); - connect(room, &RoomItem::clicked, this, &SuggestionsPopup::itemSelected); - layout_->addWidget(room); - } else { - // Update the current widget with the new data. - auto room = qobject_cast<RoomItem *>(item->widget()); - if (room) - room->updateItem(rooms.at(i)); - } - } - - resetSelection(); - adjustSize(); - - resize(geometry().width(), 40 * rooms.size()); - - selectNextSuggestion(); -} - -void -SuggestionsPopup::addUsers(const QVector<SearchResult> &users) -{ - if (users.isEmpty()) { - hide(); - return; - } - - const size_t layoutCount = layout_->count(); - const size_t userCount = users.size(); - - // Remove the extra widgets from the layout. - if (userCount < layoutCount) - removeLayoutItemsAfter(userCount - 1); - - for (size_t i = 0; i < userCount; ++i) { - auto item = layout_->itemAt(i); - - // Create a new widget if there isn't already one in that - // layout position. - if (!item) { - auto user = new UserItem(this, users.at(i).user_id); - connect(user, &UserItem::clicked, this, &SuggestionsPopup::itemSelected); - layout_->addWidget(user); - } else { - // Update the current widget with the new data. - auto userWidget = qobject_cast<UserItem *>(item->widget()); - if (userWidget) - userWidget->updateItem(users.at(i).user_id); - } - } - - resetSelection(); - adjustSize(); - - selectNextSuggestion(); -} - -void -SuggestionsPopup::hoverSelection() -{ - resetHovering(); - setHovering(selectedItem_); - update(); -} - -void -SuggestionsPopup::selectNextSuggestion() -{ - selectedItem_++; - if (selectedItem_ >= layout_->count()) - selectFirstItem(); - - hoverSelection(); -} - -void -SuggestionsPopup::selectPreviousSuggestion() -{ - selectedItem_--; - if (selectedItem_ < 0) - selectLastItem(); - - hoverSelection(); -} - -void -SuggestionsPopup::resetHovering() -{ - for (int i = 0; i < layout_->count(); ++i) { - const auto item = qobject_cast<PopupItem *>(layout_->itemAt(i)->widget()); - - if (item) - item->setHovering(false); - } -} - -void -SuggestionsPopup::setHovering(int pos) -{ - const auto &item = layout_->itemAt(pos); - const auto &widget = qobject_cast<PopupItem *>(item->widget()); - - if (widget) - widget->setHovering(true); -} - -void -SuggestionsPopup::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} diff --git a/src/SuggestionsPopup.h b/src/SuggestionsPopup.h deleted file mode 100644
index 72d6c7eb..00000000 --- a/src/SuggestionsPopup.h +++ /dev/null
@@ -1,147 +0,0 @@ -#pragma once - -#include <QHBoxLayout> -#include <QLabel> -#include <QPoint> -#include <QWidget> - -#include "AvatarProvider.h" -#include "Cache.h" -#include "ChatPage.h" - -class Avatar; -struct SearchResult; - -class PopupItem : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor hoverColor READ hoverColor WRITE setHoverColor) - Q_PROPERTY(bool hovering READ hovering WRITE setHovering) - -public: - PopupItem(QWidget *parent); - - QString selectedText() const { return QString(); } - QColor hoverColor() const { return hoverColor_; } - void setHoverColor(QColor &color) { hoverColor_ = color; } - - bool hovering() const { return hovering_; } - void setHovering(const bool hover) { hovering_ = hover; }; - -protected: - void paintEvent(QPaintEvent *event) override; - -signals: - void clicked(const QString &text); - -protected: - QHBoxLayout *topLayout_; - Avatar *avatar_; - QColor hoverColor_; - - //! Set if the item is currently being - //! hovered during tab completion (cycling). - bool hovering_; -}; - -class UserItem : public PopupItem -{ - Q_OBJECT - -public: - UserItem(QWidget *parent, const QString &user_id); - QString selectedText() const { return userId_; } - void updateItem(const QString &user_id); - -protected: - void mousePressEvent(QMouseEvent *event) override; - -private: - void resolveAvatar(const QString &user_id); - - QLabel *userName_; - QString userId_; -}; - -class RoomItem : public PopupItem -{ - Q_OBJECT - -public: - RoomItem(QWidget *parent, const RoomSearchResult &res); - QString selectedText() const { return roomId_; } - void updateItem(const RoomSearchResult &res); - -protected: - void mousePressEvent(QMouseEvent *event) override; - -private: - QLabel *roomName_; - QString roomId_; - RoomSearchResult info_; -}; - -class SuggestionsPopup : public QWidget -{ - Q_OBJECT - -public: - explicit SuggestionsPopup(QWidget *parent = nullptr); - - template<class Item> - void selectHoveredSuggestion() - { - const auto item = layout_->itemAt(selectedItem_); - if (!item) - return; - - const auto &widget = qobject_cast<Item *>(item->widget()); - emit itemSelected( - Cache::displayName(ChatPage::instance()->currentRoom(), widget->selectedText())); - - resetSelection(); - } - -public slots: - void addUsers(const QVector<SearchResult> &users); - void addRooms(const std::vector<RoomSearchResult> &rooms); - - //! Move to the next available suggestion item. - void selectNextSuggestion(); - //! Move to the previous available suggestion item. - void selectPreviousSuggestion(); - //! Remove hovering from all items. - void resetHovering(); - //! Set hovering to the item in the given layout position. - void setHovering(int pos); - -protected: - void paintEvent(QPaintEvent *event) override; - -signals: - void itemSelected(const QString &user); - -private: - void hoverSelection(); - void resetSelection() { selectedItem_ = -1; } - void selectFirstItem() { selectedItem_ = 0; } - void selectLastItem() { selectedItem_ = layout_->count() - 1; } - void removeLayoutItemsAfter(size_t startingPos) - { - size_t posToRemove = layout_->count() - 1; - - QLayoutItem *item; - while (startingPos <= posToRemove && (item = layout_->takeAt(posToRemove)) != 0) { - delete item->widget(); - delete item; - - posToRemove = layout_->count() - 1; - } - } - - QVBoxLayout *layout_; - - //! Counter for tab completion (cycling). - int selectedItem_ = -1; -}; diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp
index 934f2b2c..11f7ddda 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp
@@ -16,12 +16,9 @@ */ #include <QAbstractTextDocumentLayout> -#include <QApplication> #include <QBuffer> #include <QClipboard> -#include <QDebug> #include <QFileDialog> -#include <QImageReader> #include <QMimeData> #include <QMimeDatabase> #include <QMimeType> @@ -31,7 +28,7 @@ #include "Cache.h" #include "ChatPage.h" -#include "Config.h" +#include "Logging.h" #include "TextInputWidget.h" #include "Utils.h" #include "ui/FlatButton.h" @@ -48,7 +45,8 @@ static constexpr int ButtonHeight = 22; FilteredTextEdit::FilteredTextEdit(QWidget *parent) : QTextEdit{parent} , history_index_{0} - , popup_{parent} + , suggestionsPopup_{parent} + , replyPopup_{parent} , previewDialog_{parent} { setFrameStyle(QFrame::NoFrame); @@ -75,36 +73,43 @@ FilteredTextEdit::FilteredTextEdit(QWidget *parent) &FilteredTextEdit::uploadData); connect(this, &FilteredTextEdit::resultsRetrieved, this, &FilteredTextEdit::showResults); - connect(&popup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) { - popup_.hide(); + connect(&replyPopup_, &ReplyPopup::userSelected, this, [](const QString &text) { + // TODO: Show user avatar window. + nhlog::ui()->info("User selected: " + text.toStdString()); + }); + connect( + &suggestionsPopup_, &SuggestionsPopup::itemSelected, this, [this](const QString &text) { + suggestionsPopup_.hide(); - auto cursor = textCursor(); - const int end = cursor.position(); + auto cursor = textCursor(); + const int end = cursor.position(); - cursor.setPosition(atTriggerPosition_, QTextCursor::MoveAnchor); - cursor.setPosition(end, QTextCursor::KeepAnchor); - cursor.removeSelectedText(); - cursor.insertText(text); - }); + cursor.setPosition(atTriggerPosition_, QTextCursor::MoveAnchor); + cursor.setPosition(end, QTextCursor::KeepAnchor); + cursor.removeSelectedText(); + cursor.insertText(text); + }); + + connect(&replyPopup_, &ReplyPopup::cancel, this, [this]() { closeReply(); }); // For cycling through the suggestions by hitting tab. connect(this, &FilteredTextEdit::selectNextSuggestion, - &popup_, + &suggestionsPopup_, &SuggestionsPopup::selectNextSuggestion); connect(this, &FilteredTextEdit::selectPreviousSuggestion, - &popup_, + &suggestionsPopup_, &SuggestionsPopup::selectPreviousSuggestion); connect(this, &FilteredTextEdit::selectHoveredSuggestion, this, [this]() { - popup_.selectHoveredSuggestion<UserItem>(); + suggestionsPopup_.selectHoveredSuggestion<UserItem>(); }); previewDialog_.hide(); } void -FilteredTextEdit::showResults(const QVector<SearchResult> &results) +FilteredTextEdit::showResults(const std::vector<SearchResult> &results) { QPoint pos; @@ -117,9 +122,9 @@ FilteredTextEdit::showResults(const QVector<SearchResult> &results) pos = viewport()->mapToGlobal(rect.topLeft()); } - popup_.addUsers(results); - popup_.move(pos.x(), pos.y() - popup_.height() - 10); - popup_.show(); + suggestionsPopup_.addUsers(results); + suggestionsPopup_.move(pos.x(), pos.y() - suggestionsPopup_.height() - 10); + suggestionsPopup_.show(); } void @@ -146,7 +151,7 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) closeSuggestions(); } - if (popup_.isVisible()) { + if (suggestionsPopup_.isVisible()) { switch (event->key()) { case Qt::Key_Down: case Qt::Key_Tab: @@ -169,6 +174,17 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) } } + if (replyPopup_.isVisible()) { + switch (event->key()) { + case Qt::Key_Escape: + closeReply(); + return; + + default: + break; + } + } + switch (event->key()) { case Qt::Key_At: atTriggerPosition_ = textCursor().position(); @@ -202,6 +218,7 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event) if (!(event->modifiers() & Qt::ShiftModifier)) { stopTyping(); submit(); + closeReply(); } else { QTextEdit::keyPressEvent(event); } @@ -286,8 +303,9 @@ FilteredTextEdit::insertFromMimeData(const QMimeData *source) const auto audio = formats.filter("audio/", Qt::CaseInsensitive); const auto video = formats.filter("video/", Qt::CaseInsensitive); - if (!image.empty()) { - showPreview(source, image); + if (source->hasImage()) { + QImage img = qvariant_cast<QImage>(source->imageData()); + previewDialog_.setPreview(img, image.front()); } else if (!audio.empty()) { showPreview(source, audio); } else if (!video.empty()) { @@ -398,39 +416,49 @@ FilteredTextEdit::submit() auto name = text.mid(1, command_end - 1); auto args = text.mid(command_end + 1); if (name.isEmpty() || name == "/") { - message(args); + message(args, related); } else { command(name, args); } } else { - message(std::move(text)); + message(std::move(text), std::move(related)); } + related = {}; + clear(); } void +FilteredTextEdit::showReplyPopup(const RelatedInfo &related_) +{ + QPoint pos = viewport()->mapToGlobal(this->pos()); + + replyPopup_.setReplyContent(related_); + replyPopup_.move(pos.x(), pos.y() - replyPopup_.height() - 10); + replyPopup_.setFixedWidth(this->parentWidget()->width()); + replyPopup_.show(); +} + +void FilteredTextEdit::textChanged() { working_history_[history_index_] = toPlainText(); } void -FilteredTextEdit::uploadData(const QByteArray data, const QString &media, const QString &filename) +FilteredTextEdit::uploadData(const QByteArray data, + const QString &mediaType, + const QString &filename) { QSharedPointer<QBuffer> buffer{new QBuffer{this}}; buffer->setData(data); emit startedUpload(); - if (media == "image") - emit image(buffer, filename); - else if (media == "audio") - emit audio(buffer, filename); - else if (media == "video") - emit video(buffer, filename); - else - emit file(buffer, filename); + emit media(buffer, mediaType, filename, related); + related = {}; + closeReply(); } void @@ -492,15 +520,15 @@ TextInputWidget::TextInputWidget(QWidget *parent) emit heightChanged(widgetHeight); }); connect(input_, &FilteredTextEdit::showSuggestions, this, [this](const QString &q) { - if (q.isEmpty() || !cache::client()) + if (q.isEmpty()) return; QtConcurrent::run([this, q = q.toLower().toStdString()]() { try { - emit input_->resultsRetrieved(cache::client()->searchUsers( + emit input_->resultsRetrieved(cache::searchUsers( ChatPage::instance()->currentRoom().toStdString(), q)); } catch (const lmdb::error &e) { - std::cout << e.what() << '\n'; + nhlog::db()->error("Suggestion retrieval failed: {}", e.what()); } }); }); @@ -537,10 +565,7 @@ TextInputWidget::TextInputWidget(QWidget *parent) connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection())); connect(input_, &FilteredTextEdit::message, this, &TextInputWidget::sendTextMessage); connect(input_, &FilteredTextEdit::command, this, &TextInputWidget::command); - connect(input_, &FilteredTextEdit::image, this, &TextInputWidget::uploadImage); - connect(input_, &FilteredTextEdit::audio, this, &TextInputWidget::uploadAudio); - connect(input_, &FilteredTextEdit::video, this, &TextInputWidget::uploadVideo); - connect(input_, &FilteredTextEdit::file, this, &TextInputWidget::uploadFile); + connect(input_, &FilteredTextEdit::media, this, &TextInputWidget::uploadMedia); connect(emojiBtn_, SIGNAL(emojiSelected(const QString &)), this, @@ -574,21 +599,36 @@ void TextInputWidget::command(QString command, QString args) { if (command == "me") { - sendEmoteMessage(args); + sendEmoteMessage(args, input_->related); } else if (command == "join") { sendJoinRoomRequest(args); + } else if (command == "invite") { + sendInviteRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1)); + } else if (command == "kick") { + sendKickRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1)); + } else if (command == "ban") { + sendBanRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1)); + } else if (command == "unban") { + sendUnbanRoomRequest(args.section(' ', 0, 0), args.section(' ', 1, -1)); } else if (command == "shrug") { - sendTextMessage("¯\\_(ツ)_/¯"); + sendTextMessage("¯\\_(ツ)_/¯", input_->related); } else if (command == "fliptable") { - sendTextMessage("(╯°□°)╯︵ ┻━┻"); + sendTextMessage("(╯°□°)╯︵ ┻━┻", input_->related); + } else if (command == "unfliptable") { + sendTextMessage(" ┯━┯╭( º _ º╭)", input_->related); + } else if (command == "sovietflip") { + sendTextMessage("ノ┬─┬ノ ︵ ( \\o°o)\\", input_->related); } + + input_->related = std::nullopt; } void TextInputWidget::openFileSelection() { + const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); const auto fileName = - QFileDialog::getOpenFileName(this, tr("Select a file"), "", tr("All Files (*)")); + QFileDialog::getOpenFileName(this, tr("Select a file"), homeFolder, tr("All Files (*)")); if (fileName.isEmpty()) return; @@ -599,14 +639,10 @@ TextInputWidget::openFileSelection() const auto format = mime.name().split("/")[0]; QSharedPointer<QFile> file{new QFile{fileName, this}}; - if (format == "image") - emit uploadImage(file, fileName); - else if (format == "audio") - emit uploadAudio(file, fileName); - else if (format == "video") - emit uploadVideo(file, fileName); - else - emit uploadFile(file, fileName); + + emit uploadMedia(file, format, QFileInfo(fileName).fileName(), input_->related); + input_->related = {}; + input_->closeReply(); showUploadSpinner(); } @@ -653,12 +689,14 @@ TextInputWidget::paintEvent(QPaintEvent *) } void -TextInputWidget::addReply(const QString &username, const QString &msg) +TextInputWidget::addReply(const RelatedInfo &related) { - input_->setText(QString("> %1: %2\n\n").arg(username).arg(msg)); + // input_->setText(QString("> %1: %2\n\n").arg(username).arg(msg)); input_->setFocus(); + // input_->showReplyPopup(related); auto cursor = input_->textCursor(); cursor.movePosition(QTextCursor::End); input_->setTextCursor(cursor); + input_->related = related; } diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h
index 8f634f6b..77d77e44 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h
@@ -18,23 +18,18 @@ #pragma once #include <deque> -#include <iterator> -#include <map> +#include <optional> -#include <QApplication> -#include <QDebug> +#include <QCoreApplication> #include <QHBoxLayout> #include <QPaintEvent> #include <QTextEdit> #include <QWidget> -#include "SuggestionsPopup.h" #include "dialogs/PreviewUploadOverlay.h" #include "emoji/PickButton.h" - -namespace dialogs { -class PreviewUploadOverlay; -} +#include "popups/ReplyPopup.h" +#include "popups/SuggestionsPopup.h" struct SearchResult; @@ -54,28 +49,37 @@ public: QSize minimumSizeHint() const override; void submit(); + void showReplyPopup(const RelatedInfo &related_); + void closeReply() + { + replyPopup_.hide(); + related = {}; + } + + // Used for replies + std::optional<RelatedInfo> related; signals: void heightChanged(int height); void startedTyping(); void stoppedTyping(); void startedUpload(); - void message(QString); + void message(QString, const std::optional<RelatedInfo> &); void command(QString name, QString args); - void image(QSharedPointer<QIODevice> data, const QString &filename); - void audio(QSharedPointer<QIODevice> data, const QString &filename); - void video(QSharedPointer<QIODevice> data, const QString &filename); - void file(QSharedPointer<QIODevice> data, const QString &filename); + void media(QSharedPointer<QIODevice> data, + QString mimeClass, + const QString &filename, + const std::optional<RelatedInfo> &related); //! Trigger the suggestion popup. void showSuggestions(const QString &query); - void resultsRetrieved(const QVector<SearchResult> &results); + void resultsRetrieved(const std::vector<SearchResult> &results); void selectNextSuggestion(); void selectPreviousSuggestion(); void selectHoveredSuggestion(); public slots: - void showResults(const QVector<SearchResult> &results); + void showResults(const std::vector<SearchResult> &results); protected: void keyPressEvent(QKeyEvent *event) override; @@ -83,7 +87,7 @@ protected: void insertFromMimeData(const QMimeData *source) override; void focusOutEvent(QFocusEvent *event) override { - popup_.hide(); + suggestionsPopup_.hide(); QTextEdit::focusOutEvent(event); } @@ -92,7 +96,8 @@ private: size_t history_index_; QTimer *typingTimer_; - SuggestionsPopup popup_; + SuggestionsPopup suggestionsPopup_; + ReplyPopup replyPopup_; enum class AnchorType { @@ -104,7 +109,7 @@ private: int anchorWidth(AnchorType anchor) { return static_cast<int>(anchor); } - void closeSuggestions() { popup_.hide(); } + void closeSuggestions() { suggestionsPopup_.hide(); } void resetAnchor() { atTriggerPosition_ = -1; } bool isAnchorValid() { return atTriggerPosition_ != -1; } bool hasAnchor(int pos, AnchorType anchor) @@ -137,7 +142,7 @@ class TextInputWidget : public QWidget Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor) public: - TextInputWidget(QWidget *parent = 0); + TextInputWidget(QWidget *parent = nullptr); void stopTyping(); @@ -158,22 +163,27 @@ public slots: void openFileSelection(); void hideUploadSpinner(); void focusLineEdit() { input_->setFocus(); } - void addReply(const QString &username, const QString &msg); + void addReply(const RelatedInfo &related); + void closeReplyPopup() { input_->closeReply(); } private slots: void addSelectedEmoji(const QString &emoji); signals: - void sendTextMessage(QString msg); - void sendEmoteMessage(QString msg); + void sendTextMessage(const QString &msg, const std::optional<RelatedInfo> &related); + void sendEmoteMessage(QString msg, const std::optional<RelatedInfo> &related); void heightChanged(int height); - void uploadImage(const QSharedPointer<QIODevice> data, const QString &filename); - void uploadFile(const QSharedPointer<QIODevice> data, const QString &filename); - void uploadAudio(const QSharedPointer<QIODevice> data, const QString &filename); - void uploadVideo(const QSharedPointer<QIODevice> data, const QString &filename); + void uploadMedia(const QSharedPointer<QIODevice> data, + QString mimeClass, + const QString &filename, + const std::optional<RelatedInfo> &related); void sendJoinRoomRequest(const QString &room); + void sendInviteRoomRequest(const QString &userid, const QString &reason); + void sendKickRoomRequest(const QString &userid, const QString &reason); + void sendBanRoomRequest(const QString &userid, const QString &reason); + void sendUnbanRoomRequest(const QString &userid, const QString &reason); void startedTyping(); void stoppedTyping(); diff --git a/src/TopRoomBar.cpp b/src/TopRoomBar.cpp
index 5c817dc2..ffd57d50 100644 --- a/src/TopRoomBar.cpp +++ b/src/TopRoomBar.cpp
@@ -15,8 +15,16 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#include <QDebug> +#include <QAction> +#include <QIcon> +#include <QLabel> +#include <QPaintEvent> +#include <QPainter> +#include <QPen> +#include <QPoint> +#include <QStyle> #include <QStyleOption> +#include <QVBoxLayout> #include "Config.h" #include "MainWindow.h" @@ -46,9 +54,8 @@ TopRoomBar::TopRoomBar(QWidget *parent) topLayout_->setContentsMargins( 2 * widgetMargin, widgetMargin, 2 * widgetMargin, widgetMargin); - avatar_ = new Avatar(this); + avatar_ = new Avatar(this, fontHeight * 2); avatar_->setLetter(""); - avatar_->setSize(fontHeight * 2); textLayout_ = new QVBoxLayout(); textLayout_->setSpacing(0); @@ -80,11 +87,21 @@ TopRoomBar::TopRoomBar(QWidget *parent) settingsBtn_->setFixedSize(buttonSize_, buttonSize_); settingsBtn_->setCornerRadius(buttonSize_ / 2); + mentionsBtn_ = new FlatButton(this); + mentionsBtn_->setToolTip(tr("Mentions")); + mentionsBtn_->setFixedSize(buttonSize_, buttonSize_); + mentionsBtn_->setCornerRadius(buttonSize_ / 2); + QIcon settings_icon; settings_icon.addFile(":/icons/icons/ui/vertical-ellipsis.png"); settingsBtn_->setIcon(settings_icon); settingsBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2)); + QIcon mentions_icon; + mentions_icon.addFile(":/icons/icons/ui/at-solid.svg"); + mentionsBtn_->setIcon(mentions_icon); + mentionsBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2)); + backBtn_ = new FlatButton(this); backBtn_->setFixedSize(buttonSize_, buttonSize_); backBtn_->setCornerRadius(buttonSize_ / 2); @@ -100,6 +117,7 @@ TopRoomBar::TopRoomBar(QWidget *parent) topLayout_->addWidget(avatar_); topLayout_->addWidget(backBtn_); topLayout_->addLayout(textLayout_, 1); + topLayout_->addWidget(mentionsBtn_, 0, Qt::AlignRight); topLayout_->addWidget(settingsBtn_, 0, Qt::AlignRight); menu_ = new Menu(this); @@ -135,6 +153,11 @@ TopRoomBar::TopRoomBar(QWidget *parent) menu_->popup( QPoint(pos.x() + buttonSize_ - menu_->sizeHint().width(), pos.y() + buttonSize_)); }); + + connect(mentionsBtn_, &QPushButton::clicked, this, [this]() { + auto pos = mapToGlobal(mentionsBtn_->pos()); + emit mentionsClicked(pos); + }); } void @@ -167,7 +190,7 @@ TopRoomBar::reset() } void -TopRoomBar::updateRoomAvatar(const QImage &avatar_image) +TopRoomBar::updateRoomAvatar(const QString &avatar_image) { avatar_->setImage(avatar_image); update(); @@ -195,3 +218,19 @@ TopRoomBar::updateRoomTopic(QString topic) topicLabel_->setHtml(topic); update(); } + +void +TopRoomBar::mousePressEvent(QMouseEvent *) +{ + if (roomSettings_ != nullptr) + roomSettings_->trigger(); +} + +void +TopRoomBar::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +} diff --git a/src/TopRoomBar.h b/src/TopRoomBar.h
index 5b7d3344..63ce847e 100644 --- a/src/TopRoomBar.h +++ b/src/TopRoomBar.h
@@ -17,16 +17,9 @@ #pragma once -#include <QAction> -#include <QIcon> -#include <QImage> -#include <QLabel> -#include <QPaintEvent> -#include <QPainter> -#include <QPen> -#include <QStyle> -#include <QStyleOption> -#include <QVBoxLayout> +#include <QColor> +#include <QStringList> +#include <QWidget> class Avatar; class FlatButton; @@ -34,6 +27,12 @@ class Menu; class TextLabel; class OverlayModal; +class QPainter; +class QLabel; +class QIcon; +class QHBoxLayout; +class QVBoxLayout; + class TopRoomBar : public QWidget { Q_OBJECT @@ -41,9 +40,9 @@ class TopRoomBar : public QWidget Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor) public: - TopRoomBar(QWidget *parent = 0); + TopRoomBar(QWidget *parent = nullptr); - void updateRoomAvatar(const QImage &avatar_image); + void updateRoomAvatar(const QString &avatar_image); void updateRoomAvatar(const QIcon &icon); void updateRoomName(const QString &name); void updateRoomTopic(QString topic); @@ -63,21 +62,11 @@ public slots: signals: void inviteUsers(QStringList users); void showRoomList(); + void mentionsClicked(const QPoint &pos); protected: - void mousePressEvent(QMouseEvent *) override - { - if (roomSettings_ != nullptr) - roomSettings_->trigger(); - } - - void paintEvent(QPaintEvent *) override - { - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); - } + void mousePressEvent(QMouseEvent *) override; + void paintEvent(QPaintEvent *) override; private: QHBoxLayout *topLayout_ = nullptr; @@ -93,6 +82,7 @@ private: QAction *inviteUsers_ = nullptr; FlatButton *settingsBtn_; + FlatButton *mentionsBtn_; FlatButton *backBtn_; Avatar *avatar_; diff --git a/src/TrayIcon.cpp b/src/TrayIcon.cpp
index e7348b89..6ab011d1 100644 --- a/src/TrayIcon.cpp +++ b/src/TrayIcon.cpp
@@ -15,9 +15,11 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <QAction> #include <QApplication> #include <QList> #include <QMenu> +#include <QPainter> #include <QTimer> #include "TrayIcon.h" @@ -134,12 +136,16 @@ TrayIcon::setUnreadCount(int count) { // Use the native badge counter in MacOS. #if defined(Q_OS_MAC) +// currently, to avoid writing obj-c code, ignore deprecated warnings on the badge functions +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" auto labelText = count == 0 ? "" : QString::number(count); if (labelText == QtMac::badgeLabelText()) return; QtMac::setBadgeLabelText(labelText); +#pragma clang diagnostic pop #elif defined(Q_OS_WIN) // FIXME: Find a way to use Windows apis for the badge counter (if any). #else diff --git a/src/TrayIcon.h b/src/TrayIcon.h
index a3536cc3..24ac81da 100644 --- a/src/TrayIcon.h +++ b/src/TrayIcon.h
@@ -17,22 +17,23 @@ #pragma once -#include <QAction> #include <QIcon> #include <QIconEngine> -#include <QPainter> #include <QRect> #include <QSystemTrayIcon> +class QAction; +class QPainter; + class MsgCountComposedIcon : public QIconEngine { public: MsgCountComposedIcon(const QString &filename); - virtual void paint(QPainter *p, const QRect &rect, QIcon::Mode mode, QIcon::State state); - virtual QIconEngine *clone() const; - virtual QList<QSize> availableSizes(QIcon::Mode mode, QIcon::State state) const; - virtual QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state); + void paint(QPainter *p, const QRect &rect, QIcon::Mode mode, QIcon::State state) override; + QIconEngine *clone() const override; + QList<QSize> availableSizes(QIcon::Mode mode, QIcon::State state) const override; + QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) override; int msgCount = 0; diff --git a/src/TypingDisplay.cpp b/src/TypingDisplay.cpp deleted file mode 100644
index 11313adc..00000000 --- a/src/TypingDisplay.cpp +++ /dev/null
@@ -1,76 +0,0 @@ -#include <QDebug> -#include <QPainter> -#include <QPoint> -#include <QShowEvent> - -#include "Config.h" -#include "TypingDisplay.h" -#include "ui/Painter.h" - -constexpr int LEFT_PADDING = 24; -constexpr int RECT_PADDING = 2; - -TypingDisplay::TypingDisplay(QWidget *parent) - : OverlayWidget(parent) - , offset_{conf::textInput::height} -{ - setFixedHeight(QFontMetrics(font()).height() + RECT_PADDING); - setAttribute(Qt::WA_TransparentForMouseEvents); -} - -void -TypingDisplay::setOffset(int margin) -{ - offset_ = margin; - move(0, parentWidget()->height() - offset_ - height()); -} - -void -TypingDisplay::setUsers(const QStringList &uid) -{ - move(0, parentWidget()->height() - offset_ - height()); - - text_.clear(); - - if (uid.isEmpty()) { - hide(); - update(); - - return; - } - - text_ = uid.join(", "); - - if (uid.size() == 1) - text_ += tr(" is typing"); - else if (uid.size() > 1) - text_ += tr(" are typing"); - - show(); - update(); -} - -void -TypingDisplay::paintEvent(QPaintEvent *) -{ - Painter p(this); - PainterHighQualityEnabler hq(p); - - QFont f; - f.setPointSizeF(f.pointSizeF() * 0.9); - - p.setFont(f); - p.setPen(QPen(textColor())); - - QRect region = rect(); - region.translate(LEFT_PADDING, 0); - - QFontMetrics fm(f); - text_ = fm.elidedText(text_, Qt::ElideRight, (double)(width() * 0.75)); - - QPainterPath path; - path.addRoundedRect(QRectF(0, 0, fm.width(text_) + 2 * LEFT_PADDING, height()), 3, 3); - - p.fillPath(path, backgroundColor()); - p.drawText(region, Qt::AlignVCenter, text_); -} diff --git a/src/TypingDisplay.h b/src/TypingDisplay.h deleted file mode 100644
index 332d9c66..00000000 --- a/src/TypingDisplay.h +++ /dev/null
@@ -1,36 +0,0 @@ -#pragma once - -#include "ui/OverlayWidget.h" - -class QPaintEvent; - -class TypingDisplay : public OverlayWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - -public: - TypingDisplay(QWidget *parent = nullptr); - - void setUsers(const QStringList &user_ids); - - void setTextColor(const QColor &color) { textColor_ = color; }; - QColor textColor() const { return textColor_; }; - - void setBackgroundColor(const QColor &color) { bgColor_ = color; }; - QColor backgroundColor() const { return bgColor_; }; - -public slots: - void setOffset(int margin); - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - int offset_; - QColor textColor_; - QColor bgColor_; - QString text_; -}; diff --git a/src/UserInfoWidget.cpp b/src/UserInfoWidget.cpp
index 5345fb2a..2e21d41f 100644 --- a/src/UserInfoWidget.cpp +++ b/src/UserInfoWidget.cpp
@@ -1,3 +1,4 @@ + /* * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> * @@ -15,14 +16,15 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <QPainter> #include <QTimer> #include <iostream> #include "Config.h" #include "MainWindow.h" +#include "Splitter.h" #include "UserInfoWidget.h" -#include "Utils.h" #include "ui/Avatar.h" #include "ui/FlatButton.h" #include "ui/OverlayModal.h" @@ -52,10 +54,9 @@ UserInfoWidget::UserInfoWidget(QWidget *parent) textLayout_->setSpacing(widgetMargin / 2); textLayout_->setContentsMargins(widgetMargin * 2, widgetMargin, widgetMargin, widgetMargin); - userAvatar_ = new Avatar(this); + userAvatar_ = new Avatar(this, fontHeight * 2.5); userAvatar_->setObjectName("userAvatar"); userAvatar_->setLetter(QChar('?')); - userAvatar_->setSize(fontHeight * 2.5); QFont nameFont; nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1); @@ -108,7 +109,7 @@ UserInfoWidget::resizeEvent(QResizeEvent *event) { Q_UNUSED(event); - const auto sz = utils::calculateSidebarSizes(QFont{}); + const auto sz = splitter::calculateSidebarSizes(QFont{}); if (width() <= sz.small) { topLayout_->setContentsMargins(0, 0, logoutButtonSize_, 0); @@ -135,14 +136,6 @@ UserInfoWidget::reset() } void -UserInfoWidget::setAvatar(const QImage &img) -{ - avatar_image_ = img; - userAvatar_->setImage(img); - update(); -} - -void UserInfoWidget::setDisplayName(const QString &name) { if (name.isEmpty()) @@ -160,6 +153,14 @@ UserInfoWidget::setUserId(const QString &userid) { user_id_ = userid; userIdLabel_->setText(userid); + update(); +} + +void +UserInfoWidget::setAvatar(const QString &url) +{ + userAvatar_->setImage(url); + update(); } void diff --git a/src/UserInfoWidget.h b/src/UserInfoWidget.h
index 65de7be9..e1a925a4 100644 --- a/src/UserInfoWidget.h +++ b/src/UserInfoWidget.h
@@ -31,11 +31,11 @@ class UserInfoWidget : public QWidget Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor) public: - UserInfoWidget(QWidget *parent = 0); + UserInfoWidget(QWidget *parent = nullptr); - void setAvatar(const QImage &img); void setDisplayName(const QString &name); void setUserId(const QString &userid); + void setAvatar(const QString &url); void reset(); diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index e3c0d190..2cac783c 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp
@@ -18,14 +18,23 @@ #include <QApplication> #include <QComboBox> #include <QFileDialog> +#include <QFormLayout> #include <QInputDialog> #include <QLabel> #include <QLineEdit> #include <QMessageBox> +#include <QPainter> +#include <QProcessEnvironment> #include <QPushButton> +#include <QResizeEvent> #include <QScrollArea> +#include <QScroller> #include <QSettings> +#include <QStandardPaths> +#include <QString> +#include <QTextStream> +#include "Cache.h" #include "Config.h" #include "MatrixClient.h" #include "Olm.h" @@ -46,10 +55,13 @@ UserSettings::load() hasDesktopNotifications_ = settings.value("user/desktop_notifications", true).toBool(); isStartInTrayEnabled_ = settings.value("user/window/start_in_tray", false).toBool(); isGroupViewEnabled_ = settings.value("user/group_view", true).toBool(); + isMarkdownEnabled_ = settings.value("user/markdown_enabled", true).toBool(); isTypingNotificationsEnabled_ = settings.value("user/typing_notifications", true).toBool(); isReadReceiptsEnabled_ = settings.value("user/read_receipts", true).toBool(); - theme_ = settings.value("user/theme", "light").toString(); + theme_ = settings.value("user/theme", defaultTheme_).toString(); font_ = settings.value("user/font_family", "default").toString(); + avatarCircles_ = settings.value("user/avatar_circles", true).toBool(); + emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); applyTheme(); @@ -70,6 +82,13 @@ UserSettings::setFontFamily(QString family) } void +UserSettings::setEmojiFontFamily(QString family) +{ + emojiFont_ = family; + save(); +} + +void UserSettings::setTheme(QString theme) { theme_ = theme; @@ -107,13 +126,18 @@ UserSettings::save() settings.setValue("start_in_tray", isStartInTrayEnabled_); settings.endGroup(); + settings.setValue("avatar_circles", avatarCircles_); + settings.setValue("font_size", baseFontSize_); settings.setValue("typing_notifications", isTypingNotificationsEnabled_); settings.setValue("read_receipts", isReadReceiptsEnabled_); settings.setValue("group_view", isGroupViewEnabled_); + settings.setValue("markdown_enabled", isMarkdownEnabled_); settings.setValue("desktop_notifications", hasDesktopNotifications_); settings.setValue("theme", theme()); settings.setValue("font_family", font_); + settings.setValue("emoji_font_family", emojiFont_); + settings.endGroup(); } @@ -128,12 +152,12 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge : QWidget{parent} , settings_{settings} { - topLayout_ = new QVBoxLayout(this); + topLayout_ = new QVBoxLayout{this}; QIcon icon; icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); - auto backBtn_ = new FlatButton(this); + auto backBtn_ = new FlatButton{this}; backBtn_->setMinimumSize(QSize(24, 24)); backBtn_->setIcon(icon); backBtn_->setIconSize(QSize(24, 24)); @@ -150,106 +174,65 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge topBarLayout_->addWidget(backBtn_, 1, Qt::AlignLeft | Qt::AlignVCenter); topBarLayout_->addStretch(1); - auto trayOptionLayout_ = new QHBoxLayout; - trayOptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto trayLabel = new QLabel(tr("Minimize to tray"), this); - trayLabel->setFont(font); - trayToggle_ = new Toggle(this); + formLayout_ = new QFormLayout; - trayOptionLayout_->addWidget(trayLabel); - trayOptionLayout_->addWidget(trayToggle_, 0, Qt::AlignRight); + formLayout_->setLabelAlignment(Qt::AlignLeft); + formLayout_->setFormAlignment(Qt::AlignRight); + formLayout_->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + formLayout_->setRowWrapPolicy(QFormLayout::WrapLongRows); + formLayout_->setHorizontalSpacing(0); - auto startInTrayOptionLayout_ = new QHBoxLayout; - startInTrayOptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto startInTrayLabel = new QLabel(tr("Start in tray"), this); - startInTrayLabel->setFont(font); - startInTrayToggle_ = new Toggle(this); - if (!settings_->isTrayEnabled()) - startInTrayToggle_->setDisabled(true); - - startInTrayOptionLayout_->addWidget(startInTrayLabel); - startInTrayOptionLayout_->addWidget(startInTrayToggle_, 0, Qt::AlignRight); - - auto groupViewLayout = new QHBoxLayout; - groupViewLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto groupViewLabel = new QLabel(tr("Group's sidebar"), this); - groupViewLabel->setFont(font); - groupViewToggle_ = new Toggle(this); - - groupViewLayout->addWidget(groupViewLabel); - groupViewLayout->addWidget(groupViewToggle_, 0, Qt::AlignRight); - - auto typingLayout = new QHBoxLayout; - typingLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto typingLabel = new QLabel(tr("Typing notifications"), this); - typingLabel->setFont(font); - typingNotifications_ = new Toggle(this); - - typingLayout->addWidget(typingLabel); - typingLayout->addWidget(typingNotifications_, 0, Qt::AlignRight); + auto general_ = new QLabel{tr("GENERAL"), this}; + general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); + general_->setFont(font); - auto receiptsLayout = new QHBoxLayout; - receiptsLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto receiptsLabel = new QLabel(tr("Read receipts"), this); - receiptsLabel->setFont(font); - readReceipts_ = new Toggle(this); + trayToggle_ = new Toggle{this}; + startInTrayToggle_ = new Toggle{this}; + avatarCircles_ = new Toggle{this}; + groupViewToggle_ = new Toggle{this}; + typingNotifications_ = new Toggle{this}; + readReceipts_ = new Toggle{this}; + markdownEnabled_ = new Toggle{this}; + desktopNotifications_ = new Toggle{this}; + scaleFactorCombo_ = new QComboBox{this}; + fontSizeCombo_ = new QComboBox{this}; + fontSelectionCombo_ = new QComboBox{this}; + emojiFontSelectionCombo_ = new QComboBox{this}; - receiptsLayout->addWidget(receiptsLabel); - receiptsLayout->addWidget(readReceipts_, 0, Qt::AlignRight); + if (!settings_->isTrayEnabled()) + startInTrayToggle_->setDisabled(true); - auto desktopLayout = new QHBoxLayout; - desktopLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto desktopLabel = new QLabel(tr("Desktop notifications"), this); - desktopLabel->setFont(font); - desktopNotifications_ = new Toggle(this); + avatarCircles_->setFixedSize(64, 48); - desktopLayout->addWidget(desktopLabel); - desktopLayout->addWidget(desktopNotifications_, 0, Qt::AlignRight); + auto uiLabel_ = new QLabel{tr("INTERFACE"), this}; + uiLabel_->setFixedHeight(uiLabel_->minimumHeight() + LayoutTopMargin); + uiLabel_->setAlignment(Qt::AlignBottom); + uiLabel_->setFont(font); - auto scaleFactorOptionLayout = new QHBoxLayout; - scaleFactorOptionLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto scaleFactorLabel = new QLabel(tr("Scale factor"), this); - scaleFactorLabel->setFont(font); - scaleFactorCombo_ = new QComboBox(this); for (double option = 1; option <= 3; option += 0.25) scaleFactorCombo_->addItem(QString::number(option)); - - scaleFactorOptionLayout->addWidget(scaleFactorLabel); - scaleFactorOptionLayout->addWidget(scaleFactorCombo_, 0, Qt::AlignRight); - - auto fontSizeOptionLayout = new QHBoxLayout; - fontSizeOptionLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto fontSizeLabel = new QLabel(tr("Font size"), this); - fontSizeLabel->setFont(font); - fontSizeCombo_ = new QComboBox(this); for (double option = 10; option < 17; option += 0.5) fontSizeCombo_->addItem(QString("%1 ").arg(QString::number(option))); - fontSizeOptionLayout->addWidget(fontSizeLabel); - fontSizeOptionLayout->addWidget(fontSizeCombo_, 0, Qt::AlignRight); - - auto fontFamilyOptionLayout = new QHBoxLayout; - fontFamilyOptionLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto fontFamilyLabel = new QLabel(tr("Font Family"), this); - fontFamilyLabel->setFont(font); - fontSelectionCombo_ = new QComboBox(this); QFontDatabase fontDb; auto fontFamilies = fontDb.families(); for (const auto &family : fontFamilies) { fontSelectionCombo_->addItem(family); } - int fontIndex = fontSelectionCombo_->findText(settings_->font()); - fontSelectionCombo_->setCurrentIndex(fontIndex); + // TODO: Is there a way to limit to just emojis, rather than + // all emoji fonts? + auto emojiFamilies = fontDb.families(QFontDatabase::Symbol); + for (const auto &family : emojiFamilies) { + emojiFontSelectionCombo_->addItem(family); + } + + fontSelectionCombo_->setCurrentIndex(fontSelectionCombo_->findText(settings_->font())); - fontFamilyOptionLayout->addWidget(fontFamilyLabel); - fontFamilyOptionLayout->addWidget(fontSelectionCombo_, 0, Qt::AlignRight); + emojiFontSelectionCombo_->setCurrentIndex( + emojiFontSelectionCombo_->findText(settings_->emojiFont())); - auto themeOptionLayout_ = new QHBoxLayout; - themeOptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto themeLabel_ = new QLabel(tr("Theme"), this); - themeLabel_->setFont(font); - themeCombo_ = new QComboBox(this); + themeCombo_ = new QComboBox{this}; themeCombo_->addItem("Light"); themeCombo_->addItem("Dark"); themeCombo_->addItem("System"); @@ -259,117 +242,103 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge int themeIndex = themeCombo_->findText(themeStr); themeCombo_->setCurrentIndex(themeIndex); - themeOptionLayout_->addWidget(themeLabel_); - themeOptionLayout_->addWidget(themeCombo_, 0, Qt::AlignRight); - - auto encryptionLayout_ = new QVBoxLayout; - encryptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); - encryptionLayout_->setAlignment(Qt::AlignVCenter); + auto encryptionLabel_ = new QLabel{tr("ENCRYPTION"), this}; + encryptionLabel_->setFixedHeight(encryptionLabel_->minimumHeight() + LayoutTopMargin); + encryptionLabel_->setAlignment(Qt::AlignBottom); + encryptionLabel_->setFont(font); QFont monospaceFont; monospaceFont.setFamily("Monospace"); monospaceFont.setStyleHint(QFont::Monospace); monospaceFont.setPointSizeF(monospaceFont.pointSizeF() * 0.9); - auto deviceIdLayout = new QHBoxLayout; - deviceIdLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - - auto deviceIdLabel = new QLabel(tr("Device ID"), this); - deviceIdLabel->setFont(font); - deviceIdLabel->setMargin(0); deviceIdValue_ = new QLabel{this}; deviceIdValue_->setTextInteractionFlags(Qt::TextSelectableByMouse); deviceIdValue_->setFont(monospaceFont); - deviceIdLayout->addWidget(deviceIdLabel, 1); - deviceIdLayout->addWidget(deviceIdValue_); - - auto deviceFingerprintLayout = new QHBoxLayout; - deviceFingerprintLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto deviceFingerprintLabel = new QLabel(tr("Device Fingerprint"), this); - deviceFingerprintLabel->setFont(font); - deviceFingerprintLabel->setMargin(0); deviceFingerprintValue_ = new QLabel{this}; deviceFingerprintValue_->setTextInteractionFlags(Qt::TextSelectableByMouse); deviceFingerprintValue_->setFont(monospaceFont); - deviceFingerprintLayout->addWidget(deviceFingerprintLabel, 1); - deviceFingerprintLayout->addWidget(deviceFingerprintValue_); - auto sessionKeysLayout = new QHBoxLayout; - sessionKeysLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); - auto sessionKeysLabel = new QLabel(tr("Session Keys"), this); + deviceFingerprintValue_->setText(utils::humanReadableFingerprint(QString(44, 'X'))); + + auto sessionKeysLabel = new QLabel{tr("Session Keys"), this}; sessionKeysLabel->setFont(font); - sessionKeysLayout->addWidget(sessionKeysLabel, 1); + sessionKeysLabel->setMargin(OptionMargin); auto sessionKeysImportBtn = new QPushButton{tr("IMPORT"), this}; - connect( - sessionKeysImportBtn, &QPushButton::clicked, this, &UserSettingsPage::importSessionKeys); auto sessionKeysExportBtn = new QPushButton{tr("EXPORT"), this}; - connect( - sessionKeysExportBtn, &QPushButton::clicked, this, &UserSettingsPage::exportSessionKeys); + + auto sessionKeysLayout = new QHBoxLayout; + sessionKeysLayout->addWidget(new QLabel{"", this}, 1, Qt::AlignRight); sessionKeysLayout->addWidget(sessionKeysExportBtn, 0, Qt::AlignRight); sessionKeysLayout->addWidget(sessionKeysImportBtn, 0, Qt::AlignRight); - encryptionLayout_->addLayout(deviceIdLayout); - encryptionLayout_->addLayout(deviceFingerprintLayout); - encryptionLayout_->addWidget(new HorizontalLine{this}); - encryptionLayout_->addLayout(sessionKeysLayout); + auto boxWrap = [this, &font](QString labelText, QWidget *field) { + auto label = new QLabel{labelText, this}; + label->setFont(font); + label->setMargin(OptionMargin); - font.setWeight(QFont::Medium); + auto layout = new QHBoxLayout; + layout->addWidget(field, 0, Qt::AlignRight); - auto encryptionLabel_ = new QLabel(tr("ENCRYPTION"), this); - encryptionLabel_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); - encryptionLabel_->setFont(font); + formLayout_->addRow(label, layout); + }; - auto general_ = new QLabel(tr("GENERAL"), this); - general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); - general_->setFont(font); + formLayout_->addRow(general_); + formLayout_->addRow(new HorizontalLine{this}); + boxWrap(tr("Minimize to tray"), trayToggle_); + boxWrap(tr("Start in tray"), startInTrayToggle_); + formLayout_->addRow(new HorizontalLine{this}); + boxWrap(tr("Circular Avatars"), avatarCircles_); + boxWrap(tr("Group's sidebar"), groupViewToggle_); + boxWrap(tr("Typing notifications"), typingNotifications_); + formLayout_->addRow(new HorizontalLine{this}); + boxWrap(tr("Read receipts"), readReceipts_); + boxWrap(tr("Send messages as Markdown"), markdownEnabled_); + boxWrap(tr("Desktop notifications"), desktopNotifications_); + formLayout_->addRow(uiLabel_); + formLayout_->addRow(new HorizontalLine{this}); - mainLayout_ = new QVBoxLayout; - mainLayout_->setAlignment(Qt::AlignTop); - mainLayout_->setSpacing(7); - mainLayout_->setContentsMargins( - sideMargin_, LayoutTopMargin, sideMargin_, LayoutBottomMargin); - mainLayout_->addWidget(general_, 1, Qt::AlignLeft | Qt::AlignBottom); - mainLayout_->addWidget(new HorizontalLine(this)); - mainLayout_->addLayout(trayOptionLayout_); - mainLayout_->addLayout(startInTrayOptionLayout_); - mainLayout_->addWidget(new HorizontalLine(this)); - mainLayout_->addLayout(groupViewLayout); - mainLayout_->addWidget(new HorizontalLine(this)); - mainLayout_->addLayout(typingLayout); - mainLayout_->addLayout(receiptsLayout); - mainLayout_->addLayout(desktopLayout); - mainLayout_->addWidget(new HorizontalLine(this)); - -#if defined(Q_OS_MAC) - scaleFactorLabel->hide(); +#if !defined(Q_OS_MAC) + boxWrap(tr("Scale factor"), scaleFactorCombo_); +#else scaleFactorCombo_->hide(); #endif + boxWrap(tr("Font size"), fontSizeCombo_); + boxWrap(tr("Font Family"), fontSelectionCombo_); - mainLayout_->addLayout(scaleFactorOptionLayout); - mainLayout_->addLayout(fontSizeOptionLayout); - mainLayout_->addLayout(fontFamilyOptionLayout); - mainLayout_->addWidget(new HorizontalLine(this)); - mainLayout_->addLayout(themeOptionLayout_); - mainLayout_->addWidget(new HorizontalLine(this)); - - mainLayout_->addSpacing(50); +#if !defined(Q_OS_MAC) + boxWrap(tr("Emoji Font Family"), emojiFontSelectionCombo_); +#else + emojiFontSelectionCombo_->hide(); +#endif - mainLayout_->addWidget(encryptionLabel_, 1, Qt::AlignLeft | Qt::AlignBottom); - mainLayout_->addWidget(new HorizontalLine(this)); - mainLayout_->addLayout(encryptionLayout_); + boxWrap(tr("Theme"), themeCombo_); + formLayout_->addRow(encryptionLabel_); + formLayout_->addRow(new HorizontalLine{this}); + boxWrap(tr("Device ID"), deviceIdValue_); + boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_); + formLayout_->addRow(new HorizontalLine{this}); + formLayout_->addRow(sessionKeysLabel, sessionKeysLayout); - auto scrollArea_ = new QScrollArea(this); + auto scrollArea_ = new QScrollArea{this}; scrollArea_->setFrameShape(QFrame::NoFrame); scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); scrollArea_->setWidgetResizable(true); scrollArea_->setAlignment(Qt::AlignTop | Qt::AlignVCenter); - auto scrollAreaContents_ = new QWidget(this); + QScroller::grabGesture(scrollArea_, QScroller::TouchGesture); + + auto spacingAroundForm = new QHBoxLayout; + spacingAroundForm->addStretch(1); + spacingAroundForm->addLayout(formLayout_, 0); + spacingAroundForm->addStretch(1); + + auto scrollAreaContents_ = new QWidget{this}; scrollAreaContents_->setObjectName("UserSettingScrollWidget"); - scrollAreaContents_->setLayout(mainLayout_); + scrollAreaContents_->setLayout(spacingAroundForm); scrollArea_->setWidget(scrollAreaContents_); topLayout_->addLayout(topBarLayout_); @@ -392,6 +361,9 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge connect(fontSelectionCombo_, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated), [this](const QString &family) { settings_->setFontFamily(family.trimmed()); }); + connect(emojiFontSelectionCombo_, + static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated), + [this](const QString &family) { settings_->setEmojiFontFamily(family.trimmed()); }); connect(trayToggle_, &Toggle::toggled, this, [this](bool isDisabled) { settings_->setTray(!isDisabled); if (isDisabled) { @@ -410,6 +382,14 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge settings_->setGroupView(!isDisabled); }); + connect(avatarCircles_, &Toggle::toggled, this, [this](bool isDisabled) { + settings_->setAvatarCircles(!isDisabled); + }); + + connect(markdownEnabled_, &Toggle::toggled, this, [this](bool isDisabled) { + settings_->setMarkdownEnabled(!isDisabled); + }); + connect(typingNotifications_, &Toggle::toggled, this, [this](bool isDisabled) { settings_->setTypingNotifications(!isDisabled); }); @@ -422,6 +402,12 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge settings_->setDesktopNotifications(!isDisabled); }); + connect( + sessionKeysImportBtn, &QPushButton::clicked, this, &UserSettingsPage::importSessionKeys); + + connect( + sessionKeysExportBtn, &QPushButton::clicked, this, &UserSettingsPage::exportSessionKeys); + connect(backBtn_, &QPushButton::clicked, this, [this]() { settings_->save(); emit moveBack(); @@ -440,8 +426,10 @@ UserSettingsPage::showEvent(QShowEvent *) trayToggle_->setState(!settings_->isTrayEnabled()); startInTrayToggle_->setState(!settings_->isStartInTrayEnabled()); groupViewToggle_->setState(!settings_->isGroupViewEnabled()); + avatarCircles_->setState(!settings_->isAvatarCirclesEnabled()); typingNotifications_->setState(!settings_->isTypingNotificationsEnabled()); readReceipts_->setState(!settings_->isReadReceiptsEnabled()); + markdownEnabled_->setState(!settings_->isMarkdownEnabled()); desktopNotifications_->setState(!settings_->hasDesktopNotifications()); deviceIdValue_->setText(QString::fromStdString(http::client()->device_id())); @@ -450,16 +438,6 @@ UserSettingsPage::showEvent(QShowEvent *) } void -UserSettingsPage::resizeEvent(QResizeEvent *event) -{ - sideMargin_ = width() * 0.2; - mainLayout_->setContentsMargins( - sideMargin_, LayoutTopMargin, sideMargin_, LayoutBottomMargin); - - QWidget::resizeEvent(event); -} - -void UserSettingsPage::paintEvent(QPaintEvent *) { QStyleOption opt; @@ -471,7 +449,9 @@ UserSettingsPage::paintEvent(QPaintEvent *) void UserSettingsPage::importSessionKeys() { - auto fileName = QFileDialog::getOpenFileName(this, tr("Open Sessions File"), "", ""); + const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + const QString fileName = + QFileDialog::getOpenFileName(this, tr("Open Sessions File"), homeFolder, ""); QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { @@ -498,9 +478,9 @@ UserSettingsPage::importSessionKeys() } try { - auto sessions = mtx::crypto::decrypt_exported_sessions( - mtx::crypto::base642bin(payload), password.toStdString()); - cache::client()->importSessionKeys(std::move(sessions)); + auto sessions = + mtx::crypto::decrypt_exported_sessions(payload, password.toStdString()); + cache::importSessionKeys(std::move(sessions)); } catch (const mtx::crypto::sodium_exception &e) { QMessageBox::warning(this, tr("Error"), e.what()); } catch (const lmdb::error &e) { @@ -530,11 +510,12 @@ UserSettingsPage::exportSessionKeys() } // Open file dialog to save the file. - auto fileName = + const QString homeFolder = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + const QString fileName = QFileDialog::getSaveFileName(this, tr("File to save the exported session keys"), "", ""); QFile file(fileName); - if (!file.open(QIODevice::WriteOnly)) { + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { QMessageBox::warning(this, tr("Error"), file.errorString()); return; } @@ -542,11 +523,16 @@ UserSettingsPage::exportSessionKeys() // Export sessions & save to file. try { auto encrypted_blob = mtx::crypto::encrypt_exported_sessions( - cache::client()->exportSessionKeys(), password.toStdString()); + cache::exportSessionKeys(), password.toStdString()); - auto b64 = mtx::crypto::bin2base64(encrypted_blob); + QString b64 = QString::fromStdString(mtx::crypto::bin2base64(encrypted_blob)); - file.write(b64.data(), b64.size()); + QString prefix("-----BEGIN MEGOLM SESSION DATA-----"); + QString suffix("-----END MEGOLM SESSION DATA-----"); + QString newline("\n"); + QTextStream out(&file); + out << prefix << newline << b64 << newline << suffix; + file.close(); } catch (const mtx::crypto::sodium_exception &e) { QMessageBox::warning(this, tr("Error"), e.what()); } catch (const lmdb::error &e) { diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index 900f57e4..a1b7b084 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h
@@ -19,9 +19,11 @@ #include <QComboBox> #include <QFontDatabase> +#include <QFormLayout> #include <QFrame> #include <QLabel> #include <QLayout> +#include <QProcessEnvironment> #include <QSharedPointer> #include <QWidget> @@ -56,6 +58,7 @@ public: void setFontSize(double size); void setFontFamily(QString family); + void setEmojiFontFamily(QString family); void setGroupView(bool state) { @@ -66,6 +69,12 @@ public: save(); } + void setMarkdownEnabled(bool state) + { + isMarkdownEnabled_ = state; + save(); + } + void setReadReceipts(bool state) { isReadReceiptsEnabled_ = state; @@ -84,29 +93,46 @@ public: save(); } - QString theme() const { return !theme_.isEmpty() ? theme_ : "light"; } + void setAvatarCircles(bool state) + { + avatarCircles_ = state; + save(); + } + + QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; } bool isTrayEnabled() const { return isTrayEnabled_; } bool isStartInTrayEnabled() const { return isStartInTrayEnabled_; } bool isGroupViewEnabled() const { return isGroupViewEnabled_; } + bool isAvatarCirclesEnabled() const { return avatarCircles_; } + bool isMarkdownEnabled() const { return isMarkdownEnabled_; } bool isTypingNotificationsEnabled() const { return isTypingNotificationsEnabled_; } bool isReadReceiptsEnabled() const { return isReadReceiptsEnabled_; } bool hasDesktopNotifications() const { return hasDesktopNotifications_; } double fontSize() const { return baseFontSize_; } QString font() const { return font_; } + QString emojiFont() const { return emojiFont_; } signals: void groupViewStateChanged(bool state); private: + // Default to system theme if QT_QPA_PLATFORMTHEME var is set. + QString defaultTheme_ = + QProcessEnvironment::systemEnvironment().value("QT_QPA_PLATFORMTHEME", "").isEmpty() + ? "light" + : "system"; QString theme_; bool isTrayEnabled_; bool isStartInTrayEnabled_; bool isGroupViewEnabled_; + bool isMarkdownEnabled_; bool isTypingNotificationsEnabled_; bool isReadReceiptsEnabled_; bool hasDesktopNotifications_; + bool avatarCircles_; double baseFontSize_; QString font_; + QString emojiFont_; }; class HorizontalLine : public QFrame @@ -122,11 +148,10 @@ class UserSettingsPage : public QWidget Q_OBJECT public: - UserSettingsPage(QSharedPointer<UserSettings> settings, QWidget *parent = 0); + UserSettingsPage(QSharedPointer<UserSettings> settings, QWidget *parent = nullptr); protected: void showEvent(QShowEvent *event) override; - void resizeEvent(QResizeEvent *event) override; void paintEvent(QPaintEvent *event) override; signals: @@ -141,8 +166,8 @@ private slots: private: // Layouts QVBoxLayout *topLayout_; - QVBoxLayout *mainLayout_; QHBoxLayout *topBarLayout_; + QFormLayout *formLayout_; // Shared settings object. QSharedPointer<UserSettings> settings_; @@ -152,7 +177,9 @@ private: Toggle *groupViewToggle_; Toggle *typingNotifications_; Toggle *readReceipts_; + Toggle *markdownEnabled_; Toggle *desktopNotifications_; + Toggle *avatarCircles_; QLabel *deviceFingerprintValue_; QLabel *deviceIdValue_; @@ -160,6 +187,7 @@ private: QComboBox *scaleFactorCombo_; QComboBox *fontSizeCombo_; QComboBox *fontSelectionCombo_; + QComboBox *emojiFontSelectionCombo_; int sideMargin_ = 0; }; diff --git a/src/Utils.cpp b/src/Utils.cpp
index f8fdfaf9..33b75894 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp
@@ -3,25 +3,84 @@ #include <QApplication> #include <QComboBox> #include <QDesktopWidget> +#include <QGuiApplication> +#include <QProcessEnvironment> +#include <QScreen> #include <QSettings> #include <QTextDocument> #include <QXmlStreamReader> + #include <cmath> +#include <variant> -#include <boost/variant.hpp> #include <cmark.h> +#include "Cache.h" #include "Config.h" +#include "MatrixClient.h" using TimelineEvent = mtx::events::collections::TimelineEvents; QHash<QString, QString> authorColors_; +template<class T, class Event> +static DescInfo +createDescriptionInfo(const Event &event, const QString &localUser, const QString &room_id) +{ + const auto msg = std::get<T>(event); + const auto sender = QString::fromStdString(msg.sender); + + const auto username = cache::displayName(room_id, sender); + const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); + + return DescInfo{ + QString::fromStdString(msg.event_id), + sender, + utils::messageDescription<T>( + username, QString::fromStdString(msg.content.body).trimmed(), sender == localUser), + utils::descriptiveTime(ts), + ts}; +} + QString utils::localUser() { + return QString::fromStdString(http::client()->user_id().to_string()); +} + +QString +utils::replaceEmoji(const QString &body) +{ + QString fmtBody = ""; + + QVector<uint> utf32_string = body.toUcs4(); + QSettings settings; - return settings.value("auth/user_id").toString(); + QString userFontFamily = settings.value("user/emoji_font_family", "emoji").toString(); + + bool insideFontBlock = false; + for (auto &code : utf32_string) { + // TODO: Be more precise here. + if ((code >= 0x2600 && code <= 0x27bf) || (code >= 0x1f300 && code <= 0x1f3ff) || + (code >= 0x1f000 && code <= 0x1faff)) { + if (!insideFontBlock) { + fmtBody += QString("<font face=\"" + userFontFamily + "\">"); + insideFontBlock = true; + } + + } else { + if (insideFontBlock) { + fmtBody += "</font>"; + insideFontBlock = false; + } + } + fmtBody += QString::fromUcs4(&code, 1); + } + if (insideFontBlock) { + fmtBody += "</font>"; + } + + return fmtBody; } void @@ -37,7 +96,7 @@ utils::setScaleFactor(float factor) float utils::scaleFactor() { - QSettings settings("nheko", "nheko"); + QSettings settings; return settings.value("settings/scale_factor", -1).toFloat(); } @@ -74,13 +133,13 @@ utils::descriptiveTime(const QDateTime &then) const auto days = then.daysTo(now); if (days == 0) - return then.toString("HH:mm"); + return then.time().toString(Qt::DefaultLocaleShortDate); else if (days < 2) - return QString("Yesterday"); - else if (days < 365) - return then.toString("dd/MM"); + return QString(QCoreApplication::translate("descriptiveTime", "Yesterday")); + else if (days < 7) + return then.toString("dddd"); - return then.toString("dd/MM/yy"); + return then.date().toString(Qt::DefaultLocaleShortDate); } DescInfo @@ -97,39 +156,33 @@ utils::getMessageDescription(const TimelineEvent &event, using Video = mtx::events::RoomEvent<mtx::events::msg::Video>; using Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>; - if (boost::get<Audio>(&event) != nullptr) { + if (std::holds_alternative<Audio>(event)) { return createDescriptionInfo<Audio>(event, localUser, room_id); - } else if (boost::get<Emote>(&event) != nullptr) { + } else if (std::holds_alternative<Emote>(event)) { return createDescriptionInfo<Emote>(event, localUser, room_id); - } else if (boost::get<File>(&event) != nullptr) { + } else if (std::holds_alternative<File>(event)) { return createDescriptionInfo<File>(event, localUser, room_id); - } else if (boost::get<Image>(&event) != nullptr) { + } else if (std::holds_alternative<Image>(event)) { return createDescriptionInfo<Image>(event, localUser, room_id); - } else if (boost::get<Notice>(&event) != nullptr) { + } else if (std::holds_alternative<Notice>(event)) { return createDescriptionInfo<Notice>(event, localUser, room_id); - } else if (boost::get<Text>(&event) != nullptr) { + } else if (std::holds_alternative<Text>(event)) { return createDescriptionInfo<Text>(event, localUser, room_id); - } else if (boost::get<Video>(&event) != nullptr) { + } else if (std::holds_alternative<Video>(event)) { return createDescriptionInfo<Video>(event, localUser, room_id); - } else if (boost::get<mtx::events::Sticker>(&event) != nullptr) { + } else if (std::holds_alternative<mtx::events::Sticker>(event)) { return createDescriptionInfo<mtx::events::Sticker>(event, localUser, room_id); - } else if (boost::get<Encrypted>(&event) != nullptr) { - const auto msg = boost::get<Encrypted>(event); - const auto sender = QString::fromStdString(msg.sender); + } else if (auto msg = std::get_if<Encrypted>(&event); msg != nullptr) { + const auto sender = QString::fromStdString(msg->sender); - const auto username = Cache::displayName(room_id, sender); - const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); + const auto username = cache::displayName(room_id, sender); + const auto ts = QDateTime::fromMSecsSinceEpoch(msg->origin_server_ts); DescInfo info; - if (sender == localUser) - info.username = "You"; - else - info.username = username; - info.userid = sender; info.body = QString(" %1").arg(messageDescription<Encrypted>()); info.timestamp = utils::descriptiveTime(ts); - info.event_id = QString::fromStdString(msg.event_id); + info.event_id = QString::fromStdString(msg->event_id); info.datetime = ts; return info; @@ -197,30 +250,25 @@ utils::levenshtein_distance(const std::string &s1, const std::string &s2) } QString -utils::event_body(const mtx::events::collections::TimelineEvents &event) +utils::event_body(const mtx::events::collections::TimelineEvents &e) { using namespace mtx::events; - using namespace mtx::events::msg; + if (auto ev = std::get_if<RoomEvent<msg::Audio>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + if (auto ev = std::get_if<RoomEvent<msg::Emote>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + if (auto ev = std::get_if<RoomEvent<msg::File>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + if (auto ev = std::get_if<RoomEvent<msg::Image>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + if (auto ev = std::get_if<RoomEvent<msg::Notice>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + if (auto ev = std::get_if<RoomEvent<msg::Text>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); + if (auto ev = std::get_if<RoomEvent<msg::Video>>(&e); ev != nullptr) + return QString::fromStdString(ev->content.body); - if (boost::get<RoomEvent<Audio>>(&event) != nullptr) { - return message_body<RoomEvent<Audio>>(event); - } else if (boost::get<RoomEvent<Emote>>(&event) != nullptr) { - return message_body<RoomEvent<Emote>>(event); - } else if (boost::get<RoomEvent<File>>(&event) != nullptr) { - return message_body<RoomEvent<File>>(event); - } else if (boost::get<RoomEvent<Image>>(&event) != nullptr) { - return message_body<RoomEvent<Image>>(event); - } else if (boost::get<RoomEvent<Notice>>(&event) != nullptr) { - return message_body<RoomEvent<Notice>>(event); - } else if (boost::get<Sticker>(&event) != nullptr) { - return message_body<Sticker>(event); - } else if (boost::get<RoomEvent<Text>>(&event) != nullptr) { - return message_body<RoomEvent<Text>>(event); - } else if (boost::get<RoomEvent<Video>>(&event) != nullptr) { - return message_body<RoomEvent<Video>>(event); - } - - return QString(); + return ""; } QPixmap @@ -229,8 +277,10 @@ utils::scaleImageToPixmap(const QImage &img, int size) if (img.isNull()) return QPixmap(); + // Deprecated in 5.13: const double sz = + // std::ceil(QApplication::desktop()->screen()->devicePixelRatioF() * (double)size); const double sz = - std::ceil(QApplication::desktop()->screen()->devicePixelRatioF() * (double)size); + std::ceil(QGuiApplication::primaryScreen()->devicePixelRatio() * (double)size); return QPixmap::fromImage( img.scaled(sz, sz, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); } @@ -290,18 +340,72 @@ QString utils::linkifyMessage(const QString &body) { // Convert to valid XML. - auto doc = QString("<html>%1</html>").arg(body); + auto doc = body; doc.replace(conf::strings::url_regex, conf::strings::url_html); return doc; } QString +utils::escapeBlacklistedHtml(const QString &rawStr) +{ + static const std::array allowedTags = { + "font", "/font", "del", "/del", "h1", "/h1", "h2", "/h2", + "h3", "/h3", "h4", "/h4", "h5", "/h5", "h6", "/h6", + "blockquote", "/blockquote", "p", "/p", "a", "/a", "ul", "/ul", + "ol", "/ol", "sup", "/sup", "sub", "/sub", "li", "/li", + "b", "/b", "i", "/i", "u", "/u", "strong", "/strong", + "em", "/em", "strike", "/strike", "code", "/code", "hr", "/hr", + "br", "br/", "div", "/div", "table", "/table", "thead", "/thead", + "tbody", "/tbody", "tr", "/tr", "th", "/th", "td", "/td", + "caption", "/caption", "pre", "/pre", "span", "/span", "img", "/img"}; + QByteArray data = rawStr.toUtf8(); + QByteArray buffer; + const size_t length = data.size(); + buffer.reserve(length); + bool escapingTag = false; + for (size_t pos = 0; pos != length; ++pos) { + switch (data.at(pos)) { + case '<': { + bool oneTagMatched = false; + size_t endPos = std::min(static_cast<size_t>(data.indexOf('>', pos)), + static_cast<size_t>(data.indexOf(' ', pos))); + + auto mid = data.mid(pos + 1, endPos - pos - 1); + for (const auto &tag : allowedTags) { + // TODO: Check src and href attribute + if (mid.toLower() == tag) { + oneTagMatched = true; + } + } + if (oneTagMatched) + buffer.append('<'); + else { + escapingTag = true; + buffer.append("&lt;"); + } + break; + } + case '>': + if (escapingTag) { + buffer.append("&gt;"); + escapingTag = false; + } else + buffer.append('>'); + break; + default: + buffer.append(data.at(pos)); + break; + } + } + return QString::fromUtf8(buffer); +} + +QString utils::markdownToHtml(const QString &text) { - const auto str = text.toUtf8(); - const char *tmp_buf = - cmark_markdown_to_html(str.constData(), str.size(), CMARK_OPT_DEFAULT); + const auto str = text.toUtf8(); + const char *tmp_buf = cmark_markdown_to_html(str.constData(), str.size(), CMARK_OPT_UNSAFE); // Copy the null terminated output buffer. std::string html(tmp_buf); @@ -309,29 +413,99 @@ utils::markdownToHtml(const QString &text) // The buffer is no longer needed. free((char *)tmp_buf); - auto result = QString::fromStdString(html).trimmed(); + auto result = linkifyMessage(escapeBlacklistedHtml(QString::fromStdString(html))).trimmed(); + + if (result.count("<p>") == 1 && result.startsWith("<p>") && result.endsWith("</p>")) { + result = result.mid(3, result.size() - 3 - 4); + } return result; } QString +utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html) +{ + auto getFormattedBody = [related]() -> QString { + using MsgType = mtx::events::MessageType; + + switch (related.type) { + case MsgType::File: { + return "sent a file."; + } + case MsgType::Image: { + return "sent an image."; + } + case MsgType::Audio: { + return "sent an audio file."; + } + case MsgType::Video: { + return "sent a video"; + } + default: { + return related.quoted_formatted_body; + } + } + }; + return QString("<mx-reply><blockquote><a " + "href=\"https://matrix.to/#/%1/%2\">In reply " + "to</a> <a href=\"https://matrix.to/#/%3\">%4</a><br" + "/>%5</blockquote></mx-reply>") + .arg(related.room, + QString::fromStdString(related.related_event), + related.quoted_user, + related.quoted_user, + getFormattedBody()) + + html; +} + +QString +utils::getQuoteBody(const RelatedInfo &related) +{ + using MsgType = mtx::events::MessageType; + + switch (related.type) { + case MsgType::File: { + return "sent a file."; + } + case MsgType::Image: { + return "sent an image."; + } + case MsgType::Audio: { + return "sent an audio file."; + } + case MsgType::Video: { + return "sent a video"; + } + default: { + return related.quoted_body; + } + } +} + +QString utils::linkColor() { QSettings settings; - const auto theme = settings.value("user/theme", "light").toString(); + // Default to system theme if QT_QPA_PLATFORMTHEME var is set. + QString defaultTheme = + QProcessEnvironment::systemEnvironment().value("QT_QPA_PLATFORMTHEME", "").isEmpty() + ? "light" + : "system"; + const auto theme = settings.value("user/theme", defaultTheme).toString(); - if (theme == "light") + if (theme == "light") { return "#0077b5"; - else if (theme == "dark") + } else if (theme == "dark") { return "#38A3D8"; - - return QPalette().color(QPalette::Link).name(); + } else { + return QPalette().color(QPalette::Link).name(); + } } -int +uint32_t utils::hashQString(const QString &input) { - auto hash = 0; + uint32_t hash = 0; for (int i = 0; i < input.length(); i++) { hash = input.at(i).digitValue() + ((hash << 5) - hash); @@ -349,7 +523,7 @@ utils::generateContrastingHexColor(const QString &input, const QString &backgrou // Create a color for the input auto hash = hashQString(input); // create a hue value based on the hash of the input. - auto userHue = qAbs(hash % 360); + auto userHue = static_cast<int>(qAbs(hash % 360)); // start with moderate saturation and lightness values. auto sat = 220; auto lightness = 125; @@ -457,11 +631,13 @@ utils::centerWidget(QWidget *widget, QWidget *parent) }; if (parent) { - widget->move(findCenter(parent->geometry())); + widget->move(parent->window()->frameGeometry().topLeft() + + parent->window()->rect().center() - widget->rect().center()); return; } - widget->move(findCenter(QApplication::desktop()->screenGeometry())); + // Deprecated in 5.13: widget->move(findCenter(QApplication::desktop()->screenGeometry())); + widget->move(findCenter(QGuiApplication::primaryScreen()->geometry())); } void @@ -474,17 +650,3 @@ utils::restoreCombobox(QComboBox *combo, const QString &value) } } } - -utils::SideBarSizes -utils::calculateSidebarSizes(const QFont &f) -{ - const auto height = static_cast<double>(QFontMetrics{f}.lineSpacing()); - - SideBarSizes sz; - sz.small = std::ceil(3.5 * height + height / 4.0); - sz.normal = std::ceil(16 * height); - sz.groups = std::ceil(3 * height); - sz.collapsePoint = 2 * sz.normal; - - return sz; -} diff --git a/src/Utils.h b/src/Utils.h
index 8672e7d4..a3854dd8 100644 --- a/src/Utils.h +++ b/src/Utils.h
@@ -1,14 +1,8 @@ #pragma once -#include <boost/variant.hpp> - -#include "Cache.h" -#include "RoomInfoListItem.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" +#include <variant> +#include <QCoreApplication> #include <QDateTime> #include <QPixmap> #include <mtx/events/collections.hpp> @@ -16,13 +10,36 @@ #include <qmath.h> +struct DescInfo; + +namespace cache { +// Forward declarations to prevent dependency on Cache.h, since this header is included often! +QString +displayName(const QString &room_id, const QString &user_id); +} + class QComboBox; +// Contains information about related events for +// outgoing messages +struct RelatedInfo +{ + using MsgType = mtx::events::MessageType; + MsgType type; + QString room; + QString quoted_body, quoted_formatted_body; + std::string related_event; + QString quoted_user; +}; + namespace utils { using TimelineEvent = mtx::events::collections::TimelineEvents; QString +replaceEmoji(const QString &body); + +QString localUser(); float @@ -64,7 +81,9 @@ event_body(const mtx::events::collections::TimelineEvents &event); //! Match widgets/events with a description message. template<class T> QString -messageDescription(const QString &username = "", const QString &body = "") +messageDescription(const QString &username = "", + const QString &body = "", + const bool isLocal = false) { using Audio = mtx::events::RoomEvent<mtx::events::msg::Audio>; using Emote = mtx::events::RoomEvent<mtx::events::msg::Emote>; @@ -76,51 +95,75 @@ messageDescription(const QString &username = "", const QString &body = "") using Video = mtx::events::RoomEvent<mtx::events::msg::Video>; using Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>; - if (std::is_same<T, AudioItem>::value || std::is_same<T, Audio>::value) - return QString("sent an audio clip"); - else if (std::is_same<T, ImageItem>::value || std::is_same<T, Image>::value) - return QString("sent an image"); - else if (std::is_same<T, FileItem>::value || std::is_same<T, File>::value) - return QString("sent a file"); - else if (std::is_same<T, VideoItem>::value || std::is_same<T, Video>::value) - return QString("sent a video clip"); - else if (std::is_same<T, StickerItem>::value || std::is_same<T, Sticker>::value) - return QString("sent a sticker"); - else if (std::is_same<T, Notice>::value) - return QString("sent a notification"); - else if (std::is_same<T, Text>::value) - return QString(": %1").arg(body); - else if (std::is_same<T, Emote>::value) + if (std::is_same<T, Audio>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent an audio clip"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent an audio clip") + .arg(username); + } else if (std::is_same<T, Image>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent an image"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent an image") + .arg(username); + } else if (std::is_same<T, File>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent a file"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent a file") + .arg(username); + } else if (std::is_same<T, Video>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent a video"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent a video") + .arg(username); + } else if (std::is_same<T, Sticker>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent a sticker"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent a sticker") + .arg(username); + } else if (std::is_same<T, Notice>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent a notification"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent a notification") + .arg(username); + } else if (std::is_same<T, Text>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", "You: %1") + .arg(body); + else + return QCoreApplication::translate("message-description sent:", "%1: %2") + .arg(username) + .arg(body); + } else if (std::is_same<T, Emote>::value) { return QString("* %1 %2").arg(username).arg(body); - else if (std::is_same<T, Encrypted>::value) - return QString("sent an encrypted message"); -} - -template<class T, class Event> -DescInfo -createDescriptionInfo(const Event &event, const QString &localUser, const QString &room_id) -{ - using Text = mtx::events::RoomEvent<mtx::events::msg::Text>; - using Emote = mtx::events::RoomEvent<mtx::events::msg::Emote>; - - const auto msg = boost::get<T>(event); - const auto sender = QString::fromStdString(msg.sender); - - const auto username = Cache::displayName(room_id, sender); - const auto ts = QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts); - - bool isText = std::is_same<T, Text>::value; - bool isEmote = std::is_same<T, Emote>::value; - - return DescInfo{ - QString::fromStdString(msg.event_id), - isEmote ? "" : (sender == localUser ? "You" : username), - sender, - (isText || isEmote) - ? messageDescription<T>(username, QString::fromStdString(msg.content.body).trimmed()) - : QString(" %1").arg(messageDescription<T>()), - utils::descriptiveTime(ts), - ts}; + } else if (std::is_same<T, Encrypted>::value) { + if (isLocal) + return QCoreApplication::translate("message-description sent:", + "You sent an encrypted message"); + else + return QCoreApplication::translate("message-description sent:", + "%1 sent an encrypted message") + .arg(username); + } else { + return QCoreApplication::translate("utils", "Unknown Message Type"); + } } //! Scale down an image to fit to the given width & height limitations. @@ -143,25 +186,25 @@ erase_if(ContainerT &items, const PredicateT &predicate) inline uint64_t event_timestamp(const mtx::events::collections::TimelineEvents &event) { - return boost::apply_visitor([](auto msg) { return msg.origin_server_ts; }, event); + return std::visit([](auto msg) { return msg.origin_server_ts; }, event); } inline nlohmann::json serialize_event(const mtx::events::collections::TimelineEvents &event) { - return boost::apply_visitor([](auto msg) { return json(msg); }, event); + return std::visit([](auto msg) { return json(msg); }, event); } inline mtx::events::EventType event_type(const mtx::events::collections::TimelineEvents &event) { - return boost::apply_visitor([](auto msg) { return msg.type; }, event); + return std::visit([](auto msg) { return msg.type; }, event); } inline std::string event_id(const mtx::events::collections::TimelineEvents &event) { - return boost::apply_visitor([](auto msg) { return msg.event_id; }, event); + return std::visit([](auto msg) { return msg.event_id; }, event); } inline QString @@ -173,15 +216,14 @@ eventId(const mtx::events::collections::TimelineEvents &event) inline QString event_sender(const mtx::events::collections::TimelineEvents &event) { - return boost::apply_visitor([](auto msg) { return QString::fromStdString(msg.sender); }, - event); + return std::visit([](auto msg) { return QString::fromStdString(msg.sender); }, event); } template<class T> QString message_body(const mtx::events::collections::TimelineEvents &event) { - return QString::fromStdString(boost::get<T>(event).content.body); + return QString::fromStdString(std::get<T>(event).content.body); } //! Calculate the Levenshtein distance between two strings with character skipping. @@ -211,7 +253,7 @@ getMessageBody(const RoomMessageT &event) if (event.content.format.empty()) return QString::fromStdString(event.content.body).toHtmlEscaped(); - if (event.content.format != common::FORMAT_MSG_TYPE) + if (event.content.format != mtx::common::FORMAT_MSG_TYPE) return QString::fromStdString(event.content.body).toHtmlEscaped(); return QString::fromStdString(event.content.formatted_body); @@ -225,12 +267,24 @@ linkifyMessage(const QString &body); QString markdownToHtml(const QString &text); +//! Escape every html tag, that was not whitelisted +QString +escapeBlacklistedHtml(const QString &data); + +//! Generate a Rich Reply quote message +QString +getFormattedQuoteBody(const RelatedInfo &related, const QString &html); + +//! Get the body for the quote, depending on the event type. +QString +getQuoteBody(const RelatedInfo &related); + //! Retrieve the color of the links based on the current theme. QString linkColor(); //! Returns the hash code of the input QString -int +uint32_t hashQString(const QString &input); //! Generate a color (matching #RRGGBB) that has an acceptable contrast to background that is based @@ -253,14 +307,4 @@ centerWidget(QWidget *widget, QWidget *parent); void restoreCombobox(QComboBox *combo, const QString &value); -struct SideBarSizes -{ - int small; - int normal; - int groups; - int collapsePoint; -}; - -SideBarSizes -calculateSidebarSizes(const QFont &f); } diff --git a/src/WelcomePage.cpp b/src/WelcomePage.cpp
index 8c3f6487..e4b0e1c6 100644 --- a/src/WelcomePage.cpp +++ b/src/WelcomePage.cpp
@@ -17,6 +17,7 @@ #include <QLabel> #include <QLayout> +#include <QPainter> #include <QStyleOption> #include "Config.h" diff --git a/src/WelcomePage.h b/src/WelcomePage.h
index 480dc702..ae660215 100644 --- a/src/WelcomePage.h +++ b/src/WelcomePage.h
@@ -7,7 +7,7 @@ class WelcomePage : public QWidget Q_OBJECT public: - explicit WelcomePage(QWidget *parent = 0); + explicit WelcomePage(QWidget *parent = nullptr); protected: void paintEvent(QPaintEvent *) override; diff --git a/src/dialogs/CreateRoom.h b/src/dialogs/CreateRoom.h
index 22ac6a43..a482a636 100644 --- a/src/dialogs/CreateRoom.h +++ b/src/dialogs/CreateRoom.h
@@ -2,7 +2,7 @@ #include <QFrame> -#include <mtx.hpp> +#include <mtx/requests.hpp> class QPushButton; class TextField; diff --git a/src/dialogs/FallbackAuth.cpp b/src/dialogs/FallbackAuth.cpp new file mode 100644
index 00000000..a0633c1e --- /dev/null +++ b/src/dialogs/FallbackAuth.cpp
@@ -0,0 +1,69 @@ +#include <QDesktopServices> +#include <QLabel> +#include <QPushButton> +#include <QUrl> +#include <QVBoxLayout> + +#include "dialogs/FallbackAuth.h" + +#include "Config.h" +#include "MatrixClient.h" + +using namespace dialogs; + +FallbackAuth::FallbackAuth(const QString &authType, const QString &session, QWidget *parent) + : QWidget(parent) +{ + setAutoFillBackground(true); + setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); + setWindowModality(Qt::WindowModal); + setAttribute(Qt::WA_DeleteOnClose, true); + + auto layout = new QVBoxLayout(this); + layout->setSpacing(conf::modals::WIDGET_SPACING); + layout->setMargin(conf::modals::WIDGET_MARGIN); + + auto buttonLayout = new QHBoxLayout(); + buttonLayout->setSpacing(8); + buttonLayout->setMargin(0); + + openBtn_ = new QPushButton(tr("Open Fallback in Browser"), this); + cancelBtn_ = new QPushButton(tr("Cancel"), this); + confirmBtn_ = new QPushButton(tr("Confirm"), this); + confirmBtn_->setDefault(true); + + buttonLayout->addStretch(1); + buttonLayout->addWidget(openBtn_); + buttonLayout->addWidget(cancelBtn_); + buttonLayout->addWidget(confirmBtn_); + + QFont font; + font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO); + + auto label = new QLabel( + tr("Open the fallback, follow the steps and confirm after completing them."), this); + label->setFont(font); + + layout->addWidget(label); + layout->addLayout(buttonLayout); + + connect(openBtn_, &QPushButton::clicked, [session, authType]() { + const auto url = QString("https://%1:%2/_matrix/client/r0/auth/%4/" + "fallback/web?session=%3") + .arg(QString::fromStdString(http::client()->server())) + .arg(http::client()->port()) + .arg(session) + .arg(authType); + + QDesktopServices::openUrl(url); + }); + + connect(confirmBtn_, &QPushButton::clicked, this, [this]() { + emit confirmation(); + emit close(); + }); + connect(cancelBtn_, &QPushButton::clicked, this, [this]() { + emit cancel(); + emit close(); + }); +} diff --git a/src/dialogs/FallbackAuth.h b/src/dialogs/FallbackAuth.h new file mode 100644
index 00000000..245fa03e --- /dev/null +++ b/src/dialogs/FallbackAuth.h
@@ -0,0 +1,26 @@ +#pragma once + +#include <QWidget> + +class QPushButton; +class QLabel; + +namespace dialogs { + +class FallbackAuth : public QWidget +{ + Q_OBJECT + +public: + FallbackAuth(const QString &authType, const QString &session, QWidget *parent = nullptr); + +signals: + void confirmation(); + void cancel(); + +private: + QPushButton *openBtn_; + QPushButton *confirmBtn_; + QPushButton *cancelBtn_; +}; +} // dialogs diff --git a/src/dialogs/ImageOverlay.cpp b/src/dialogs/ImageOverlay.cpp
index dbf5bbe4..e075fb67 100644 --- a/src/dialogs/ImageOverlay.cpp +++ b/src/dialogs/ImageOverlay.cpp
@@ -17,7 +17,9 @@ #include <QApplication> #include <QDesktopWidget> +#include <QGuiApplication> #include <QPainter> +#include <QScreen> #include "dialogs/ImageOverlay.h" @@ -30,7 +32,7 @@ ImageOverlay::ImageOverlay(QPixmap image, QWidget *parent) , originalImage_{image} { setMouseTracking(true); - setParent(0); + setParent(nullptr); setWindowFlags(windowFlags() | Qt::FramelessWindowHint); @@ -39,11 +41,6 @@ ImageOverlay::ImageOverlay(QPixmap image, QWidget *parent) setAttribute(Qt::WA_DeleteOnClose, true); setWindowState(Qt::WindowFullScreen); - screen_ = QApplication::desktop()->availableGeometry(); - - move(QApplication::desktop()->mapToGlobal(screen_.topLeft())); - resize(screen_.size()); - connect(this, SIGNAL(closing()), this, SLOT(close())); raise(); @@ -58,15 +55,15 @@ ImageOverlay::paintEvent(QPaintEvent *event) painter.setRenderHint(QPainter::Antialiasing); // Full screen overlay. - painter.fillRect(QRect(0, 0, screen_.width(), screen_.height()), QColor(55, 55, 55, 170)); + painter.fillRect(QRect(0, 0, width(), height()), QColor(55, 55, 55, 170)); // Left and Right margins - int outer_margin = screen_.width() * 0.12; + int outer_margin = width() * 0.12; int buttonSize = 36; int margin = outer_margin * 0.1; - int max_width = screen_.width() - 2 * outer_margin; - int max_height = screen_.height(); + int max_width = width() - 2 * outer_margin; + int max_height = height(); image_ = utils::scaleDown(max_width, max_height, originalImage_); @@ -74,10 +71,9 @@ ImageOverlay::paintEvent(QPaintEvent *event) int diff_y = max_height - image_.height(); content_ = QRect(outer_margin + diff_x / 2, diff_y / 2, image_.width(), image_.height()); - close_button_ = - QRect(screen_.width() - margin - buttonSize, margin, buttonSize, buttonSize); + close_button_ = QRect(width() - margin - buttonSize, margin, buttonSize, buttonSize); save_button_ = - QRect(screen_.width() - (2 * margin) - (2 * buttonSize), margin, buttonSize, buttonSize); + QRect(width() - (2 * margin) - (2 * buttonSize), margin, buttonSize, buttonSize); // Draw main content_. painter.drawPixmap(content_, image_); diff --git a/src/dialogs/ImageOverlay.h b/src/dialogs/ImageOverlay.h
index 26257fc1..bf566ce4 100644 --- a/src/dialogs/ImageOverlay.h +++ b/src/dialogs/ImageOverlay.h
@@ -44,6 +44,5 @@ private: QRect content_; QRect close_button_; QRect save_button_; - QRect screen_; }; } // dialogs diff --git a/src/dialogs/InviteUsers.cpp b/src/dialogs/InviteUsers.cpp
index bacfe498..691035ce 100644 --- a/src/dialogs/InviteUsers.cpp +++ b/src/dialogs/InviteUsers.cpp
@@ -13,7 +13,7 @@ #include "InviteeItem.h" #include "ui/TextField.h" -#include "mtx.hpp" +#include <mtx/identifiers.hpp> using namespace dialogs; diff --git a/src/dialogs/MemberList.cpp b/src/dialogs/MemberList.cpp
index f4167143..54e7bf96 100644 --- a/src/dialogs/MemberList.cpp +++ b/src/dialogs/MemberList.cpp
@@ -1,4 +1,5 @@ #include <QAbstractSlider> +#include <QLabel> #include <QListWidgetItem> #include <QPainter> #include <QPushButton> @@ -9,10 +10,10 @@ #include "dialogs/MemberList.h" -#include "AvatarProvider.h" #include "Cache.h" #include "ChatPage.h" #include "Config.h" +#include "Logging.h" #include "Utils.h" #include "ui/Avatar.h" @@ -28,17 +29,10 @@ MemberItem::MemberItem(const RoomMember &member, QWidget *parent) textLayout_->setMargin(0); textLayout_->setSpacing(0); - avatar_ = new Avatar(this); - avatar_->setSize(44); + avatar_ = new Avatar(this, 44); avatar_->setLetter(utils::firstChar(member.display_name)); - if (!member.avatar.isNull()) - avatar_->setImage(member.avatar); - else - AvatarProvider::resolve(ChatPage::instance()->currentRoom(), - member.user_id, - this, - [this](const QImage &img) { avatar_->setImage(img); }); + avatar_->setImage(ChatPage::instance()->currentRoom(), member.user_id); QFont nameFont; nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1); @@ -97,7 +91,7 @@ MemberList::MemberList(const QString &room_id, QWidget *parent) topLabel_->setAlignment(Qt::AlignCenter); topLabel_->setFont(font); - auto okBtn = new QPushButton("OK", this); + auto okBtn = new QPushButton(tr("OK"), this); auto buttonLayout = new QHBoxLayout(); buttonLayout->setSpacing(15); @@ -117,16 +111,16 @@ MemberList::MemberList(const QString &room_id, QWidget *parent) const size_t numMembers = list_->count() - 1; if (numMembers > 0) - addUsers(cache::client()->getMembers(room_id_.toStdString(), numMembers)); + addUsers(cache::getMembers(room_id_.toStdString(), numMembers)); }); try { - addUsers(cache::client()->getMembers(room_id_.toStdString())); + addUsers(cache::getMembers(room_id_.toStdString())); } catch (const lmdb::error &e) { - qCritical() << e.what(); + nhlog::db()->critical("Failed to retrieve members from cache: {}", e.what()); } - auto closeShortcut = new QShortcut(QKeySequence(tr("ESC")), this); + auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this); connect(closeShortcut, &QShortcut::activated, this, &MemberList::close); connect(okBtn, &QPushButton::clicked, this, &MemberList::close); } diff --git a/src/dialogs/PreviewUploadOverlay.cpp b/src/dialogs/PreviewUploadOverlay.cpp
index c404799e..42558d67 100644 --- a/src/dialogs/PreviewUploadOverlay.cpp +++ b/src/dialogs/PreviewUploadOverlay.cpp
@@ -15,7 +15,6 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#include <QApplication> #include <QBuffer> #include <QFile> #include <QFileInfo> @@ -135,6 +134,28 @@ PreviewUploadOverlay::setLabels(const QString &type, const QString &mime, uint64 } void +PreviewUploadOverlay::setPreview(const QImage &src, const QString &mime) +{ + auto const &split = mime.split('/'); + auto const &type = split[1]; + + QBuffer buffer(&data_); + buffer.open(QIODevice::WriteOnly); + if (src.save(&buffer, type.toStdString().c_str())) + titleLabel_.setText(QString{tr(DEFAULT)}.arg("image")); + else + titleLabel_.setText(QString{tr(ERR_MSG)}.arg(type)); + + mediaType_ = split[0]; + filePath_ = "clipboard." + type; + image_.convertFromImage(src); + isImage_ = true; + + titleLabel_.setText(QString{tr(DEFAULT)}.arg("image")); + init(); +} + +void PreviewUploadOverlay::setPreview(const QByteArray data, const QString &mime) { auto const &split = mime.split('/'); diff --git a/src/dialogs/PreviewUploadOverlay.h b/src/dialogs/PreviewUploadOverlay.h
index 8099d9c2..11cd49bc 100644 --- a/src/dialogs/PreviewUploadOverlay.h +++ b/src/dialogs/PreviewUploadOverlay.h
@@ -17,6 +17,7 @@ #pragma once +#include <QImage> #include <QLabel> #include <QLineEdit> #include <QPixmap> @@ -33,6 +34,7 @@ class PreviewUploadOverlay : public QWidget public: PreviewUploadOverlay(QWidget *parent = nullptr); + void setPreview(const QImage &src, const QString &mime); void setPreview(const QByteArray data, const QString &mime); void setPreview(const QString &path); diff --git a/src/dialogs/ReCaptcha.cpp b/src/dialogs/ReCaptcha.cpp
index 7849aa4f..21dc8c77 100644 --- a/src/dialogs/ReCaptcha.cpp +++ b/src/dialogs/ReCaptcha.cpp
@@ -60,5 +60,8 @@ ReCaptcha::ReCaptcha(const QString &session, QWidget *parent) emit confirmation(); emit close(); }); - connect(cancelBtn_, &QPushButton::clicked, this, &dialogs::ReCaptcha::close); + connect(cancelBtn_, &QPushButton::clicked, this, [this]() { + emit cancel(); + emit close(); + }); } diff --git a/src/dialogs/ReCaptcha.h b/src/dialogs/ReCaptcha.h
index f8407640..88ff3722 100644 --- a/src/dialogs/ReCaptcha.h +++ b/src/dialogs/ReCaptcha.h
@@ -15,6 +15,7 @@ public: signals: void confirmation(); + void cancel(); private: QPushButton *openCaptchaBtn_; diff --git a/src/dialogs/ReadReceipts.cpp b/src/dialogs/ReadReceipts.cpp
index dc4145db..0edd1ebf 100644 --- a/src/dialogs/ReadReceipts.cpp +++ b/src/dialogs/ReadReceipts.cpp
@@ -35,10 +35,9 @@ ReceiptItem::ReceiptItem(QWidget *parent, QFont nameFont; nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1); - auto displayName = Cache::displayName(room_id, user_id); + auto displayName = cache::displayName(room_id, user_id); - avatar_ = new Avatar(this); - avatar_->setSize(44); + avatar_ = new Avatar(this, 44); avatar_->setLetter(utils::firstChar(displayName)); // If it's a matrix id we use the second letter. @@ -56,10 +55,7 @@ ReceiptItem::ReceiptItem(QWidget *parent, topLayout_->addWidget(avatar_); topLayout_->addLayout(textLayout_, 1); - AvatarProvider::resolve(ChatPage::instance()->currentRoom(), - user_id, - this, - [this](const QImage &img) { avatar_->setImage(img); }); + avatar_->setImage(ChatPage::instance()->currentRoom(), user_id); } void @@ -78,13 +74,15 @@ ReceiptItem::dateFormat(const QDateTime &then) const auto days = then.daysTo(now); if (days == 0) - return QString("Today %1").arg(then.toString("HH:mm")); + return tr("Today %1").arg(then.time().toString(Qt::DefaultLocaleShortDate)); else if (days < 2) - return QString("Yesterday %1").arg(then.toString("HH:mm")); - else if (days < 365) - return then.toString("dd/MM HH:mm"); + return tr("Yesterday %1").arg(then.time().toString(Qt::DefaultLocaleShortDate)); + else if (days < 7) + return QString("%1 %2") + .arg(then.toString("dddd")) + .arg(then.time().toString(Qt::DefaultLocaleShortDate)); - return then.toString("dd/MM/yy"); + return then.toString(Qt::DefaultLocaleShortDate); } ReadReceipts::ReadReceipts(QWidget *parent) @@ -131,7 +129,7 @@ ReadReceipts::ReadReceipts(QWidget *parent) layout->addWidget(userList_); layout->addLayout(buttonLayout); - auto closeShortcut = new QShortcut(QKeySequence(tr("ESC")), this); + auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this); connect(closeShortcut, &QShortcut::activated, this, &ReadReceipts::close); connect(okBtn, &QPushButton::clicked, this, &ReadReceipts::close); } diff --git a/src/dialogs/ReadReceipts.h b/src/dialogs/ReadReceipts.h
index 8e1b6b75..e298af0a 100644 --- a/src/dialogs/ReadReceipts.h +++ b/src/dialogs/ReadReceipts.h
@@ -22,7 +22,7 @@ public: const QString &room_id); protected: - void paintEvent(QPaintEvent *); + void paintEvent(QPaintEvent *) override; private: QString dateFormat(const QDateTime &then) const; diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp
index f9b7e913..cc10ac91 100644 --- a/src/dialogs/RoomSettings.cpp +++ b/src/dialogs/RoomSettings.cpp
@@ -11,11 +11,13 @@ #include <QPushButton> #include <QShortcut> #include <QShowEvent> +#include <QStandardPaths> #include <QStyleOption> #include <QVBoxLayout> #include "dialogs/RoomSettings.h" +#include "Cache.h" #include "ChatPage.h" #include "Config.h" #include "Logging.h" @@ -199,12 +201,112 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) Qt::AlignBottom | Qt::AlignLeft); roomIdLayout->addWidget(roomIdLabel, 0, Qt::AlignBottom | Qt::AlignRight); + auto roomVersionLabel = new QLabel(QString::fromStdString(info_.version), this); + roomVersionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + roomVersionLabel->setFont(monospaceFont); + + auto roomVersionLayout = new QHBoxLayout; + roomVersionLayout->setMargin(0); + roomVersionLayout->addWidget(new QLabel(tr("Room Version"), this), + Qt::AlignBottom | Qt::AlignLeft); + roomVersionLayout->addWidget(roomVersionLabel, 0, Qt::AlignBottom | Qt::AlignRight); + auto notifLabel = new QLabel(tr("Notifications"), this); - auto notifCombo = new QComboBox(this); - notifCombo->setDisabled(true); - notifCombo->addItem(tr("Muted")); - notifCombo->addItem(tr("Mentions only")); - notifCombo->addItem(tr("All messages")); + notifCombo = new QComboBox(this); + notifCombo->addItem(tr( + "Muted")); //{"conditions":[{"kind":"event_match","key":"room_id","pattern":"!jxlRxnrZCsjpjDubDX:matrix.org"}],"actions":["dont_notify"]} + notifCombo->addItem(tr("Mentions only")); // {"actions":["dont_notify"]} + notifCombo->addItem(tr("All messages")); // delete rule + + connect(this, &RoomSettings::notifChanged, notifCombo, &QComboBox::setCurrentIndex); + http::client()->get_pushrules( + "global", + "override", + room_id_.toStdString(), + [this](const mtx::pushrules::PushRule &rule, mtx::http::RequestErr &err) { + if (err) { + if (err->status_code == boost::beast::http::status::not_found) + http::client()->get_pushrules( + "global", + "room", + room_id_.toStdString(), + [this](const mtx::pushrules::PushRule &rule, + mtx::http::RequestErr &err) { + if (err) { + emit notifChanged(2); // all messages + return; + } + + if (rule.enabled) + emit notifChanged(1); // mentions only + }); + return; + } + + if (rule.enabled) + emit notifChanged(0); // muted + else + emit notifChanged(2); // all messages + }); + + connect(notifCombo, QOverload<int>::of(&QComboBox::activated), [this](int index) { + std::string room_id = room_id_.toStdString(); + if (index == 0) { + // mute room + // delete old rule first, then add new rule + mtx::pushrules::PushRule rule; + rule.actions = {mtx::pushrules::actions::dont_notify{}}; + mtx::pushrules::PushCondition condition; + condition.kind = "event_match"; + condition.key = "room_id"; + condition.pattern = room_id; + rule.conditions = {condition}; + + http::client()->put_pushrules( + "global", + "override", + room_id, + rule, + [room_id](mtx::http::RequestErr &err) { + if (err) + nhlog::net()->error( + "failed to set pushrule for room {}: {} {}", + room_id, + static_cast<int>(err->status_code), + err->matrix_error.error); + http::client()->delete_pushrules( + "global", "room", room_id, [room_id](mtx::http::RequestErr &) { + }); + }); + } else if (index == 1) { + // mentions only + // delete old rule first, then add new rule + mtx::pushrules::PushRule rule; + rule.actions = {mtx::pushrules::actions::dont_notify{}}; + http::client()->put_pushrules( + "global", "room", room_id, rule, [room_id](mtx::http::RequestErr &err) { + if (err) + nhlog::net()->error( + "failed to set pushrule for room {}: {} {}", + room_id, + static_cast<int>(err->status_code), + err->matrix_error.error); + http::client()->delete_pushrules( + "global", + "override", + room_id, + [room_id](mtx::http::RequestErr &) {}); + }); + } else { + // all messages + http::client()->delete_pushrules( + "global", "override", room_id, [room_id](mtx::http::RequestErr &) { + http::client()->delete_pushrules( + "global", "room", room_id, [room_id](mtx::http::RequestErr &) { + }); + }); + } + }); auto notifOptionLayout_ = new QHBoxLayout; notifOptionLayout_->setMargin(0); @@ -238,10 +340,10 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) switch (index) { case 0: case 1: - event.join_rule = JoinRule::Public; + event.join_rule = state::JoinRule::Public; break; default: - event.join_rule = JoinRule::Invite; + event.join_rule = state::JoinRule::Invite; } return event; @@ -250,7 +352,7 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) updateAccessRules(room_id_.toStdString(), join_rule, guest_access); }); - if (info_.join_rule == JoinRule::Public) { + if (info_.join_rule == state::JoinRule::Public) { if (info_.guest_access) { accessCombo->setCurrentIndex(0); } else { @@ -332,7 +434,7 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) } // Hide encryption option for public rooms. - if (!usesEncryption_ && (info_.join_rule == JoinRule::Public)) { + if (!usesEncryption_ && (info_.join_rule == state::JoinRule::Public)) { encryptionToggle_->hide(); encryptionLabel->hide(); @@ -340,12 +442,10 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) keyRequestsToggle_->hide(); } - avatar_ = new Avatar(this); - avatar_->setSize(128); - if (avatarImg_.isNull()) - avatar_->setLetter(utils::firstChar(QString::fromStdString(info_.name))); - else - avatar_->setImage(avatarImg_); + avatar_ = new Avatar(this, 128); + avatar_->setLetter(utils::firstChar(QString::fromStdString(info_.name))); + if (!info_.avatar_url.empty()) + avatar_->setImage(QString::fromStdString(info_.avatar_url)); if (canChangeAvatar(room_id_.toStdString(), utils::localUser().toStdString())) { auto filter = new ClickableFilter(this); @@ -400,6 +500,7 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) layout->addLayout(keyRequestsLayout); layout->addWidget(infoLabel, Qt::AlignLeft); layout->addLayout(roomIdLayout); + layout->addLayout(roomVersionLayout); layout->addWidget(errorLabel_); layout->addLayout(buttonLayout); layout->addLayout(spinnerLayout); @@ -427,7 +528,7 @@ RoomSettings::RoomSettings(const QString &room_id, QWidget *parent) resetErrorLabel(); }); - auto closeShortcut = new QShortcut(QKeySequence(tr("ESC")), this); + auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this); connect(closeShortcut, &QShortcut::activated, this, &RoomSettings::close); connect(okBtn, &QPushButton::clicked, this, &RoomSettings::close); } @@ -474,10 +575,10 @@ void RoomSettings::retrieveRoomInfo() { try { - usesEncryption_ = cache::client()->isRoomEncrypted(room_id_.toStdString()); - info_ = cache::client()->singleRoomInfo(room_id_.toStdString()); - setAvatar(QImage::fromData(cache::client()->image(info_.avatar_url))); - } catch (const lmdb::error &e) { + usesEncryption_ = cache::isRoomEncrypted(room_id_.toStdString()); + info_ = cache::singleRoomInfo(room_id_.toStdString()); + setAvatar(); + } catch (const lmdb::error &) { nhlog::db()->warn("failed to retrieve room info from cache: {}", room_id_.toStdString()); } @@ -518,8 +619,7 @@ bool RoomSettings::canChangeJoinRules(const std::string &room_id, const std::string &user_id) const { try { - return cache::client()->hasEnoughPowerLevel( - {EventType::RoomJoinRules}, room_id, user_id); + return cache::hasEnoughPowerLevel({EventType::RoomJoinRules}, room_id, user_id); } catch (const lmdb::error &e) { nhlog::db()->warn("lmdb error: {}", e.what()); } @@ -531,7 +631,7 @@ bool RoomSettings::canChangeNameAndTopic(const std::string &room_id, const std::string &user_id) const { try { - return cache::client()->hasEnoughPowerLevel( + return cache::hasEnoughPowerLevel( {EventType::RoomName, EventType::RoomTopic}, room_id, user_id); } catch (const lmdb::error &e) { nhlog::db()->warn("lmdb error: {}", e.what()); @@ -544,8 +644,7 @@ bool RoomSettings::canChangeAvatar(const std::string &room_id, const std::string &user_id) const { try { - return cache::client()->hasEnoughPowerLevel( - {EventType::RoomAvatar}, room_id, user_id); + return cache::hasEnoughPowerLevel({EventType::RoomAvatar}, room_id, user_id); } catch (const lmdb::error &e) { nhlog::db()->warn("lmdb error: {}", e.what()); } @@ -622,14 +721,12 @@ RoomSettings::displayErrorMessage(const QString &msg) } void -RoomSettings::setAvatar(const QImage &img) +RoomSettings::setAvatar() { stopLoadingSpinner(); - avatarImg_ = img; - if (avatar_) - avatar_->setImage(img); + avatar_->setImage(QString::fromStdString(info_.avatar_url)); } void @@ -644,8 +741,10 @@ RoomSettings::resetErrorLabel() void RoomSettings::updateAvatar() { - const auto fileName = - QFileDialog::getOpenFileName(this, tr("Select an avatar"), "", tr("All Files (*)")); + const QString picturesFolder = + QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); + const QString fileName = QFileDialog::getOpenFileName( + this, tr("Select an avatar"), picturesFolder, tr("All Files (*)")); if (fileName.isEmpty()) return; @@ -657,12 +756,12 @@ RoomSettings::updateAvatar() QFile file{fileName, this}; if (format != "image") { - displayErrorMessage(tr("The selected media is not an image")); + displayErrorMessage(tr("The selected file is not an image")); return; } if (!file.open(QIODevice::ReadOnly)) { - displayErrorMessage(tr("Error while reading media: %1").arg(file.errorString())); + displayErrorMessage(tr("Error while reading file: %1").arg(file.errorString())); return; } @@ -722,7 +821,7 @@ RoomSettings::updateAvatar() return; } - emit proxy->avatarChanged(QImage::fromData(content)); + emit proxy->avatarChanged(); }); }); } diff --git a/src/dialogs/RoomSettings.h b/src/dialogs/RoomSettings.h
index 6667b68b..e41c866c 100644 --- a/src/dialogs/RoomSettings.h +++ b/src/dialogs/RoomSettings.h
@@ -5,7 +5,9 @@ #include <QImage> #include <QLabel> -#include "Cache.h" +#include <mtx/events/guest_access.hpp> + +#include "CacheStructs.h" class Avatar; class FlatButton; @@ -33,7 +35,7 @@ signals: void clicked(); protected: - bool eventFilter(QObject *obj, QEvent *event) + bool eventFilter(QObject *obj, QEvent *event) override { if (event->type() == QEvent::MouseButtonRelease) { emit clicked(); @@ -52,7 +54,7 @@ class ThreadProxy : public QObject signals: void error(const QString &msg); - void avatarChanged(const QImage &img); + void avatarChanged(); void nameEventSent(const QString &); void topicEventSent(); }; @@ -117,6 +119,7 @@ signals: void enableEncryptionError(const QString &msg); void showErrorMessage(const QString &msg); void accessRulesUpdated(); + void notifChanged(int index); protected: void showEvent(QShowEvent *event) override; @@ -140,7 +143,7 @@ private: void resetErrorLabel(); void displayErrorMessage(const QString &msg); - void setAvatar(const QImage &img); + void setAvatar(); void setupEditButton(); //! Retrieve the current room information from cache. void retrieveRoomInfo(); @@ -161,6 +164,7 @@ private: QLabel *errorLabel_ = nullptr; LoadingIndicator *spinner_ = nullptr; + QComboBox *notifCombo = nullptr; QComboBox *accessCombo = nullptr; Toggle *encryptionToggle_ = nullptr; Toggle *keyRequestsToggle_ = nullptr; diff --git a/src/dialogs/UserProfile.cpp b/src/dialogs/UserProfile.cpp
index b8040f9f..f1dd77df 100644 --- a/src/dialogs/UserProfile.cpp +++ b/src/dialogs/UserProfile.cpp
@@ -1,13 +1,12 @@ #include <QHBoxLayout> #include <QLabel> #include <QListWidget> -#include <QSettings> #include <QShortcut> #include <QVBoxLayout> -#include "AvatarProvider.h" #include "Cache.h" #include "ChatPage.h" +#include "Logging.h" #include "MatrixClient.h" #include "Utils.h" #include "dialogs/UserProfile.h" @@ -16,6 +15,8 @@ using namespace dialogs; +Q_DECLARE_METATYPE(std::vector<DeviceInfo>) + constexpr int BUTTON_SIZE = 36; constexpr int BUTTON_RADIUS = BUTTON_SIZE / 2; constexpr int WIDGET_MARGIN = 20; @@ -49,7 +50,6 @@ UserProfile::UserProfile(QWidget *parent) { setAutoFillBackground(true); setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setWindowModality(Qt::WindowModal); setAttribute(Qt::WA_DeleteOnClose, true); QIcon banIcon, kickIcon, ignoreIcon, startChatIcon; @@ -61,7 +61,6 @@ UserProfile::UserProfile(QWidget *parent) banBtn_->setIcon(banIcon); banBtn_->setIconSize(QSize(BUTTON_RADIUS, BUTTON_RADIUS)); banBtn_->setToolTip(tr("Ban the user from the room")); - banBtn_->setDisabled(true); // Not used yet. ignoreIcon.addFile(":/icons/icons/ui/volume-off-indicator.png"); ignoreBtn_ = new FlatButton(this); @@ -79,7 +78,6 @@ UserProfile::UserProfile(QWidget *parent) kickBtn_->setIcon(kickIcon); kickBtn_->setIconSize(QSize(BUTTON_RADIUS, BUTTON_RADIUS)); kickBtn_->setToolTip(tr("Kick the user from the room")); - kickBtn_->setDisabled(true); // Not used yet. startChatIcon.addFile(":/icons/icons/ui/black-bubble-speech.png"); startChat_ = new FlatButton(this); @@ -102,6 +100,13 @@ UserProfile::UserProfile(QWidget *parent) emit ChatPage::instance()->createRoom(req); }); + connect(banBtn_, &QPushButton::clicked, this, [this] { + ChatPage::instance()->banUser(userIdLabel_->text(), ""); + }); + connect(kickBtn_, &QPushButton::clicked, this, [this] { + ChatPage::instance()->kickUser(userIdLabel_->text(), ""); + }); + // Button line auto btnLayout = new QHBoxLayout; btnLayout->addStretch(1); @@ -114,9 +119,8 @@ UserProfile::UserProfile(QWidget *parent) btnLayout->setSpacing(8); btnLayout->setMargin(0); - avatar_ = new Avatar(this); + avatar_ = new Avatar(this, 128); avatar_->setLetter("X"); - avatar_->setSize(128); QFont font; font.setPointSizeF(font.pointSizeF() * 2); @@ -167,10 +171,6 @@ UserProfile::UserProfile(QWidget *parent) vlayout->setAlignment(avatar_, Qt::AlignCenter | Qt::AlignTop); vlayout->setAlignment(userIdLabel_, Qt::AlignCenter | Qt::AlignTop); - setAutoFillBackground(true); - setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); - setWindowModality(Qt::WindowModal); - QFont largeFont; largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5); @@ -181,9 +181,10 @@ UserProfile::UserProfile(QWidget *parent) vlayout->setSpacing(WIDGET_SPACING); vlayout->setContentsMargins(WIDGET_MARGIN, TOP_WIDGET_MARGIN, WIDGET_MARGIN, WIDGET_MARGIN); - qRegisterMetaType<std::vector<DeviceInfo>>(); + static auto ignored = qRegisterMetaType<std::vector<DeviceInfo>>(); + (void)ignored; - auto closeShortcut = new QShortcut(QKeySequence(tr("ESC")), this); + auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this); connect(closeShortcut, &QShortcut::activated, this, &UserProfile::close); connect(okBtn, &QPushButton::clicked, this, &UserProfile::close); } @@ -204,22 +205,21 @@ UserProfile::init(const QString &userId, const QString &roomId) { resetToDefaults(); - auto displayName = Cache::displayName(roomId, userId); + auto displayName = cache::displayName(roomId, userId); userIdLabel_->setText(userId); displayNameLabel_->setText(displayName); avatar_->setLetter(utils::firstChar(displayName)); - AvatarProvider::resolve( - roomId, userId, this, [this](const QImage &img) { avatar_->setImage(img); }); + avatar_->setImage(roomId, userId); auto localUser = utils::localUser(); try { bool hasMemberRights = - cache::client()->hasEnoughPowerLevel({mtx::events::EventType::RoomMember}, - roomId.toStdString(), - localUser.toStdString()); + cache::hasEnoughPowerLevel({mtx::events::EventType::RoomMember}, + roomId.toStdString(), + localUser.toStdString()); if (!hasMemberRights) { kickBtn_->hide(); banBtn_->hide(); diff --git a/src/dialogs/UserProfile.h b/src/dialogs/UserProfile.h
index 0f684cda..81276d2a 100644 --- a/src/dialogs/UserProfile.h +++ b/src/dialogs/UserProfile.h
@@ -15,8 +15,6 @@ struct DeviceInfo QString display_name; }; -Q_DECLARE_METATYPE(std::vector<DeviceInfo>) - class Proxy : public QObject { Q_OBJECT diff --git a/src/emoji/Category.cpp b/src/emoji/Category.cpp
index fbfbf4fc..e674e9db 100644 --- a/src/emoji/Category.cpp +++ b/src/emoji/Category.cpp
@@ -43,15 +43,18 @@ Category::Category(QString category, std::vector<Emoji> emoji, QWidget *parent) emojiListView_->setViewMode(QListView::IconMode); emojiListView_->setFlow(QListView::LeftToRight); emojiListView_->setResizeMode(QListView::Adjust); + emojiListView_->setMouseTracking(true); emojiListView_->verticalScrollBar()->setEnabled(false); emojiListView_->horizontalScrollBar()->setEnabled(false); const int cols = 7; - const int rows = emoji.size() / 7; + const int rows = emoji.size() / 7 + 1; + const int emojiSize = 48; + const int gridSize = emojiSize + 4; // TODO: Be precise here. Take the parent into consideration. - emojiListView_->setFixedSize(cols * 50 + 20, rows * 50 + 20); - emojiListView_->setGridSize(QSize(50, 50)); + emojiListView_->setFixedSize(cols * gridSize + 20, rows * gridSize); + emojiListView_->setGridSize(QSize(gridSize, gridSize)); emojiListView_->setDragEnabled(false); emojiListView_->setEditTriggers(QAbstractItemView::NoEditTriggers); @@ -59,7 +62,7 @@ Category::Category(QString category, std::vector<Emoji> emoji, QWidget *parent) data_->unicode = e.unicode; auto item = new QStandardItem; - item->setSizeHint(QSize(24, 24)); + item->setSizeHint(QSize(emojiSize, emojiSize)); QVariant unicode(data_->unicode); item->setData(unicode.toString(), Qt::UserRole); @@ -72,7 +75,6 @@ Category::Category(QString category, std::vector<Emoji> emoji, QWidget *parent) category_ = new QLabel(category, this); category_->setFont(font); - category_->setStyleSheet("margin: 20px 0 20px 8px;"); mainLayout_->addWidget(category_); mainLayout_->addWidget(emojiListView_); diff --git a/src/emoji/Category.h b/src/emoji/Category.h
index a14029c8..2f39d621 100644 --- a/src/emoji/Category.h +++ b/src/emoji/Category.h
@@ -17,6 +17,7 @@ #pragma once +#include <QColor> #include <QLabel> #include <QLayout> #include <QListView> @@ -29,9 +30,13 @@ namespace emoji { class Category : public QWidget { Q_OBJECT + Q_PROPERTY( + QColor hoverBackgroundColor READ hoverBackgroundColor WRITE setHoverBackgroundColor) public: Category(QString category, std::vector<Emoji> emoji, QWidget *parent = nullptr); + QColor hoverBackgroundColor() const { return hoverBackgroundColor_; } + void setHoverBackgroundColor(QColor color) { hoverBackgroundColor_ = color; } signals: void emojiSelected(const QString &emoji); @@ -55,5 +60,7 @@ private: emoji::ItemDelegate *delegate_; QLabel *category_; + + QColor hoverBackgroundColor_; }; } // namespace emoji diff --git a/src/emoji/ItemDelegate.cpp b/src/emoji/ItemDelegate.cpp
index b79ae0fc..afa01625 100644 --- a/src/emoji/ItemDelegate.cpp +++ b/src/emoji/ItemDelegate.cpp
@@ -15,8 +15,8 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#include <QDebug> #include <QPainter> +#include <QSettings> #include "emoji/ItemDelegate.h" @@ -37,12 +37,30 @@ ItemDelegate::paint(QPainter *painter, { Q_UNUSED(index); + painter->save(); + QStyleOptionViewItem viewOption(option); auto emoji = index.data(Qt::UserRole).toString(); - // QFont font("Emoji One"); + QSettings settings; + QFont font; + QString userFontFamily = settings.value("user/emoji_font_family", "emoji").toString(); + if (!userFontFamily.isEmpty()) { + font.setFamily(userFontFamily); + } else { + font.setFamily("emoji"); + } + + font.setPixelSize(36); painter->setFont(font); + if (option.state & QStyle::State_MouseOver) { + painter->setBackgroundMode(Qt::OpaqueMode); + QColor hoverColor = parent()->property("hoverBackgroundColor").value<QColor>(); + painter->setBackground(hoverColor); + } painter->drawText(viewOption.rect, Qt::AlignCenter, emoji); + + painter->restore(); } diff --git a/src/emoji/ItemDelegate.h b/src/emoji/ItemDelegate.h
index e0456308..d6b9b9d7 100644 --- a/src/emoji/ItemDelegate.h +++ b/src/emoji/ItemDelegate.h
@@ -31,7 +31,7 @@ class ItemDelegate : public QStyledItemDelegate public: explicit ItemDelegate(QObject *parent = nullptr); - ~ItemDelegate(); + ~ItemDelegate() override; void paint(QPainter *painter, const QStyleOptionViewItem &option, diff --git a/src/emoji/Panel.cpp b/src/emoji/Panel.cpp
index 710b501e..f0e4449d 100644 --- a/src/emoji/Panel.cpp +++ b/src/emoji/Panel.cpp
@@ -15,6 +15,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <QPainter> #include <QPushButton> #include <QScrollBar> #include <QVBoxLayout> @@ -34,10 +35,6 @@ Panel::Panel(QWidget *parent) , height_{350} , categoryIconSize_{20} { - setStyleSheet("QWidget {border: none;}" - "QScrollBar:vertical { width: 0px; margin: 0px; }" - "QScrollBar::handle:vertical { min-height: 30px; }"); - setAttribute(Qt::WA_ShowWithoutActivating, true); setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint); @@ -202,14 +199,12 @@ Panel::showCategory(const Category *category) return; // HACK - // If we want to go to a previous category and position the label at the top - // the 6*50 offset won't work because not all the categories have the same - // height. To ensure the category is at the top, we move to the top and go as - // normal to the next category. + // We want the top of the category to be visible, so scroll to the top first and then to the + // category if (current > posToGo) this->scrollArea_->ensureVisible(0, 0, 0, 0); - posToGo += 6 * 50; + posToGo += scrollArea_->height(); this->scrollArea_->ensureVisible(0, posToGo, 0, 0); } diff --git a/src/emoji/Provider.cpp b/src/emoji/Provider.cpp
index f7b8dab9..4ed8bd71 100644 --- a/src/emoji/Provider.cpp +++ b/src/emoji/Provider.cpp
@@ -15,1383 +15,4702 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#include <QByteArray> - #include "emoji/Provider.h" using namespace emoji; -const std::vector<Emoji> Provider::people = { - Emoji{QString::fromUtf8("\xf0\x9f\x98\x80"), ":grinning:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x81"), ":grin:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x82"), ":joy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa3"), ":rofl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x83"), ":smiley:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x84"), ":smile:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x85"), ":sweat_smile:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x86"), ":laughing:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x89"), ":wink:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x8a"), ":blush:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x8b"), ":yum:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x8e"), ":sunglasses:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x8d"), ":heart_eyes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x98"), ":kissing_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x97"), ":kissing:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x99"), ":kissing_smiling_eyes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x9a"), ":kissing_closed_eyes:"}, - Emoji{QString::fromUtf8("\xe2\x98\xba"), ":relaxed:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x82"), ":slight_smile:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x97"), ":hugging:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x94"), ":thinking:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x90"), ":neutral_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x91"), ":expressionless:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb6"), ":no_mouth:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x84"), ":rolling_eyes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x8f"), ":smirk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa3"), ":persevere:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa5"), ":disappointed_relieved:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xae"), ":open_mouth:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x90"), ":zipper_mouth:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xaf"), ":hushed:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xaa"), ":sleepy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xab"), ":tired_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb4"), ":sleeping:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x8c"), ":relieved:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x93"), ":nerd:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x9b"), ":stuck_out_tongue:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x9c"), ":stuck_out_tongue_winking_eye:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x9d"), ":stuck_out_tongue_closed_eyes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa4"), ":drooling_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x92"), ":unamused:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x93"), ":sweat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x94"), ":pensive:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x95"), ":confused:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x83"), ":upside_down:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x91"), ":money_mouth:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb2"), ":astonished:"}, - Emoji{QString::fromUtf8("\xe2\x98\xb9"), ":frowning2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x81"), ":slight_frown:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x96"), ":confounded:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x9e"), ":disappointed:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x9f"), ":worried:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa4"), ":triumph:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa2"), ":cry:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xad"), ":sob:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa6"), ":frowning:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa7"), ":anguished:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa8"), ":fearful:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa9"), ":weary:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xac"), ":grimacing:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb0"), ":cold_sweat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb1"), ":scream:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb3"), ":flushed:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb5"), ":dizzy_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa1"), ":rage:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xa0"), ":angry:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x87"), ":innocent:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa0"), ":cowboy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa1"), ":clown:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa5"), ":lying_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb7"), ":mask:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x92"), ":thermometer_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x95"), ":head_bandage:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa2"), ":nauseated_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa7"), ":sneezing_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\x88"), ":smiling_imp:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xbf"), ":imp:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb9"), ":japanese_ogre:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xba"), ":japanese_goblin:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x80"), ":skull:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xbb"), ":ghost:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xbd"), ":alien:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x96"), ":robot:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa9"), ":poop:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xba"), ":smiley_cat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb8"), ":smile_cat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xb9"), ":joy_cat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xbb"), ":heart_eyes_cat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xbc"), ":smirk_cat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xbd"), ":kissing_cat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x80"), ":scream_cat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xbf"), ":crying_cat_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x98\xbe"), ":pouting_cat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa6"), ":boy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa7"), ":girl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8"), ":man:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9"), ":woman:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb4"), ":older_man:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb5"), ":older_woman:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb6"), ":baby:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xbc"), ":angel:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xae"), ":cop:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5"), ":spy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x82"), ":guardsman:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7"), ":construction_worker:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3"), ":man_with_turban:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1"), ":person_with_blond_hair:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x85"), ":santa:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb6"), ":mrs_claus:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb8"), ":princess:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb4"), ":prince:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0"), ":bride_with_veil:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5"), ":man_in_tuxedo:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb0"), ":pregnant_woman:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xb2"), ":man_with_gua_pi_mao:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d"), ":person_frowning:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e"), ":person_with_pouting_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x85"), ":no_good:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x86"), ":ok_woman:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x81"), ":information_desk_person:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b"), ":raising_hand:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x87"), ":bow:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6"), ":face_palm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7"), ":shrug:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x86"), ":massage:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x87"), ":haircut:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6"), ":walking:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83"), ":runner:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x83"), ":dancer:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xba"), ":man_dancing:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xaf"), ":dancers:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xa3"), ":speaking_head:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa4"), ":bust_in_silhouette:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa5"), ":busts_in_silhouette:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xab"), ":couple:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xac"), ":two_men_holding_hands:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xad"), ":two_women_holding_hands:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x8f"), ":couplekiss:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x91"), ":couple_with_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xaa"), ":family:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xaa"), ":muscle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb3"), ":selfie:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x88"), ":point_left:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x89"), ":point_right:"}, - Emoji{QString::fromUtf8("\xe2\x98\x9d"), ":point_up:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x86"), ":point_up_2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\x95"), ":middle_finger:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x87"), ":point_down:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x8c"), ":v:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9e"), ":fingers_crossed:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\x96"), ":vulcan:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x98"), ":metal:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x99"), ":call_me:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\x90"), ":hand_splayed:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x8b"), ":raised_hand:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x8c"), ":ok_hand:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x8d"), ":thumbsup:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x8e"), ":thumbsdown:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x8a"), ":fist:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x8a"), ":punch:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9b"), ":left_facing_fist:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9c"), ":right_facing_fist:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9a"), ":raised_back_of_hand:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x8b"), ":wave:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x8f"), ":clap:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x8d"), ":writing_hand:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x90"), ":open_hands:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x8c"), ":raised_hands:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x8f"), ":pray:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9d"), ":handshake:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x85"), ":nail_care:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x82"), ":ear:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x83"), ":nose:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa3"), ":footprints:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x80"), ":eyes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x81"), ":eye:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x85"), ":tongue:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x84"), ":lips:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x8b"), ":kiss:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa4"), ":zzz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x93"), ":eyeglasses:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xb6"), ":dark_sunglasses:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x94"), ":necktie:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x95"), ":shirt:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x96"), ":jeans:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x97"), ":dress:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x98"), ":kimono:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x99"), ":bikini:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x9a"), ":womans_clothes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x9b"), ":purse:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x9c"), ":handbag:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x9d"), ":pouch:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x92"), ":school_satchel:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x9e"), ":mans_shoe:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x9f"), ":athletic_shoe:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa0"), ":high_heel:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa1"), ":sandal:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\xa2"), ":boot:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x91"), ":crown:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x91\x92"), ":womans_hat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa9"), ":tophat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x93"), ":mortar_board:"}, - Emoji{QString::fromUtf8("\xe2\x9b\x91"), ":helmet_with_cross:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x84"), ":lipstick:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x8d"), ":ring:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x82"), ":closed_umbrella:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xbc"), ":briefcase:"}, +const std::vector<Emoji> emoji::Provider::people = { + Emoji{QString::fromUtf8("\xf0\x9f\x98\x80"), "grinning face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x83"), "grinning face with big eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x84"), "grinning face with smiling eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x81"), "beaming face with smiling eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x86"), "grinning squinting face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x85"), "grinning face with sweat"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa3"), "rolling on the floor laughing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x82"), "face with tears of joy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x82"), "slightly smiling face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x83"), "upside-down face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x89"), "winking face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x8a"), "smiling face with smiling eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x87"), "smiling face with halo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb0"), "smiling face with hearts"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x8d"), "smiling face with heart-eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa9"), "star-struck"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x98"), "face blowing a kiss"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x97"), "kissing face"}, + Emoji{QString::fromUtf8("\xe2\x98\xba"), "smiling face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x9a"), "kissing face with closed eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x99"), "kissing face with smiling eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb2"), "smiling face with tear"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x8b"), "face savoring food"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x9b"), "face with tongue"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x9c"), "winking face with tongue"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xaa"), "zany face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x9d"), "squinting face with tongue"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x91"), "money-mouth face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x97"), "hugging face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xad"), "face with hand over mouth"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xab"), "shushing face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x94"), "thinking face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x90"), "zipper-mouth face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa8"), "face with raised eyebrow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x90"), "neutral face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x91"), "expressionless face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb6"), "face without mouth"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x8f"), "smirking face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x92"), "unamused face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x84"), "face with rolling eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xac"), "grimacing face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa5"), "lying face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x8c"), "relieved face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x94"), "pensive face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xaa"), "sleepy face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa4"), "drooling face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb4"), "sleeping face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb7"), "face with medical mask"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x92"), "face with thermometer"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x95"), "face with head-bandage"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa2"), "nauseated face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xae"), "face vomiting"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa7"), "sneezing face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb5"), "hot face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb6"), "cold face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb4"), "woozy face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb5"), "dizzy face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xaf"), "exploding head"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa0"), "cowboy hat face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb3"), "partying face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb8"), "disguised face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x8e"), "smiling face with sunglasses"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x93"), "nerd face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x90"), "face with monocle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x95"), "confused face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x9f"), "worried face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x81"), "slightly frowning face"}, + Emoji{QString::fromUtf8("\xe2\x98\xb9"), "frowning face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xae"), "face with open mouth"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xaf"), "hushed face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb2"), "astonished face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb3"), "flushed face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xba"), "pleading face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa6"), "frowning face with open mouth"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa7"), "anguished face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa8"), "fearful face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb0"), "anxious face with sweat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa5"), "sad but relieved face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa2"), "crying face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xad"), "loudly crying face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb1"), "face screaming in fear"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x96"), "confounded face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa3"), "persevering face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x9e"), "disappointed face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x93"), "downcast face with sweat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa9"), "weary face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xab"), "tired face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb1"), "yawning face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa4"), "face with steam from nose"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa1"), "pouting face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xa0"), "angry face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xac"), "face with symbols on mouth"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\x88"), "smiling face with horns"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbf"), "angry face with horns"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x80"), "skull"}, + Emoji{QString::fromUtf8("\xe2\x98\xa0"), "skull and crossbones"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa9"), "pile of poo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa1"), "clown face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb9"), "ogre"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xba"), "goblin"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbb"), "ghost"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbd"), "alien"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbe"), "alien monster"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x96"), "robot"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xba"), "grinning cat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb8"), "grinning cat with smiling eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xb9"), "cat with tears of joy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xbb"), "smiling cat with heart-eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xbc"), "cat with wry smile"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xbd"), "kissing cat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x80"), "weary cat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xbf"), "crying cat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x98\xbe"), "pouting cat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x88"), "see-no-evil monkey"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x89"), "hear-no-evil monkey"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8a"), "speak-no-evil monkey"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x8b"), "kiss mark"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x8c"), "love letter"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x98"), "heart with arrow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x9d"), "heart with ribbon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x96"), "sparkling heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x97"), "growing heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x93"), "beating heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x9e"), "revolving hearts"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x95"), "two hearts"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x9f"), "heart decoration"}, + Emoji{QString::fromUtf8("\xe2\x9d\xa3"), "heart exclamation"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x94"), "broken heart"}, + Emoji{QString::fromUtf8("\xe2\x9d\xa4"), "red heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa1"), "orange heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x9b"), "yellow heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x9a"), "green heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x99"), "blue heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x9c"), "purple heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8e"), "brown heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\xa4"), "black heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8d"), "white heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xaf"), "hundred points"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa2"), "anger symbol"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa5"), "collision"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xab"), "dizzy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa6"), "sweat droplets"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa8"), "dashing away"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb3"), "hole"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa3"), "bomb"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xac"), "speech balloon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x81\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x97\xa8"), + "eye in speech bubble"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x81\xe2\x80\x8d\xf0\x9f\x97\xa8"), "eye in speech bubble"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xa8"), "left speech bubble"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xaf"), "right anger bubble"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xad"), "thought balloon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa4"), "zzz"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8b"), "waving hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8b\xf0\x9f\x8f\xbb"), "waving hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8b\xf0\x9f\x8f\xbc"), + "waving hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8b\xf0\x9f\x8f\xbd"), "waving hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8b\xf0\x9f\x8f\xbe"), + "waving hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8b\xf0\x9f\x8f\xbf"), "waving hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9a"), "raised back of hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9a\xf0\x9f\x8f\xbb"), + "raised back of hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9a\xf0\x9f\x8f\xbc"), + "raised back of hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9a\xf0\x9f\x8f\xbd"), + "raised back of hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9a\xf0\x9f\x8f\xbe"), + "raised back of hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9a\xf0\x9f\x8f\xbf"), + "raised back of hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x90"), "hand with fingers splayed"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x90\xf0\x9f\x8f\xbb"), + "hand with fingers splayed: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x90\xf0\x9f\x8f\xbc"), + "hand with fingers splayed: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x90\xf0\x9f\x8f\xbd"), + "hand with fingers splayed: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x90\xf0\x9f\x8f\xbe"), + "hand with fingers splayed: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x90\xf0\x9f\x8f\xbf"), + "hand with fingers splayed: dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8b"), "raised hand"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8b\xf0\x9f\x8f\xbb"), "raised hand: light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8b\xf0\x9f\x8f\xbc"), "raised hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8b\xf0\x9f\x8f\xbd"), "raised hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8b\xf0\x9f\x8f\xbe"), "raised hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8b\xf0\x9f\x8f\xbf"), "raised hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x96"), "vulcan salute"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x96\xf0\x9f\x8f\xbb"), "vulcan salute: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x96\xf0\x9f\x8f\xbc"), + "vulcan salute: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x96\xf0\x9f\x8f\xbd"), "vulcan salute: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x96\xf0\x9f\x8f\xbe"), + "vulcan salute: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x96\xf0\x9f\x8f\xbf"), "vulcan salute: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8c"), "OK hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8c\xf0\x9f\x8f\xbb"), "OK hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8c\xf0\x9f\x8f\xbc"), "OK hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8c\xf0\x9f\x8f\xbd"), "OK hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8c\xf0\x9f\x8f\xbe"), "OK hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8c\xf0\x9f\x8f\xbf"), "OK hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8c"), "pinched fingers"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8c\xf0\x9f\x8f\xbb"), "pinched fingers: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8c\xf0\x9f\x8f\xbc"), + "pinched fingers: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8c\xf0\x9f\x8f\xbd"), "pinched fingers: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8c\xf0\x9f\x8f\xbe"), + "pinched fingers: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8c\xf0\x9f\x8f\xbf"), "pinched fingers: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8f"), "pinching hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8f\xf0\x9f\x8f\xbb"), "pinching hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8f\xf0\x9f\x8f\xbc"), + "pinching hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8f\xf0\x9f\x8f\xbd"), "pinching hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8f\xf0\x9f\x8f\xbe"), + "pinching hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x8f\xf0\x9f\x8f\xbf"), "pinching hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8c"), "victory hand"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8c\xf0\x9f\x8f\xbb"), "victory hand: light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8c\xf0\x9f\x8f\xbc"), "victory hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8c\xf0\x9f\x8f\xbd"), "victory hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8c\xf0\x9f\x8f\xbe"), "victory hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8c\xf0\x9f\x8f\xbf"), "victory hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9e"), "crossed fingers"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9e\xf0\x9f\x8f\xbb"), "crossed fingers: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9e\xf0\x9f\x8f\xbc"), + "crossed fingers: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9e\xf0\x9f\x8f\xbd"), "crossed fingers: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9e\xf0\x9f\x8f\xbe"), + "crossed fingers: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9e\xf0\x9f\x8f\xbf"), "crossed fingers: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9f"), "love-you gesture"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9f\xf0\x9f\x8f\xbb"), "love-you gesture: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9f\xf0\x9f\x8f\xbc"), + "love-you gesture: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9f\xf0\x9f\x8f\xbd"), + "love-you gesture: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9f\xf0\x9f\x8f\xbe"), + "love-you gesture: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9f\xf0\x9f\x8f\xbf"), "love-you gesture: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x98"), "sign of the horns"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x98\xf0\x9f\x8f\xbb"), + "sign of the horns: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x98\xf0\x9f\x8f\xbc"), + "sign of the horns: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x98\xf0\x9f\x8f\xbd"), + "sign of the horns: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x98\xf0\x9f\x8f\xbe"), + "sign of the horns: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x98\xf0\x9f\x8f\xbf"), "sign of the horns: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x99"), "call me hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x99\xf0\x9f\x8f\xbb"), "call me hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x99\xf0\x9f\x8f\xbc"), + "call me hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x99\xf0\x9f\x8f\xbd"), "call me hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x99\xf0\x9f\x8f\xbe"), + "call me hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x99\xf0\x9f\x8f\xbf"), "call me hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x88"), "backhand index pointing left"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x88\xf0\x9f\x8f\xbb"), + "backhand index pointing left: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x88\xf0\x9f\x8f\xbc"), + "backhand index pointing left: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x88\xf0\x9f\x8f\xbd"), + "backhand index pointing left: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x88\xf0\x9f\x8f\xbe"), + "backhand index pointing left: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x88\xf0\x9f\x8f\xbf"), + "backhand index pointing left: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x89"), "backhand index pointing right"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x89\xf0\x9f\x8f\xbb"), + "backhand index pointing right: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x89\xf0\x9f\x8f\xbc"), + "backhand index pointing right: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x89\xf0\x9f\x8f\xbd"), + "backhand index pointing right: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x89\xf0\x9f\x8f\xbe"), + "backhand index pointing right: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x89\xf0\x9f\x8f\xbf"), + "backhand index pointing right: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x86"), "backhand index pointing up"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x86\xf0\x9f\x8f\xbb"), + "backhand index pointing up: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x86\xf0\x9f\x8f\xbc"), + "backhand index pointing up: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x86\xf0\x9f\x8f\xbd"), + "backhand index pointing up: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x86\xf0\x9f\x8f\xbe"), + "backhand index pointing up: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x86\xf0\x9f\x8f\xbf"), + "backhand index pointing up: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x95"), "middle finger"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x95\xf0\x9f\x8f\xbb"), "middle finger: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x95\xf0\x9f\x8f\xbc"), + "middle finger: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x95\xf0\x9f\x8f\xbd"), "middle finger: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x95\xf0\x9f\x8f\xbe"), + "middle finger: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x95\xf0\x9f\x8f\xbf"), "middle finger: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x87"), "backhand index pointing down"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x87\xf0\x9f\x8f\xbb"), + "backhand index pointing down: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x87\xf0\x9f\x8f\xbc"), + "backhand index pointing down: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x87\xf0\x9f\x8f\xbd"), + "backhand index pointing down: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x87\xf0\x9f\x8f\xbe"), + "backhand index pointing down: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x87\xf0\x9f\x8f\xbf"), + "backhand index pointing down: dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x98\x9d"), "index pointing up"}, + Emoji{QString::fromUtf8("\xe2\x98\x9d\xf0\x9f\x8f\xbb"), "index pointing up: light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x98\x9d\xf0\x9f\x8f\xbc"), + "index pointing up: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x98\x9d\xf0\x9f\x8f\xbd"), "index pointing up: medium skin tone"}, + Emoji{QString::fromUtf8("\xe2\x98\x9d\xf0\x9f\x8f\xbe"), + "index pointing up: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x98\x9d\xf0\x9f\x8f\xbf"), "index pointing up: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8d"), "thumbs up"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8d\xf0\x9f\x8f\xbb"), "thumbs up: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8d\xf0\x9f\x8f\xbc"), "thumbs up: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8d\xf0\x9f\x8f\xbd"), "thumbs up: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8d\xf0\x9f\x8f\xbe"), "thumbs up: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8d\xf0\x9f\x8f\xbf"), "thumbs up: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8e"), "thumbs down"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8e\xf0\x9f\x8f\xbb"), "thumbs down: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8e\xf0\x9f\x8f\xbc"), + "thumbs down: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8e\xf0\x9f\x8f\xbd"), "thumbs down: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8e\xf0\x9f\x8f\xbe"), + "thumbs down: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8e\xf0\x9f\x8f\xbf"), "thumbs down: dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8a"), "raised fist"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8a\xf0\x9f\x8f\xbb"), "raised fist: light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8a\xf0\x9f\x8f\xbc"), "raised fist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8a\xf0\x9f\x8f\xbd"), "raised fist: medium skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8a\xf0\x9f\x8f\xbe"), "raised fist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8a\xf0\x9f\x8f\xbf"), "raised fist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8a"), "oncoming fist"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8a\xf0\x9f\x8f\xbb"), "oncoming fist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8a\xf0\x9f\x8f\xbc"), + "oncoming fist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8a\xf0\x9f\x8f\xbd"), "oncoming fist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8a\xf0\x9f\x8f\xbe"), + "oncoming fist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8a\xf0\x9f\x8f\xbf"), "oncoming fist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9b"), "left-facing fist"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9b\xf0\x9f\x8f\xbb"), "left-facing fist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9b\xf0\x9f\x8f\xbc"), + "left-facing fist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9b\xf0\x9f\x8f\xbd"), + "left-facing fist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9b\xf0\x9f\x8f\xbe"), + "left-facing fist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9b\xf0\x9f\x8f\xbf"), "left-facing fist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9c"), "right-facing fist"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9c\xf0\x9f\x8f\xbb"), + "right-facing fist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9c\xf0\x9f\x8f\xbc"), + "right-facing fist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9c\xf0\x9f\x8f\xbd"), + "right-facing fist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9c\xf0\x9f\x8f\xbe"), + "right-facing fist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9c\xf0\x9f\x8f\xbf"), "right-facing fist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8f"), "clapping hands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8f\xf0\x9f\x8f\xbb"), "clapping hands: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8f\xf0\x9f\x8f\xbc"), + "clapping hands: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8f\xf0\x9f\x8f\xbd"), "clapping hands: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8f\xf0\x9f\x8f\xbe"), + "clapping hands: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x8f\xf0\x9f\x8f\xbf"), "clapping hands: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8c"), "raising hands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8c\xf0\x9f\x8f\xbb"), "raising hands: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8c\xf0\x9f\x8f\xbc"), + "raising hands: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8c\xf0\x9f\x8f\xbd"), "raising hands: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8c\xf0\x9f\x8f\xbe"), + "raising hands: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8c\xf0\x9f\x8f\xbf"), "raising hands: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x90"), "open hands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x90\xf0\x9f\x8f\xbb"), "open hands: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x90\xf0\x9f\x8f\xbc"), + "open hands: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x90\xf0\x9f\x8f\xbd"), "open hands: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x90\xf0\x9f\x8f\xbe"), "open hands: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x90\xf0\x9f\x8f\xbf"), "open hands: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb2"), "palms up together"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb2\xf0\x9f\x8f\xbb"), + "palms up together: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb2\xf0\x9f\x8f\xbc"), + "palms up together: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb2\xf0\x9f\x8f\xbd"), + "palms up together: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb2\xf0\x9f\x8f\xbe"), + "palms up together: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb2\xf0\x9f\x8f\xbf"), "palms up together: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9d"), "handshake"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8f"), "folded hands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8f\xf0\x9f\x8f\xbb"), "folded hands: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8f\xf0\x9f\x8f\xbc"), + "folded hands: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8f\xf0\x9f\x8f\xbd"), "folded hands: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8f\xf0\x9f\x8f\xbe"), + "folded hands: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8f\xf0\x9f\x8f\xbf"), "folded hands: dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8d"), "writing hand"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8d\xf0\x9f\x8f\xbb"), "writing hand: light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8d\xf0\x9f\x8f\xbc"), "writing hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8d\xf0\x9f\x8f\xbd"), "writing hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8d\xf0\x9f\x8f\xbe"), "writing hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8d\xf0\x9f\x8f\xbf"), "writing hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x85"), "nail polish"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x85\xf0\x9f\x8f\xbb"), "nail polish: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x85\xf0\x9f\x8f\xbc"), + "nail polish: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x85\xf0\x9f\x8f\xbd"), "nail polish: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x85\xf0\x9f\x8f\xbe"), + "nail polish: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x85\xf0\x9f\x8f\xbf"), "nail polish: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb3"), "selfie"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb3\xf0\x9f\x8f\xbb"), "selfie: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb3\xf0\x9f\x8f\xbc"), "selfie: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb3\xf0\x9f\x8f\xbd"), "selfie: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb3\xf0\x9f\x8f\xbe"), "selfie: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb3\xf0\x9f\x8f\xbf"), "selfie: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xaa"), "flexed biceps"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xaa\xf0\x9f\x8f\xbb"), "flexed biceps: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xaa\xf0\x9f\x8f\xbc"), + "flexed biceps: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xaa\xf0\x9f\x8f\xbd"), "flexed biceps: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xaa\xf0\x9f\x8f\xbe"), + "flexed biceps: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xaa\xf0\x9f\x8f\xbf"), "flexed biceps: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbe"), "mechanical arm"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbf"), "mechanical leg"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb5"), "leg"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb5\xf0\x9f\x8f\xbb"), "leg: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb5\xf0\x9f\x8f\xbc"), "leg: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb5\xf0\x9f\x8f\xbd"), "leg: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb5\xf0\x9f\x8f\xbe"), "leg: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb5\xf0\x9f\x8f\xbf"), "leg: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb6"), "foot"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb6\xf0\x9f\x8f\xbb"), "foot: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb6\xf0\x9f\x8f\xbc"), "foot: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb6\xf0\x9f\x8f\xbd"), "foot: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb6\xf0\x9f\x8f\xbe"), "foot: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb6\xf0\x9f\x8f\xbf"), "foot: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x82"), "ear"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x82\xf0\x9f\x8f\xbb"), "ear: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x82\xf0\x9f\x8f\xbc"), "ear: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x82\xf0\x9f\x8f\xbd"), "ear: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x82\xf0\x9f\x8f\xbe"), "ear: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x82\xf0\x9f\x8f\xbf"), "ear: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbb"), "ear with hearing aid"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbb\xf0\x9f\x8f\xbb"), + "ear with hearing aid: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbb\xf0\x9f\x8f\xbc"), + "ear with hearing aid: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbb\xf0\x9f\x8f\xbd"), + "ear with hearing aid: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbb\xf0\x9f\x8f\xbe"), + "ear with hearing aid: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbb\xf0\x9f\x8f\xbf"), + "ear with hearing aid: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x83"), "nose"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x83\xf0\x9f\x8f\xbb"), "nose: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x83\xf0\x9f\x8f\xbc"), "nose: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x83\xf0\x9f\x8f\xbd"), "nose: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x83\xf0\x9f\x8f\xbe"), "nose: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x83\xf0\x9f\x8f\xbf"), "nose: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa0"), "brain"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x80"), "anatomical heart"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x81"), "lungs"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb7"), "tooth"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb4"), "bone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x80"), "eyes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x81"), "eye"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x85"), "tongue"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x84"), "mouth"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb6"), "baby"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb6\xf0\x9f\x8f\xbb"), "baby: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb6\xf0\x9f\x8f\xbc"), "baby: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb6\xf0\x9f\x8f\xbd"), "baby: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb6\xf0\x9f\x8f\xbe"), "baby: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb6\xf0\x9f\x8f\xbf"), "baby: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x92"), "child"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x92\xf0\x9f\x8f\xbb"), "child: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x92\xf0\x9f\x8f\xbc"), "child: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x92\xf0\x9f\x8f\xbd"), "child: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x92\xf0\x9f\x8f\xbe"), "child: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x92\xf0\x9f\x8f\xbf"), "child: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa6"), "boy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa6\xf0\x9f\x8f\xbb"), "boy: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa6\xf0\x9f\x8f\xbc"), "boy: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa6\xf0\x9f\x8f\xbd"), "boy: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa6\xf0\x9f\x8f\xbe"), "boy: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa6\xf0\x9f\x8f\xbf"), "boy: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa7"), "girl"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa7\xf0\x9f\x8f\xbb"), "girl: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa7\xf0\x9f\x8f\xbc"), "girl: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa7\xf0\x9f\x8f\xbd"), "girl: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa7\xf0\x9f\x8f\xbe"), "girl: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa7\xf0\x9f\x8f\xbf"), "girl: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91"), "person"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb"), "person: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc"), "person: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd"), "person: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe"), "person: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf"), "person: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1"), "person: blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbb"), + "person: light skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbc"), + "person: medium-light skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbd"), + "person: medium skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbe"), + "person: medium-dark skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbf"), + "person: dark skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8"), "man"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb"), "man: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc"), "man: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd"), "man: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe"), "man: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf"), "man: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x94"), "man: beard"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbb"), "man: light skin tone, beard"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbc"), + "man: medium-light skin tone, beard"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbd"), "man: medium skin tone, beard"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbe"), "man: medium-dark skin tone, beard"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbf"), "man: dark skin tone, beard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xb0"), "man: red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "man: light skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "man: medium-light skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "man: medium skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "man: medium-dark skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "man: dark skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xb1"), "man: curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "man: light skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "man: medium-light skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "man: medium skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "man: medium-dark skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "man: dark skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xb3"), "man: white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "man: light skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "man: medium-light skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "man: medium skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "man: medium-dark skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "man: dark skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xb2"), "man: bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "man: light skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "man: medium-light skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "man: medium skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "man: medium-dark skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "man: dark skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9"), "woman"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb"), "woman: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc"), "woman: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd"), "woman: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe"), "woman: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf"), "woman: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xb0"), "woman: red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "woman: light skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "woman: medium-light skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "woman: medium skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "woman: medium-dark skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "woman: dark skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xb0"), "person: red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "person: light skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "person: medium-light skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "person: medium skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "person: medium-dark skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb0"), + "person: dark skin tone, red hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xb1"), "woman: curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "woman: light skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "woman: medium-light skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "woman: medium skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "woman: medium-dark skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "woman: dark skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xb1"), "person: curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "person: light skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "person: medium-light skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "person: medium skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "person: medium-dark skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb1"), + "person: dark skin tone, curly hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xb3"), "woman: white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "woman: light skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "woman: medium-light skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "woman: medium skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "woman: medium-dark skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "woman: dark skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xb3"), "person: white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "person: light skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "person: medium-light skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "person: medium skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "person: medium-dark skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb3"), + "person: dark skin tone, white hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xb2"), "woman: bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "woman: light skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "woman: medium-light skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "woman: medium skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "woman: medium-dark skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "woman: dark skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xb2"), "person: bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "person: light skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "person: medium-light skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "person: medium skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "person: medium-dark skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb2"), + "person: dark skin tone, bald"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xe2\x80\x8d\xe2\x99\x80"), "woman: blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman: light skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman: medium-light skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman: medium skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman: medium-dark skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman: dark skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xe2\x80\x8d\xe2\x99\x82"), "man: blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man: light skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man: medium-light skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man: medium skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man: medium-dark skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man: dark skin tone, blond hair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x93"), "older person"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x93\xf0\x9f\x8f\xbb"), "older person: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x93\xf0\x9f\x8f\xbc"), + "older person: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x93\xf0\x9f\x8f\xbd"), "older person: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x93\xf0\x9f\x8f\xbe"), + "older person: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x93\xf0\x9f\x8f\xbf"), "older person: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb4"), "old man"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb4\xf0\x9f\x8f\xbb"), "old man: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb4\xf0\x9f\x8f\xbc"), "old man: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb4\xf0\x9f\x8f\xbd"), "old man: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb4\xf0\x9f\x8f\xbe"), "old man: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb4\xf0\x9f\x8f\xbf"), "old man: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb5"), "old woman"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb5\xf0\x9f\x8f\xbb"), "old woman: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb5\xf0\x9f\x8f\xbc"), "old woman: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb5\xf0\x9f\x8f\xbd"), "old woman: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb5\xf0\x9f\x8f\xbe"), "old woman: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb5\xf0\x9f\x8f\xbf"), "old woman: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d"), "person frowning"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbb"), "person frowning: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbc"), + "person frowning: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbd"), "person frowning: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbe"), + "person frowning: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbf"), "person frowning: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xe2\x80\x8d\xe2\x99\x82"), "man frowning"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man frowning: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man frowning: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man frowning: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man frowning: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man frowning: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xe2\x80\x8d\xe2\x99\x80"), "woman frowning"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman frowning: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman frowning: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman frowning: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman frowning: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman frowning: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e"), "person pouting"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbb"), "person pouting: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbc"), + "person pouting: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbd"), "person pouting: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbe"), + "person pouting: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbf"), "person pouting: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xe2\x80\x8d\xe2\x99\x82"), "man pouting"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man pouting: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man pouting: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man pouting: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man pouting: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man pouting: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xe2\x80\x8d\xe2\x99\x80"), "woman pouting"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman pouting: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman pouting: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman pouting: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman pouting: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman pouting: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85"), "person gesturing NO"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbb"), + "person gesturing NO: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbc"), + "person gesturing NO: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbd"), + "person gesturing NO: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbe"), + "person gesturing NO: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbf"), + "person gesturing NO: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xe2\x80\x8d\xe2\x99\x82"), "man gesturing NO"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing NO: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing NO: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing NO: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing NO: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing NO: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xe2\x80\x8d\xe2\x99\x80"), "woman gesturing NO"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing NO: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing NO: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing NO: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing NO: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x85\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing NO: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86"), "person gesturing OK"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbb"), + "person gesturing OK: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbc"), + "person gesturing OK: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbd"), + "person gesturing OK: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbe"), + "person gesturing OK: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbf"), + "person gesturing OK: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xe2\x80\x8d\xe2\x99\x82"), "man gesturing OK"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing OK: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing OK: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing OK: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing OK: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man gesturing OK: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xe2\x80\x8d\xe2\x99\x80"), "woman gesturing OK"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing OK: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing OK: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing OK: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing OK: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x86\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman gesturing OK: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81"), "person tipping hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbb"), + "person tipping hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbc"), + "person tipping hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbd"), + "person tipping hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbe"), + "person tipping hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbf"), + "person tipping hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xe2\x80\x8d\xe2\x99\x82"), "man tipping hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man tipping hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man tipping hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man tipping hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man tipping hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man tipping hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xe2\x80\x8d\xe2\x99\x80"), "woman tipping hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman tipping hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman tipping hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman tipping hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman tipping hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x81\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman tipping hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b"), "person raising hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbb"), + "person raising hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbc"), + "person raising hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbd"), + "person raising hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbe"), + "person raising hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbf"), + "person raising hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xe2\x80\x8d\xe2\x99\x82"), "man raising hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man raising hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man raising hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man raising hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man raising hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man raising hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xe2\x80\x8d\xe2\x99\x80"), "woman raising hand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman raising hand: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman raising hand: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman raising hand: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman raising hand: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman raising hand: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f"), "deaf person"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbb"), "deaf person: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbc"), + "deaf person: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbd"), "deaf person: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbe"), + "deaf person: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbf"), "deaf person: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xe2\x80\x8d\xe2\x99\x82"), "deaf man"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "deaf man: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "deaf man: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "deaf man: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "deaf man: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "deaf man: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xe2\x80\x8d\xe2\x99\x80"), "deaf woman"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "deaf woman: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "deaf woman: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "deaf woman: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "deaf woman: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "deaf woman: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87"), "person bowing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbb"), "person bowing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbc"), + "person bowing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbd"), "person bowing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbe"), + "person bowing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbf"), "person bowing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xe2\x80\x8d\xe2\x99\x82"), "man bowing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man bowing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man bowing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man bowing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man bowing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man bowing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xe2\x80\x8d\xe2\x99\x80"), "woman bowing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman bowing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman bowing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman bowing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman bowing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x99\x87\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman bowing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6"), "person facepalming"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbb"), + "person facepalming: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbc"), + "person facepalming: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbd"), + "person facepalming: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbe"), + "person facepalming: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbf"), + "person facepalming: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xe2\x80\x8d\xe2\x99\x82"), "man facepalming"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man facepalming: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man facepalming: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man facepalming: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man facepalming: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man facepalming: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xe2\x80\x8d\xe2\x99\x80"), "woman facepalming"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman facepalming: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman facepalming: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman facepalming: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman facepalming: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman facepalming: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7"), "person shrugging"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbb"), "person shrugging: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbc"), + "person shrugging: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbd"), + "person shrugging: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbe"), + "person shrugging: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbf"), "person shrugging: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xe2\x80\x8d\xe2\x99\x82"), "man shrugging"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man shrugging: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man shrugging: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man shrugging: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man shrugging: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man shrugging: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xe2\x80\x8d\xe2\x99\x80"), "woman shrugging"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman shrugging: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman shrugging: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman shrugging: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman shrugging: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman shrugging: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xe2\x9a\x95"), "health worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9a\x95"), + "health worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9a\x95"), + "health worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9a\x95"), + "health worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9a\x95"), + "health worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9a\x95"), + "health worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9a\x95"), "man health worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9a\x95"), + "man health worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9a\x95"), + "man health worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9a\x95"), + "man health worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9a\x95"), + "man health worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9a\x95"), + "man health worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9a\x95"), "woman health worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9a\x95"), + "woman health worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9a\x95"), + "woman health worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9a\x95"), + "woman health worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9a\x95"), + "woman health worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9a\x95"), + "woman health worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8e\x93"), "student"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "student: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "student: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "student: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "student: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "student: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8e\x93"), "man student"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "man student: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "man student: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "man student: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "man student: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "man student: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8e\x93"), "woman student"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "woman student: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "woman student: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "woman student: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "woman student: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\x93"), + "woman student: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8f\xab"), "teacher"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "teacher: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "teacher: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "teacher: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "teacher: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "teacher: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8f\xab"), "man teacher"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "man teacher: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "man teacher: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "man teacher: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "man teacher: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "man teacher: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8f\xab"), "woman teacher"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "woman teacher: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "woman teacher: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "woman teacher: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "woman teacher: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8f\xab"), + "woman teacher: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xe2\x9a\x96"), "judge"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9a\x96"), + "judge: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9a\x96"), + "judge: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9a\x96"), + "judge: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9a\x96"), + "judge: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9a\x96"), + "judge: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9a\x96"), "man judge"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9a\x96"), + "man judge: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9a\x96"), + "man judge: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9a\x96"), + "man judge: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9a\x96"), + "man judge: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9a\x96"), + "man judge: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9a\x96"), "woman judge"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9a\x96"), + "woman judge: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9a\x96"), + "woman judge: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9a\x96"), + "woman judge: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9a\x96"), + "woman judge: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9a\x96"), + "woman judge: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8c\xbe"), "farmer"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "farmer: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "farmer: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "farmer: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "farmer: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "farmer: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8c\xbe"), "man farmer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "man farmer: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "man farmer: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "man farmer: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "man farmer: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "man farmer: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8c\xbe"), "woman farmer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "woman farmer: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "woman farmer: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "woman farmer: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "woman farmer: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8c\xbe"), + "woman farmer: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8d\xb3"), "cook"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "cook: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "cook: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "cook: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "cook: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "cook: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8d\xb3"), "man cook"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "man cook: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "man cook: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "man cook: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "man cook: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "man cook: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8d\xb3"), "woman cook"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "woman cook: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "woman cook: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "woman cook: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "woman cook: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8d\xb3"), + "woman cook: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x94\xa7"), "mechanic"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "mechanic: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "mechanic: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "mechanic: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "mechanic: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "mechanic: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x94\xa7"), "man mechanic"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "man mechanic: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "man mechanic: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "man mechanic: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "man mechanic: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "man mechanic: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x94\xa7"), "woman mechanic"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "woman mechanic: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "woman mechanic: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "woman mechanic: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "woman mechanic: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x94\xa7"), + "woman mechanic: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8f\xad"), "factory worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "factory worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "factory worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "factory worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "factory worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "factory worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8f\xad"), "man factory worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "man factory worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "man factory worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "man factory worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "man factory worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "man factory worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8f\xad"), "woman factory worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "woman factory worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "woman factory worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "woman factory worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "woman factory worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8f\xad"), + "woman factory worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x92\xbc"), "office worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "office worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "office worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "office worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "office worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "office worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x92\xbc"), "man office worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "man office worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "man office worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "man office worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "man office worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "man office worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x92\xbc"), "woman office worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "woman office worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "woman office worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "woman office worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "woman office worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x92\xbc"), + "woman office worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x94\xac"), "scientist"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x94\xac"), + "scientist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x94\xac"), + "scientist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x94\xac"), + "scientist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x94\xac"), + "scientist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x94\xac"), + "scientist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x94\xac"), "man scientist"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x94\xac"), + "man scientist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x94\xac"), + "man scientist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x94\xac"), + "man scientist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x94\xac"), + "man scientist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x94\xac"), + "man scientist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x94\xac"), "woman scientist"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x94\xac"), + "woman scientist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x94\xac"), + "woman scientist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x94\xac"), + "woman scientist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x94\xac"), + "woman scientist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x94\xac"), + "woman scientist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x92\xbb"), "technologist"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "technologist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "technologist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "technologist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "technologist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "technologist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x92\xbb"), "man technologist"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "man technologist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "man technologist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "man technologist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "man technologist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "man technologist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x92\xbb"), "woman technologist"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "woman technologist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "woman technologist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "woman technologist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "woman technologist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x92\xbb"), + "woman technologist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8e\xa4"), "singer"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "singer: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "singer: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "singer: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "singer: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "singer: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8e\xa4"), "man singer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "man singer: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "man singer: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "man singer: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "man singer: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "man singer: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8e\xa4"), "woman singer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "woman singer: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "woman singer: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "woman singer: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "woman singer: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\xa4"), + "woman singer: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8e\xa8"), "artist"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "artist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "artist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "artist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "artist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "artist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8e\xa8"), "man artist"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "man artist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "man artist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "man artist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "man artist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "man artist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8e\xa8"), "woman artist"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "woman artist: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "woman artist: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "woman artist: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "woman artist: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\xa8"), + "woman artist: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xe2\x9c\x88"), "pilot"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9c\x88"), + "pilot: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9c\x88"), + "pilot: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9c\x88"), + "pilot: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9c\x88"), + "pilot: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9c\x88"), + "pilot: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9c\x88"), "man pilot"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9c\x88"), + "man pilot: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9c\x88"), + "man pilot: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9c\x88"), + "man pilot: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9c\x88"), + "man pilot: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9c\x88"), + "man pilot: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9c\x88"), "woman pilot"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9c\x88"), + "woman pilot: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9c\x88"), + "woman pilot: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9c\x88"), + "woman pilot: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9c\x88"), + "woman pilot: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9c\x88"), + "woman pilot: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x9a\x80"), "astronaut"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "astronaut: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "astronaut: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "astronaut: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "astronaut: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "astronaut: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x9a\x80"), "man astronaut"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "man astronaut: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "man astronaut: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "man astronaut: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "man astronaut: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "man astronaut: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x9a\x80"), "woman astronaut"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "woman astronaut: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "woman astronaut: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "woman astronaut: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "woman astronaut: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x9a\x80"), + "woman astronaut: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x9a\x92"), "firefighter"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "firefighter: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "firefighter: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "firefighter: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "firefighter: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "firefighter: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x9a\x92"), "man firefighter"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "man firefighter: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "man firefighter: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "man firefighter: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "man firefighter: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "man firefighter: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x9a\x92"), "woman firefighter"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "woman firefighter: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "woman firefighter: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "woman firefighter: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "woman firefighter: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x9a\x92"), + "woman firefighter: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae"), "police officer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbb"), "police officer: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbc"), + "police officer: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbd"), "police officer: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbe"), + "police officer: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbf"), "police officer: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xe2\x80\x8d\xe2\x99\x82"), "man police officer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man police officer: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man police officer: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man police officer: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man police officer: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man police officer: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xe2\x80\x8d\xe2\x99\x80"), "woman police officer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman police officer: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman police officer: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman police officer: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman police officer: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xae\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman police officer: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5"), "detective"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbb"), "detective: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbc"), "detective: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbd"), "detective: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbe"), "detective: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbf"), "detective: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x82"), "man detective"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xe2\x80\x8d\xe2\x99\x82"), "man detective"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man detective: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man detective: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man detective: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man detective: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man detective: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x80"), + "woman detective"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xe2\x80\x8d\xe2\x99\x80"), "woman detective"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman detective: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman detective: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman detective: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman detective: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman detective: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82"), "guard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbb"), "guard: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbc"), "guard: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbd"), "guard: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbe"), "guard: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbf"), "guard: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xe2\x80\x8d\xe2\x99\x82"), "man guard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man guard: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man guard: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man guard: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man guard: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man guard: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xe2\x80\x8d\xe2\x99\x80"), "woman guard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman guard: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman guard: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman guard: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman guard: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x82\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman guard: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7"), "construction worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbb"), + "construction worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbc"), + "construction worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbd"), + "construction worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbe"), + "construction worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbf"), + "construction worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xe2\x80\x8d\xe2\x99\x82"), "man construction worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man construction worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man construction worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man construction worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man construction worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man construction worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xe2\x80\x8d\xe2\x99\x80"), "woman construction worker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman construction worker: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman construction worker: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman construction worker: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman construction worker: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman construction worker: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb4"), "prince"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb4\xf0\x9f\x8f\xbb"), "prince: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb4\xf0\x9f\x8f\xbc"), "prince: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb4\xf0\x9f\x8f\xbd"), "prince: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb4\xf0\x9f\x8f\xbe"), "prince: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb4\xf0\x9f\x8f\xbf"), "prince: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb8"), "princess"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb8\xf0\x9f\x8f\xbb"), "princess: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb8\xf0\x9f\x8f\xbc"), "princess: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb8\xf0\x9f\x8f\xbd"), "princess: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb8\xf0\x9f\x8f\xbe"), "princess: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb8\xf0\x9f\x8f\xbf"), "princess: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3"), "person wearing turban"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbb"), + "person wearing turban: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbc"), + "person wearing turban: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbd"), + "person wearing turban: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbe"), + "person wearing turban: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbf"), + "person wearing turban: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xe2\x80\x8d\xe2\x99\x82"), "man wearing turban"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man wearing turban: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man wearing turban: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man wearing turban: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man wearing turban: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man wearing turban: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xe2\x80\x8d\xe2\x99\x80"), "woman wearing turban"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman wearing turban: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman wearing turban: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman wearing turban: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman wearing turban: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman wearing turban: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb2"), "man with skullcap"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb2\xf0\x9f\x8f\xbb"), + "man with skullcap: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb2\xf0\x9f\x8f\xbc"), + "man with skullcap: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb2\xf0\x9f\x8f\xbd"), + "man with skullcap: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb2\xf0\x9f\x8f\xbe"), + "man with skullcap: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb2\xf0\x9f\x8f\xbf"), "man with skullcap: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x95"), "woman with headscarf"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x95\xf0\x9f\x8f\xbb"), + "woman with headscarf: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x95\xf0\x9f\x8f\xbc"), + "woman with headscarf: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x95\xf0\x9f\x8f\xbd"), + "woman with headscarf: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x95\xf0\x9f\x8f\xbe"), + "woman with headscarf: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x95\xf0\x9f\x8f\xbf"), + "woman with headscarf: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5"), "man in tuxedo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbb"), "man in tuxedo: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbc"), + "man in tuxedo: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbd"), "man in tuxedo: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbe"), + "man in tuxedo: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbf"), "man in tuxedo: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xe2\x80\x8d\xe2\x99\x82"), "man in tuxedo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man in tuxedo: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man in tuxedo: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man in tuxedo: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man in tuxedo: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man in tuxedo: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xe2\x80\x8d\xe2\x99\x80"), "woman in tuxedo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman in tuxedo: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman in tuxedo: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman in tuxedo: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman in tuxedo: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman in tuxedo: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0"), "bride with veil"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbb"), "bride with veil: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbc"), + "bride with veil: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbd"), "bride with veil: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbe"), + "bride with veil: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbf"), "bride with veil: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xe2\x80\x8d\xe2\x99\x82"), "man with veil"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man with veil: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man with veil: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man with veil: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man with veil: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man with veil: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xe2\x80\x8d\xe2\x99\x80"), "woman with veil"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman with veil: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman with veil: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman with veil: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman with veil: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman with veil: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb0"), "pregnant woman"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb0\xf0\x9f\x8f\xbb"), "pregnant woman: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb0\xf0\x9f\x8f\xbc"), + "pregnant woman: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb0\xf0\x9f\x8f\xbd"), "pregnant woman: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb0\xf0\x9f\x8f\xbe"), + "pregnant woman: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb0\xf0\x9f\x8f\xbf"), "pregnant woman: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb1"), "breast-feeding"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb1\xf0\x9f\x8f\xbb"), "breast-feeding: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb1\xf0\x9f\x8f\xbc"), + "breast-feeding: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb1\xf0\x9f\x8f\xbd"), "breast-feeding: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb1\xf0\x9f\x8f\xbe"), + "breast-feeding: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb1\xf0\x9f\x8f\xbf"), "breast-feeding: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8d\xbc"), "woman feeding baby"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "woman feeding baby: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "woman feeding baby: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "woman feeding baby: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "woman feeding baby: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "woman feeding baby: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8d\xbc"), "man feeding baby"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "man feeding baby: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "man feeding baby: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "man feeding baby: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "man feeding baby: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "man feeding baby: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8d\xbc"), "person feeding baby"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "person feeding baby: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "person feeding baby: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "person feeding baby: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "person feeding baby: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8d\xbc"), + "person feeding baby: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbc"), "baby angel"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbc\xf0\x9f\x8f\xbb"), "baby angel: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbc\xf0\x9f\x8f\xbc"), + "baby angel: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbc\xf0\x9f\x8f\xbd"), "baby angel: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbc\xf0\x9f\x8f\xbe"), "baby angel: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xbc\xf0\x9f\x8f\xbf"), "baby angel: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x85"), "Santa Claus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x85\xf0\x9f\x8f\xbb"), "Santa Claus: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x85\xf0\x9f\x8f\xbc"), + "Santa Claus: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x85\xf0\x9f\x8f\xbd"), "Santa Claus: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x85\xf0\x9f\x8f\xbe"), + "Santa Claus: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x85\xf0\x9f\x8f\xbf"), "Santa Claus: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb6"), "Mrs. Claus"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb6\xf0\x9f\x8f\xbb"), "Mrs. Claus: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb6\xf0\x9f\x8f\xbc"), + "Mrs. Claus: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb6\xf0\x9f\x8f\xbd"), "Mrs. Claus: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb6\xf0\x9f\x8f\xbe"), "Mrs. Claus: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb6\xf0\x9f\x8f\xbf"), "Mrs. Claus: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8e\x84"), "mx claus"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\x84"), + "mx claus: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\x84"), + "mx claus: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\x84"), + "mx claus: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\x84"), + "mx claus: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\x84"), + "mx claus: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8"), "superhero"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbb"), "superhero: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbc"), "superhero: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbd"), "superhero: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbe"), "superhero: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbf"), "superhero: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xe2\x80\x8d\xe2\x99\x82"), "man superhero"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man superhero: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man superhero: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man superhero: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man superhero: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man superhero: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xe2\x80\x8d\xe2\x99\x80"), "woman superhero"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman superhero: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman superhero: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman superhero: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman superhero: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman superhero: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9"), "supervillain"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbb"), "supervillain: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbc"), + "supervillain: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbd"), "supervillain: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbe"), + "supervillain: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbf"), "supervillain: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xe2\x80\x8d\xe2\x99\x82"), "man supervillain"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man supervillain: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man supervillain: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man supervillain: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man supervillain: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man supervillain: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xe2\x80\x8d\xe2\x99\x80"), "woman supervillain"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman supervillain: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman supervillain: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman supervillain: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman supervillain: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman supervillain: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99"), "mage"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbb"), "mage: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbc"), "mage: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbd"), "mage: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbe"), "mage: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbf"), "mage: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xe2\x80\x8d\xe2\x99\x82"), "man mage"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man mage: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man mage: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man mage: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man mage: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man mage: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xe2\x80\x8d\xe2\x99\x80"), "woman mage"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman mage: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman mage: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman mage: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman mage: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman mage: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a"), "fairy"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbb"), "fairy: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbc"), "fairy: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbd"), "fairy: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbe"), "fairy: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbf"), "fairy: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xe2\x80\x8d\xe2\x99\x82"), "man fairy"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man fairy: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man fairy: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man fairy: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man fairy: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man fairy: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xe2\x80\x8d\xe2\x99\x80"), "woman fairy"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman fairy: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman fairy: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman fairy: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman fairy: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman fairy: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b"), "vampire"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbb"), "vampire: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbc"), "vampire: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbd"), "vampire: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbe"), "vampire: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbf"), "vampire: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xe2\x80\x8d\xe2\x99\x82"), "man vampire"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man vampire: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man vampire: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man vampire: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man vampire: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man vampire: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xe2\x80\x8d\xe2\x99\x80"), "woman vampire"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman vampire: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman vampire: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman vampire: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman vampire: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman vampire: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c"), "merperson"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbb"), "merperson: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbc"), "merperson: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbd"), "merperson: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbe"), "merperson: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbf"), "merperson: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xe2\x80\x8d\xe2\x99\x82"), "merman"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "merman: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "merman: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "merman: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "merman: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "merman: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xe2\x80\x8d\xe2\x99\x80"), "mermaid"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "mermaid: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "mermaid: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "mermaid: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "mermaid: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "mermaid: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d"), "elf"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbb"), "elf: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbc"), "elf: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbd"), "elf: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbe"), "elf: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbf"), "elf: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xe2\x80\x8d\xe2\x99\x82"), "man elf"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man elf: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man elf: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man elf: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man elf: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man elf: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xe2\x80\x8d\xe2\x99\x80"), "woman elf"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman elf: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman elf: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman elf: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman elf: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman elf: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9e"), "genie"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9e\xe2\x80\x8d\xe2\x99\x82"), "man genie"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9e\xe2\x80\x8d\xe2\x99\x80"), "woman genie"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9f"), "zombie"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9f\xe2\x80\x8d\xe2\x99\x82"), "man zombie"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x9f\xe2\x80\x8d\xe2\x99\x80"), "woman zombie"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86"), "person getting massage"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbb"), + "person getting massage: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbc"), + "person getting massage: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbd"), + "person getting massage: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbe"), + "person getting massage: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbf"), + "person getting massage: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xe2\x80\x8d\xe2\x99\x82"), "man getting massage"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man getting massage: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man getting massage: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man getting massage: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man getting massage: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man getting massage: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xe2\x80\x8d\xe2\x99\x80"), "woman getting massage"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman getting massage: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman getting massage: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman getting massage: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman getting massage: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x86\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman getting massage: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87"), "person getting haircut"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbb"), + "person getting haircut: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbc"), + "person getting haircut: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbd"), + "person getting haircut: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbe"), + "person getting haircut: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbf"), + "person getting haircut: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xe2\x80\x8d\xe2\x99\x82"), "man getting haircut"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man getting haircut: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man getting haircut: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man getting haircut: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man getting haircut: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man getting haircut: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xe2\x80\x8d\xe2\x99\x80"), "woman getting haircut"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman getting haircut: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman getting haircut: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman getting haircut: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman getting haircut: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x87\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman getting haircut: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6"), "person walking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbb"), "person walking: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbc"), + "person walking: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbd"), "person walking: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbe"), + "person walking: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbf"), "person walking: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xe2\x80\x8d\xe2\x99\x82"), "man walking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man walking: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man walking: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man walking: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man walking: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man walking: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xe2\x80\x8d\xe2\x99\x80"), "woman walking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman walking: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman walking: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman walking: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman walking: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman walking: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d"), "person standing"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbb"), "person standing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbc"), + "person standing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbd"), "person standing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbe"), + "person standing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbf"), "person standing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xe2\x80\x8d\xe2\x99\x82"), "man standing"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man standing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man standing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man standing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man standing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man standing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xe2\x80\x8d\xe2\x99\x80"), "woman standing"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman standing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman standing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman standing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman standing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman standing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e"), "person kneeling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbb"), "person kneeling: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbc"), + "person kneeling: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbd"), "person kneeling: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbe"), + "person kneeling: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbf"), "person kneeling: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xe2\x80\x8d\xe2\x99\x82"), "man kneeling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man kneeling: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man kneeling: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man kneeling: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man kneeling: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man kneeling: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xe2\x80\x8d\xe2\x99\x80"), "woman kneeling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman kneeling: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman kneeling: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman kneeling: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman kneeling: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman kneeling: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "person with probing cane"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "person with probing cane: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "person with probing cane: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "person with probing cane: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "person with probing cane: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "person with probing cane: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xaf"), "man with probing cane"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "man with probing cane: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "man with probing cane: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "man with probing cane: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "man with probing cane: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "man with probing cane: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "woman with probing cane"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "woman with probing cane: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "woman with probing cane: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "woman with probing cane: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "woman with probing cane: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xaf"), + "woman with probing cane: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "person in motorized wheelchair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "person in motorized wheelchair: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "person in motorized wheelchair: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "person in motorized wheelchair: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "person in motorized wheelchair: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "person in motorized wheelchair: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "man in motorized wheelchair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "man in motorized wheelchair: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "man in motorized wheelchair: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "man in motorized wheelchair: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "man in motorized wheelchair: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "man in motorized wheelchair: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "woman in motorized wheelchair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "woman in motorized wheelchair: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "woman in motorized wheelchair: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "woman in motorized wheelchair: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "woman in motorized wheelchair: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbc"), + "woman in motorized wheelchair: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "person in manual wheelchair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "person in manual wheelchair: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "person in manual wheelchair: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "person in manual wheelchair: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "person in manual wheelchair: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "person in manual wheelchair: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "man in manual wheelchair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "man in manual wheelchair: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "man in manual wheelchair: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "man in manual wheelchair: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "man in manual wheelchair: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "man in manual wheelchair: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "woman in manual wheelchair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "woman in manual wheelchair: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "woman in manual wheelchair: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "woman in manual wheelchair: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "woman in manual wheelchair: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbd"), + "woman in manual wheelchair: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83"), "person running"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbb"), "person running: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbc"), + "person running: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbd"), "person running: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbe"), + "person running: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbf"), "person running: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xe2\x80\x8d\xe2\x99\x82"), "man running"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man running: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man running: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man running: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man running: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man running: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xe2\x80\x8d\xe2\x99\x80"), "woman running"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman running: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman running: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman running: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman running: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman running: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x83"), "woman dancing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x83\xf0\x9f\x8f\xbb"), "woman dancing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x83\xf0\x9f\x8f\xbc"), + "woman dancing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x83\xf0\x9f\x8f\xbd"), "woman dancing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x83\xf0\x9f\x8f\xbe"), + "woman dancing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x83\xf0\x9f\x8f\xbf"), "woman dancing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xba"), "man dancing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xba\xf0\x9f\x8f\xbb"), "man dancing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xba\xf0\x9f\x8f\xbc"), + "man dancing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xba\xf0\x9f\x8f\xbd"), "man dancing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xba\xf0\x9f\x8f\xbe"), + "man dancing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xba\xf0\x9f\x8f\xbf"), "man dancing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb4"), "man in suit levitating"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb4\xf0\x9f\x8f\xbb"), + "man in suit levitating: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb4\xf0\x9f\x8f\xbc"), + "man in suit levitating: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb4\xf0\x9f\x8f\xbd"), + "man in suit levitating: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb4\xf0\x9f\x8f\xbe"), + "man in suit levitating: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb4\xf0\x9f\x8f\xbf"), + "man in suit levitating: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xaf"), "people with bunny ears"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xaf\xe2\x80\x8d\xe2\x99\x82"), "men with bunny ears"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xaf\xe2\x80\x8d\xe2\x99\x80"), "women with bunny ears"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96"), "person in steamy room"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbb"), + "person in steamy room: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbc"), + "person in steamy room: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbd"), + "person in steamy room: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbe"), + "person in steamy room: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbf"), + "person in steamy room: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xe2\x80\x8d\xe2\x99\x82"), "man in steamy room"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man in steamy room: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man in steamy room: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man in steamy room: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man in steamy room: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man in steamy room: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xe2\x80\x8d\xe2\x99\x80"), "woman in steamy room"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman in steamy room: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman in steamy room: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman in steamy room: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman in steamy room: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman in steamy room: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97"), "person climbing"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbb"), "person climbing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbc"), + "person climbing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbd"), "person climbing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbe"), + "person climbing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbf"), "person climbing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xe2\x80\x8d\xe2\x99\x82"), "man climbing"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man climbing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man climbing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man climbing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man climbing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man climbing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xe2\x80\x8d\xe2\x99\x80"), "woman climbing"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman climbing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman climbing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman climbing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman climbing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman climbing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xb7"), "ninja"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xba"), "person fencing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87"), "horse racing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbb"), "horse racing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbc"), + "horse racing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbd"), "horse racing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbe"), + "horse racing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbf"), "horse racing: dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb7"), "skier"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x82"), "snowboarder"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x82\xf0\x9f\x8f\xbb"), "snowboarder: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x82\xf0\x9f\x8f\xbc"), + "snowboarder: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x82\xf0\x9f\x8f\xbd"), "snowboarder: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x82\xf0\x9f\x8f\xbe"), + "snowboarder: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x82\xf0\x9f\x8f\xbf"), "snowboarder: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c"), "person golfing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbb"), "person golfing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbc"), + "person golfing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbd"), "person golfing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbe"), + "person golfing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbf"), "person golfing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x82"), "man golfing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xe2\x80\x8d\xe2\x99\x82"), "man golfing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man golfing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man golfing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man golfing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man golfing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man golfing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x80"), "woman golfing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xe2\x80\x8d\xe2\x99\x80"), "woman golfing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman golfing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman golfing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman golfing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman golfing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman golfing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84"), "person surfing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbb"), "person surfing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbc"), + "person surfing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbd"), "person surfing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbe"), + "person surfing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbf"), "person surfing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xe2\x80\x8d\xe2\x99\x82"), "man surfing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man surfing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man surfing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man surfing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man surfing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man surfing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xe2\x80\x8d\xe2\x99\x80"), "woman surfing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman surfing: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman surfing: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman surfing: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman surfing: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman surfing: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3"), "person rowing boat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbb"), + "person rowing boat: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbc"), + "person rowing boat: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbd"), + "person rowing boat: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbe"), + "person rowing boat: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbf"), + "person rowing boat: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xe2\x80\x8d\xe2\x99\x82"), "man rowing boat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man rowing boat: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man rowing boat: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man rowing boat: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man rowing boat: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man rowing boat: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xe2\x80\x8d\xe2\x99\x80"), "woman rowing boat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman rowing boat: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman rowing boat: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman rowing boat: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman rowing boat: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman rowing boat: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a"), "person swimming"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbb"), "person swimming: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbc"), + "person swimming: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbd"), "person swimming: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbe"), + "person swimming: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbf"), "person swimming: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xe2\x80\x8d\xe2\x99\x82"), "man swimming"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man swimming: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man swimming: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man swimming: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man swimming: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man swimming: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xe2\x80\x8d\xe2\x99\x80"), "woman swimming"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman swimming: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman swimming: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman swimming: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman swimming: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman swimming: dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9"), "person bouncing ball"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbb"), "person bouncing ball: light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbc"), + "person bouncing ball: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbd"), + "person bouncing ball: medium skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbe"), + "person bouncing ball: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbf"), "person bouncing ball: dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x82"), "man bouncing ball"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xe2\x80\x8d\xe2\x99\x82"), "man bouncing ball"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man bouncing ball: light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man bouncing ball: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man bouncing ball: medium skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man bouncing ball: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man bouncing ball: dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x80"), + "woman bouncing ball"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xe2\x80\x8d\xe2\x99\x80"), "woman bouncing ball"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman bouncing ball: light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman bouncing ball: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman bouncing ball: medium skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman bouncing ball: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman bouncing ball: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b"), "person lifting weights"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbb"), + "person lifting weights: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbc"), + "person lifting weights: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbd"), + "person lifting weights: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbe"), + "person lifting weights: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbf"), + "person lifting weights: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x82"), + "man lifting weights"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xe2\x80\x8d\xe2\x99\x82"), "man lifting weights"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man lifting weights: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man lifting weights: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man lifting weights: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man lifting weights: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man lifting weights: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x80"), + "woman lifting weights"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xe2\x80\x8d\xe2\x99\x80"), "woman lifting weights"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman lifting weights: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman lifting weights: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman lifting weights: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman lifting weights: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman lifting weights: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4"), "person biking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbb"), "person biking: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbc"), + "person biking: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbd"), "person biking: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbe"), + "person biking: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbf"), "person biking: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xe2\x80\x8d\xe2\x99\x82"), "man biking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man biking: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man biking: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man biking: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man biking: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man biking: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xe2\x80\x8d\xe2\x99\x80"), "woman biking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman biking: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman biking: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman biking: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman biking: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman biking: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5"), "person mountain biking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbb"), + "person mountain biking: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbc"), + "person mountain biking: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbd"), + "person mountain biking: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbe"), + "person mountain biking: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbf"), + "person mountain biking: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xe2\x80\x8d\xe2\x99\x82"), "man mountain biking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man mountain biking: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man mountain biking: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man mountain biking: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man mountain biking: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man mountain biking: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xe2\x80\x8d\xe2\x99\x80"), "woman mountain biking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman mountain biking: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman mountain biking: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman mountain biking: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman mountain biking: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman mountain biking: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8"), "person cartwheeling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbb"), + "person cartwheeling: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbc"), + "person cartwheeling: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbd"), + "person cartwheeling: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbe"), + "person cartwheeling: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbf"), + "person cartwheeling: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xe2\x80\x8d\xe2\x99\x82"), "man cartwheeling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man cartwheeling: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man cartwheeling: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man cartwheeling: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man cartwheeling: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man cartwheeling: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xe2\x80\x8d\xe2\x99\x80"), "woman cartwheeling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman cartwheeling: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman cartwheeling: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman cartwheeling: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman cartwheeling: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman cartwheeling: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbc"), "people wrestling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbc\xe2\x80\x8d\xe2\x99\x82"), "men wrestling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbc\xe2\x80\x8d\xe2\x99\x80"), "women wrestling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd"), "person playing water polo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbb"), + "person playing water polo: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbc"), + "person playing water polo: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbd"), + "person playing water polo: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbe"), + "person playing water polo: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbf"), + "person playing water polo: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xe2\x80\x8d\xe2\x99\x82"), "man playing water polo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man playing water polo: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man playing water polo: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man playing water polo: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man playing water polo: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man playing water polo: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xe2\x80\x8d\xe2\x99\x80"), "woman playing water polo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman playing water polo: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman playing water polo: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman playing water polo: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman playing water polo: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman playing water polo: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe"), "person playing handball"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbb"), + "person playing handball: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbc"), + "person playing handball: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbd"), + "person playing handball: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbe"), + "person playing handball: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbf"), + "person playing handball: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xe2\x80\x8d\xe2\x99\x82"), "man playing handball"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man playing handball: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man playing handball: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man playing handball: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man playing handball: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man playing handball: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xe2\x80\x8d\xe2\x99\x80"), "woman playing handball"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman playing handball: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman playing handball: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman playing handball: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman playing handball: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman playing handball: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9"), "person juggling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbb"), "person juggling: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbc"), + "person juggling: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbd"), "person juggling: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbe"), + "person juggling: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbf"), "person juggling: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xe2\x80\x8d\xe2\x99\x82"), "man juggling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man juggling: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man juggling: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man juggling: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man juggling: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man juggling: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xe2\x80\x8d\xe2\x99\x80"), "woman juggling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman juggling: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman juggling: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman juggling: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman juggling: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman juggling: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98"), "person in lotus position"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbb"), + "person in lotus position: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbc"), + "person in lotus position: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbd"), + "person in lotus position: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbe"), + "person in lotus position: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbf"), + "person in lotus position: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xe2\x80\x8d\xe2\x99\x82"), "man in lotus position"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82"), + "man in lotus position: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82"), + "man in lotus position: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82"), + "man in lotus position: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82"), + "man in lotus position: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82"), + "man in lotus position: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xe2\x80\x8d\xe2\x99\x80"), "woman in lotus position"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80"), + "woman in lotus position: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80"), + "woman in lotus position: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80"), + "woman in lotus position: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80"), + "woman in lotus position: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80"), + "woman in lotus position: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x80"), "person taking bath"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbb"), + "person taking bath: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbc"), + "person taking bath: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbd"), + "person taking bath: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbe"), + "person taking bath: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbf"), + "person taking bath: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8c"), "person in bed"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8c\xf0\x9f\x8f\xbb"), "person in bed: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8c\xf0\x9f\x8f\xbc"), + "person in bed: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8c\xf0\x9f\x8f\xbd"), "person in bed: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8c\xf0\x9f\x8f\xbe"), + "person in bed: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8c\xf0\x9f\x8f\xbf"), "person in bed: dark skin tone"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91"), + "people holding hands"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb"), + "people holding hands: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc"), + "people holding hands: light skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd"), + "people holding hands: light skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe"), + "people holding hands: light skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf"), + "people holding hands: light skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb"), + "people holding hands: medium-light skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc"), + "people holding hands: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd"), + "people holding hands: medium-light skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe"), + "people holding hands: medium-light skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf"), + "people holding hands: medium-light skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb"), + "people holding hands: medium skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc"), + "people holding hands: medium skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd"), + "people holding hands: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe"), + "people holding hands: medium skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf"), + "people holding hands: medium skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb"), + "people holding hands: medium-dark skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc"), + "people holding hands: medium-dark skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd"), + "people holding hands: medium-dark skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe"), + "people holding hands: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf"), + "people holding hands: medium-dark skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb"), + "people holding hands: dark skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc"), + "people holding hands: dark skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd"), + "people holding hands: dark skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe"), + "people holding hands: dark skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf"), + "people holding hands: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xad"), "women holding hands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xad\xf0\x9f\x8f\xbb"), + "women holding hands: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc"), + "women holding hands: light skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd"), + "women holding hands: light skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe"), + "women holding hands: light skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf"), + "women holding hands: light skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb"), + "women holding hands: medium-light skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xad\xf0\x9f\x8f\xbc"), + "women holding hands: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd"), + "women holding hands: medium-light skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe"), + "women holding hands: medium-light skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf"), + "women holding hands: medium-light skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb"), + "women holding hands: medium skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc"), + "women holding hands: medium skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xad\xf0\x9f\x8f\xbd"), + "women holding hands: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe"), + "women holding hands: medium skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf"), + "women holding hands: medium skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb"), + "women holding hands: medium-dark skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc"), + "women holding hands: medium-dark skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd"), + "women holding hands: medium-dark skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xad\xf0\x9f\x8f\xbe"), + "women holding hands: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf"), + "women holding hands: medium-dark skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb"), + "women holding hands: dark skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc"), + "women holding hands: dark skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd"), + "women holding hands: dark skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe"), + "women holding hands: dark skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xad\xf0\x9f\x8f\xbf"), + "women holding hands: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xab"), "woman and man holding hands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xab\xf0\x9f\x8f\xbb"), + "woman and man holding hands: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc"), + "woman and man holding hands: light skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd"), + "woman and man holding hands: light skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe"), + "woman and man holding hands: light skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf"), + "woman and man holding hands: light skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb"), + "woman and man holding hands: medium-light skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xab\xf0\x9f\x8f\xbc"), + "woman and man holding hands: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd"), + "woman and man holding hands: medium-light skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe"), + "woman and man holding hands: medium-light skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf"), + "woman and man holding hands: medium-light skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb"), + "woman and man holding hands: medium skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc"), + "woman and man holding hands: medium skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xab\xf0\x9f\x8f\xbd"), + "woman and man holding hands: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe"), + "woman and man holding hands: medium skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf"), + "woman and man holding hands: medium skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb"), + "woman and man holding hands: medium-dark skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc"), + "woman and man holding hands: medium-dark skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd"), + "woman and man holding hands: medium-dark skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xab\xf0\x9f\x8f\xbe"), + "woman and man holding hands: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf"), + "woman and man holding hands: medium-dark skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb"), + "woman and man holding hands: dark skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc"), + "woman and man holding hands: dark skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd"), + "woman and man holding hands: dark skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe"), + "woman and man holding hands: dark skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xab\xf0\x9f\x8f\xbf"), + "woman and man holding hands: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xac"), "men holding hands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xac\xf0\x9f\x8f\xbb"), + "men holding hands: light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc"), + "men holding hands: light skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd"), + "men holding hands: light skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe"), + "men holding hands: light skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf"), + "men holding hands: light skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb"), + "men holding hands: medium-light skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xac\xf0\x9f\x8f\xbc"), + "men holding hands: medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd"), + "men holding hands: medium-light skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe"), + "men holding hands: medium-light skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf"), + "men holding hands: medium-light skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb"), + "men holding hands: medium skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc"), + "men holding hands: medium skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xac\xf0\x9f\x8f\xbd"), + "men holding hands: medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe"), + "men holding hands: medium skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf"), + "men holding hands: medium skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb"), + "men holding hands: medium-dark skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc"), + "men holding hands: medium-dark skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd"), + "men holding hands: medium-dark skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xac\xf0\x9f\x8f\xbe"), + "men holding hands: medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf"), + "men holding hands: medium-dark skin tone, dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb"), + "men holding hands: dark skin tone, light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc"), + "men holding hands: dark skin tone, medium-light skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd"), + "men holding hands: dark skin tone, medium skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d" + "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe"), + "men holding hands: dark skin tone, medium-dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xac\xf0\x9f\x8f\xbf"), "men holding hands: dark skin tone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x8f"), "kiss"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f" + "\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8"), + "kiss: woman, man"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2" + "\x80\x8d\xf0\x9f\x91\xa8"), + "kiss: woman, man"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f" + "\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8"), + "kiss: man, man"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9d\xa4\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2" + "\x80\x8d\xf0\x9f\x91\xa8"), + "kiss: man, man"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f" + "\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9"), + "kiss: woman, woman"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2" + "\x80\x8d\xf0\x9f\x91\xa9"), + "kiss: woman, woman"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x91"), "couple with heart"}, + Emoji{QString::fromUtf8( + "\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8"), + "couple with heart: woman, man"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xe2\x80\x8d\xf0\x9f\x91\xa8"), + "couple with heart: woman, man"}, + Emoji{QString::fromUtf8( + "\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8"), + "couple with heart: man, man"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9d\xa4\xe2\x80\x8d\xf0\x9f\x91\xa8"), + "couple with heart: man, man"}, + Emoji{QString::fromUtf8( + "\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9"), + "couple with heart: woman, woman"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xe2\x80\x8d\xf0\x9f\x91\xa9"), + "couple with heart: woman, woman"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xaa"), "family"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: man, woman, boy"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7"), + "family: man, woman, girl"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7" + "\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: man, woman, girl, boy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6" + "\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: man, woman, boy, boy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7" + "\xe2\x80\x8d\xf0\x9f\x91\xa7"), + "family: man, woman, girl, girl"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: man, man, boy"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7"), + "family: man, man, girl"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7" + "\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: man, man, girl, boy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa6" + "\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: man, man, boy, boy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7" + "\xe2\x80\x8d\xf0\x9f\x91\xa7"), + "family: man, man, girl, girl"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: woman, woman, boy"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7"), + "family: woman, woman, girl"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7" + "\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: woman, woman, girl, boy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6" + "\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: woman, woman, boy, boy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7" + "\xe2\x80\x8d\xf0\x9f\x91\xa7"), + "family: woman, woman, girl, girl"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa6"), "family: man, boy"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa6\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: man, boy, boy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7"), "family: man, girl"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: man, girl, boy"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa7"), + "family: man, girl, girl"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6"), "family: woman, boy"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: woman, boy, boy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7"), "family: woman, girl"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa6"), + "family: woman, girl, boy"}, + Emoji{ + QString::fromUtf8("\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa7"), + "family: woman, girl, girl"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xa3"), "speaking head"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa4"), "bust in silhouette"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa5"), "busts in silhouette"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x82"), "people hugging"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa3"), "footprints"}, }; -const std::vector<Emoji> Provider::nature = { - Emoji{QString::fromUtf8("\xf0\x9f\x99\x88"), ":see_no_evil:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x89"), ":hear_no_evil:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x99\x8a"), ":speak_no_evil:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa6"), ":sweat_drops:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa8"), ":dash:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb5"), ":monkey_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x92"), ":monkey:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8d"), ":gorilla:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb6"), ":dog:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x95"), ":dog2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa9"), ":poodle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xba"), ":wolf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8a"), ":fox:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb1"), ":cat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x88"), ":cat2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x81"), ":lion_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xaf"), ":tiger:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x85"), ":tiger2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x86"), ":leopard:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb4"), ":horse:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x8e"), ":racehorse:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8c"), ":deer:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x84"), ":unicorn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xae"), ":cow:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x82"), ":ox:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x83"), ":water_buffalo:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x84"), ":cow2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb7"), ":pig:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x96"), ":pig2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x97"), ":boar:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xbd"), ":pig_nose:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x8f"), ":ram:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x91"), ":sheep:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x90"), ":goat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xaa"), ":dromedary_camel:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xab"), ":camel:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x98"), ":elephant:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8f"), ":rhino:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xad"), ":mouse:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x81"), ":mouse2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x80"), ":rat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb9"), ":hamster:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb0"), ":rabbit:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x87"), ":rabbit2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xbf"), ":chipmunk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x87"), ":bat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xbb"), ":bear:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa8"), ":koala:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xbc"), ":panda_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xbe"), ":feet:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x83"), ":turkey:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x94"), ":chicken:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x93"), ":rooster:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa3"), ":hatching_chick:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa4"), ":baby_chick:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa5"), ":hatched_chick:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa6"), ":bird:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa7"), ":penguin:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x8a"), ":dove:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x85"), ":eagle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x86"), ":duck:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x89"), ":owl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb8"), ":frog:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x8a"), ":crocodile:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa2"), ":turtle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8e"), ":lizard:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x8d"), ":snake:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb2"), ":dragon_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x89"), ":dragon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xb3"), ":whale:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x8b"), ":whale2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xac"), ":dolphin:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x9f"), ":fish:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa0"), ":tropical_fish:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\xa1"), ":blowfish:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x88"), ":shark:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x99"), ":octopus:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x9a"), ":shell:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x80"), ":crab:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x90"), ":shrimp:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x91"), ":squid:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8b"), ":butterfly:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x8c"), ":snail:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x9b"), ":bug:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x9c"), ":ant:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x9d"), ":bee:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x90\x9e"), ":beetle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xb7"), ":spider:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xb8"), ":spider_web:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa6\x82"), ":scorpion:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x90"), ":bouquet:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb8"), ":cherry_blossom:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb5"), ":rosette:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb9"), ":rose:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x80"), ":wilted_rose:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xba"), ":hibiscus:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbb"), ":sunflower:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbc"), ":blossom:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb7"), ":tulip:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb1"), ":seedling:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb2"), ":evergreen_tree:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb3"), ":deciduous_tree:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb4"), ":palm_tree:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb5"), ":cactus:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbe"), ":ear_of_rice:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbf"), ":herb:"}, - Emoji{QString::fromUtf8("\xe2\x98\x98"), ":shamrock:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x80"), ":four_leaf_clover:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x81"), ":maple_leaf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x82"), ":fallen_leaf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x83"), ":leaves:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x84"), ":mushroom:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb0"), ":chestnut:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8d"), ":earth_africa:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8e"), ":earth_americas:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8f"), ":earth_asia:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x91"), ":new_moon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x92"), ":waxing_crescent_moon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x93"), ":first_quarter_moon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x94"), ":waxing_gibbous_moon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x95"), ":full_moon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x96"), ":waning_gibbous_moon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x97"), ":last_quarter_moon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x98"), ":waning_crescent_moon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x99"), ":crescent_moon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9a"), ":new_moon_with_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9b"), ":first_quarter_moon_with_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9c"), ":last_quarter_moon_with_face:"}, - Emoji{QString::fromUtf8("\xe2\x98\x80"), ":sunny:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9d"), ":full_moon_with_face:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9e"), ":sun_with_face:"}, - Emoji{QString::fromUtf8("\xe2\xad\x90"), ":star:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9f"), ":star2:"}, - Emoji{QString::fromUtf8("\xe2\x98\x81"), ":cloud:"}, - Emoji{QString::fromUtf8("\xe2\x9b\x85"), ":partly_sunny:"}, - Emoji{QString::fromUtf8("\xe2\x9b\x88"), ":thunder_cloud_rain:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa4"), ":white_sun_small_cloud:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa5"), ":white_sun_cloud:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa6"), ":white_sun_rain_cloud:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa7"), ":cloud_rain:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa8"), ":cloud_snow:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa9"), ":cloud_lightning:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xaa"), ":cloud_tornado:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xab"), ":fog:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xac"), ":wind_blowing_face:"}, - Emoji{QString::fromUtf8("\xe2\x98\x82"), ":umbrella2:"}, - Emoji{QString::fromUtf8("\xe2\x98\x94"), ":umbrella:"}, - Emoji{QString::fromUtf8("\xe2\x9a\xa1"), ":zap:"}, - Emoji{QString::fromUtf8("\xe2\x9d\x84"), ":snowflake:"}, - Emoji{QString::fromUtf8("\xe2\x98\x83"), ":snowman2:"}, - Emoji{QString::fromUtf8("\xe2\x9b\x84"), ":snowman:"}, - Emoji{QString::fromUtf8("\xe2\x98\x84"), ":comet:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa5"), ":fire:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa7"), ":droplet:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8a"), ":ocean:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x83"), ":jack_o_lantern:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x84"), ":christmas_tree:"}, - Emoji{QString::fromUtf8("\xe2\x9c\xa8"), ":sparkles:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8b"), ":tanabata_tree:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8d"), ":bamboo:"}, +const std::vector<Emoji> emoji::Provider::nature = { + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb5"), "monkey face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x92"), "monkey"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8d"), "gorilla"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa7"), "orangutan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb6"), "dog face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x95"), "dog"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xae"), "guide dog"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x95\xe2\x80\x8d\xf0\x9f\xa6\xba"), "service dog"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa9"), "poodle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xba"), "wolf"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8a"), "fox"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x9d"), "raccoon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb1"), "cat face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x88"), "cat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x88\xe2\x80\x8d\xe2\xac\x9b"), "black cat"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x81"), "lion"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xaf"), "tiger face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x85"), "tiger"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x86"), "leopard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb4"), "horse face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x8e"), "horse"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x84"), "unicorn"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x93"), "zebra"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8c"), "deer"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xac"), "bison"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xae"), "cow face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x82"), "ox"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x83"), "water buffalo"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x84"), "cow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb7"), "pig face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x96"), "pig"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x97"), "boar"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xbd"), "pig nose"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x8f"), "ram"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x91"), "ewe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x90"), "goat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xaa"), "camel"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xab"), "two-hump camel"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x99"), "llama"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x92"), "giraffe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x98"), "elephant"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa3"), "mammoth"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8f"), "rhinoceros"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x9b"), "hippopotamus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xad"), "mouse face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x81"), "mouse"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x80"), "rat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb9"), "hamster"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb0"), "rabbit face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x87"), "rabbit"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xbf"), "chipmunk"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xab"), "beaver"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x94"), "hedgehog"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x87"), "bat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xbb"), "bear"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xbb\xe2\x80\x8d\xe2\x9d\x84"), "polar bear"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa8"), "koala"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xbc"), "panda"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa5"), "sloth"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa6"), "otter"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa8"), "skunk"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x98"), "kangaroo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa1"), "badger"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xbe"), "paw prints"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x83"), "turkey"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x94"), "chicken"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x93"), "rooster"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa3"), "hatching chick"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa4"), "baby chick"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa5"), "front-facing baby chick"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa6"), "bird"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa7"), "penguin"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x8a"), "dove"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x85"), "eagle"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x86"), "duck"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa2"), "swan"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x89"), "owl"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa4"), "dodo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xb6"), "feather"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa9"), "flamingo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x9a"), "peacock"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x9c"), "parrot"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb8"), "frog"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x8a"), "crocodile"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa2"), "turtle"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8e"), "lizard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x8d"), "snake"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb2"), "dragon face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x89"), "dragon"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x95"), "sauropod"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x96"), "T-Rex"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xb3"), "spouting whale"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x8b"), "whale"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xac"), "dolphin"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xad"), "seal"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x9f"), "fish"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa0"), "tropical fish"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\xa1"), "blowfish"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x88"), "shark"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x99"), "octopus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x9a"), "spiral shell"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x8c"), "snail"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8b"), "butterfly"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x9b"), "bug"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x9c"), "ant"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x9d"), "honeybee"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xb2"), "beetle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x90\x9e"), "lady beetle"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x97"), "cricket"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xb3"), "cockroach"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb7"), "spider"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb8"), "spider web"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x82"), "scorpion"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x9f"), "mosquito"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xb0"), "fly"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xb1"), "worm"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xa0"), "microbe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x90"), "bouquet"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb8"), "cherry blossom"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xae"), "white flower"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb5"), "rosette"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb9"), "rose"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x80"), "wilted flower"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xba"), "hibiscus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbb"), "sunflower"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbc"), "blossom"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb7"), "tulip"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb1"), "seedling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xb4"), "potted plant"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb2"), "evergreen tree"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb3"), "deciduous tree"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb4"), "palm tree"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb5"), "cactus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbe"), "sheaf of rice"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbf"), "herb"}, + Emoji{QString::fromUtf8("\xe2\x98\x98"), "shamrock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x80"), "four leaf clover"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x81"), "maple leaf"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x82"), "fallen leaf"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x83"), "leaf fluttering in wind"}, }; -const std::vector<Emoji> Provider::food = { - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x87"), ":grapes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x88"), ":melon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x89"), ":watermelon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8a"), ":tangerine:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8b"), ":lemon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8c"), ":banana:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8d"), ":pineapple:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8e"), ":apple:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8f"), ":green_apple:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x90"), ":pear:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x91"), ":peach:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x92"), ":cherries:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x93"), ":strawberry:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9d"), ":kiwi:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x85"), ":tomato:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x91"), ":avocado:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x86"), ":eggplant:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x94"), ":potato:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x95"), ":carrot:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbd"), ":corn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb6"), ":hot_pepper:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x92"), ":cucumber:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9c"), ":peanuts:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9e"), ":bread:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x90"), ":croissant:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x96"), ":french_bread:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9e"), ":pancakes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa7\x80"), ":cheese:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x96"), ":meat_on_bone:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x97"), ":poultry_leg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x93"), ":bacon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x94"), ":hamburger:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9f"), ":fries:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x95"), ":pizza:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xad"), ":hotdog:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xae"), ":taco:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xaf"), ":burrito:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x99"), ":stuffed_flatbread:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9a"), ":egg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb3"), ":cooking:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x98"), ":shallow_pan_of_food:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb2"), ":stew:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x97"), ":salad:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbf"), ":popcorn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb1"), ":bento:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x98"), ":rice_cracker:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x99"), ":rice_ball:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9a"), ":rice:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9b"), ":curry:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9c"), ":ramen:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9d"), ":spaghetti:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa0"), ":sweet_potato:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa2"), ":oden:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa3"), ":sushi:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa4"), ":fried_shrimp:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa5"), ":fish_cake:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa1"), ":dango:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa6"), ":icecream:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa7"), ":shaved_ice:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa8"), ":ice_cream:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa9"), ":doughnut:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xaa"), ":cookie:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x82"), ":birthday:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb0"), ":cake:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xab"), ":chocolate_bar:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xac"), ":candy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xad"), ":lollipop:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xae"), ":custard:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xaf"), ":honey_pot:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbc"), ":baby_bottle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9b"), ":milk:"}, - Emoji{QString::fromUtf8("\xe2\x98\x95"), ":coffee:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb5"), ":tea:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb6"), ":sake:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbe"), ":champagne:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb7"), ":wine_glass:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb8"), ":cocktail:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb9"), ":tropical_drink:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xba"), ":beer:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbb"), ":beers:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x82"), ":champagne_glass:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x83"), ":tumbler_glass:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbd"), ":fork_knife_plate:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb4"), ":fork_and_knife:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x84"), ":spoon:"}, +const std::vector<Emoji> emoji::Provider::food = { + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x87"), "grapes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x88"), "melon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x89"), "watermelon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8a"), "tangerine"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8b"), "lemon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8c"), "banana"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8d"), "pineapple"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xad"), "mango"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8e"), "red apple"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8f"), "green apple"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x90"), "pear"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x91"), "peach"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x92"), "cherries"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x93"), "strawberry"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x90"), "blueberries"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9d"), "kiwi fruit"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x85"), "tomato"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x92"), "olive"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa5"), "coconut"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x91"), "avocado"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x86"), "eggplant"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x94"), "potato"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x95"), "carrot"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbd"), "ear of corn"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb6"), "hot pepper"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x91"), "bell pepper"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x92"), "cucumber"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xac"), "leafy green"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa6"), "broccoli"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x84"), "garlic"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x85"), "onion"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x84"), "mushroom"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9c"), "peanuts"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb0"), "chestnut"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9e"), "bread"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x90"), "croissant"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x96"), "baguette bread"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x93"), "flatbread"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa8"), "pretzel"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xaf"), "bagel"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9e"), "pancakes"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x87"), "waffle"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x80"), "cheese wedge"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x96"), "meat on bone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x97"), "poultry leg"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa9"), "cut of meat"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x93"), "bacon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x94"), "hamburger"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9f"), "french fries"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x95"), "pizza"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xad"), "hot dog"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xaa"), "sandwich"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xae"), "taco"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xaf"), "burrito"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x94"), "tamale"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x99"), "stuffed flatbread"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x86"), "falafel"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9a"), "egg"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb3"), "cooking"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x98"), "shallow pan of food"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb2"), "pot of food"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x95"), "fondue"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa3"), "bowl with spoon"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x97"), "green salad"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbf"), "popcorn"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x88"), "butter"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x82"), "salt"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xab"), "canned food"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb1"), "bento box"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x98"), "rice cracker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x99"), "rice ball"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9a"), "cooked rice"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9b"), "curry rice"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9c"), "steaming bowl"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9d"), "spaghetti"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa0"), "roasted sweet potato"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa2"), "oden"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa3"), "sushi"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa4"), "fried shrimp"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa5"), "fish cake with swirl"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xae"), "moon cake"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa1"), "dango"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9f"), "dumpling"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa0"), "fortune cookie"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa1"), "takeout box"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x80"), "crab"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x9e"), "lobster"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x90"), "shrimp"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\x91"), "squid"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xaa"), "oyster"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa6"), "soft ice cream"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa7"), "shaved ice"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa8"), "ice cream"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa9"), "doughnut"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xaa"), "cookie"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x82"), "birthday cake"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb0"), "shortcake"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x81"), "cupcake"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa7"), "pie"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xab"), "chocolate bar"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xac"), "candy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xad"), "lollipop"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xae"), "custard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xaf"), "honey pot"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbc"), "baby bottle"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9b"), "glass of milk"}, + Emoji{QString::fromUtf8("\xe2\x98\x95"), "hot beverage"}, + Emoji{QString::fromUtf8("\xf0\x9f\xab\x96"), "teapot"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb5"), "teacup without handle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb6"), "sake"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbe"), "bottle with popping cork"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb7"), "wine glass"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb8"), "cocktail glass"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb9"), "tropical drink"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xba"), "beer mug"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbb"), "clinking beer mugs"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x82"), "clinking glasses"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x83"), "tumbler glass"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa4"), "cup with straw"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8b"), "bubble tea"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x83"), "beverage box"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x89"), "mate"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\x8a"), "ice"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xa2"), "chopsticks"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbd"), "fork and knife with plate"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb4"), "fork and knife"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x84"), "spoon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xaa"), "kitchen knife"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xba"), "amphora"}, }; -const std::vector<Emoji> Provider::activity = { - Emoji{QString::fromUtf8("\xf0\x9f\x91\xbe"), ":space_invader:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xb4"), ":levitate:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xba"), ":fencer:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87"), ":horse_racing:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xb7"), ":skier:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x82"), ":snowboarder:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c"), ":golfer:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84"), ":surfer:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3"), ":rowboat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a"), ":swimmer:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xb9"), ":basketball_player:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b"), ":lifter:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4"), ":bicyclist:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5"), ":mountain_bicyclist:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8"), ":cartwheel:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbc"), ":wrestlers:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd"), ":water_polo:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe"), ":handball:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9"), ":juggling:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xaa"), ":circus_tent:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xad"), ":performing_arts:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa8"), ":art:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb0"), ":slot_machine:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x80"), ":bath:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x97"), ":reminder_ribbon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9f"), ":tickets:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xab"), ":ticket:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x96"), ":military_medal:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x86"), ":trophy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x85"), ":medal:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x87"), ":first_place:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x88"), ":second_place:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x89"), ":third_place:"}, - Emoji{QString::fromUtf8("\xe2\x9a\xbd"), ":soccer:"}, - Emoji{QString::fromUtf8("\xe2\x9a\xbe"), ":baseball:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x80"), ":basketball:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x90"), ":volleyball:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x88"), ":football:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x89"), ":rugby_football:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbe"), ":tennis:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb1"), ":8ball:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb3"), ":bowling:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8f"), ":cricket:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x91"), ":field_hockey:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x92"), ":hockey:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x93"), ":ping_pong:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb8"), ":badminton:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x8a"), ":boxing_glove:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x8b"), ":martial_arts_uniform:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x85"), ":goal:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xaf"), ":dart:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xb3"), ":golf:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xb8"), ":ice_skate:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa3"), ":fishing_pole_and_fish:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbd"), ":running_shirt_with_sash:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbf"), ":ski:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xae"), ":video_game:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb2"), ":game_die:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbc"), ":musical_score:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa4"), ":microphone:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa7"), ":headphones:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb7"), ":saxophone:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb8"), ":guitar:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb9"), ":musical_keyboard:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xba"), ":trumpet:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbb"), ":violin:"}, - Emoji{QString::fromUtf8("\xf0\x9f\xa5\x81"), ":drum:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xac"), ":clapper:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb9"), ":bow_and_arrow:"}, +const std::vector<Emoji> emoji::Provider::activity = { + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x83"), "jack-o-lantern"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x84"), "Christmas tree"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x86"), "fireworks"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x87"), "sparkler"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa8"), "firecracker"}, + Emoji{QString::fromUtf8("\xe2\x9c\xa8"), "sparkles"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x88"), "balloon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x89"), "party popper"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8a"), "confetti ball"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8b"), "tanabata tree"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8d"), "pine decoration"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8e"), "Japanese dolls"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8f"), "carp streamer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x90"), "wind chime"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x91"), "moon viewing ceremony"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa7"), "red envelope"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x80"), "ribbon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x81"), "wrapped gift"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x97"), "reminder ribbon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9f"), "admission tickets"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xab"), "ticket"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x96"), "military medal"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x86"), "trophy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x85"), "sports medal"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x87"), "1st place medal"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x88"), "2nd place medal"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x89"), "3rd place medal"}, + Emoji{QString::fromUtf8("\xe2\x9a\xbd"), "soccer ball"}, + Emoji{QString::fromUtf8("\xe2\x9a\xbe"), "baseball"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x8e"), "softball"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x80"), "basketball"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x90"), "volleyball"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x88"), "american football"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x89"), "rugby football"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbe"), "tennis"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x8f"), "flying disc"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb3"), "bowling"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8f"), "cricket game"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x91"), "field hockey"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x92"), "ice hockey"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x8d"), "lacrosse"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x93"), "ping pong"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb8"), "badminton"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x8a"), "boxing glove"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x8b"), "martial arts uniform"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x85"), "goal net"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb3"), "flag in hole"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb8"), "ice skate"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa3"), "fishing pole"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbf"), "diving mask"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbd"), "running shirt"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbf"), "skis"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb7"), "sled"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x8c"), "curling stone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xaf"), "direct hit"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x80"), "yo-yo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x81"), "kite"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb1"), "pool 8 ball"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xae"), "crystal ball"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x84"), "magic wand"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xbf"), "nazar amulet"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xae"), "video game"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb9"), "joystick"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb0"), "slot machine"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb2"), "game die"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa9"), "puzzle piece"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb8"), "teddy bear"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x85"), "piñata"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x86"), "nesting dolls"}, + Emoji{QString::fromUtf8("\xe2\x99\xa0"), "spade suit"}, + Emoji{QString::fromUtf8("\xe2\x99\xa5"), "heart suit"}, + Emoji{QString::fromUtf8("\xe2\x99\xa6"), "diamond suit"}, + Emoji{QString::fromUtf8("\xe2\x99\xa3"), "club suit"}, + Emoji{QString::fromUtf8("\xe2\x99\x9f"), "chess pawn"}, + Emoji{QString::fromUtf8("\xf0\x9f\x83\x8f"), "joker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x80\x84"), "mahjong red dragon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb4"), "flower playing cards"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xad"), "performing arts"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\xbc"), "framed picture"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa8"), "artist palette"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb5"), "thread"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa1"), "sewing needle"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb6"), "yarn"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa2"), "knot"}, }; -const std::vector<Emoji> Provider::travel = { - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8e"), ":race_car:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8d"), ":motorcycle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xbe"), ":japan:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x94"), ":mountain_snow:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xb0"), ":mountain:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8b"), ":volcano:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xbb"), ":mount_fuji:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x95"), ":camping:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x96"), ":beach:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9c"), ":desert:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9d"), ":island:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9e"), ":park:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9f"), ":stadium:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9b"), ":classical_building:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x97"), ":construction_site:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x98"), ":homes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x99"), ":cityscape:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9a"), ":house_abandoned:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa0"), ":house:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa1"), ":house_with_garden:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa2"), ":office:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa3"), ":post_office:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa4"), ":european_post_office:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa5"), ":hospital:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa6"), ":bank:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa8"), ":hotel:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa9"), ":love_hotel:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xaa"), ":convenience_store:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xab"), ":school:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xac"), ":department_store:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xad"), ":factory:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xaf"), ":japanese_castle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb0"), ":european_castle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x92"), ":wedding:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xbc"), ":tokyo_tower:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xbd"), ":statue_of_liberty:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xaa"), ":church:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x8c"), ":mosque:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x8d"), ":synagogue:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xa9"), ":shinto_shrine:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x8b"), ":kaaba:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xb2"), ":fountain:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xba"), ":tent:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x81"), ":foggy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x83"), ":night_with_stars:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x84"), ":sunrise_over_mountains:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x85"), ":sunrise:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x86"), ":city_dusk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x87"), ":city_sunset:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x89"), ":bridge_at_night:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8c"), ":milky_way:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa0"), ":carousel_horse:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa1"), ":ferris_wheel:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa2"), ":roller_coaster:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x82"), ":steam_locomotive:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x83"), ":railway_car:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x84"), ":bullettrain_side:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x85"), ":bullettrain_front:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x86"), ":train2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x87"), ":metro:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x88"), ":light_rail:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x89"), ":station:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8a"), ":tram:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9d"), ":monorail:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9e"), ":mountain_railway:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8b"), ":train:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8c"), ":bus:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8d"), ":oncoming_bus:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8e"), ":trolleybus:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x90"), ":minibus:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x91"), ":ambulance:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x92"), ":fire_engine:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x93"), ":police_car:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x94"), ":oncoming_police_car:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x95"), ":taxi:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x96"), ":oncoming_taxi:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x97"), ":red_car:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x98"), ":oncoming_automobile:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x99"), ":blue_car:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9a"), ":truck:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9b"), ":articulated_lorry:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9c"), ":tractor:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb2"), ":bike:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb4"), ":scooter:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb5"), ":motor_scooter:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8f"), ":busstop:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa3"), ":motorway:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa4"), ":railway_track:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xbd"), ":fuelpump:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa8"), ":rotating_light:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa5"), ":traffic_light:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa6"), ":vertical_traffic_light:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa7"), ":construction:"}, - Emoji{QString::fromUtf8("\xe2\x9a\x93"), ":anchor:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xb5"), ":sailboat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb6"), ":canoe:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa4"), ":speedboat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb3"), ":cruise_ship:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xb4"), ":ferry:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa5"), ":motorboat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa2"), ":ship:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x88"), ":airplane:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa9"), ":airplane_small:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xab"), ":airplane_departure:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xac"), ":airplane_arriving:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xba"), ":seat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x81"), ":helicopter:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9f"), ":suspension_railway:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa0"), ":mountain_cableway:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa1"), ":aerial_tramway:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\x80"), ":rocket:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb0"), ":satellite_orbital:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa0"), ":stars:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x88"), ":rainbow:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x86"), ":fireworks:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x87"), ":sparkler:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x91"), ":rice_scene:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\x81"), ":checkered_flag:"}, +const std::vector<Emoji> emoji::Provider::travel = { + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8d"), "globe showing Europe-Africa"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8e"), "globe showing Americas"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8f"), "globe showing Asia-Australia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x90"), "globe with meridians"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xba"), "world map"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xbe"), "map of Japan"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xad"), "compass"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x94"), "snow-capped mountain"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb0"), "mountain"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8b"), "volcano"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xbb"), "mount fuji"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x95"), "camping"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x96"), "beach with umbrella"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9c"), "desert"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9d"), "desert island"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9e"), "national park"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9f"), "stadium"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9b"), "classical building"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x97"), "building construction"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb1"), "brick"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa8"), "rock"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xb5"), "wood"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x96"), "hut"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x98"), "houses"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9a"), "derelict house"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa0"), "house"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa1"), "house with garden"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa2"), "office building"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa3"), "Japanese post office"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa4"), "post office"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa5"), "hospital"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa6"), "bank"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa8"), "hotel"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa9"), "love hotel"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xaa"), "convenience store"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xab"), "school"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xac"), "department store"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xad"), "factory"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xaf"), "Japanese castle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb0"), "castle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x92"), "wedding"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xbc"), "Tokyo tower"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xbd"), "Statue of Liberty"}, + Emoji{QString::fromUtf8("\xe2\x9b\xaa"), "church"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x8c"), "mosque"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x95"), "hindu temple"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x8d"), "synagogue"}, + Emoji{QString::fromUtf8("\xe2\x9b\xa9"), "shinto shrine"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x8b"), "kaaba"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb2"), "fountain"}, + Emoji{QString::fromUtf8("\xe2\x9b\xba"), "tent"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x81"), "foggy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x83"), "night with stars"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x99"), "cityscape"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x84"), "sunrise over mountains"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x85"), "sunrise"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x86"), "cityscape at dusk"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x87"), "sunset"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x89"), "bridge at night"}, + Emoji{QString::fromUtf8("\xe2\x99\xa8"), "hot springs"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa0"), "carousel horse"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa1"), "ferris wheel"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa2"), "roller coaster"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x88"), "barber pole"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xaa"), "circus tent"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x82"), "locomotive"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x83"), "railway car"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x84"), "high-speed train"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x85"), "bullet train"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x86"), "train"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x87"), "metro"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x88"), "light rail"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x89"), "station"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8a"), "tram"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9d"), "monorail"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9e"), "mountain railway"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8b"), "tram car"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8c"), "bus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8d"), "oncoming bus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8e"), "trolleybus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x90"), "minibus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x91"), "ambulance"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x92"), "fire engine"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x93"), "police car"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x94"), "oncoming police car"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x95"), "taxi"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x96"), "oncoming taxi"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x97"), "automobile"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x98"), "oncoming automobile"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x99"), "sport utility vehicle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xbb"), "pickup truck"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9a"), "delivery truck"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9b"), "articulated lorry"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9c"), "tractor"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8e"), "racing car"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8d"), "motorcycle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb5"), "motor scooter"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbd"), "manual wheelchair"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xbc"), "motorized wheelchair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xba"), "auto rickshaw"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb2"), "bicycle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb4"), "kick scooter"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb9"), "skateboard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xbc"), "roller skate"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8f"), "bus stop"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa3"), "motorway"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa4"), "railway track"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa2"), "oil drum"}, + Emoji{QString::fromUtf8("\xe2\x9b\xbd"), "fuel pump"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa8"), "police car light"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa5"), "horizontal traffic light"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa6"), "vertical traffic light"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x91"), "stop sign"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa7"), "construction"}, + Emoji{QString::fromUtf8("\xe2\x9a\x93"), "anchor"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb5"), "sailboat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb6"), "canoe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa4"), "speedboat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb3"), "passenger ship"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb4"), "ferry"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa5"), "motor boat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa2"), "ship"}, + Emoji{QString::fromUtf8("\xe2\x9c\x88"), "airplane"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa9"), "small airplane"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xab"), "airplane departure"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xac"), "airplane arrival"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x82"), "parachute"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xba"), "seat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x81"), "helicopter"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9f"), "suspension railway"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa0"), "mountain cableway"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa1"), "aerial tramway"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb0"), "satellite"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\x80"), "rocket"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb8"), "flying saucer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8e"), "bellhop bell"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb3"), "luggage"}, + Emoji{QString::fromUtf8("\xe2\x8c\x9b"), "hourglass done"}, + Emoji{QString::fromUtf8("\xe2\x8f\xb3"), "hourglass not done"}, + Emoji{QString::fromUtf8("\xe2\x8c\x9a"), "watch"}, + Emoji{QString::fromUtf8("\xe2\x8f\xb0"), "alarm clock"}, + Emoji{QString::fromUtf8("\xe2\x8f\xb1"), "stopwatch"}, + Emoji{QString::fromUtf8("\xe2\x8f\xb2"), "timer clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb0"), "mantelpiece clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x9b"), "twelve o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xa7"), "twelve-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x90"), "one o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x9c"), "one-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x91"), "two o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x9d"), "two-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x92"), "three o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x9e"), "three-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x93"), "four o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x9f"), "four-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x94"), "five o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xa0"), "five-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x95"), "six o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xa1"), "six-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x96"), "seven o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xa2"), "seven-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x97"), "eight o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xa3"), "eight-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x98"), "nine o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xa4"), "nine-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x99"), "ten o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xa5"), "ten-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x9a"), "eleven o’clock"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xa6"), "eleven-thirty"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x91"), "new moon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x92"), "waxing crescent moon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x93"), "first quarter moon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x94"), "waxing gibbous moon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x95"), "full moon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x96"), "waning gibbous moon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x97"), "last quarter moon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x98"), "waning crescent moon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x99"), "crescent moon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9a"), "new moon face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9b"), "first quarter moon face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9c"), "last quarter moon face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa1"), "thermometer"}, + Emoji{QString::fromUtf8("\xe2\x98\x80"), "sun"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9d"), "full moon face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9e"), "sun with face"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x90"), "ringed planet"}, + Emoji{QString::fromUtf8("\xe2\xad\x90"), "star"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9f"), "glowing star"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa0"), "shooting star"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8c"), "milky way"}, + Emoji{QString::fromUtf8("\xe2\x98\x81"), "cloud"}, + Emoji{QString::fromUtf8("\xe2\x9b\x85"), "sun behind cloud"}, + Emoji{QString::fromUtf8("\xe2\x9b\x88"), "cloud with lightning and rain"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa4"), "sun behind small cloud"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa5"), "sun behind large cloud"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa6"), "sun behind rain cloud"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa7"), "cloud with rain"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa8"), "cloud with snow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa9"), "cloud with lightning"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xaa"), "tornado"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xab"), "fog"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\xac"), "wind face"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x80"), "cyclone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x88"), "rainbow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x82"), "closed umbrella"}, + Emoji{QString::fromUtf8("\xe2\x98\x82"), "umbrella"}, + Emoji{QString::fromUtf8("\xe2\x98\x94"), "umbrella with rain drops"}, + Emoji{QString::fromUtf8("\xe2\x9b\xb1"), "umbrella on ground"}, + Emoji{QString::fromUtf8("\xe2\x9a\xa1"), "high voltage"}, + Emoji{QString::fromUtf8("\xe2\x9d\x84"), "snowflake"}, + Emoji{QString::fromUtf8("\xe2\x98\x83"), "snowman"}, + Emoji{QString::fromUtf8("\xe2\x9b\x84"), "snowman without snow"}, + Emoji{QString::fromUtf8("\xe2\x98\x84"), "comet"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa5"), "fire"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa7"), "droplet"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8a"), "water wave"}, }; -const std::vector<Emoji> Provider::objects = { - Emoji{QString::fromUtf8("\xe2\x98\xa0"), ":skull_crossbones:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x8c"), ":love_letter:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa3"), ":bomb:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xb3"), ":hole:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8d"), ":shopping_bags:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xbf"), ":prayer_beads:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x8e"), ":gem:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xaa"), ":knife:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xba"), ":amphora:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xba"), ":map:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x88"), ":barber:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\xbc"), ":frame_photo:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8e"), ":bellhop:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xaa"), ":door:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8c"), ":sleeping_accommodation:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8f"), ":bed:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8b"), ":couch:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbd"), ":toilet:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbf"), ":shower:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x81"), ":bathtub:"}, - Emoji{QString::fromUtf8("\xe2\x8c\x9b"), ":hourglass:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xb3"), ":hourglass_flowing_sand:"}, - Emoji{QString::fromUtf8("\xe2\x8c\x9a"), ":watch:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xb0"), ":alarm_clock:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xb1"), ":stopwatch:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xb2"), ":timer:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xb0"), ":clock:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa1"), ":thermometer:"}, - Emoji{QString::fromUtf8("\xe2\x9b\xb1"), ":beach_umbrella:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x88"), ":balloon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x89"), ":tada:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8a"), ":confetti_ball:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8e"), ":dolls:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8f"), ":flags:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x90"), ":wind_chime:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x80"), ":ribbon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x81"), ":gift:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xb9"), ":joystick:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xaf"), ":postal_horn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x99"), ":microphone2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9a"), ":level_slider:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9b"), ":control_knobs:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xbb"), ":radio:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb1"), ":iphone:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb2"), ":calling:"}, - Emoji{QString::fromUtf8("\xe2\x98\x8e"), ":telephone:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x9e"), ":telephone_receiver:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x9f"), ":pager:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa0"), ":fax:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x8b"), ":battery:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x8c"), ":electric_plug:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xbb"), ":computer:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\xa5"), ":desktop:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\xa8"), ":printer:"}, - Emoji{QString::fromUtf8("\xe2\x8c\xa8"), ":keyboard:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\xb1"), ":mouse_three_button:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\xb2"), ":trackball:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xbd"), ":minidisc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xbe"), ":floppy_disk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xbf"), ":cd:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x80"), ":dvd:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa5"), ":movie_camera:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9e"), ":film_frames:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xbd"), ":projector:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xba"), ":tv:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb7"), ":camera:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb8"), ":camera_with_flash:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb9"), ":video_camera:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xbc"), ":vhs:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x8d"), ":mag:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x8e"), ":mag_right:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xac"), ":microscope:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xad"), ":telescope:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa1"), ":satellite:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xaf"), ":candle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa1"), ":bulb:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa6"), ":flashlight:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xae"), ":izakaya_lantern:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x94"), ":notebook_with_decorative_cover:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x95"), ":closed_book:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x96"), ":book:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x97"), ":green_book:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x98"), ":blue_book:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x99"), ":orange_book:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x9a"), ":books:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x93"), ":notebook:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x92"), ":ledger:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x83"), ":page_with_curl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x9c"), ":scroll:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x84"), ":page_facing_up:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb0"), ":newspaper:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\x9e"), ":newspaper2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x91"), ":bookmark_tabs:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x96"), ":bookmark:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb7"), ":label:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb0"), ":moneybag:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb4"), ":yen:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb5"), ":dollar:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb6"), ":euro:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb7"), ":pound:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb8"), ":money_with_wings:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb3"), ":credit_card:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x89"), ":envelope:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa7"), ":e-mail:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa8"), ":incoming_envelope:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa9"), ":envelope_with_arrow:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa4"), ":outbox_tray:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa5"), ":inbox_tray:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa6"), ":package:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xab"), ":mailbox:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xaa"), ":mailbox_closed:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xac"), ":mailbox_with_mail:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xad"), ":mailbox_with_no_mail:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xae"), ":postbox:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xb3"), ":ballot_box:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x8f"), ":pencil2:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x92"), ":black_nib:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\x8b"), ":pen_fountain:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\x8a"), ":pen_ballpoint:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\x8c"), ":paintbrush:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\x8d"), ":crayon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x9d"), ":pencil:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x81"), ":file_folder:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x82"), ":open_file_folder:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\x82"), ":dividers:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x85"), ":date:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x86"), ":calendar:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\x92"), ":notepad_spiral:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\x93"), ":calendar_spiral:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x87"), ":card_index:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x88"), ":chart_with_upwards_trend:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x89"), ":chart_with_downwards_trend:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x8a"), ":bar_chart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x8b"), ":clipboard:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x8c"), ":pushpin:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x8d"), ":round_pushpin:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x8e"), ":paperclip:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\x87"), ":paperclips:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x8f"), ":straight_ruler:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x90"), ":triangular_ruler:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x82"), ":scissors:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\x83"), ":card_box:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\x84"), ":file_cabinet:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\x91"), ":wastebasket:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x92"), ":lock:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x93"), ":unlock:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x8f"), ":lock_with_ink_pen:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x90"), ":closed_lock_with_key:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x91"), ":key:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\x9d"), ":key2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa8"), ":hammer:"}, - Emoji{QString::fromUtf8("\xe2\x9b\x8f"), ":pick:"}, - Emoji{QString::fromUtf8("\xe2\x9a\x92"), ":hammer_pick:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa0"), ":tools:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xa1"), ":dagger:"}, - Emoji{QString::fromUtf8("\xe2\x9a\x94"), ":crossed_swords:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xab"), ":gun:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa1"), ":shield:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa7"), ":wrench:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa9"), ":nut_and_bolt:"}, - Emoji{QString::fromUtf8("\xe2\x9a\x99"), ":gear:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\x9c"), ":compression:"}, - Emoji{QString::fromUtf8("\xe2\x9a\x97"), ":alembic:"}, - Emoji{QString::fromUtf8("\xe2\x9a\x96"), ":scales:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x97"), ":link:"}, - Emoji{QString::fromUtf8("\xe2\x9b\x93"), ":chains:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x89"), ":syringe:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x8a"), ":pill:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xac"), ":smoking:"}, - Emoji{QString::fromUtf8("\xe2\x9a\xb0"), ":coffin:"}, - Emoji{QString::fromUtf8("\xe2\x9a\xb1"), ":urn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xbf"), ":moyai:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa2"), ":oil:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xae"), ":crystal_ball:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x92"), ":shopping_cart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa9"), ":triangular_flag_on_post:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8c"), ":crossed_flags:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb4"), ":flag_black:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb3"), ":flag_white:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb3\xf0\x9f\x8c\x88"), ":rainbow_flag:"}, +const std::vector<Emoji> emoji::Provider::objects = { + Emoji{QString::fromUtf8("\xf0\x9f\x91\x93"), "glasses"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xb6"), "sunglasses"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xbd"), "goggles"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xbc"), "lab coat"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xba"), "safety vest"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x94"), "necktie"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x95"), "t-shirt"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x96"), "jeans"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa3"), "scarf"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa4"), "gloves"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa5"), "coat"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa6"), "socks"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x97"), "dress"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x98"), "kimono"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xbb"), "sari"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb1"), "one-piece swimsuit"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb2"), "briefs"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb3"), "shorts"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x99"), "bikini"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x9a"), "woman’s clothes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x9b"), "purse"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x9c"), "handbag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x9d"), "clutch bag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8d"), "shopping bags"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x92"), "backpack"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb4"), "thong sandal"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x9e"), "man’s shoe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x9f"), "running shoe"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xbe"), "hiking boot"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\xbf"), "flat shoe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa0"), "high-heeled shoe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa1"), "woman’s sandal"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb0"), "ballet shoes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\xa2"), "woman’s boot"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x91"), "crown"}, + Emoji{QString::fromUtf8("\xf0\x9f\x91\x92"), "woman’s hat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa9"), "top hat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x93"), "graduation cap"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xa2"), "billed cap"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x96"), "military helmet"}, + Emoji{QString::fromUtf8("\xe2\x9b\x91"), "rescue worker’s helmet"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xbf"), "prayer beads"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x84"), "lipstick"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x8d"), "ring"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x8e"), "gem stone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x87"), "muted speaker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x88"), "speaker low volume"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x89"), "speaker medium volume"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x8a"), "speaker high volume"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa2"), "loudspeaker"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa3"), "megaphone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xaf"), "postal horn"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x94"), "bell"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x95"), "bell with slash"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbc"), "musical score"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb5"), "musical note"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb6"), "musical notes"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x99"), "studio microphone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9a"), "level slider"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9b"), "control knobs"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa4"), "microphone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa7"), "headphone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xbb"), "radio"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb7"), "saxophone"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x97"), "accordion"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb8"), "guitar"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb9"), "musical keyboard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xba"), "trumpet"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbb"), "violin"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x95"), "banjo"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa5\x81"), "drum"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x98"), "long drum"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb1"), "mobile phone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb2"), "mobile phone with arrow"}, + Emoji{QString::fromUtf8("\xe2\x98\x8e"), "telephone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x9e"), "telephone receiver"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x9f"), "pager"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa0"), "fax machine"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x8b"), "battery"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x8c"), "electric plug"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xbb"), "laptop"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\xa5"), "desktop computer"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\xa8"), "printer"}, + Emoji{QString::fromUtf8("\xe2\x8c\xa8"), "keyboard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\xb1"), "computer mouse"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\xb2"), "trackball"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xbd"), "computer disk"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xbe"), "floppy disk"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xbf"), "optical disk"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x80"), "dvd"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xae"), "abacus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa5"), "movie camera"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9e"), "film frames"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xbd"), "film projector"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xac"), "clapper board"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xba"), "television"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb7"), "camera"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb8"), "camera with flash"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb9"), "video camera"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xbc"), "videocassette"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x8d"), "magnifying glass tilted left"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x8e"), "magnifying glass tilted right"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\xaf"), "candle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa1"), "light bulb"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa6"), "flashlight"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xae"), "red paper lantern"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x94"), "diya lamp"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x94"), "notebook with decorative cover"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x95"), "closed book"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x96"), "open book"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x97"), "green book"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x98"), "blue book"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x99"), "orange book"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x9a"), "books"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x93"), "notebook"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x92"), "ledger"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x83"), "page with curl"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x9c"), "scroll"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x84"), "page facing up"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb0"), "newspaper"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\x9e"), "rolled-up newspaper"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x91"), "bookmark tabs"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x96"), "bookmark"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb7"), "label"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb0"), "money bag"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x99"), "coin"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb4"), "yen banknote"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb5"), "dollar banknote"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb6"), "euro banknote"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb7"), "pound banknote"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb8"), "money with wings"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb3"), "credit card"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xbe"), "receipt"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb9"), "chart increasing with yen"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb1"), "currency exchange"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xb2"), "heavy dollar sign"}, + Emoji{QString::fromUtf8("\xe2\x9c\x89"), "envelope"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa7"), "e-mail"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa8"), "incoming envelope"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa9"), "envelope with arrow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa4"), "outbox tray"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa5"), "inbox tray"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa6"), "package"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xab"), "closed mailbox with raised flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xaa"), "closed mailbox with lowered flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xac"), "open mailbox with raised flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xad"), "open mailbox with lowered flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xae"), "postbox"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xb3"), "ballot box with ballot"}, + Emoji{QString::fromUtf8("\xe2\x9c\x8f"), "pencil"}, + Emoji{QString::fromUtf8("\xe2\x9c\x92"), "black nib"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x8b"), "fountain pen"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x8a"), "pen"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x8c"), "paintbrush"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x8d"), "crayon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x9d"), "memo"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xbc"), "briefcase"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x81"), "file folder"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x82"), "open file folder"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\x82"), "card index dividers"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x85"), "calendar"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x86"), "tear-off calendar"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\x92"), "spiral notepad"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\x93"), "spiral calendar"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x87"), "card index"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x88"), "chart increasing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x89"), "chart decreasing"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x8a"), "bar chart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x8b"), "clipboard"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x8c"), "pushpin"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x8d"), "round pushpin"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x8e"), "paperclip"}, + Emoji{QString::fromUtf8("\xf0\x9f\x96\x87"), "linked paperclips"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x8f"), "straight ruler"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x90"), "triangular ruler"}, + Emoji{QString::fromUtf8("\xe2\x9c\x82"), "scissors"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\x83"), "card file box"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\x84"), "file cabinet"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\x91"), "wastebasket"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x92"), "locked"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x93"), "unlocked"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x8f"), "locked with pen"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x90"), "locked with key"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x91"), "key"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\x9d"), "old key"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa8"), "hammer"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x93"), "axe"}, + Emoji{QString::fromUtf8("\xe2\x9b\x8f"), "pick"}, + Emoji{QString::fromUtf8("\xe2\x9a\x92"), "hammer and pick"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa0"), "hammer and wrench"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xa1"), "dagger"}, + Emoji{QString::fromUtf8("\xe2\x9a\x94"), "crossed swords"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xab"), "pistol"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x83"), "boomerang"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb9"), "bow and arrow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa1"), "shield"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x9a"), "carpentry saw"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa7"), "wrench"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x9b"), "screwdriver"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa9"), "nut and bolt"}, + Emoji{QString::fromUtf8("\xe2\x9a\x99"), "gear"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\x9c"), "clamp"}, + Emoji{QString::fromUtf8("\xe2\x9a\x96"), "balance scale"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa6\xaf"), "probing cane"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x97"), "link"}, + Emoji{QString::fromUtf8("\xe2\x9b\x93"), "chains"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x9d"), "hook"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb0"), "toolbox"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb2"), "magnet"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x9c"), "ladder"}, + Emoji{QString::fromUtf8("\xe2\x9a\x97"), "alembic"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xaa"), "test tube"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xab"), "petri dish"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xac"), "dna"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xac"), "microscope"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xad"), "telescope"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xa1"), "satellite antenna"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x89"), "syringe"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb8"), "drop of blood"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\x8a"), "pill"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa9\xb9"), "adhesive bandage"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa9\xba"), "stethoscope"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xaa"), "door"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x97"), "elevator"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x9e"), "mirror"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x9f"), "window"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8f"), "bed"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8b"), "couch and lamp"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x91"), "chair"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbd"), "toilet"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa0"), "plunger"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbf"), "shower"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x81"), "bathtub"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa4"), "mouse trap"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\x92"), "razor"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb4"), "lotion bottle"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb7"), "safety pin"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xb9"), "broom"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xba"), "basket"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xbb"), "roll of paper"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa3"), "bucket"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xbc"), "soap"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa5"), "toothbrush"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xbd"), "sponge"}, + Emoji{QString::fromUtf8("\xf0\x9f\xa7\xaf"), "fire extinguisher"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x92"), "shopping cart"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xac"), "cigarette"}, + Emoji{QString::fromUtf8("\xe2\x9a\xb0"), "coffin"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa6"), "headstone"}, + Emoji{QString::fromUtf8("\xe2\x9a\xb1"), "funeral urn"}, + Emoji{QString::fromUtf8("\xf0\x9f\x97\xbf"), "moai"}, + Emoji{QString::fromUtf8("\xf0\x9f\xaa\xa7"), "placard"}, }; -const std::vector<Emoji> Provider::symbols = { - Emoji{QString::fromUtf8("\xf0\x9f\x91\x81\xf0\x9f\x97\xa8"), ":eye_in_speech_bubble:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x98"), ":cupid:"}, - Emoji{QString::fromUtf8("\xe2\x9d\xa4"), ":heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x93"), ":heartbeat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x94"), ":broken_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x95"), ":two_hearts:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x96"), ":sparkling_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x97"), ":heartpulse:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x99"), ":blue_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x9a"), ":green_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x9b"), ":yellow_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x9c"), ":purple_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x96\xa4"), ":black_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x9d"), ":gift_heart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x9e"), ":revolving_hearts:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\x9f"), ":heart_decoration:"}, - Emoji{QString::fromUtf8("\xe2\x9d\xa3"), ":heart_exclamation:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa2"), ":anger:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa5"), ":boom:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xab"), ":dizzy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xac"), ":speech_balloon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xa8"), ":speech_left:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x97\xaf"), ":anger_right:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xad"), ":thought_balloon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xae"), ":white_flower:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x90"), ":globe_with_meridians:"}, - Emoji{QString::fromUtf8("\xe2\x99\xa8"), ":hotsprings:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x91"), ":octagonal_sign:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x9b"), ":clock12:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xa7"), ":clock1230:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x90"), ":clock1:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x9c"), ":clock130:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x91"), ":clock2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x9d"), ":clock230:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x92"), ":clock3:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x9e"), ":clock330:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x93"), ":clock4:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x9f"), ":clock430:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x94"), ":clock5:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xa0"), ":clock530:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x95"), ":clock6:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xa1"), ":clock630:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x96"), ":clock7:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xa2"), ":clock730:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x97"), ":clock8:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xa3"), ":clock830:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x98"), ":clock9:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xa4"), ":clock930:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x99"), ":clock10:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xa5"), ":clock1030:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x9a"), ":clock11:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\xa6"), ":clock1130:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8c\x80"), ":cyclone:"}, - Emoji{QString::fromUtf8("\xe2\x99\xa0"), ":spades:"}, - Emoji{QString::fromUtf8("\xe2\x99\xa5"), ":hearts:"}, - Emoji{QString::fromUtf8("\xe2\x99\xa6"), ":diamonds:"}, - Emoji{QString::fromUtf8("\xe2\x99\xa3"), ":clubs:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x83\x8f"), ":black_joker:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x80\x84"), ":mahjong:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb4"), ":flower_playing_cards:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x87"), ":mute:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x88"), ":speaker:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x89"), ":sound:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x8a"), ":loud_sound:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa2"), ":loudspeaker:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xa3"), ":mega:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x94"), ":bell:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x95"), ":no_bell:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb5"), ":musical_note:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb6"), ":notes:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb9"), ":chart:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb1"), ":currency_exchange:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xb2"), ":heavy_dollar_sign:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa7"), ":atm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xae"), ":put_litter_in_its_place:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb0"), ":potable_water:"}, - Emoji{QString::fromUtf8("\xe2\x99\xbf"), ":wheelchair:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb9"), ":mens:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xba"), ":womens:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbb"), ":restroom:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbc"), ":baby_symbol:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbe"), ":wc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x82"), ":passport_control:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x83"), ":customs:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x84"), ":baggage_claim:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x85"), ":left_luggage:"}, - Emoji{QString::fromUtf8("\xe2\x9a\xa0"), ":warning:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb8"), ":children_crossing:"}, - Emoji{QString::fromUtf8("\xe2\x9b\x94"), ":no_entry:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xab"), ":no_entry_sign:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb3"), ":no_bicycles:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xad"), ":no_smoking:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xaf"), ":do_not_litter:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb1"), ":non-potable_water:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb7"), ":no_pedestrians:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb5"), ":no_mobile_phones:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x9e"), ":underage:"}, - Emoji{QString::fromUtf8("\xe2\x98\xa2"), ":radioactive:"}, - Emoji{QString::fromUtf8("\xe2\x98\xa3"), ":biohazard:"}, - Emoji{QString::fromUtf8("\xe2\xac\x86"), ":arrow_up:"}, - Emoji{QString::fromUtf8("\xe2\x86\x97"), ":arrow_upper_right:"}, - Emoji{QString::fromUtf8("\xe2\x9e\xa1"), ":arrow_right:"}, - Emoji{QString::fromUtf8("\xe2\x86\x98"), ":arrow_lower_right:"}, - Emoji{QString::fromUtf8("\xe2\xac\x87"), ":arrow_down:"}, - Emoji{QString::fromUtf8("\xe2\x86\x99"), ":arrow_lower_left:"}, - Emoji{QString::fromUtf8("\xe2\xac\x85"), ":arrow_left:"}, - Emoji{QString::fromUtf8("\xe2\x86\x96"), ":arrow_upper_left:"}, - Emoji{QString::fromUtf8("\xe2\x86\x95"), ":arrow_up_down:"}, - Emoji{QString::fromUtf8("\xe2\x86\x94"), ":left_right_arrow:"}, - Emoji{QString::fromUtf8("\xe2\x86\xa9"), ":leftwards_arrow_with_hook:"}, - Emoji{QString::fromUtf8("\xe2\x86\xaa"), ":arrow_right_hook:"}, - Emoji{QString::fromUtf8("\xe2\xa4\xb4"), ":arrow_heading_up:"}, - Emoji{QString::fromUtf8("\xe2\xa4\xb5"), ":arrow_heading_down:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x83"), ":arrows_clockwise:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x84"), ":arrows_counterclockwise:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x99"), ":back:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x9a"), ":end:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x9b"), ":on:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x9c"), ":soon:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x9d"), ":top:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x9b\x90"), ":place_of_worship:"}, - Emoji{QString::fromUtf8("\xe2\x9a\x9b"), ":atom:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x89"), ":om_symbol:"}, - Emoji{QString::fromUtf8("\xe2\x9c\xa1"), ":star_of_david:"}, - Emoji{QString::fromUtf8("\xe2\x98\xb8"), ":wheel_of_dharma:"}, - Emoji{QString::fromUtf8("\xe2\x98\xaf"), ":yin_yang:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x9d"), ":cross:"}, - Emoji{QString::fromUtf8("\xe2\x98\xa6"), ":orthodox_cross:"}, - Emoji{QString::fromUtf8("\xe2\x98\xaa"), ":star_and_crescent:"}, - Emoji{QString::fromUtf8("\xe2\x98\xae"), ":peace:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x95\x8e"), ":menorah:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xaf"), ":six_pointed_star:"}, - Emoji{QString::fromUtf8("\xe2\x99\x88"), ":aries:"}, - Emoji{QString::fromUtf8("\xe2\x99\x89"), ":taurus:"}, - Emoji{QString::fromUtf8("\xe2\x99\x8a"), ":gemini:"}, - Emoji{QString::fromUtf8("\xe2\x99\x8b"), ":cancer:"}, - Emoji{QString::fromUtf8("\xe2\x99\x8c"), ":leo:"}, - Emoji{QString::fromUtf8("\xe2\x99\x8d"), ":virgo:"}, - Emoji{QString::fromUtf8("\xe2\x99\x8e"), ":libra:"}, - Emoji{QString::fromUtf8("\xe2\x99\x8f"), ":scorpius:"}, - Emoji{QString::fromUtf8("\xe2\x99\x90"), ":sagittarius:"}, - Emoji{QString::fromUtf8("\xe2\x99\x91"), ":capricorn:"}, - Emoji{QString::fromUtf8("\xe2\x99\x92"), ":aquarius:"}, - Emoji{QString::fromUtf8("\xe2\x99\x93"), ":pisces:"}, - Emoji{QString::fromUtf8("\xe2\x9b\x8e"), ":ophiuchus:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x80"), ":twisted_rightwards_arrows:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x81"), ":repeat:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x82"), ":repeat_one:"}, - Emoji{QString::fromUtf8("\xe2\x96\xb6"), ":arrow_forward:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xa9"), ":fast_forward:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xad"), ":track_next:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xaf"), ":play_pause:"}, - Emoji{QString::fromUtf8("\xe2\x97\x80"), ":arrow_backward:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xaa"), ":rewind:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xae"), ":track_previous:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xbc"), ":arrow_up_small:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xab"), ":arrow_double_up:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xbd"), ":arrow_down_small:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xac"), ":arrow_double_down:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xb8"), ":pause_button:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xb9"), ":stop_button:"}, - Emoji{QString::fromUtf8("\xe2\x8f\xba"), ":record_button:"}, - Emoji{QString::fromUtf8("\xe2\x8f\x8f"), ":eject:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa6"), ":cinema:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x85"), ":low_brightness:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x86"), ":high_brightness:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb6"), ":signal_strength:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb3"), ":vibration_mode:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\xb4"), ":mobile_phone_off:"}, - Emoji{QString::fromUtf8("\xe2\x99\xbb"), ":recycle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x93\x9b"), ":name_badge:"}, - Emoji{QString::fromUtf8("\xe2\x9a\x9c"), ":fleur-de-lis:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb0"), ":beginner:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb1"), ":trident:"}, - Emoji{QString::fromUtf8("\xe2\xad\x95"), ":o:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x85"), ":white_check_mark:"}, - Emoji{QString::fromUtf8("\xe2\x98\x91"), ":ballot_box_with_check:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x94"), ":heavy_check_mark:"}, - Emoji{QString::fromUtf8("\xe2\x9c\x96"), ":heavy_multiplication_x:"}, - Emoji{QString::fromUtf8("\xe2\x9d\x8c"), ":x:"}, - Emoji{QString::fromUtf8("\xe2\x9d\x8e"), ":negative_squared_cross_mark:"}, - Emoji{QString::fromUtf8("\xe2\x9e\x95"), ":heavy_plus_sign:"}, - Emoji{QString::fromUtf8("\xe2\x9e\x96"), ":heavy_minus_sign:"}, - Emoji{QString::fromUtf8("\xe2\x9e\x97"), ":heavy_division_sign:"}, - Emoji{QString::fromUtf8("\xe2\x9e\xb0"), ":curly_loop:"}, - Emoji{QString::fromUtf8("\xe2\x9e\xbf"), ":loop:"}, - Emoji{QString::fromUtf8("\xe3\x80\xbd"), ":part_alternation_mark:"}, - Emoji{QString::fromUtf8("\xe2\x9c\xb3"), ":eight_spoked_asterisk:"}, - Emoji{QString::fromUtf8("\xe2\x9c\xb4"), ":eight_pointed_black_star:"}, - Emoji{QString::fromUtf8("\xe2\x9d\x87"), ":sparkle:"}, - Emoji{QString::fromUtf8("\xe2\x80\xbc"), ":bangbang:"}, - Emoji{QString::fromUtf8("\xe2\x81\x89"), ":interrobang:"}, - Emoji{QString::fromUtf8("\xe2\x9d\x93"), ":question:"}, - Emoji{QString::fromUtf8("\xe2\x9d\x94"), ":grey_question:"}, - Emoji{QString::fromUtf8("\xe2\x9d\x95"), ":grey_exclamation:"}, - Emoji{QString::fromUtf8("\xe2\x9d\x97"), ":exclamation:"}, - Emoji{QString::fromUtf8("\xe3\x80\xb0"), ":wavy_dash:"}, - Emoji{QString::fromUtf8("\xc2\xa9"), ":copyright:"}, - Emoji{QString::fromUtf8("\xc2\xae"), ":registered:"}, - Emoji{QString::fromUtf8("\xe2\x84\xa2"), ":tm:"}, - Emoji{QString::fromUtf8("#\xe2\x83\xa3"), ":hash:"}, - Emoji{QString::fromUtf8("*\xe2\x83\xa3"), ":asterisk:"}, - Emoji{QString::fromUtf8("0\xe2\x83\xa3"), ":zero:"}, - Emoji{QString::fromUtf8("1\xe2\x83\xa3"), ":one:"}, - Emoji{QString::fromUtf8("2\xe2\x83\xa3"), ":two:"}, - Emoji{QString::fromUtf8("3\xe2\x83\xa3"), ":three:"}, - Emoji{QString::fromUtf8("4\xe2\x83\xa3"), ":four:"}, - Emoji{QString::fromUtf8("5\xe2\x83\xa3"), ":five:"}, - Emoji{QString::fromUtf8("6\xe2\x83\xa3"), ":six:"}, - Emoji{QString::fromUtf8("7\xe2\x83\xa3"), ":seven:"}, - Emoji{QString::fromUtf8("8\xe2\x83\xa3"), ":eight:"}, - Emoji{QString::fromUtf8("9\xe2\x83\xa3"), ":nine:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x9f"), ":keycap_ten:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xaf"), ":100:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa0"), ":capital_abcd:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa1"), ":abcd:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa2"), ":1234:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa3"), ":symbols:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xa4"), ":abc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x85\xb0"), ":a:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x8e"), ":ab:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x85\xb1"), ":b:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x91"), ":cl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x92"), ":cool:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x93"), ":free:"}, - Emoji{QString::fromUtf8("\xe2\x84\xb9"), ":information_source:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x94"), ":id:"}, - Emoji{QString::fromUtf8("\xe2\x93\x82"), ":m:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x95"), ":new:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x96"), ":ng:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x85\xbe"), ":o2:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x97"), ":ok:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x85\xbf"), ":parking:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x98"), ":sos:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x99"), ":up:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x86\x9a"), ":vs:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\x81"), ":koko:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\x82"), ":sa:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xb7"), ":u6708:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xb6"), ":u6709:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xaf"), ":u6307:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x89\x90"), ":ideograph_advantage:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xb9"), ":u5272:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\x9a"), ":u7121:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xb2"), ":u7981:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x89\x91"), ":accept:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xb8"), ":u7533:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xb4"), ":u5408:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xb3"), ":u7a7a:"}, - Emoji{QString::fromUtf8("\xe3\x8a\x97"), ":congratulations:"}, - Emoji{QString::fromUtf8("\xe3\x8a\x99"), ":secret:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xba"), ":u55b6:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x88\xb5"), ":u6e80:"}, - Emoji{QString::fromUtf8("\xe2\x96\xaa"), ":black_small_square:"}, - Emoji{QString::fromUtf8("\xe2\x96\xab"), ":white_small_square:"}, - Emoji{QString::fromUtf8("\xe2\x97\xbb"), ":white_medium_square:"}, - Emoji{QString::fromUtf8("\xe2\x97\xbc"), ":black_medium_square:"}, - Emoji{QString::fromUtf8("\xe2\x97\xbd"), ":white_medium_small_square:"}, - Emoji{QString::fromUtf8("\xe2\x97\xbe"), ":black_medium_small_square:"}, - Emoji{QString::fromUtf8("\xe2\xac\x9b"), ":black_large_square:"}, - Emoji{QString::fromUtf8("\xe2\xac\x9c"), ":white_large_square:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb6"), ":large_orange_diamond:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb7"), ":large_blue_diamond:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb8"), ":small_orange_diamond:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb9"), ":small_blue_diamond:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xba"), ":small_red_triangle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xbb"), ":small_red_triangle_down:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x92\xa0"), ":diamond_shape_with_a_dot_inside:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\x98"), ":radio_button:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb2"), ":black_square_button:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb3"), ":white_square_button:"}, - Emoji{QString::fromUtf8("\xe2\x9a\xaa"), ":white_circle:"}, - Emoji{QString::fromUtf8("\xe2\x9a\xab"), ":black_circle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb4"), ":red_circle:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x94\xb5"), ":blue_circle:"}, +const std::vector<Emoji> emoji::Provider::symbols = { + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa7"), "ATM sign"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xae"), "litter in bin sign"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb0"), "potable water"}, + Emoji{QString::fromUtf8("\xe2\x99\xbf"), "wheelchair symbol"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb9"), "men’s room"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xba"), "women’s room"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbb"), "restroom"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbc"), "baby symbol"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbe"), "water closet"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x82"), "passport control"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x83"), "customs"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x84"), "baggage claim"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x85"), "left luggage"}, + Emoji{QString::fromUtf8("\xe2\x9a\xa0"), "warning"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb8"), "children crossing"}, + Emoji{QString::fromUtf8("\xe2\x9b\x94"), "no entry"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xab"), "prohibited"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb3"), "no bicycles"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xad"), "no smoking"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xaf"), "no littering"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb1"), "non-potable water"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb7"), "no pedestrians"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb5"), "no mobile phones"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x9e"), "no one under eighteen"}, + Emoji{QString::fromUtf8("\xe2\x98\xa2"), "radioactive"}, + Emoji{QString::fromUtf8("\xe2\x98\xa3"), "biohazard"}, + Emoji{QString::fromUtf8("\xe2\xac\x86"), "up arrow"}, + Emoji{QString::fromUtf8("\xe2\x86\x97"), "up-right arrow"}, + Emoji{QString::fromUtf8("\xe2\x9e\xa1"), "right arrow"}, + Emoji{QString::fromUtf8("\xe2\x86\x98"), "down-right arrow"}, + Emoji{QString::fromUtf8("\xe2\xac\x87"), "down arrow"}, + Emoji{QString::fromUtf8("\xe2\x86\x99"), "down-left arrow"}, + Emoji{QString::fromUtf8("\xe2\xac\x85"), "left arrow"}, + Emoji{QString::fromUtf8("\xe2\x86\x96"), "up-left arrow"}, + Emoji{QString::fromUtf8("\xe2\x86\x95"), "up-down arrow"}, + Emoji{QString::fromUtf8("\xe2\x86\x94"), "left-right arrow"}, + Emoji{QString::fromUtf8("\xe2\x86\xa9"), "right arrow curving left"}, + Emoji{QString::fromUtf8("\xe2\x86\xaa"), "left arrow curving right"}, + Emoji{QString::fromUtf8("\xe2\xa4\xb4"), "right arrow curving up"}, + Emoji{QString::fromUtf8("\xe2\xa4\xb5"), "right arrow curving down"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x83"), "clockwise vertical arrows"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x84"), "counterclockwise arrows button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x99"), "BACK arrow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x9a"), "END arrow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x9b"), "ON! arrow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x9c"), "SOON arrow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x9d"), "TOP arrow"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9b\x90"), "place of worship"}, + Emoji{QString::fromUtf8("\xe2\x9a\x9b"), "atom symbol"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x89"), "om"}, + Emoji{QString::fromUtf8("\xe2\x9c\xa1"), "star of David"}, + Emoji{QString::fromUtf8("\xe2\x98\xb8"), "wheel of dharma"}, + Emoji{QString::fromUtf8("\xe2\x98\xaf"), "yin yang"}, + Emoji{QString::fromUtf8("\xe2\x9c\x9d"), "latin cross"}, + Emoji{QString::fromUtf8("\xe2\x98\xa6"), "orthodox cross"}, + Emoji{QString::fromUtf8("\xe2\x98\xaa"), "star and crescent"}, + Emoji{QString::fromUtf8("\xe2\x98\xae"), "peace symbol"}, + Emoji{QString::fromUtf8("\xf0\x9f\x95\x8e"), "menorah"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xaf"), "dotted six-pointed star"}, + Emoji{QString::fromUtf8("\xe2\x99\x88"), "Aries"}, + Emoji{QString::fromUtf8("\xe2\x99\x89"), "Taurus"}, + Emoji{QString::fromUtf8("\xe2\x99\x8a"), "Gemini"}, + Emoji{QString::fromUtf8("\xe2\x99\x8b"), "Cancer"}, + Emoji{QString::fromUtf8("\xe2\x99\x8c"), "Leo"}, + Emoji{QString::fromUtf8("\xe2\x99\x8d"), "Virgo"}, + Emoji{QString::fromUtf8("\xe2\x99\x8e"), "Libra"}, + Emoji{QString::fromUtf8("\xe2\x99\x8f"), "Scorpio"}, + Emoji{QString::fromUtf8("\xe2\x99\x90"), "Sagittarius"}, + Emoji{QString::fromUtf8("\xe2\x99\x91"), "Capricorn"}, + Emoji{QString::fromUtf8("\xe2\x99\x92"), "Aquarius"}, + Emoji{QString::fromUtf8("\xe2\x99\x93"), "Pisces"}, + Emoji{QString::fromUtf8("\xe2\x9b\x8e"), "Ophiuchus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x80"), "shuffle tracks button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x81"), "repeat button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x82"), "repeat single button"}, + Emoji{QString::fromUtf8("\xe2\x96\xb6"), "play button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xa9"), "fast-forward button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xad"), "next track button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xaf"), "play or pause button"}, + Emoji{QString::fromUtf8("\xe2\x97\x80"), "reverse button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xaa"), "fast reverse button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xae"), "last track button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xbc"), "upwards button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xab"), "fast up button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xbd"), "downwards button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xac"), "fast down button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xb8"), "pause button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xb9"), "stop button"}, + Emoji{QString::fromUtf8("\xe2\x8f\xba"), "record button"}, + Emoji{QString::fromUtf8("\xe2\x8f\x8f"), "eject button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa6"), "cinema"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x85"), "dim button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x86"), "bright button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb6"), "antenna bars"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb3"), "vibration mode"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\xb4"), "mobile phone off"}, + Emoji{QString::fromUtf8("\xe2\x99\x80"), "female sign"}, + Emoji{QString::fromUtf8("\xe2\x99\x82"), "male sign"}, + Emoji{QString::fromUtf8("\xe2\x9a\xa7"), "transgender symbol"}, + Emoji{QString::fromUtf8("\xe2\x9a\x95"), "medical symbol"}, + Emoji{QString::fromUtf8("\xe2\x99\xbe"), "infinity"}, + Emoji{QString::fromUtf8("\xe2\x99\xbb"), "recycling symbol"}, + Emoji{QString::fromUtf8("\xe2\x9a\x9c"), "fleur-de-lis"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb1"), "trident emblem"}, + Emoji{QString::fromUtf8("\xf0\x9f\x93\x9b"), "name badge"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb0"), "Japanese symbol for beginner"}, + Emoji{QString::fromUtf8("\xe2\xad\x95"), "hollow red circle"}, + Emoji{QString::fromUtf8("\xe2\x9c\x85"), "check mark button"}, + Emoji{QString::fromUtf8("\xe2\x98\x91"), "check box with check"}, + Emoji{QString::fromUtf8("\xe2\x9c\x94"), "check mark"}, + Emoji{QString::fromUtf8("\xe2\x9c\x96"), "multiplication sign"}, + Emoji{QString::fromUtf8("\xe2\x9d\x8c"), "cross mark"}, + Emoji{QString::fromUtf8("\xe2\x9d\x8e"), "cross mark button"}, + Emoji{QString::fromUtf8("\xe2\x9e\x95"), "plus sign"}, + Emoji{QString::fromUtf8("\xe2\x9e\x96"), "minus sign"}, + Emoji{QString::fromUtf8("\xe2\x9e\x97"), "division sign"}, + Emoji{QString::fromUtf8("\xe2\x9e\xb0"), "curly loop"}, + Emoji{QString::fromUtf8("\xe2\x9e\xbf"), "double curly loop"}, + Emoji{QString::fromUtf8("\xe3\x80\xbd"), "part alternation mark"}, + Emoji{QString::fromUtf8("\xe2\x9c\xb3"), "eight-spoked asterisk"}, + Emoji{QString::fromUtf8("\xe2\x9c\xb4"), "eight-pointed star"}, + Emoji{QString::fromUtf8("\xe2\x9d\x87"), "sparkle"}, + Emoji{QString::fromUtf8("\xe2\x80\xbc"), "double exclamation mark"}, + Emoji{QString::fromUtf8("\xe2\x81\x89"), "exclamation question mark"}, + Emoji{QString::fromUtf8("\xe2\x9d\x93"), "question mark"}, + Emoji{QString::fromUtf8("\xe2\x9d\x94"), "white question mark"}, + Emoji{QString::fromUtf8("\xe2\x9d\x95"), "white exclamation mark"}, + Emoji{QString::fromUtf8("\xe2\x9d\x97"), "exclamation mark"}, + Emoji{QString::fromUtf8("\xe3\x80\xb0"), "wavy dash"}, + Emoji{QString::fromUtf8("\xc2\xa9"), "copyright"}, + Emoji{QString::fromUtf8("\xc2\xae"), "registered"}, + Emoji{QString::fromUtf8("\xe2\x84\xa2"), "trade mark"}, + Emoji{QString::fromUtf8("#\xef\xb8\x8f\xe2\x83\xa3"), "keycap: #"}, + Emoji{QString::fromUtf8("#\xe2\x83\xa3"), "keycap: #"}, + Emoji{QString::fromUtf8("*\xef\xb8\x8f\xe2\x83\xa3"), "keycap: *"}, + Emoji{QString::fromUtf8("*\xe2\x83\xa3"), "keycap: *"}, + Emoji{QString::fromUtf8("0\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 0"}, + Emoji{QString::fromUtf8("0\xe2\x83\xa3"), "keycap: 0"}, + Emoji{QString::fromUtf8("1\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 1"}, + Emoji{QString::fromUtf8("1\xe2\x83\xa3"), "keycap: 1"}, + Emoji{QString::fromUtf8("2\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 2"}, + Emoji{QString::fromUtf8("2\xe2\x83\xa3"), "keycap: 2"}, + Emoji{QString::fromUtf8("3\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 3"}, + Emoji{QString::fromUtf8("3\xe2\x83\xa3"), "keycap: 3"}, + Emoji{QString::fromUtf8("4\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 4"}, + Emoji{QString::fromUtf8("4\xe2\x83\xa3"), "keycap: 4"}, + Emoji{QString::fromUtf8("5\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 5"}, + Emoji{QString::fromUtf8("5\xe2\x83\xa3"), "keycap: 5"}, + Emoji{QString::fromUtf8("6\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 6"}, + Emoji{QString::fromUtf8("6\xe2\x83\xa3"), "keycap: 6"}, + Emoji{QString::fromUtf8("7\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 7"}, + Emoji{QString::fromUtf8("7\xe2\x83\xa3"), "keycap: 7"}, + Emoji{QString::fromUtf8("8\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 8"}, + Emoji{QString::fromUtf8("8\xe2\x83\xa3"), "keycap: 8"}, + Emoji{QString::fromUtf8("9\xef\xb8\x8f\xe2\x83\xa3"), "keycap: 9"}, + Emoji{QString::fromUtf8("9\xe2\x83\xa3"), "keycap: 9"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x9f"), "keycap: 10"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa0"), "input latin uppercase"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa1"), "input latin lowercase"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa2"), "input numbers"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa3"), "input symbols"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xa4"), "input latin letters"}, + Emoji{QString::fromUtf8("\xf0\x9f\x85\xb0"), "A button (blood type)"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x8e"), "AB button (blood type)"}, + Emoji{QString::fromUtf8("\xf0\x9f\x85\xb1"), "B button (blood type)"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x91"), "CL button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x92"), "COOL button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x93"), "FREE button"}, + Emoji{QString::fromUtf8("\xe2\x84\xb9"), "information"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x94"), "ID button"}, + Emoji{QString::fromUtf8("\xe2\x93\x82"), "circled M"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x95"), "NEW button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x96"), "NG button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x85\xbe"), "O button (blood type)"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x97"), "OK button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x85\xbf"), "P button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x98"), "SOS button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x99"), "UP! button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x86\x9a"), "VS button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\x81"), "Japanese “here” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\x82"), "Japanese “service charge” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xb7"), "Japanese “monthly amount” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xb6"), "Japanese “not free of charge” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xaf"), "Japanese “reserved” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x89\x90"), "Japanese “bargain” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xb9"), "Japanese “discount” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\x9a"), "Japanese “free of charge” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xb2"), "Japanese “prohibited” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x89\x91"), "Japanese “acceptable” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xb8"), "Japanese “application” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xb4"), "Japanese “passing grade” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xb3"), "Japanese “vacancy” button"}, + Emoji{QString::fromUtf8("\xe3\x8a\x97"), "Japanese “congratulations” button"}, + Emoji{QString::fromUtf8("\xe3\x8a\x99"), "Japanese “secret” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xba"), "Japanese “open for business” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x88\xb5"), "Japanese “no vacancy” button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb4"), "red circle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa0"), "orange circle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa1"), "yellow circle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa2"), "green circle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb5"), "blue circle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa3"), "purple circle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa4"), "brown circle"}, + Emoji{QString::fromUtf8("\xe2\x9a\xab"), "black circle"}, + Emoji{QString::fromUtf8("\xe2\x9a\xaa"), "white circle"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa5"), "red square"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa7"), "orange square"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa8"), "yellow square"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa9"), "green square"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xa6"), "blue square"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xaa"), "purple square"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9f\xab"), "brown square"}, + Emoji{QString::fromUtf8("\xe2\xac\x9b"), "black large square"}, + Emoji{QString::fromUtf8("\xe2\xac\x9c"), "white large square"}, + Emoji{QString::fromUtf8("\xe2\x97\xbc"), "black medium square"}, + Emoji{QString::fromUtf8("\xe2\x97\xbb"), "white medium square"}, + Emoji{QString::fromUtf8("\xe2\x97\xbe"), "black medium-small square"}, + Emoji{QString::fromUtf8("\xe2\x97\xbd"), "white medium-small square"}, + Emoji{QString::fromUtf8("\xe2\x96\xaa"), "black small square"}, + Emoji{QString::fromUtf8("\xe2\x96\xab"), "white small square"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb6"), "large orange diamond"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb7"), "large blue diamond"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb8"), "small orange diamond"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb9"), "small blue diamond"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xba"), "red triangle pointed up"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xbb"), "red triangle pointed down"}, + Emoji{QString::fromUtf8("\xf0\x9f\x92\xa0"), "diamond with a dot"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\x98"), "radio button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb3"), "white square button"}, + Emoji{QString::fromUtf8("\xf0\x9f\x94\xb2"), "black square button"}, }; -const std::vector<Emoji> Provider::flags = { - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xa8"), ":flag_ac:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xa9"), ":flag_ad:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xaa"), ":flag_ae:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xab"), ":flag_af:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xac"), ":flag_ag:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xae"), ":flag_ai:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb1"), ":flag_al:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb2"), ":flag_am:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb4"), ":flag_ao:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb6"), ":flag_aq:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb7"), ":flag_ar:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb8"), ":flag_as:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb9"), ":flag_at:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xba"), ":flag_au:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xbc"), ":flag_aw:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xbd"), ":flag_ax:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xbf"), ":flag_az:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xa6"), ":flag_ba:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xa7"), ":flag_bb:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xa9"), ":flag_bd:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xaa"), ":flag_be:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xab"), ":flag_bf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xac"), ":flag_bg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xad"), ":flag_bh:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xae"), ":flag_bi:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xaf"), ":flag_bj:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb1"), ":flag_bl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb2"), ":flag_bm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb3"), ":flag_bn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb4"), ":flag_bo:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb6"), ":flag_bq:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb7"), ":flag_br:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb8"), ":flag_bs:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb9"), ":flag_bt:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbb"), ":flag_bv:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbc"), ":flag_bw:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbe"), ":flag_by:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbf"), ":flag_bz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xa6"), ":flag_ca:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xa8"), ":flag_cc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xa9"), ":flag_cd:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xab"), ":flag_cf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xac"), ":flag_cg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xad"), ":flag_ch:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xae"), ":flag_ci:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb0"), ":flag_ck:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb1"), ":flag_cl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb2"), ":flag_cm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb3"), ":flag_cn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb4"), ":flag_co:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb5"), ":flag_cp:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb7"), ":flag_cr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xba"), ":flag_cu:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbb"), ":flag_cv:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbc"), ":flag_cw:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbd"), ":flag_cx:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbe"), ":flag_cy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbf"), ":flag_cz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xaa"), ":flag_de:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xac"), ":flag_dg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xaf"), ":flag_dj:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xb0"), ":flag_dk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xb2"), ":flag_dm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xb4"), ":flag_do:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xbf"), ":flag_dz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xa6"), ":flag_ea:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xa8"), ":flag_ec:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xaa"), ":flag_ee:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xac"), ":flag_eg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xad"), ":flag_eh:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xb7"), ":flag_er:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xb8"), ":flag_es:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xb9"), ":flag_et:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xba"), ":flag_eu:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xae"), ":flag_fi:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xaf"), ":flag_fj:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb0"), ":flag_fk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb2"), ":flag_fm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb4"), ":flag_fo:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb7"), ":flag_fr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xa6"), ":flag_ga:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xa7"), ":flag_gb:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xa9"), ":flag_gd:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xaa"), ":flag_ge:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xab"), ":flag_gf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xac"), ":flag_gg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xad"), ":flag_gh:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xae"), ":flag_gi:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb1"), ":flag_gl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb2"), ":flag_gm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb3"), ":flag_gn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb5"), ":flag_gp:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb6"), ":flag_gq:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb7"), ":flag_gr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb8"), ":flag_gs:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb9"), ":flag_gt:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xba"), ":flag_gu:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xbc"), ":flag_gw:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xbe"), ":flag_gy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb0"), ":flag_hk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb2"), ":flag_hm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb3"), ":flag_hn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb7"), ":flag_hr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb9"), ":flag_ht:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xba"), ":flag_hu:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xa8"), ":flag_ic:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xa9"), ":flag_id:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xaa"), ":flag_ie:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb1"), ":flag_il:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb2"), ":flag_im:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb3"), ":flag_in:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb4"), ":flag_io:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb6"), ":flag_iq:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb7"), ":flag_ir:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb8"), ":flag_is:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb9"), ":flag_it:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xaa"), ":flag_je:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xb2"), ":flag_jm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xb4"), ":flag_jo:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xb5"), ":flag_jp:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xaa"), ":flag_ke:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xac"), ":flag_kg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xad"), ":flag_kh:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xae"), ":flag_ki:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb2"), ":flag_km:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb3"), ":flag_kn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb5"), ":flag_kp:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb7"), ":flag_kr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xbc"), ":flag_kw:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xbe"), ":flag_ky:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xbf"), ":flag_kz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xa6"), ":flag_la:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xa7"), ":flag_lb:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xa8"), ":flag_lc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xae"), ":flag_li:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb0"), ":flag_lk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb7"), ":flag_lr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb8"), ":flag_ls:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb9"), ":flag_lt:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xba"), ":flag_lu:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xbb"), ":flag_lv:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xbe"), ":flag_ly:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xa6"), ":flag_ma:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xa8"), ":flag_mc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xa9"), ":flag_md:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xaa"), ":flag_me:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xab"), ":flag_mf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xac"), ":flag_mg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xad"), ":flag_mh:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb0"), ":flag_mk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb1"), ":flag_ml:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb2"), ":flag_mm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb3"), ":flag_mn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb4"), ":flag_mo:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb5"), ":flag_mp:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb6"), ":flag_mq:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb7"), ":flag_mr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb8"), ":flag_ms:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb9"), ":flag_mt:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xba"), ":flag_mu:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbb"), ":flag_mv:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbc"), ":flag_mw:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbd"), ":flag_mx:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbe"), ":flag_my:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbf"), ":flag_mz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xa6"), ":flag_na:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xa8"), ":flag_nc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xaa"), ":flag_ne:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xab"), ":flag_nf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xac"), ":flag_ng:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xae"), ":flag_ni:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb1"), ":flag_nl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb4"), ":flag_no:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb5"), ":flag_np:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb7"), ":flag_nr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xba"), ":flag_nu:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xbf"), ":flag_nz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb4\xf0\x9f\x87\xb2"), ":flag_om:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xa6"), ":flag_pa:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xaa"), ":flag_pe:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xab"), ":flag_pf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xac"), ":flag_pg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xad"), ":flag_ph:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb0"), ":flag_pk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb1"), ":flag_pl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb2"), ":flag_pm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb3"), ":flag_pn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb7"), ":flag_pr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb8"), ":flag_ps:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb9"), ":flag_pt:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xbc"), ":flag_pw:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xbe"), ":flag_py:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb6\xf0\x9f\x87\xa6"), ":flag_qa:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xaa"), ":flag_re:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xb4"), ":flag_ro:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xb8"), ":flag_rs:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xba"), ":flag_ru:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xbc"), ":flag_rw:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa6"), ":flag_sa:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa7"), ":flag_sb:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa8"), ":flag_sc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa9"), ":flag_sd:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xaa"), ":flag_se:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xac"), ":flag_sg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xad"), ":flag_sh:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xae"), ":flag_si:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xaf"), ":flag_sj:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb0"), ":flag_sk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb1"), ":flag_sl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb2"), ":flag_sm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb3"), ":flag_sn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb4"), ":flag_so:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb7"), ":flag_sr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb8"), ":flag_ss:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb9"), ":flag_st:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbb"), ":flag_sv:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbd"), ":flag_sx:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbe"), ":flag_sy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbf"), ":flag_sz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xa6"), ":flag_ta:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xa8"), ":flag_tc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xa9"), ":flag_td:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xab"), ":flag_tf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xac"), ":flag_tg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xad"), ":flag_th:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xaf"), ":flag_tj:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb0"), ":flag_tk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb1"), ":flag_tl:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb2"), ":flag_tm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb3"), ":flag_tn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb4"), ":flag_to:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb7"), ":flag_tr:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb9"), ":flag_tt:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xbb"), ":flag_tv:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xbc"), ":flag_tw:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xbf"), ":flag_tz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xa6"), ":flag_ua:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xac"), ":flag_ug:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xb2"), ":flag_um:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xb8"), ":flag_us:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xbe"), ":flag_uy:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xbf"), ":flag_uz:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xa6"), ":flag_va:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xa8"), ":flag_vc:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xaa"), ":flag_ve:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xac"), ":flag_vg:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xae"), ":flag_vi:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xb3"), ":flag_vn:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xba"), ":flag_vu:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbc\xf0\x9f\x87\xab"), ":flag_wf:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbc\xf0\x9f\x87\xb8"), ":flag_ws:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbd\xf0\x9f\x87\xb0"), ":flag_xk:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbe\xf0\x9f\x87\xaa"), ":flag_ye:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbe\xf0\x9f\x87\xb9"), ":flag_yt:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbf\xf0\x9f\x87\xa6"), ":flag_za:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbf\xf0\x9f\x87\xb2"), ":flag_zm:"}, - Emoji{QString::fromUtf8("\xf0\x9f\x87\xbf\xf0\x9f\x87\xbc"), ":flag_zw:"}, +const std::vector<Emoji> emoji::Provider::flags = { + Emoji{QString::fromUtf8("\xf0\x9f\x8f\x81"), "chequered flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa9"), "triangular flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8c"), "crossed flags"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb4"), "black flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb3"), "white flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb3\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x8c\x88"), + "rainbow flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb3\xe2\x80\x8d\xf0\x9f\x8c\x88"), "rainbow flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb3\xef\xb8\x8f\xe2\x80\x8d\xe2\x9a\xa7"), + "transgender flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb3\xe2\x80\x8d\xe2\x9a\xa7"), "transgender flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb4\xe2\x80\x8d\xe2\x98\xa0"), "pirate flag"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xa8"), "flag: Ascension Island"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xa9"), "flag: Andorra"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xaa"), "flag: United Arab Emirates"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xab"), "flag: Afghanistan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xac"), "flag: Antigua & Barbuda"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xae"), "flag: Anguilla"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb1"), "flag: Albania"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb2"), "flag: Armenia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb4"), "flag: Angola"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb6"), "flag: Antarctica"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb7"), "flag: Argentina"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb8"), "flag: American Samoa"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb9"), "flag: Austria"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xba"), "flag: Australia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xbc"), "flag: Aruba"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xbd"), "flag: Åland Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xbf"), "flag: Azerbaijan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xa6"), "flag: Bosnia & Herzegovina"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xa7"), "flag: Barbados"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xa9"), "flag: Bangladesh"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xaa"), "flag: Belgium"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xab"), "flag: Burkina Faso"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xac"), "flag: Bulgaria"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xad"), "flag: Bahrain"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xae"), "flag: Burundi"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xaf"), "flag: Benin"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb1"), "flag: St. Barthélemy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb2"), "flag: Bermuda"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb3"), "flag: Brunei"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb4"), "flag: Bolivia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb6"), "flag: Caribbean Netherlands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb7"), "flag: Brazil"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb8"), "flag: Bahamas"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb9"), "flag: Bhutan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbb"), "flag: Bouvet Island"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbc"), "flag: Botswana"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbe"), "flag: Belarus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbf"), "flag: Belize"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xa6"), "flag: Canada"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xa8"), "flag: Cocos (Keeling) Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xa9"), "flag: Congo - Kinshasa"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xab"), "flag: Central African Republic"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xac"), "flag: Congo - Brazzaville"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xad"), "flag: Switzerland"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xae"), "flag: Côte d’Ivoire"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb0"), "flag: Cook Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb1"), "flag: Chile"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb2"), "flag: Cameroon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb3"), "flag: China"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb4"), "flag: Colombia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb5"), "flag: Clipperton Island"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb7"), "flag: Costa Rica"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xba"), "flag: Cuba"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbb"), "flag: Cape Verde"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbc"), "flag: Curaçao"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbd"), "flag: Christmas Island"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbe"), "flag: Cyprus"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbf"), "flag: Czechia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xaa"), "flag: Germany"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xac"), "flag: Diego Garcia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xaf"), "flag: Djibouti"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xb0"), "flag: Denmark"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xb2"), "flag: Dominica"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xb4"), "flag: Dominican Republic"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xbf"), "flag: Algeria"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xa6"), "flag: Ceuta & Melilla"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xa8"), "flag: Ecuador"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xaa"), "flag: Estonia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xac"), "flag: Egypt"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xad"), "flag: Western Sahara"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xb7"), "flag: Eritrea"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xb8"), "flag: Spain"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xb9"), "flag: Ethiopia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xba"), "flag: European Union"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xae"), "flag: Finland"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xaf"), "flag: Fiji"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb0"), "flag: Falkland Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb2"), "flag: Micronesia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb4"), "flag: Faroe Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb7"), "flag: France"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xa6"), "flag: Gabon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xa7"), "flag: United Kingdom"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xa9"), "flag: Grenada"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xaa"), "flag: Georgia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xab"), "flag: French Guiana"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xac"), "flag: Guernsey"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xad"), "flag: Ghana"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xae"), "flag: Gibraltar"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb1"), "flag: Greenland"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb2"), "flag: Gambia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb3"), "flag: Guinea"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb5"), "flag: Guadeloupe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb6"), "flag: Equatorial Guinea"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb7"), "flag: Greece"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb8"), + "flag: South Georgia & South Sandwich Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb9"), "flag: Guatemala"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xba"), "flag: Guam"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xbc"), "flag: Guinea-Bissau"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xbe"), "flag: Guyana"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb0"), "flag: Hong Kong SAR China"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb2"), "flag: Heard & McDonald Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb3"), "flag: Honduras"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb7"), "flag: Croatia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb9"), "flag: Haiti"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xba"), "flag: Hungary"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xa8"), "flag: Canary Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xa9"), "flag: Indonesia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xaa"), "flag: Ireland"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb1"), "flag: Israel"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb2"), "flag: Isle of Man"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb3"), "flag: India"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb4"), + "flag: British Indian Ocean Territory"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb6"), "flag: Iraq"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb7"), "flag: Iran"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb8"), "flag: Iceland"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb9"), "flag: Italy"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xaa"), "flag: Jersey"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xb2"), "flag: Jamaica"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xb4"), "flag: Jordan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xb5"), "flag: Japan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xaa"), "flag: Kenya"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xac"), "flag: Kyrgyzstan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xad"), "flag: Cambodia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xae"), "flag: Kiribati"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb2"), "flag: Comoros"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb3"), "flag: St. Kitts & Nevis"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb5"), "flag: North Korea"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb7"), "flag: South Korea"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xbc"), "flag: Kuwait"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xbe"), "flag: Cayman Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xbf"), "flag: Kazakhstan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xa6"), "flag: Laos"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xa7"), "flag: Lebanon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xa8"), "flag: St. Lucia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xae"), "flag: Liechtenstein"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb0"), "flag: Sri Lanka"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb7"), "flag: Liberia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb8"), "flag: Lesotho"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb9"), "flag: Lithuania"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xba"), "flag: Luxembourg"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xbb"), "flag: Latvia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xbe"), "flag: Libya"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xa6"), "flag: Morocco"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xa8"), "flag: Monaco"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xa9"), "flag: Moldova"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xaa"), "flag: Montenegro"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xab"), "flag: St. Martin"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xac"), "flag: Madagascar"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xad"), "flag: Marshall Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb0"), "flag: North Macedonia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb1"), "flag: Mali"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb2"), "flag: Myanmar (Burma)"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb3"), "flag: Mongolia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb4"), "flag: Macao SAR China"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb5"), "flag: Northern Mariana Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb6"), "flag: Martinique"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb7"), "flag: Mauritania"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb8"), "flag: Montserrat"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb9"), "flag: Malta"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xba"), "flag: Mauritius"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbb"), "flag: Maldives"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbc"), "flag: Malawi"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbd"), "flag: Mexico"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbe"), "flag: Malaysia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbf"), "flag: Mozambique"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xa6"), "flag: Namibia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xa8"), "flag: New Caledonia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xaa"), "flag: Niger"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xab"), "flag: Norfolk Island"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xac"), "flag: Nigeria"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xae"), "flag: Nicaragua"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb1"), "flag: Netherlands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb4"), "flag: Norway"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb5"), "flag: Nepal"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb7"), "flag: Nauru"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xba"), "flag: Niue"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xbf"), "flag: New Zealand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb4\xf0\x9f\x87\xb2"), "flag: Oman"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xa6"), "flag: Panama"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xaa"), "flag: Peru"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xab"), "flag: French Polynesia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xac"), "flag: Papua New Guinea"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xad"), "flag: Philippines"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb0"), "flag: Pakistan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb1"), "flag: Poland"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb2"), "flag: St. Pierre & Miquelon"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb3"), "flag: Pitcairn Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb7"), "flag: Puerto Rico"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb8"), "flag: Palestinian Territories"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb9"), "flag: Portugal"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xbc"), "flag: Palau"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xbe"), "flag: Paraguay"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb6\xf0\x9f\x87\xa6"), "flag: Qatar"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xaa"), "flag: Réunion"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xb4"), "flag: Romania"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xb8"), "flag: Serbia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xba"), "flag: Russia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xbc"), "flag: Rwanda"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa6"), "flag: Saudi Arabia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa7"), "flag: Solomon Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa8"), "flag: Seychelles"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa9"), "flag: Sudan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xaa"), "flag: Sweden"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xac"), "flag: Singapore"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xad"), "flag: St. Helena"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xae"), "flag: Slovenia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xaf"), "flag: Svalbard & Jan Mayen"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb0"), "flag: Slovakia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb1"), "flag: Sierra Leone"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb2"), "flag: San Marino"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb3"), "flag: Senegal"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb4"), "flag: Somalia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb7"), "flag: Suriname"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb8"), "flag: South Sudan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb9"), "flag: São Tomé & Príncipe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbb"), "flag: El Salvador"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbd"), "flag: Sint Maarten"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbe"), "flag: Syria"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbf"), "flag: Eswatini"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xa6"), "flag: Tristan da Cunha"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xa8"), "flag: Turks & Caicos Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xa9"), "flag: Chad"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xab"), "flag: French Southern Territories"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xac"), "flag: Togo"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xad"), "flag: Thailand"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xaf"), "flag: Tajikistan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb0"), "flag: Tokelau"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb1"), "flag: Timor-Leste"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb2"), "flag: Turkmenistan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb3"), "flag: Tunisia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb4"), "flag: Tonga"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb7"), "flag: Turkey"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb9"), "flag: Trinidad & Tobago"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xbb"), "flag: Tuvalu"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xbc"), "flag: Taiwan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xbf"), "flag: Tanzania"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xa6"), "flag: Ukraine"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xac"), "flag: Uganda"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xb2"), "flag: U.S. Outlying Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xb3"), "flag: United Nations"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xb8"), "flag: United States"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xbe"), "flag: Uruguay"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xbf"), "flag: Uzbekistan"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xa6"), "flag: Vatican City"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xa8"), "flag: St. Vincent & Grenadines"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xaa"), "flag: Venezuela"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xac"), "flag: British Virgin Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xae"), "flag: U.S. Virgin Islands"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xb3"), "flag: Vietnam"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xba"), "flag: Vanuatu"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbc\xf0\x9f\x87\xab"), "flag: Wallis & Futuna"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbc\xf0\x9f\x87\xb8"), "flag: Samoa"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbd\xf0\x9f\x87\xb0"), "flag: Kosovo"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbe\xf0\x9f\x87\xaa"), "flag: Yemen"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbe\xf0\x9f\x87\xb9"), "flag: Mayotte"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbf\xf0\x9f\x87\xa6"), "flag: South Africa"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbf\xf0\x9f\x87\xb2"), "flag: Zambia"}, + Emoji{QString::fromUtf8("\xf0\x9f\x87\xbf\xf0\x9f\x87\xbc"), "flag: Zimbabwe"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb4\xf3\xa0\x81\xa7\xf3\xa0\x81\xa2\xf3\xa0\x81\xa5\xf3\xa0" + "\x81\xae\xf3\xa0\x81\xa7\xf3\xa0\x81\xbf"), + "flag: England"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb4\xf3\xa0\x81\xa7\xf3\xa0\x81\xa2\xf3\xa0\x81\xb3\xf3\xa0" + "\x81\xa3\xf3\xa0\x81\xb4\xf3\xa0\x81\xbf"), + "flag: Scotland"}, + Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb4\xf3\xa0\x81\xa7\xf3\xa0\x81\xa2\xf3\xa0\x81\xb7\xf3\xa0" + "\x81\xac\xf3\xa0\x81\xb3\xf3\xa0\x81\xbf"), + "flag: Wales"}, }; diff --git a/src/main.cpp b/src/main.cpp
index 0c196a33..042ef8c0 100644 --- a/src/main.cpp +++ b/src/main.cpp
@@ -15,16 +15,20 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <iostream> + #include <QApplication> #include <QCommandLineParser> #include <QDesktopWidget> #include <QDir> #include <QFile> #include <QFontDatabase> +#include <QGuiApplication> #include <QLabel> #include <QLibraryInfo> #include <QMessageBox> #include <QPoint> +#include <QScreen> #include <QSettings> #include <QStandardPaths> #include <QTranslator> @@ -33,17 +37,22 @@ #include "Logging.h" #include "MainWindow.h" #include "MatrixClient.h" -#include "RunGuard.h" #include "Utils.h" #include "config/nheko.h" +#include "singleapplication.h" #if defined(Q_OS_MAC) #include "emoji/MacHelper.h" #endif +#ifdef QML_DEBUGGING +#include <QQmlDebuggingEnabler> +QQmlDebuggingEnabler enabler; +#endif + #if defined(Q_OS_LINUX) #include <boost/stacktrace.hpp> -#include <signal.h> +#include <csignal> void stacktraceHandler(int signum) @@ -72,7 +81,8 @@ registerSignalHandlers() QPoint screenCenter(int width, int height) { - QRect screenGeometry = QApplication::desktop()->screenGeometry(); + // Deprecated in 5.13: QRect screenGeometry = QApplication::desktop()->screenGeometry(); + QRect screenGeometry = QGuiApplication::primaryScreen()->geometry(); int x = (screenGeometry.width() - width) / 2; int y = (screenGeometry.height() - height) / 2; @@ -94,18 +104,6 @@ createCacheDirectory() int main(int argc, char *argv[]) { - RunGuard guard("run_guard"); - - if (!guard.tryToRun()) { - QApplication a(argc, argv); - - QMessageBox msgBox; - msgBox.setText("Another instance of Nheko is running"); - msgBox.exec(); - - return 0; - } - #if defined(Q_OS_LINUX) || defined(Q_OS_WIN) || defined(Q_OS_FREEBSD) if (qgetenv("QT_SCALE_FACTOR").size() == 0) { float factor = utils::scaleFactor(); @@ -115,24 +113,23 @@ main(int argc, char *argv[]) } #endif - QApplication app(argc, argv); QCoreApplication::setApplicationName("nheko"); QCoreApplication::setApplicationVersion(nheko::version); QCoreApplication::setOrganizationName("nheko"); QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + SingleApplication app(argc, + argv, + false, + SingleApplication::Mode::User | + SingleApplication::Mode::ExcludeAppPath | + SingleApplication::Mode::ExcludeAppVersion); QCommandLineParser parser; parser.addHelpOption(); parser.addVersionOption(); parser.process(app); - QFontDatabase::addApplicationFont(":/fonts/fonts/OpenSans/OpenSans-Regular.ttf"); - QFontDatabase::addApplicationFont(":/fonts/fonts/OpenSans/OpenSans-Italic.ttf"); - QFontDatabase::addApplicationFont(":/fonts/fonts/OpenSans/OpenSans-Bold.ttf"); - QFontDatabase::addApplicationFont(":/fonts/fonts/OpenSans/OpenSans-Semibold.ttf"); - QFontDatabase::addApplicationFont(":/fonts/fonts/EmojiOne/emojione-android.ttf"); - app.setWindowIcon(QIcon(":/logos/nheko.png")); http::init(); @@ -188,6 +185,11 @@ main(int argc, char *argv[]) nhlog::net()->debug("bye"); } }); + QObject::connect(&app, &SingleApplication::instanceStarted, &w, [&w]() { + w.show(); + w.raise(); + w.activateWindow(); + }); #if defined(Q_OS_MAC) // Temporary solution for the emoji picker until diff --git a/src/notifications/ManagerLinux.cpp b/src/notifications/ManagerLinux.cpp
index d3901c52..1914f61c 100644 --- a/src/notifications/ManagerLinux.cpp +++ b/src/notifications/ManagerLinux.cpp
@@ -142,7 +142,11 @@ operator<<(QDBusArgument &arg, const QImage &image) int channels = i.isGrayscale() ? 1 : (i.hasAlphaChannel() ? 4 : 3); arg << i.depth() / channels; arg << channels; +#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) arg << QByteArray(reinterpret_cast<const char *>(i.bits()), i.byteCount()); +#else + arg << QByteArray(reinterpret_cast<const char *>(i.bits()), i.sizeInBytes()); +#endif arg.endStructure(); return arg; } diff --git a/src/popups/PopupItem.cpp b/src/popups/PopupItem.cpp new file mode 100644
index 00000000..db97e4a3 --- /dev/null +++ b/src/popups/PopupItem.cpp
@@ -0,0 +1,141 @@ +#include <QPaintEvent> +#include <QPainter> +#include <QStyleOption> + +#include "../Utils.h" +#include "../ui/Avatar.h" +#include "PopupItem.h" + +constexpr int PopupHMargin = 4; +constexpr int PopupItemMargin = 3; + +PopupItem::PopupItem(QWidget *parent) + : QWidget(parent) + , avatar_{new Avatar(this, conf::popup::avatar)} + , hovering_{false} +{ + setMouseTracking(true); + setAttribute(Qt::WA_Hover); + + topLayout_ = new QHBoxLayout(this); + topLayout_->setContentsMargins( + PopupHMargin, PopupItemMargin, PopupHMargin, PopupItemMargin); + + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); +} + +void +PopupItem::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); + + if (underMouse() || hovering_) + p.fillRect(rect(), hoverColor_); +} + +UserItem::UserItem(QWidget *parent) + : PopupItem(parent) +{ + userName_ = new QLabel("Placeholder", this); + avatar_->setLetter("P"); + topLayout_->addWidget(avatar_); + topLayout_->addWidget(userName_, 1); +} + +UserItem::UserItem(QWidget *parent, const QString &user_id) + : PopupItem(parent) + , userId_{user_id} +{ + auto displayName = cache::displayName(ChatPage::instance()->currentRoom(), userId_); + + avatar_->setLetter(utils::firstChar(displayName)); + + // If it's a matrix id we use the second letter. + if (displayName.size() > 1 && displayName.at(0) == '@') + avatar_->setLetter(QChar(displayName.at(1))); + + userName_ = new QLabel(displayName, this); + + topLayout_->addWidget(avatar_); + topLayout_->addWidget(userName_, 1); + + resolveAvatar(user_id); +} + +void +UserItem::updateItem(const QString &user_id) +{ + userId_ = user_id; + + auto displayName = cache::displayName(ChatPage::instance()->currentRoom(), userId_); + + // If it's a matrix id we use the second letter. + if (displayName.size() > 1 && displayName.at(0) == '@') + avatar_->setLetter(QChar(displayName.at(1))); + else + avatar_->setLetter(utils::firstChar(displayName)); + + userName_->setText(displayName); + resolveAvatar(user_id); +} + +void +UserItem::resolveAvatar(const QString &user_id) +{ + avatar_->setImage(ChatPage::instance()->currentRoom(), user_id); +} + +void +UserItem::mousePressEvent(QMouseEvent *event) +{ + if (event->buttons() != Qt::RightButton) + emit clicked( + cache::displayName(ChatPage::instance()->currentRoom(), selectedText())); + + QWidget::mousePressEvent(event); +} + +RoomItem::RoomItem(QWidget *parent, const RoomSearchResult &res) + : PopupItem(parent) + , roomId_{QString::fromStdString(res.room_id)} +{ + auto name = QFontMetrics(QFont()).elidedText( + QString::fromStdString(res.info.name), Qt::ElideRight, parentWidget()->width() - 10); + + avatar_->setLetter(utils::firstChar(name)); + + roomName_ = new QLabel(name, this); + roomName_->setMargin(0); + + topLayout_->addWidget(avatar_); + topLayout_->addWidget(roomName_, 1); + + avatar_->setImage(QString::fromStdString(res.info.avatar_url)); +} + +void +RoomItem::updateItem(const RoomSearchResult &result) +{ + roomId_ = QString::fromStdString(std::move(result.room_id)); + + auto name = + QFontMetrics(QFont()).elidedText(QString::fromStdString(std::move(result.info.name)), + Qt::ElideRight, + parentWidget()->width() - 10); + + roomName_->setText(name); + + avatar_->setImage(QString::fromStdString(result.info.avatar_url)); +} + +void +RoomItem::mousePressEvent(QMouseEvent *event) +{ + if (event->buttons() != Qt::RightButton) + emit clicked(selectedText()); + + QWidget::mousePressEvent(event); +} diff --git a/src/popups/PopupItem.h b/src/popups/PopupItem.h new file mode 100644
index 00000000..7a710fdb --- /dev/null +++ b/src/popups/PopupItem.h
@@ -0,0 +1,83 @@ +#pragma once + +#include <QHBoxLayout> +#include <QLabel> +#include <QPoint> +#include <QWidget> + +#include "../AvatarProvider.h" +#include "../ChatPage.h" + +class Avatar; +struct SearchResult; + +class PopupItem : public QWidget +{ + Q_OBJECT + + Q_PROPERTY(QColor hoverColor READ hoverColor WRITE setHoverColor) + Q_PROPERTY(bool hovering READ hovering WRITE setHovering) + +public: + PopupItem(QWidget *parent); + + QString selectedText() const { return QString(); } + QColor hoverColor() const { return hoverColor_; } + void setHoverColor(QColor &color) { hoverColor_ = color; } + + bool hovering() const { return hovering_; } + void setHovering(const bool hover) { hovering_ = hover; }; + +protected: + void paintEvent(QPaintEvent *event) override; + +signals: + void clicked(const QString &text); + +protected: + QHBoxLayout *topLayout_; + Avatar *avatar_; + QColor hoverColor_; + + //! Set if the item is currently being + //! hovered during tab completion (cycling). + bool hovering_; +}; + +class UserItem : public PopupItem +{ + Q_OBJECT + +public: + UserItem(QWidget *parent); + UserItem(QWidget *parent, const QString &user_id); + QString selectedText() const { return userId_; } + void updateItem(const QString &user_id); + +protected: + void mousePressEvent(QMouseEvent *event) override; + +private: + void resolveAvatar(const QString &user_id); + + QLabel *userName_; + QString userId_; +}; + +class RoomItem : public PopupItem +{ + Q_OBJECT + +public: + RoomItem(QWidget *parent, const RoomSearchResult &res); + QString selectedText() const { return roomId_; } + void updateItem(const RoomSearchResult &res); + +protected: + void mousePressEvent(QMouseEvent *event) override; + +private: + QLabel *roomName_; + QString roomId_; + RoomSearchResult info_; +}; diff --git a/src/popups/ReplyPopup.cpp b/src/popups/ReplyPopup.cpp new file mode 100644
index 00000000..5058c039 --- /dev/null +++ b/src/popups/ReplyPopup.cpp
@@ -0,0 +1,103 @@ +#include <QLabel> +#include <QPaintEvent> +#include <QPainter> +#include <QStyleOption> + +#include "../Config.h" +#include "../Utils.h" +#include "../ui/Avatar.h" +#include "../ui/DropShadow.h" +#include "../ui/TextLabel.h" +#include "PopupItem.h" +#include "ReplyPopup.h" + +ReplyPopup::ReplyPopup(QWidget *parent) + : QWidget(parent) + , userItem_{nullptr} + , msgLabel_{nullptr} + , eventLabel_{nullptr} +{ + setAttribute(Qt::WA_ShowWithoutActivating, true); + setWindowFlags(Qt::ToolTip | Qt::NoDropShadowWindowHint); + + mainLayout_ = new QVBoxLayout(this); + mainLayout_->setMargin(0); + mainLayout_->setSpacing(0); + + topLayout_ = new QHBoxLayout(); + topLayout_->setSpacing(0); + topLayout_->setContentsMargins(13, 1, 13, 0); + + userItem_ = new UserItem(this); + connect(userItem_, &UserItem::clicked, this, &ReplyPopup::userSelected); + topLayout_->addWidget(userItem_); + + buttonLayout_ = new QHBoxLayout(); + buttonLayout_->setSpacing(0); + buttonLayout_->setMargin(0); + + topLayout_->addLayout(buttonLayout_); + QFont f; + f.setPointSizeF(f.pointSizeF()); + const int fontHeight = QFontMetrics(f).height(); + buttonSize_ = std::min(fontHeight, 20); + + closeBtn_ = new FlatButton(this); + closeBtn_->setToolTip(tr("Logout")); + closeBtn_->setCornerRadius(buttonSize_ / 4); + closeBtn_->setText("X"); + + QIcon icon; + icon.addFile(":/icons/icons/ui/remove-symbol.png"); + + closeBtn_->setIcon(icon); + closeBtn_->setIconSize(QSize(buttonSize_, buttonSize_)); + connect(closeBtn_, &FlatButton::clicked, this, [this]() { emit cancel(); }); + + buttonLayout_->addWidget(closeBtn_); + + topLayout_->addLayout(buttonLayout_); + + mainLayout_->addLayout(topLayout_); + msgLabel_ = new TextLabel(this); + msgLabel_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); + mainLayout_->addWidget(msgLabel_); + eventLabel_ = new QLabel(this); + mainLayout_->addWidget(eventLabel_); + + setLayout(mainLayout_); +} + +void +ReplyPopup::setReplyContent(const RelatedInfo &related) +{ + // Update the current widget with the new data. + userItem_->updateItem(related.quoted_user); + + msgLabel_->setText(utils::getFormattedQuoteBody(related, "") + .replace("<mx-reply>", "") + .replace("</mx-reply>", "")); + + // eventLabel_->setText(srcEvent); + + adjustSize(); +} + +void +ReplyPopup::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +} + +void +ReplyPopup::mousePressEvent(QMouseEvent *event) +{ + if (event->buttons() != Qt::RightButton) { + emit clicked(eventLabel_->text()); + } + + QWidget::mousePressEvent(event); +} diff --git a/src/popups/ReplyPopup.h b/src/popups/ReplyPopup.h new file mode 100644
index 00000000..1fa3bb83 --- /dev/null +++ b/src/popups/ReplyPopup.h
@@ -0,0 +1,44 @@ +#pragma once + +#include <QHBoxLayout> +#include <QLabel> +#include <QVBoxLayout> +#include <QWidget> + +#include "../ui/FlatButton.h" +#include "../ui/TextLabel.h" + +struct RelatedInfo; +class UserItem; + +class ReplyPopup : public QWidget +{ + Q_OBJECT + +public: + explicit ReplyPopup(QWidget *parent = nullptr); + +public slots: + void setReplyContent(const RelatedInfo &related); + +protected: + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + +signals: + void userSelected(const QString &user); + void clicked(const QString &text); + void cancel(); + +private: + QHBoxLayout *topLayout_; + QVBoxLayout *mainLayout_; + QHBoxLayout *buttonLayout_; + + UserItem *userItem_; + FlatButton *closeBtn_; + TextLabel *msgLabel_; + QLabel *eventLabel_; + + int buttonSize_; +}; diff --git a/src/popups/SuggestionsPopup.cpp b/src/popups/SuggestionsPopup.cpp new file mode 100644
index 00000000..8f355b38 --- /dev/null +++ b/src/popups/SuggestionsPopup.cpp
@@ -0,0 +1,156 @@ +#include <QPaintEvent> +#include <QPainter> +#include <QStyleOption> + +#include "../Config.h" +#include "../Utils.h" +#include "../ui/Avatar.h" +#include "../ui/DropShadow.h" +#include "SuggestionsPopup.h" + +SuggestionsPopup::SuggestionsPopup(QWidget *parent) + : QWidget(parent) +{ + setAttribute(Qt::WA_ShowWithoutActivating, true); + setWindowFlags(Qt::ToolTip | Qt::NoDropShadowWindowHint); + + layout_ = new QVBoxLayout(this); + layout_->setMargin(0); + layout_->setSpacing(0); +} + +void +SuggestionsPopup::addRooms(const std::vector<RoomSearchResult> &rooms) +{ + if (rooms.empty()) { + hide(); + return; + } + + const size_t layoutCount = layout_->count(); + const size_t roomCount = rooms.size(); + + // Remove the extra widgets from the layout. + if (roomCount < layoutCount) + removeLayoutItemsAfter(roomCount - 1); + + for (size_t i = 0; i < roomCount; ++i) { + auto item = layout_->itemAt(i); + + // Create a new widget if there isn't already one in that + // layout position. + if (!item) { + auto room = new RoomItem(this, rooms.at(i)); + connect(room, &RoomItem::clicked, this, &SuggestionsPopup::itemSelected); + layout_->addWidget(room); + } else { + // Update the current widget with the new data. + auto room = qobject_cast<RoomItem *>(item->widget()); + if (room) + room->updateItem(rooms.at(i)); + } + } + + resetSelection(); + adjustSize(); + + resize(geometry().width(), 40 * rooms.size()); + + selectNextSuggestion(); +} + +void +SuggestionsPopup::addUsers(const std::vector<SearchResult> &users) +{ + if (users.empty()) { + hide(); + return; + } + + const size_t layoutCount = layout_->count(); + const size_t userCount = users.size(); + + // Remove the extra widgets from the layout. + if (userCount < layoutCount) + removeLayoutItemsAfter(userCount - 1); + + for (size_t i = 0; i < userCount; ++i) { + auto item = layout_->itemAt(i); + + // Create a new widget if there isn't already one in that + // layout position. + if (!item) { + auto user = new UserItem(this, users.at(i).user_id); + connect(user, &UserItem::clicked, this, &SuggestionsPopup::itemSelected); + layout_->addWidget(user); + } else { + // Update the current widget with the new data. + auto userWidget = qobject_cast<UserItem *>(item->widget()); + if (userWidget) + userWidget->updateItem(users.at(i).user_id); + } + } + + resetSelection(); + adjustSize(); + + selectNextSuggestion(); +} + +void +SuggestionsPopup::hoverSelection() +{ + resetHovering(); + setHovering(selectedItem_); + update(); +} + +void +SuggestionsPopup::selectNextSuggestion() +{ + selectedItem_++; + if (selectedItem_ >= layout_->count()) + selectFirstItem(); + + hoverSelection(); +} + +void +SuggestionsPopup::selectPreviousSuggestion() +{ + selectedItem_--; + if (selectedItem_ < 0) + selectLastItem(); + + hoverSelection(); +} + +void +SuggestionsPopup::resetHovering() +{ + for (int i = 0; i < layout_->count(); ++i) { + const auto item = qobject_cast<PopupItem *>(layout_->itemAt(i)->widget()); + + if (item) + item->setHovering(false); + } +} + +void +SuggestionsPopup::setHovering(int pos) +{ + const auto &item = layout_->itemAt(pos); + const auto &widget = qobject_cast<PopupItem *>(item->widget()); + + if (widget) + widget->setHovering(true); +} + +void +SuggestionsPopup::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +} diff --git a/src/popups/SuggestionsPopup.h b/src/popups/SuggestionsPopup.h new file mode 100644
index 00000000..63c44538 --- /dev/null +++ b/src/popups/SuggestionsPopup.h
@@ -0,0 +1,75 @@ +#pragma once + +#include <QHBoxLayout> +#include <QLabel> +#include <QPoint> +#include <QWidget> + +#include "CacheStructs.h" +#include "ChatPage.h" +#include "PopupItem.h" + +class SuggestionsPopup : public QWidget +{ + Q_OBJECT + +public: + explicit SuggestionsPopup(QWidget *parent = nullptr); + + template<class Item> + void selectHoveredSuggestion() + { + const auto item = layout_->itemAt(selectedItem_); + if (!item) + return; + + const auto &widget = qobject_cast<Item *>(item->widget()); + emit itemSelected( + cache::displayName(ChatPage::instance()->currentRoom(), widget->selectedText())); + + resetSelection(); + } + +public slots: + void addUsers(const std::vector<SearchResult> &users); + void addRooms(const std::vector<RoomSearchResult> &rooms); + + //! Move to the next available suggestion item. + void selectNextSuggestion(); + //! Move to the previous available suggestion item. + void selectPreviousSuggestion(); + //! Remove hovering from all items. + void resetHovering(); + //! Set hovering to the item in the given layout position. + void setHovering(int pos); + +protected: + void paintEvent(QPaintEvent *event) override; + +signals: + void itemSelected(const QString &user); + +private: + void hoverSelection(); + void resetSelection() { selectedItem_ = -1; } + void selectFirstItem() { selectedItem_ = 0; } + void selectLastItem() { selectedItem_ = layout_->count() - 1; } + void removeLayoutItemsAfter(size_t startingPos) + { + size_t posToRemove = layout_->count() - 1; + + QLayoutItem *item; + while (startingPos <= posToRemove && + (item = layout_->takeAt(posToRemove)) != nullptr) { + delete item->widget(); + delete item; + + posToRemove = layout_->count() - 1; + } + } + + QVBoxLayout *layout_; + + //! Counter for tab completion (cycling). + int selectedItem_ = -1; +}; diff --git a/src/popups/UserMentions.cpp b/src/popups/UserMentions.cpp new file mode 100644
index 00000000..2e70dbd3 --- /dev/null +++ b/src/popups/UserMentions.cpp
@@ -0,0 +1,171 @@ +#include <QPaintEvent> +#include <QPainter> +#include <QScrollArea> +#include <QStyleOption> +#include <QTabWidget> +#include <QTimer> +#include <QVBoxLayout> + +#include "Cache.h" +#include "ChatPage.h" +#include "Logging.h" +#include "UserMentions.h" +//#include "timeline/TimelineItem.h" + +using namespace popups; + +UserMentions::UserMentions(QWidget *parent) + : QWidget{parent} +{ + setAttribute(Qt::WA_ShowWithoutActivating, true); + setWindowFlags(Qt::FramelessWindowHint | Qt::Popup); + + tab_layout_ = new QTabWidget(this); + + top_layout_ = new QVBoxLayout(this); + top_layout_->setSpacing(0); + top_layout_->setMargin(0); + + local_scroll_area_ = new QScrollArea(this); + local_scroll_area_->setWidgetResizable(true); + local_scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + local_scroll_widget_ = new QWidget(this); + local_scroll_widget_->setObjectName("local_scroll_widget"); + + all_scroll_area_ = new QScrollArea(this); + all_scroll_area_->setWidgetResizable(true); + all_scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + all_scroll_widget_ = new QWidget(this); + all_scroll_widget_->setObjectName("all_scroll_widget"); + + // Height of the typing display. + QFont f; + f.setPointSizeF(f.pointSizeF() * 0.9); + const int bottomMargin = QFontMetrics(f).height() + 6; + + local_scroll_layout_ = new QVBoxLayout(local_scroll_widget_); + local_scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin); + local_scroll_layout_->setSpacing(0); + local_scroll_layout_->setObjectName("localscrollarea"); + + all_scroll_layout_ = new QVBoxLayout(all_scroll_widget_); + all_scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin); + all_scroll_layout_->setSpacing(0); + all_scroll_layout_->setObjectName("allscrollarea"); + + local_scroll_area_->setWidget(local_scroll_widget_); + local_scroll_area_->setAlignment(Qt::AlignBottom); + + all_scroll_area_->setWidget(all_scroll_widget_); + all_scroll_area_->setAlignment(Qt::AlignBottom); + + tab_layout_->addTab(local_scroll_area_, tr("This Room")); + tab_layout_->addTab(all_scroll_area_, tr("All Rooms")); + top_layout_->addWidget(tab_layout_); + + setLayout(top_layout_); +} + +void +UserMentions::initializeMentions(const QMap<QString, mtx::responses::Notifications> &notifs) +{ + nhlog::ui()->debug("Initializing " + std::to_string(notifs.size()) + " notifications."); + + for (const auto &item : notifs) { + for (const auto &notif : item.notifications) { + const auto event_id = QString::fromStdString(utils::event_id(notif.event)); + + try { + const auto room_id = QString::fromStdString(notif.room_id); + const auto user_id = utils::event_sender(notif.event); + const auto body = utils::event_body(notif.event); + + pushItem(event_id, + user_id, + body, + room_id, + ChatPage::instance()->currentRoom()); + + } catch (const lmdb::error &e) { + nhlog::db()->warn("error while sending desktop notification: {}", + e.what()); + } + } + } +} + +void +UserMentions::showPopup() +{ + for (auto widget : all_scroll_layout_->findChildren<QWidget *>()) { + delete widget; + } + for (auto widget : local_scroll_layout_->findChildren<QWidget *>()) { + delete widget; + } + + auto notifs = cache::getTimelineMentions(); + + initializeMentions(notifs); + show(); +} + +void +UserMentions::pushItem(const QString &event_id, + const QString &user_id, + const QString &body, + const QString &room_id, + const QString &current_room_id) +{ + (void)event_id; + (void)user_id; + (void)body; + (void)room_id; + (void)current_room_id; + // setUpdatesEnabled(false); + // + // // Add to the 'all' section + // TimelineItem *view_item = new TimelineItem( + // mtx::events::MessageType::Text, user_id, body, true, room_id, + // all_scroll_widget_); + // view_item->setEventId(event_id); + // view_item->hide(); + // + // all_scroll_layout_->addWidget(view_item); + // QTimer::singleShot(0, this, [view_item, this]() { + // view_item->show(); + // view_item->adjustSize(); + // setUpdatesEnabled(true); + // }); + // + // // if it matches the current room... add it to the current room as well. + // if (QString::compare(room_id, current_room_id, Qt::CaseInsensitive) == 0) { + // // Add to the 'local' section + // TimelineItem *local_view_item = new + // TimelineItem(mtx::events::MessageType::Text, + // user_id, + // body, + // true, + // room_id, + // local_scroll_widget_); + // local_view_item->setEventId(event_id); + // local_view_item->hide(); + // local_scroll_layout_->addWidget(local_view_item); + // + // QTimer::singleShot(0, this, [local_view_item]() { + // local_view_item->show(); + // local_view_item->adjustSize(); + // }); + // } +} + +void +UserMentions::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +} diff --git a/src/popups/UserMentions.h b/src/popups/UserMentions.h new file mode 100644
index 00000000..b7c4e51d --- /dev/null +++ b/src/popups/UserMentions.h
@@ -0,0 +1,45 @@ +#pragma once + +#include <mtx/responses.hpp> + +#include <QMap> +#include <QString> +#include <QWidget> + +class QPaintEvent; +class QTabWidget; +class QScrollArea; +class QVBoxLayout; + +namespace popups { + +class UserMentions : public QWidget +{ + Q_OBJECT +public: + UserMentions(QWidget *parent = nullptr); + + void initializeMentions(const QMap<QString, mtx::responses::Notifications> &notifs); + void showPopup(); + +protected: + void paintEvent(QPaintEvent *) override; + +private: + void pushItem(const QString &event_id, + const QString &user_id, + const QString &body, + const QString &room_id, + const QString &current_room_id); + QTabWidget *tab_layout_; + QVBoxLayout *top_layout_; + QVBoxLayout *local_scroll_layout_; + QVBoxLayout *all_scroll_layout_; + + QScrollArea *local_scroll_area_; + QWidget *local_scroll_widget_; + + QScrollArea *all_scroll_area_; + QWidget *all_scroll_widget_; +}; +} diff --git a/src/timeline/DelegateChooser.cpp b/src/timeline/DelegateChooser.cpp new file mode 100644
index 00000000..46ab6c0e --- /dev/null +++ b/src/timeline/DelegateChooser.cpp
@@ -0,0 +1,138 @@ +#include "DelegateChooser.h" + +#include "Logging.h" + +// uses private API, which moved between versions +#include <QQmlEngine> +#include <QtGlobal> + +QQmlComponent * +DelegateChoice::delegate() const +{ + return delegate_; +} + +void +DelegateChoice::setDelegate(QQmlComponent *delegate) +{ + if (delegate != delegate_) { + delegate_ = delegate; + emit delegateChanged(); + emit changed(); + } +} + +QVariant +DelegateChoice::roleValue() const +{ + return roleValue_; +} + +void +DelegateChoice::setRoleValue(const QVariant &value) +{ + if (value != roleValue_) { + roleValue_ = value; + emit roleValueChanged(); + emit changed(); + } +} + +QVariant +DelegateChooser::roleValue() const +{ + return roleValue_; +} + +void +DelegateChooser::setRoleValue(const QVariant &value) +{ + if (value != roleValue_) { + roleValue_ = value; + recalcChild(); + emit roleValueChanged(); + } +} + +QQmlListProperty<DelegateChoice> +DelegateChooser::choices() +{ + return QQmlListProperty<DelegateChoice>(this, + this, + &DelegateChooser::appendChoice, + &DelegateChooser::choiceCount, + &DelegateChooser::choice, + &DelegateChooser::clearChoices); +} + +void +DelegateChooser::appendChoice(QQmlListProperty<DelegateChoice> *p, DelegateChoice *c) +{ + DelegateChooser *dc = static_cast<DelegateChooser *>(p->object); + dc->choices_.append(c); +} + +int +DelegateChooser::choiceCount(QQmlListProperty<DelegateChoice> *p) +{ + return static_cast<DelegateChooser *>(p->object)->choices_.count(); +} +DelegateChoice * +DelegateChooser::choice(QQmlListProperty<DelegateChoice> *p, int index) +{ + return static_cast<DelegateChooser *>(p->object)->choices_.at(index); +} +void +DelegateChooser::clearChoices(QQmlListProperty<DelegateChoice> *p) +{ + static_cast<DelegateChooser *>(p->object)->choices_.clear(); +} + +void +DelegateChooser::recalcChild() +{ + for (const auto choice : qAsConst(choices_)) { + auto choiceValue = choice->roleValue(); + if (!roleValue_.isValid() || !choiceValue.isValid() || choiceValue == roleValue_) { + if (child) { + child->setParentItem(nullptr); + child = nullptr; + } + + choice->delegate()->create(incubator, QQmlEngine::contextForObject(this)); + return; + } + } +} + +void +DelegateChooser::componentComplete() +{ + QQuickItem::componentComplete(); + recalcChild(); +} + +void +DelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status) +{ + if (status == QQmlIncubator::Ready) { + chooser.child = dynamic_cast<QQuickItem *>(object()); + if (chooser.child == nullptr) { + nhlog::ui()->error("Delegate has to be derived of Item!"); + return; + } + + chooser.child->setParentItem(&chooser); + connect(chooser.child, &QQuickItem::heightChanged, &chooser, [this]() { + chooser.setHeight(chooser.child->height()); + }); + chooser.setHeight(chooser.child->height()); + QQmlEngine::setObjectOwnership(chooser.child, + QQmlEngine::ObjectOwnership::JavaScriptOwnership); + + } else if (status == QQmlIncubator::Error) { + for (const auto &e : errors()) + nhlog::ui()->error("Error instantiating delegate: {}", + e.toString().toStdString()); + } +} diff --git a/src/timeline/DelegateChooser.h b/src/timeline/DelegateChooser.h new file mode 100644
index 00000000..68ebeb04 --- /dev/null +++ b/src/timeline/DelegateChooser.h
@@ -0,0 +1,82 @@ +// A DelegateChooser like the one, that was added to Qt5.12 (in labs), but compatible with older Qt +// versions see KDE/kquickitemviews see qtdeclarative/qqmldelagatecomponent + +#pragma once + +#include <QQmlComponent> +#include <QQmlIncubator> +#include <QQmlListProperty> +#include <QQuickItem> +#include <QtCore/QObject> +#include <QtCore/QVariant> + +class QQmlAdaptorModel; + +class DelegateChoice : public QObject +{ + Q_OBJECT + Q_CLASSINFO("DefaultProperty", "delegate") + +public: + Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) + Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) + + QQmlComponent *delegate() const; + void setDelegate(QQmlComponent *delegate); + + QVariant roleValue() const; + void setRoleValue(const QVariant &value); + +signals: + void delegateChanged(); + void roleValueChanged(); + void changed(); + +private: + QVariant roleValue_; + QQmlComponent *delegate_ = nullptr; +}; + +class DelegateChooser : public QQuickItem +{ + Q_OBJECT + Q_CLASSINFO("DefaultProperty", "choices") + +public: + Q_PROPERTY(QQmlListProperty<DelegateChoice> choices READ choices CONSTANT) + Q_PROPERTY(QVariant roleValue READ roleValue WRITE setRoleValue NOTIFY roleValueChanged) + + QQmlListProperty<DelegateChoice> choices(); + + QVariant roleValue() const; + void setRoleValue(const QVariant &value); + + void recalcChild(); + void componentComplete() override; + +signals: + void roleChanged(); + void roleValueChanged(); + +private: + struct DelegateIncubator : public QQmlIncubator + { + DelegateIncubator(DelegateChooser &parent) + : QQmlIncubator(QQmlIncubator::AsynchronousIfNested) + , chooser(parent) + {} + void statusChanged(QQmlIncubator::Status status) override; + + DelegateChooser &chooser; + }; + + QVariant roleValue_; + QList<DelegateChoice *> choices_; + QQuickItem *child = nullptr; + DelegateIncubator incubator{*this}; + + static void appendChoice(QQmlListProperty<DelegateChoice> *, DelegateChoice *); + static int choiceCount(QQmlListProperty<DelegateChoice> *); + static DelegateChoice *choice(QQmlListProperty<DelegateChoice> *, int index); + static void clearChoices(QQmlListProperty<DelegateChoice> *); +}; diff --git a/src/timeline/TimelineItem.cpp b/src/timeline/TimelineItem.cpp deleted file mode 100644
index d23dbf49..00000000 --- a/src/timeline/TimelineItem.cpp +++ /dev/null
@@ -1,918 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -#include <functional> - -#include <QContextMenuEvent> -#include <QDesktopServices> -#include <QFontDatabase> -#include <QMenu> -#include <QTimer> - -#include "ChatPage.h" -#include "Config.h" -#include "Logging.h" -#include "MainWindow.h" -#include "Olm.h" -#include "ui/Avatar.h" -#include "ui/Painter.h" -#include "ui/TextLabel.h" - -#include "timeline/TimelineItem.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" - -#include "dialogs/RawMessage.h" -#include "mtx/identifiers.hpp" - -constexpr int MSG_RIGHT_MARGIN = 7; -constexpr int MSG_PADDING = 20; - -StatusIndicator::StatusIndicator(QWidget *parent) - : QWidget(parent) -{ - lockIcon_.addFile(":/icons/icons/ui/lock.png"); - clockIcon_.addFile(":/icons/icons/ui/clock.png"); - checkmarkIcon_.addFile(":/icons/icons/ui/checkmark.png"); - doubleCheckmarkIcon_.addFile(":/icons/icons/ui/double-tick-indicator.png"); -} - -void -StatusIndicator::paintIcon(QPainter &p, QIcon &icon) -{ - auto pixmap = icon.pixmap(width()); - - QPainter painter(&pixmap); - painter.setCompositionMode(QPainter::CompositionMode_SourceIn); - painter.fillRect(pixmap.rect(), p.pen().color()); - - QIcon(pixmap).paint(&p, rect(), Qt::AlignCenter, QIcon::Normal); -} - -void -StatusIndicator::paintEvent(QPaintEvent *) -{ - if (state_ == StatusIndicatorState::Empty) - return; - - Painter p(this); - PainterHighQualityEnabler hq(p); - - p.setPen(iconColor_); - - switch (state_) { - case StatusIndicatorState::Sent: { - paintIcon(p, clockIcon_); - break; - } - case StatusIndicatorState::Encrypted: - paintIcon(p, lockIcon_); - break; - case StatusIndicatorState::Received: { - paintIcon(p, checkmarkIcon_); - break; - } - case StatusIndicatorState::Read: { - paintIcon(p, doubleCheckmarkIcon_); - break; - } - case StatusIndicatorState::Empty: - break; - } -} - -void -StatusIndicator::setState(StatusIndicatorState state) -{ - state_ = state; - - switch (state) { - case StatusIndicatorState::Encrypted: - setToolTip(tr("Encrypted")); - break; - case StatusIndicatorState::Received: - setToolTip(tr("Delivered")); - break; - case StatusIndicatorState::Read: - setToolTip(tr("Seen")); - break; - case StatusIndicatorState::Sent: - setToolTip(tr("Sent")); - break; - case StatusIndicatorState::Empty: - setToolTip(""); - break; - } - - update(); -} - -void -TimelineItem::adjustMessageLayoutForWidget() -{ - messageLayout_->addLayout(widgetLayout_, 1); - messageLayout_->addWidget(statusIndicator_); - messageLayout_->addWidget(timestamp_); - - messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop); - messageLayout_->setAlignment(timestamp_, Qt::AlignTop); - - mainLayout_->addLayout(messageLayout_); -} - -void -TimelineItem::adjustMessageLayout() -{ - messageLayout_->addWidget(body_, 1); - messageLayout_->addWidget(statusIndicator_); - messageLayout_->addWidget(timestamp_); - - messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop); - messageLayout_->setAlignment(timestamp_, Qt::AlignTop); - - mainLayout_->addLayout(messageLayout_); -} - -void -TimelineItem::init() -{ - userAvatar_ = nullptr; - timestamp_ = nullptr; - userName_ = nullptr; - body_ = nullptr; - - contextMenu_ = new QMenu(this); - showReadReceipts_ = new QAction("Read receipts", this); - markAsRead_ = new QAction("Mark as read", this); - viewRawMessage_ = new QAction("View raw message", this); - redactMsg_ = new QAction("Redact message", this); - contextMenu_->addAction(showReadReceipts_); - contextMenu_->addAction(viewRawMessage_); - contextMenu_->addAction(markAsRead_); - contextMenu_->addAction(redactMsg_); - - connect(showReadReceipts_, &QAction::triggered, this, [this]() { - if (!event_id_.isEmpty()) - MainWindow::instance()->openReadReceiptsDialog(event_id_); - }); - - connect(this, &TimelineItem::eventRedacted, this, [this](const QString &event_id) { - emit ChatPage::instance()->removeTimelineEvent(room_id_, event_id); - }); - connect(this, &TimelineItem::redactionFailed, this, [](const QString &msg) { - emit ChatPage::instance()->showNotification(msg); - }); - connect(redactMsg_, &QAction::triggered, this, [this]() { - if (!event_id_.isEmpty()) - http::client()->redact_event( - room_id_.toStdString(), - event_id_.toStdString(), - [this](const mtx::responses::EventId &, mtx::http::RequestErr err) { - if (err) { - emit redactionFailed(tr("Message redaction failed: %1") - .arg(QString::fromStdString( - err->matrix_error.error))); - return; - } - - emit eventRedacted(event_id_); - }); - }); - connect( - ChatPage::instance(), &ChatPage::themeChanged, this, &TimelineItem::refreshAuthorColor); - connect(markAsRead_, &QAction::triggered, this, &TimelineItem::sendReadReceipt); - connect(viewRawMessage_, &QAction::triggered, this, &TimelineItem::openRawMessageViewer); - - colorGenerating_ = new QFutureWatcher<QString>(this); - connect(colorGenerating_, - &QFutureWatcher<QString>::finished, - this, - &TimelineItem::finishedGeneratingColor); - - topLayout_ = new QHBoxLayout(this); - mainLayout_ = new QVBoxLayout; - messageLayout_ = new QHBoxLayout; - messageLayout_->setContentsMargins(0, 0, MSG_RIGHT_MARGIN, 0); - messageLayout_->setSpacing(MSG_PADDING); - - topLayout_->setContentsMargins( - conf::timeline::msgLeftMargin, conf::timeline::msgTopMargin, 0, 0); - topLayout_->setSpacing(0); - topLayout_->addLayout(mainLayout_); - - mainLayout_->setContentsMargins(conf::timeline::headerLeftMargin, 0, 0, 0); - mainLayout_->setSpacing(0); - - timestampFont_.setPointSizeF(timestampFont_.pointSizeF() * 0.9); - timestampFont_.setFamily("Monospace"); - timestampFont_.setStyleHint(QFont::Monospace); - - QFontMetrics tsFm(timestampFont_); - - statusIndicator_ = new StatusIndicator(this); - statusIndicator_->setFixedWidth(tsFm.height() - tsFm.leading()); - statusIndicator_->setFixedHeight(tsFm.height() - tsFm.leading()); - - parentWidget()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); - setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); -} - -/* - * For messages created locally. - */ -TimelineItem::TimelineItem(mtx::events::MessageType ty, - const QString &userid, - QString body, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - auto displayName = Cache::displayName(room_id_, userid); - auto timestamp = QDateTime::currentDateTime(); - - // Generate the html body to be rendered. - auto formatted_body = utils::markdownToHtml(body); - - // Escape html if the input is not formatted. - if (formatted_body == body.trimmed().toHtmlEscaped()) - formatted_body = body.toHtmlEscaped(); - - QString emptyEventId; - - if (ty == mtx::events::MessageType::Emote) { - formatted_body = QString("<em>%1</em>").arg(formatted_body); - descriptionMsg_ = {emptyEventId, - "", - userid, - QString("* %1 %2").arg(displayName).arg(body), - utils::descriptiveTime(timestamp), - timestamp}; - } else { - descriptionMsg_ = {emptyEventId, - "You: ", - userid, - body, - utils::descriptiveTime(timestamp), - timestamp}; - } - - formatted_body = utils::linkifyMessage(formatted_body); - - generateTimestamp(timestamp); - - if (withSender) { - generateBody(userid, displayName, formatted_body); - setupAvatarLayout(displayName); - - AvatarProvider::resolve( - room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -TimelineItem::TimelineItem(ImageItem *image, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout<ImageItem>(image, userid, withSender); - - addSaveImageAction(image); -} - -TimelineItem::TimelineItem(FileItem *file, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout<FileItem>(file, userid, withSender); -} - -TimelineItem::TimelineItem(AudioItem *audio, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout<AudioItem>(audio, userid, withSender); -} - -TimelineItem::TimelineItem(VideoItem *video, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent) - : QWidget{parent} - , room_id_{room_id} -{ - init(); - - setupLocalWidgetLayout<VideoItem>(video, userid, withSender); -} - -TimelineItem::TimelineItem(ImageItem *image, - const mtx::events::RoomEvent<mtx::events::msg::Image> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Image>, ImageItem>( - image, event, with_sender); - - markOwnMessagesAsReceived(event.sender); - - addSaveImageAction(image); -} - -TimelineItem::TimelineItem(StickerItem *image, - const mtx::events::Sticker &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - setupWidgetLayout<mtx::events::Sticker, StickerItem>(image, event, with_sender); - - markOwnMessagesAsReceived(event.sender); - - addSaveImageAction(image); -} - -TimelineItem::TimelineItem(FileItem *file, - const mtx::events::RoomEvent<mtx::events::msg::File> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::File>, FileItem>( - file, event, with_sender); - - markOwnMessagesAsReceived(event.sender); -} - -TimelineItem::TimelineItem(AudioItem *audio, - const mtx::events::RoomEvent<mtx::events::msg::Audio> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Audio>, AudioItem>( - audio, event, with_sender); - - markOwnMessagesAsReceived(event.sender); -} - -TimelineItem::TimelineItem(VideoItem *video, - const mtx::events::RoomEvent<mtx::events::msg::Video> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Video>, VideoItem>( - video, event, with_sender); - - markOwnMessagesAsReceived(event.sender); -} - -/* - * Used to display remote notice messages. - */ -TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - markOwnMessagesAsReceived(event.sender); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - - auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed()); - auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped(); - - descriptionMsg_ = {event_id_, - Cache::displayName(room_id_, sender), - sender, - " sent a notification", - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - if (with_sender) { - auto displayName = Cache::displayName(room_id_, sender); - - generateBody(sender, displayName, formatted_body); - setupAvatarLayout(displayName); - - AvatarProvider::resolve( - room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -/* - * Used to display remote emote messages. - */ -TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - markOwnMessagesAsReceived(event.sender); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed()); - auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped(); - - auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = Cache::displayName(room_id_, sender); - formatted_body = QString("<em>%1</em>").arg(formatted_body); - - descriptionMsg_ = {event_id_, - "", - sender, - QString("* %1 %2").arg(displayName).arg(body), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - if (with_sender) { - generateBody(sender, displayName, formatted_body); - setupAvatarLayout(displayName); - - AvatarProvider::resolve( - room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -/* - * Used to display remote text messages. - */ -TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text> &event, - bool with_sender, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - addReplyAction(); - - markOwnMessagesAsReceived(event.sender); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed()); - auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped(); - - auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = Cache::displayName(room_id_, sender); - - QSettings settings; - descriptionMsg_ = {event_id_, - sender == settings.value("auth/user_id") ? "You" : displayName, - sender, - QString(": %1").arg(body), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - if (with_sender) { - generateBody(sender, displayName, formatted_body); - setupAvatarLayout(displayName); - - AvatarProvider::resolve( - room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); - } else { - generateBody(formatted_body); - setupSimpleLayout(); - } - - adjustMessageLayout(); -} - -TimelineItem::~TimelineItem() -{ - colorGenerating_->cancel(); - colorGenerating_->waitForFinished(); -} - -void -TimelineItem::markSent() -{ - statusIndicator_->setState(StatusIndicatorState::Sent); -} - -void -TimelineItem::markOwnMessagesAsReceived(const std::string &sender) -{ - QSettings settings; - if (sender == settings.value("auth/user_id").toString().toStdString()) - statusIndicator_->setState(StatusIndicatorState::Received); -} - -void -TimelineItem::markRead() -{ - if (statusIndicator_->state() != StatusIndicatorState::Encrypted) - statusIndicator_->setState(StatusIndicatorState::Read); -} - -void -TimelineItem::markReceived(bool isEncrypted) -{ - isReceived_ = true; - - if (isEncrypted) - statusIndicator_->setState(StatusIndicatorState::Encrypted); - else - statusIndicator_->setState(StatusIndicatorState::Received); - - sendReadReceipt(); -} - -// Only the body is displayed. -void -TimelineItem::generateBody(const QString &body) -{ - body_ = new TextLabel(replaceEmoji(body), this); - body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); - - connect(body_, &TextLabel::userProfileTriggered, this, [](const QString &user_id) { - MainWindow::instance()->openUserProfile(user_id, - ChatPage::instance()->currentRoom()); - }); -} - -void -TimelineItem::refreshAuthorColor() -{ - // Cancel and wait if we are already generating the color. - if (colorGenerating_->isRunning()) { - colorGenerating_->cancel(); - colorGenerating_->waitForFinished(); - } - if (userName_) { - // generate user's unique color. - std::function<QString()> generate = [this]() { - QString userColor = utils::generateContrastingHexColor( - userName_->toolTip(), backgroundColor().name()); - return userColor; - }; - - QString userColor = Cache::userColor(userName_->toolTip()); - - // If the color is empty, then generate it asynchronously - if (userColor.isEmpty()) { - colorGenerating_->setFuture(QtConcurrent::run(generate)); - } else { - userName_->setStyleSheet("QLabel { color : " + userColor + "; }"); - } - } -} - -void -TimelineItem::finishedGeneratingColor() -{ - nhlog::ui()->debug("finishedGeneratingColor for: {}", userName_->toolTip().toStdString()); - QString userColor = colorGenerating_->result(); - - if (!userColor.isEmpty()) { - // another TimelineItem might have inserted in the meantime. - if (Cache::userColor(userName_->toolTip()).isEmpty()) { - Cache::insertUserColor(userName_->toolTip(), userColor); - } - userName_->setStyleSheet("QLabel { color : " + userColor + "; }"); - } -} -// The username/timestamp is displayed along with the message body. -void -TimelineItem::generateBody(const QString &user_id, const QString &displayname, const QString &body) -{ - generateUserName(user_id, displayname); - generateBody(body); -} - -void -TimelineItem::generateUserName(const QString &user_id, const QString &displayname) -{ - auto sender = displayname; - - if (displayname.startsWith("@")) { - // TODO: Fix this by using a UserId type. - if (displayname.split(":")[0].split("@").size() > 1) - sender = displayname.split(":")[0].split("@")[1]; - } - - QFont usernameFont; - usernameFont.setPointSizeF(usernameFont.pointSizeF() * 1.1); - usernameFont.setWeight(QFont::Medium); - - QFontMetrics fm(usernameFont); - - userName_ = new QLabel(this); - userName_->setFont(usernameFont); - userName_->setText(fm.elidedText(sender, Qt::ElideRight, 500)); - userName_->setToolTip(user_id); - userName_->setToolTipDuration(1500); - userName_->setAttribute(Qt::WA_Hover); - userName_->setAlignment(Qt::AlignLeft | Qt::AlignTop); - userName_->setFixedWidth(QFontMetrics(userName_->font()).width(userName_->text())); - - // Set the user color asynchronously if it hasn't been generated yet, - // otherwise this will just set it. - refreshAuthorColor(); - - auto filter = new UserProfileFilter(user_id, userName_); - userName_->installEventFilter(filter); - userName_->setCursor(Qt::PointingHandCursor); - - connect(filter, &UserProfileFilter::hoverOn, this, [this]() { - QFont f = userName_->font(); - f.setUnderline(true); - userName_->setFont(f); - }); - - connect(filter, &UserProfileFilter::hoverOff, this, [this]() { - QFont f = userName_->font(); - f.setUnderline(false); - userName_->setFont(f); - }); - - connect(filter, &UserProfileFilter::clicked, this, [this, user_id]() { - MainWindow::instance()->openUserProfile(user_id, room_id_); - }); -} - -void -TimelineItem::generateTimestamp(const QDateTime &time) -{ - timestamp_ = new QLabel(this); - timestamp_->setFont(timestampFont_); - timestamp_->setText( - QString("<span style=\"color: #999\"> %1 </span>").arg(time.toString("HH:mm"))); -} - -QString -TimelineItem::replaceEmoji(const QString &body) -{ - QString fmtBody = ""; - - QVector<uint> utf32_string = body.toUcs4(); - - for (auto &code : utf32_string) { - // TODO: Be more precise here. - if (code > 9000) - fmtBody += QString("<span style=\"font-family: emoji;\">") + - QString::fromUcs4(&code, 1) + "</span>"; - else - fmtBody += QString::fromUcs4(&code, 1); - } - - return fmtBody; -} - -void -TimelineItem::setupAvatarLayout(const QString &userName) -{ - topLayout_->setContentsMargins( - conf::timeline::msgLeftMargin, conf::timeline::msgAvatarTopMargin, 0, 0); - - QFont f; - f.setPointSizeF(f.pointSizeF()); - - userAvatar_ = new Avatar(this); - userAvatar_->setLetter(QChar(userName[0]).toUpper()); - userAvatar_->setSize(QFontMetrics(f).height() * 2); - - // TODO: The provided user name should be a UserId class - if (userName[0] == '@' && userName.size() > 1) - userAvatar_->setLetter(QChar(userName[1]).toUpper()); - - topLayout_->insertWidget(0, userAvatar_); - topLayout_->setAlignment(userAvatar_, Qt::AlignTop | Qt::AlignLeft); - - if (userName_) - mainLayout_->insertWidget(0, userName_, Qt::AlignTop | Qt::AlignLeft); -} - -void -TimelineItem::setupSimpleLayout() -{ - QFont f; - f.setPointSizeF(f.pointSizeF()); - - topLayout_->setContentsMargins(conf::timeline::msgLeftMargin + - QFontMetrics(f).height() * 2 + 2, - conf::timeline::msgTopMargin, - 0, - 0); -} - -void -TimelineItem::setUserAvatar(const QImage &avatar) -{ - if (userAvatar_ == nullptr) - return; - - userAvatar_->setImage(avatar); -} - -void -TimelineItem::contextMenuEvent(QContextMenuEvent *event) -{ - if (contextMenu_) - contextMenu_->exec(event->globalPos()); -} - -void -TimelineItem::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -TimelineItem::addSaveImageAction(ImageItem *image) -{ - if (contextMenu_) { - auto saveImage = new QAction("Save image", this); - contextMenu_->addAction(saveImage); - - connect(saveImage, &QAction::triggered, image, &ImageItem::saveAs); - } -} - -void -TimelineItem::addReplyAction() -{ - if (contextMenu_) { - auto replyAction = new QAction("Reply", this); - contextMenu_->addAction(replyAction); - - connect(replyAction, &QAction::triggered, this, [this]() { - if (!body_) - return; - - emit ChatPage::instance()->messageReply( - Cache::displayName(room_id_, descriptionMsg_.userid), - body_->toPlainText()); - }); - } -} - -void -TimelineItem::addKeyRequestAction() -{ - if (contextMenu_) { - auto requestKeys = new QAction("Request encryption keys", this); - contextMenu_->addAction(requestKeys); - - connect(requestKeys, &QAction::triggered, this, [this]() { - olm::request_keys(room_id_.toStdString(), event_id_.toStdString()); - }); - } -} - -void -TimelineItem::addAvatar() -{ - if (userAvatar_) - return; - - // TODO: should be replaced with the proper event struct. - auto userid = descriptionMsg_.userid; - auto displayName = Cache::displayName(room_id_, userid); - - generateUserName(userid, displayName); - - setupAvatarLayout(displayName); - - AvatarProvider::resolve( - room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); -} - -void -TimelineItem::sendReadReceipt() const -{ - if (!event_id_.isEmpty()) - http::client()->read_event(room_id_.toStdString(), - event_id_.toStdString(), - [this](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to read_event ({}, {})", - room_id_.toStdString(), - event_id_.toStdString()); - } - }); -} - -void -TimelineItem::openRawMessageViewer() const -{ - const auto event_id = event_id_.toStdString(); - const auto room_id = room_id_.toStdString(); - - auto proxy = std::make_shared<EventProxy>(); - connect(proxy.get(), &EventProxy::eventRetrieved, this, [](const nlohmann::json &obj) { - auto dialog = new dialogs::RawMessage{QString::fromStdString(obj.dump(4))}; - Q_UNUSED(dialog); - }); - - http::client()->get_event( - room_id, - event_id, - [event_id, room_id, proxy = std::move(proxy)]( - const mtx::events::collections::TimelineEvents &res, mtx::http::RequestErr err) { - using namespace mtx::events; - - if (err) { - nhlog::net()->warn( - "failed to retrieve event {} from {}", event_id, room_id); - return; - } - - try { - emit proxy->eventRetrieved(utils::serialize_event(res)); - } catch (const nlohmann::json::exception &e) { - nhlog::net()->warn( - "failed to serialize event ({}, {})", room_id, event_id); - } - }); -} \ No newline at end of file diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h deleted file mode 100644
index 7bf6a076..00000000 --- a/src/timeline/TimelineItem.h +++ /dev/null
@@ -1,377 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#pragma once - -#include <QApplication> -#include <QDateTime> -#include <QHBoxLayout> -#include <QLabel> -#include <QLayout> -#include <QPainter> -#include <QSettings> -#include <QTimer> - -#include <QtConcurrent> - -#include "AvatarProvider.h" -#include "RoomInfoListItem.h" -#include "Utils.h" - -#include "Cache.h" -#include "MatrixClient.h" - -class ImageItem; -class StickerItem; -class AudioItem; -class VideoItem; -class FileItem; -class Avatar; -class TextLabel; - -enum class StatusIndicatorState -{ - //! The encrypted message was received by the server. - Encrypted, - //! The plaintext message was received by the server. - Received, - //! At least one of the participants has read the message. - Read, - //! The client sent the message. Not yet received. - Sent, - //! When the message is loaded from cache or backfill. - Empty, -}; - -//! -//! Used to notify the user about the status of a message. -//! -class StatusIndicator : public QWidget -{ - Q_OBJECT - -public: - explicit StatusIndicator(QWidget *parent); - void setState(StatusIndicatorState state); - StatusIndicatorState state() const { return state_; } - -protected: - void paintEvent(QPaintEvent *event) override; - -private: - void paintIcon(QPainter &p, QIcon &icon); - - QIcon lockIcon_; - QIcon clockIcon_; - QIcon checkmarkIcon_; - QIcon doubleCheckmarkIcon_; - - QColor iconColor_ = QColor("#999"); - - StatusIndicatorState state_ = StatusIndicatorState::Empty; - - static constexpr int MaxWidth = 24; -}; - -class EventProxy : public QObject -{ - Q_OBJECT - -signals: - void eventRetrieved(const nlohmann::json &); -}; - -class UserProfileFilter : public QObject -{ - Q_OBJECT - -public: - explicit UserProfileFilter(const QString &user_id, QLabel *parent) - : QObject(parent) - , user_id_{user_id} - {} - -signals: - void hoverOff(); - void hoverOn(); - void clicked(); - -protected: - bool eventFilter(QObject *obj, QEvent *event) - { - if (event->type() == QEvent::MouseButtonRelease) { - emit clicked(); - return true; - } else if (event->type() == QEvent::HoverLeave) { - emit hoverOff(); - return true; - } else if (event->type() == QEvent::HoverEnter) { - emit hoverOn(); - return true; - } - - return QObject::eventFilter(obj, event); - } - -private: - QString user_id_; -}; - -class TimelineItem : public QWidget -{ - Q_OBJECT - Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor) - -public: - TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text> &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> &e, - bool with_sender, - const QString &room_id, - QWidget *parent = 0); - - // For local messages. - // m.text & m.emote - TimelineItem(mtx::events::MessageType ty, - const QString &userid, - QString body, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - // m.image - TimelineItem(ImageItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(FileItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(AudioItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - TimelineItem(VideoItem *item, - const QString &userid, - bool withSender, - const QString &room_id, - QWidget *parent = 0); - - TimelineItem(ImageItem *img, - const mtx::events::RoomEvent<mtx::events::msg::Image> &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(StickerItem *img, - const mtx::events::Sticker &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(FileItem *file, - const mtx::events::RoomEvent<mtx::events::msg::File> &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(AudioItem *audio, - const mtx::events::RoomEvent<mtx::events::msg::Audio> &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - TimelineItem(VideoItem *video, - const mtx::events::RoomEvent<mtx::events::msg::Video> &e, - bool with_sender, - const QString &room_id, - QWidget *parent); - - ~TimelineItem(); - - void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } - QColor backgroundColor() const { return backgroundColor_; } - - void setUserAvatar(const QImage &pixmap); - DescInfo descriptionMessage() const { return descriptionMsg_; } - QString eventId() const { return event_id_; } - void setEventId(const QString &event_id) { event_id_ = event_id; } - void markReceived(bool isEncrypted); - void markRead(); - void markSent(); - bool isReceived() { return isReceived_; }; - void setRoomId(QString room_id) { room_id_ = room_id; } - void sendReadReceipt() const; - void openRawMessageViewer() const; - - //! Add a user avatar for this event. - void addAvatar(); - void addKeyRequestAction(); - -signals: - void eventRedacted(const QString &event_id); - void redactionFailed(const QString &msg); - -public slots: - void refreshAuthorColor(); - void finishedGeneratingColor(); - -protected: - void paintEvent(QPaintEvent *event) override; - void contextMenuEvent(QContextMenuEvent *event) override; - -private: - //! If we are the sender of the message the event wil be marked as received by the server. - void markOwnMessagesAsReceived(const std::string &sender); - void init(); - //! Add a context menu option to save the image of the timeline item. - void addSaveImageAction(ImageItem *image); - //! Add the reply action in the context menu for widgets that support it. - void addReplyAction(); - - template<class Widget> - void setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender); - - template<class Event, class Widget> - void setupWidgetLayout(Widget *widget, const Event &event, bool withSender); - - void generateBody(const QString &body); - void generateBody(const QString &user_id, const QString &displayname, const QString &body); - void generateTimestamp(const QDateTime &time); - void generateUserName(const QString &userid, const QString &displayname); - - void setupAvatarLayout(const QString &userName); - void setupSimpleLayout(); - - void adjustMessageLayout(); - void adjustMessageLayoutForWidget(); - - //! Whether or not the event associated with the widget - //! has been acknowledged by the server. - bool isReceived_ = false; - - QFutureWatcher<QString> *colorGenerating_; - - QString replaceEmoji(const QString &body); - QString event_id_; - QString room_id_; - - DescInfo descriptionMsg_; - - QMenu *contextMenu_; - QAction *showReadReceipts_; - QAction *markAsRead_; - QAction *redactMsg_; - QAction *viewRawMessage_; - QAction *replyMsg_; - - QHBoxLayout *topLayout_ = nullptr; - QHBoxLayout *messageLayout_ = nullptr; - QVBoxLayout *mainLayout_ = nullptr; - QHBoxLayout *widgetLayout_ = nullptr; - - Avatar *userAvatar_; - - QFont timestampFont_; - - StatusIndicator *statusIndicator_; - - QLabel *timestamp_; - QLabel *userName_; - TextLabel *body_; - - QColor backgroundColor_; -}; - -template<class Widget> -void -TimelineItem::setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender) -{ - auto displayName = Cache::displayName(room_id_, userid); - auto timestamp = QDateTime::currentDateTime(); - - descriptionMsg_ = {"", // No event_id up until this point. - "You", - userid, - QString(" %1").arg(utils::messageDescription<Widget>()), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - widgetLayout_ = new QHBoxLayout; - widgetLayout_->setContentsMargins(0, 2, 0, 2); - widgetLayout_->addWidget(widget); - widgetLayout_->addStretch(1); - - if (withSender) { - generateBody(userid, displayName, ""); - setupAvatarLayout(displayName); - - AvatarProvider::resolve( - room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); - } else { - setupSimpleLayout(); - } - - adjustMessageLayoutForWidget(); -} - -template<class Event, class Widget> -void -TimelineItem::setupWidgetLayout(Widget *widget, const Event &event, bool withSender) -{ - init(); - - event_id_ = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); - auto displayName = Cache::displayName(room_id_, sender); - - QSettings settings; - descriptionMsg_ = {event_id_, - sender == settings.value("auth/user_id") ? "You" : displayName, - sender, - QString(" %1").arg(utils::messageDescription<Widget>()), - utils::descriptiveTime(timestamp), - timestamp}; - - generateTimestamp(timestamp); - - widgetLayout_ = new QHBoxLayout(); - widgetLayout_->setContentsMargins(0, 2, 0, 2); - widgetLayout_->addWidget(widget); - widgetLayout_->addStretch(1); - - if (withSender) { - generateBody(sender, displayName, ""); - setupAvatarLayout(displayName); - - AvatarProvider::resolve( - room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); - } else { - setupSimpleLayout(); - } - - adjustMessageLayoutForWidget(); -} diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp new file mode 100644
index 00000000..cad39bc5 --- /dev/null +++ b/src/timeline/TimelineModel.cpp
@@ -0,0 +1,1570 @@ +#include "TimelineModel.h" + +#include <algorithm> +#include <thread> +#include <type_traits> + +#include <QFileDialog> +#include <QMimeDatabase> +#include <QRegularExpression> +#include <QStandardPaths> + +#include "ChatPage.h" +#include "EventAccessors.h" +#include "Logging.h" +#include "MainWindow.h" +#include "MatrixClient.h" +#include "MxcImageProvider.h" +#include "Olm.h" +#include "TimelineViewManager.h" +#include "Utils.h" +#include "dialogs/RawMessage.h" + +Q_DECLARE_METATYPE(QModelIndex) + +namespace { +struct RoomEventType +{ + template<class T> + qml_mtx_events::EventType operator()(const mtx::events::Event<T> &e) + { + using mtx::events::EventType; + switch (e.type) { + case EventType::RoomKeyRequest: + return qml_mtx_events::EventType::KeyRequest; + case EventType::RoomAliases: + return qml_mtx_events::EventType::Aliases; + case EventType::RoomAvatar: + return qml_mtx_events::EventType::Avatar; + case EventType::RoomCanonicalAlias: + return qml_mtx_events::EventType::CanonicalAlias; + case EventType::RoomCreate: + return qml_mtx_events::EventType::Create; + case EventType::RoomEncrypted: + return qml_mtx_events::EventType::Encrypted; + case EventType::RoomEncryption: + return qml_mtx_events::EventType::Encryption; + case EventType::RoomGuestAccess: + return qml_mtx_events::EventType::GuestAccess; + case EventType::RoomHistoryVisibility: + return qml_mtx_events::EventType::HistoryVisibility; + case EventType::RoomJoinRules: + return qml_mtx_events::EventType::JoinRules; + case EventType::RoomMember: + return qml_mtx_events::EventType::Member; + case EventType::RoomMessage: + return qml_mtx_events::EventType::UnknownMessage; + case EventType::RoomName: + return qml_mtx_events::EventType::Name; + case EventType::RoomPowerLevels: + return qml_mtx_events::EventType::PowerLevels; + case EventType::RoomTopic: + return qml_mtx_events::EventType::Topic; + case EventType::RoomTombstone: + return qml_mtx_events::EventType::Tombstone; + case EventType::RoomRedaction: + return qml_mtx_events::EventType::Redaction; + case EventType::RoomPinnedEvents: + return qml_mtx_events::EventType::PinnedEvents; + case EventType::Sticker: + return qml_mtx_events::EventType::Sticker; + case EventType::Tag: + return qml_mtx_events::EventType::Tag; + case EventType::Unsupported: + return qml_mtx_events::EventType::Unsupported; + default: + return qml_mtx_events::EventType::UnknownMessage; + } + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Audio> &) + { + return qml_mtx_events::EventType::AudioMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Emote> &) + { + return qml_mtx_events::EventType::EmoteMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::File> &) + { + return qml_mtx_events::EventType::FileMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Image> &) + { + return qml_mtx_events::EventType::ImageMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Notice> &) + { + return qml_mtx_events::EventType::NoticeMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Text> &) + { + return qml_mtx_events::EventType::TextMessage; + } + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Video> &) + { + return qml_mtx_events::EventType::VideoMessage; + } + + qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Redacted> &) + { + return qml_mtx_events::EventType::Redacted; + } + // ::EventType::Type operator()(const Event<mtx::events::msg::Location> &e) { return + // ::EventType::LocationMessage; } +}; +} + +qml_mtx_events::EventType +toRoomEventType(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit(RoomEventType{}, event); +} + +QString +toRoomEventTypeString(const mtx::events::collections::TimelineEvents &event) +{ + return std::visit([](const auto &e) { return QString::fromStdString(to_string(e.type)); }, + event); +} + +TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent) + : QAbstractListModel(parent) + , room_id_(room_id) + , manager_(manager) +{ + connect( + this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents); + connect(this, &TimelineModel::messageFailed, this, [this](QString txn_id) { + pending.removeOne(txn_id); + failed.insert(txn_id); + int idx = idToIndex(txn_id); + if (idx < 0) { + nhlog::ui()->warn("Failed index out of range"); + return; + } + isProcessingPending = false; + emit dataChanged(index(idx, 0), index(idx, 0)); + }); + connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) { + pending.removeOne(txn_id); + + int idx = idToIndex(txn_id); + if (idx < 0) { + // transaction already received via sync + return; + } + eventOrder[idx] = event_id; + auto ev = events.value(txn_id); + ev = std::visit( + [event_id](const auto &e) -> mtx::events::collections::TimelineEvents { + auto eventCopy = e; + eventCopy.event_id = event_id.toStdString(); + return eventCopy; + }, + ev); + + events.remove(txn_id); + events.insert(event_id, ev); + + // mark our messages as read + readEvent(event_id.toStdString()); + + // ask to be notified for read receipts + cache::addPendingReceipt(room_id_, event_id); + + isProcessingPending = false; + emit dataChanged(index(idx, 0), index(idx, 0)); + + if (pending.size() > 0) + emit nextPendingMessage(); + }); + connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) { + emit ChatPage::instance()->showNotification(msg); + }); + + connect( + this, &TimelineModel::nextPendingMessage, this, &TimelineModel::processOnePendingMessage); + connect(this, &TimelineModel::newMessageToSend, this, &TimelineModel::addPendingMessage); + + connect(this, + &TimelineModel::eventFetched, + this, + [this](QString requestingEvent, mtx::events::collections::TimelineEvents event) { + events.insert(QString::fromStdString(mtx::accessors::event_id(event)), + event); + auto idx = idToIndex(requestingEvent); + if (idx >= 0) + emit dataChanged(index(idx, 0), index(idx, 0)); + }); +} + +QHash<int, QByteArray> +TimelineModel::roleNames() const +{ + return { + {Section, "section"}, + {Type, "type"}, + {TypeString, "typeString"}, + {Body, "body"}, + {FormattedBody, "formattedBody"}, + {UserId, "userId"}, + {UserName, "userName"}, + {Timestamp, "timestamp"}, + {Url, "url"}, + {ThumbnailUrl, "thumbnailUrl"}, + {Filename, "filename"}, + {Filesize, "filesize"}, + {MimeType, "mimetype"}, + {Height, "height"}, + {Width, "width"}, + {ProportionalHeight, "proportionalHeight"}, + {Id, "id"}, + {State, "state"}, + {IsEncrypted, "isEncrypted"}, + {ReplyTo, "replyTo"}, + {RoomName, "roomName"}, + {RoomTopic, "roomTopic"}, + {Dump, "dump"}, + }; +} +int +TimelineModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return (int)this->eventOrder.size(); +} + +QVariantMap +TimelineModel::getDump(QString eventId) const +{ + if (events.contains(eventId)) + return data(eventId, Dump).toMap(); + return {}; +} + +QVariant +TimelineModel::data(const QString &id, int role) const +{ + using namespace mtx::accessors; + namespace acc = mtx::accessors; + mtx::events::collections::TimelineEvents event = events.value(id); + + if (auto e = + std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) { + event = decryptEvent(*e).event; + } + + switch (role) { + case UserId: + return QVariant(QString::fromStdString(acc::sender(event))); + case UserName: + return QVariant(displayName(QString::fromStdString(acc::sender(event)))); + + case Timestamp: + return QVariant(origin_server_ts(event)); + case Type: + return QVariant(toRoomEventType(event)); + case TypeString: + return QVariant(toRoomEventTypeString(event)); + case Body: + return QVariant(utils::replaceEmoji(QString::fromStdString(body(event)))); + case FormattedBody: { + const static QRegularExpression replyFallback( + "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption); + + bool isReply = !in_reply_to_event(event).empty(); + + auto formattedBody_ = QString::fromStdString(formatted_body(event)); + if (formattedBody_.isEmpty()) { + auto body_ = QString::fromStdString(body(event)); + + if (isReply) { + while (body_.startsWith("> ")) + body_ = body_.right(body_.size() - body_.indexOf('\n') - 1); + if (body_.startsWith('\n')) + body_ = body_.right(body_.size() - 1); + } + formattedBody_ = body_.toHtmlEscaped().replace('\n', "<br>"); + } else { + if (isReply) + formattedBody_ = formattedBody_.remove(replyFallback); + } + return QVariant(utils::replaceEmoji( + utils::linkifyMessage(utils::escapeBlacklistedHtml(formattedBody_)))); + } + case Url: + return QVariant(QString::fromStdString(url(event))); + case ThumbnailUrl: + return QVariant(QString::fromStdString(thumbnail_url(event))); + case Filename: + return QVariant(QString::fromStdString(filename(event))); + case Filesize: + return QVariant(utils::humanReadableFileSize(filesize(event))); + case MimeType: + return QVariant(QString::fromStdString(mimetype(event))); + case Height: + return QVariant(qulonglong{media_height(event)}); + case Width: + return QVariant(qulonglong{media_width(event)}); + case ProportionalHeight: { + auto w = media_width(event); + if (w == 0) + w = 1; + + double prop = media_height(event) / (double)w; + + return QVariant(prop > 0 ? prop : 1.); + } + case Id: + return id; + case State: + // only show read receipts for messages not from us + if (acc::sender(event) != http::client()->user_id().to_string()) + return qml_mtx_events::Empty; + else if (failed.contains(id)) + return qml_mtx_events::Failed; + else if (pending.contains(id)) + return qml_mtx_events::Sent; + else if (read.contains(id) || cache::readReceipts(id, room_id_).size() > 1) + return qml_mtx_events::Read; + else + return qml_mtx_events::Received; + case IsEncrypted: { + return std::holds_alternative< + mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(events[id]); + } + case ReplyTo: + return QVariant(QString::fromStdString(in_reply_to_event(event))); + case RoomName: + return QVariant(QString::fromStdString(room_name(event))); + case RoomTopic: + return QVariant(QString::fromStdString(room_topic(event))); + case Dump: { + QVariantMap m; + auto names = roleNames(); + + // m.insert(names[Section], data(id, static_cast<int>(Section))); + m.insert(names[Type], data(id, static_cast<int>(Type))); + m.insert(names[TypeString], data(id, static_cast<int>(TypeString))); + m.insert(names[Body], data(id, static_cast<int>(Body))); + m.insert(names[FormattedBody], data(id, static_cast<int>(FormattedBody))); + m.insert(names[UserId], data(id, static_cast<int>(UserId))); + m.insert(names[UserName], data(id, static_cast<int>(UserName))); + m.insert(names[Timestamp], data(id, static_cast<int>(Timestamp))); + m.insert(names[Url], data(id, static_cast<int>(Url))); + m.insert(names[ThumbnailUrl], data(id, static_cast<int>(ThumbnailUrl))); + m.insert(names[Filename], data(id, static_cast<int>(Filename))); + m.insert(names[Filesize], data(id, static_cast<int>(Filesize))); + m.insert(names[MimeType], data(id, static_cast<int>(MimeType))); + m.insert(names[Height], data(id, static_cast<int>(Height))); + m.insert(names[Width], data(id, static_cast<int>(Width))); + m.insert(names[ProportionalHeight], data(id, static_cast<int>(ProportionalHeight))); + m.insert(names[Id], data(id, static_cast<int>(Id))); + m.insert(names[State], data(id, static_cast<int>(State))); + m.insert(names[IsEncrypted], data(id, static_cast<int>(IsEncrypted))); + m.insert(names[ReplyTo], data(id, static_cast<int>(ReplyTo))); + m.insert(names[RoomName], data(id, static_cast<int>(RoomName))); + m.insert(names[RoomTopic], data(id, static_cast<int>(RoomTopic))); + + return QVariant(m); + } + default: + return QVariant(); + } +} + +QVariant +TimelineModel::data(const QModelIndex &index, int role) const +{ + using namespace mtx::accessors; + namespace acc = mtx::accessors; + if (index.row() < 0 && index.row() >= (int)eventOrder.size()) + return QVariant(); + + QString id = eventOrder[index.row()]; + + mtx::events::collections::TimelineEvents event = events.value(id); + + if (role == Section) { + QDateTime date = origin_server_ts(event); + date.setTime(QTime()); + + std::string userId = acc::sender(event); + + for (size_t r = index.row() + 1; r < eventOrder.size(); r++) { + auto tempEv = events.value(eventOrder[r]); + QDateTime prevDate = origin_server_ts(tempEv); + prevDate.setTime(QTime()); + if (prevDate != date) + return QString("%2 %1") + .arg(date.toMSecsSinceEpoch()) + .arg(QString::fromStdString(userId)); + + std::string prevUserId = acc::sender(tempEv); + if (userId != prevUserId) + break; + } + + return QString("%1").arg(QString::fromStdString(userId)); + } + + return data(id, role); +} + +bool +TimelineModel::canFetchMore(const QModelIndex &) const +{ + if (eventOrder.empty()) + return true; + if (!std::holds_alternative<mtx::events::StateEvent<mtx::events::state::Create>>( + events[eventOrder.back()])) + return true; + else + + return false; +} + +void +TimelineModel::fetchMore(const QModelIndex &) +{ + if (paginationInProgress) { + nhlog::ui()->warn("Already loading older messages"); + return; + } + + paginationInProgress = true; + mtx::http::MessagesOpts opts; + opts.room_id = room_id_.toStdString(); + opts.from = prev_batch_token_.toStdString(); + + nhlog::ui()->debug("Paginating room {}", opts.room_id); + + http::client()->messages( + opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("failed to call /messages ({}): {} - {}", + opts.room_id, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + paginationInProgress = false; + return; + } + + emit oldMessagesRetrieved(std::move(res)); + paginationInProgress = false; + }); +} + +void +TimelineModel::addEvents(const mtx::responses::Timeline &timeline) +{ + if (isInitialSync) { + prev_batch_token_ = QString::fromStdString(timeline.prev_batch); + isInitialSync = false; + } + + if (timeline.events.empty()) + return; + + std::vector<QString> ids = internalAddEvents(timeline.events); + + if (ids.empty()) + return; + + beginInsertRows(QModelIndex(), 0, static_cast<int>(ids.size() - 1)); + this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend()); + endInsertRows(); + + updateLastMessage(); +} + +template<typename T> +auto +isMessage(const mtx::events::RoomEvent<T> &e) + -> std::enable_if_t<std::is_same<decltype(e.content.msgtype), std::string>::value, bool> +{ + return true; +} + +template<typename T> +auto +isMessage(const mtx::events::Event<T> &) +{ + return false; +} + +void +TimelineModel::updateLastMessage() +{ + for (auto it = eventOrder.begin(); it != eventOrder.end(); ++it) { + auto event = events.value(*it); + if (auto e = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>( + &event)) { + event = decryptEvent(*e).event; + } + + if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, event)) + continue; + + auto description = utils::getMessageDescription( + event, QString::fromStdString(http::client()->user_id().to_string()), room_id_); + emit manager_->updateRoomsLastMessage(room_id_, description); + return; + } +} + +std::vector<QString> +TimelineModel::internalAddEvents( + const std::vector<mtx::events::collections::TimelineEvents> &timeline) +{ + std::vector<QString> ids; + for (auto e : timeline) { + QString id = QString::fromStdString(mtx::accessors::event_id(e)); + + if (this->events.contains(id)) { + this->events.insert(id, e); + int idx = idToIndex(id); + emit dataChanged(index(idx, 0), index(idx, 0)); + continue; + } + + QString txid = QString::fromStdString(mtx::accessors::transaction_id(e)); + if (this->pending.removeOne(txid)) { + this->events.insert(id, e); + this->events.remove(txid); + int idx = idToIndex(txid); + if (idx < 0) { + nhlog::ui()->warn("Received index out of range"); + continue; + } + eventOrder[idx] = id; + emit dataChanged(index(idx, 0), index(idx, 0)); + continue; + } + + if (auto redaction = + std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(&e)) { + QString redacts = QString::fromStdString(redaction->redacts); + auto redacted = std::find(eventOrder.begin(), eventOrder.end(), redacts); + + if (redacted != eventOrder.end()) { + auto redactedEvent = std::visit( + [](const auto &ev) + -> mtx::events::RoomEvent<mtx::events::msg::Redacted> { + mtx::events::RoomEvent<mtx::events::msg::Redacted> + replacement = {}; + replacement.event_id = ev.event_id; + replacement.room_id = ev.room_id; + replacement.sender = ev.sender; + replacement.origin_server_ts = ev.origin_server_ts; + replacement.type = ev.type; + return replacement; + }, + e); + events.insert(redacts, redactedEvent); + + int row = (int)std::distance(eventOrder.begin(), redacted); + emit dataChanged(index(row, 0), index(row, 0)); + } + + continue; // don't insert redaction into timeline + } + + if (auto event = + std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&e)) { + e = decryptEvent(*event).event; + } + auto encInfo = mtx::accessors::file(e); + + if (encInfo) + emit newEncryptedImage(encInfo.value()); + + this->events.insert(id, e); + ids.push_back(id); + + auto replyTo = mtx::accessors::in_reply_to_event(e); + auto qReplyTo = QString::fromStdString(replyTo); + if (!replyTo.empty() && !events.contains(qReplyTo)) { + http::client()->get_event( + this->room_id_.toStdString(), + replyTo, + [this, id, replyTo]( + const mtx::events::collections::TimelineEvents &timeline, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error( + "Failed to retrieve event with id {}, which was " + "requested to show the replyTo for event {}", + replyTo, + id.toStdString()); + return; + } + emit eventFetched(id, timeline); + }); + } + } + return ids; +} + +void +TimelineModel::setCurrentIndex(int index) +{ + auto oldIndex = idToIndex(currentId); + currentId = indexToId(index); + emit currentIndexChanged(index); + + if ((oldIndex > index || oldIndex == -1) && !pending.contains(currentId) && + ChatPage::instance()->isActiveWindow()) { + readEvent(currentId.toStdString()); + } +} + +void +TimelineModel::readEvent(const std::string &id) +{ + http::client()->read_event(room_id_.toStdString(), id, [this](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to read_event ({}, {})", + room_id_.toStdString(), + currentId.toStdString()); + } + }); +} + +void +TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs) +{ + std::vector<QString> ids = internalAddEvents(msgs.chunk); + + if (!ids.empty()) { + beginInsertRows(QModelIndex(), + static_cast<int>(this->eventOrder.size()), + static_cast<int>(this->eventOrder.size() + ids.size() - 1)); + this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end()); + endInsertRows(); + } + + prev_batch_token_ = QString::fromStdString(msgs.end); +} + +QString +TimelineModel::displayName(QString id) const +{ + return cache::displayName(room_id_, id).toHtmlEscaped(); +} + +QString +TimelineModel::avatarUrl(QString id) const +{ + return cache::avatarUrl(room_id_, id); +} + +QString +TimelineModel::formatDateSeparator(QDate date) const +{ + auto now = QDateTime::currentDateTime(); + + QString fmt = QLocale::system().dateFormat(QLocale::LongFormat); + + if (now.date().year() == date.year()) { + QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*"); + fmt = fmt.remove(rx); + } + + return date.toString(fmt); +} + +QString +TimelineModel::escapeEmoji(QString str) const +{ + return utils::replaceEmoji(str); +} + +void +TimelineModel::viewRawMessage(QString id) const +{ + std::string ev = utils::serialize_event(events.value(id)).dump(4); + auto dialog = new dialogs::RawMessage(QString::fromStdString(ev)); + Q_UNUSED(dialog); +} + +void + +TimelineModel::openUserProfile(QString userid) const +{ + MainWindow::instance()->openUserProfile(userid, room_id_); +} + +DecryptionResult +TimelineModel::decryptEvent(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) const +{ + MegolmSessionIndex index; + index.room_id = room_id_.toStdString(); + index.session_id = e.content.session_id; + index.sender_key = e.content.sender_key; + + mtx::events::RoomEvent<mtx::events::msg::Notice> dummy; + dummy.origin_server_ts = e.origin_server_ts; + dummy.event_id = e.event_id; + dummy.sender = e.sender; + dummy.content.body = + tr("-- Encrypted Event (No keys found for decryption) --", + "Placeholder, when the message was not decrypted yet or can't be decrypted") + .toStdString(); + + try { + if (!cache::inboundMegolmSessionExists(index)) { + nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", + index.room_id, + index.session_id, + e.sender); + // TODO: request megolm session_id & session_key from the sender. + return {dummy, false}; + } + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to check megolm session's existence: {}", e.what()); + dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --", + "Placeholder, when the message can't be decrypted, because " + "the DB access failed when trying to lookup the session.") + .toStdString(); + return {dummy, false}; + } + + std::string msg_str; + try { + auto session = cache::getInboundMegolmSession(index); + auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); + msg_str = std::string((char *)res.data.data(), res.data.size()); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", + index.room_id, + index.session_id, + index.sender_key, + e.what()); + dummy.content.body = + tr("-- Decryption Error (failed to retrieve megolm keys from db) --", + "Placeholder, when the message can't be decrypted, because the DB access " + "failed.") + .toStdString(); + return {dummy, false}; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", + index.room_id, + index.session_id, + index.sender_key, + e.what()); + dummy.content.body = + tr("-- Decryption Error (%1) --", + "Placeholder, when the message can't be decrypted. In this case, the Olm " + "decrytion returned an error, which is passed ad %1") + .arg(e.what()) + .toStdString(); + return {dummy, false}; + } + + // Add missing fields for the event. + json body = json::parse(msg_str); + body["event_id"] = e.event_id; + body["sender"] = e.sender; + body["origin_server_ts"] = e.origin_server_ts; + body["unsigned"] = e.unsigned_data; + + // relations are unencrypted in content... + if (json old_ev = e; old_ev["content"].count("m.relates_to") != 0) + body["content"]["m.relates_to"] = old_ev["content"]["m.relates_to"]; + + json event_array = json::array(); + event_array.push_back(body); + + std::vector<mtx::events::collections::TimelineEvents> temp_events; + mtx::responses::utils::parse_timeline_events(event_array, temp_events); + + if (temp_events.size() == 1) + return {temp_events.at(0), true}; + + dummy.content.body = + tr("-- Encrypted Event (Unknown event type) --", + "Placeholder, when the message was decrypted, but we couldn't parse it, because " + "Nheko/mtxclient don't support that event type yet") + .toStdString(); + return {dummy, false}; +} + +void +TimelineModel::replyAction(QString id) +{ + auto event = events.value(id); + if (auto e = + std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) { + event = decryptEvent(*e).event; + } + + RelatedInfo related = {}; + related.quoted_user = QString::fromStdString(mtx::accessors::sender(event)); + related.related_event = mtx::accessors::event_id(event); + related.type = mtx::accessors::msg_type(event); + related.quoted_body = QString::fromStdString(mtx::accessors::body(event)); + related.quoted_body = utils::getQuoteBody(related); + related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event); + related.quoted_formatted_body.remove(QRegularExpression( + "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption)); + nhlog::ui()->debug("after replacement: {}", related.quoted_body.toStdString()); + related.room = room_id_; + + ChatPage::instance()->messageReply(related); +} + +void +TimelineModel::readReceiptsAction(QString id) const +{ + MainWindow::instance()->openReadReceiptsDialog(id); +} + +void +TimelineModel::redactEvent(QString id) +{ + if (!id.isEmpty()) + http::client()->redact_event( + room_id_.toStdString(), + id.toStdString(), + [this, id](const mtx::responses::EventId &, mtx::http::RequestErr err) { + if (err) { + emit redactionFailed( + tr("Message redaction failed: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit eventRedacted(id); + }); +} + +int +TimelineModel::idToIndex(QString id) const +{ + if (id.isEmpty()) + return -1; + for (int i = 0; i < (int)eventOrder.size(); i++) + if (id == eventOrder[i]) + return i; + return -1; +} + +QString +TimelineModel::indexToId(int index) const +{ + if (index < 0 || index >= (int)eventOrder.size()) + return ""; + return eventOrder[index]; +} + +// Note: this will only be called for our messages +void +TimelineModel::markEventsAsRead(const std::vector<QString> &event_ids) +{ + for (const auto &id : event_ids) { + read.insert(id); + int idx = idToIndex(id); + if (idx < 0) { + nhlog::ui()->warn("Read index out of range"); + return; + } + emit dataChanged(index(idx, 0), index(idx, 0)); + } +} + +void +TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json content) +{ + const auto room_id = room_id_.toStdString(); + + using namespace mtx::events; + using namespace mtx::identifiers; + + json doc = {{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}}; + + try { + // Check if we have already an outbound megolm session then we can use. + if (cache::outboundMegolmSessionExists(room_id)) { + auto data = + olm::encrypt_group_message(room_id, http::client()->device_id(), doc); + + http::client()->send_room_message<msg::Encrypted, EventType::RoomEncrypted>( + room_id, + txn_id, + data, + [this, txn_id](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = + static_cast<int>(err->status_code); + nhlog::net()->warn("[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); + emit messageFailed(QString::fromStdString(txn_id)); + } + emit messageSent( + QString::fromStdString(txn_id), + QString::fromStdString(res.event_id.to_string())); + }); + return; + } + + nhlog::ui()->debug("creating new outbound megolm session"); + + // Create a new outbound megolm session. + auto outbound_session = olm::client()->init_outbound_group_session(); + const auto session_id = mtx::crypto::session_id(outbound_session.get()); + const auto session_key = mtx::crypto::session_key(outbound_session.get()); + + // TODO: needs to be moved in the lib. + auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, + {"room_id", room_id}, + {"session_id", session_id}, + {"session_key", session_key}}; + + // Saving the new megolm session. + // TODO: Maybe it's too early to save. + OutboundGroupSessionData session_data; + session_data.session_id = session_id; + session_data.session_key = session_key; + session_data.message_index = 0; // TODO Update me + cache::saveOutboundMegolmSession( + room_id, session_data, std::move(outbound_session)); + + const auto members = cache::roomMembers(room_id); + nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); + + auto keeper = + std::make_shared<StateKeeper>([megolm_payload, room_id, doc, txn_id, this]() { + try { + auto data = olm::encrypt_group_message( + room_id, http::client()->device_id(), doc); + + http::client() + ->send_room_message<msg::Encrypted, EventType::RoomEncrypted>( + room_id, + txn_id, + data, + [this, txn_id](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = + static_cast<int>(err->status_code); + nhlog::net()->warn( + "[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); + emit messageFailed( + QString::fromStdString(txn_id)); + } + emit messageSent( + QString::fromStdString(txn_id), + QString::fromStdString(res.event_id.to_string())); + }); + } catch (const lmdb::error &e) { + nhlog::db()->critical( + "failed to save megolm outbound session: {}", e.what()); + emit messageFailed(QString::fromStdString(txn_id)); + } + }); + + mtx::requests::QueryKeys req; + for (const auto &member : members) + req.device_keys[member] = {}; + + http::client()->query_keys( + req, + [keeper = std::move(keeper), megolm_payload, txn_id, this]( + const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to query device keys: {} {}", + err->matrix_error.error, + static_cast<int>(err->status_code)); + // TODO: Mark the event as failed. Communicate with the UI. + emit messageFailed(QString::fromStdString(txn_id)); + return; + } + + for (const auto &user : res.device_keys) { + // Mapping from a device_id with valid identity keys to the + // generated room_key event used for sharing the megolm session. + std::map<std::string, std::string> room_key_msgs; + std::map<std::string, DevicePublicKeys> deviceKeys; + + room_key_msgs.clear(); + deviceKeys.clear(); + + for (const auto &dev : user.second) { + const auto user_id = ::UserId(dev.second.user_id); + const auto device_id = DeviceId(dev.second.device_id); + + const auto device_keys = dev.second.keys; + const auto curveKey = "curve25519:" + device_id.get(); + const auto edKey = "ed25519:" + device_id.get(); + + if ((device_keys.find(curveKey) == device_keys.end()) || + (device_keys.find(edKey) == device_keys.end())) { + nhlog::net()->debug( + "ignoring malformed keys for device {}", + device_id.get()); + continue; + } + + DevicePublicKeys pks; + pks.ed25519 = device_keys.at(edKey); + pks.curve25519 = device_keys.at(curveKey); + + try { + if (!mtx::crypto::verify_identity_signature( + json(dev.second), device_id, user_id)) { + nhlog::crypto()->warn( + "failed to verify identity keys: {}", + json(dev.second).dump(2)); + continue; + } + } catch (const json::exception &e) { + nhlog::crypto()->warn( + "failed to parse device key json: {}", + e.what()); + continue; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->warn( + "failed to verify device key json: {}", + e.what()); + continue; + } + + auto room_key = olm::client() + ->create_room_key_event( + user_id, pks.ed25519, megolm_payload) + .dump(); + + room_key_msgs.emplace(device_id, room_key); + deviceKeys.emplace(device_id, pks); + } + + std::vector<std::string> valid_devices; + valid_devices.reserve(room_key_msgs.size()); + for (auto const &d : room_key_msgs) { + valid_devices.push_back(d.first); + + nhlog::net()->info("{}", d.first); + nhlog::net()->info(" curve25519 {}", + deviceKeys.at(d.first).curve25519); + nhlog::net()->info(" ed25519 {}", + deviceKeys.at(d.first).ed25519); + } + + nhlog::net()->info( + "sending claim request for user {} with {} devices", + user.first, + valid_devices.size()); + + http::client()->claim_keys( + user.first, + valid_devices, + std::bind(&TimelineModel::handleClaimedKeys, + this, + keeper, + room_key_msgs, + deviceKeys, + user.first, + std::placeholders::_1, + std::placeholders::_2)); + + // TODO: Wait before sending the next batch of requests. + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + }); + + // TODO: Let the user know about the errors. + } catch (const lmdb::error &e) { + nhlog::db()->critical( + "failed to open outbound megolm session ({}): {}", room_id, e.what()); + emit messageFailed(QString::fromStdString(txn_id)); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical( + "failed to open outbound megolm session ({}): {}", room_id, e.what()); + emit messageFailed(QString::fromStdString(txn_id)); + } +} + +void +TimelineModel::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper, + const std::map<std::string, std::string> &room_keys, + const std::map<std::string, DevicePublicKeys> &pks, + const std::string &user_id, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err) +{ + if (err) { + nhlog::net()->warn("claim keys error: {} {} {}", + err->matrix_error.error, + err->parse_error, + static_cast<int>(err->status_code)); + return; + } + + nhlog::net()->debug("claimed keys for {}", user_id); + + if (res.one_time_keys.size() == 0) { + nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); + return; + } + + if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) { + nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); + return; + } + + auto retrieved_devices = res.one_time_keys.at(user_id); + + // Payload with all the to_device message to be sent. + json body; + body["messages"][user_id] = json::object(); + + for (const auto &rd : retrieved_devices) { + const auto device_id = rd.first; + nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2)); + + // TODO: Verify signatures + auto otk = rd.second.begin()->at("key"); + + if (pks.find(device_id) == pks.end()) { + nhlog::net()->critical("couldn't find public key for device: {}", + device_id); + continue; + } + + auto id_key = pks.at(device_id).curve25519; + auto s = olm::client()->create_outbound_session(id_key, otk); + + if (room_keys.find(device_id) == room_keys.end()) { + nhlog::net()->critical("couldn't find m.room_key for device: {}", + device_id); + continue; + } + + auto device_msg = olm::client()->create_olm_encrypted_content( + s.get(), room_keys.at(device_id), pks.at(device_id).curve25519); + + try { + cache::saveOlmSession(id_key, std::move(s)); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to pickle outbound olm session: {}", + e.what()); + } + + body["messages"][user_id][device_id] = device_msg; + } + + nhlog::net()->info("send_to_device: {}", user_id); + + http::client()->send_to_device( + "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to send " + "send_to_device " + "message: {}", + err->matrix_error.error); + } + + (void)keeper; + }); +} + +struct SendMessageVisitor +{ + SendMessageVisitor(const QString &txn_id, TimelineModel *model) + : txn_id_qstr_(txn_id) + , model_(model) + {} + + template<typename T> + void operator()(const mtx::events::Event<T> &) + {} + + template<typename T, + std::enable_if_t<std::is_same<decltype(T::msgtype), std::string>::value, int> = 0> + void operator()(const mtx::events::RoomEvent<T> &msg) + + { + if (cache::isRoomEncrypted(model_->room_id_.toStdString())) { + model_->sendEncryptedMessage(txn_id_qstr_.toStdString(), + nlohmann::json(msg.content)); + } else { + QString txn_id_qstr = txn_id_qstr_; + TimelineModel *model = model_; + http::client()->send_room_message<T, mtx::events::EventType::RoomMessage>( + model->room_id_.toStdString(), + txn_id_qstr.toStdString(), + msg.content, + [txn_id_qstr, model](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = + static_cast<int>(err->status_code); + nhlog::net()->warn("[{}] failed to send message: {} {}", + txn_id_qstr.toStdString(), + err->matrix_error.error, + status_code); + emit model->messageFailed(txn_id_qstr); + } + emit model->messageSent( + txn_id_qstr, QString::fromStdString(res.event_id.to_string())); + }); + } + } + + QString txn_id_qstr_; + TimelineModel *model_; +}; + +void +TimelineModel::processOnePendingMessage() +{ + if (isProcessingPending || pending.isEmpty()) + return; + + isProcessingPending = true; + + QString txn_id_qstr = pending.first(); + + auto event = events.value(txn_id_qstr); + std::visit(SendMessageVisitor{txn_id_qstr, this}, event); +} + +void +TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event) +{ + std::visit( + [](auto &msg) { + msg.type = mtx::events::EventType::RoomMessage; + msg.event_id = http::client()->generate_txn_id(); + msg.sender = http::client()->user_id().to_string(); + msg.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); + }, + event); + + internalAddEvents({event}); + + QString txn_id_qstr = QString::fromStdString(mtx::accessors::event_id(event)); + beginInsertRows(QModelIndex(), 0, 0); + pending.push_back(txn_id_qstr); + this->eventOrder.insert(this->eventOrder.begin(), txn_id_qstr); + endInsertRows(); + updateLastMessage(); + + if (!isProcessingPending) + emit nextPendingMessage(); +} + +void +TimelineModel::saveMedia(QString eventId) const +{ + mtx::events::collections::TimelineEvents event = events.value(eventId); + + if (auto e = + std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) { + event = decryptEvent(*e).event; + } + + QString mxcUrl = QString::fromStdString(mtx::accessors::url(event)); + QString originalFilename = QString::fromStdString(mtx::accessors::filename(event)); + QString mimeType = QString::fromStdString(mtx::accessors::mimetype(event)); + + auto encryptionInfo = mtx::accessors::file(event); + + qml_mtx_events::EventType eventType = toRoomEventType(event); + + QString dialogTitle; + if (eventType == qml_mtx_events::EventType::ImageMessage) { + dialogTitle = tr("Save image"); + } else if (eventType == qml_mtx_events::EventType::VideoMessage) { + dialogTitle = tr("Save video"); + } else if (eventType == qml_mtx_events::EventType::AudioMessage) { + dialogTitle = tr("Save audio"); + } else { + dialogTitle = tr("Save file"); + } + + const QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); + const QString downloadsFolder = + QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + const QString openLocation = downloadsFolder + "/" + originalFilename; + + const QString filename = QFileDialog::getSaveFileName( + manager_->getWidget(), dialogTitle, openLocation, filterString); + + if (filename.isEmpty()) + return; + + const auto url = mxcUrl.toStdString(); + + http::client()->download( + url, + [filename, url, encryptionInfo](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + try { + auto temp = data; + if (encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + QFile file(filename); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(temp.data(), (int)temp.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + }); +} + +void +TimelineModel::cacheMedia(QString eventId) +{ + mtx::events::collections::TimelineEvents event = events.value(eventId); + + if (auto e = + std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) { + event = decryptEvent(*e).event; + } + + QString mxcUrl = QString::fromStdString(mtx::accessors::url(event)); + QString originalFilename = QString::fromStdString(mtx::accessors::filename(event)); + QString mimeType = QString::fromStdString(mtx::accessors::mimetype(event)); + + auto encryptionInfo = mtx::accessors::file(event); + + // If the message is a link to a non mxcUrl, don't download it + if (!mxcUrl.startsWith("mxc://")) { + emit mediaCached(mxcUrl, mxcUrl); + return; + } + + QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); + + const auto url = mxcUrl.toStdString(); + QFileInfo filename(QString("%1/media_cache/%2.%3") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .arg(QString(mxcUrl).remove("mxc://")) + .arg(suffix)); + if (QDir::cleanPath(filename.path()) != filename.path()) { + nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); + return; + } + + QDir().mkpath(filename.path()); + + if (filename.isReadable()) { + emit mediaCached(mxcUrl, filename.filePath()); + return; + } + + http::client()->download( + url, + [this, mxcUrl, filename, url, encryptionInfo](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast<int>(err->status_code)); + return; + } + + try { + auto temp = data; + if (encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + QFile file(filename.filePath()); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(temp.data(), temp.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + + emit mediaCached(mxcUrl, filename.filePath()); + }); +} + +QString +TimelineModel::formatTypingUsers(const std::vector<QString> &users, QColor bg) +{ + QString temp = + tr("%1 and %2 are typing", + "Multiple users are typing. First argument is a comma separated list of potentially " + "multiple users. Second argument is the last user of that list. (If only one user is " + "typing, %1 is empty. You should still use it in your string though to silence Qt " + "warnings.)", + users.size()); + + if (users.empty()) { + return ""; + } + + QStringList uidWithoutLast; + + auto formatUser = [this, bg](const QString &user_id) -> QString { + auto uncoloredUsername = escapeEmoji(displayName(user_id).toHtmlEscaped()); + QString prefix = + QString("<font color=\"%1\">").arg(manager_->userColor(user_id, bg).name()); + + // color only parts that don't have a font already specified + QString coloredUsername; + int index = 0; + do { + auto startIndex = uncoloredUsername.indexOf("<font", index); + + if (startIndex - index != 0) + coloredUsername += + prefix + + uncoloredUsername.midRef( + index, startIndex > 0 ? startIndex - index : -1) + + "</font>"; + + auto endIndex = uncoloredUsername.indexOf("</font>", startIndex); + if (endIndex > 0) + endIndex += sizeof("</font>") - 1; + + if (endIndex - startIndex != 0) + coloredUsername += + uncoloredUsername.midRef(startIndex, endIndex - startIndex); + + index = endIndex; + } while (index > 0 && index < uncoloredUsername.size()); + + return coloredUsername; + }; + + for (size_t i = 0; i + 1 < users.size(); i++) { + uidWithoutLast.append(formatUser(users[i])); + } + + return temp.arg(uidWithoutLast.join(", ")).arg(formatUser(users.back())); +} + +QString +TimelineModel::formatMemberEvent(QString id) +{ + if (!events.contains(id)) + return ""; + + auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(&events[id]); + if (!event) + return ""; + + mtx::events::StateEvent<mtx::events::state::Member> *prevEvent = nullptr; + QString prevEventId = QString::fromStdString(event->unsigned_data.replaces_state); + if (!prevEventId.isEmpty()) { + if (!events.contains(prevEventId)) { + http::client()->get_event( + this->room_id_.toStdString(), + event->unsigned_data.replaces_state, + [this, id, prevEventId]( + const mtx::events::collections::TimelineEvents &timeline, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error( + "Failed to retrieve event with id {}, which was " + "requested to show the membership for event {}", + prevEventId.toStdString(), + id.toStdString()); + return; + } + emit eventFetched(id, timeline); + }); + } else { + prevEvent = + std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>( + &events[prevEventId]); + } + } + + QString user = QString::fromStdString(event->state_key); + QString name = escapeEmoji(displayName(user)); + + // see table https://matrix.org/docs/spec/client_server/latest#m-room-member + using namespace mtx::events::state; + switch (event->content.membership) { + case Membership::Invite: + return tr("%1 was invited.").arg(name); + case Membership::Join: + if (prevEvent && prevEvent->content.membership == Membership::Join) { + bool displayNameChanged = + prevEvent->content.display_name != event->content.display_name; + bool avatarChanged = + prevEvent->content.avatar_url != event->content.avatar_url; + + if (displayNameChanged && avatarChanged) + return tr("%1 changed their display name and avatar.").arg(name); + else if (displayNameChanged) + return tr("%1 changed their display name.").arg(name); + else if (avatarChanged) + return tr("%1 changed their avatar.").arg(name); + // the case of nothing changed but join follows join shouldn't happen, so + // just show it as join + } + return tr("%1 joined.").arg(name); + case Membership::Leave: + if (!prevEvent) // Should only ever happen temporarily + return ""; + + if (prevEvent->content.membership == Membership::Invite) { + if (event->state_key == event->sender) + return tr("%1 rejected their invite.").arg(name); + else + return tr("Revoked the invite to %1.").arg(name); + } else if (prevEvent->content.membership == Membership::Join) { + if (event->state_key == event->sender) + return tr("%1 left the room.").arg(name); + else + return tr("Kicked %1.").arg(name); + } else if (prevEvent->content.membership == Membership::Ban) { + return tr("Unbanned %1").arg(name); + } else if (prevEvent->content.membership == Membership::Knock) { + if (event->state_key == event->sender) + return tr("%1 redacted their knock.").arg(name); + else + return tr("Rejected the knock from %1.").arg(name); + } else + return tr("%1 left after having already left!", + "This is a leave event after the user already left and shouln't " + "happen apart from state resets") + .arg(name); + + case Membership::Ban: + return tr("%1 was banned.").arg(name); + case Membership::Knock: + return tr("%1 knocked.").arg(name); + default: + return ""; + } +} diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h new file mode 100644
index 00000000..f06de5d9 --- /dev/null +++ b/src/timeline/TimelineModel.h
@@ -0,0 +1,265 @@ +#pragma once + +#include <QAbstractListModel> +#include <QColor> +#include <QDate> +#include <QHash> +#include <QSet> + +#include <mtxclient/http/errors.hpp> + +#include "CacheCryptoStructs.h" + +namespace mtx::http { +using RequestErr = const std::optional<mtx::http::ClientError> &; +} +namespace mtx::responses { +struct Timeline; +struct Messages; +struct ClaimKeys; +} + +namespace qml_mtx_events { +Q_NAMESPACE + +enum EventType +{ + // Unsupported event + Unsupported, + /// m.room_key_request + KeyRequest, + /// m.room.aliases + Aliases, + /// m.room.avatar + Avatar, + /// m.room.canonical_alias + CanonicalAlias, + /// m.room.create + Create, + /// m.room.encrypted. + Encrypted, + /// m.room.encryption. + Encryption, + /// m.room.guest_access + GuestAccess, + /// m.room.history_visibility + HistoryVisibility, + /// m.room.join_rules + JoinRules, + /// m.room.member + Member, + /// m.room.name + Name, + /// m.room.power_levels + PowerLevels, + /// m.room.tombstone + Tombstone, + /// m.room.topic + Topic, + /// m.room.redaction + Redaction, + /// m.room.pinned_events + PinnedEvents, + // m.sticker + Sticker, + // m.tag + Tag, + /// m.room.message + AudioMessage, + EmoteMessage, + FileMessage, + ImageMessage, + LocationMessage, + NoticeMessage, + TextMessage, + VideoMessage, + Redacted, + UnknownMessage, +}; +Q_ENUM_NS(EventType) + +enum EventState +{ + //! The plaintext message was received by the server. + Received, + //! At least one of the participants has read the message. + Read, + //! The client sent the message. Not yet received. + Sent, + //! When the message is loaded from cache or backfill. + Empty, + //! When the message failed to send + Failed, +}; +Q_ENUM_NS(EventState) +} + +class StateKeeper +{ +public: + StateKeeper(std::function<void()> &&fn) + : fn_(std::move(fn)) + {} + + ~StateKeeper() { fn_(); } + +private: + std::function<void()> fn_; +}; + +struct DecryptionResult +{ + //! The decrypted content as a normal plaintext event. + mtx::events::collections::TimelineEvents event; + //! Whether or not the decryption was successful. + bool isDecrypted = false; +}; + +class TimelineViewManager; + +class TimelineModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY( + int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) + Q_PROPERTY(std::vector<QString> typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY + typingUsersChanged) + +public: + explicit TimelineModel(TimelineViewManager *manager, + QString room_id, + QObject *parent = nullptr); + + enum Roles + { + Section, + Type, + TypeString, + Body, + FormattedBody, + UserId, + UserName, + Timestamp, + Url, + ThumbnailUrl, + Filename, + Filesize, + MimeType, + Height, + Width, + ProportionalHeight, + Id, + State, + IsEncrypted, + ReplyTo, + RoomName, + RoomTopic, + Dump, + }; + + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant data(const QString &id, int role) const; + + bool canFetchMore(const QModelIndex &) const override; + void fetchMore(const QModelIndex &) override; + + Q_INVOKABLE QString displayName(QString id) const; + Q_INVOKABLE QString avatarUrl(QString id) const; + Q_INVOKABLE QString formatDateSeparator(QDate date) const; + Q_INVOKABLE QString formatTypingUsers(const std::vector<QString> &users, QColor bg); + Q_INVOKABLE QString formatMemberEvent(QString id); + + Q_INVOKABLE QString escapeEmoji(QString str) const; + Q_INVOKABLE void viewRawMessage(QString id) const; + Q_INVOKABLE void openUserProfile(QString userid) const; + Q_INVOKABLE void replyAction(QString id); + Q_INVOKABLE void readReceiptsAction(QString id) const; + Q_INVOKABLE void redactEvent(QString id); + Q_INVOKABLE int idToIndex(QString id) const; + Q_INVOKABLE QString indexToId(int index) const; + Q_INVOKABLE void cacheMedia(QString eventId); + Q_INVOKABLE void saveMedia(QString eventId) const; + + void addEvents(const mtx::responses::Timeline &events); + template<class T> + void sendMessage(const T &msg); + +public slots: + void setCurrentIndex(int index); + int currentIndex() const { return idToIndex(currentId); } + void markEventsAsRead(const std::vector<QString> &event_ids); + QVariantMap getDump(QString eventId) const; + void updateTypingUsers(const std::vector<QString> &users) + { + if (this->typingUsers_ != users) { + this->typingUsers_ = users; + emit typingUsersChanged(typingUsers_); + } + } + std::vector<QString> typingUsers() const { return typingUsers_; } + +private slots: + // Add old events at the top of the timeline. + void addBackwardsEvents(const mtx::responses::Messages &msgs); + void processOnePendingMessage(); + void addPendingMessage(mtx::events::collections::TimelineEvents event); + +signals: + void oldMessagesRetrieved(const mtx::responses::Messages &res); + void messageFailed(QString txn_id); + void messageSent(QString txn_id, QString event_id); + void currentIndexChanged(int index); + void redactionFailed(QString id); + void eventRedacted(QString id); + void nextPendingMessage(); + void newMessageToSend(mtx::events::collections::TimelineEvents event); + void mediaCached(QString mxcUrl, QString cacheUrl); + void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo); + void eventFetched(QString requestingEvent, mtx::events::collections::TimelineEvents event); + void typingUsersChanged(std::vector<QString> users); + +private: + DecryptionResult decryptEvent( + const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) const; + std::vector<QString> internalAddEvents( + const std::vector<mtx::events::collections::TimelineEvents> &timeline); + void sendEncryptedMessage(const std::string &txn_id, nlohmann::json content); + void handleClaimedKeys(std::shared_ptr<StateKeeper> keeper, + const std::map<std::string, std::string> &room_key, + const std::map<std::string, DevicePublicKeys> &pks, + const std::string &user_id, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err); + void updateLastMessage(); + void readEvent(const std::string &id); + + QHash<QString, mtx::events::collections::TimelineEvents> events; + QSet<QString> failed, read; + QList<QString> pending; + std::vector<QString> eventOrder; + + QString room_id_; + QString prev_batch_token_; + + bool isInitialSync = true; + bool paginationInProgress = false; + bool isProcessingPending = false; + + QString currentId; + std::vector<QString> typingUsers_; + + TimelineViewManager *manager_; + + friend struct SendMessageVisitor; +}; + +template<class T> +void +TimelineModel::sendMessage(const T &msg) +{ + mtx::events::RoomEvent<T> msgCopy = {}; + msgCopy.content = msg; + emit newMessageToSend(msgCopy); +} diff --git a/src/timeline/TimelineView.cpp b/src/timeline/TimelineView.cpp deleted file mode 100644
index 2b4f979b..00000000 --- a/src/timeline/TimelineView.cpp +++ /dev/null
@@ -1,1583 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#include <boost/variant.hpp> - -#include <QApplication> -#include <QFileInfo> -#include <QTimer> -#include <QtConcurrent> - -#include "Cache.h" -#include "ChatPage.h" -#include "Config.h" -#include "Logging.h" -#include "Olm.h" -#include "UserSettingsPage.h" -#include "Utils.h" -#include "ui/FloatingButton.h" -#include "ui/InfoMessage.h" - -#include "timeline/TimelineView.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" - -using TimelineEvent = mtx::events::collections::TimelineEvents; - -//! Maximum number of widgets to keep in the timeline layout. -constexpr int MAX_RETAINED_WIDGETS = 100; -constexpr int MIN_SCROLLBAR_HANDLE = 60; - -//! Retrieve the timestamp of the event represented by the given widget. -QDateTime -getDate(QWidget *widget) -{ - auto item = qobject_cast<TimelineItem *>(widget); - if (item) - return item->descriptionMessage().datetime; - - auto infoMsg = qobject_cast<InfoMessage *>(widget); - if (infoMsg) - return infoMsg->datetime(); - - return QDateTime(); -} - -TimelineView::TimelineView(const mtx::responses::Timeline &timeline, - const QString &room_id, - QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - addEvents(timeline); -} - -TimelineView::TimelineView(const QString &room_id, QWidget *parent) - : QWidget(parent) - , room_id_{room_id} -{ - init(); - getMessages(); -} - -void -TimelineView::sliderRangeChanged(int min, int max) -{ - Q_UNUSED(min); - - if (!scroll_area_->verticalScrollBar()->isVisible()) { - scroll_area_->verticalScrollBar()->setValue(max); - return; - } - - // If the scrollbar is close to the bottom and a new message - // is added we move the scrollbar. - if (max - scroll_area_->verticalScrollBar()->value() < SCROLL_BAR_GAP) { - scroll_area_->verticalScrollBar()->setValue(max); - return; - } - - int currentHeight = scroll_widget_->size().height(); - int diff = currentHeight - oldHeight_; - int newPosition = oldPosition_ + diff; - - // Keep the scroll bar to the bottom if it hasn't been activated yet. - if (oldPosition_ == 0 && !scroll_area_->verticalScrollBar()->isVisible()) - newPosition = max; - - if (lastMessageDirection_ == TimelineDirection::Top) - scroll_area_->verticalScrollBar()->setValue(newPosition); -} - -void -TimelineView::fetchHistory() -{ - if (!isScrollbarActivated() && !isTimelineFinished) { - if (!isVisible()) - return; - - isPaginationInProgress_ = true; - getMessages(); - paginationTimer_->start(2000); - - return; - } - - paginationTimer_->stop(); -} - -void -TimelineView::scrollDown() -{ - int current = scroll_area_->verticalScrollBar()->value(); - int max = scroll_area_->verticalScrollBar()->maximum(); - - // The first time we enter the room move the scroll bar to the bottom. - if (!isInitialized) { - scroll_area_->verticalScrollBar()->setValue(max); - isInitialized = true; - return; - } - - // If the gap is small enough move the scroll bar down. e.g when a new - // message appears. - if (max - current < SCROLL_BAR_GAP) - scroll_area_->verticalScrollBar()->setValue(max); -} - -void -TimelineView::sliderMoved(int position) -{ - if (!scroll_area_->verticalScrollBar()->isVisible()) - return; - - toggleScrollDownButton(); - - // The scrollbar is high enough so we can start retrieving old events. - if (position < SCROLL_BAR_GAP) { - if (isTimelineFinished) - return; - - // Prevent user from moving up when there is pagination in - // progress. - if (isPaginationInProgress_) - return; - - isPaginationInProgress_ = true; - - getMessages(); - } -} - -bool -TimelineView::isStartOfTimeline(const mtx::responses::Messages &msgs) -{ - return (msgs.chunk.size() == 0 && (msgs.end.empty() || msgs.end == msgs.start)); -} - -void -TimelineView::addBackwardsEvents(const mtx::responses::Messages &msgs) -{ - // We've reached the start of the timline and there're no more messages. - if (isStartOfTimeline(msgs)) { - nhlog::ui()->info("[{}] start of timeline reached, no more messages to fetch", - room_id_.toStdString()); - isTimelineFinished = true; - return; - } - - isTimelineFinished = false; - - // Queue incoming messages to be rendered later. - topMessages_.insert(topMessages_.end(), - std::make_move_iterator(msgs.chunk.begin()), - std::make_move_iterator(msgs.chunk.end())); - - // The RoomList message preview will be updated only if this - // is the first batch of messages received through /messages - // i.e there are no other messages currently present. - if (!topMessages_.empty() && scroll_layout_->count() == 0) - notifyForLastEvent(findFirstViewableEvent(topMessages_)); - - if (isVisible()) { - renderTopEvents(topMessages_); - - // Free up space for new messages. - topMessages_.clear(); - - // Send a read receipt for the last event. - if (isActiveWindow()) - readLastEvent(); - } - - prev_batch_token_ = QString::fromStdString(msgs.end); - isPaginationInProgress_ = false; -} - -QWidget * -TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents &event, - TimelineDirection direction) -{ - using namespace mtx::events; - - using AudioEvent = RoomEvent<msg::Audio>; - using EmoteEvent = RoomEvent<msg::Emote>; - using FileEvent = RoomEvent<msg::File>; - using ImageEvent = RoomEvent<msg::Image>; - using NoticeEvent = RoomEvent<msg::Notice>; - using TextEvent = RoomEvent<msg::Text>; - using VideoEvent = RoomEvent<msg::Video>; - - if (boost::get<RedactionEvent<msg::Redaction>>(&event) != nullptr) { - auto redaction_event = boost::get<RedactionEvent<msg::Redaction>>(event); - const auto event_id = QString::fromStdString(redaction_event.redacts); - - QTimer::singleShot(0, this, [event_id, this]() { - if (eventIds_.contains(event_id)) - removeEvent(event_id); - }); - - return nullptr; - } else if (boost::get<StateEvent<state::Encryption>>(&event) != nullptr) { - auto msg = boost::get<StateEvent<state::Encryption>>(event); - auto event_id = QString::fromStdString(msg.event_id); - - if (eventIds_.contains(event_id)) - return nullptr; - - auto item = new InfoMessage(tr("Encryption is enabled"), this); - item->saveDatetime(QDateTime::fromMSecsSinceEpoch(msg.origin_server_ts)); - eventIds_[event_id] = item; - - // Force the next message to have avatar by not providing the current username. - saveMessageInfo("", msg.origin_server_ts, direction); - - return item; - } else if (boost::get<RoomEvent<msg::Audio>>(&event) != nullptr) { - auto audio = boost::get<RoomEvent<msg::Audio>>(event); - return processMessageEvent<AudioEvent, AudioItem>(audio, direction); - } else if (boost::get<RoomEvent<msg::Emote>>(&event) != nullptr) { - auto emote = boost::get<RoomEvent<msg::Emote>>(event); - return processMessageEvent<EmoteEvent>(emote, direction); - } else if (boost::get<RoomEvent<msg::File>>(&event) != nullptr) { - auto file = boost::get<RoomEvent<msg::File>>(event); - return processMessageEvent<FileEvent, FileItem>(file, direction); - } else if (boost::get<RoomEvent<msg::Image>>(&event) != nullptr) { - auto image = boost::get<RoomEvent<msg::Image>>(event); - return processMessageEvent<ImageEvent, ImageItem>(image, direction); - } else if (boost::get<RoomEvent<msg::Notice>>(&event) != nullptr) { - auto notice = boost::get<RoomEvent<msg::Notice>>(event); - return processMessageEvent<NoticeEvent>(notice, direction); - } else if (boost::get<RoomEvent<msg::Text>>(&event) != nullptr) { - auto text = boost::get<RoomEvent<msg::Text>>(event); - return processMessageEvent<TextEvent>(text, direction); - } else if (boost::get<RoomEvent<msg::Video>>(&event) != nullptr) { - auto video = boost::get<RoomEvent<msg::Video>>(event); - return processMessageEvent<VideoEvent, VideoItem>(video, direction); - } else if (boost::get<Sticker>(&event) != nullptr) { - return processMessageEvent<Sticker, StickerItem>(boost::get<Sticker>(event), - direction); - } else if (boost::get<EncryptedEvent<msg::Encrypted>>(&event) != nullptr) { - auto res = parseEncryptedEvent(boost::get<EncryptedEvent<msg::Encrypted>>(event)); - auto widget = parseMessageEvent(res.event, direction); - - if (widget == nullptr) - return nullptr; - - auto item = qobject_cast<TimelineItem *>(widget); - - if (item && res.isDecrypted) - item->markReceived(true); - else if (item && !res.isDecrypted) - item->addKeyRequestAction(); - - return widget; - } - - return nullptr; -} - -DecryptionResult -TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) -{ - MegolmSessionIndex index; - index.room_id = room_id_.toStdString(); - index.session_id = e.content.session_id; - index.sender_key = e.content.sender_key; - - mtx::events::RoomEvent<mtx::events::msg::Notice> dummy; - dummy.origin_server_ts = e.origin_server_ts; - dummy.event_id = e.event_id; - dummy.sender = e.sender; - dummy.content.body = "-- Encrypted Event (No keys found for decryption) --"; - - try { - if (!cache::client()->inboundMegolmSessionExists(index)) { - nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", - index.room_id, - index.session_id, - e.sender); - // TODO: request megolm session_id & session_key from the sender. - return {dummy, false}; - } - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to check megolm session's existence: {}", e.what()); - dummy.content.body = "-- Decryption Error (failed to communicate with DB) --"; - return {dummy, false}; - } - - std::string msg_str; - try { - auto session = cache::client()->getInboundMegolmSession(index); - auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); - msg_str = std::string((char *)res.data.data(), res.data.size()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = - "-- Decryption Error (failed to retrieve megolm keys from db) --"; - return {dummy, false}; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", - index.room_id, - index.session_id, - index.sender_key, - e.what()); - dummy.content.body = "-- Decryption Error (" + std::string(e.what()) + ") --"; - return {dummy, false}; - } - - // Add missing fields for the event. - json body = json::parse(msg_str); - body["event_id"] = e.event_id; - body["sender"] = e.sender; - body["origin_server_ts"] = e.origin_server_ts; - body["unsigned"] = e.unsigned_data; - - nhlog::crypto()->debug("decrypted event: {}", e.event_id); - - json event_array = json::array(); - event_array.push_back(body); - - std::vector<TimelineEvent> events; - mtx::responses::utils::parse_timeline_events(event_array, events); - - if (events.size() == 1) - return {events.at(0), true}; - - dummy.content.body = "-- Encrypted Event (Unknown event type) --"; - return {dummy, false}; -} - -void -TimelineView::displayReadReceipts(std::vector<TimelineEvent> events) -{ - QtConcurrent::run( - [events = std::move(events), room_id = room_id_, local_user = local_user_, this]() { - std::vector<QString> event_ids; - - for (const auto &e : events) { - if (utils::event_sender(e) == local_user) - event_ids.emplace_back( - QString::fromStdString(utils::event_id(e))); - } - - auto readEvents = - cache::client()->filterReadEvents(room_id, event_ids, local_user.toStdString()); - - if (!readEvents.empty()) - emit markReadEvents(readEvents); - }); -} - -void -TimelineView::renderBottomEvents(const std::vector<TimelineEvent> &events) -{ - int counter = 0; - - for (const auto &event : events) { - QWidget *item = parseMessageEvent(event, TimelineDirection::Bottom); - - if (item != nullptr) { - addTimelineItem(item, TimelineDirection::Bottom); - counter++; - - // Prevent blocking of the event-loop - // by calling processEvents every 10 items we render. - if (counter % 4 == 0) - QApplication::processEvents(); - } - } - - lastMessageDirection_ = TimelineDirection::Bottom; - - displayReadReceipts(events); - - QApplication::processEvents(); -} - -void -TimelineView::renderTopEvents(const std::vector<TimelineEvent> &events) -{ - std::vector<QWidget *> items; - - // Reset the sender of the first message in the timeline - // cause we're about to insert a new one. - firstSender_.clear(); - firstMsgTimestamp_ = QDateTime(); - - // Parse in reverse order to determine where we should not show sender's name. - for (auto it = events.rbegin(); it != events.rend(); ++it) { - auto item = parseMessageEvent(*it, TimelineDirection::Top); - - if (item != nullptr) - items.push_back(item); - } - - // Reverse again to render them. - std::reverse(items.begin(), items.end()); - - oldPosition_ = scroll_area_->verticalScrollBar()->value(); - oldHeight_ = scroll_widget_->size().height(); - - for (const auto &item : items) - addTimelineItem(item, TimelineDirection::Top); - - lastMessageDirection_ = TimelineDirection::Top; - - QApplication::processEvents(); - - displayReadReceipts(events); - - // If this batch is the first being rendered (i.e the first and the last - // events originate from this batch), set the last sender. - if (lastSender_.isEmpty() && !items.empty()) { - for (const auto &w : items) { - auto timelineItem = qobject_cast<TimelineItem *>(w); - if (timelineItem) { - saveLastMessageInfo(timelineItem->descriptionMessage().userid, - timelineItem->descriptionMessage().datetime); - break; - } - } - } -} - -void -TimelineView::addEvents(const mtx::responses::Timeline &timeline) -{ - if (isInitialSync) { - prev_batch_token_ = QString::fromStdString(timeline.prev_batch); - isInitialSync = false; - } - - bottomMessages_.insert(bottomMessages_.end(), - std::make_move_iterator(timeline.events.begin()), - std::make_move_iterator(timeline.events.end())); - - if (!bottomMessages_.empty()) - notifyForLastEvent(findLastViewableEvent(bottomMessages_)); - - // If the current timeline is open and there are messages to be rendered. - if (isVisible() && !bottomMessages_.empty()) { - renderBottomEvents(bottomMessages_); - - // Free up space for new messages. - bottomMessages_.clear(); - - // Send a read receipt for the last event. - if (isActiveWindow()) - readLastEvent(); - } -} - -void -TimelineView::init() -{ - local_user_ = utils::localUser(); - - QIcon icon; - icon.addFile(":/icons/icons/ui/angle-arrow-down.png"); - scrollDownBtn_ = new FloatingButton(icon, this); - scrollDownBtn_->hide(); - - connect(scrollDownBtn_, &QPushButton::clicked, this, [this]() { - const int max = scroll_area_->verticalScrollBar()->maximum(); - scroll_area_->verticalScrollBar()->setValue(max); - }); - top_layout_ = new QVBoxLayout(this); - top_layout_->setSpacing(0); - top_layout_->setMargin(0); - - scroll_area_ = new QScrollArea(this); - scroll_area_->setWidgetResizable(true); - scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - scroll_widget_ = new QWidget(this); - scroll_widget_->setObjectName("scroll_widget"); - - // Height of the typing display. - QFont f; - f.setPointSizeF(f.pointSizeF() * 0.9); - const int bottomMargin = QFontMetrics(f).height() + 6; - - scroll_layout_ = new QVBoxLayout(scroll_widget_); - scroll_layout_->setContentsMargins(4, 0, 15, bottomMargin); - scroll_layout_->setSpacing(0); - scroll_layout_->setObjectName("timelinescrollarea"); - - scroll_area_->setWidget(scroll_widget_); - scroll_area_->setAlignment(Qt::AlignBottom); - - top_layout_->addWidget(scroll_area_); - - setLayout(top_layout_); - - paginationTimer_ = new QTimer(this); - connect(paginationTimer_, &QTimer::timeout, this, &TimelineView::fetchHistory); - - connect(this, &TimelineView::messagesRetrieved, this, &TimelineView::addBackwardsEvents); - - connect(this, &TimelineView::messageFailed, this, &TimelineView::handleFailedMessage); - connect(this, &TimelineView::messageSent, this, &TimelineView::updatePendingMessage); - - connect( - this, &TimelineView::markReadEvents, this, [this](const std::vector<QString> &event_ids) { - for (const auto &event : event_ids) { - if (eventIds_.contains(event)) { - auto widget = eventIds_[event]; - if (!widget) - return; - - auto item = qobject_cast<TimelineItem *>(widget); - if (!item) - return; - - item->markRead(); - } - } - }); - - connect(scroll_area_->verticalScrollBar(), - SIGNAL(valueChanged(int)), - this, - SLOT(sliderMoved(int))); - connect(scroll_area_->verticalScrollBar(), - SIGNAL(rangeChanged(int, int)), - this, - SLOT(sliderRangeChanged(int, int))); -} - -void -TimelineView::getMessages() -{ - mtx::http::MessagesOpts opts; - opts.room_id = room_id_.toStdString(); - opts.from = prev_batch_token_.toStdString(); - - http::client()->messages( - opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->error("failed to call /messages ({}): {} - {}", - opts.room_id, - mtx::errors::to_string(err->matrix_error.errcode), - err->matrix_error.error); - return; - } - - emit messagesRetrieved(std::move(res)); - }); -} - -void -TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction) -{ - if (direction == TimelineDirection::Bottom) - lastSender_ = user_id; - else - firstSender_ = user_id; -} - -bool -TimelineView::isSenderRendered(const QString &user_id, - uint64_t origin_server_ts, - TimelineDirection direction) -{ - if (direction == TimelineDirection::Bottom) { - return (lastSender_ != user_id) || - isDateDifference(lastMsgTimestamp_, - QDateTime::fromMSecsSinceEpoch(origin_server_ts)); - } else { - return (firstSender_ != user_id) || - isDateDifference(firstMsgTimestamp_, - QDateTime::fromMSecsSinceEpoch(origin_server_ts)); - } -} - -void -TimelineView::addTimelineItem(QWidget *item, TimelineDirection direction) -{ - const auto newDate = getDate(item); - - if (direction == TimelineDirection::Bottom) { - QWidget *lastItem = nullptr; - int lastItemPosition = 0; - - if (scroll_layout_->count() > 0) { - lastItemPosition = scroll_layout_->count() - 1; - lastItem = scroll_layout_->itemAt(lastItemPosition)->widget(); - } - - if (lastItem) { - const auto oldDate = getDate(lastItem); - - if (oldDate.daysTo(newDate) != 0) { - auto separator = new DateSeparator(newDate, this); - - if (separator) - pushTimelineItem(separator, direction); - } - } - - pushTimelineItem(item, direction); - } else { - if (scroll_layout_->count() > 0) { - const auto firstItem = scroll_layout_->itemAt(0)->widget(); - - if (firstItem) { - const auto oldDate = getDate(firstItem); - - if (newDate.daysTo(oldDate) != 0) { - auto separator = new DateSeparator(oldDate); - - if (separator) - pushTimelineItem(separator, direction); - } - } - } - - pushTimelineItem(item, direction); - } -} - -void -TimelineView::updatePendingMessage(const std::string &txn_id, const QString &event_id) -{ - nhlog::ui()->debug("[{}] message was received by the server", txn_id); - if (!pending_msgs_.isEmpty() && - pending_msgs_.head().txn_id == txn_id) { // We haven't received it yet - auto msg = pending_msgs_.dequeue(); - msg.event_id = event_id; - - if (msg.widget) { - msg.widget->setEventId(event_id); - eventIds_[event_id] = msg.widget; - - // If the response comes after we have received the event from sync - // we've already marked the widget as received. - if (!msg.widget->isReceived()) { - msg.widget->markReceived(msg.is_encrypted); - cache::client()->addPendingReceipt(room_id_, event_id); - pending_sent_msgs_.append(msg); - } - } else { - nhlog::ui()->warn("[{}] received message response for invalid widget", - txn_id); - } - } - - sendNextPendingMessage(); -} - -void -TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body) -{ - auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_); - - TimelineItem *view_item = - new TimelineItem(ty, local_user_, body, with_sender, room_id_, scroll_widget_); - - PendingMessage message; - message.ty = ty; - message.txn_id = http::client()->generate_txn_id(); - message.body = body; - message.widget = view_item; - - try { - message.is_encrypted = cache::client()->isRoomEncrypted(room_id_.toStdString()); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to check encryption status of room {}", e.what()); - view_item->deleteLater(); - - // TODO: Send a notification to the user. - - return; - } - - addTimelineItem(view_item); - - lastMessageDirection_ = TimelineDirection::Bottom; - - saveLastMessageInfo(local_user_, QDateTime::currentDateTime()); - handleNewUserMessage(message); -} - -void -TimelineView::handleNewUserMessage(PendingMessage msg) -{ - pending_msgs_.enqueue(msg); - if (pending_msgs_.size() == 1 && pending_sent_msgs_.isEmpty()) - sendNextPendingMessage(); -} - -void -TimelineView::sendNextPendingMessage() -{ - if (pending_msgs_.size() == 0) - return; - - using namespace mtx::events; - - PendingMessage &m = pending_msgs_.head(); - - nhlog::ui()->debug("[{}] sending next queued message", m.txn_id); - - if (m.widget) - m.widget->markSent(); - - if (m.is_encrypted) { - nhlog::ui()->debug("[{}] sending encrypted event", m.txn_id); - prepareEncryptedMessage(std::move(m)); - return; - } - - switch (m.ty) { - case mtx::events::MessageType::Audio: { - http::client()->send_room_message<msg::Audio, EventType::RoomMessage>( - room_id_.toStdString(), - m.txn_id, - toRoomMessage<msg::Audio>(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Image: { - http::client()->send_room_message<msg::Image, EventType::RoomMessage>( - room_id_.toStdString(), - m.txn_id, - toRoomMessage<msg::Image>(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Video: { - http::client()->send_room_message<msg::Video, EventType::RoomMessage>( - room_id_.toStdString(), - m.txn_id, - toRoomMessage<msg::Video>(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::File: { - http::client()->send_room_message<msg::File, EventType::RoomMessage>( - room_id_.toStdString(), - m.txn_id, - toRoomMessage<msg::File>(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Text: { - http::client()->send_room_message<msg::Text, EventType::RoomMessage>( - room_id_.toStdString(), - m.txn_id, - toRoomMessage<msg::Text>(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - break; - } - case mtx::events::MessageType::Emote: { - http::client()->send_room_message<msg::Emote, EventType::RoomMessage>( - room_id_.toStdString(), - m.txn_id, - toRoomMessage<msg::Emote>(m), - std::bind(&TimelineView::sendRoomMessageHandler, - this, - m.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - break; - } - default: - nhlog::ui()->warn("cannot send unknown message type: {}", m.body.toStdString()); - break; - } -} - -void -TimelineView::notifyForLastEvent() -{ - if (scroll_layout_->count() == 0) { - nhlog::ui()->error("notifyForLastEvent called with empty timeline"); - return; - } - - auto lastItem = scroll_layout_->itemAt(scroll_layout_->count() - 1); - - if (!lastItem) - return; - - auto *lastTimelineItem = qobject_cast<TimelineItem *>(lastItem->widget()); - - if (lastTimelineItem) - emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage()); - else - nhlog::ui()->warn("cast to TimelineItem failed: {}", room_id_.toStdString()); -} - -void -TimelineView::notifyForLastEvent(const TimelineEvent &event) -{ - auto descInfo = utils::getMessageDescription(event, local_user_, room_id_); - - if (!descInfo.timestamp.isEmpty()) - emit updateLastTimelineMessage(room_id_, descInfo); -} - -bool -TimelineView::isPendingMessage(const std::string &txn_id, - const QString &sender, - const QString &local_userid) -{ - if (sender != local_userid) - return false; - - auto match_txnid = [txn_id](const auto &msg) -> bool { return msg.txn_id == txn_id; }; - - return std::any_of(pending_msgs_.cbegin(), pending_msgs_.cend(), match_txnid) || - std::any_of(pending_sent_msgs_.cbegin(), pending_sent_msgs_.cend(), match_txnid); -} - -void -TimelineView::removePendingMessage(const std::string &txn_id) -{ - if (txn_id.empty()) - return; - - for (auto it = pending_sent_msgs_.begin(); it != pending_sent_msgs_.end(); ++it) { - if (it->txn_id == txn_id) { - int index = std::distance(pending_sent_msgs_.begin(), it); - pending_sent_msgs_.removeAt(index); - - if (pending_sent_msgs_.isEmpty()) - sendNextPendingMessage(); - - nhlog::ui()->debug("[{}] removed message with sync", txn_id); - } - } - for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) { - if (it->txn_id == txn_id) { - if (it->widget) { - it->widget->markReceived(it->is_encrypted); - - // TODO: update when a solution for encrypted messages is available. - if (!it->is_encrypted) - cache::client()->addPendingReceipt(room_id_, it->event_id); - } - - nhlog::ui()->debug("[{}] received sync before message response", txn_id); - return; - } - } -} - -void -TimelineView::handleFailedMessage(const std::string &txn_id) -{ - Q_UNUSED(txn_id); - // Note: We do this even if the message has already been echoed. - QTimer::singleShot(2000, this, SLOT(sendNextPendingMessage())); -} - -void -TimelineView::paintEvent(QPaintEvent *) -{ - QStyleOption opt; - opt.init(this); - QPainter p(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); -} - -void -TimelineView::readLastEvent() const -{ - if (!ChatPage::instance()->userSettings()->isReadReceiptsEnabled()) - return; - - const auto eventId = getLastEventId(); - - if (!eventId.isEmpty()) - http::client()->read_event(room_id_.toStdString(), - eventId.toStdString(), - [this, eventId](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to read event ({}, {})", - room_id_.toStdString(), - eventId.toStdString()); - } - }); -} - -QString -TimelineView::getLastEventId() const -{ - auto index = scroll_layout_->count(); - - // Search backwards for the first event that has a valid event id. - while (index > 0) { - --index; - - auto lastItem = scroll_layout_->itemAt(index); - auto *lastTimelineItem = qobject_cast<TimelineItem *>(lastItem->widget()); - - if (lastTimelineItem && !lastTimelineItem->eventId().isEmpty()) - return lastTimelineItem->eventId(); - } - - return QString(""); -} - -void -TimelineView::showEvent(QShowEvent *event) -{ - if (!topMessages_.empty()) { - renderTopEvents(topMessages_); - topMessages_.clear(); - } - - if (!bottomMessages_.empty()) { - renderBottomEvents(bottomMessages_); - bottomMessages_.clear(); - scrollDown(); - } - - toggleScrollDownButton(); - - readLastEvent(); - - QWidget::showEvent(event); -} - -void -TimelineView::hideEvent(QHideEvent *event) -{ - const auto handleHeight = scroll_area_->verticalScrollBar()->sizeHint().height(); - const auto widgetsNum = scroll_layout_->count(); - - // Remove widgets from the timeline to reduce the memory footprint. - if (handleHeight < MIN_SCROLLBAR_HANDLE && widgetsNum > MAX_RETAINED_WIDGETS) - clearTimeline(); - - QWidget::hideEvent(event); -} - -bool -TimelineView::event(QEvent *event) -{ - if (event->type() == QEvent::WindowActivate) - readLastEvent(); - - return QWidget::event(event); -} - -void -TimelineView::clearTimeline() -{ - // Delete all widgets. - QLayoutItem *item; - while ((item = scroll_layout_->takeAt(0)) != nullptr) { - delete item->widget(); - delete item; - } - - // The next call to /messages will be without a prev token. - prev_batch_token_.clear(); - eventIds_.clear(); - - // Clear queues with pending messages to be rendered. - bottomMessages_.clear(); - topMessages_.clear(); - - firstSender_.clear(); - lastSender_.clear(); -} - -void -TimelineView::toggleScrollDownButton() -{ - const int maxScroll = scroll_area_->verticalScrollBar()->maximum(); - const int currentScroll = scroll_area_->verticalScrollBar()->value(); - - if (maxScroll - currentScroll > SCROLL_BAR_GAP) { - scrollDownBtn_->show(); - scrollDownBtn_->raise(); - } else { - scrollDownBtn_->hide(); - } -} - -void -TimelineView::removeEvent(const QString &event_id) -{ - if (!eventIds_.contains(event_id)) { - nhlog::ui()->warn("cannot remove widget with unknown event_id: {}", - event_id.toStdString()); - return; - } - - auto removedItem = eventIds_[event_id]; - - // Find the next and the previous widgets in the timeline - auto prevWidget = relativeWidget(removedItem, -1); - auto nextWidget = relativeWidget(removedItem, 1); - - // See if they are timeline items - auto prevItem = qobject_cast<TimelineItem *>(prevWidget); - auto nextItem = qobject_cast<TimelineItem *>(nextWidget); - - // ... or a date separator - auto prevLabel = qobject_cast<DateSeparator *>(prevWidget); - - // If it's a TimelineItem add an avatar. - if (prevItem) { - prevItem->addAvatar(); - } - - if (nextItem) { - nextItem->addAvatar(); - } else if (prevLabel) { - // If there's no chat message after this, and we have a label before us, delete the - // label. - prevLabel->deleteLater(); - } - - // If we deleted the last item in the timeline... - if (!nextItem && prevItem) - saveLastMessageInfo(prevItem->descriptionMessage().userid, - prevItem->descriptionMessage().datetime); - - // If we deleted the first item in the timeline... - if (!prevItem && nextItem) - saveFirstMessageInfo(nextItem->descriptionMessage().userid, - nextItem->descriptionMessage().datetime); - - // If we deleted the only item in the timeline... - if (!prevItem && !nextItem) { - firstSender_.clear(); - firstMsgTimestamp_ = QDateTime(); - lastSender_.clear(); - lastMsgTimestamp_ = QDateTime(); - } - - // Finally remove the event. - removedItem->deleteLater(); - eventIds_.remove(event_id); - - // Update the room list with a view of the last message after - // all events have been processed. - QTimer::singleShot(0, this, [this]() { notifyForLastEvent(); }); -} - -QWidget * -TimelineView::relativeWidget(QWidget *item, int dt) const -{ - int pos = scroll_layout_->indexOf(item); - - if (pos == -1) - return nullptr; - - pos = pos + dt; - - bool isOutOfBounds = (pos < 0 || pos > scroll_layout_->count() - 1); - - return isOutOfBounds ? nullptr : scroll_layout_->itemAt(pos)->widget(); -} - -TimelineEvent -TimelineView::findFirstViewableEvent(const std::vector<TimelineEvent> &events) -{ - auto it = std::find_if(events.begin(), events.end(), [](const auto &event) { - return mtx::events::EventType::RoomMessage == utils::event_type(event); - }); - - return (it == std::end(events)) ? events.front() : *it; -} - -TimelineEvent -TimelineView::findLastViewableEvent(const std::vector<TimelineEvent> &events) -{ - auto it = std::find_if(events.rbegin(), events.rend(), [](const auto &event) { - return (mtx::events::EventType::RoomMessage == utils::event_type(event)) || - (mtx::events::EventType::RoomEncrypted == utils::event_type(event)); - }); - - return (it == std::rend(events)) ? events.back() : *it; -} - -void -TimelineView::saveMessageInfo(const QString &sender, - uint64_t origin_server_ts, - TimelineDirection direction) -{ - updateLastSender(sender, direction); - - if (direction == TimelineDirection::Bottom) - lastMsgTimestamp_ = QDateTime::fromMSecsSinceEpoch(origin_server_ts); - else - firstMsgTimestamp_ = QDateTime::fromMSecsSinceEpoch(origin_server_ts); -} - -bool -TimelineView::isDateDifference(const QDateTime &first, const QDateTime &second) const -{ - // Check if the dates are in a different day. - if (std::abs(first.daysTo(second)) != 0) - return true; - - const uint64_t diffInSeconds = std::abs(first.msecsTo(second)) / 1000; - constexpr uint64_t fifteenMins = 15 * 60; - - return diffInSeconds > fifteenMins; -} - -void -TimelineView::sendRoomMessageHandler(const std::string &txn_id, - const mtx::responses::EventId &res, - mtx::http::RequestErr err) -{ - if (err) { - const int status_code = static_cast<int>(err->status_code); - nhlog::net()->warn("[{}] failed to send message: {} {}", - txn_id, - err->matrix_error.error, - status_code); - emit messageFailed(txn_id); - return; - } - - emit messageSent(txn_id, QString::fromStdString(res.event_id.to_string())); -} - -template<> -mtx::events::msg::Audio -toRoomMessage<mtx::events::msg::Audio>(const PendingMessage &m) -{ - mtx::events::msg::Audio audio; - audio.info.mimetype = m.mime.toStdString(); - audio.info.size = m.media_size; - audio.body = m.filename.toStdString(); - audio.url = m.body.toStdString(); - return audio; -} - -template<> -mtx::events::msg::Image -toRoomMessage<mtx::events::msg::Image>(const PendingMessage &m) -{ - mtx::events::msg::Image image; - image.info.mimetype = m.mime.toStdString(); - image.info.size = m.media_size; - image.body = m.filename.toStdString(); - image.url = m.body.toStdString(); - image.info.h = m.dimensions.height(); - image.info.w = m.dimensions.width(); - return image; -} - -template<> -mtx::events::msg::Video -toRoomMessage<mtx::events::msg::Video>(const PendingMessage &m) -{ - mtx::events::msg::Video video; - video.info.mimetype = m.mime.toStdString(); - video.info.size = m.media_size; - video.body = m.filename.toStdString(); - video.url = m.body.toStdString(); - return video; -} - -template<> -mtx::events::msg::Emote -toRoomMessage<mtx::events::msg::Emote>(const PendingMessage &m) -{ - auto html = utils::markdownToHtml(m.body); - - mtx::events::msg::Emote emote; - emote.body = m.body.trimmed().toStdString(); - - if (html != m.body.trimmed().toHtmlEscaped()) - emote.formatted_body = html.toStdString(); - - return emote; -} - -template<> -mtx::events::msg::File -toRoomMessage<mtx::events::msg::File>(const PendingMessage &m) -{ - mtx::events::msg::File file; - file.info.mimetype = m.mime.toStdString(); - file.info.size = m.media_size; - file.body = m.filename.toStdString(); - file.url = m.body.toStdString(); - return file; -} - -template<> -mtx::events::msg::Text -toRoomMessage<mtx::events::msg::Text>(const PendingMessage &m) -{ - auto html = utils::markdownToHtml(m.body); - - mtx::events::msg::Text text; - text.body = m.body.trimmed().toStdString(); - - if (html != m.body.trimmed().toHtmlEscaped()) - text.formatted_body = html.toStdString(); - - return text; -} - -void -TimelineView::prepareEncryptedMessage(const PendingMessage &msg) -{ - const auto room_id = room_id_.toStdString(); - - using namespace mtx::events; - using namespace mtx::identifiers; - - json content; - - // Serialize the message to the plaintext that will be encrypted. - switch (msg.ty) { - case MessageType::Audio: { - content = json(toRoomMessage<msg::Audio>(msg)); - break; - } - case MessageType::Emote: { - content = json(toRoomMessage<msg::Emote>(msg)); - break; - } - case MessageType::File: { - content = json(toRoomMessage<msg::File>(msg)); - break; - } - case MessageType::Image: { - content = json(toRoomMessage<msg::Image>(msg)); - break; - } - case MessageType::Text: { - content = json(toRoomMessage<msg::Text>(msg)); - break; - } - case MessageType::Video: { - content = json(toRoomMessage<msg::Video>(msg)); - break; - } - default: - break; - } - - json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}}; - - try { - // Check if we have already an outbound megolm session then we can use. - if (cache::client()->outboundMegolmSessionExists(room_id)) { - auto data = olm::encrypt_group_message( - room_id, http::client()->device_id(), doc.dump()); - - http::client()->send_room_message<msg::Encrypted, EventType::RoomEncrypted>( - room_id, - msg.txn_id, - data, - std::bind(&TimelineView::sendRoomMessageHandler, - this, - msg.txn_id, - std::placeholders::_1, - std::placeholders::_2)); - return; - } - - nhlog::ui()->debug("creating new outbound megolm session"); - - // Create a new outbound megolm session. - auto outbound_session = olm::client()->init_outbound_group_session(); - const auto session_id = mtx::crypto::session_id(outbound_session.get()); - const auto session_key = mtx::crypto::session_key(outbound_session.get()); - - // TODO: needs to be moved in the lib. - auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, - {"room_id", room_id}, - {"session_id", session_id}, - {"session_key", session_key}}; - - // Saving the new megolm session. - // TODO: Maybe it's too early to save. - OutboundGroupSessionData session_data; - session_data.session_id = session_id; - session_data.session_key = session_key; - session_data.message_index = 0; // TODO Update me - cache::client()->saveOutboundMegolmSession( - room_id, session_data, std::move(outbound_session)); - - const auto members = cache::client()->roomMembers(room_id); - nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); - - auto keeper = std::make_shared<StateKeeper>( - [megolm_payload, room_id, doc, txn_id = msg.txn_id, this]() { - try { - auto data = olm::encrypt_group_message( - room_id, http::client()->device_id(), doc.dump()); - - http::client() - ->send_room_message<msg::Encrypted, EventType::RoomEncrypted>( - room_id, - txn_id, - data, - std::bind(&TimelineView::sendRoomMessageHandler, - this, - txn_id, - std::placeholders::_1, - std::placeholders::_2)); - - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to save megolm outbound session: {}", e.what()); - } - }); - - mtx::requests::QueryKeys req; - for (const auto &member : members) - req.device_keys[member] = {}; - - http::client()->query_keys( - req, - [keeper = std::move(keeper), megolm_payload, this]( - const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to query device keys: {} {}", - err->matrix_error.error, - static_cast<int>(err->status_code)); - // TODO: Mark the event as failed. Communicate with the UI. - return; - } - - for (const auto &user : res.device_keys) { - // Mapping from a device_id with valid identity keys to the - // generated room_key event used for sharing the megolm session. - std::map<std::string, std::string> room_key_msgs; - std::map<std::string, DevicePublicKeys> deviceKeys; - - room_key_msgs.clear(); - deviceKeys.clear(); - - for (const auto &dev : user.second) { - const auto user_id = UserId(dev.second.user_id); - const auto device_id = DeviceId(dev.second.device_id); - - const auto device_keys = dev.second.keys; - const auto curveKey = "curve25519:" + device_id.get(); - const auto edKey = "ed25519:" + device_id.get(); - - if ((device_keys.find(curveKey) == device_keys.end()) || - (device_keys.find(edKey) == device_keys.end())) { - nhlog::net()->debug( - "ignoring malformed keys for device {}", - device_id.get()); - continue; - } - - DevicePublicKeys pks; - pks.ed25519 = device_keys.at(edKey); - pks.curve25519 = device_keys.at(curveKey); - - try { - if (!mtx::crypto::verify_identity_signature( - json(dev.second), device_id, user_id)) { - nhlog::crypto()->warn( - "failed to verify identity keys: {}", - json(dev.second).dump(2)); - continue; - } - } catch (const json::exception &e) { - nhlog::crypto()->warn( - "failed to parse device key json: {}", - e.what()); - continue; - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->warn( - "failed to verify device key json: {}", - e.what()); - continue; - } - - auto room_key = olm::client() - ->create_room_key_event( - user_id, pks.ed25519, megolm_payload) - .dump(); - - room_key_msgs.emplace(device_id, room_key); - deviceKeys.emplace(device_id, pks); - } - - std::vector<std::string> valid_devices; - valid_devices.reserve(room_key_msgs.size()); - for (auto const &d : room_key_msgs) { - valid_devices.push_back(d.first); - - nhlog::net()->info("{}", d.first); - nhlog::net()->info(" curve25519 {}", - deviceKeys.at(d.first).curve25519); - nhlog::net()->info(" ed25519 {}", - deviceKeys.at(d.first).ed25519); - } - - nhlog::net()->info( - "sending claim request for user {} with {} devices", - user.first, - valid_devices.size()); - - http::client()->claim_keys( - user.first, - valid_devices, - std::bind(&TimelineView::handleClaimedKeys, - this, - keeper, - room_key_msgs, - deviceKeys, - user.first, - std::placeholders::_1, - std::placeholders::_2)); - - // TODO: Wait before sending the next batch of requests. - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } - }); - - // TODO: Let the user know about the errors. - } catch (const lmdb::error &e) { - nhlog::db()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical( - "failed to open outbound megolm session ({}): {}", room_id, e.what()); - } -} - -void -TimelineView::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper, - const std::map<std::string, std::string> &room_keys, - const std::map<std::string, DevicePublicKeys> &pks, - const std::string &user_id, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err) -{ - if (err) { - nhlog::net()->warn("claim keys error: {} {} {}", - err->matrix_error.error, - err->parse_error, - static_cast<int>(err->status_code)); - return; - } - - nhlog::net()->debug("claimed keys for {}", user_id); - - if (res.one_time_keys.size() == 0) { - nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); - return; - } - - if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) { - nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); - return; - } - - auto retrieved_devices = res.one_time_keys.at(user_id); - - // Payload with all the to_device message to be sent. - json body; - body["messages"][user_id] = json::object(); - - for (const auto &rd : retrieved_devices) { - const auto device_id = rd.first; - nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2)); - - // TODO: Verify signatures - auto otk = rd.second.begin()->at("key"); - - if (pks.find(device_id) == pks.end()) { - nhlog::net()->critical("couldn't find public key for device: {}", - device_id); - continue; - } - - auto id_key = pks.at(device_id).curve25519; - auto s = olm::client()->create_outbound_session(id_key, otk); - - if (room_keys.find(device_id) == room_keys.end()) { - nhlog::net()->critical("couldn't find m.room_key for device: {}", - device_id); - continue; - } - - auto device_msg = olm::client()->create_olm_encrypted_content( - s.get(), room_keys.at(device_id), pks.at(device_id).curve25519); - - try { - cache::client()->saveOlmSession(id_key, std::move(s)); - } catch (const lmdb::error &e) { - nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); - } catch (const mtx::crypto::olm_exception &e) { - nhlog::crypto()->critical("failed to pickle outbound olm session: {}", - e.what()); - } - - body["messages"][user_id][device_id] = device_msg; - } - - nhlog::net()->info("send_to_device: {}", user_id); - - http::client()->send_to_device( - "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to send " - "send_to_device " - "message: {}", - err->matrix_error.error); - } - - (void)keeper; - }); -} diff --git a/src/timeline/TimelineView.h b/src/timeline/TimelineView.h deleted file mode 100644
index b0909b44..00000000 --- a/src/timeline/TimelineView.h +++ /dev/null
@@ -1,444 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#pragma once - -#include <QApplication> -#include <QLayout> -#include <QList> -#include <QQueue> -#include <QScrollArea> -#include <QScrollBar> -#include <QStyle> -#include <QStyleOption> -#include <QTimer> - -#include <mtx/events.hpp> -#include <mtx/responses/messages.hpp> - -#include "MatrixClient.h" -#include "timeline/TimelineItem.h" - -class StateKeeper -{ -public: - StateKeeper(std::function<void()> &&fn) - : fn_(std::move(fn)) - {} - - ~StateKeeper() { fn_(); } - -private: - std::function<void()> fn_; -}; - -struct DecryptionResult -{ - //! The decrypted content as a normal plaintext event. - utils::TimelineEvent event; - //! Whether or not the decryption was successful. - bool isDecrypted = false; -}; - -class FloatingButton; -struct DescInfo; - -// Contains info about a message shown in the history view -// but not yet confirmed by the homeserver through sync. -struct PendingMessage -{ - mtx::events::MessageType ty; - std::string txn_id; - QString body; - QString filename; - QString mime; - uint64_t media_size; - QString event_id; - TimelineItem *widget; - QSize dimensions; - bool is_encrypted = false; -}; - -template<class MessageT> -MessageT -toRoomMessage(const PendingMessage &) = delete; - -template<> -mtx::events::msg::Audio -toRoomMessage<mtx::events::msg::Audio>(const PendingMessage &m); - -template<> -mtx::events::msg::Emote -toRoomMessage<mtx::events::msg::Emote>(const PendingMessage &m); - -template<> -mtx::events::msg::File -toRoomMessage<mtx::events::msg::File>(const PendingMessage &); - -template<> -mtx::events::msg::Image -toRoomMessage<mtx::events::msg::Image>(const PendingMessage &m); - -template<> -mtx::events::msg::Text -toRoomMessage<mtx::events::msg::Text>(const PendingMessage &); - -template<> -mtx::events::msg::Video -toRoomMessage<mtx::events::msg::Video>(const PendingMessage &m); - -// In which place new TimelineItems should be inserted. -enum class TimelineDirection -{ - Top, - Bottom, -}; - -class TimelineView : public QWidget -{ - Q_OBJECT - -public: - TimelineView(const mtx::responses::Timeline &timeline, - const QString &room_id, - QWidget *parent = 0); - TimelineView(const QString &room_id, QWidget *parent = 0); - - // Add new events at the end of the timeline. - void addEvents(const mtx::responses::Timeline &timeline); - void addUserMessage(mtx::events::MessageType ty, const QString &msg); - - template<class Widget, mtx::events::MessageType MsgType> - void addUserMessage(const QString &url, - const QString &filename, - const QString &mime, - uint64_t size, - const QSize &dimensions = QSize()); - void updatePendingMessage(const std::string &txn_id, const QString &event_id); - void scrollDown(); - - //! Remove an item from the timeline with the given Event ID. - void removeEvent(const QString &event_id); - void setPrevBatchToken(const QString &token) { prev_batch_token_ = token; } - -public slots: - void sliderRangeChanged(int min, int max); - void sliderMoved(int position); - void fetchHistory(); - - // Add old events at the top of the timeline. - void addBackwardsEvents(const mtx::responses::Messages &msgs); - - // Whether or not the initial batch has been loaded. - bool hasLoaded() { return scroll_layout_->count() > 0 || isTimelineFinished; } - - void handleFailedMessage(const std::string &txn_id); - -private slots: - void sendNextPendingMessage(); - -signals: - void updateLastTimelineMessage(const QString &user, const DescInfo &info); - void messagesRetrieved(const mtx::responses::Messages &res); - void messageFailed(const std::string &txn_id); - void messageSent(const std::string &txn_id, const QString &event_id); - void markReadEvents(const std::vector<QString> &event_ids); - -protected: - void paintEvent(QPaintEvent *event) override; - void showEvent(QShowEvent *event) override; - void hideEvent(QHideEvent *event) override; - bool event(QEvent *event) override; - -private: - using TimelineEvent = mtx::events::collections::TimelineEvents; - - //! Mark our own widgets as read if they have more than one receipt. - void displayReadReceipts(std::vector<TimelineEvent> events); - //! Determine if the start of the timeline is reached from the response of /messages. - bool isStartOfTimeline(const mtx::responses::Messages &msgs); - - QWidget *relativeWidget(QWidget *item, int dt) const; - - DecryptionResult parseEncryptedEvent( - const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e); - - void handleClaimedKeys(std::shared_ptr<StateKeeper> keeper, - const std::map<std::string, std::string> &room_key, - const std::map<std::string, DevicePublicKeys> &pks, - const std::string &user_id, - const mtx::responses::ClaimKeys &res, - mtx::http::RequestErr err); - - //! Callback for all message sending. - void sendRoomMessageHandler(const std::string &txn_id, - const mtx::responses::EventId &res, - mtx::http::RequestErr err); - void prepareEncryptedMessage(const PendingMessage &msg); - - //! Call the /messages endpoint to fill the timeline. - void getMessages(); - //! HACK: Fixing layout flickering when adding to the bottom - //! of the timeline. - void pushTimelineItem(QWidget *item, TimelineDirection dir) - { - setUpdatesEnabled(false); - item->hide(); - - if (dir == TimelineDirection::Top) - scroll_layout_->insertWidget(0, item); - else - scroll_layout_->addWidget(item); - - QTimer::singleShot(0, this, [item, this]() { - item->show(); - item->adjustSize(); - setUpdatesEnabled(true); - }); - } - - //! Decides whether or not to show or hide the scroll down button. - void toggleScrollDownButton(); - void init(); - void addTimelineItem(QWidget *item, - TimelineDirection direction = TimelineDirection::Bottom); - void updateLastSender(const QString &user_id, TimelineDirection direction); - void notifyForLastEvent(); - void notifyForLastEvent(const TimelineEvent &event); - //! Keep track of the sender and the timestamp of the current message. - void saveLastMessageInfo(const QString &sender, const QDateTime &datetime) - { - lastSender_ = sender; - lastMsgTimestamp_ = datetime; - } - void saveFirstMessageInfo(const QString &sender, const QDateTime &datetime) - { - firstSender_ = sender; - firstMsgTimestamp_ = datetime; - } - //! Keep track of the sender and the timestamp of the current message. - void saveMessageInfo(const QString &sender, - uint64_t origin_server_ts, - TimelineDirection direction); - - TimelineEvent findFirstViewableEvent(const std::vector<TimelineEvent> &events); - TimelineEvent findLastViewableEvent(const std::vector<TimelineEvent> &events); - - //! Mark the last event as read. - void readLastEvent() const; - //! Whether or not the scrollbar is visible (non-zero height). - bool isScrollbarActivated() { return scroll_area_->verticalScrollBar()->value() != 0; } - //! Retrieve the event id of the last item. - QString getLastEventId() const; - - template<class Event, class Widget> - TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction); - - // TODO: Remove this eventually. - template<class Event> - TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction); - - // For events with custom display widgets. - template<class Event, class Widget> - TimelineItem *createTimelineItem(const Event &event, bool withSender); - - // For events without custom display widgets. - // TODO: All events should have custom widgets. - template<class Event> - TimelineItem *createTimelineItem(const Event &event, bool withSender); - - // Used to determine whether or not we should prefix a message with the - // sender's name. - bool isSenderRendered(const QString &user_id, - uint64_t origin_server_ts, - TimelineDirection direction); - - bool isPendingMessage(const std::string &txn_id, - const QString &sender, - const QString &userid); - void removePendingMessage(const std::string &txn_id); - - bool isDuplicate(const QString &event_id) { return eventIds_.contains(event_id); } - - void handleNewUserMessage(PendingMessage msg); - bool isDateDifference(const QDateTime &first, - const QDateTime &second = QDateTime::currentDateTime()) const; - - // Return nullptr if the event couldn't be parsed. - QWidget *parseMessageEvent(const mtx::events::collections::TimelineEvents &event, - TimelineDirection direction); - - //! Store the event id associated with the given widget. - void saveEventId(QWidget *widget); - //! Remove all widgets from the timeline layout. - void clearTimeline(); - - QVBoxLayout *top_layout_; - QVBoxLayout *scroll_layout_; - - QScrollArea *scroll_area_; - QWidget *scroll_widget_; - - QString firstSender_; - QDateTime firstMsgTimestamp_; - QString lastSender_; - QDateTime lastMsgTimestamp_; - - QString room_id_; - QString prev_batch_token_; - QString local_user_; - - bool isPaginationInProgress_ = false; - - // Keeps track whether or not the user has visited the view. - bool isInitialized = false; - bool isTimelineFinished = false; - bool isInitialSync = true; - - const int SCROLL_BAR_GAP = 200; - - QTimer *paginationTimer_; - - int scroll_height_ = 0; - int previous_max_height_ = 0; - - int oldPosition_; - int oldHeight_; - - FloatingButton *scrollDownBtn_; - - TimelineDirection lastMessageDirection_; - - //! Messages received by sync not added to the timeline. - std::vector<TimelineEvent> bottomMessages_; - //! Messages received by /messages not added to the timeline. - std::vector<TimelineEvent> topMessages_; - - //! Render the given timeline events to the bottom of the timeline. - void renderBottomEvents(const std::vector<TimelineEvent> &events); - //! Render the given timeline events to the top of the timeline. - void renderTopEvents(const std::vector<TimelineEvent> &events); - - // The events currently rendered. Used for duplicate detection. - QMap<QString, QWidget *> eventIds_; - QQueue<PendingMessage> pending_msgs_; - QList<PendingMessage> pending_sent_msgs_; -}; - -template<class Widget, mtx::events::MessageType MsgType> -void -TimelineView::addUserMessage(const QString &url, - const QString &filename, - const QString &mime, - uint64_t size, - const QSize &dimensions) -{ - auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_); - auto trimmed = QFileInfo{filename}.fileName(); // Trim file path. - - auto widget = new Widget(url, trimmed, size, this); - - TimelineItem *view_item = - new TimelineItem(widget, local_user_, with_sender, room_id_, scroll_widget_); - - addTimelineItem(view_item); - - lastMessageDirection_ = TimelineDirection::Bottom; - - // Keep track of the sender and the timestamp of the current message. - saveLastMessageInfo(local_user_, QDateTime::currentDateTime()); - - PendingMessage message; - message.ty = MsgType; - message.txn_id = http::client()->generate_txn_id(); - message.body = url; - message.filename = trimmed; - message.mime = mime; - message.media_size = size; - message.widget = view_item; - message.dimensions = dimensions; - - handleNewUserMessage(message); -} - -template<class Event> -TimelineItem * -TimelineView::createTimelineItem(const Event &event, bool withSender) -{ - TimelineItem *item = new TimelineItem(event, withSender, room_id_, scroll_widget_); - return item; -} - -template<class Event, class Widget> -TimelineItem * -TimelineView::createTimelineItem(const Event &event, bool withSender) -{ - auto eventWidget = new Widget(event); - auto item = new TimelineItem(eventWidget, event, withSender, room_id_, scroll_widget_); - - return item; -} - -template<class Event> -TimelineItem * -TimelineView::processMessageEvent(const Event &event, TimelineDirection direction) -{ - const auto event_id = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - const auto txn_id = event.unsigned_data.transaction_id; - if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) || - isDuplicate(event_id)) { - removePendingMessage(txn_id); - return nullptr; - } - - auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction); - - saveMessageInfo(sender, event.origin_server_ts, direction); - - auto item = createTimelineItem<Event>(event, with_sender); - - eventIds_[event_id] = item; - - return item; -} - -template<class Event, class Widget> -TimelineItem * -TimelineView::processMessageEvent(const Event &event, TimelineDirection direction) -{ - const auto event_id = QString::fromStdString(event.event_id); - const auto sender = QString::fromStdString(event.sender); - - const auto txn_id = event.unsigned_data.transaction_id; - if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) || - isDuplicate(event_id)) { - removePendingMessage(txn_id); - return nullptr; - } - - auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction); - - saveMessageInfo(sender, event.origin_server_ts, direction); - - auto item = createTimelineItem<Event, Widget>(event, with_sender); - - eventIds_[event_id] = item; - - return item; -} diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index feab46a3..a3827501 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp
@@ -1,327 +1,355 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ +#include "TimelineViewManager.h" -#include <random> +#include <QMetaType> +#include <QPalette> +#include <QQmlContext> -#include <QApplication> -#include <QFileInfo> -#include <QSettings> - -#include "Cache.h" +#include "ChatPage.h" +#include "ColorImageProvider.h" +#include "DelegateChooser.h" #include "Logging.h" -#include "timeline/TimelineView.h" -#include "timeline/TimelineViewManager.h" -#include "timeline/widgets/AudioItem.h" -#include "timeline/widgets/FileItem.h" -#include "timeline/widgets/ImageItem.h" -#include "timeline/widgets/VideoItem.h" +#include "MatrixClient.h" +#include "MxcImageProvider.h" +#include "UserSettingsPage.h" +#include "dialogs/ImageOverlay.h" -TimelineViewManager::TimelineViewManager(QWidget *parent) - : QStackedWidget(parent) -{} +Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents) void -TimelineViewManager::updateReadReceipts(const QString &room_id, - const std::vector<QString> &event_ids) +TimelineViewManager::updateColorPalette() { - if (timelineViewExists(room_id)) { - auto view = views_[room_id]; - if (view) - emit view->markReadEvents(event_ids); + userColors.clear(); + + if (settings->theme() == "light") { + QPalette lightActive(/*windowText*/ QColor("#333"), + /*button*/ QColor("#333"), + /*light*/ QColor(), + /*dark*/ QColor(220, 220, 220), + /*mid*/ QColor(), + /*text*/ QColor("#333"), + /*bright_text*/ QColor(), + /*base*/ QColor(220, 220, 220), + /*window*/ QColor("white")); + lightActive.setColor(QPalette::ToolTipBase, lightActive.base().color()); + lightActive.setColor(QPalette::ToolTipText, lightActive.text().color()); + lightActive.setColor(QPalette::Link, QColor("#0077b5")); + view->rootContext()->setContextProperty("currentActivePalette", lightActive); + view->rootContext()->setContextProperty("currentInactivePalette", lightActive); + } else if (settings->theme() == "dark") { + QPalette darkActive(/*windowText*/ QColor("#caccd1"), + /*button*/ QColor("#caccd1"), + /*light*/ QColor(), + /*dark*/ QColor("#2d3139"), + /*mid*/ QColor(), + /*text*/ QColor("#caccd1"), + /*bright_text*/ QColor(), + /*base*/ QColor("#2d3139"), + /*window*/ QColor("#202228")); + darkActive.setColor(QPalette::Highlight, QColor("#e7e7e9")); + darkActive.setColor(QPalette::ToolTipBase, darkActive.base().color()); + darkActive.setColor(QPalette::ToolTipText, darkActive.text().color()); + darkActive.setColor(QPalette::Link, QColor("#38a3d8")); + view->rootContext()->setContextProperty("currentActivePalette", darkActive); + view->rootContext()->setContextProperty("currentInactivePalette", darkActive); + } else { + view->rootContext()->setContextProperty("currentActivePalette", QPalette()); + view->rootContext()->setContextProperty("currentInactivePalette", nullptr); } } -void -TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id) +QColor +TimelineViewManager::userColor(QString id, QColor background) { - auto view = views_[room_id]; - - if (view) - view->removeEvent(event_id); + if (!userColors.contains(id)) + userColors.insert( + id, QColor(utils::generateContrastingHexColor(id, background.name()))); + return userColors.value(id); } -void -TimelineViewManager::queueTextMessage(const QString &msg) +TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettings, QWidget *parent) + : imgProvider(new MxcImageProvider()) + , colorImgProvider(new ColorImageProvider()) + , settings(userSettings) { - if (active_room_.isEmpty()) - return; + qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, + "im.nheko", + 1, + 0, + "MtxEvent", + "Can't instantiate enum!"); + qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice"); + qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser"); + qRegisterMetaType<mtx::events::collections::TimelineEvents>(); + +#ifdef USE_QUICK_VIEW + view = new QQuickView(); + container = QWidget::createWindowContainer(view, parent); +#else + view = new QQuickWidget(parent); + container = view; + view->setResizeMode(QQuickWidget::SizeRootObjectToView); + container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - auto room_id = active_room_; - auto view = views_[room_id]; + connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) { + nhlog::ui()->debug("Status changed to {}", status); + }); +#endif + container->setMinimumSize(200, 200); + view->rootContext()->setContextProperty("timelineManager", this); + updateColorPalette(); + view->engine()->addImageProvider("MxcImage", imgProvider); + view->engine()->addImageProvider("colorimage", colorImgProvider); + view->setSource(QUrl("qrc:///qml/TimelineView.qml")); - view->addUserMessage(mtx::events::MessageType::Text, msg); + connect(dynamic_cast<ChatPage *>(parent), + &ChatPage::themeChanged, + this, + &TimelineViewManager::updateColorPalette); } void -TimelineViewManager::queueEmoteMessage(const QString &msg) +TimelineViewManager::sync(const mtx::responses::Rooms &rooms) { - if (active_room_.isEmpty()) - return; + for (const auto &[room_id, room] : rooms.join) { + // addRoom will only add the room, if it doesn't exist + addRoom(QString::fromStdString(room_id)); + const auto &room_model = models.value(QString::fromStdString(room_id)); + room_model->addEvents(room.timeline); - auto room_id = active_room_; - auto view = views_[room_id]; + if (ChatPage::instance()->userSettings()->isTypingNotificationsEnabled()) { + std::vector<QString> typing; + typing.reserve(room.ephemeral.typing.size()); + for (const auto &user : room.ephemeral.typing) { + if (user != http::client()->user_id().to_string()) + typing.push_back(QString::fromStdString(user)); + } + room_model->updateTypingUsers(typing); + } + } - view->addUserMessage(mtx::events::MessageType::Emote, msg); + this->isInitialSync_ = false; + emit initialSyncChanged(false); } void -TimelineViewManager::queueImageMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size, - const QSize &dimensions) +TimelineViewManager::addRoom(const QString &room_id) { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("Cannot send m.image message to a non-managed view"); - return; + if (!models.contains(room_id)) { + QSharedPointer<TimelineModel> newRoom(new TimelineModel(this, room_id)); + connect(newRoom.data(), + &TimelineModel::newEncryptedImage, + imgProvider, + &MxcImageProvider::addEncryptionInfo); + models.insert(room_id, std::move(newRoom)); } - - auto view = views_[roomid]; - - view->addUserMessage<ImageItem, mtx::events::MessageType::Image>( - url, filename, mime, size, dimensions); } void -TimelineViewManager::queueFileMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size) +TimelineViewManager::setHistoryView(const QString &room_id) { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.file message to a non-managed view"); - return; - } - - auto view = views_[roomid]; + nhlog::ui()->info("Trying to activate room {}", room_id.toStdString()); - view->addUserMessage<FileItem, mtx::events::MessageType::File>(url, filename, mime, size); + auto room = models.find(room_id); + if (room != models.end()) { + timeline_ = room.value().data(); + emit activeTimelineChanged(timeline_); + nhlog::ui()->info("Activated room {}", room_id.toStdString()); + } } void -TimelineViewManager::queueAudioMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size) +TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) const { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.audio message to a non-managed view"); - return; - } - - auto view = views_[roomid]; + QQuickImageResponse *imgResponse = + imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize()); + connect(imgResponse, &QQuickImageResponse::finished, this, [this, eventId, imgResponse]() { + if (!imgResponse->errorString().isEmpty()) { + nhlog::ui()->error("Error when retrieving image for overlay: {}", + imgResponse->errorString().toStdString()); + return; + } + auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image()); - view->addUserMessage<AudioItem, mtx::events::MessageType::Audio>(url, filename, mime, size); + auto imgDialog = new dialogs::ImageOverlay(pixmap); + imgDialog->showFullScreen(); + connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId]() { + timeline_->saveMedia(eventId); + }); + }); } void -TimelineViewManager::queueVideoMessage(const QString &roomid, - const QString &filename, - const QString &url, - const QString &mime, - uint64_t size) +TimelineViewManager::updateReadReceipts(const QString &room_id, + const std::vector<QString> &event_ids) { - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("cannot send m.video message to a non-managed view"); - return; + auto room = models.find(room_id); + if (room != models.end()) { + room.value()->markEventsAsRead(event_ids); } - - auto view = views_[roomid]; - - view->addUserMessage<VideoItem, mtx::events::MessageType::Video>(url, filename, mime, size); } void -TimelineViewManager::initialize(const mtx::responses::Rooms &rooms) +TimelineViewManager::initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs) { - for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { - addRoom(it->second, QString::fromStdString(it->first)); - } + for (const auto &e : msgs) { + addRoom(e.first); - sync(rooms); + models.value(e.first)->addEvents(e.second); + } } void -TimelineViewManager::initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs) +TimelineViewManager::queueTextMessage(const QString &msg, const std::optional<RelatedInfo> &related) { - for (auto it = msgs.cbegin(); it != msgs.cend(); ++it) { - if (timelineViewExists(it->first)) - return; + mtx::events::msg::Text text = {}; + text.body = msg.trimmed().toStdString(); - // Create a history view with the room events. - TimelineView *view = new TimelineView(it->second, it->first); - views_.emplace(it->first, QSharedPointer<TimelineView>(view)); + if (settings->isMarkdownEnabled()) { + text.formatted_body = utils::markdownToHtml(msg).toStdString(); - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); - - // Add the view in the widget stack. - addWidget(view); + // Don't send formatted_body, when we don't need to + if (text.formatted_body.find("<") == std::string::npos) + text.formatted_body = ""; + else + text.format = "org.matrix.custom.html"; } -} -void -TimelineViewManager::initialize(const std::vector<std::string> &rooms) -{ - for (const auto &roomid : rooms) - addRoom(QString::fromStdString(roomid)); -} + if (related) { + QString body; + bool firstLine = true; + for (const auto &line : related->quoted_body.split("\n")) { + if (firstLine) { + firstLine = false; + body = QString("> <%1> %2\n").arg(related->quoted_user).arg(line); + } else { + body = QString("%1\n> %2\n").arg(body).arg(line); + } + } -void -TimelineViewManager::addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id) -{ - if (timelineViewExists(room_id)) - return; + text.body = QString("%1\n%2").arg(body).arg(msg).toStdString(); - // Create a history view with the room events. - TimelineView *view = new TimelineView(room.timeline, room_id); - views_.emplace(room_id, QSharedPointer<TimelineView>(view)); + // NOTE(Nico): rich replies always need a formatted_body! + text.format = "org.matrix.custom.html"; + if (settings->isMarkdownEnabled()) + text.formatted_body = + utils::getFormattedQuoteBody(*related, utils::markdownToHtml(msg)) + .toStdString(); + else + text.formatted_body = + utils::getFormattedQuoteBody(*related, msg.toHtmlEscaped()).toStdString(); - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); + text.relates_to.in_reply_to.event_id = related->related_event; + } - // Add the view in the widget stack. - addWidget(view); + if (timeline_) + timeline_->sendMessage(text); } void -TimelineViewManager::addRoom(const QString &room_id) +TimelineViewManager::queueEmoteMessage(const QString &msg) { - if (timelineViewExists(room_id)) - return; + auto html = utils::markdownToHtml(msg); - // Create a history view without any events. - TimelineView *view = new TimelineView(room_id); - views_.emplace(room_id, QSharedPointer<TimelineView>(view)); + mtx::events::msg::Emote emote; + emote.body = msg.trimmed().toStdString(); - connect(view, - &TimelineView::updateLastTimelineMessage, - this, - &TimelineViewManager::updateRoomsLastMessage); + if (html != msg.trimmed().toHtmlEscaped() && settings->isMarkdownEnabled()) { + emote.formatted_body = html.toStdString(); + emote.format = "org.matrix.custom.html"; + } - // Add the view in the widget stack. - addWidget(view); + if (timeline_) + timeline_->sendMessage(emote); } void -TimelineViewManager::sync(const mtx::responses::Rooms &rooms) +TimelineViewManager::queueImageMessage(const QString &roomid, + const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &file, + const QString &url, + const QString &mime, + uint64_t dsize, + const QSize &dimensions, + const std::optional<RelatedInfo> &related) { - for (const auto &room : rooms.join) { - auto roomid = QString::fromStdString(room.first); - - if (!timelineViewExists(roomid)) { - nhlog::ui()->warn("ignoring event from unknown room: {}", - roomid.toStdString()); - continue; - } + mtx::events::msg::Image image; + image.info.mimetype = mime.toStdString(); + image.info.size = dsize; + image.body = filename.toStdString(); + image.url = url.toStdString(); + image.info.h = dimensions.height(); + image.info.w = dimensions.width(); + image.file = file; - auto view = views_.at(roomid); + if (related) + image.relates_to.in_reply_to.event_id = related->related_event; - view->addEvents(room.second.timeline); - } + models.value(roomid)->sendMessage(image); } void -TimelineViewManager::setHistoryView(const QString &room_id) +TimelineViewManager::queueFileMessage( + const QString &roomid, + const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &encryptedFile, + const QString &url, + const QString &mime, + uint64_t dsize, + const std::optional<RelatedInfo> &related) { - if (!timelineViewExists(room_id)) { - nhlog::ui()->warn("room from RoomList is not present in ViewManager: {}", - room_id.toStdString()); - return; - } - - active_room_ = room_id; - auto view = views_.at(room_id); + mtx::events::msg::File file; + file.info.mimetype = mime.toStdString(); + file.info.size = dsize; + file.body = filename.toStdString(); + file.url = url.toStdString(); + file.file = encryptedFile; - setCurrentWidget(view.data()); + if (related) + file.relates_to.in_reply_to.event_id = related->related_event; - view->fetchHistory(); - view->scrollDown(); + models.value(roomid)->sendMessage(file); } -QString -TimelineViewManager::chooseRandomColor() +void +TimelineViewManager::queueAudioMessage(const QString &roomid, + const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &file, + const QString &url, + const QString &mime, + uint64_t dsize, + const std::optional<RelatedInfo> &related) { - std::random_device random_device; - std::mt19937 engine{random_device()}; - std::uniform_real_distribution<float> dist(0, 1); - - float hue = dist(engine); - float saturation = 0.9; - float value = 0.7; - - int hue_i = hue * 6; + mtx::events::msg::Audio audio; + audio.info.mimetype = mime.toStdString(); + audio.info.size = dsize; + audio.body = filename.toStdString(); + audio.url = url.toStdString(); + audio.file = file; - float f = hue * 6 - hue_i; + if (related) + audio.relates_to.in_reply_to.event_id = related->related_event; - float p = value * (1 - saturation); - float q = value * (1 - f * saturation); - float t = value * (1 - (1 - f) * saturation); - - float r = 0; - float g = 0; - float b = 0; - - if (hue_i == 0) { - r = value; - g = t; - b = p; - } else if (hue_i == 1) { - r = q; - g = value; - b = p; - } else if (hue_i == 2) { - r = p; - g = value; - b = t; - } else if (hue_i == 3) { - r = p; - g = q; - b = value; - } else if (hue_i == 4) { - r = t; - g = p; - b = value; - } else if (hue_i == 5) { - r = value; - g = p; - b = q; - } - - int ri = r * 256; - int gi = g * 256; - int bi = b * 256; - - QColor color(ri, gi, bi); - - return color.name(); + models.value(roomid)->sendMessage(audio); } -bool -TimelineViewManager::hasLoaded() const +void +TimelineViewManager::queueVideoMessage(const QString &roomid, + const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &file, + const QString &url, + const QString &mime, + uint64_t dsize, + const std::optional<RelatedInfo> &related) { - return std::all_of(views_.cbegin(), views_.cend(), [](const auto &view) { - return view.second->hasLoaded(); - }); + mtx::events::msg::Video video; + video.info.mimetype = mime.toStdString(); + video.info.size = dsize; + video.body = filename.toStdString(); + video.url = url.toStdString(); + video.file = file; + + if (related) + video.relates_to.in_reply_to.event_id = related->related_event; + + models.value(roomid)->sendMessage(video); } diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index d23345d3..338101c7 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h
@@ -1,95 +1,123 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - #pragma once +#include <QQuickView> +#include <QQuickWidget> #include <QSharedPointer> -#include <QStackedWidget> +#include <QWidget> -#include <mtx.hpp> +#include <mtx/common.hpp> +#include <mtx/responses.hpp> -class QFile; +#include "Cache.h" +#include "Logging.h" +#include "TimelineModel.h" +#include "Utils.h" -class RoomInfoListItem; -class TimelineView; -struct DescInfo; -struct SavedMessages; +class MxcImageProvider; +class ColorImageProvider; +class UserSettings; -class TimelineViewManager : public QStackedWidget +class TimelineViewManager : public QObject { Q_OBJECT -public: - TimelineViewManager(QWidget *parent); - - // Initialize with timeline events. - void initialize(const mtx::responses::Rooms &rooms); - // Empty initialization. - void initialize(const std::vector<std::string> &rooms); + Q_PROPERTY( + TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged) + Q_PROPERTY( + bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged) + Q_PROPERTY(QString replyingEvent READ getReplyingEvent WRITE updateReplyingEvent NOTIFY + replyingEventChanged) - void addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id); - void addRoom(const QString &room_id); +public: + TimelineViewManager(QSharedPointer<UserSettings> userSettings, QWidget *parent = nullptr); + QWidget *getWidget() const { return container; } void sync(const mtx::responses::Rooms &rooms); - void clearAll() { views_.clear(); } + void addRoom(const QString &room_id); - // Check if all the timelines have been loaded. - bool hasLoaded() const; + void clearAll() { models.clear(); } - static QString chooseRandomColor(); + Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } + Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } + Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId) const; + Q_INVOKABLE QColor userColor(QString id, QColor background); signals: void clearRoomMessageCount(QString roomid); - void updateRoomsLastMessage(const QString &user, const DescInfo &info); + void updateRoomsLastMessage(QString roomid, const DescInfo &info); + void activeTimelineChanged(TimelineModel *timeline); + void initialSyncChanged(bool isInitialSync); + void replyingEventChanged(QString replyingEvent); + void replyClosed(); public slots: + void updateReplyingEvent(const QString &replyingEvent) + { + if (this->replyingEvent_ != replyingEvent) { + this->replyingEvent_ = replyingEvent; + emit replyingEventChanged(replyingEvent_); + } + } + void closeReply() + { + this->updateReplyingEvent(nullptr); + emit replyClosed(); + } + QString getReplyingEvent() const { return replyingEvent_; } void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids); - void removeTimelineEvent(const QString &room_id, const QString &event_id); void initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs); void setHistoryView(const QString &room_id); - void queueTextMessage(const QString &msg); + void updateColorPalette(); + + void queueTextMessage(const QString &msg, const std::optional<RelatedInfo> &related); void queueEmoteMessage(const QString &msg); void queueImageMessage(const QString &roomid, const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &file, const QString &url, const QString &mime, uint64_t dsize, - const QSize &dimensions); + const QSize &dimensions, + const std::optional<RelatedInfo> &related); void queueFileMessage(const QString &roomid, const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &file, const QString &url, const QString &mime, - uint64_t dsize); + uint64_t dsize, + const std::optional<RelatedInfo> &related); void queueAudioMessage(const QString &roomid, const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &file, const QString &url, const QString &mime, - uint64_t dsize); + uint64_t dsize, + const std::optional<RelatedInfo> &related); void queueVideoMessage(const QString &roomid, const QString &filename, + const std::optional<mtx::crypto::EncryptedFile> &file, const QString &url, const QString &mime, - uint64_t dsize); + uint64_t dsize, + const std::optional<RelatedInfo> &related); private: - //! Check if the given room id is managed by a TimelineView. - bool timelineViewExists(const QString &id) { return views_.find(id) != views_.end(); } +#ifdef USE_QUICK_VIEW + QQuickView *view; +#else + QQuickWidget *view; +#endif + QWidget *container; + + MxcImageProvider *imgProvider; + ColorImageProvider *colorImgProvider; + + QHash<QString, QSharedPointer<TimelineModel>> models; + TimelineModel *timeline_ = nullptr; + bool isInitialSync_ = true; + QString replyingEvent_; - QString active_room_; - std::map<QString, QSharedPointer<TimelineView>> views_; + QSharedPointer<UserSettings> settings; + QHash<QString, QColor> userColors; }; diff --git a/src/timeline/widgets/AudioItem.cpp b/src/timeline/widgets/AudioItem.cpp deleted file mode 100644
index 72332174..00000000 --- a/src/timeline/widgets/AudioItem.cpp +++ /dev/null
@@ -1,230 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#include <QBrush> -#include <QDesktopServices> -#include <QFile> -#include <QFileDialog> -#include <QPainter> -#include <QPixmap> - -#include "Logging.h" -#include "MatrixClient.h" -#include "Utils.h" - -#include "timeline/widgets/AudioItem.h" - -constexpr int MaxWidth = 400; -constexpr int Height = 70; -constexpr int IconRadius = 22; -constexpr int IconDiameter = IconRadius * 2; -constexpr int HorizontalPadding = 12; -constexpr int TextPadding = 15; -constexpr int ActionIconRadius = IconRadius - 4; - -constexpr double VerticalPadding = Height - 2 * IconRadius; -constexpr double IconYCenter = Height / 2; -constexpr double IconXCenter = HorizontalPadding + IconRadius; - -void -AudioItem::init() -{ - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - playIcon_.addFile(":/icons/icons/ui/play-sign.png"); - pauseIcon_.addFile(":/icons/icons/ui/pause-symbol.png"); - - player_ = new QMediaPlayer; - player_->setMedia(QUrl(url_)); - player_->setVolume(100); - player_->setNotifyInterval(1000); - - connect(player_, &QMediaPlayer::stateChanged, this, [this](QMediaPlayer::State state) { - if (state == QMediaPlayer::StoppedState) { - state_ = AudioState::Play; - player_->setMedia(QUrl(url_)); - update(); - } - }); - - setFixedHeight(Height); -} - -AudioItem::AudioItem(const mtx::events::RoomEvent<mtx::events::msg::Audio> &event, QWidget *parent) - : QWidget(parent) - , url_{QUrl(QString::fromStdString(event.content.url))} - , text_{QString::fromStdString(event.content.body)} - , event_{event} -{ - readableFileSize_ = utils::humanReadableFileSize(event.content.info.size); - - init(); -} - -AudioItem::AudioItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - readableFileSize_ = utils::humanReadableFileSize(size); - - init(); -} - -QSize -AudioItem::sizeHint() const -{ - return QSize(MaxWidth, Height); -} - -void -AudioItem::mousePressEvent(QMouseEvent *event) -{ - if (event->button() != Qt::LeftButton) - return; - - auto point = event->pos(); - - // Click on the download icon. - if (QRect(HorizontalPadding, VerticalPadding / 2, IconDiameter, IconDiameter) - .contains(point)) { - if (state_ == AudioState::Play) { - state_ = AudioState::Pause; - player_->play(); - } else { - state_ = AudioState::Play; - player_->pause(); - } - - update(); - } else { - filenameToSave_ = QFileDialog::getSaveFileName(this, tr("Save File"), text_); - - if (filenameToSave_.isEmpty()) - return; - - auto proxy = std::make_shared<MediaProxy>(); - connect(proxy.get(), &MediaProxy::fileDownloaded, this, &AudioItem::fileDownloaded); - - http::client()->download( - url_.toString().toStdString(), - [proxy = std::move(proxy), url = url_](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->info("failed to retrieve m.audio content: {}", - url.toString().toStdString()); - return; - } - - emit proxy->fileDownloaded(QByteArray(data.data(), data.size())); - }); - } -} - -void -AudioItem::fileDownloaded(const QByteArray &data) -{ - try { - QFile file(filenameToSave_); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(data); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("error while saving file: {}", e.what()); - } -} - -void -AudioItem::resizeEvent(QResizeEvent *event) -{ - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); - const int computedWidth = std::min( - fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth); - - resize(computedWidth, Height); - - event->accept(); -} - -void -AudioItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); - - QPainterPath path; - path.addRoundedRect(QRectF(0, 0, width(), height()), 10, 10); - - painter.setPen(Qt::NoPen); - painter.fillPath(path, backgroundColor_); - painter.drawPath(path); - - QPainterPath circle; - circle.addEllipse(QPoint(IconXCenter, IconYCenter), IconRadius, IconRadius); - - painter.setPen(Qt::NoPen); - painter.fillPath(circle, iconColor_); - painter.drawPath(circle); - - QIcon icon_; - if (state_ == AudioState::Play) - icon_ = playIcon_; - else - icon_ = pauseIcon_; - - icon_.paint(&painter, - QRect(IconXCenter - ActionIconRadius / 2, - IconYCenter - ActionIconRadius / 2, - ActionIconRadius, - ActionIconRadius), - Qt::AlignCenter, - QIcon::Normal); - - const int textStartX = HorizontalPadding + 2 * IconRadius + TextPadding; - const int textStartY = VerticalPadding + fm.ascent() / 2; - - // Draw the filename. - QString elidedText = fm.elidedText( - text_, Qt::ElideRight, width() - HorizontalPadding * 2 - TextPadding - 2 * IconRadius); - - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY), elidedText); - - // Draw the filesize. - font.setWeight(QFont::Normal); - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY + 1.5 * fm.ascent()), readableFileSize_); -} diff --git a/src/timeline/widgets/AudioItem.h b/src/timeline/widgets/AudioItem.h deleted file mode 100644
index c32b7731..00000000 --- a/src/timeline/widgets/AudioItem.h +++ /dev/null
@@ -1,104 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#pragma once - -#include <QEvent> -#include <QIcon> -#include <QMediaPlayer> -#include <QMouseEvent> -#include <QSharedPointer> -#include <QWidget> - -#include <mtx.hpp> - -class AudioItem : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - - Q_PROPERTY(QColor durationBackgroundColor WRITE setDurationBackgroundColor READ - durationBackgroundColor) - Q_PROPERTY(QColor durationForegroundColor WRITE setDurationForegroundColor READ - durationForegroundColor) - -public: - AudioItem(const mtx::events::RoomEvent<mtx::events::msg::Audio> &event, - QWidget *parent = nullptr); - - AudioItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - - void setTextColor(const QColor &color) { textColor_ = color; } - void setIconColor(const QColor &color) { iconColor_ = color; } - void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } - - void setDurationBackgroundColor(const QColor &color) { durationBgColor_ = color; } - void setDurationForegroundColor(const QColor &color) { durationFgColor_ = color; } - - QColor textColor() const { return textColor_; } - QColor iconColor() const { return iconColor_; } - QColor backgroundColor() const { return backgroundColor_; } - - QColor durationBackgroundColor() const { return durationBgColor_; } - QColor durationForegroundColor() const { return durationFgColor_; } - -protected: - void paintEvent(QPaintEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - -private slots: - void fileDownloaded(const QByteArray &data); - -private: - void init(); - - enum class AudioState - { - Play, - Pause, - }; - - AudioState state_ = AudioState::Play; - - QUrl url_; - QString text_; - QString readableFileSize_; - QString filenameToSave_; - - mtx::events::RoomEvent<mtx::events::msg::Audio> event_; - - QMediaPlayer *player_; - - QIcon playIcon_; - QIcon pauseIcon_; - - QColor textColor_ = QColor("white"); - QColor iconColor_ = QColor("#38A3D8"); - QColor backgroundColor_ = QColor("#333"); - - QColor durationBgColor_ = QColor("black"); - QColor durationFgColor_ = QColor("blue"); -}; diff --git a/src/timeline/widgets/FileItem.cpp b/src/timeline/widgets/FileItem.cpp deleted file mode 100644
index e97554e2..00000000 --- a/src/timeline/widgets/FileItem.cpp +++ /dev/null
@@ -1,215 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#include <QBrush> -#include <QDesktopServices> -#include <QFile> -#include <QFileDialog> -#include <QPainter> -#include <QPixmap> - -#include "Logging.h" -#include "MatrixClient.h" -#include "Utils.h" - -#include "timeline/widgets/FileItem.h" - -constexpr int MaxWidth = 400; -constexpr int Height = 70; -constexpr int IconRadius = 22; -constexpr int IconDiameter = IconRadius * 2; -constexpr int HorizontalPadding = 12; -constexpr int TextPadding = 15; -constexpr int DownloadIconRadius = IconRadius - 4; - -constexpr double VerticalPadding = Height - 2 * IconRadius; -constexpr double IconYCenter = Height / 2; -constexpr double IconXCenter = HorizontalPadding + IconRadius; - -void -FileItem::init() -{ - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - icon_.addFile(":/icons/icons/ui/arrow-pointing-down.png"); - - setFixedHeight(Height); -} - -FileItem::FileItem(const mtx::events::RoomEvent<mtx::events::msg::File> &event, QWidget *parent) - : QWidget(parent) - , url_{QString::fromStdString(event.content.url)} - , text_{QString::fromStdString(event.content.body)} - , event_{event} -{ - readableFileSize_ = utils::humanReadableFileSize(event.content.info.size); - - init(); -} - -FileItem::FileItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - readableFileSize_ = utils::humanReadableFileSize(size); - - init(); -} - -void -FileItem::openUrl() -{ - if (url_.toString().isEmpty()) - return; - - auto urlToOpen = utils::mxcToHttp( - url_, QString::fromStdString(http::client()->server()), http::client()->port()); - - if (!QDesktopServices::openUrl(urlToOpen)) - nhlog::ui()->warn("Could not open url: {}", urlToOpen.toStdString()); -} - -QSize -FileItem::sizeHint() const -{ - return QSize(MaxWidth, Height); -} - -void -FileItem::mousePressEvent(QMouseEvent *event) -{ - if (event->button() != Qt::LeftButton) - return; - - auto point = event->pos(); - - // Click on the download icon. - if (QRect(HorizontalPadding, VerticalPadding / 2, IconDiameter, IconDiameter) - .contains(point)) { - filenameToSave_ = QFileDialog::getSaveFileName(this, tr("Save File"), text_); - - if (filenameToSave_.isEmpty()) - return; - - auto proxy = std::make_shared<MediaProxy>(); - connect(proxy.get(), &MediaProxy::fileDownloaded, this, &FileItem::fileDownloaded); - - http::client()->download( - url_.toString().toStdString(), - [proxy = std::move(proxy), url = url_](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::ui()->warn("failed to retrieve m.file content: {}", - url.toString().toStdString()); - return; - } - - emit proxy->fileDownloaded(QByteArray(data.data(), data.size())); - }); - } else { - openUrl(); - } -} - -void -FileItem::fileDownloaded(const QByteArray &data) -{ - try { - QFile file(filenameToSave_); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(data); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } -} - -void -FileItem::resizeEvent(QResizeEvent *event) -{ - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); - const int computedWidth = std::min( - fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth); - - resize(computedWidth, Height); - - event->accept(); -} - -void -FileItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QFont font; - font.setWeight(QFont::Medium); - - QFontMetrics fm(font); - - QPainterPath path; - path.addRoundedRect(QRectF(0, 0, width(), height()), 10, 10); - - painter.setPen(Qt::NoPen); - painter.fillPath(path, backgroundColor_); - painter.drawPath(path); - - QPainterPath circle; - circle.addEllipse(QPoint(IconXCenter, IconYCenter), IconRadius, IconRadius); - - painter.setPen(Qt::NoPen); - painter.fillPath(circle, iconColor_); - painter.drawPath(circle); - - icon_.paint(&painter, - QRect(IconXCenter - DownloadIconRadius / 2, - IconYCenter - DownloadIconRadius / 2, - DownloadIconRadius, - DownloadIconRadius), - Qt::AlignCenter, - QIcon::Normal); - - const int textStartX = HorizontalPadding + 2 * IconRadius + TextPadding; - const int textStartY = VerticalPadding + fm.ascent() / 2; - - // Draw the filename. - QString elidedText = fm.elidedText( - text_, Qt::ElideRight, width() - HorizontalPadding * 2 - TextPadding - 2 * IconRadius); - - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY), elidedText); - - // Draw the filesize. - font.setWeight(QFont::Normal); - painter.setFont(font); - painter.setPen(QPen(textColor_)); - painter.drawText(QPoint(textStartX, textStartY + 1.5 * fm.ascent()), readableFileSize_); -} diff --git a/src/timeline/widgets/FileItem.h b/src/timeline/widgets/FileItem.h deleted file mode 100644
index d63cce88..00000000 --- a/src/timeline/widgets/FileItem.h +++ /dev/null
@@ -1,79 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#pragma once - -#include <QEvent> -#include <QIcon> -#include <QMouseEvent> -#include <QSharedPointer> -#include <QWidget> - -#include <mtx.hpp> - -class FileItem : public QWidget -{ - Q_OBJECT - - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) - Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor) - Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) - -public: - FileItem(const mtx::events::RoomEvent<mtx::events::msg::File> &event, - QWidget *parent = nullptr); - - FileItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - - void setTextColor(const QColor &color) { textColor_ = color; } - void setIconColor(const QColor &color) { iconColor_ = color; } - void setBackgroundColor(const QColor &color) { backgroundColor_ = color; } - - QColor textColor() const { return textColor_; } - QColor iconColor() const { return iconColor_; } - QColor backgroundColor() const { return backgroundColor_; } - -protected: - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - -private slots: - void fileDownloaded(const QByteArray &data); - -private: - void openUrl(); - void init(); - - QUrl url_; - QString text_; - QString readableFileSize_; - QString filenameToSave_; - - mtx::events::RoomEvent<mtx::events::msg::File> event_; - - QIcon icon_; - - QColor textColor_ = QColor("white"); - QColor iconColor_ = QColor("#38A3D8"); - QColor backgroundColor_ = QColor("#333"); -}; diff --git a/src/timeline/widgets/ImageItem.cpp b/src/timeline/widgets/ImageItem.cpp deleted file mode 100644
index 4ee9e42a..00000000 --- a/src/timeline/widgets/ImageItem.cpp +++ /dev/null
@@ -1,264 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#include <QBrush> -#include <QDesktopServices> -#include <QFileDialog> -#include <QFileInfo> -#include <QPainter> -#include <QPixmap> -#include <QUuid> - -#include "Config.h" -#include "ImageItem.h" -#include "Logging.h" -#include "MatrixClient.h" -#include "Utils.h" -#include "dialogs/ImageOverlay.h" - -void -ImageItem::downloadMedia(const QUrl &url) -{ - auto proxy = std::make_shared<MediaProxy>(); - connect(proxy.get(), &MediaProxy::imageDownloaded, this, &ImageItem::setImage); - - http::client()->download(url.toString().toStdString(), - [proxy = std::move(proxy), url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn( - "failed to retrieve image {}: {} {}", - url.toString().toStdString(), - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - QPixmap img; - img.loadFromData(QByteArray(data.data(), data.size())); - - emit proxy->imageDownloaded(img); - }); -} - -void -ImageItem::saveImage(const QString &filename, const QByteArray &data) -{ - try { - QFile file(filename); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(data); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } -} - -void -ImageItem::init() -{ - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); - - downloadMedia(url_); -} - -ImageItem::ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &event, QWidget *parent) - : QWidget(parent) - , event_{event} -{ - url_ = QString::fromStdString(event.content.url); - text_ = QString::fromStdString(event.content.body); - - init(); -} - -ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - Q_UNUSED(size); - init(); -} - -void -ImageItem::openUrl() -{ - if (url_.toString().isEmpty()) - return; - - auto urlToOpen = utils::mxcToHttp( - url_, QString::fromStdString(http::client()->server()), http::client()->port()); - - if (!QDesktopServices::openUrl(urlToOpen)) - nhlog::ui()->warn("could not open url: {}", urlToOpen.toStdString()); -} - -QSize -ImageItem::sizeHint() const -{ - if (image_.isNull()) - return QSize(max_width_, bottom_height_); - - return QSize(width_, height_); -} - -void -ImageItem::setImage(const QPixmap &image) -{ - image_ = image; - scaled_image_ = utils::scaleDown(max_width_, max_height_, image_); - - width_ = scaled_image_.width(); - height_ = scaled_image_.height(); - - setFixedSize(width_, height_); - update(); -} - -void -ImageItem::mousePressEvent(QMouseEvent *event) -{ - if (!isInteractive_) { - event->accept(); - return; - } - - if (event->button() != Qt::LeftButton) - return; - - if (image_.isNull()) { - openUrl(); - return; - } - - if (textRegion_.contains(event->pos())) { - openUrl(); - } else { - auto imgDialog = new dialogs::ImageOverlay(image_); - imgDialog->show(); - connect(imgDialog, &dialogs::ImageOverlay::saving, this, &ImageItem::saveAs); - } -} - -void -ImageItem::resizeEvent(QResizeEvent *event) -{ - if (!image_) - return QWidget::resizeEvent(event); - - scaled_image_ = utils::scaleDown(max_width_, max_height_, image_); - - width_ = scaled_image_.width(); - height_ = scaled_image_.height(); - - setFixedSize(width_, height_); -} - -void -ImageItem::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - QFont font; - - QFontMetrics metrics(font); - const int fontHeight = metrics.height() + metrics.ascent(); - - if (image_.isNull()) { - QString elidedText = metrics.elidedText(text_, Qt::ElideRight, max_width_ - 10); - - setFixedSize(metrics.width(elidedText), fontHeight); - - painter.setFont(font); - painter.setPen(QPen(QColor(66, 133, 244))); - painter.drawText(QPoint(0, fontHeight / 2), elidedText); - - return; - } - - imageRegion_ = QRectF(0, 0, width_, height_); - - QPainterPath path; - path.addRoundedRect(imageRegion_, 5, 5); - - painter.setPen(Qt::NoPen); - painter.fillPath(path, scaled_image_); - painter.drawPath(path); - - // Bottom text section - if (isInteractive_ && underMouse()) { - const int textBoxHeight = fontHeight / 2 + 6; - - textRegion_ = QRectF(0, height_ - textBoxHeight, width_, textBoxHeight); - - QPainterPath textPath; - textPath.addRoundedRect(textRegion_, 0, 0); - - painter.fillPath(textPath, QColor(40, 40, 40, 140)); - - QString elidedText = metrics.elidedText(text_, Qt::ElideRight, width_ - 10); - - font.setWeight(QFont::Medium); - painter.setFont(font); - painter.setPen(QPen(QColor(Qt::white))); - - textRegion_.adjust(5, 0, 5, 0); - painter.drawText(textRegion_, Qt::AlignVCenter, elidedText); - } -} - -void -ImageItem::saveAs() -{ - auto filename = QFileDialog::getSaveFileName(this, tr("Save image"), text_); - - if (filename.isEmpty()) - return; - - const auto url = url_.toString().toStdString(); - - auto proxy = std::make_shared<MediaProxy>(); - connect(proxy.get(), &MediaProxy::imageSaved, this, &ImageItem::saveImage); - - http::client()->download( - url, - [proxy = std::move(proxy), filename, url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve image {}: {} {}", - url, - err->matrix_error.error, - static_cast<int>(err->status_code)); - return; - } - - emit proxy->imageSaved(filename, QByteArray(data.data(), data.size())); - }); -} diff --git a/src/timeline/widgets/ImageItem.h b/src/timeline/widgets/ImageItem.h deleted file mode 100644
index 65bd962d..00000000 --- a/src/timeline/widgets/ImageItem.h +++ /dev/null
@@ -1,104 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#pragma once - -#include <QEvent> -#include <QMouseEvent> -#include <QSharedPointer> -#include <QWidget> - -#include <mtx.hpp> - -namespace dialogs { -class ImageOverlay; -} - -class ImageItem : public QWidget -{ - Q_OBJECT -public: - ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &event, - QWidget *parent = nullptr); - - ImageItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - - QSize sizeHint() const override; - -public slots: - //! Show a save as dialog for the image. - void saveAs(); - void setImage(const QPixmap &image); - void saveImage(const QString &filename, const QByteArray &data); - -protected: - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - - //! Whether the user can interact with the displayed image. - bool isInteractive_ = true; - -private: - void init(); - void openUrl(); - void downloadMedia(const QUrl &url); - - int max_width_ = 500; - int max_height_ = 300; - - int width_; - int height_; - - QPixmap scaled_image_; - QPixmap image_; - - QUrl url_; - QString text_; - - int bottom_height_ = 30; - - QRectF textRegion_; - QRectF imageRegion_; - - mtx::events::RoomEvent<mtx::events::msg::Image> event_; -}; - -class StickerItem : public ImageItem -{ - Q_OBJECT - -public: - StickerItem(const mtx::events::Sticker &event, QWidget *parent = nullptr) - : ImageItem{QString::fromStdString(event.content.url), - QString::fromStdString(event.content.body), - event.content.info.size, - parent} - , event_{event} - { - isInteractive_ = false; - setCursor(Qt::ArrowCursor); - setMouseTracking(false); - setAttribute(Qt::WA_Hover, false); - } - -private: - mtx::events::Sticker event_; -}; diff --git a/src/timeline/widgets/VideoItem.cpp b/src/timeline/widgets/VideoItem.cpp deleted file mode 100644
index 4b5dc022..00000000 --- a/src/timeline/widgets/VideoItem.cpp +++ /dev/null
@@ -1,65 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#include <QLabel> -#include <QVBoxLayout> - -#include "Config.h" -#include "MatrixClient.h" -#include "Utils.h" -#include "timeline/widgets/VideoItem.h" - -void -VideoItem::init() -{ - url_ = utils::mxcToHttp( - url_, QString::fromStdString(http::client()->server()), http::client()->port()); -} - -VideoItem::VideoItem(const mtx::events::RoomEvent<mtx::events::msg::Video> &event, QWidget *parent) - : QWidget(parent) - , url_{QString::fromStdString(event.content.url)} - , text_{QString::fromStdString(event.content.body)} - , event_{event} -{ - readableFileSize_ = utils::humanReadableFileSize(event.content.info.size); - - init(); - - auto layout = new QVBoxLayout(this); - layout->setMargin(0); - layout->setSpacing(0); - - QString link = QString("<a href=%1>%2</a>").arg(url_.toString()).arg(text_); - - label_ = new QLabel(link, this); - label_->setMargin(0); - label_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); - label_->setOpenExternalLinks(true); - - layout->addWidget(label_); -} - -VideoItem::VideoItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent) - : QWidget(parent) - , url_{url} - , text_{filename} -{ - readableFileSize_ = utils::humanReadableFileSize(size); - - init(); -} diff --git a/src/timeline/widgets/VideoItem.h b/src/timeline/widgets/VideoItem.h deleted file mode 100644
index 26fa1c35..00000000 --- a/src/timeline/widgets/VideoItem.h +++ /dev/null
@@ -1,51 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#pragma once - -#include <QEvent> -#include <QLabel> -#include <QSharedPointer> -#include <QUrl> -#include <QWidget> - -#include <mtx.hpp> - -class VideoItem : public QWidget -{ - Q_OBJECT - -public: - VideoItem(const mtx::events::RoomEvent<mtx::events::msg::Video> &event, - QWidget *parent = nullptr); - - VideoItem(const QString &url, - const QString &filename, - uint64_t size, - QWidget *parent = nullptr); - -private: - void init(); - - QUrl url_; - QString text_; - QString readableFileSize_; - - QLabel *label_; - - mtx::events::RoomEvent<mtx::events::msg::Video> event_; -}; diff --git a/src/ui/Avatar.cpp b/src/ui/Avatar.cpp
index 4b4cd272..3589fce5 100644 --- a/src/ui/Avatar.cpp +++ b/src/ui/Avatar.cpp
@@ -1,12 +1,14 @@ #include <QPainter> +#include <QSettings> +#include "AvatarProvider.h" #include "Utils.h" #include "ui/Avatar.h" -Avatar::Avatar(QWidget *parent) +Avatar::Avatar(QWidget *parent, int size) : QWidget(parent) + , size_(size) { - size_ = ui::AvatarSize; type_ = ui::AvatarType::Letter; letter_ = "A"; @@ -61,35 +63,41 @@ Avatar::setBackgroundColor(const QColor &color) } void -Avatar::setSize(int size) +Avatar::setLetter(const QString &letter) { - size_ = size; - - if (!image_.isNull()) - pixmap_ = utils::scaleImageToPixmap(image_, size_); - - QFont _font(font()); - _font.setPointSizeF(size_ * (ui::FontSize) / 40); - - setFont(_font); + letter_ = letter; + type_ = ui::AvatarType::Letter; update(); } void -Avatar::setLetter(const QString &letter) +Avatar::setImage(const QString &avatar_url) { - letter_ = letter; - type_ = ui::AvatarType::Letter; - update(); + avatar_url_ = avatar_url; + AvatarProvider::resolve(avatar_url, + static_cast<int>(size_ * pixmap_.devicePixelRatio()), + this, + [this](QPixmap pm) { + type_ = ui::AvatarType::Image; + pixmap_ = pm; + update(); + }); } void -Avatar::setImage(const QImage &image) +Avatar::setImage(const QString &room, const QString &user) { - image_ = image; - type_ = ui::AvatarType::Image; - pixmap_ = utils::scaleImageToPixmap(image_, size_); - update(); + room_ = room; + user_ = user; + AvatarProvider::resolve(room, + user, + static_cast<int>(size_ * pixmap_.devicePixelRatio()), + this, + [this](QPixmap pm) { + type_ = ui::AvatarType::Image; + pixmap_ = pm; + update(); + }); } void @@ -103,6 +111,8 @@ Avatar::setIcon(const QIcon &icon) void Avatar::paintEvent(QPaintEvent *) { + bool rounded = QSettings().value("user/avatar_circles", true).toBool(); + QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); @@ -116,7 +126,18 @@ Avatar::paintEvent(QPaintEvent *) painter.setPen(Qt::NoPen); painter.setBrush(brush); - painter.drawEllipse(r.center(), hs, hs); + rounded ? painter.drawEllipse(r.center(), hs, hs) + : painter.drawRoundedRect(r, 3, 3); + } else if (painter.isActive() && + abs(pixmap_.devicePixelRatio() - painter.device()->devicePixelRatioF()) > 0.01) { + pixmap_ = + pixmap_.scaled(QSize(size_, size_) * painter.device()->devicePixelRatioF()); + pixmap_.setDevicePixelRatio(painter.device()->devicePixelRatioF()); + + if (!avatar_url_.isEmpty()) + setImage(avatar_url_); + else + setImage(room_, user_); } switch (type_) { @@ -129,7 +150,10 @@ Avatar::paintEvent(QPaintEvent *) } case ui::AvatarType::Image: { QPainterPath ppath; - ppath.addEllipse(width() / 2 - hs, height() / 2 - hs, size_, size_); + + rounded ? ppath.addEllipse(width() / 2 - hs, height() / 2 - hs, size_, size_) + : ppath.addRoundedRect(r, 3, 3); + painter.setClipPath(ppath); painter.drawPixmap(QRect(width() / 2 - hs, height() / 2 - hs, size_, size_), pixmap_); diff --git a/src/ui/Avatar.h b/src/ui/Avatar.h
index 41967af5..aea7d3e6 100644 --- a/src/ui/Avatar.h +++ b/src/ui/Avatar.h
@@ -15,13 +15,13 @@ class Avatar : public QWidget Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) public: - explicit Avatar(QWidget *parent = 0); + explicit Avatar(QWidget *parent = nullptr, int size = ui::AvatarSize); void setBackgroundColor(const QColor &color); void setIcon(const QIcon &icon); - void setImage(const QImage &image); + void setImage(const QString &avatar_url); + void setImage(const QString &room, const QString &user); void setLetter(const QString &letter); - void setSize(int size); void setTextColor(const QColor &color); QColor backgroundColor() const; @@ -38,10 +38,10 @@ private: ui::AvatarType type_; QString letter_; + QString avatar_url_, room_, user_; QColor background_color_; QColor text_color_; QIcon icon_; - QImage image_; QPixmap pixmap_; int size_; }; diff --git a/src/ui/Badge.h b/src/ui/Badge.h
index fd73ad30..748b56fd 100644 --- a/src/ui/Badge.h +++ b/src/ui/Badge.h
@@ -16,9 +16,9 @@ class Badge : public OverlayWidget Q_PROPERTY(QPointF relativePosition WRITE setRelativePosition READ relativePosition) public: - explicit Badge(QWidget *parent = 0); - explicit Badge(const QIcon &icon, QWidget *parent = 0); - explicit Badge(const QString &text, QWidget *parent = 0); + explicit Badge(QWidget *parent = nullptr); + explicit Badge(const QIcon &icon, QWidget *parent = nullptr); + explicit Badge(const QString &text, QWidget *parent = nullptr); void setBackgroundColor(const QColor &color); void setTextColor(const QColor &color); diff --git a/src/ui/DropShadow.cpp b/src/ui/DropShadow.cpp new file mode 100644
index 00000000..d437975c --- /dev/null +++ b/src/ui/DropShadow.cpp
@@ -0,0 +1,108 @@ +#include "DropShadow.h" + +#include <QLinearGradient> +#include <QPainter> + +void +DropShadow::draw(QPainter &painter, + qint16 margin, + qreal radius, + QColor start, + QColor end, + qreal startPosition, + qreal endPosition0, + qreal endPosition1, + qreal width, + qreal height) +{ + painter.setPen(Qt::NoPen); + + QLinearGradient gradient; + gradient.setColorAt(startPosition, start); + gradient.setColorAt(endPosition0, end); + + // Right + QPointF right0(width - margin, height / 2); + QPointF right1(width, height / 2); + gradient.setStart(right0); + gradient.setFinalStop(right1); + painter.setBrush(QBrush(gradient)); + // Deprecated in 5.13: painter.drawRoundRect( + // QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - + // margin)), 0.0, 0.0); + painter.drawRoundedRect( + QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - margin)), + 0.0, + 0.0); + + // Left + QPointF left0(margin, height / 2); + QPointF left1(0, height / 2); + gradient.setStart(left0); + gradient.setFinalStop(left1); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect( + QRectF(QPointF(margin * radius, margin), QPointF(0, height - margin)), 0.0, 0.0); + + // Top + QPointF top0(width / 2, margin); + QPointF top1(width / 2, 0); + gradient.setStart(top0); + gradient.setFinalStop(top1); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect( + QRectF(QPointF(width - margin, 0), QPointF(margin, margin)), 0.0, 0.0); + + // Bottom + QPointF bottom0(width / 2, height - margin); + QPointF bottom1(width / 2, height); + gradient.setStart(bottom0); + gradient.setFinalStop(bottom1); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect( + QRectF(QPointF(margin, height - margin), QPointF(width - margin, height)), 0.0, 0.0); + + // BottomRight + QPointF bottomright0(width - margin, height - margin); + QPointF bottomright1(width, height); + gradient.setStart(bottomright0); + gradient.setFinalStop(bottomright1); + gradient.setColorAt(endPosition1, end); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect(QRectF(bottomright0, bottomright1), 0.0, 0.0); + + // BottomLeft + QPointF bottomleft0(margin, height - margin); + QPointF bottomleft1(0, height); + gradient.setStart(bottomleft0); + gradient.setFinalStop(bottomleft1); + gradient.setColorAt(endPosition1, end); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect(QRectF(bottomleft0, bottomleft1), 0.0, 0.0); + + // TopLeft + QPointF topleft0(margin, margin); + QPointF topleft1(0, 0); + gradient.setStart(topleft0); + gradient.setFinalStop(topleft1); + gradient.setColorAt(endPosition1, end); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect(QRectF(topleft0, topleft1), 0.0, 0.0); + + // TopRight + QPointF topright0(width - margin, margin); + QPointF topright1(width, 0); + gradient.setStart(topright0); + gradient.setFinalStop(topright1); + gradient.setColorAt(endPosition1, end); + painter.setBrush(QBrush(gradient)); + painter.drawRoundedRect(QRectF(topright0, topright1), 0.0, 0.0); + + // Widget + painter.setBrush(QBrush("#FFFFFF")); + painter.setRenderHint(QPainter::Antialiasing); + painter.drawRoundedRect( + QRectF(QPointF(margin, margin), QPointF(width - margin, height - margin)), + radius, + radius); +} diff --git a/src/ui/DropShadow.h b/src/ui/DropShadow.h
index b7ba1985..6997e1a0 100644 --- a/src/ui/DropShadow.h +++ b/src/ui/DropShadow.h
@@ -1,8 +1,8 @@ #pragma once #include <QColor> -#include <QLinearGradient> -#include <QPainter> + +class QPainter; class DropShadow { @@ -16,96 +16,5 @@ public: qreal endPosition0, qreal endPosition1, qreal width, - qreal height) - { - painter.setPen(Qt::NoPen); - - QLinearGradient gradient; - gradient.setColorAt(startPosition, start); - gradient.setColorAt(endPosition0, end); - - // Right - QPointF right0(width - margin, height / 2); - QPointF right1(width, height / 2); - gradient.setStart(right0); - gradient.setFinalStop(right1); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect( - QRectF(QPointF(width - margin * radius, margin), QPointF(width, height - margin)), - 0.0, - 0.0); - - // Left - QPointF left0(margin, height / 2); - QPointF left1(0, height / 2); - gradient.setStart(left0); - gradient.setFinalStop(left1); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect( - QRectF(QPointF(margin * radius, margin), QPointF(0, height - margin)), 0.0, 0.0); - - // Top - QPointF top0(width / 2, margin); - QPointF top1(width / 2, 0); - gradient.setStart(top0); - gradient.setFinalStop(top1); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect( - QRectF(QPointF(width - margin, 0), QPointF(margin, margin)), 0.0, 0.0); - - // Bottom - QPointF bottom0(width / 2, height - margin); - QPointF bottom1(width / 2, height); - gradient.setStart(bottom0); - gradient.setFinalStop(bottom1); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect( - QRectF(QPointF(margin, height - margin), QPointF(width - margin, height)), - 0.0, - 0.0); - - // BottomRight - QPointF bottomright0(width - margin, height - margin); - QPointF bottomright1(width, height); - gradient.setStart(bottomright0); - gradient.setFinalStop(bottomright1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect(QRectF(bottomright0, bottomright1), 0.0, 0.0); - - // BottomLeft - QPointF bottomleft0(margin, height - margin); - QPointF bottomleft1(0, height); - gradient.setStart(bottomleft0); - gradient.setFinalStop(bottomleft1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect(QRectF(bottomleft0, bottomleft1), 0.0, 0.0); - - // TopLeft - QPointF topleft0(margin, margin); - QPointF topleft1(0, 0); - gradient.setStart(topleft0); - gradient.setFinalStop(topleft1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect(QRectF(topleft0, topleft1), 0.0, 0.0); - - // TopRight - QPointF topright0(width - margin, margin); - QPointF topright1(width, 0); - gradient.setStart(topright0); - gradient.setFinalStop(topright1); - gradient.setColorAt(endPosition1, end); - painter.setBrush(QBrush(gradient)); - painter.drawRoundRect(QRectF(topright0, topright1), 0.0, 0.0); - - // Widget - painter.setBrush(QBrush("#FFFFFF")); - painter.setRenderHint(QPainter::Antialiasing); - painter.drawRoundRect( - QRectF(QPointF(margin, margin), QPointF(width - margin, height - margin)), - radius, - radius); - } + qreal height); }; diff --git a/src/ui/FlatButton.cpp b/src/ui/FlatButton.cpp
index a828f582..6660c58d 100644 --- a/src/ui/FlatButton.cpp +++ b/src/ui/FlatButton.cpp
@@ -2,6 +2,8 @@ #include <QFontDatabase> #include <QIcon> #include <QMouseEvent> +#include <QPaintEvent> +#include <QPainter> #include <QPainterPath> #include <QResizeEvent> #include <QSignalTransition> diff --git a/src/ui/FlatButton.h b/src/ui/FlatButton.h
index 9c2bf425..3749a0d9 100644 --- a/src/ui/FlatButton.h +++ b/src/ui/FlatButton.h
@@ -1,7 +1,5 @@ #pragma once -#include <QPaintEvent> -#include <QPainter> #include <QPushButton> #include <QStateMachine> @@ -20,7 +18,7 @@ class FlatButtonStateMachine : public QStateMachine public: explicit FlatButtonStateMachine(FlatButton *parent); - ~FlatButtonStateMachine(); + ~FlatButtonStateMachine() override; void setOverlayOpacity(qreal opacity); void setCheckedOverlayProgress(qreal opacity); @@ -93,16 +91,16 @@ class FlatButton : public QPushButton Q_PROPERTY(qreal fontSize WRITE setFontSize READ fontSize) public: - explicit FlatButton(QWidget *parent = 0, + explicit FlatButton(QWidget *parent = nullptr, ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset); explicit FlatButton(const QString &text, - QWidget *parent = 0, + QWidget *parent = nullptr, ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset); FlatButton(const QString &text, ui::Role role, - QWidget *parent = 0, + QWidget *parent = nullptr, ui::ButtonPreset preset = ui::ButtonPreset::FlatPreset); - ~FlatButton(); + ~FlatButton() override; void applyPreset(ui::ButtonPreset preset); diff --git a/src/ui/FloatingButton.cpp b/src/ui/FloatingButton.cpp
index 74dcd482..f3a09ccd 100644 --- a/src/ui/FloatingButton.cpp +++ b/src/ui/FloatingButton.cpp
@@ -1,3 +1,4 @@ +#include <QPainter> #include <QPainterPath> #include "FloatingButton.h" diff --git a/src/ui/InfoMessage.cpp b/src/ui/InfoMessage.cpp
index e9de20cc..27bc0a5f 100644 --- a/src/ui/InfoMessage.cpp +++ b/src/ui/InfoMessage.cpp
@@ -2,8 +2,10 @@ #include "Config.h" #include <QDateTime> +#include <QLocale> #include <QPainter> #include <QPen> +#include <QtGlobal> constexpr int VPadding = 6; constexpr int HPadding = 12; @@ -22,7 +24,13 @@ InfoMessage::InfoMessage(QString msg, QWidget *parent) initFont(); QFontMetrics fm{font()}; - width_ = fm.width(msg_) + HPadding * 2; +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) + // width deprecated in 5.13 + width_ = fm.width(msg_) + HPadding * 2; +#else + width_ = fm.horizontalAdvance(msg_) + HPadding * 2; +#endif + height_ = fm.ascent() + 2 * VPadding; setFixedHeight(height_ + 2 * HMargin); @@ -54,17 +62,22 @@ DateSeparator::DateSeparator(QDateTime datetime, QWidget *parent) { auto now = QDateTime::currentDateTime(); - QString fmt; + QString fmt = QLocale::system().dateFormat(QLocale::LongFormat); - if (now.date().year() != datetime.date().year()) - fmt = QString("ddd d MMMM yy"); - else - fmt = QString("ddd d MMMM"); + if (now.date().year() == datetime.date().year()) { + QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*"); + fmt = fmt.remove(rx); + } - msg_ = datetime.toString(fmt); + msg_ = datetime.date().toString(fmt); QFontMetrics fm{font()}; - width_ = fm.width(msg_) + HPadding * 2; +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) + // width deprecated in 5.13 + width_ = fm.width(msg_) + HPadding * 2; +#else + width_ = fm.horizontalAdvance(msg_) + HPadding * 2; +#endif height_ = fm.ascent() + 2 * VPadding; setFixedHeight(height_ + 2 * HMargin); diff --git a/src/ui/LoadingIndicator.cpp b/src/ui/LoadingIndicator.cpp
index c8337089..d2b1240d 100644 --- a/src/ui/LoadingIndicator.cpp +++ b/src/ui/LoadingIndicator.cpp
@@ -1,7 +1,8 @@ #include "LoadingIndicator.h" -#include <QPoint> -#include <QtGlobal> +#include <QPaintEvent> +#include <QPainter> +#include <QTimer> LoadingIndicator::LoadingIndicator(QWidget *parent) : QWidget(parent) diff --git a/src/ui/LoadingIndicator.h b/src/ui/LoadingIndicator.h
index e8de0aec..678ef611 100644 --- a/src/ui/LoadingIndicator.h +++ b/src/ui/LoadingIndicator.h
@@ -1,20 +1,20 @@ #pragma once #include <QColor> -#include <QPaintEvent> -#include <QPainter> -#include <QTimer> #include <QWidget> +class QPainter; +class QTimer; +class QPaintEvent; class LoadingIndicator : public QWidget { Q_OBJECT Q_PROPERTY(QColor color READ color WRITE setColor) public: - LoadingIndicator(QWidget *parent = 0); + LoadingIndicator(QWidget *parent = nullptr); - void paintEvent(QPaintEvent *e); + void paintEvent(QPaintEvent *e) override; void start(); void stop(); diff --git a/src/ui/OverlayWidget.cpp b/src/ui/OverlayWidget.cpp
index ccac0116..a32d86b6 100644 --- a/src/ui/OverlayWidget.cpp +++ b/src/ui/OverlayWidget.cpp
@@ -1,5 +1,7 @@ #include "OverlayWidget.h" -#include <QEvent> + +#include <QPainter> +#include <QStyleOption> OverlayWidget::OverlayWidget(QWidget *parent) : QWidget(parent) diff --git a/src/ui/OverlayWidget.h b/src/ui/OverlayWidget.h
index 6662479d..ed3ef52d 100644 --- a/src/ui/OverlayWidget.h +++ b/src/ui/OverlayWidget.h
@@ -1,10 +1,10 @@ #pragma once #include <QEvent> -#include <QPainter> -#include <QStyleOption> #include <QWidget> +class QPainter; + class OverlayWidget : public QWidget { Q_OBJECT diff --git a/src/ui/Painter.h b/src/ui/Painter.h
index 8de39651..4d227a5a 100644 --- a/src/ui/Painter.h +++ b/src/ui/Painter.h
@@ -3,6 +3,7 @@ #include <QFontMetrics> #include <QPaintDevice> #include <QPainter> +#include <QtGlobal> class Painter : public QPainter { @@ -20,8 +21,14 @@ public: void drawTextRight(int x, int y, int outerw, const QString &text, int textWidth = -1) { QFontMetrics m(fontMetrics()); - if (textWidth < 0) + if (textWidth < 0) { +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) + // deprecated in 5.13: textWidth = m.width(text); +#else + textWidth = m.horizontalAdvance(text); +#endif + } drawText((outerw - x - textWidth), y + m.ascent(), text); } @@ -133,8 +140,7 @@ public: { static constexpr QPainter::RenderHint Hints[] = {QPainter::Antialiasing, QPainter::SmoothPixmapTransform, - QPainter::TextAntialiasing, - QPainter::HighQualityAntialiasing}; + QPainter::TextAntialiasing}; auto hints = _painter.renderHints(); for (const auto &hint : Hints) { diff --git a/src/ui/RaisedButton.h b/src/ui/RaisedButton.h
index edd5ee4a..47ef1acd 100644 --- a/src/ui/RaisedButton.h +++ b/src/ui/RaisedButton.h
@@ -11,9 +11,9 @@ class RaisedButton : public FlatButton Q_OBJECT public: - explicit RaisedButton(QWidget *parent = 0); - explicit RaisedButton(const QString &text, QWidget *parent = 0); - ~RaisedButton(); + explicit RaisedButton(QWidget *parent = nullptr); + explicit RaisedButton(const QString &text, QWidget *parent = nullptr); + ~RaisedButton() override; protected: bool event(QEvent *event) override; diff --git a/src/ui/Ripple.cpp b/src/ui/Ripple.cpp
index e22c4a62..ef8a62dd 100644 --- a/src/ui/Ripple.cpp +++ b/src/ui/Ripple.cpp
@@ -3,7 +3,7 @@ Ripple::Ripple(const QPoint &center, QObject *parent) : QParallelAnimationGroup(parent) - , overlay_(0) + , overlay_(nullptr) , radius_anim_(animate("radius")) , opacity_anim_(animate("opacity")) , radius_(0) diff --git a/src/ui/Ripple.h b/src/ui/Ripple.h
index 9184f061..3701fb6c 100644 --- a/src/ui/Ripple.h +++ b/src/ui/Ripple.h
@@ -16,8 +16,8 @@ class Ripple : public QParallelAnimationGroup Q_PROPERTY(qreal opacity WRITE setOpacity READ opacity) public: - explicit Ripple(const QPoint &center, QObject *parent = 0); - Ripple(const QPoint &center, RippleOverlay *overlay, QObject *parent = 0); + explicit Ripple(const QPoint &center, QObject *parent = nullptr); + Ripple(const QPoint &center, RippleOverlay *overlay, QObject *parent = nullptr); inline void setOverlay(RippleOverlay *overlay); diff --git a/src/ui/RippleOverlay.h b/src/ui/RippleOverlay.h
index 9ef91fbf..5d12aff7 100644 --- a/src/ui/RippleOverlay.h +++ b/src/ui/RippleOverlay.h
@@ -11,7 +11,7 @@ class RippleOverlay : public OverlayWidget Q_OBJECT public: - explicit RippleOverlay(QWidget *parent = 0); + explicit RippleOverlay(QWidget *parent = nullptr); void addRipple(Ripple *ripple); void addRipple(const QPoint &position, qreal radius = 300); diff --git a/src/ui/SnackBar.cpp b/src/ui/SnackBar.cpp
index 8a05d937..5daa697e 100644 --- a/src/ui/SnackBar.cpp +++ b/src/ui/SnackBar.cpp
@@ -1,6 +1,6 @@ #include <QPainter> -#include <tweeny/tweeny.h> +#include <tweeny.h> #include "SnackBar.h" diff --git a/src/ui/TextField.cpp b/src/ui/TextField.cpp
index c4582085..4bb7596a 100644 --- a/src/ui/TextField.cpp +++ b/src/ui/TextField.cpp
@@ -1,6 +1,6 @@ #include "TextField.h" -#include <QApplication> +#include <QCoreApplication> #include <QEventTransition> #include <QFontDatabase> #include <QPaintEvent> @@ -16,7 +16,7 @@ TextField::TextField(QWidget *parent) QPalette pal; state_machine_ = new TextFieldStateMachine(this); - label_ = 0; + label_ = nullptr; label_font_size_ = 15; show_label_ = false; background_color_ = pal.color(QPalette::Window); @@ -103,23 +103,6 @@ TextField::label() const } void -TextField::setTextColor(const QColor &color) -{ - text_color_ = color; - setStyleSheet(QString("QLineEdit { color: %1; }").arg(color.name())); -} - -QColor -TextField::textColor() const -{ - if (!text_color_.isValid()) { - return QPalette().color(QPalette::Text); - } - - return text_color_; -} - -void TextField::setLabelColor(const QColor &color) { label_color_ = color; @@ -230,9 +213,9 @@ TextFieldStateMachine::TextFieldStateMachine(TextField *parent) normal_state_ = new QState; focused_state_ = new QState; - label_ = 0; - offset_anim_ = 0; - color_anim_ = 0; + label_ = nullptr; + offset_anim_ = nullptr; + color_anim_ = nullptr; progress_ = 0.0; addState(normal_state_); diff --git a/src/ui/TextField.h b/src/ui/TextField.h
index 1675a2e0..85d5036d 100644 --- a/src/ui/TextField.h +++ b/src/ui/TextField.h
@@ -15,14 +15,13 @@ class TextField : public QLineEdit { Q_OBJECT - Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor) Q_PROPERTY(QColor inkColor WRITE setInkColor READ inkColor) Q_PROPERTY(QColor labelColor WRITE setLabelColor READ labelColor) Q_PROPERTY(QColor underlineColor WRITE setUnderlineColor READ underlineColor) Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor) public: - explicit TextField(QWidget *parent = 0); + explicit TextField(QWidget *parent = nullptr); void setInkColor(const QColor &color); void setBackgroundColor(const QColor &color); @@ -30,12 +29,10 @@ public: void setLabelColor(const QColor &color); void setLabelFontSize(qreal size); void setShowLabel(bool value); - void setTextColor(const QColor &color); void setUnderlineColor(const QColor &color); QColor inkColor() const; QColor labelColor() const; - QColor textColor() const; QColor underlineColor() const; QColor backgroundColor() const; QString label() const; @@ -52,7 +49,6 @@ private: QColor ink_color_; QColor background_color_; QColor label_color_; - QColor text_color_; QColor underline_color_; QString label_text_; TextFieldLabel *label_; diff --git a/src/ui/TextLabel.h b/src/ui/TextLabel.h
index 1470d64e..56778dcc 100644 --- a/src/ui/TextLabel.h +++ b/src/ui/TextLabel.h
@@ -22,7 +22,7 @@ signals: void contextMenuIsOpening(); protected: - bool eventFilter(QObject *obj, QEvent *event); + bool eventFilter(QObject *obj, QEvent *event) override; }; class TextLabel : public QTextBrowser diff --git a/src/ui/Theme.h b/src/ui/Theme.h
index d1d7e2a6..ecff02b5 100644 --- a/src/ui/Theme.h +++ b/src/ui/Theme.h
@@ -78,7 +78,7 @@ class Theme : public QObject { Q_OBJECT public: - explicit Theme(QObject *parent = 0); + explicit Theme(QObject *parent = nullptr); QColor getColor(const QString &key) const; diff --git a/src/ui/ToggleButton.cpp b/src/ui/ToggleButton.cpp
index 755f528f..f9411489 100644 --- a/src/ui/ToggleButton.cpp +++ b/src/ui/ToggleButton.cpp
@@ -1,5 +1,5 @@ -#include <QApplication> #include <QColor> +#include <QCoreApplication> #include <QEvent> #include <QPainter>