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 ¬if : 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 ¬if : 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 ¬if) {
+ 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> ¬ifs);
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("<");
+ }
+ break;
+ }
+ case '>':
+ if (escapingTag) {
+ buffer.append(">");
+ 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> ¬ifs)
+{
+ nhlog::ui()->debug("Initializing " + std::to_string(notifs.size()) + " notifications.");
+
+ for (const auto &item : notifs) {
+ for (const auto ¬if : 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 ¤t_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> ¬ifs);
+ 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 ¤t_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 ¢er, 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 ¢er, QObject *parent = 0);
- Ripple(const QPoint ¢er, RippleOverlay *overlay, QObject *parent = 0);
+ explicit Ripple(const QPoint ¢er, QObject *parent = nullptr);
+ Ripple(const QPoint ¢er, 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>
|