Merge branch 'master' into nhekoRoomDirectory

This commit is contained in:
kamathmanu 2021-08-07 21:20:43 +00:00 committed by GitHub
commit 2dfccda73c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 2099 additions and 1313 deletions

View file

@ -125,7 +125,7 @@ template<class T>
bool
containsStateUpdates(const T &e)
{
return std::visit([](const auto &ev) { return Cache::isStateEvent(ev); }, e);
return std::visit([](const auto &ev) { return Cache::isStateEvent_<decltype(ev)>; }, e);
}
bool
@ -288,6 +288,9 @@ Cache::setup()
outboundMegolmSessionDb_ = lmdb::dbi::open(txn, OUTBOUND_MEGOLM_SESSIONS_DB, MDB_CREATE);
megolmSessionDataDb_ = lmdb::dbi::open(txn, MEGOLM_SESSIONS_DATA_DB, MDB_CREATE);
// What rooms are encrypted
encryptedRooms_ = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
txn.commit();
databaseReady_ = true;
@ -298,8 +301,7 @@ Cache::setEncryptedRoom(lmdb::txn &txn, const std::string &room_id)
{
nhlog::db()->info("mark room {} as encrypted", room_id);
auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
db.put(txn, room_id, "0");
encryptedRooms_.put(txn, room_id, "0");
}
bool
@ -308,8 +310,7 @@ Cache::isRoomEncrypted(const std::string &room_id)
std::string_view unused;
auto txn = ro_txn(env_);
auto db = lmdb::dbi::open(txn, ENCRYPTED_ROOMS_DB, MDB_CREATE);
auto res = db.get(txn, room_id, unused);
auto res = encryptedRooms_.get(txn, room_id, unused);
return res;
}
@ -3400,7 +3401,7 @@ Cache::getImagePacks(const std::string &room_id, std::optional<bool> stickers)
info.pack.pack = pack.pack;
for (const auto &img : pack.images) {
if (img.second.overrides_usage() &&
if (stickers.has_value() && img.second.overrides_usage() &&
(stickers ? !img.second.is_sticker() : !img.second.is_emoji()))
continue;
@ -3541,7 +3542,7 @@ Cache::roomMembers(const std::string &room_id)
}
std::map<std::string, std::optional<UserKeyCache>>
Cache::getMembersWithKeys(const std::string &room_id)
Cache::getMembersWithKeys(const std::string &room_id, bool verified_only)
{
std::string_view keys;
@ -3558,10 +3559,51 @@ Cache::getMembersWithKeys(const std::string &room_id)
auto res = keysDb.get(txn, user_id, keys);
if (res) {
members[std::string(user_id)] =
json::parse(keys).get<UserKeyCache>();
auto k = json::parse(keys).get<UserKeyCache>();
if (verified_only) {
auto verif = verificationStatus(std::string(user_id));
if (verif.user_verified == crypto::Trust::Verified ||
!verif.verified_devices.empty()) {
auto keyCopy = k;
keyCopy.device_keys.clear();
std::copy_if(
k.device_keys.begin(),
k.device_keys.end(),
std::inserter(keyCopy.device_keys,
keyCopy.device_keys.end()),
[&verif](const auto &key) {
auto curve25519 = key.second.keys.find(
"curve25519:" + key.first);
if (curve25519 == key.second.keys.end())
return false;
if (auto t =
verif.verified_device_keys.find(
curve25519->second);
t ==
verif.verified_device_keys.end() ||
t->second != crypto::Trust::Verified)
return false;
return key.first ==
key.second.device_id &&
std::find(
verif.verified_devices.begin(),
verif.verified_devices.end(),
key.first) !=
verif.verified_devices.end();
});
if (!keyCopy.device_keys.empty())
members[std::string(user_id)] =
std::move(keyCopy);
}
} else {
members[std::string(user_id)] = std::move(k);
}
} else {
members[std::string(user_id)] = {};
if (!verified_only)
members[std::string(user_id)] = {};
}
}
cursor.close();

View file

@ -48,7 +48,8 @@ public:
// user cache stores user keys
std::optional<UserKeyCache> userKeys(const std::string &user_id);
std::map<std::string, std::optional<UserKeyCache>> getMembersWithKeys(
const std::string &room_id);
const std::string &room_id,
bool verified_only);
void updateUserKeys(const std::string &sync_token,
const mtx::responses::QueryKeys &keyQuery);
void markUserKeysOutOfDate(lmdb::txn &txn,
@ -290,15 +291,9 @@ public:
std::optional<std::string> secret(const std::string name);
template<class T>
static constexpr bool isStateEvent(const mtx::events::StateEvent<T> &)
{
return true;
}
template<class T>
static constexpr bool isStateEvent(const mtx::events::Event<T> &)
{
return false;
}
constexpr static bool isStateEvent_ =
std::is_same_v<std::remove_cv_t<std::remove_reference_t<T>>,
mtx::events::StateEvent<decltype(std::declval<T>().content)>>;
static int compare_state_key(const MDB_val *a, const MDB_val *b)
{
@ -415,11 +410,27 @@ private:
}
std::visit(
[&txn, &statesdb, &stateskeydb, &eventsDb](auto e) {
if constexpr (isStateEvent(e)) {
[&txn, &statesdb, &stateskeydb, &eventsDb, &membersdb](const auto &e) {
if constexpr (isStateEvent_<decltype(e)>) {
eventsDb.put(txn, e.event_id, json(e).dump());
if (e.type != EventType::Unsupported) {
if (std::is_same_v<
std::remove_cv_t<std::remove_reference_t<decltype(e)>>,
StateEvent<mtx::events::msg::Redacted>>) {
if (e.type == EventType::RoomMember)
membersdb.del(txn, e.state_key, "");
else if (e.state_key.empty())
statesdb.del(txn, to_string(e.type));
else
stateskeydb.del(
txn,
to_string(e.type),
json::object({
{"key", e.state_key},
{"id", e.event_id},
})
.dump());
} else if (e.type != EventType::Unsupported) {
if (e.state_key.empty())
statesdb.put(
txn, to_string(e.type), json(e).dump());
@ -689,6 +700,8 @@ private:
lmdb::dbi outboundMegolmSessionDb_;
lmdb::dbi megolmSessionDataDb_;
lmdb::dbi encryptedRooms_;
QString localUserId_;
QString cacheDirectory_;

View file

@ -31,7 +31,6 @@
#include "notifications/Manager.h"
#include "dialogs/ReadReceipts.h"
#include "timeline/TimelineViewManager.h"
#include "blurhash.hpp"

View file

@ -74,3 +74,21 @@ ImagePackListModel::packAt(int row)
QQmlEngine::setObjectOwnership(e, QQmlEngine::CppOwnership);
return e;
}
SingleImagePackModel *
ImagePackListModel::newPack(bool inRoom)
{
ImagePackInfo info{};
if (inRoom)
info.source_room = room_id;
return new SingleImagePackModel(info);
}
bool
ImagePackListModel::containsAccountPack() const
{
for (const auto &p : packs)
if (p->roomid().isEmpty())
return true;
return false;
}

View file

@ -12,6 +12,7 @@ class SingleImagePackModel;
class ImagePackListModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(bool containsAccountPack READ containsAccountPack CONSTANT)
public:
enum Roles
{
@ -29,6 +30,9 @@ public:
QVariant data(const QModelIndex &index, int role) const override;
Q_INVOKABLE SingleImagePackModel *packAt(int row);
Q_INVOKABLE SingleImagePackModel *newPack(bool inRoom);
bool containsAccountPack() const;
private:
std::string room_id;

View file

@ -36,7 +36,6 @@
#include "dialogs/JoinRoom.h"
#include "dialogs/LeaveRoom.h"
#include "dialogs/Logout.h"
#include "dialogs/ReadReceipts.h"
MainWindow *MainWindow::instance_ = nullptr;
@ -398,27 +397,6 @@ MainWindow::openLogoutDialog()
showDialog(dialog);
}
void
MainWindow::openReadReceiptsDialog(const QString &event_id)
{
auto dialog = new dialogs::ReadReceipts(this);
const auto room_id = chat_page_->currentRoom();
try {
dialog->addUsers(cache::readReceipts(event_id, room_id));
} catch (const lmdb::error &) {
nhlog::db()->warn("failed to retrieve read receipts for {} {}",
event_id.toStdString(),
chat_page_->currentRoom().toStdString());
dialog->deleteLater();
return;
}
showDialog(dialog);
}
bool
MainWindow::hasActiveDialogs() const
{

View file

@ -65,7 +65,6 @@ public:
std::function<void(const mtx::requests::CreateRoom &request)> callback);
void openJoinRoomDialog(std::function<void(const QString &room_id)> callback);
void openLogoutDialog();
void openReadReceiptsDialog(const QString &event_id);
void hideOverlay();
void showSolidOverlayModal(QWidget *content,

View file

@ -2,16 +2,6 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QAbstractSlider>
#include <QLabel>
#include <QListWidgetItem>
#include <QPainter>
#include <QPushButton>
#include <QScrollBar>
#include <QShortcut>
#include <QStyleOption>
#include <QVBoxLayout>
#include "MemberList.h"
#include "Cache.h"
@ -20,7 +10,6 @@
#include "Logging.h"
#include "Utils.h"
#include "timeline/TimelineViewManager.h"
#include "ui/Avatar.h"
MemberList::MemberList(const QString &room_id, QObject *parent)
: QAbstractListModel{parent}

View file

@ -4,9 +4,10 @@
#pragma once
#include "CacheStructs.h"
#include <QAbstractListModel>
#include "CacheStructs.h"
class MemberList : public QAbstractListModel
{
Q_OBJECT

View file

@ -22,7 +22,14 @@ QHash<QString, mtx::crypto::EncryptedFile> infos;
QQuickImageResponse *
MxcImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
{
MxcImageResponse *response = new MxcImageResponse(id, requestedSize);
auto id_ = id;
bool crop = true;
if (id.endsWith("?scale")) {
crop = false;
id_.remove("?scale");
}
MxcImageResponse *response = new MxcImageResponse(id_, crop, requestedSize);
pool.start(response);
return response;
}
@ -36,20 +43,24 @@ void
MxcImageResponse::run()
{
MxcImageProvider::download(
m_id, m_requestedSize, [this](QString, QSize, QImage image, QString) {
m_id,
m_requestedSize,
[this](QString, QSize, QImage image, QString) {
if (image.isNull()) {
m_error = "Failed to download image.";
} else {
m_image = image;
}
emit finished();
});
},
m_crop);
}
void
MxcImageProvider::download(const QString &id,
const QSize &requestedSize,
std::function<void(QString, QSize, QImage, QString)> then)
std::function<void(QString, QSize, QImage, QString)> then,
bool crop)
{
std::optional<mtx::crypto::EncryptedFile> encryptionInfo;
auto temp = infos.find("mxc://" + id);
@ -58,11 +69,12 @@ MxcImageProvider::download(const QString &id,
if (requestedSize.isValid() && !encryptionInfo) {
QString fileName =
QString("%1_%2x%3_crop")
QString("%1_%2x%3_%4")
.arg(QString::fromUtf8(id.toUtf8().toBase64(QByteArray::Base64UrlEncoding |
QByteArray::OmitTrailingEquals)))
.arg(requestedSize.width())
.arg(requestedSize.height());
.arg(requestedSize.height())
.arg(crop ? "crop" : "scale");
QFileInfo fileInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/media_cache",
fileName);
@ -85,7 +97,7 @@ MxcImageProvider::download(const QString &id,
opts.mxc_url = "mxc://" + id.toStdString();
opts.width = requestedSize.width() > 0 ? requestedSize.width() : -1;
opts.height = requestedSize.height() > 0 ? requestedSize.height() : -1;
opts.method = "crop";
opts.method = crop ? "crop" : "scale";
http::client()->get_thumbnail(
opts,
[fileInfo, requestedSize, then, id](const std::string &res,

View file

@ -19,9 +19,10 @@ class MxcImageResponse
, public QRunnable
{
public:
MxcImageResponse(const QString &id, const QSize &requestedSize)
MxcImageResponse(const QString &id, bool crop, const QSize &requestedSize)
: m_id(id)
, m_requestedSize(requestedSize)
, m_crop(crop)
{
setAutoDelete(false);
}
@ -37,6 +38,7 @@ public:
QString m_id, m_error;
QSize m_requestedSize;
QImage m_image;
bool m_crop;
};
class MxcImageProvider
@ -51,7 +53,8 @@ public slots:
static void addEncryptionInfo(mtx::crypto::EncryptedFile info);
static void download(const QString &id,
const QSize &requestedSize,
std::function<void(QString, QSize, QImage, QString)> then);
std::function<void(QString, QSize, QImage, QString)> then,
bool crop = true);
private:
QThreadPool pool;

View file

@ -286,11 +286,17 @@ handle_olm_message(const OlmMessage &msg, const UserKeyCache &otherUserDeviceKey
bool from_their_device = false;
for (auto [device_id, key] : otherUserDeviceKeys.device_keys) {
if (key.keys.at("curve25519:" + device_id) == msg.sender_key) {
if (key.keys.at("ed25519:" + device_id) == sender_ed25519) {
from_their_device = true;
break;
}
auto c_key = key.keys.find("curve25519:" + device_id);
auto e_key = key.keys.find("ed25519:" + device_id);
if (c_key == key.keys.end() || e_key == key.keys.end()) {
nhlog::crypto()->warn(
"Skipping device {} as we have no keys for it.",
device_id);
} else if (c_key->second == msg.sender_key &&
e_key->second == sender_ed25519) {
from_their_device = true;
break;
}
}
if (!from_their_device) {
@ -518,7 +524,8 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
auto own_user_id = http::client()->user_id().to_string();
auto members = cache::client()->getMembersWithKeys(room_id);
auto members = cache::client()->getMembersWithKeys(
room_id, UserSettings::instance()->onlyShareKeysWithVerifiedUsers());
std::map<std::string, std::vector<std::string>> sendSessionTo;
mtx::crypto::OutboundGroupSessionPtr session = nullptr;
@ -1062,7 +1069,7 @@ decryptEvent(const MegolmSessionIndex &index,
mtx::events::collections::TimelineEvent te;
mtx::events::collections::from_json(body, te);
return {std::nullopt, std::nullopt, std::move(te.data)};
return {DecryptionErrorCode::NoError, std::nullopt, std::move(te.data)};
} catch (std::exception &e) {
return {DecryptionErrorCode::ParsingFailed, e.what(), std::nullopt};
}
@ -1138,9 +1145,23 @@ send_encrypted_to_device_messages(const std::map<std::string, std::vector<std::s
auto session = cache::getLatestOlmSession(device_curve);
if (!session || force_new_session) {
claims.one_time_keys[user][device] = mtx::crypto::SIGNED_CURVE25519;
pks[user][device].ed25519 = d.keys.at("ed25519:" + device);
pks[user][device].curve25519 = d.keys.at("curve25519:" + device);
static QMap<QPair<std::string, std::string>, qint64> rateLimit;
auto currentTime = QDateTime::currentSecsSinceEpoch();
if (rateLimit.value(QPair(user, device)) + 60 * 60 * 10 <
currentTime) {
claims.one_time_keys[user][device] =
mtx::crypto::SIGNED_CURVE25519;
pks[user][device].ed25519 = d.keys.at("ed25519:" + device);
pks[user][device].curve25519 =
d.keys.at("curve25519:" + device);
rateLimit.insert(QPair(user, device), currentTime);
} else {
nhlog::crypto()->warn("Not creating new session with {}:{} "
"because of rate limit",
user,
device);
}
continue;
}

View file

@ -14,9 +14,11 @@
constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2";
namespace olm {
Q_NAMESPACE
enum class DecryptionErrorCode
enum DecryptionErrorCode
{
NoError,
MissingSession, // Session was not found, retrieve from backup or request from other devices
// and try again
MissingSessionIndex, // Session was found, but it does not reach back enough to this index,
@ -25,14 +27,13 @@ enum class DecryptionErrorCode
DecryptionFailed, // libolm error
ParsingFailed, // Failed to parse the actual event
ReplayAttack, // Megolm index reused
UnknownFingerprint, // Unknown device Fingerprint
};
Q_ENUM_NS(DecryptionErrorCode)
struct DecryptionResult
{
std::optional<DecryptionErrorCode> error;
DecryptionErrorCode error;
std::optional<std::string> error_message;
std::optional<mtx::events::collections::TimelineEvents> event;
};

131
src/ReadReceiptsModel.cpp Normal file
View file

@ -0,0 +1,131 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ReadReceiptsModel.h"
#include <QLocale>
#include "Cache.h"
#include "Cache_p.h"
#include "Logging.h"
#include "Utils.h"
ReadReceiptsModel::ReadReceiptsModel(QString event_id, QString room_id, QObject *parent)
: QAbstractListModel{parent}
, event_id_{event_id}
, room_id_{room_id}
{
try {
addUsers(cache::readReceipts(event_id_, room_id_));
} catch (const lmdb::error &) {
nhlog::db()->warn("failed to retrieve read receipts for {} {}",
event_id_.toStdString(),
room_id_.toStdString());
return;
}
connect(cache::client(), &Cache::newReadReceipts, this, &ReadReceiptsModel::update);
}
void
ReadReceiptsModel::update()
{
try {
addUsers(cache::readReceipts(event_id_, room_id_));
} catch (const lmdb::error &) {
nhlog::db()->warn("failed to retrieve read receipts for {} {}",
event_id_.toStdString(),
room_id_.toStdString());
return;
}
}
QHash<int, QByteArray>
ReadReceiptsModel::roleNames() const
{
// Note: RawTimestamp is purposely not included here
return {
{Mxid, "mxid"},
{DisplayName, "displayName"},
{AvatarUrl, "avatarUrl"},
{Timestamp, "timestamp"},
};
}
QVariant
ReadReceiptsModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() >= (int)readReceipts_.size() || index.row() < 0)
return {};
switch (role) {
case Mxid:
return readReceipts_[index.row()].first;
case DisplayName:
return cache::displayName(room_id_, readReceipts_[index.row()].first);
case AvatarUrl:
return cache::avatarUrl(room_id_, readReceipts_[index.row()].first);
case Timestamp:
return dateFormat(readReceipts_[index.row()].second);
case RawTimestamp:
return readReceipts_[index.row()].second;
default:
return {};
}
}
void
ReadReceiptsModel::addUsers(
const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users)
{
auto newReceipts = users.size() - readReceipts_.size();
if (newReceipts > 0) {
beginInsertRows(
QModelIndex{}, readReceipts_.size(), readReceipts_.size() + newReceipts - 1);
for (const auto &user : users) {
QPair<QString, QDateTime> item = {
QString::fromStdString(user.second),
QDateTime::fromMSecsSinceEpoch(user.first)};
if (!readReceipts_.contains(item))
readReceipts_.push_back(item);
}
endInsertRows();
}
}
QString
ReadReceiptsModel::dateFormat(const QDateTime &then) const
{
auto now = QDateTime::currentDateTime();
auto days = then.daysTo(now);
if (days == 0)
return QLocale::system().toString(then.time(), QLocale::ShortFormat);
else if (days < 2)
return tr("Yesterday, %1")
.arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
else if (days < 7)
//: %1 is the name of the current day, %2 is the time the read receipt was read. The
//: result may look like this: Monday, 7:15
return QString("%1, %2")
.arg(then.toString("dddd"))
.arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
return QLocale::system().toString(then.time(), QLocale::ShortFormat);
}
ReadReceiptsProxy::ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent)
: QSortFilterProxyModel{parent}
, model_{event_id, room_id, this}
{
setSourceModel(&model_);
setSortRole(ReadReceiptsModel::RawTimestamp);
sort(0, Qt::DescendingOrder);
setDynamicSortFilter(true);
}

73
src/ReadReceiptsModel.h Normal file
View file

@ -0,0 +1,73 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef READRECEIPTSMODEL_H
#define READRECEIPTSMODEL_H
#include <QAbstractListModel>
#include <QDateTime>
#include <QObject>
#include <QSortFilterProxyModel>
#include <QString>
class ReadReceiptsModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles
{
Mxid,
DisplayName,
AvatarUrl,
Timestamp,
RawTimestamp,
};
explicit ReadReceiptsModel(QString event_id, QString room_id, QObject *parent = nullptr);
QString eventId() const { return event_id_; }
QString roomId() const { return room_id_; }
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent) const override
{
Q_UNUSED(parent)
return readReceipts_.size();
}
QVariant data(const QModelIndex &index, int role) const override;
public slots:
void addUsers(const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users);
void update();
private:
QString dateFormat(const QDateTime &then) const;
QString event_id_;
QString room_id_;
QVector<QPair<QString, QDateTime>> readReceipts_;
};
class ReadReceiptsProxy : public QSortFilterProxyModel
{
Q_OBJECT
Q_PROPERTY(QString eventId READ eventId CONSTANT)
Q_PROPERTY(QString roomId READ roomId CONSTANT)
public:
explicit ReadReceiptsProxy(QString event_id, QString room_id, QObject *parent = nullptr);
QString eventId() const { return event_id_; }
QString roomId() const { return room_id_; }
private:
QString event_id_;
QString room_id_;
ReadReceiptsModel model_;
};
#endif // READRECEIPTSMODEL_H

View file

@ -12,6 +12,7 @@
#include <mtx/responses/register.hpp>
#include <mtx/responses/well-known.hpp>
#include <mtxclient/http/client.hpp>
#include "Config.h"
#include "Logging.h"
@ -93,6 +94,7 @@ RegisterPage::RegisterPage(QWidget *parent)
server_input_ = new TextField();
server_input_->setLabel(tr("Homeserver"));
server_input_->setRegexp(QRegularExpression(".+"));
server_input_->setToolTip(
tr("A server that allows registration. Since matrix is decentralized, you need to first "
"find a server you can register on or host your own."));
@ -145,178 +147,39 @@ RegisterPage::RegisterPage(QWidget *parent)
top_layout_->addLayout(button_layout_);
top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter);
top_layout_->addStretch(1);
connect(
this,
&RegisterPage::versionErrorCb,
this,
[this](const QString &msg) {
error_server_label_->show();
server_input_->setValid(false);
showError(error_server_label_, msg);
},
Qt::QueuedConnection);
setLayout(top_layout_);
connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
connect(register_button_, SIGNAL(clicked()), this, SLOT(onRegisterButtonClicked()));
connect(username_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkFields);
connect(username_input_, &TextField::editingFinished, this, &RegisterPage::checkUsername);
connect(password_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkFields);
connect(password_input_, &TextField::editingFinished, this, &RegisterPage::checkPassword);
connect(password_confirmation_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
connect(
password_confirmation_, &TextField::editingFinished, this, &RegisterPage::checkFields);
connect(password_confirmation_,
&TextField::editingFinished,
this,
&RegisterPage::checkPasswordConfirmation);
connect(server_input_, SIGNAL(returnPressed()), register_button_, SLOT(click()));
connect(server_input_, &TextField::editingFinished, this, &RegisterPage::checkFields);
connect(this, &RegisterPage::registerErrorCb, this, [this](const QString &msg) {
showError(msg);
});
connect(
this,
&RegisterPage::registrationFlow,
this,
[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.empty() ? http::client()->generate_txn_id()
: 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();
emit registerAuth(
user,
pass,
mtx::user_interactive::Auth{
session, mtx::user_interactive::auth::Fallback{}});
});
connect(captchaDialog,
&dialogs::ReCaptcha::cancel,
this,
&RegisterPage::errorOccurred);
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(server_input_, &TextField::editingFinished, this, &RegisterPage::checkServer);
connect(
this,
&RegisterPage::registerAuth,
&RegisterPage::serverError,
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);
http::client()->set_device_id(res.device_id);
[this](const QString &msg) {
server_input_->setValid(false);
showError(error_server_label_, msg);
},
Qt::QueuedConnection);
emit registerOk();
return;
}
// The server requires registration flows.
if (err->status_code == 401) {
if (err->matrix_error.unauthorized.flows.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;
}
emit registrationFlow(
user, pass, err->matrix_error.unauthorized);
return;
}
nhlog::net()->warn("failed to register: status_code ({}), "
"matrix_error: ({}), parser error ({})",
static_cast<int>(err->status_code),
err->matrix_error.error,
err->parse_error);
emit registerErrorCb(QString::fromStdString(err->matrix_error.error));
});
});
setLayout(top_layout_);
connect(this, &RegisterPage::wellKnownLookup, this, &RegisterPage::doWellKnownLookup);
connect(this, &RegisterPage::versionsCheck, this, &RegisterPage::doVersionsCheck);
connect(this, &RegisterPage::registration, this, &RegisterPage::doRegistration);
connect(this, &RegisterPage::UIA, this, &RegisterPage::doUIA);
connect(
this, &RegisterPage::registrationWithAuth, this, &RegisterPage::doRegistrationWithAuth);
}
void
@ -345,191 +208,298 @@ RegisterPage::showError(QLabel *label, const QString &msg)
int height = rect.height();
label->setFixedHeight((int)qCeil(width / 200.0) * height);
label->setText(msg);
label->show();
}
bool
RegisterPage::checkOneField(QLabel *label, const TextField *t_field, const QString &msg)
{
if (t_field->isValid()) {
label->setText("");
label->hide();
return true;
} else {
label->show();
showError(label, msg);
return false;
}
}
bool
RegisterPage::checkFields()
RegisterPage::checkUsername()
{
error_label_->setText("");
error_username_label_->setText("");
error_password_label_->setText("");
error_password_confirmation_label_->setText("");
error_server_label_->setText("");
return checkOneField(error_username_label_,
username_input_,
tr("The username must not be empty, and must contain only the "
"characters a-z, 0-9, ., _, =, -, and /."));
}
error_username_label_->hide();
error_password_label_->hide();
error_password_confirmation_label_->hide();
error_server_label_->hide();
bool
RegisterPage::checkPassword()
{
return checkOneField(
error_password_label_, password_input_, tr("Password is not long enough (min 8 chars)"));
}
password_confirmation_->setValid(true);
server_input_->setValid(true);
bool all_fields_good = true;
if (username_input_->isModified() &&
!checkOneField(error_username_label_,
username_input_,
tr("The username must not be empty, and must contain only the "
"characters a-z, 0-9, ., _, =, -, and /."))) {
all_fields_good = false;
} else if (password_input_->isModified() &&
!checkOneField(error_password_label_,
password_input_,
tr("Password is not long enough (min 8 chars)"))) {
all_fields_good = false;
} else if (password_confirmation_->isModified() &&
password_input_->text() != password_confirmation_->text()) {
error_password_confirmation_label_->show();
bool
RegisterPage::checkPasswordConfirmation()
{
if (password_input_->text() == password_confirmation_->text()) {
error_password_confirmation_label_->hide();
password_confirmation_->setValid(true);
return true;
} else {
showError(error_password_confirmation_label_, tr("Passwords don't match"));
password_confirmation_->setValid(false);
all_fields_good = false;
} else if (server_input_->isModified() &&
(!server_input_->hasAcceptableInput() || server_input_->text().isEmpty())) {
error_server_label_->show();
showError(error_server_label_, tr("Invalid server name"));
server_input_->setValid(false);
all_fields_good = false;
return false;
}
if (!username_input_->isModified() || !password_input_->isModified() ||
!password_confirmation_->isModified() || !server_input_->isModified()) {
all_fields_good = false;
}
return all_fields_good;
}
bool
RegisterPage::checkServer()
{
// This doesn't check that the server is reachable,
// just that the input is not obviously wrong.
return checkOneField(error_server_label_, server_input_, tr("Invalid server name"));
}
void
RegisterPage::onRegisterButtonClicked()
{
if (!checkFields()) {
showError(error_label_,
tr("One or more fields have invalid inputs. Please correct those issues "
"and try again."));
return;
} else {
auto username = username_input_->text().toStdString();
auto password = password_input_->text().toStdString();
auto server = server_input_->text().toStdString();
if (checkUsername() && checkPassword() && checkPasswordConfirmation() && checkServer()) {
auto server = server_input_->text().toStdString();
http::client()->set_server(server);
http::client()->verify_certificates(
!UserSettings::instance()->disableCertificateValidation());
http::client()->well_known(
[this, username, password](const mtx::responses::WellKnown &res,
mtx::http::RequestErr err) {
if (err) {
if (err->status_code == 404) {
nhlog::net()->info("Autodiscovery: No .well-known.");
checkVersionAndRegister(username, password);
return;
}
// This starts a chain of `emit`s which ends up doing the
// registration. Signals are used rather than normal function
// calls so that the dialogs used in UIA work correctly.
//
// The sequence of events looks something like this:
//
// dowellKnownLookup
// v
// doVersionsCheck
// v
// doRegistration
// v
// doUIA <-----------------+
// v | More auth required
// doRegistrationWithAuth -+
// | Success
// v
// registering
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. {} {}",
err->status_code,
err->error_code);
return;
}
nhlog::net()->info("Autodiscovery: Discovered '" +
res.homeserver.base_url + "'");
http::client()->set_server(res.homeserver.base_url);
checkVersionAndRegister(username, password);
});
emit wellKnownLookup();
emit registering();
}
}
void
RegisterPage::checkVersionAndRegister(const std::string &username, const std::string &password)
RegisterPage::doWellKnownLookup()
{
http::client()->versions(
[this, username, password](const mtx::responses::Versions &, mtx::http::RequestErr err) {
http::client()->well_known(
[this](const mtx::responses::WellKnown &res, mtx::http::RequestErr err) {
if (err) {
if (err->status_code == 404) {
emit versionErrorCb(tr("The required endpoints were not found. "
"Possibly not a Matrix server."));
nhlog::net()->info("Autodiscovery: No .well-known.");
// Check that the homeserver can be reached
emit versionsCheck();
return;
}
if (!err->parse_error.empty()) {
emit versionErrorCb(tr("Received malformed response. Make sure "
"the homeserver domain is valid."));
emit serverError(
tr("Autodiscovery failed. Received malformed response."));
nhlog::net()->error(
"Autodiscovery failed. Received malformed response.");
return;
}
emit versionErrorCb(tr(
"An unknown error occured. Make sure the homeserver domain is valid."));
emit serverError(tr("Autodiscovery failed. Unknown error when "
"requesting .well-known."));
nhlog::net()->error("Autodiscovery failed. Unknown error when "
"requesting .well-known. {} {}",
err->status_code,
err->error_code);
return;
}
http::client()->registration(
username,
password,
[this, username, password](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 registerOk();
return;
}
// The server requires registration flows.
if (err->status_code == 401) {
if (err->matrix_error.unauthorized.flows.empty()) {
nhlog::net()->warn(
"failed to retrieve registration flows1: ({}) "
"{}",
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, err->matrix_error.unauthorized);
return;
}
nhlog::net()->error(
"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();
});
nhlog::net()->info("Autodiscovery: Discovered '" + res.homeserver.base_url + "'");
http::client()->set_server(res.homeserver.base_url);
// Check that the homeserver can be reached
emit versionsCheck();
});
}
void
RegisterPage::doVersionsCheck()
{
// Make a request to /_matrix/client/versions to check the address
// given is a Matrix homeserver.
http::client()->versions(
[this](const mtx::responses::Versions &, mtx::http::RequestErr err) {
if (err) {
if (err->status_code == 404) {
emit serverError(
tr("The required endpoints were not found. Possibly "
"not a Matrix server."));
return;
}
if (!err->parse_error.empty()) {
emit serverError(
tr("Received malformed response. Make sure the homeserver "
"domain is valid."));
return;
}
emit serverError(tr("An unknown error occured. Make sure the "
"homeserver domain is valid."));
return;
}
// Attempt registration without an `auth` dict
emit registration();
});
}
void
RegisterPage::doRegistration()
{
// These inputs should still be alright, but check just in case
if (checkUsername() && checkPassword() && checkPasswordConfirmation()) {
auto username = username_input_->text().toStdString();
auto password = password_input_->text().toStdString();
http::client()->registration(username, password, registrationCb());
}
}
void
RegisterPage::doRegistrationWithAuth(const mtx::user_interactive::Auth &auth)
{
// These inputs should still be alright, but check just in case
if (checkUsername() && checkPassword() && checkPasswordConfirmation()) {
auto username = username_input_->text().toStdString();
auto password = password_input_->text().toStdString();
http::client()->registration(username, password, auth, registrationCb());
}
}
mtx::http::Callback<mtx::responses::Register>
RegisterPage::registrationCb()
{
// Return a function to be used as the callback when an attempt at
// registration is made.
return [this](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 registerOk();
return;
}
// The server requires registration flows.
if (err->status_code == 401) {
if (err->matrix_error.unauthorized.flows.empty()) {
nhlog::net()->warn("failed to retrieve registration flows: "
"status_code({}), matrix_error({}) ",
static_cast<int>(err->status_code),
err->matrix_error.error);
showError(QString::fromStdString(err->matrix_error.error));
return;
}
// Attempt to complete a UIA stage
emit UIA(err->matrix_error.unauthorized);
return;
}
nhlog::net()->error("failed to register: status_code ({}), matrix_error({})",
static_cast<int>(err->status_code),
err->matrix_error.error);
showError(QString::fromStdString(err->matrix_error.error));
};
}
void
RegisterPage::doUIA(const mtx::user_interactive::Unauthorized &unauthorized)
{
auto completed_stages = unauthorized.completed;
auto flows = unauthorized.flows;
auto session =
unauthorized.session.empty() ? http::client()->generate_txn_id() : unauthorized.session;
nhlog::ui()->info("Completed stages: {}", completed_stages.size());
if (!completed_stages.empty()) {
// Get rid of all flows which don't start with the sequence of
// stages that have already been completed.
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::ui()->error("No available registration flows!");
showError(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, session, captchaDialog]() {
captchaDialog->close();
captchaDialog->deleteLater();
doRegistrationWithAuth(mtx::user_interactive::Auth{
session, mtx::user_interactive::auth::Fallback{}});
});
connect(
captchaDialog, &dialogs::ReCaptcha::cancel, this, &RegisterPage::errorOccurred);
QTimer::singleShot(1000, this, [captchaDialog]() { captchaDialog->show(); });
} else if (current_stage == mtx::user_interactive::auth_types::dummy) {
doRegistrationWithAuth(
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, session, dialog]() {
dialog->close();
dialog->deleteLater();
emit registrationWithAuth(mtx::user_interactive::Auth{
session, mtx::user_interactive::auth::Fallback{}});
});
connect(dialog, &dialogs::FallbackAuth::cancel, this, &RegisterPage::errorOccurred);
dialog->show();
}
}
void
RegisterPage::paintEvent(QPaintEvent *)
{

View file

@ -10,6 +10,7 @@
#include <memory>
#include <mtx/user_interactive.hpp>
#include <mtxclient/http/client.hpp>
class FlatButton;
class RaisedButton;
@ -33,17 +34,16 @@ signals:
void errorOccurred();
//! Used to trigger the corresponding slot outside of the main thread.
void versionErrorCb(const QString &err);
void serverError(const QString &err);
void wellKnownLookup();
void versionsCheck();
void registration();
void UIA(const mtx::user_interactive::Unauthorized &unauthorized);
void registrationWithAuth(const mtx::user_interactive::Auth &auth);
void registering();
void registerOk();
void registerErrorCb(const QString &msg);
void registrationFlow(const std::string &user,
const std::string &pass,
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();
@ -51,12 +51,22 @@ private slots:
// function for showing different errors
void showError(const QString &msg);
void showError(QLabel *label, const QString &msg);
bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg);
bool checkUsername();
bool checkPassword();
bool checkPasswordConfirmation();
bool checkServer();
void doWellKnownLookup();
void doVersionsCheck();
void doRegistration();
void doUIA(const mtx::user_interactive::Unauthorized &unauthorized);
void doRegistrationWithAuth(const mtx::user_interactive::Auth &auth);
mtx::http::Callback<mtx::responses::Register> registrationCb();
private:
bool checkOneField(QLabel *label, const TextField *t_field, const QString &msg);
bool checkFields();
void showError(QLabel *label, const QString &msg);
void checkVersionAndRegister(const std::string &username, const std::string &password);
QVBoxLayout *top_layout_;
QHBoxLayout *back_layout_;
@ -69,6 +79,7 @@ private:
QLabel *error_password_label_;
QLabel *error_password_confirmation_label_;
QLabel *error_server_label_;
QLabel *error_registration_token_label_;
FlatButton *back_button_;
RaisedButton *register_button_;
@ -81,4 +92,5 @@ private:
TextField *password_input_;
TextField *password_confirmation_;
TextField *server_input_;
TextField *registration_token_input_;
};

View file

@ -4,20 +4,35 @@
#include "SingleImagePackModel.h"
#include <QFile>
#include <QMimeDatabase>
#include "Cache_p.h"
#include "ChatPage.h"
#include "Logging.h"
#include "MatrixClient.h"
#include "Utils.h"
#include "timeline/Permissions.h"
#include "timeline/TimelineModel.h"
Q_DECLARE_METATYPE(mtx::common::ImageInfo)
SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent)
: QAbstractListModel(parent)
, roomid_(std::move(pack_.source_room))
, statekey_(std::move(pack_.state_key))
, old_statekey_(statekey_)
, pack(std::move(pack_.pack))
{
[[maybe_unused]] static auto imageInfoType = qRegisterMetaType<mtx::common::ImageInfo>();
if (!pack.pack)
pack.pack = mtx::events::msc2545::ImagePack::PackDescription{};
for (const auto &e : pack.images)
shortcodes.push_back(e.first);
connect(this, &SingleImagePackModel::addImage, this, &SingleImagePackModel::addImageCb);
}
int
@ -61,6 +76,73 @@ SingleImagePackModel::data(const QModelIndex &index, int role) const
return {};
}
bool
SingleImagePackModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
using mtx::events::msc2545::PackUsage;
if (hasIndex(index.row(), index.column(), index.parent())) {
auto &img = pack.images.at(shortcodes.at(index.row()));
switch (role) {
case ShortCode: {
auto newCode = value.toString().toStdString();
// otherwise we delete this by accident
if (pack.images.count(newCode))
return false;
auto tmp = img;
auto oldCode = shortcodes.at(index.row());
pack.images.erase(oldCode);
shortcodes[index.row()] = newCode;
pack.images.insert({newCode, tmp});
emit dataChanged(
this->index(index.row()), this->index(index.row()), {Roles::ShortCode});
return true;
}
case Body:
img.body = value.toString().toStdString();
emit dataChanged(
this->index(index.row()), this->index(index.row()), {Roles::Body});
return true;
case IsEmote: {
bool isEmote = value.toBool();
bool isSticker =
img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker();
img.usage.set(PackUsage::Emoji, isEmote);
img.usage.set(PackUsage::Sticker, isSticker);
if (img.usage == pack.pack->usage)
img.usage.reset();
emit dataChanged(
this->index(index.row()), this->index(index.row()), {Roles::IsEmote});
return true;
}
case IsSticker: {
bool isEmote =
img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji();
bool isSticker = value.toBool();
img.usage.set(PackUsage::Emoji, isEmote);
img.usage.set(PackUsage::Sticker, isSticker);
if (img.usage == pack.pack->usage)
img.usage.reset();
emit dataChanged(
this->index(index.row()), this->index(index.row()), {Roles::IsSticker});
return true;
}
}
}
return false;
}
bool
SingleImagePackModel::isGloballyEnabled() const
{
@ -98,3 +180,171 @@ SingleImagePackModel::setGloballyEnabled(bool enabled)
// emit this->globallyEnabledChanged();
});
}
bool
SingleImagePackModel::canEdit() const
{
if (roomid_.empty())
return true;
else
return Permissions(QString::fromStdString(roomid_))
.canChange(qml_mtx_events::ImagePackInRoom);
}
void
SingleImagePackModel::setPackname(QString val)
{
auto val_ = val.toStdString();
if (val_ != this->pack.pack->display_name) {
this->pack.pack->display_name = val_;
emit packnameChanged();
}
}
void
SingleImagePackModel::setAttribution(QString val)
{
auto val_ = val.toStdString();
if (val_ != this->pack.pack->attribution) {
this->pack.pack->attribution = val_;
emit attributionChanged();
}
}
void
SingleImagePackModel::setAvatarUrl(QString val)
{
auto val_ = val.toStdString();
if (val_ != this->pack.pack->avatar_url) {
this->pack.pack->avatar_url = val_;
emit avatarUrlChanged();
}
}
void
SingleImagePackModel::setStatekey(QString val)
{
auto val_ = val.toStdString();
if (val_ != statekey_) {
statekey_ = val_;
emit statekeyChanged();
}
}
void
SingleImagePackModel::setIsStickerPack(bool val)
{
using mtx::events::msc2545::PackUsage;
if (val != pack.pack->is_sticker()) {
pack.pack->usage.set(PackUsage::Sticker, val);
emit isStickerPackChanged();
}
}
void
SingleImagePackModel::setIsEmotePack(bool val)
{
using mtx::events::msc2545::PackUsage;
if (val != pack.pack->is_emoji()) {
pack.pack->usage.set(PackUsage::Emoji, val);
emit isEmotePackChanged();
}
}
void
SingleImagePackModel::save()
{
if (roomid_.empty()) {
http::client()->put_account_data(pack, [](mtx::http::RequestErr e) {
if (e)
ChatPage::instance()->showNotification(
tr("Failed to update image pack: {}")
.arg(QString::fromStdString(e->matrix_error.error)));
});
} else {
if (old_statekey_ != statekey_) {
http::client()->send_state_event(
roomid_,
to_string(mtx::events::EventType::ImagePackInRoom),
old_statekey_,
nlohmann::json::object(),
[](const mtx::responses::EventId &, mtx::http::RequestErr e) {
if (e)
ChatPage::instance()->showNotification(
tr("Failed to delete old image pack: {}")
.arg(QString::fromStdString(e->matrix_error.error)));
});
}
http::client()->send_state_event(
roomid_,
statekey_,
pack,
[this](const mtx::responses::EventId &, mtx::http::RequestErr e) {
if (e)
ChatPage::instance()->showNotification(
tr("Failed to update image pack: {}")
.arg(QString::fromStdString(e->matrix_error.error)));
nhlog::net()->info("Uploaded image pack: {}", statekey_);
});
}
}
void
SingleImagePackModel::addStickers(QList<QUrl> files)
{
for (const auto &f : files) {
auto file = QFile(f.toLocalFile());
if (!file.open(QFile::ReadOnly)) {
ChatPage::instance()->showNotification(
tr("Failed to open image: {}").arg(f.toLocalFile()));
return;
}
auto bytes = file.readAll();
auto img = utils::readImage(bytes);
mtx::common::ImageInfo info{};
auto sz = img.size() / 2;
if (sz.width() > 512 || sz.height() > 512) {
sz.scale(512, 512, Qt::AspectRatioMode::KeepAspectRatio);
}
info.h = sz.height();
info.w = sz.width();
info.size = bytes.size();
auto filename = f.fileName().toStdString();
http::client()->upload(
bytes.toStdString(),
QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(),
filename,
[this, filename, info](const mtx::responses::ContentURI &uri,
mtx::http::RequestErr e) {
if (e) {
ChatPage::instance()->showNotification(
tr("Failed to upload image: {}")
.arg(QString::fromStdString(e->matrix_error.error)));
return;
}
emit addImage(uri.content_uri, filename, info);
});
}
}
void
SingleImagePackModel::addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info)
{
mtx::events::msc2545::PackImage img{};
img.url = uri;
img.info = info;
beginInsertRows(
QModelIndex(), static_cast<int>(shortcodes.size()), static_cast<int>(shortcodes.size()));
pack.images[filename] = img;
shortcodes.push_back(filename);
endInsertRows();
}

View file

@ -5,6 +5,8 @@
#pragma once
#include <QAbstractListModel>
#include <QList>
#include <QUrl>
#include <mtx/events/mscs/image_packs.hpp>
@ -15,14 +17,18 @@ class SingleImagePackModel : public QAbstractListModel
Q_OBJECT
Q_PROPERTY(QString roomid READ roomid CONSTANT)
Q_PROPERTY(QString statekey READ statekey CONSTANT)
Q_PROPERTY(QString attribution READ statekey CONSTANT)
Q_PROPERTY(QString packname READ packname CONSTANT)
Q_PROPERTY(QString avatarUrl READ avatarUrl CONSTANT)
Q_PROPERTY(bool isStickerPack READ isStickerPack CONSTANT)
Q_PROPERTY(bool isEmotePack READ isEmotePack CONSTANT)
Q_PROPERTY(QString statekey READ statekey WRITE setStatekey NOTIFY statekeyChanged)
Q_PROPERTY(
QString attribution READ attribution WRITE setAttribution NOTIFY attributionChanged)
Q_PROPERTY(QString packname READ packname WRITE setPackname NOTIFY packnameChanged)
Q_PROPERTY(QString avatarUrl READ avatarUrl WRITE setAvatarUrl NOTIFY avatarUrlChanged)
Q_PROPERTY(
bool isStickerPack READ isStickerPack WRITE setIsStickerPack NOTIFY isStickerPackChanged)
Q_PROPERTY(bool isEmotePack READ isEmotePack WRITE setIsEmotePack NOTIFY isEmotePackChanged)
Q_PROPERTY(bool isGloballyEnabled READ isGloballyEnabled WRITE setGloballyEnabled NOTIFY
globallyEnabledChanged)
Q_PROPERTY(bool canEdit READ canEdit CONSTANT)
public:
enum Roles
{
@ -32,11 +38,15 @@ public:
IsEmote,
IsSticker,
};
Q_ENUM(Roles);
SingleImagePackModel(ImagePackInfo pack_, QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
bool setData(const QModelIndex &index,
const QVariant &value,
int role = Qt::EditRole) override;
QString roomid() const { return QString::fromStdString(roomid_); }
QString statekey() const { return QString::fromStdString(statekey_); }
@ -47,14 +57,36 @@ public:
bool isEmotePack() const { return pack.pack->is_emoji(); }
bool isGloballyEnabled() const;
bool canEdit() const;
void setGloballyEnabled(bool enabled);
void setPackname(QString val);
void setAttribution(QString val);
void setAvatarUrl(QString val);
void setStatekey(QString val);
void setIsStickerPack(bool val);
void setIsEmotePack(bool val);
Q_INVOKABLE void save();
Q_INVOKABLE void addStickers(QList<QUrl> files);
signals:
void globallyEnabledChanged();
void statekeyChanged();
void attributionChanged();
void packnameChanged();
void avatarUrlChanged();
void isEmotePackChanged();
void isStickerPackChanged();
void addImage(std::string uri, std::string filename, mtx::common::ImageInfo info);
private slots:
void addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info);
private:
std::string roomid_;
std::string statekey_;
std::string statekey_, old_statekey_;
mtx::events::msc2545::ImagePack pack;
std::vector<std::string> shortcodes;

View file

@ -90,13 +90,11 @@ UserSettings::load(std::optional<QString> profile)
decryptSidebar_ = settings.value("user/decrypt_sidebar", true).toBool();
privacyScreen_ = settings.value("user/privacy_screen", false).toBool();
privacyScreenTimeout_ = settings.value("user/privacy_screen_timeout", 0).toInt();
shareKeysWithTrustedUsers_ =
settings.value("user/automatically_share_keys_with_trusted_users", false).toBool();
mobileMode_ = settings.value("user/mobile_mode", false).toBool();
emojiFont_ = settings.value("user/emoji_font_family", "default").toString();
baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble();
auto tempPresence = settings.value("user/presence", "").toString().toStdString();
auto presenceValue = QMetaEnum::fromType<Presence>().keyToValue(tempPresence.c_str());
mobileMode_ = settings.value("user/mobile_mode", false).toBool();
emojiFont_ = settings.value("user/emoji_font_family", "default").toString();
baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble();
auto tempPresence = settings.value("user/presence", "").toString().toStdString();
auto presenceValue = QMetaEnum::fromType<Presence>().keyToValue(tempPresence.c_str());
if (presenceValue < 0)
presenceValue = 0;
presence_ = static_cast<Presence>(presenceValue);
@ -123,6 +121,12 @@ UserSettings::load(std::optional<QString> profile)
userId_ = settings.value(prefix + "auth/user_id", "").toString();
deviceId_ = settings.value(prefix + "auth/device_id", "").toString();
shareKeysWithTrustedUsers_ =
settings.value(prefix + "user/automatically_share_keys_with_trusted_users", false)
.toBool();
onlyShareKeysWithVerifiedUsers_ =
settings.value(prefix + "user/only_share_keys_with_verified_users", false).toBool();
disableCertificateValidation_ =
settings.value("disable_certificate_validation", false).toBool();
@ -401,6 +405,17 @@ UserSettings::setUseStunServer(bool useStunServer)
save();
}
void
UserSettings::setOnlyShareKeysWithVerifiedUsers(bool shareKeys)
{
if (shareKeys == onlyShareKeysWithVerifiedUsers_)
return;
onlyShareKeysWithVerifiedUsers_ = shareKeys;
emit onlyShareKeysWithVerifiedUsersChanged(shareKeys);
save();
}
void
UserSettings::setShareKeysWithTrustedUsers(bool shareKeys)
{
@ -610,8 +625,6 @@ UserSettings::save()
settings.setValue("decrypt_sidebar", decryptSidebar_);
settings.setValue("privacy_screen", privacyScreen_);
settings.setValue("privacy_screen_timeout", privacyScreenTimeout_);
settings.setValue("automatically_share_keys_with_trusted_users",
shareKeysWithTrustedUsers_);
settings.setValue("mobile_mode", mobileMode_);
settings.setValue("font_size", baseFontSize_);
settings.setValue("typing_notifications", typingNotifications_);
@ -650,6 +663,11 @@ UserSettings::save()
settings.setValue(prefix + "auth/user_id", userId_);
settings.setValue(prefix + "auth/device_id", deviceId_);
settings.setValue(prefix + "user/automatically_share_keys_with_trusted_users",
shareKeysWithTrustedUsers_);
settings.setValue(prefix + "user/only_share_keys_with_verified_users",
onlyShareKeysWithVerifiedUsers_);
settings.setValue("disable_certificate_validation", disableCertificateValidation_);
settings.sync();
@ -703,41 +721,43 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
general_->setFont(font);
trayToggle_ = new Toggle{this};
startInTrayToggle_ = new Toggle{this};
avatarCircles_ = new Toggle{this};
decryptSidebar_ = new Toggle(this);
privacyScreen_ = new Toggle{this};
shareKeysWithTrustedUsers_ = new Toggle(this);
groupViewToggle_ = new Toggle{this};
timelineButtonsToggle_ = new Toggle{this};
typingNotifications_ = new Toggle{this};
messageHoverHighlight_ = new Toggle{this};
enlargeEmojiOnlyMessages_ = new Toggle{this};
sortByImportance_ = new Toggle{this};
readReceipts_ = new Toggle{this};
markdown_ = new Toggle{this};
desktopNotifications_ = new Toggle{this};
alertOnNotification_ = new Toggle{this};
useStunServer_ = new Toggle{this};
mobileMode_ = new Toggle{this};
scaleFactorCombo_ = new QComboBox{this};
fontSizeCombo_ = new QComboBox{this};
fontSelectionCombo_ = new QFontComboBox{this};
emojiFontSelectionCombo_ = new QComboBox{this};
ringtoneCombo_ = new QComboBox{this};
microphoneCombo_ = new QComboBox{this};
cameraCombo_ = new QComboBox{this};
cameraResolutionCombo_ = new QComboBox{this};
cameraFrameRateCombo_ = new QComboBox{this};
timelineMaxWidthSpin_ = new QSpinBox{this};
privacyScreenTimeout_ = new QSpinBox{this};
trayToggle_ = new Toggle{this};
startInTrayToggle_ = new Toggle{this};
avatarCircles_ = new Toggle{this};
decryptSidebar_ = new Toggle(this);
privacyScreen_ = new Toggle{this};
onlyShareKeysWithVerifiedUsers_ = new Toggle(this);
shareKeysWithTrustedUsers_ = new Toggle(this);
groupViewToggle_ = new Toggle{this};
timelineButtonsToggle_ = new Toggle{this};
typingNotifications_ = new Toggle{this};
messageHoverHighlight_ = new Toggle{this};
enlargeEmojiOnlyMessages_ = new Toggle{this};
sortByImportance_ = new Toggle{this};
readReceipts_ = new Toggle{this};
markdown_ = new Toggle{this};
desktopNotifications_ = new Toggle{this};
alertOnNotification_ = new Toggle{this};
useStunServer_ = new Toggle{this};
mobileMode_ = new Toggle{this};
scaleFactorCombo_ = new QComboBox{this};
fontSizeCombo_ = new QComboBox{this};
fontSelectionCombo_ = new QFontComboBox{this};
emojiFontSelectionCombo_ = new QComboBox{this};
ringtoneCombo_ = new QComboBox{this};
microphoneCombo_ = new QComboBox{this};
cameraCombo_ = new QComboBox{this};
cameraResolutionCombo_ = new QComboBox{this};
cameraFrameRateCombo_ = new QComboBox{this};
timelineMaxWidthSpin_ = new QSpinBox{this};
privacyScreenTimeout_ = new QSpinBox{this};
trayToggle_->setChecked(settings_->tray());
startInTrayToggle_->setChecked(settings_->startInTray());
avatarCircles_->setChecked(settings_->avatarCircles());
decryptSidebar_->setChecked(settings_->decryptSidebar());
privacyScreen_->setChecked(settings_->privacyScreen());
onlyShareKeysWithVerifiedUsers_->setChecked(settings_->onlyShareKeysWithVerifiedUsers());
shareKeysWithTrustedUsers_->setChecked(settings_->shareKeysWithTrustedUsers());
groupViewToggle_->setChecked(settings_->groupView());
timelineButtonsToggle_->setChecked(settings_->buttonsInTimeline());
@ -1008,10 +1028,14 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
formLayout_->addRow(new HorizontalLine{this});
boxWrap(tr("Device ID"), deviceIdValue_);
boxWrap(tr("Device Fingerprint"), deviceFingerprintValue_);
boxWrap(
tr("Share keys with verified users and devices"),
shareKeysWithTrustedUsers_,
tr("Automatically replies to key requests from other users, if they are verified."));
boxWrap(tr("Send encrypted messages to verified users only"),
onlyShareKeysWithVerifiedUsers_,
tr("Requires a user to be verified to send encrypted messages to them. This "
"improves safety but makes E2EE more tedious."));
boxWrap(tr("Share keys with verified users and devices"),
shareKeysWithTrustedUsers_,
tr("Automatically replies to key requests from other users, if they are verified, "
"even if that device shouldn't have access to those keys otherwise."));
formLayout_->addRow(new HorizontalLine{this});
formLayout_->addRow(sessionKeysLabel, sessionKeysLayout);
formLayout_->addRow(crossSigningKeysLabel, crossSigningKeysLayout);
@ -1179,6 +1203,10 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
}
});
connect(onlyShareKeysWithVerifiedUsers_, &Toggle::toggled, this, [this](bool enabled) {
settings_->setOnlyShareKeysWithVerifiedUsers(enabled);
});
connect(shareKeysWithTrustedUsers_, &Toggle::toggled, this, [this](bool enabled) {
settings_->setShareKeysWithTrustedUsers(enabled);
});
@ -1271,6 +1299,7 @@ UserSettingsPage::showEvent(QShowEvent *)
groupViewToggle_->setState(settings_->groupView());
decryptSidebar_->setState(settings_->decryptSidebar());
privacyScreen_->setState(settings_->privacyScreen());
onlyShareKeysWithVerifiedUsers_->setState(settings_->onlyShareKeysWithVerifiedUsers());
shareKeysWithTrustedUsers_->setState(settings_->shareKeysWithTrustedUsers());
avatarCircles_->setState(settings_->avatarCircles());
typingNotifications_->setState(settings_->typingNotifications());

View file

@ -88,6 +88,8 @@ class UserSettings : public QObject
setScreenShareHideCursor NOTIFY screenShareHideCursorChanged)
Q_PROPERTY(
bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged)
Q_PROPERTY(bool onlyShareKeysWithVerifiedUsers READ onlyShareKeysWithVerifiedUsers WRITE
setOnlyShareKeysWithVerifiedUsers NOTIFY onlyShareKeysWithVerifiedUsersChanged)
Q_PROPERTY(bool shareKeysWithTrustedUsers READ shareKeysWithTrustedUsers WRITE
setShareKeysWithTrustedUsers NOTIFY shareKeysWithTrustedUsersChanged)
Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged)
@ -152,6 +154,7 @@ public:
void setScreenShareRemoteVideo(bool state);
void setScreenShareHideCursor(bool state);
void setUseStunServer(bool state);
void setOnlyShareKeysWithVerifiedUsers(bool state);
void setShareKeysWithTrustedUsers(bool state);
void setProfile(QString profile);
void setUserId(QString userId);
@ -208,6 +211,7 @@ public:
bool screenShareHideCursor() const { return screenShareHideCursor_; }
bool useStunServer() const { return useStunServer_; }
bool shareKeysWithTrustedUsers() const { return shareKeysWithTrustedUsers_; }
bool onlyShareKeysWithVerifiedUsers() const { return onlyShareKeysWithVerifiedUsers_; }
QString profile() const { return profile_; }
QString userId() const { return userId_; }
QString accessToken() const { return accessToken_; }
@ -252,6 +256,7 @@ signals:
void screenShareRemoteVideoChanged(bool state);
void screenShareHideCursorChanged(bool state);
void useStunServerChanged(bool state);
void onlyShareKeysWithVerifiedUsersChanged(bool state);
void shareKeysWithTrustedUsersChanged(bool state);
void profileChanged(QString profile);
void userIdChanged(QString userId);
@ -284,6 +289,7 @@ private:
bool privacyScreen_;
int privacyScreenTimeout_;
bool shareKeysWithTrustedUsers_;
bool onlyShareKeysWithVerifiedUsers_;
bool mobileMode_;
int timelineMaxWidth_;
int roomListWidth_;
@ -372,6 +378,7 @@ private:
Toggle *privacyScreen_;
QSpinBox *privacyScreenTimeout_;
Toggle *shareKeysWithTrustedUsers_;
Toggle *onlyShareKeysWithVerifiedUsers_;
Toggle *mobileMode_;
QLabel *deviceFingerprintValue_;
QLabel *deviceIdValue_;

View file

@ -1,60 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QFont>
#include <QFontDatabase>
#include <QTextBrowser>
#include <QVBoxLayout>
#include <QWidget>
#include "nlohmann/json.hpp"
#include "Logging.h"
#include "MainWindow.h"
#include "ui/FlatButton.h"
namespace dialogs {
class RawMessage : public QWidget
{
Q_OBJECT
public:
RawMessage(QString msg, QWidget *parent = nullptr)
: QWidget{parent}
{
QFont monospaceFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
auto layout = new QVBoxLayout{this};
auto viewer = new QTextBrowser{this};
viewer->setFont(monospaceFont);
viewer->setText(msg);
layout->setSpacing(0);
layout->setMargin(0);
layout->addWidget(viewer);
setAutoFillBackground(true);
setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
setAttribute(Qt::WA_DeleteOnClose, true);
QSize winsize;
QPoint center;
auto window = MainWindow::instance();
if (window) {
winsize = window->frameGeometry().size();
center = window->frameGeometry().center();
move(center.x() - (width() * 0.5), center.y() - (height() * 0.5));
} else {
nhlog::ui()->warn("unable to retrieve MainWindow's size");
}
raise();
show();
}
};
} // namespace dialogs

View file

@ -1,179 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QDebug>
#include <QIcon>
#include <QLabel>
#include <QListWidgetItem>
#include <QPainter>
#include <QPushButton>
#include <QShortcut>
#include <QStyleOption>
#include <QTimer>
#include <QVBoxLayout>
#include "dialogs/ReadReceipts.h"
#include "AvatarProvider.h"
#include "Cache.h"
#include "ChatPage.h"
#include "Config.h"
#include "Utils.h"
#include "ui/Avatar.h"
using namespace dialogs;
ReceiptItem::ReceiptItem(QWidget *parent,
const QString &user_id,
uint64_t timestamp,
const QString &room_id)
: QWidget(parent)
{
topLayout_ = new QHBoxLayout(this);
topLayout_->setMargin(0);
textLayout_ = new QVBoxLayout;
textLayout_->setMargin(0);
textLayout_->setSpacing(conf::modals::TEXT_SPACING);
QFont nameFont;
nameFont.setPointSizeF(nameFont.pointSizeF() * 1.1);
auto displayName = cache::displayName(room_id, user_id);
avatar_ = new Avatar(this, 44);
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);
userName_->setFont(nameFont);
timestamp_ = new QLabel(dateFormat(QDateTime::fromMSecsSinceEpoch(timestamp)), this);
textLayout_->addWidget(userName_);
textLayout_->addWidget(timestamp_);
topLayout_->addWidget(avatar_);
topLayout_->addLayout(textLayout_, 1);
avatar_->setImage(ChatPage::instance()->currentRoom(), user_id);
}
void
ReceiptItem::paintEvent(QPaintEvent *)
{
QStyleOption opt;
opt.init(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}
QString
ReceiptItem::dateFormat(const QDateTime &then) const
{
auto now = QDateTime::currentDateTime();
auto days = then.daysTo(now);
if (days == 0)
return tr("Today %1")
.arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
else if (days < 2)
return tr("Yesterday %1")
.arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
else if (days < 7)
return QString("%1 %2")
.arg(then.toString("dddd"))
.arg(QLocale::system().toString(then.time(), QLocale::ShortFormat));
return QLocale::system().toString(then.time(), QLocale::ShortFormat);
}
ReadReceipts::ReadReceipts(QWidget *parent)
: QFrame(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);
userList_ = new QListWidget;
userList_->setFrameStyle(QFrame::NoFrame);
userList_->setSelectionMode(QAbstractItemView::NoSelection);
userList_->setSpacing(conf::modals::TEXT_SPACING);
QFont largeFont;
largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5);
setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
setMinimumHeight(userList_->sizeHint().height() * 2);
setMinimumWidth(std::max(userList_->sizeHint().width() + 4 * conf::modals::WIDGET_MARGIN,
QFontMetrics(largeFont).averageCharWidth() * 30 -
2 * conf::modals::WIDGET_MARGIN));
QFont font;
font.setPointSizeF(font.pointSizeF() * conf::modals::LABEL_MEDIUM_SIZE_RATIO);
topLabel_ = new QLabel(tr("Read receipts"), this);
topLabel_->setAlignment(Qt::AlignCenter);
topLabel_->setFont(font);
auto okBtn = new QPushButton(tr("Close"), this);
auto buttonLayout = new QHBoxLayout();
buttonLayout->setSpacing(15);
buttonLayout->addStretch(1);
buttonLayout->addWidget(okBtn);
layout->addWidget(topLabel_);
layout->addWidget(userList_);
layout->addLayout(buttonLayout);
auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this);
connect(closeShortcut, &QShortcut::activated, this, &ReadReceipts::close);
connect(okBtn, &QPushButton::clicked, this, &ReadReceipts::close);
}
void
ReadReceipts::addUsers(const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &receipts)
{
// We want to remove any previous items that have been set.
userList_->clear();
for (const auto &receipt : receipts) {
auto user = new ReceiptItem(this,
QString::fromStdString(receipt.second),
receipt.first,
ChatPage::instance()->currentRoom());
auto item = new QListWidgetItem(userList_);
item->setSizeHint(user->minimumSizeHint());
item->setFlags(Qt::NoItemFlags);
item->setTextAlignment(Qt::AlignCenter);
userList_->setItemWidget(item, user);
}
}
void
ReadReceipts::paintEvent(QPaintEvent *)
{
QStyleOption opt;
opt.init(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}
void
ReadReceipts::hideEvent(QHideEvent *event)
{
userList_->clear();
QFrame::hideEvent(event);
}

View file

@ -1,61 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QDateTime>
#include <QFrame>
class Avatar;
class QLabel;
class QListWidget;
class QHBoxLayout;
class QVBoxLayout;
namespace dialogs {
class ReceiptItem : public QWidget
{
Q_OBJECT
public:
ReceiptItem(QWidget *parent,
const QString &user_id,
uint64_t timestamp,
const QString &room_id);
protected:
void paintEvent(QPaintEvent *) override;
private:
QString dateFormat(const QDateTime &then) const;
QHBoxLayout *topLayout_;
QVBoxLayout *textLayout_;
Avatar *avatar_;
QLabel *userName_;
QLabel *timestamp_;
};
class ReadReceipts : public QFrame
{
Q_OBJECT
public:
explicit ReadReceipts(QWidget *parent = nullptr);
public slots:
void addUsers(const std::multimap<uint64_t, std::string, std::greater<uint64_t>> &users);
protected:
void paintEvent(QPaintEvent *event) override;
void hideEvent(QHideEvent *event) override;
private:
QLabel *topLabel_;
QListWidget *userList_;
};
} // dialogs

View file

@ -20,8 +20,7 @@
Q_DECLARE_METATYPE(Reaction)
QCache<EventStore::IdIndex, mtx::events::collections::TimelineEvents> EventStore::decryptedEvents_{
1000};
QCache<EventStore::IdIndex, olm::DecryptionResult> EventStore::decryptedEvents_{1000};
QCache<EventStore::IdIndex, mtx::events::collections::TimelineEvents> EventStore::events_by_id_{
1000};
QCache<EventStore::Index, mtx::events::collections::TimelineEvents> EventStore::events_{1000};
@ -144,12 +143,16 @@ EventStore::EventStore(std::string room_id, QObject *)
mtx::events::msg::Encrypted>) {
auto event =
decryptEvent({room_id_, e.event_id}, e);
if (auto dec =
std::get_if<mtx::events::RoomEvent<
mtx::events::msg::
KeyVerificationRequest>>(event)) {
emit updateFlowEventId(
event_id.event_id.to_string());
if (event->event) {
if (auto dec = std::get_if<
mtx::events::RoomEvent<
mtx::events::msg::
KeyVerificationRequest>>(
&event->event.value())) {
emit updateFlowEventId(
event_id.event_id
.to_string());
}
}
}
});
@ -393,12 +396,12 @@ EventStore::handleSync(const mtx::responses::Timeline &events)
if (auto encrypted =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
&event)) {
mtx::events::collections::TimelineEvents *d_event =
decryptEvent({room_id_, encrypted->event_id}, *encrypted);
if (std::visit(
auto d_event = decryptEvent({room_id_, encrypted->event_id}, *encrypted);
if (d_event->event &&
std::visit(
[](auto e) { return (e.sender != utils::localUser().toStdString()); },
*d_event)) {
handle_room_verification(*d_event);
*d_event->event)) {
handle_room_verification(*d_event->event);
}
}
}
@ -599,11 +602,15 @@ EventStore::get(int idx, bool decrypt)
events_.insert(index, event_ptr);
}
if (decrypt)
if (decrypt) {
if (auto encrypted =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
event_ptr))
return decryptEvent({room_id_, encrypted->event_id}, *encrypted);
event_ptr)) {
auto decrypted = decryptEvent({room_id_, encrypted->event_id}, *encrypted);
if (decrypted->event)
return &*decrypted->event;
}
}
return event_ptr;
}
@ -629,7 +636,7 @@ EventStore::indexToId(int idx) const
return cache::client()->getTimelineEventId(room_id_, toInternalIdx(idx));
}
mtx::events::collections::TimelineEvents *
olm::DecryptionResult *
EventStore::decryptEvent(const IdIndex &idx,
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e)
{
@ -641,57 +648,24 @@ EventStore::decryptEvent(const IdIndex &idx,
index.session_id = e.content.session_id;
index.sender_key = e.content.sender_key;
auto asCacheEntry = [&idx](mtx::events::collections::TimelineEvents &&event) {
auto event_ptr = new mtx::events::collections::TimelineEvents(std::move(event));
auto asCacheEntry = [&idx](olm::DecryptionResult &&event) {
auto event_ptr = new olm::DecryptionResult(std::move(event));
decryptedEvents_.insert(idx, event_ptr);
return event_ptr;
};
auto decryptionResult = olm::decryptEvent(index, e);
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;
if (decryptionResult.error) {
switch (*decryptionResult.error) {
switch (decryptionResult.error) {
case olm::DecryptionErrorCode::MissingSession:
case olm::DecryptionErrorCode::MissingSessionIndex: {
if (decryptionResult.error == olm::DecryptionErrorCode::MissingSession)
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();
else
dummy.content.body =
tr("-- Encrypted Event (Key not valid for this index) --",
"Placeholder, when the message can't be decrypted with this "
"key since it is not valid for this index ")
.toStdString();
nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})",
index.room_id,
index.session_id,
e.sender);
// we may not want to request keys during initial sync and such
if (suppressKeyRequests)
break;
// TODO: Check if this actually works and look in key backup
auto copy = e;
copy.room_id = room_id_;
if (pending_key_requests.count(e.content.session_id)) {
pending_key_requests.at(e.content.session_id)
.events.push_back(copy);
} else {
PendingKeyRequests request;
request.request_id =
"key_request." + http::client()->generate_txn_id();
request.events.push_back(copy);
olm::send_key_request_for(copy, request.request_id);
pending_key_requests[e.content.session_id] = request;
}
requestSession(e, false);
break;
}
case olm::DecryptionErrorCode::DbError:
@ -701,12 +675,6 @@ EventStore::decryptEvent(const IdIndex &idx,
index.session_id,
index.sender_key,
decryptionResult.error_message.value_or(""));
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();
break;
case olm::DecryptionErrorCode::DecryptionFailed:
nhlog::crypto()->critical(
@ -715,22 +683,8 @@ EventStore::decryptEvent(const IdIndex &idx,
index.session_id,
index.sender_key,
decryptionResult.error_message.value_or(""));
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 as %1.")
.arg(
QString::fromStdString(decryptionResult.error_message.value_or("")))
.toStdString();
break;
case olm::DecryptionErrorCode::ParsingFailed:
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();
break;
case olm::DecryptionErrorCode::ReplayAttack:
nhlog::crypto()->critical(
@ -738,85 +692,50 @@ EventStore::decryptEvent(const IdIndex &idx,
e.event_id,
room_id_,
index.sender_key);
dummy.content.body =
tr("-- Replay attack! This message index was reused! --").toStdString();
break;
case olm::DecryptionErrorCode::UnknownFingerprint:
// TODO: don't fail, just show in UI.
nhlog::crypto()->critical("Message by unverified fingerprint {}",
index.sender_key);
dummy.content.body =
tr("-- Message by unverified device! --").toStdString();
case olm::DecryptionErrorCode::NoError:
// unreachable
break;
}
return asCacheEntry(std::move(dummy));
}
std::string msg_str;
try {
auto session = cache::client()->getInboundMegolmSession(index);
auto res =
olm::client()->decrypt_group_message(session.get(), 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 asCacheEntry(std::move(dummy));
} 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 as %1.")
.arg(e.what())
.toStdString();
return asCacheEntry(std::move(dummy));
}
// 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...
mtx::common::add_relations(body["content"], e.content.relations);
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) {
auto encInfo = mtx::accessors::file(temp_events[0]);
if (encInfo)
emit newEncryptedImage(encInfo.value());
return asCacheEntry(std::move(temp_events[0]));
return asCacheEntry(std::move(decryptionResult));
}
auto encInfo = mtx::accessors::file(decryptionResult.event.value());
if (encInfo)
emit newEncryptedImage(encInfo.value());
return asCacheEntry(std::move(decryptionResult.event.value()));
return asCacheEntry(std::move(decryptionResult));
}
void
EventStore::requestSession(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &ev,
bool manual)
{
// we may not want to request keys during initial sync and such
if (suppressKeyRequests)
return;
// TODO: Look in key backup
auto copy = ev;
copy.room_id = room_id_;
if (pending_key_requests.count(ev.content.session_id)) {
auto &r = pending_key_requests.at(ev.content.session_id);
r.events.push_back(copy);
// automatically request once every 10 min, manually every 1 min
qint64 delay = manual ? 60 : (60 * 10);
if (r.requested_at + delay < QDateTime::currentSecsSinceEpoch()) {
r.requested_at = QDateTime::currentSecsSinceEpoch();
olm::send_key_request_for(copy, r.request_id);
}
} else {
PendingKeyRequests request;
request.request_id = "key_request." + http::client()->generate_txn_id();
request.requested_at = QDateTime::currentSecsSinceEpoch();
request.events.push_back(copy);
olm::send_key_request_for(copy, request.request_id);
pending_key_requests[ev.content.session_id] = request;
}
}
void
@ -877,15 +796,56 @@ EventStore::get(std::string id, std::string_view related_to, bool decrypt, bool
events_by_id_.insert(index, event_ptr);
}
if (decrypt)
if (decrypt) {
if (auto encrypted =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
event_ptr))
return decryptEvent(index, *encrypted);
event_ptr)) {
auto decrypted = decryptEvent(index, *encrypted);
if (decrypted->event)
return &*decrypted->event;
}
}
return event_ptr;
}
olm::DecryptionErrorCode
EventStore::decryptionError(std::string id)
{
if (this->thread() != QThread::currentThread())
nhlog::db()->warn("{} called from a different thread!", __func__);
if (id.empty())
return olm::DecryptionErrorCode::NoError;
IdIndex index{room_id_, std::move(id)};
auto edits_ = edits(index.id);
if (!edits_.empty()) {
index.id = mtx::accessors::event_id(edits_.back());
auto event_ptr =
new mtx::events::collections::TimelineEvents(std::move(edits_.back()));
events_by_id_.insert(index, event_ptr);
}
auto event_ptr = events_by_id_.object(index);
if (!event_ptr) {
auto event = cache::client()->getEvent(room_id_, index.id);
if (!event) {
return olm::DecryptionErrorCode::NoError;
}
event_ptr = new mtx::events::collections::TimelineEvents(std::move(event->data));
events_by_id_.insert(index, event_ptr);
}
if (auto encrypted =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(event_ptr)) {
auto decrypted = decryptEvent(index, *encrypted);
return decrypted->error;
}
return olm::DecryptionErrorCode::NoError;
}
void
EventStore::fetchMore()
{

View file

@ -15,6 +15,7 @@
#include <mtx/responses/messages.hpp>
#include <mtx/responses/sync.hpp>
#include "Olm.h"
#include "Reaction.h"
class EventStore : public QObject
@ -78,6 +79,9 @@ public:
mtx::events::collections::TimelineEvents *get(int idx, bool decrypt = true);
QVariantList reactions(const std::string &event_id);
olm::DecryptionErrorCode decryptionError(std::string id);
void requestSession(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &ev,
bool manual);
int size() const
{
@ -119,7 +123,7 @@ public slots:
private:
std::vector<mtx::events::collections::TimelineEvents> edits(const std::string &event_id);
mtx::events::collections::TimelineEvents *decryptEvent(
olm::DecryptionResult *decryptEvent(
const IdIndex &idx,
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
void handle_room_verification(mtx::events::collections::TimelineEvents event);
@ -129,7 +133,7 @@ private:
uint64_t first = std::numeric_limits<uint64_t>::max(),
last = std::numeric_limits<uint64_t>::max();
static QCache<IdIndex, mtx::events::collections::TimelineEvents> decryptedEvents_;
static QCache<IdIndex, olm::DecryptionResult> decryptedEvents_;
static QCache<Index, mtx::events::collections::TimelineEvents> events_;
static QCache<IdIndex, mtx::events::collections::TimelineEvents> events_by_id_;
@ -137,6 +141,7 @@ private:
{
std::string request_id;
std::vector<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>> events;
qint64 requested_at;
};
std::map<std::string, PendingKeyRequests> pending_key_requests;

View file

@ -533,6 +533,8 @@ RoomlistModel::initializeRooms()
for (const auto &id : cache::client()->roomIds())
addRoom(id, true);
nhlog::db()->info("Restored {} rooms from cache", rowCount());
endResetModel();
}

View file

@ -28,9 +28,9 @@
#include "MemberList.h"
#include "MxcImageProvider.h"
#include "Olm.h"
#include "ReadReceiptsModel.h"
#include "TimelineViewManager.h"
#include "Utils.h"
#include "dialogs/RawMessage.h"
Q_DECLARE_METATYPE(QModelIndex)
@ -308,6 +308,15 @@ qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t)
case qml_mtx_events::KeyVerificationDone:
case qml_mtx_events::KeyVerificationReady:
return mtx::events::EventType::RoomMessage;
//! m.image_pack, currently im.ponies.room_emotes
case qml_mtx_events::ImagePackInRoom:
return mtx::events::EventType::ImagePackRooms;
//! m.image_pack, currently im.ponies.user_emotes
case qml_mtx_events::ImagePackInAccountData:
return mtx::events::EventType::ImagePackInAccountData;
//! m.image_pack.rooms, currently im.ponies.emote_rooms
case qml_mtx_events::ImagePackRooms:
return mtx::events::EventType::ImagePackRooms;
default:
return mtx::events::EventType::Unsupported;
};
@ -443,6 +452,7 @@ TimelineModel::roleNames() const
{IsEditable, "isEditable"},
{IsEncrypted, "isEncrypted"},
{Trustlevel, "trustlevel"},
{EncryptionError, "encryptionError"},
{ReplyTo, "replyTo"},
{Reactions, "reactions"},
{RoomId, "roomId"},
@ -630,6 +640,9 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
return crypto::Trust::Unverified;
}
case EncryptionError:
return events.decryptionError(event_id(event));
case ReplyTo:
return QVariant(QString::fromStdString(relations(event).reply_to().value_or("")));
case Reactions: {
@ -681,6 +694,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
m.insert(names[RoomName], data(event, static_cast<int>(RoomName)));
m.insert(names[RoomTopic], data(event, static_cast<int>(RoomTopic)));
m.insert(names[CallType], data(event, static_cast<int>(CallType)));
m.insert(names[EncryptionError], data(event, static_cast<int>(EncryptionError)));
return QVariant(m);
}
@ -1025,14 +1039,13 @@ TimelineModel::formatDateSeparator(QDate date) const
}
void
TimelineModel::viewRawMessage(QString id) const
TimelineModel::viewRawMessage(QString id)
{
auto e = events.get(id.toStdString(), "", false);
if (!e)
return;
std::string ev = mtx::accessors::serialize_event(*e).dump(4);
auto dialog = new dialogs::RawMessage(QString::fromStdString(ev));
Q_UNUSED(dialog);
emit showRawMessageDialog(QString::fromStdString(ev));
}
void
@ -1046,15 +1059,14 @@ TimelineModel::forwardMessage(QString eventId, QString roomId)
}
void
TimelineModel::viewDecryptedRawMessage(QString id) const
TimelineModel::viewDecryptedRawMessage(QString id)
{
auto e = events.get(id.toStdString(), "");
if (!e)
return;
std::string ev = mtx::accessors::serialize_event(*e).dump(4);
auto dialog = new dialogs::RawMessage(QString::fromStdString(ev));
Q_UNUSED(dialog);
emit showRawMessageDialog(QString::fromStdString(ev));
}
void
@ -1089,9 +1101,9 @@ TimelineModel::relatedInfo(QString id)
}
void
TimelineModel::readReceiptsAction(QString id) const
TimelineModel::showReadReceipts(QString id)
{
MainWindow::instance()->openReadReceiptsDialog(id);
emit openReadReceiptsDialog(new ReadReceiptsProxy{id, roomId(), this});
}
void
@ -1544,6 +1556,17 @@ TimelineModel::scrollTimerEvent()
}
}
void
TimelineModel::requestKeyForEvent(QString id)
{
auto encrypted_event = events.get(id.toStdString(), "", false);
if (encrypted_event) {
if (auto ev = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
encrypted_event))
events.requestSession(*ev, true);
}
}
void
TimelineModel::copyLinkToEvent(QString eventId) const
{

View file

@ -20,6 +20,7 @@
#include "InviteesModel.h"
#include "MemberList.h"
#include "Permissions.h"
#include "ReadReceiptsModel.h"
#include "ui/RoomSettings.h"
#include "ui/UserProfile.h"
@ -106,7 +107,13 @@ enum EventType
KeyVerificationCancel,
KeyVerificationKey,
KeyVerificationDone,
KeyVerificationReady
KeyVerificationReady,
//! m.image_pack, currently im.ponies.room_emotes
ImagePackInRoom,
//! m.image_pack, currently im.ponies.user_emotes
ImagePackInAccountData,
//! m.image_pack.rooms, currently im.ponies.emote_rooms
ImagePackRooms,
};
Q_ENUM_NS(EventType)
mtx::events::EventType fromRoomEventType(qml_mtx_events::EventType);
@ -205,6 +212,7 @@ public:
IsEditable,
IsEncrypted,
Trustlevel,
EncryptionError,
ReplyTo,
Reactions,
RoomId,
@ -235,13 +243,13 @@ public:
Q_INVOKABLE QString formatGuestAccessEvent(QString id);
Q_INVOKABLE QString formatPowerLevelEvent(QString id);
Q_INVOKABLE void viewRawMessage(QString id) const;
Q_INVOKABLE void viewRawMessage(QString id);
Q_INVOKABLE void forwardMessage(QString eventId, QString roomId);
Q_INVOKABLE void viewDecryptedRawMessage(QString id) const;
Q_INVOKABLE void viewDecryptedRawMessage(QString id);
Q_INVOKABLE void openUserProfile(QString userid);
Q_INVOKABLE void editAction(QString id);
Q_INVOKABLE void replyAction(QString id);
Q_INVOKABLE void readReceiptsAction(QString id) const;
Q_INVOKABLE void showReadReceipts(QString id);
Q_INVOKABLE void redactEvent(QString id);
Q_INVOKABLE int idToIndex(QString id) const;
Q_INVOKABLE QString indexToId(int index) const;
@ -257,6 +265,8 @@ public:
endResetModel();
}
Q_INVOKABLE void requestKeyForEvent(QString id);
std::vector<::Reaction> reactions(const std::string &event_id)
{
auto list = events.reactions(event_id);
@ -348,6 +358,8 @@ signals:
void typingUsersChanged(std::vector<QString> users);
void replyChanged(QString reply);
void editChanged(QString reply);
void openReadReceiptsDialog(ReadReceiptsProxy *rr);
void showRawMessageDialog(QString rawMessage);
void paginationInProgressChanged(const bool);
void newCallEvent(const mtx::events::collections::TimelineEvents &event);
void scrollToIndex(int index);

View file

@ -161,6 +161,8 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
0,
"MtxEvent",
"Can't instantiate enum!");
qmlRegisterUncreatableMetaObject(
olm::staticMetaObject, "im.nheko", 1, 0, "Olm", "Can't instantiate enum!");
qmlRegisterUncreatableMetaObject(
crypto::staticMetaObject, "im.nheko", 1, 0, "Crypto", "Can't instantiate enum!");
qmlRegisterUncreatableMetaObject(verification::staticMetaObject,
@ -210,6 +212,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
0,
"InviteesModel",
"InviteesModel needs to be instantiated on the C++ side");
qmlRegisterUncreatableType<ReadReceiptsProxy>(
"im.nheko",
1,
0,
"ReadReceiptsProxy",
"ReadReceiptsProxy needs to be instantiated on the C++ side");
static auto self = this;
qmlRegisterSingletonType<MainWindow>(

View file

@ -1,168 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QPainter>
#include <QPainterPath>
#include <QSettings>
#include "AvatarProvider.h"
#include "Utils.h"
#include "ui/Avatar.h"
Avatar::Avatar(QWidget *parent, int size)
: QWidget(parent)
, size_(size)
{
type_ = ui::AvatarType::Letter;
letter_ = "A";
QFont _font(font());
_font.setPointSizeF(ui::FontSize);
setFont(_font);
QSizePolicy policy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
setSizePolicy(policy);
}
QColor
Avatar::textColor() const
{
if (!text_color_.isValid())
return QColor("black");
return text_color_;
}
QColor
Avatar::backgroundColor() const
{
if (!text_color_.isValid())
return QColor("white");
return background_color_;
}
QSize
Avatar::sizeHint() const
{
return QSize(size_ + 2, size_ + 2);
}
void
Avatar::setTextColor(const QColor &color)
{
text_color_ = color;
}
void
Avatar::setBackgroundColor(const QColor &color)
{
background_color_ = color;
}
void
Avatar::setLetter(const QString &letter)
{
letter_ = letter;
type_ = ui::AvatarType::Letter;
update();
}
void
Avatar::setImage(const QString &avatar_url)
{
avatar_url_ = avatar_url;
AvatarProvider::resolve(avatar_url,
static_cast<int>(size_ * pixmap_.devicePixelRatio()),
this,
[this, requestedRatio = pixmap_.devicePixelRatio()](QPixmap pm) {
if (pm.isNull())
return;
type_ = ui::AvatarType::Image;
pixmap_ = pm;
pixmap_.setDevicePixelRatio(requestedRatio);
update();
});
}
void
Avatar::setImage(const QString &room, const QString &user)
{
room_ = room;
user_ = user;
AvatarProvider::resolve(room,
user,
static_cast<int>(size_ * pixmap_.devicePixelRatio()),
this,
[this, requestedRatio = pixmap_.devicePixelRatio()](QPixmap pm) {
if (pm.isNull())
return;
type_ = ui::AvatarType::Image;
pixmap_ = pm;
pixmap_.setDevicePixelRatio(requestedRatio);
update();
});
}
void
Avatar::setDevicePixelRatio(double ratio)
{
if (type_ == ui::AvatarType::Image && abs(pixmap_.devicePixelRatio() - ratio) > 0.01) {
pixmap_ = pixmap_.scaled(QSize(size_, size_) * ratio);
pixmap_.setDevicePixelRatio(ratio);
if (!avatar_url_.isEmpty())
setImage(avatar_url_);
else
setImage(room_, user_);
}
}
void
Avatar::paintEvent(QPaintEvent *)
{
bool rounded = QSettings().value(QStringLiteral("user/avatar_circles"), true).toBool();
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform |
QPainter::TextAntialiasing);
QRectF r = rect();
const int hs = size_ / 2;
if (type_ != ui::AvatarType::Image) {
QBrush brush;
brush.setStyle(Qt::SolidPattern);
brush.setColor(backgroundColor());
painter.setPen(Qt::NoPen);
painter.setBrush(brush);
rounded ? painter.drawEllipse(r) : painter.drawRoundedRect(r, 3, 3);
} else if (painter.isActive()) {
setDevicePixelRatio(painter.device()->devicePixelRatioF());
}
switch (type_) {
case ui::AvatarType::Image: {
QPainterPath ppath;
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_);
break;
}
case ui::AvatarType::Letter: {
painter.setPen(textColor());
painter.setBrush(Qt::NoBrush);
painter.drawText(r.translated(0, -1), Qt::AlignCenter, letter_);
break;
}
default:
break;
}
}

View file

@ -1,48 +0,0 @@
// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QImage>
#include <QPixmap>
#include <QWidget>
#include "Theme.h"
class Avatar : public QWidget
{
Q_OBJECT
Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor)
Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor)
public:
explicit Avatar(QWidget *parent = nullptr, int size = ui::AvatarSize);
void setBackgroundColor(const QColor &color);
void setImage(const QString &avatar_url);
void setImage(const QString &room, const QString &user);
void setLetter(const QString &letter);
void setTextColor(const QColor &color);
void setDevicePixelRatio(double ratio);
QColor backgroundColor() const;
QColor textColor() const;
QSize sizeHint() const override;
protected:
void paintEvent(QPaintEvent *event) override;
private:
void init();
ui::AvatarType type_;
QString letter_;
QString avatar_url_, room_, user_;
QColor background_color_;
QColor text_color_;
QPixmap pixmap_;
int size_;
};

View file

@ -6,6 +6,7 @@
#include <QDesktopServices>
#include <QUrl>
#include <QWindow>
#include "Cache_p.h"
#include "ChatPage.h"
@ -140,3 +141,9 @@ Nheko::openJoinRoomDialog() const
MainWindow::instance()->openJoinRoomDialog(
[](const QString &room_id) { ChatPage::instance()->joinRoom(room_id); });
}
void
Nheko::reparent(QWindow *win) const
{
win->setTransientParent(MainWindow::instance()->windowHandle());
}

View file

@ -4,12 +4,15 @@
#pragma once
#include <QFontDatabase>
#include <QObject>
#include <QPalette>
#include "Theme.h"
#include "UserProfile.h"
class QWindow;
class Nheko : public QObject
{
Q_OBJECT
@ -38,12 +41,17 @@ public:
int paddingLarge() const { return 20; }
UserProfile *currentUser() const;
Q_INVOKABLE QFont monospaceFont() const
{
return QFontDatabase::systemFont(QFontDatabase::FixedFont);
}
Q_INVOKABLE void openLink(QString link) const;
Q_INVOKABLE void setStatusMessage(QString msg) const;
Q_INVOKABLE void showUserSettingsPage() const;
Q_INVOKABLE void openLogoutDialog() const;
Q_INVOKABLE void openCreateRoomDialog() const;
Q_INVOKABLE void openJoinRoomDialog() const;
Q_INVOKABLE void reparent(QWindow *win) const;
public slots:
void updateUserProfile();