diff --git a/src/Cache.cpp b/src/Cache.cpp
index 8cf66d21..c81840c6 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -102,6 +102,20 @@ namespace {
std::unique_ptr<Cache> instance_ = nullptr;
}
+template<class T>
+static T
+to(lmdb::val &value)
+{
+ static_assert(std::is_trivial_v<T>, "Can only convert to trivial types!");
+ T temp;
+
+ if (value.size() < sizeof(T))
+ throw lmdb::runtime_error(__func__, MDB_BAD_VALSIZE);
+
+ std::memcpy(&temp, value.data(), sizeof(T));
+ return temp;
+}
+
bool
Cache::isHiddenEvent(lmdb::txn &txn,
mtx::events::collections::TimelineEvents e,
@@ -1667,14 +1681,14 @@ Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id, uint64_t
auto cursor = lmdb::cursor::open(txn, orderDb);
if (index == std::numeric_limits<uint64_t>::max()) {
if (cursor.get(indexVal, event_id, forward ? MDB_FIRST : MDB_LAST)) {
- index = *indexVal.data<uint64_t>();
+ index = to<uint64_t>(indexVal);
} else {
messages.end_of_cache = true;
return messages;
}
} else {
if (cursor.get(indexVal, event_id, MDB_SET)) {
- index = *indexVal.data<uint64_t>();
+ index = to<uint64_t>(indexVal);
} else {
messages.end_of_cache = true;
return messages;
@@ -1708,7 +1722,7 @@ Cache::getTimelineMessages(lmdb::txn &txn, const std::string &room_id, uint64_t
cursor.close();
// std::reverse(timeline.events.begin(), timeline.events.end());
- messages.next_index = *indexVal.data<uint64_t>();
+ messages.next_index = to<uint64_t>(indexVal);
messages.end_of_cache = !ret;
return messages;
@@ -1861,12 +1875,12 @@ Cache::getTimelineRange(const std::string &room_id)
}
TimelineRange range{};
- range.last = *indexVal.data<uint64_t>();
+ range.last = to<uint64_t>(indexVal);
if (!cursor.get(indexVal, val, MDB_FIRST)) {
return {};
}
- range.first = *indexVal.data<uint64_t>();
+ range.first = to<uint64_t>(indexVal);
return range;
}
@@ -1892,7 +1906,7 @@ Cache::getTimelineIndex(const std::string &room_id, std::string_view event_id)
return {};
}
- return *val.data<uint64_t>();
+ return to<uint64_t>(val);
}
std::optional<uint64_t>
@@ -1920,7 +1934,7 @@ Cache::getEventIndex(const std::string &room_id, std::string_view event_id)
return {};
}
- return *val.data<uint64_t>();
+ return to<uint64_t>(val);
}
std::optional<std::pair<uint64_t, std::string>>
@@ -1951,7 +1965,7 @@ Cache::lastInvisibleEventAfter(const std::string &room_id, std::string_view even
if (!success) {
return {};
}
- uint64_t prevIdx = *indexVal.data<uint64_t>();
+ uint64_t prevIdx = to<uint64_t>(indexVal);
std::string prevId{eventIdVal.data(), eventIdVal.size()};
auto cursor = lmdb::cursor::open(txn, eventOrderDb);
@@ -1964,7 +1978,7 @@ Cache::lastInvisibleEventAfter(const std::string &room_id, std::string_view even
if (lmdb::dbi_get(txn, timelineDb, lmdb::val(evId.data(), evId.size()), temp)) {
return std::pair{prevIdx, std::string(prevId)};
} else {
- prevIdx = *indexVal.data<uint64_t>();
+ prevIdx = to<uint64_t>(indexVal);
prevId = std::move(evId);
}
}
@@ -1994,7 +2008,7 @@ Cache::getArrivalIndex(const std::string &room_id, std::string_view event_id)
return {};
}
- return *val.data<uint64_t>();
+ return to<uint64_t>(val);
}
std::optional<std::string>
@@ -2775,13 +2789,13 @@ Cache::saveTimelineMessages(lmdb::txn &txn,
uint64_t index = std::numeric_limits<uint64_t>::max() / 2;
auto cursor = lmdb::cursor::open(txn, orderDb);
if (cursor.get(indexVal, val, MDB_LAST)) {
- index = *indexVal.data<int64_t>();
+ index = to<uint64_t>(indexVal);
}
uint64_t msgIndex = std::numeric_limits<uint64_t>::max() / 2;
auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
if (msgCursor.get(indexVal, val, MDB_LAST)) {
- msgIndex = *indexVal.data<uint64_t>();
+ msgIndex = to<uint64_t>(indexVal);
}
bool first = true;
@@ -2942,7 +2956,7 @@ Cache::saveOldMessages(const std::string &room_id, const mtx::responses::Message
{
auto cursor = lmdb::cursor::open(txn, orderDb);
if (cursor.get(indexVal, val, MDB_FIRST)) {
- index = *indexVal.data<uint64_t>();
+ index = to<uint64_t>(indexVal);
}
}
@@ -2950,7 +2964,7 @@ Cache::saveOldMessages(const std::string &room_id, const mtx::responses::Message
{
auto msgCursor = lmdb::cursor::open(txn, order2msgDb);
if (msgCursor.get(indexVal, val, MDB_FIRST)) {
- msgIndex = *indexVal.data<uint64_t>();
+ msgIndex = to<uint64_t>(indexVal);
}
}
@@ -3258,12 +3272,12 @@ Cache::deleteOldMessages()
uint64_t first, last;
if (cursor.get(indexVal, val, MDB_LAST)) {
- last = *indexVal.data<uint64_t>();
+ last = to<uint64_t>(indexVal);
} else {
continue;
}
if (cursor.get(indexVal, val, MDB_FIRST)) {
- first = *indexVal.data<uint64_t>();
+ first = to<uint64_t>(indexVal);
} else {
continue;
}
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 45802789..aae9271d 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -253,6 +253,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
this, &ChatPage::updateGroupsInfo, communitiesList_, &CommunitiesList::setCommunities);
connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom);
+ connect(this, &ChatPage::newRoom, this, &ChatPage::changeRoom, Qt::QueuedConnection);
connect(this, &ChatPage::notificationsRetrieved, this, &ChatPage::sendNotifications);
connect(this,
&ChatPage::highlightedNotifsRetrieved,
@@ -920,6 +921,13 @@ ChatPage::joinRoom(const QString &room)
void
ChatPage::joinRoomVia(const std::string &room_id, const std::vector<std::string> &via)
{
+ if (QMessageBox::Yes !=
+ QMessageBox::question(
+ this,
+ tr("Confirm join"),
+ tr("Do you really want to join %1?").arg(QString::fromStdString(room_id))))
+ return;
+
http::client()->join_room(
room_id, via, [this, room_id](const mtx::responses::RoomId &, mtx::http::RequestErr err) {
if (err) {
@@ -960,8 +968,9 @@ ChatPage::createRoom(const mtx::requests::CreateRoom &req)
return;
}
- emit showNotification(
- tr("Room %1 created.").arg(QString::fromStdString(res.room_id.to_string())));
+ QString newRoomId = QString::fromStdString(res.room_id.to_string());
+ emit showNotification(tr("Room %1 created.").arg(newRoomId));
+ emit newRoom(newRoomId);
});
}
@@ -983,6 +992,13 @@ ChatPage::leaveRoom(const QString &room_id)
}
void
+ChatPage::changeRoom(const QString &room_id)
+{
+ view_manager_->setHistoryView(room_id);
+ room_list_->highlightSelectedRoom(room_id);
+}
+
+void
ChatPage::inviteUser(QString userid, QString reason)
{
auto room = current_room_;
@@ -1308,6 +1324,13 @@ ChatPage::startChat(QString userid)
}
}
+ if (QMessageBox::Yes !=
+ QMessageBox::question(
+ this,
+ tr("Confirm invite"),
+ tr("Do you really want to start a private chat with %1?").arg(userid)))
+ return;
+
mtx::requests::CreateRoom req;
req.preset = mtx::requests::Preset::PrivateChat;
req.visibility = mtx::common::RoomVisibility::Private;
@@ -1326,14 +1349,14 @@ mxidFromSegments(QStringRef sigil, QStringRef mxid)
auto mxid_ = QUrl::fromPercentEncoding(mxid.toUtf8());
- if (sigil == "user") {
+ if (sigil == "u") {
return "@" + mxid_;
} else if (sigil == "roomid") {
return "!" + mxid_;
- } else if (sigil == "room") {
+ } else if (sigil == "r") {
return "#" + mxid_;
- } else if (sigil == "group") {
- return "+" + mxid_;
+ //} else if (sigil == "group") {
+ // return "+" + mxid_;
} else {
return "";
}
@@ -1383,7 +1406,7 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
}
}
- if (sigil1 == "user") {
+ if (sigil1 == "u") {
if (action.isEmpty()) {
view_manager_->activeTimeline()->openUserProfile(mxid1);
} else if (action == "chat") {
@@ -1400,10 +1423,10 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
}
}
- if (action == "join") {
+ if (action == "join" || action.isEmpty()) {
joinRoomVia(targetRoomId, vias);
}
- } else if (sigil1 == "room") {
+ } else if (sigil1 == "r") {
auto joined_rooms = cache::joinedRooms();
auto targetRoomAlias = mxid1.toStdString();
@@ -1418,7 +1441,7 @@ ChatPage::handleMatrixUri(const QByteArray &uri)
}
}
- if (action == "join") {
+ if (action == "join" || action.isEmpty()) {
joinRoomVia(mxid1.toStdString(), vias);
}
}
diff --git a/src/ChatPage.h b/src/ChatPage.h
index 917bd785..dc6b8299 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -154,6 +154,7 @@ signals:
void tryInitialSyncCb();
void newSyncResponse(const mtx::responses::Sync &res);
void leftRoom(const QString &room_id);
+ void newRoom(const QString &room_id);
void initializeRoomList(QMap<QString, RoomInfo>);
void initializeViews(const mtx::responses::Rooms &rooms);
@@ -201,6 +202,7 @@ signals:
private slots:
void logout();
void removeRoom(const QString &room_id);
+ void changeRoom(const QString &room_id);
void dropToLoginPage(const QString &msg);
void handleSyncResponse(const mtx::responses::Sync &res);
diff --git a/src/LoginPage.cpp b/src/LoginPage.cpp
index 15aeb12a..cd54431d 100644
--- a/src/LoginPage.cpp
+++ b/src/LoginPage.cpp
@@ -147,16 +147,23 @@ LoginPage::LoginPage(QWidget *parent)
error_matrixid_label_->hide();
button_layout_ = new QHBoxLayout();
- button_layout_->setSpacing(0);
+ button_layout_->setSpacing(20);
button_layout_->setContentsMargins(0, 0, 0, 30);
login_button_ = new RaisedButton(tr("LOGIN"), this);
- login_button_->setMinimumSize(350, 65);
+ login_button_->setMinimumSize(150, 65);
login_button_->setFontSize(20);
login_button_->setCornerRadius(3);
+ sso_login_button_ = new RaisedButton(tr("SSO LOGIN"), this);
+ sso_login_button_->setMinimumSize(150, 65);
+ sso_login_button_->setFontSize(20);
+ sso_login_button_->setCornerRadius(3);
+ sso_login_button_->setVisible(false);
+
button_layout_->addStretch(1);
button_layout_->addWidget(login_button_);
+ button_layout_->addWidget(sso_login_button_);
button_layout_->addStretch(1);
error_label_ = new QLabel(this);
@@ -179,7 +186,17 @@ LoginPage::LoginPage(QWidget *parent)
this, &LoginPage::versionErrorCb, this, &LoginPage::versionError, Qt::QueuedConnection);
connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked()));
- connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked()));
+ connect(login_button_, &RaisedButton::clicked, this, [this]() {
+ onLoginButtonClicked(passwordSupported ? LoginMethod::Password : LoginMethod::SSO);
+ });
+ connect(sso_login_button_, &RaisedButton::clicked, this, [this]() {
+ onLoginButtonClicked(LoginMethod::SSO);
+ });
+ connect(this,
+ &LoginPage::showErrorMessage,
+ this,
+ static_cast<void (LoginPage::*)(QLabel *, const QString &)>(&LoginPage::showError),
+ Qt::QueuedConnection);
connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
connect(deviceName_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
@@ -314,16 +331,19 @@ LoginPage::checkHomeserverVersion()
http::client()->get_login(
[this](mtx::responses::LoginFlows flows, mtx::http::RequestErr err) {
if (err || flows.flows.empty())
- emit versionOkCb(LoginMethod::Password);
+ emit versionOkCb(true, false);
- LoginMethod loginMethod_ = LoginMethod::Password;
+ bool ssoSupported_ = false;
+ bool passwordSupported_ = false;
for (const auto &flow : flows.flows) {
if (flow.type == mtx::user_interactive::auth_types::sso) {
- loginMethod_ = LoginMethod::SSO;
- break;
+ ssoSupported_ = true;
+ } else if (flow.type ==
+ mtx::user_interactive::auth_types::password) {
+ passwordSupported_ = true;
}
}
- emit versionOkCb(loginMethod_);
+ emit versionOkCb(passwordSupported_, ssoSupported_);
});
});
}
@@ -355,28 +375,24 @@ LoginPage::versionError(const QString &error)
}
void
-LoginPage::versionOk(LoginMethod loginMethod_)
+LoginPage::versionOk(bool passwordSupported_, bool ssoSupported_)
{
- this->loginMethod = loginMethod_;
+ passwordSupported = passwordSupported_;
+ ssoSupported = ssoSupported_;
serverLayout_->removeWidget(spinner_);
matrixidLayout_->removeWidget(spinner_);
spinner_->stop();
- if (loginMethod == LoginMethod::SSO) {
- password_input_->hide();
- login_button_->setText(tr("SSO LOGIN"));
- } else {
- password_input_->show();
- login_button_->setText(tr("LOGIN"));
- }
+ sso_login_button_->setVisible(ssoSupported);
+ login_button_->setVisible(passwordSupported);
if (serverInput_->isVisible())
serverInput_->hide();
}
void
-LoginPage::onLoginButtonClicked()
+LoginPage::onLoginButtonClicked(LoginMethod loginMethod)
{
error_label_->setText("");
@@ -411,8 +427,8 @@ LoginPage::onLoginButtonClicked()
: deviceName_->text().toStdString(),
[this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
if (err) {
- showError(error_label_,
- QString::fromStdString(err->matrix_error.error));
+ showErrorMessage(error_label_,
+ QString::fromStdString(err->matrix_error.error));
emit errorOccurred();
return;
}
@@ -437,7 +453,7 @@ LoginPage::onLoginButtonClicked()
http::client()->login(
req, [this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
if (err) {
- showError(
+ showErrorMessage(
error_label_,
QString::fromStdString(err->matrix_error.error));
emit errorOccurred();
@@ -456,7 +472,7 @@ LoginPage::onLoginButtonClicked()
sso->deleteLater();
});
connect(sso, &SSOHandler::ssoFailed, this, [this, sso]() {
- showError(error_label_, tr("SSO login failed"));
+ showErrorMessage(error_label_, tr("SSO login failed"));
emit errorOccurred();
sso->deleteLater();
});
diff --git a/src/LoginPage.h b/src/LoginPage.h
index 5ed21dec..f6428cbb 100644
--- a/src/LoginPage.h
+++ b/src/LoginPage.h
@@ -56,9 +56,10 @@ signals:
//! Used to trigger the corresponding slot outside of the main thread.
void versionErrorCb(const QString &err);
- void versionOkCb(LoginPage::LoginMethod method);
+ void versionOkCb(bool passwordSupported, bool ssoSupported);
void loginOk(const mtx::responses::Login &res);
+ void showErrorMessage(QLabel *label, const QString &msg);
protected:
void paintEvent(QPaintEvent *event) override;
@@ -73,7 +74,7 @@ private slots:
void onBackButtonClicked();
// Callback for the login button.
- void onLoginButtonClicked();
+ void onLoginButtonClicked(LoginMethod loginMethod);
// Callback for probing the server found in the mxid
void onMatrixIdEntered();
@@ -84,7 +85,7 @@ private slots:
// Callback for errors produced during server probing
void versionError(const QString &error_message);
// Callback for successful server probing
- void versionOk(LoginPage::LoginMethod method);
+ void versionOk(bool passwordSupported, bool ssoSupported);
private:
void checkHomeserverVersion();
@@ -120,7 +121,7 @@ private:
QString inferredServerAddress_;
FlatButton *back_button_;
- RaisedButton *login_button_;
+ RaisedButton *login_button_, *sso_login_button_;
QWidget *form_widget_;
QHBoxLayout *form_wrapper_;
@@ -130,5 +131,6 @@ private:
TextField *password_input_;
TextField *deviceName_;
TextField *serverInput_;
- LoginMethod loginMethod = LoginMethod::Password;
+ bool passwordSupported = true;
+ bool ssoSupported = false;
};
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index ab3c2cf2..ae532ef3 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -51,7 +51,6 @@
#include "dialogs/Logout.h"
#include "dialogs/MemberList.h"
#include "dialogs/ReadReceipts.h"
-#include "dialogs/RoomSettings.h"
MainWindow *MainWindow::instance_ = nullptr;
@@ -364,14 +363,6 @@ MainWindow::hasActiveUser()
}
void
-MainWindow::openRoomSettings(const QString &room_id)
-{
- auto dialog = new dialogs::RoomSettings(room_id, this);
-
- showDialog(dialog);
-}
-
-void
MainWindow::openMemberListDialog(const QString &room_id)
{
auto dialog = new dialogs::MemberList(room_id, this);
diff --git a/src/MainWindow.h b/src/MainWindow.h
index bb219813..4a8ea642 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -54,7 +54,6 @@ class LeaveRoom;
class Logout;
class MemberList;
class ReCaptcha;
-class RoomSettings;
}
class MainWindow : public QMainWindow
@@ -78,7 +77,6 @@ public:
std::function<void(const mtx::requests::CreateRoom &request)> callback);
void openJoinRoomDialog(std::function<void(const QString &room_id)> callback);
void openLogoutDialog();
- void openRoomSettings(const QString &room_id);
void openMemberListDialog(const QString &room_id);
void openReadReceiptsDialog(const QString &event_id);
diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp
index 44ad7a3d..004d5b98 100644
--- a/src/RegisterPage.cpp
+++ b/src/RegisterPage.cpp
@@ -277,6 +277,7 @@ RegisterPage::RegisterPage(QWidget *parent)
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);
emit registerOk();
return;
diff --git a/src/RoomInfoListItem.h b/src/RoomInfoListItem.h
index baa8b98b..c2826f6f 100644
--- a/src/RoomInfoListItem.h
+++ b/src/RoomInfoListItem.h
@@ -217,4 +217,6 @@ private:
QColor bubbleBgColor_;
QColor bubbleFgColor_;
+
+ friend struct room_sort;
};
diff --git a/src/RoomList.cpp b/src/RoomList.cpp
index 67a7ac40..10042c94 100644
--- a/src/RoomList.cpp
+++ b/src/RoomList.cpp
@@ -353,8 +353,8 @@ RoomList::updateRoomDescription(const QString &roomid, const DescInfo &info)
struct room_sort
{
- bool operator()(const QSharedPointer<RoomInfoListItem> a,
- const QSharedPointer<RoomInfoListItem> b) const
+ bool operator()(const QSharedPointer<RoomInfoListItem> &a,
+ const QSharedPointer<RoomInfoListItem> &b) const
{
// Sort by "importance" (i.e. invites before mentions before
// notifs before new events before old events), then secondly
@@ -370,9 +370,9 @@ struct room_sort
// Now sort by recency
// Zero if empty, otherwise the time that the event occured
const uint64_t a_recency =
- a->lastMessageInfo().userid.isEmpty() ? 0 : a->lastMessageInfo().timestamp;
+ a->lastMsgInfo_.userid.isEmpty() ? 0 : a->lastMsgInfo_.timestamp;
const uint64_t b_recency =
- b->lastMessageInfo().userid.isEmpty() ? 0 : b->lastMessageInfo().timestamp;
+ b->lastMsgInfo_.userid.isEmpty() ? 0 : b->lastMsgInfo_.timestamp;
return a_recency > b_recency;
}
};
diff --git a/src/RoomsModel.cpp b/src/RoomsModel.cpp
new file mode 100644
index 00000000..4286f87b
--- /dev/null
+++ b/src/RoomsModel.cpp
@@ -0,0 +1,69 @@
+#include "RoomsModel.h"
+
+#include <QUrl>
+
+#include "Cache_p.h"
+#include "CompletionModelRoles.h"
+
+RoomsModel::RoomsModel(bool showOnlyRoomWithAliases, QObject *parent)
+ : QAbstractListModel(parent)
+ , showOnlyRoomWithAliases_(showOnlyRoomWithAliases)
+{
+ std::vector<std::string> rooms_ = cache::joinedRooms();
+ roomInfos = cache::getRoomInfo(rooms_);
+
+ for (const auto &r : rooms_) {
+ auto roomAliasesList = cache::client()->getRoomAliases(r);
+
+ if (showOnlyRoomWithAliases_) {
+ if (roomAliasesList && !roomAliasesList->alias.empty()) {
+ roomids.push_back(QString::fromStdString(r));
+ roomAliases.push_back(
+ QString::fromStdString(roomAliasesList->alias));
+ }
+ } else {
+ roomids.push_back(QString::fromStdString(r));
+ roomAliases.push_back(
+ roomAliasesList ? QString::fromStdString(roomAliasesList->alias) : "");
+ }
+ }
+}
+
+QHash<int, QByteArray>
+RoomsModel::roleNames() const
+{
+ return {{CompletionModel::CompletionRole, "completionRole"},
+ {CompletionModel::SearchRole, "searchRole"},
+ {CompletionModel::SearchRole2, "searchRole2"},
+ {Roles::RoomAlias, "roomAlias"},
+ {Roles::AvatarUrl, "avatarUrl"},
+ {Roles::RoomID, "roomid"},
+ {Roles::RoomName, "roomName"}};
+}
+
+QVariant
+RoomsModel::data(const QModelIndex &index, int role) const
+{
+ if (hasIndex(index.row(), index.column(), index.parent())) {
+ switch (role) {
+ case CompletionModel::CompletionRole: {
+ QString percentEncoding = QUrl::toPercentEncoding(roomAliases[index.row()]);
+ return QString("[%1](https://matrix.to/#/%2)")
+ .arg(roomAliases[index.row()], percentEncoding);
+ }
+ case CompletionModel::SearchRole:
+ case Qt::DisplayRole:
+ case Roles::RoomAlias:
+ return roomAliases[index.row()];
+ case CompletionModel::SearchRole2:
+ case Roles::RoomName:
+ return QString::fromStdString(roomInfos.at(roomids[index.row()]).name);
+ case Roles::AvatarUrl:
+ return QString::fromStdString(
+ roomInfos.at(roomids[index.row()]).avatar_url);
+ case Roles::RoomID:
+ return roomids[index.row()];
+ }
+ }
+ return {};
+}
diff --git a/src/RoomsModel.h b/src/RoomsModel.h
new file mode 100644
index 00000000..0e006448
--- /dev/null
+++ b/src/RoomsModel.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "Cache.h"
+
+#include <QAbstractListModel>
+#include <QString>
+
+class RoomsModel : public QAbstractListModel
+{
+public:
+ enum Roles
+ {
+ AvatarUrl = Qt::UserRole,
+ RoomAlias,
+ RoomID,
+ RoomName,
+ };
+
+ RoomsModel(bool showOnlyRoomWithAliases = false, QObject *parent = nullptr);
+ QHash<int, QByteArray> roleNames() const override;
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override
+ {
+ (void)parent;
+ return (int)roomids.size();
+ }
+ QVariant data(const QModelIndex &index, int role) const override;
+
+private:
+ std::vector<QString> roomids;
+ std::vector<QString> roomAliases;
+ std::map<QString, RoomInfo> roomInfos;
+ bool showOnlyRoomWithAliases_;
+};
diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp
deleted file mode 100644
index bd3cc26f..00000000
--- a/src/dialogs/RoomSettings.cpp
+++ /dev/null
@@ -1,865 +0,0 @@
-#include "dialogs/RoomSettings.h"
-#include <QApplication>
-#include <QComboBox>
-#include <QEvent>
-#include <QFileDialog>
-#include <QFontDatabase>
-#include <QImageReader>
-#include <QLabel>
-#include <QMessageBox>
-#include <QMimeDatabase>
-#include <QPainter>
-#include <QPixmap>
-#include <QPushButton>
-#include <QShortcut>
-#include <QShowEvent>
-#include <QStandardPaths>
-#include <QStyleOption>
-#include <QVBoxLayout>
-#include <mtx/responses/common.hpp>
-#include <mtx/responses/media.hpp>
-
-#include "Cache.h"
-#include "ChatPage.h"
-#include "Config.h"
-#include "Logging.h"
-#include "MatrixClient.h"
-#include "Utils.h"
-#include "ui/Avatar.h"
-#include "ui/FlatButton.h"
-#include "ui/LoadingIndicator.h"
-#include "ui/Painter.h"
-#include "ui/TextField.h"
-#include "ui/ToggleButton.h"
-
-using namespace dialogs;
-using namespace mtx::events;
-
-constexpr int BUTTON_SIZE = 36;
-constexpr int BUTTON_RADIUS = BUTTON_SIZE / 2;
-constexpr int WIDGET_MARGIN = 20;
-constexpr int TOP_WIDGET_MARGIN = 2 * WIDGET_MARGIN;
-constexpr int WIDGET_SPACING = 15;
-constexpr int TEXT_SPACING = 4;
-constexpr int BUTTON_SPACING = 2 * TEXT_SPACING;
-
-bool
-ClickableFilter::eventFilter(QObject *obj, QEvent *event)
-{
- if (event->type() == QEvent::MouseButtonRelease) {
- emit clicked();
- return true;
- }
-
- return QObject::eventFilter(obj, event);
-}
-
-EditModal::EditModal(const QString &roomId, QWidget *parent)
- : QWidget(parent)
- , roomId_{roomId}
-{
- setAutoFillBackground(true);
- setAttribute(Qt::WA_DeleteOnClose, true);
- setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
- setWindowModality(Qt::WindowModal);
-
- QFont largeFont;
- largeFont.setPointSizeF(largeFont.pointSizeF() * 1.4);
- setMinimumWidth(conf::window::minModalWidth);
- setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
-
- auto layout = new QVBoxLayout(this);
-
- applyBtn_ = new QPushButton(tr("Apply"), this);
- cancelBtn_ = new QPushButton(tr("Cancel"), this);
- cancelBtn_->setDefault(true);
-
- auto btnLayout = new QHBoxLayout;
- btnLayout->addStretch(1);
- btnLayout->setSpacing(15);
- btnLayout->addWidget(cancelBtn_);
- btnLayout->addWidget(applyBtn_);
-
- nameInput_ = new TextField(this);
- nameInput_->setLabel(tr("Name").toUpper());
- topicInput_ = new TextField(this);
- topicInput_->setLabel(tr("Topic").toUpper());
-
- errorField_ = new QLabel(this);
- errorField_->setWordWrap(true);
- errorField_->hide();
-
- layout->addWidget(nameInput_);
- layout->addWidget(topicInput_);
- layout->addLayout(btnLayout, 1);
-
- auto labelLayout = new QHBoxLayout;
- labelLayout->setAlignment(Qt::AlignHCenter);
- labelLayout->addWidget(errorField_);
- layout->addLayout(labelLayout);
-
- connect(applyBtn_, &QPushButton::clicked, this, &EditModal::applyClicked);
- connect(cancelBtn_, &QPushButton::clicked, this, &EditModal::close);
-
- auto window = QApplication::activeWindow();
- auto center = window->frameGeometry().center();
- move(center.x() - (width() * 0.5), center.y() - (height() * 0.5));
-}
-
-void
-EditModal::topicEventSent()
-{
- errorField_->hide();
- close();
-}
-
-void
-EditModal::nameEventSent(const QString &name)
-{
- errorField_->hide();
- emit nameChanged(name);
- close();
-}
-
-void
-EditModal::error(const QString &msg)
-{
- errorField_->setText(msg);
- errorField_->show();
-}
-
-void
-EditModal::applyClicked()
-{
- // Check if the values are changed from the originals.
- auto newName = nameInput_->text().trimmed();
- auto newTopic = topicInput_->text().trimmed();
-
- errorField_->hide();
-
- if (newName == initialName_ && newTopic == initialTopic_) {
- close();
- return;
- }
-
- using namespace mtx::events;
- auto proxy = std::make_shared<ThreadProxy>();
- connect(proxy.get(), &ThreadProxy::topicEventSent, this, &EditModal::topicEventSent);
- connect(proxy.get(), &ThreadProxy::nameEventSent, this, &EditModal::nameEventSent);
- connect(proxy.get(), &ThreadProxy::error, this, &EditModal::error);
-
- if (newName != initialName_ && !newName.isEmpty()) {
- state::Name body;
- body.name = newName.toStdString();
-
- http::client()->send_state_event(
- roomId_.toStdString(),
- body,
- [proxy, newName](const mtx::responses::EventId &, mtx::http::RequestErr err) {
- if (err) {
- emit proxy->error(
- QString::fromStdString(err->matrix_error.error));
- return;
- }
-
- emit proxy->nameEventSent(newName);
- });
- }
-
- if (newTopic != initialTopic_ && !newTopic.isEmpty()) {
- state::Topic body;
- body.topic = newTopic.toStdString();
-
- http::client()->send_state_event(
- roomId_.toStdString(),
- body,
- [proxy](const mtx::responses::EventId &, mtx::http::RequestErr err) {
- if (err) {
- emit proxy->error(
- QString::fromStdString(err->matrix_error.error));
- return;
- }
-
- emit proxy->topicEventSent();
- });
- }
-}
-
-void
-EditModal::setFields(const QString &roomName, const QString &roomTopic)
-{
- initialName_ = roomName;
- initialTopic_ = roomTopic;
-
- nameInput_->setText(roomName);
- topicInput_->setText(roomTopic);
-}
-
-RoomSettings::RoomSettings(const QString &room_id, QWidget *parent)
- : QFrame(parent)
- , room_id_{std::move(room_id)}
-{
- retrieveRoomInfo();
-
- setAutoFillBackground(true);
- setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
- setWindowModality(Qt::WindowModal);
- setAttribute(Qt::WA_DeleteOnClose, true);
-
- QFont largeFont;
- largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5);
-
- setMinimumWidth(conf::window::minModalWidth);
- setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
-
- auto layout = new QVBoxLayout(this);
- layout->setSpacing(WIDGET_SPACING);
- layout->setContentsMargins(WIDGET_MARGIN, TOP_WIDGET_MARGIN, WIDGET_MARGIN, WIDGET_MARGIN);
-
- QFont font;
- font.setWeight(QFont::Medium);
- auto settingsLabel = new QLabel(tr("Settings").toUpper(), this);
- settingsLabel->setFont(font);
-
- auto infoLabel = new QLabel(tr("Info").toUpper(), this);
- infoLabel->setFont(font);
-
- QFont monospaceFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
-
- auto roomIdLabel = new QLabel(room_id, this);
- roomIdLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
- roomIdLabel->setFont(monospaceFont);
-
- auto roomIdLayout = new QHBoxLayout;
- roomIdLayout->setMargin(0);
- roomIdLayout->addWidget(new QLabel(tr("Internal ID"), this),
- 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);
- 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);
- notifOptionLayout_->addWidget(notifLabel, Qt::AlignBottom | Qt::AlignLeft);
- notifOptionLayout_->addWidget(notifCombo, 0, Qt::AlignBottom | Qt::AlignRight);
-
- auto accessLabel = new QLabel(tr("Room access"), this);
- accessCombo = new QComboBox(this);
- accessCombo->addItem(tr("Anyone and guests"));
- accessCombo->addItem(tr("Anyone"));
- accessCombo->addItem(tr("Invited users"));
- accessCombo->setDisabled(
- !canChangeJoinRules(room_id_.toStdString(), utils::localUser().toStdString()));
- connect(accessCombo, QOverload<int>::of(&QComboBox::activated), [this](int index) {
- using namespace mtx::events::state;
-
- auto guest_access = [](int index) -> state::GuestAccess {
- state::GuestAccess event;
-
- if (index == 0)
- event.guest_access = state::AccessState::CanJoin;
- else
- event.guest_access = state::AccessState::Forbidden;
-
- return event;
- }(index);
-
- auto join_rule = [](int index) -> state::JoinRules {
- state::JoinRules event;
-
- switch (index) {
- case 0:
- case 1:
- event.join_rule = state::JoinRule::Public;
- break;
- default:
- event.join_rule = state::JoinRule::Invite;
- }
-
- return event;
- }(index);
-
- updateAccessRules(room_id_.toStdString(), join_rule, guest_access);
- });
-
- if (info_.join_rule == state::JoinRule::Public) {
- if (info_.guest_access) {
- accessCombo->setCurrentIndex(0);
- } else {
- accessCombo->setCurrentIndex(1);
- }
- } else {
- accessCombo->setCurrentIndex(2);
- }
-
- auto accessOptionLayout = new QHBoxLayout();
- accessOptionLayout->setMargin(0);
- accessOptionLayout->addWidget(accessLabel, Qt::AlignBottom | Qt::AlignLeft);
- accessOptionLayout->addWidget(accessCombo, 0, Qt::AlignBottom | Qt::AlignRight);
-
- auto encryptionLabel = new QLabel(tr("Encryption"), this);
- encryptionToggle_ = new Toggle(this);
-
- auto encryptionOptionLayout = new QHBoxLayout;
- encryptionOptionLayout->setMargin(0);
- encryptionOptionLayout->addWidget(encryptionLabel, Qt::AlignBottom | Qt::AlignLeft);
- encryptionOptionLayout->addWidget(encryptionToggle_, 0, Qt::AlignBottom | Qt::AlignRight);
-
- auto keyRequestsLabel = new QLabel(tr("Respond to key requests"), this);
- keyRequestsLabel->setToolTipDuration(6000);
- keyRequestsLabel->setToolTip(
- tr("Whether or not the client should respond automatically with the session keys\n"
- " upon request. Use with caution, this is a temporary measure to test the\n"
- " E2E implementation until device verification is completed."));
- keyRequestsToggle_ = new Toggle(this);
- connect(keyRequestsToggle_, &Toggle::toggled, this, [this](bool isOn) {
- utils::setKeyRequestsPreference(room_id_, isOn);
- });
-
- auto keyRequestsLayout = new QHBoxLayout;
- keyRequestsLayout->setMargin(0);
- keyRequestsLayout->setSpacing(0);
- keyRequestsLayout->addWidget(keyRequestsLabel, Qt::AlignBottom | Qt::AlignLeft);
- keyRequestsLayout->addWidget(keyRequestsToggle_, 0, Qt::AlignBottom | Qt::AlignRight);
-
- connect(encryptionToggle_, &Toggle::toggled, this, [this, keyRequestsLabel](bool isOn) {
- if (!isOn || usesEncryption_)
- return;
-
- QMessageBox msgBox;
- msgBox.setIcon(QMessageBox::Question);
- msgBox.setWindowTitle(tr("End-to-End Encryption"));
- msgBox.setText(tr(
- "Encryption is currently experimental and things might break unexpectedly. <br>"
- "Please take note that it can't be disabled afterwards."));
- msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
- msgBox.setDefaultButton(QMessageBox::Save);
- int ret = msgBox.exec();
-
- switch (ret) {
- case QMessageBox::Ok: {
- encryptionToggle_->setState(true);
- encryptionToggle_->setEnabled(false);
- enableEncryption();
- keyRequestsToggle_->show();
- keyRequestsLabel->show();
- break;
- }
- default: {
- break;
- }
- }
- });
-
- // Disable encryption button.
- if (usesEncryption_) {
- encryptionToggle_->setState(true);
- encryptionToggle_->setEnabled(false);
-
- keyRequestsToggle_->setState(utils::respondsToKeyRequests(room_id_));
- } else {
- encryptionToggle_->setState(false);
-
- keyRequestsLabel->hide();
- keyRequestsToggle_->hide();
- }
-
- // Hide encryption option for public rooms.
- if (!usesEncryption_ && (info_.join_rule == state::JoinRule::Public)) {
- encryptionToggle_->hide();
- encryptionLabel->hide();
-
- keyRequestsLabel->hide();
- keyRequestsToggle_->hide();
- }
-
- 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);
- avatar_->installEventFilter(filter);
- avatar_->setCursor(Qt::PointingHandCursor);
- connect(filter, &ClickableFilter::clicked, this, &RoomSettings::updateAvatar);
- }
-
- roomNameLabel_ = new QLabel(QString::fromStdString(info_.name), this);
- roomNameLabel_->setFont(largeFont);
-
- auto membersLabel = new QLabel(tr("%n member(s)", "", (int)info_.member_count), this);
-
- auto textLayout = new QVBoxLayout;
- textLayout->addWidget(roomNameLabel_);
- textLayout->addWidget(membersLabel);
- textLayout->setAlignment(roomNameLabel_, Qt::AlignCenter | Qt::AlignTop);
- textLayout->setAlignment(membersLabel, Qt::AlignCenter | Qt::AlignTop);
- textLayout->setSpacing(TEXT_SPACING);
- textLayout->setMargin(0);
-
- setupEditButton();
-
- errorLabel_ = new QLabel(this);
- errorLabel_->setAlignment(Qt::AlignCenter);
- errorLabel_->hide();
-
- spinner_ = new LoadingIndicator(this);
- spinner_->setFixedHeight(30);
- spinner_->setFixedWidth(30);
- spinner_->hide();
- auto spinnerLayout = new QVBoxLayout;
- spinnerLayout->addWidget(spinner_);
- spinnerLayout->setAlignment(Qt::AlignCenter);
- spinnerLayout->setMargin(0);
- spinnerLayout->setSpacing(0);
-
- auto okBtn = new QPushButton("OK", this);
-
- auto buttonLayout = new QHBoxLayout();
- buttonLayout->setSpacing(15);
- buttonLayout->addStretch(1);
- buttonLayout->addWidget(okBtn);
-
- layout->addWidget(avatar_, Qt::AlignCenter | Qt::AlignTop);
- layout->addLayout(textLayout);
- layout->addLayout(btnLayout_);
- layout->addWidget(settingsLabel, Qt::AlignLeft);
- layout->addLayout(notifOptionLayout_);
- layout->addLayout(accessOptionLayout);
- layout->addLayout(encryptionOptionLayout);
- layout->addLayout(keyRequestsLayout);
- layout->addWidget(infoLabel, Qt::AlignLeft);
- layout->addLayout(roomIdLayout);
- layout->addLayout(roomVersionLayout);
- layout->addWidget(errorLabel_);
- layout->addLayout(buttonLayout);
- layout->addLayout(spinnerLayout);
- layout->addStretch(1);
-
- connect(this, &RoomSettings::enableEncryptionError, this, [this](const QString &msg) {
- encryptionToggle_->setState(false);
- keyRequestsToggle_->setState(false);
- keyRequestsToggle_->setEnabled(false);
- keyRequestsToggle_->hide();
-
- emit ChatPage::instance()->showNotification(msg);
- });
-
- connect(this, &RoomSettings::showErrorMessage, this, [this](const QString &msg) {
- if (!errorLabel_)
- return;
-
- stopLoadingSpinner();
-
- errorLabel_->show();
- errorLabel_->setText(msg);
- });
-
- connect(this, &RoomSettings::accessRulesUpdated, this, [this]() {
- stopLoadingSpinner();
- resetErrorLabel();
- });
-
- auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this);
- connect(closeShortcut, &QShortcut::activated, this, &RoomSettings::close);
- connect(okBtn, &QPushButton::clicked, this, &RoomSettings::close);
-}
-
-void
-RoomSettings::setupEditButton()
-{
- btnLayout_ = new QHBoxLayout;
- btnLayout_->setSpacing(BUTTON_SPACING);
- btnLayout_->setMargin(0);
-
- if (!canChangeNameAndTopic(room_id_.toStdString(), utils::localUser().toStdString()))
- return;
-
- QIcon editIcon;
- editIcon.addFile(":/icons/icons/ui/edit.png");
- editFieldsBtn_ = new FlatButton(this);
- editFieldsBtn_->setFixedSize(BUTTON_SIZE, BUTTON_SIZE);
- editFieldsBtn_->setCornerRadius(BUTTON_RADIUS);
- editFieldsBtn_->setIcon(editIcon);
- editFieldsBtn_->setIcon(editIcon);
- editFieldsBtn_->setIconSize(QSize(BUTTON_RADIUS, BUTTON_RADIUS));
-
- connect(editFieldsBtn_, &QPushButton::clicked, this, [this]() {
- retrieveRoomInfo();
-
- auto modal = new EditModal(room_id_, this);
- modal->setFields(QString::fromStdString(info_.name),
- QString::fromStdString(info_.topic));
- modal->raise();
- modal->show();
- connect(modal, &EditModal::nameChanged, this, [this](const QString &newName) {
- if (roomNameLabel_)
- roomNameLabel_->setText(newName);
- });
- });
-
- btnLayout_->addStretch(1);
- btnLayout_->addWidget(editFieldsBtn_);
- btnLayout_->addStretch(1);
-}
-
-void
-RoomSettings::retrieveRoomInfo()
-{
- try {
- 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());
- }
-}
-
-void
-RoomSettings::enableEncryption()
-{
- const auto room_id = room_id_.toStdString();
- http::client()->enable_encryption(
- room_id, [room_id, this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
- if (err) {
- int status_code = static_cast<int>(err->status_code);
- nhlog::net()->warn("failed to enable encryption in room ({}): {} {}",
- room_id,
- err->matrix_error.error,
- status_code);
- emit enableEncryptionError(
- tr("Failed to enable encryption: %1")
- .arg(QString::fromStdString(err->matrix_error.error)));
- return;
- }
-
- nhlog::net()->info("enabled encryption on room ({})", room_id);
- });
-}
-
-void
-RoomSettings::showEvent(QShowEvent *event)
-{
- resetErrorLabel();
- stopLoadingSpinner();
-
- QWidget::showEvent(event);
-}
-
-bool
-RoomSettings::canChangeJoinRules(const std::string &room_id, const std::string &user_id) const
-{
- try {
- return cache::hasEnoughPowerLevel({EventType::RoomJoinRules}, room_id, user_id);
- } catch (const lmdb::error &e) {
- nhlog::db()->warn("lmdb error: {}", e.what());
- }
-
- return false;
-}
-
-bool
-RoomSettings::canChangeNameAndTopic(const std::string &room_id, const std::string &user_id) const
-{
- try {
- return cache::hasEnoughPowerLevel(
- {EventType::RoomName, EventType::RoomTopic}, room_id, user_id);
- } catch (const lmdb::error &e) {
- nhlog::db()->warn("lmdb error: {}", e.what());
- }
-
- return false;
-}
-
-bool
-RoomSettings::canChangeAvatar(const std::string &room_id, const std::string &user_id) const
-{
- try {
- return cache::hasEnoughPowerLevel({EventType::RoomAvatar}, room_id, user_id);
- } catch (const lmdb::error &e) {
- nhlog::db()->warn("lmdb error: {}", e.what());
- }
-
- return false;
-}
-
-void
-RoomSettings::updateAccessRules(const std::string &room_id,
- const mtx::events::state::JoinRules &join_rule,
- const mtx::events::state::GuestAccess &guest_access)
-{
- startLoadingSpinner();
- resetErrorLabel();
-
- http::client()->send_state_event(
- room_id,
- join_rule,
- [this, room_id, guest_access](const mtx::responses::EventId &,
- mtx::http::RequestErr err) {
- if (err) {
- nhlog::net()->warn("failed to send m.room.join_rule: {} {}",
- static_cast<int>(err->status_code),
- err->matrix_error.error);
- emit showErrorMessage(QString::fromStdString(err->matrix_error.error));
-
- return;
- }
-
- http::client()->send_state_event(
- room_id,
- guest_access,
- [this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
- if (err) {
- nhlog::net()->warn("failed to send m.room.guest_access: {} {}",
- static_cast<int>(err->status_code),
- err->matrix_error.error);
- emit showErrorMessage(
- QString::fromStdString(err->matrix_error.error));
-
- return;
- }
-
- emit accessRulesUpdated();
- });
- });
-}
-
-void
-RoomSettings::stopLoadingSpinner()
-{
- if (spinner_) {
- spinner_->stop();
- spinner_->hide();
- }
-}
-
-void
-RoomSettings::startLoadingSpinner()
-{
- if (spinner_) {
- spinner_->start();
- spinner_->show();
- }
-}
-
-void
-RoomSettings::displayErrorMessage(const QString &msg)
-{
- stopLoadingSpinner();
-
- errorLabel_->show();
- errorLabel_->setText(msg);
-}
-
-void
-RoomSettings::setAvatar()
-{
- stopLoadingSpinner();
-
- if (avatar_)
- avatar_->setImage(QString::fromStdString(info_.avatar_url));
-}
-
-void
-RoomSettings::resetErrorLabel()
-{
- if (errorLabel_) {
- errorLabel_->hide();
- errorLabel_->clear();
- }
-}
-
-void
-RoomSettings::updateAvatar()
-{
- 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;
-
- QMimeDatabase db;
- QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent);
-
- const auto format = mime.name().split("/")[0];
-
- QFile file{fileName, this};
- if (format != "image") {
- displayErrorMessage(tr("The selected file is not an image"));
- return;
- }
-
- if (!file.open(QIODevice::ReadOnly)) {
- displayErrorMessage(tr("Error while reading file: %1").arg(file.errorString()));
- return;
- }
-
- if (spinner_) {
- startLoadingSpinner();
- resetErrorLabel();
- }
-
- // Events emitted from the http callbacks (different threads) will
- // be queued back into the UI thread through this proxy object.
- auto proxy = std::make_shared<ThreadProxy>();
- connect(proxy.get(), &ThreadProxy::error, this, &RoomSettings::displayErrorMessage);
- connect(proxy.get(), &ThreadProxy::avatarChanged, this, &RoomSettings::setAvatar);
-
- const auto bin = file.peek(file.size());
- const auto payload = std::string(bin.data(), bin.size());
- const auto dimensions = QImageReader(&file).size();
-
- // First we need to create a new mxc URI
- // (i.e upload media to the Matrix content repository) for the new avatar.
- http::client()->upload(
- payload,
- mime.name().toStdString(),
- QFileInfo(fileName).fileName().toStdString(),
- [proxy = std::move(proxy),
- dimensions,
- payload,
- mimetype = mime.name().toStdString(),
- size = payload.size(),
- room_id = room_id_.toStdString(),
- content = std::move(bin)](const mtx::responses::ContentURI &res,
- mtx::http::RequestErr err) {
- if (err) {
- emit proxy->error(
- tr("Failed to upload image: %s")
- .arg(QString::fromStdString(err->matrix_error.error)));
- return;
- }
-
- using namespace mtx::events;
- state::Avatar avatar_event;
- avatar_event.image_info.w = dimensions.width();
- avatar_event.image_info.h = dimensions.height();
- avatar_event.image_info.mimetype = mimetype;
- avatar_event.image_info.size = size;
- avatar_event.url = res.content_uri;
-
- http::client()->send_state_event(
- room_id,
- avatar_event,
- [content = std::move(content), proxy = std::move(proxy)](
- const mtx::responses::EventId &, mtx::http::RequestErr err) {
- if (err) {
- emit proxy->error(
- tr("Failed to upload image: %s")
- .arg(QString::fromStdString(err->matrix_error.error)));
- return;
- }
-
- emit proxy->avatarChanged();
- });
- });
-}
diff --git a/src/dialogs/RoomSettings.h b/src/dialogs/RoomSettings.h
deleted file mode 100644
index e0918afd..00000000
--- a/src/dialogs/RoomSettings.h
+++ /dev/null
@@ -1,150 +0,0 @@
-#pragma once
-
-#include <QFrame>
-#include <QImage>
-
-#include <mtx/events/guest_access.hpp>
-
-#include "CacheStructs.h"
-
-class Avatar;
-class FlatButton;
-class QPushButton;
-class QComboBox;
-class QHBoxLayout;
-class QShowEvent;
-class LoadingIndicator;
-class QLayout;
-class QPixmap;
-class TextField;
-class TextField;
-class Toggle;
-class QLabel;
-class QEvent;
-
-class ClickableFilter : public QObject
-{
- Q_OBJECT
-
-public:
- explicit ClickableFilter(QWidget *parent)
- : QObject(parent)
- {}
-
-signals:
- void clicked();
-
-protected:
- bool eventFilter(QObject *obj, QEvent *event) override;
-};
-
-/// Convenience class which connects events emmited from threads
-/// outside of main with the UI code.
-class ThreadProxy : public QObject
-{
- Q_OBJECT
-
-signals:
- void error(const QString &msg);
- void avatarChanged();
- void nameEventSent(const QString &);
- void topicEventSent();
-};
-
-class EditModal : public QWidget
-{
- Q_OBJECT
-
-public:
- EditModal(const QString &roomId, QWidget *parent = nullptr);
-
- void setFields(const QString &roomName, const QString &roomTopic);
-
-signals:
- void nameChanged(const QString &roomName);
-
-private slots:
- void topicEventSent();
- void nameEventSent(const QString &name);
- void error(const QString &msg);
-
- void applyClicked();
-
-private:
- QString roomId_;
- QString initialName_;
- QString initialTopic_;
-
- QLabel *errorField_;
-
- TextField *nameInput_;
- TextField *topicInput_;
-
- QPushButton *applyBtn_;
- QPushButton *cancelBtn_;
-};
-
-namespace dialogs {
-
-class RoomSettings : public QFrame
-{
- Q_OBJECT
-public:
- RoomSettings(const QString &room_id, QWidget *parent = nullptr);
-
-signals:
- void enableEncryptionError(const QString &msg);
- void showErrorMessage(const QString &msg);
- void accessRulesUpdated();
- void notifChanged(int index);
-
-protected:
- void showEvent(QShowEvent *event) override;
-
-private slots:
- //! The file dialog opens so the user can select and upload a new room avatar.
- void updateAvatar();
-
-private:
- //! Whether the user has enough power level to send m.room.join_rules events.
- bool canChangeJoinRules(const std::string &room_id, const std::string &user_id) const;
- //! Whether the user has enough power level to send m.room.name & m.room.topic events.
- bool canChangeNameAndTopic(const std::string &room_id, const std::string &user_id) const;
- //! Whether the user has enough power level to send m.room.avatar event.
- bool canChangeAvatar(const std::string &room_id, const std::string &user_id) const;
- void updateAccessRules(const std::string &room_id,
- const mtx::events::state::JoinRules &,
- const mtx::events::state::GuestAccess &);
- void stopLoadingSpinner();
- void startLoadingSpinner();
- void resetErrorLabel();
- void displayErrorMessage(const QString &msg);
-
- void setAvatar();
- void setupEditButton();
- //! Retrieve the current room information from cache.
- void retrieveRoomInfo();
- void enableEncryption();
-
- Avatar *avatar_ = nullptr;
-
- bool usesEncryption_ = false;
- QHBoxLayout *btnLayout_;
-
- FlatButton *editFieldsBtn_ = nullptr;
-
- RoomInfo info_;
- QString room_id_;
- QImage avatarImg_;
-
- QLabel *roomNameLabel_ = nullptr;
- QLabel *errorLabel_ = nullptr;
- LoadingIndicator *spinner_ = nullptr;
-
- QComboBox *notifCombo = nullptr;
- QComboBox *accessCombo = nullptr;
- Toggle *encryptionToggle_ = nullptr;
- Toggle *keyRequestsToggle_ = nullptr;
-};
-
-} // dialogs
diff --git a/src/notifications/ManagerLinux.cpp b/src/notifications/ManagerLinux.cpp
index fb424b2a..c7fd4023 100644
--- a/src/notifications/ManagerLinux.cpp
+++ b/src/notifications/ManagerLinux.cpp
@@ -109,7 +109,7 @@ NotificationsManager::closeNotification(uint id)
"org.freedesktop.Notifications");
auto call = closeCall.asyncCall("CloseNotification", (uint)id); // replace_id
auto watcher = new QDBusPendingCallWatcher{call, this};
- connect(watcher, &QDBusPendingCallWatcher::finished, this, [watcher, this]() {
+ connect(watcher, &QDBusPendingCallWatcher::finished, this, [watcher]() {
if (watcher->reply().type() == QDBusMessage::ErrorMessage) {
qDebug() << "D-Bus Error:" << watcher->reply().errorMessage();
};
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index 08cbd15b..b1580f97 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -19,6 +19,7 @@
#include "MainWindow.h"
#include "MatrixClient.h"
#include "Olm.h"
+#include "RoomsModel.h"
#include "TimelineModel.h"
#include "TimelineViewManager.h"
#include "UserSettingsPage.h"
@@ -122,6 +123,20 @@ InputBar::insertMimeData(const QMimeData *md)
}
void
+InputBar::setText(QString newText)
+{
+ if (history_.empty())
+ history_.push_front(newText);
+ else
+ history_.front() = newText;
+ history_index_ = 0;
+
+ if (history_.size() == INPUT_HISTORY_SIZE)
+ history_.pop_back();
+
+ emit textChanged(newText);
+}
+void
InputBar::updateState(int selectionStart_, int selectionEnd_, int cursorPosition_, QString text_)
{
if (text_.isEmpty())
@@ -186,6 +201,11 @@ InputBar::completerFor(QString completerName)
auto proxy = new CompletionProxyModel(emojiModel);
emojiModel->setParent(proxy);
return proxy;
+ } else if (completerName == "room") {
+ auto roomModel = new RoomsModel(true);
+ auto proxy = new CompletionProxyModel(roomModel);
+ roomModel->setParent(proxy);
+ return proxy;
}
return nullptr;
}
@@ -196,6 +216,10 @@ InputBar::send()
if (text().trimmed().isEmpty())
return;
+ nhlog::ui()->debug("Send: {}", text().toStdString());
+
+ auto wasEdit = !room->edit().isEmpty();
+
if (text().startsWith('/')) {
int command_end = text().indexOf(' ');
if (command_end == -1)
@@ -211,12 +235,10 @@ InputBar::send()
message(text());
}
- nhlog::ui()->debug("Send: {}", text().toStdString());
-
- if (history_.size() == INPUT_HISTORY_SIZE)
- history_.pop_back();
- history_.push_front("");
- history_index_ = 0;
+ if (!wasEdit) {
+ history_.push_front("");
+ setText("");
+ }
}
void
@@ -272,12 +294,10 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown)
if (!room->reply().isEmpty()) {
text.relations.relations.push_back(
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
- room->resetReply();
}
text.relations.relations.push_back(
{mtx::common::RelationType::Replace, room->edit().toStdString()});
- room->resetEdit();
} else if (!room->reply().isEmpty()) {
auto related = room->relatedInfo(room->reply());
@@ -307,7 +327,6 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown)
text.relations.relations.push_back(
{mtx::common::RelationType::InReplyTo, related.related_event});
- room->resetReply();
}
room->sendMessageEvent(text, mtx::events::EventType::RoomMessage);
@@ -330,12 +349,10 @@ InputBar::emote(QString msg)
if (!room->reply().isEmpty()) {
emote.relations.relations.push_back(
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
- room->resetReply();
}
if (!room->edit().isEmpty()) {
emote.relations.relations.push_back(
{mtx::common::RelationType::Replace, room->edit().toStdString()});
- room->resetEdit();
}
room->sendMessageEvent(emote, mtx::events::EventType::RoomMessage);
@@ -366,12 +383,10 @@ InputBar::image(const QString &filename,
if (!room->reply().isEmpty()) {
image.relations.relations.push_back(
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
- room->resetReply();
}
if (!room->edit().isEmpty()) {
image.relations.relations.push_back(
{mtx::common::RelationType::Replace, room->edit().toStdString()});
- room->resetEdit();
}
room->sendMessageEvent(image, mtx::events::EventType::RoomMessage);
@@ -397,12 +412,10 @@ InputBar::file(const QString &filename,
if (!room->reply().isEmpty()) {
file.relations.relations.push_back(
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
- room->resetReply();
}
if (!room->edit().isEmpty()) {
file.relations.relations.push_back(
{mtx::common::RelationType::Replace, room->edit().toStdString()});
- room->resetEdit();
}
room->sendMessageEvent(file, mtx::events::EventType::RoomMessage);
@@ -429,12 +442,10 @@ InputBar::audio(const QString &filename,
if (!room->reply().isEmpty()) {
audio.relations.relations.push_back(
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
- room->resetReply();
}
if (!room->edit().isEmpty()) {
audio.relations.relations.push_back(
{mtx::common::RelationType::Replace, room->edit().toStdString()});
- room->resetEdit();
}
room->sendMessageEvent(audio, mtx::events::EventType::RoomMessage);
@@ -460,12 +471,10 @@ InputBar::video(const QString &filename,
if (!room->reply().isEmpty()) {
video.relations.relations.push_back(
{mtx::common::RelationType::InReplyTo, room->reply().toStdString()});
- room->resetReply();
}
if (!room->edit().isEmpty()) {
video.relations.relations.push_back(
{mtx::common::RelationType::Replace, room->edit().toStdString()});
- room->resetEdit();
}
room->sendMessageEvent(video, mtx::events::EventType::RoomMessage);
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index 696a0dd9..4cb6da7b 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -41,7 +41,7 @@ public slots:
QString text() const;
QString previousText();
QString nextText();
- void setText(QString newText) { emit textChanged(newText); }
+ void setText(QString newText);
void send();
void paste(bool fromMouse);
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 5c904932..d46a313a 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -362,6 +362,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
const static QRegularExpression replyFallback(
"<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption);
+ auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent();
+
bool isReply = relations(event).reply_to().has_value();
auto formattedBody_ = QString::fromStdString(formatted_body(event));
@@ -380,8 +382,14 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
formattedBody_ = formattedBody_.remove(replyFallback);
}
- formattedBody_.replace("<img src=\"mxc://", "<img src=\"image://mxcImage/");
- formattedBody_.replace("<img src=\"mxc://", "<img src=\"image://mxcImage/");
+ // TODO(Nico): Don't parse html with a regex
+ const static QRegularExpression matchImgUri(
+ "(<img [^>]*)src=\"mxc://([^\"]*)\"([^>]*>)");
+ formattedBody_.replace(matchImgUri, "\\1 src=\"image://mxcImage/\\2\"\\3");
+ const static QRegularExpression matchEmoticonHeight(
+ "(<img data-mx-emoticon [^>]*)height=\"([^\"]*)\"([^>]*>)");
+ formattedBody_.replace(matchEmoticonHeight,
+ QString("\\1 height=\"%1\"\\3").arg(ascent));
return QVariant(utils::replaceEmoji(
utils::linkifyMessage(utils::escapeBlacklistedHtml(formattedBody_))));
@@ -491,6 +499,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
data(event, static_cast<int>(ProportionalHeight)));
m.insert(names[Id], data(event, static_cast<int>(Id)));
m.insert(names[State], data(event, static_cast<int>(State)));
+ m.insert(names[IsEdited], data(event, static_cast<int>(IsEdited)));
+ m.insert(names[IsEditable], data(event, static_cast<int>(IsEditable)));
m.insert(names[IsEncrypted], data(event, static_cast<int>(IsEncrypted)));
m.insert(names[IsRoomEncrypted], data(event, static_cast<int>(IsRoomEncrypted)));
m.insert(names[ReplyTo], data(event, static_cast<int>(ReplyTo)));
@@ -753,11 +763,6 @@ TimelineModel::setCurrentIndex(int index)
(!oldReadIndex || *oldReadIndex < nextEventIndexAndId->first)) {
readEvent(nextEventIndexAndId->second);
currentReadId = QString::fromStdString(nextEventIndexAndId->second);
-
- nhlog::net()->info("Marked as read {}, index {}, oldReadIndex {}",
- nextEventIndexAndId->second,
- nextEventIndexAndId->first,
- *oldReadIndex);
}
}
}
@@ -834,6 +839,14 @@ TimelineModel::openUserProfile(QString userid, bool global)
}
void
+TimelineModel::openRoomSettings()
+{
+ RoomSettings *settings = new RoomSettings(roomId(), this);
+ connect(this, &TimelineModel::roomAvatarUrlChanged, settings, &RoomSettings::avatarChanged);
+ openRoomSettingsDialog(settings);
+}
+
+void
TimelineModel::replyAction(QString id)
{
setReply(id);
@@ -1539,6 +1552,17 @@ TimelineModel::setEdit(QString newEdit)
if (edit_.startsWith('m'))
return;
+ if (newEdit.isEmpty()) {
+ resetEdit();
+ return;
+ }
+
+ if (edit_.isEmpty()) {
+ this->textBeforeEdit = input()->text();
+ this->replyBeforeEdit = reply_;
+ nhlog::ui()->debug("Stored: {}", textBeforeEdit.toStdString());
+ }
+
if (edit_ != newEdit) {
auto ev = events.get(newEdit.toStdString(), "");
if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) {
@@ -1573,8 +1597,14 @@ TimelineModel::resetEdit()
if (!edit_.isEmpty()) {
edit_ = "";
emit editChanged(edit_);
- input()->setText("");
- resetReply();
+ nhlog::ui()->debug("Restoring: {}", textBeforeEdit.toStdString());
+ input()->setText(textBeforeEdit);
+ textBeforeEdit.clear();
+ if (replyBeforeEdit.isEmpty())
+ resetReply();
+ else
+ setReply(replyBeforeEdit);
+ replyBeforeEdit.clear();
}
}
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 83012cd8..e02539bb 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -11,6 +11,7 @@
#include "CacheCryptoStructs.h"
#include "EventStore.h"
#include "InputBar.h"
+#include "ui/RoomSettings.h"
#include "ui/UserProfile.h"
namespace mtx::http {
@@ -216,6 +217,7 @@ public:
Q_INVOKABLE void viewRawMessage(QString id) const;
Q_INVOKABLE void viewDecryptedRawMessage(QString id) const;
Q_INVOKABLE void openUserProfile(QString userid, bool global = false);
+ Q_INVOKABLE void openRoomSettings();
Q_INVOKABLE void editAction(QString id);
Q_INVOKABLE void replyAction(QString id);
Q_INVOKABLE void readReceiptsAction(QString id) const;
@@ -307,6 +309,7 @@ signals:
void newCallEvent(const mtx::events::collections::TimelineEvents &event);
void openProfile(UserProfile *profile);
+ void openRoomSettingsDialog(RoomSettings *settings);
void newMessageToSend(mtx::events::collections::TimelineEvents event);
void addPendingMessageToStore(mtx::events::collections::TimelineEvents event);
@@ -334,6 +337,7 @@ private:
QString currentId, currentReadId;
QString reply_, edit_;
+ QString textBeforeEdit, replyBeforeEdit;
std::vector<QString> typingUsers_;
TimelineViewManager *manager_;
@@ -351,4 +355,6 @@ TimelineModel::sendMessageEvent(const T &content, mtx::events::EventType eventTy
msgCopy.content = content;
msgCopy.type = eventType;
emit newMessageToSend(msgCopy);
+ resetReply();
+ resetEdit();
}
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index b7d2bfb1..f2e6d571 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -128,6 +128,12 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
0,
"UserProfileModel",
"UserProfile needs to be instantiated on the C++ side");
+ qmlRegisterUncreatableType<RoomSettings>(
+ "im.nheko",
+ 1,
+ 0,
+ "RoomSettingsModel",
+ "Room Settings needs to be instantiated on the C++ side");
static auto self = this;
qmlRegisterSingletonType<MainWindow>(
@@ -387,11 +393,6 @@ TimelineViewManager::openLeaveRoomDialog() const
{
MainWindow::instance()->openLeaveRoomDialog(timeline_->roomId());
}
-void
-TimelineViewManager::openRoomSettings() const
-{
- MainWindow::instance()->openRoomSettings(timeline_->roomId());
-}
void
TimelineViewManager::verifyUser(QString userid)
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 7c994a14..61fce574 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -70,7 +70,6 @@ public:
Q_INVOKABLE void openInviteUsersDialog();
Q_INVOKABLE void openMemberListDialog() const;
Q_INVOKABLE void openLeaveRoomDialog() const;
- Q_INVOKABLE void openRoomSettings() const;
Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow);
void verifyUser(QString userid);
diff --git a/src/ui/RoomSettings.cpp b/src/ui/RoomSettings.cpp
new file mode 100644
index 00000000..a264c78b
--- /dev/null
+++ b/src/ui/RoomSettings.cpp
@@ -0,0 +1,625 @@
+#include "RoomSettings.h"
+
+#include <QApplication>
+#include <QFileDialog>
+#include <QHBoxLayout>
+#include <QImageReader>
+#include <QMimeDatabase>
+#include <QStandardPaths>
+#include <QVBoxLayout>
+#include <mtx/responses/common.hpp>
+#include <mtx/responses/media.hpp>
+
+#include "Cache.h"
+#include "Config.h"
+#include "Logging.h"
+#include "MatrixClient.h"
+#include "Utils.h"
+#include "ui/TextField.h"
+
+using namespace mtx::events;
+
+EditModal::EditModal(const QString &roomId, QWidget *parent)
+ : QWidget(parent)
+ , roomId_{roomId}
+{
+ setAutoFillBackground(true);
+ setAttribute(Qt::WA_DeleteOnClose, true);
+ setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
+ setWindowModality(Qt::WindowModal);
+
+ QFont largeFont;
+ largeFont.setPointSizeF(largeFont.pointSizeF() * 1.4);
+ setMinimumWidth(conf::window::minModalWidth);
+ setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
+
+ auto layout = new QVBoxLayout(this);
+
+ applyBtn_ = new QPushButton(tr("Apply"), this);
+ cancelBtn_ = new QPushButton(tr("Cancel"), this);
+ cancelBtn_->setDefault(true);
+
+ auto btnLayout = new QHBoxLayout;
+ btnLayout->addStretch(1);
+ btnLayout->setSpacing(15);
+ btnLayout->addWidget(cancelBtn_);
+ btnLayout->addWidget(applyBtn_);
+
+ nameInput_ = new TextField(this);
+ nameInput_->setLabel(tr("Name").toUpper());
+ topicInput_ = new TextField(this);
+ topicInput_->setLabel(tr("Topic").toUpper());
+
+ errorField_ = new QLabel(this);
+ errorField_->setWordWrap(true);
+ errorField_->hide();
+
+ layout->addWidget(nameInput_);
+ layout->addWidget(topicInput_);
+ layout->addLayout(btnLayout, 1);
+
+ auto labelLayout = new QHBoxLayout;
+ labelLayout->setAlignment(Qt::AlignHCenter);
+ labelLayout->addWidget(errorField_);
+ layout->addLayout(labelLayout);
+
+ connect(applyBtn_, &QPushButton::clicked, this, &EditModal::applyClicked);
+ connect(cancelBtn_, &QPushButton::clicked, this, &EditModal::close);
+
+ auto window = QApplication::activeWindow();
+
+ if (window != nullptr) {
+ auto center = window->frameGeometry().center();
+ move(center.x() - (width() * 0.5), center.y() - (height() * 0.5));
+ }
+}
+
+void
+EditModal::topicEventSent(const QString &topic)
+{
+ errorField_->hide();
+ emit topicChanged(topic);
+ close();
+}
+
+void
+EditModal::nameEventSent(const QString &name)
+{
+ errorField_->hide();
+ emit nameChanged(name);
+ close();
+}
+
+void
+EditModal::error(const QString &msg)
+{
+ errorField_->setText(msg);
+ errorField_->show();
+}
+
+void
+EditModal::applyClicked()
+{
+ // Check if the values are changed from the originals.
+ auto newName = nameInput_->text().trimmed();
+ auto newTopic = topicInput_->text().trimmed();
+
+ errorField_->hide();
+
+ if (newName == initialName_ && newTopic == initialTopic_) {
+ close();
+ return;
+ }
+
+ using namespace mtx::events;
+ auto proxy = std::make_shared<ThreadProxy>();
+ connect(proxy.get(), &ThreadProxy::topicEventSent, this, &EditModal::topicEventSent);
+ connect(proxy.get(), &ThreadProxy::nameEventSent, this, &EditModal::nameEventSent);
+ connect(proxy.get(), &ThreadProxy::error, this, &EditModal::error);
+
+ if (newName != initialName_ && !newName.isEmpty()) {
+ state::Name body;
+ body.name = newName.toStdString();
+
+ http::client()->send_state_event(
+ roomId_.toStdString(),
+ body,
+ [proxy, newName](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+ if (err) {
+ emit proxy->error(
+ QString::fromStdString(err->matrix_error.error));
+ return;
+ }
+
+ emit proxy->nameEventSent(newName);
+ });
+ }
+
+ if (newTopic != initialTopic_ && !newTopic.isEmpty()) {
+ state::Topic body;
+ body.topic = newTopic.toStdString();
+
+ http::client()->send_state_event(
+ roomId_.toStdString(),
+ body,
+ [proxy, newTopic](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+ if (err) {
+ emit proxy->error(
+ QString::fromStdString(err->matrix_error.error));
+ return;
+ }
+
+ emit proxy->topicEventSent(newTopic);
+ });
+ }
+}
+
+void
+EditModal::setFields(const QString &roomName, const QString &roomTopic)
+{
+ initialName_ = roomName;
+ initialTopic_ = roomTopic;
+
+ nameInput_->setText(roomName);
+ topicInput_->setText(roomTopic);
+}
+
+RoomSettings::RoomSettings(QString roomid, QObject *parent)
+ : QObject(parent)
+ , roomid_{std::move(roomid)}
+{
+ retrieveRoomInfo();
+
+ // get room setting notifications
+ http::client()->get_pushrules(
+ "global",
+ "override",
+ roomid_.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",
+ roomid_.toStdString(),
+ [this](const mtx::pushrules::PushRule &rule,
+ mtx::http::RequestErr &err) {
+ if (err) {
+ notifications_ = 2; // all messages
+ emit notificationsChanged();
+ return;
+ }
+
+ if (rule.enabled) {
+ notifications_ = 1; // mentions only
+ emit notificationsChanged();
+ }
+ });
+ return;
+ }
+
+ if (rule.enabled) {
+ notifications_ = 0; // muted
+ emit notificationsChanged();
+ } else {
+ notifications_ = 2; // all messages
+ emit notificationsChanged();
+ }
+ });
+
+ // access rules
+ if (info_.join_rule == state::JoinRule::Public) {
+ if (info_.guest_access) {
+ accessRules_ = 0;
+ } else {
+ accessRules_ = 1;
+ }
+ } else {
+ accessRules_ = 2;
+ }
+ emit accessJoinRulesChanged();
+}
+
+QString
+RoomSettings::roomName() const
+{
+ return QString::fromStdString(info_.name);
+}
+
+QString
+RoomSettings::roomTopic() const
+{
+ return utils::linkifyMessage(QString::fromStdString(info_.topic).toHtmlEscaped());
+}
+
+QString
+RoomSettings::roomId() const
+{
+ return roomid_;
+}
+
+QString
+RoomSettings::roomVersion() const
+{
+ return QString::fromStdString(info_.version);
+}
+
+bool
+RoomSettings::isLoading() const
+{
+ return isLoading_;
+}
+
+QString
+RoomSettings::roomAvatarUrl()
+{
+ return QString::fromStdString(info_.avatar_url);
+}
+
+int
+RoomSettings::memberCount() const
+{
+ return info_.member_count;
+}
+
+void
+RoomSettings::retrieveRoomInfo()
+{
+ try {
+ usesEncryption_ = cache::isRoomEncrypted(roomid_.toStdString());
+ info_ = cache::singleRoomInfo(roomid_.toStdString());
+ } catch (const lmdb::error &) {
+ nhlog::db()->warn("failed to retrieve room info from cache: {}",
+ roomid_.toStdString());
+ }
+}
+
+int
+RoomSettings::notifications()
+{
+ return notifications_;
+}
+
+int
+RoomSettings::accessJoinRules()
+{
+ return accessRules_;
+}
+
+bool
+RoomSettings::respondsToKeyRequests()
+{
+ return usesEncryption_ && utils::respondsToKeyRequests(roomid_);
+}
+
+void
+RoomSettings::changeKeyRequestsPreference(bool isOn)
+{
+ utils::setKeyRequestsPreference(roomid_, isOn);
+ emit keyRequestsChanged();
+}
+
+void
+RoomSettings::enableEncryption()
+{
+ if (usesEncryption_)
+ return;
+
+ const auto room_id = roomid_.toStdString();
+ http::client()->enable_encryption(
+ room_id, [room_id, this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+ if (err) {
+ int status_code = static_cast<int>(err->status_code);
+ nhlog::net()->warn("failed to enable encryption in room ({}): {} {}",
+ room_id,
+ err->matrix_error.error,
+ status_code);
+ emit displayError(
+ tr("Failed to enable encryption: %1")
+ .arg(QString::fromStdString(err->matrix_error.error)));
+ usesEncryption_ = false;
+ emit encryptionChanged();
+ return;
+ }
+
+ nhlog::net()->info("enabled encryption on room ({})", room_id);
+ });
+
+ usesEncryption_ = true;
+ emit encryptionChanged();
+}
+
+bool
+RoomSettings::canChangeJoinRules() const
+{
+ try {
+ return cache::hasEnoughPowerLevel({EventType::RoomJoinRules},
+ roomid_.toStdString(),
+ utils::localUser().toStdString());
+ } catch (const lmdb::error &e) {
+ nhlog::db()->warn("lmdb error: {}", e.what());
+ }
+
+ return false;
+}
+
+bool
+RoomSettings::canChangeNameAndTopic() const
+{
+ try {
+ return cache::hasEnoughPowerLevel({EventType::RoomName, EventType::RoomTopic},
+ roomid_.toStdString(),
+ utils::localUser().toStdString());
+ } catch (const lmdb::error &e) {
+ nhlog::db()->warn("lmdb error: {}", e.what());
+ }
+
+ return false;
+}
+
+bool
+RoomSettings::canChangeAvatar() const
+{
+ try {
+ return cache::hasEnoughPowerLevel(
+ {EventType::RoomAvatar}, roomid_.toStdString(), utils::localUser().toStdString());
+ } catch (const lmdb::error &e) {
+ nhlog::db()->warn("lmdb error: {}", e.what());
+ }
+
+ return false;
+}
+
+bool
+RoomSettings::isEncryptionEnabled() const
+{
+ return usesEncryption_;
+}
+
+void
+RoomSettings::openEditModal()
+{
+ retrieveRoomInfo();
+
+ auto modal = new EditModal(roomid_);
+ modal->setFields(QString::fromStdString(info_.name), QString::fromStdString(info_.topic));
+ modal->raise();
+ modal->show();
+ connect(modal, &EditModal::nameChanged, this, [this](const QString &newName) {
+ info_.name = newName.toStdString();
+ emit roomNameChanged();
+ });
+
+ connect(modal, &EditModal::topicChanged, this, [this](const QString &newTopic) {
+ info_.topic = newTopic.toStdString();
+ emit roomTopicChanged();
+ });
+}
+
+void
+RoomSettings::changeNotifications(int currentIndex)
+{
+ notifications_ = currentIndex;
+
+ std::string room_id = roomid_.toStdString();
+ if (notifications_ == 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 (notifications_ == 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 &) {});
+ });
+ }
+}
+
+void
+RoomSettings::changeAccessRules(int index)
+{
+ using namespace mtx::events::state;
+
+ auto guest_access = [](int index) -> state::GuestAccess {
+ state::GuestAccess event;
+
+ if (index == 0)
+ event.guest_access = state::AccessState::CanJoin;
+ else
+ event.guest_access = state::AccessState::Forbidden;
+
+ return event;
+ }(index);
+
+ auto join_rule = [](int index) -> state::JoinRules {
+ state::JoinRules event;
+
+ switch (index) {
+ case 0:
+ case 1:
+ event.join_rule = state::JoinRule::Public;
+ break;
+ default:
+ event.join_rule = state::JoinRule::Invite;
+ }
+
+ return event;
+ }(index);
+
+ updateAccessRules(roomid_.toStdString(), join_rule, guest_access);
+}
+
+void
+RoomSettings::updateAccessRules(const std::string &room_id,
+ const mtx::events::state::JoinRules &join_rule,
+ const mtx::events::state::GuestAccess &guest_access)
+{
+ isLoading_ = true;
+ emit loadingChanged();
+
+ http::client()->send_state_event(
+ room_id,
+ join_rule,
+ [this, room_id, guest_access](const mtx::responses::EventId &,
+ mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn("failed to send m.room.join_rule: {} {}",
+ static_cast<int>(err->status_code),
+ err->matrix_error.error);
+ emit displayError(QString::fromStdString(err->matrix_error.error));
+ isLoading_ = false;
+ emit loadingChanged();
+ return;
+ }
+
+ http::client()->send_state_event(
+ room_id,
+ guest_access,
+ [this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn("failed to send m.room.guest_access: {} {}",
+ static_cast<int>(err->status_code),
+ err->matrix_error.error);
+ emit displayError(
+ QString::fromStdString(err->matrix_error.error));
+ }
+
+ isLoading_ = false;
+ emit loadingChanged();
+ });
+ });
+}
+
+void
+RoomSettings::stopLoading()
+{
+ isLoading_ = false;
+ emit loadingChanged();
+}
+
+void
+RoomSettings::avatarChanged()
+{
+ retrieveRoomInfo();
+ emit avatarUrlChanged();
+}
+
+void
+RoomSettings::updateAvatar()
+{
+ const QString picturesFolder =
+ QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
+ const QString fileName = QFileDialog::getOpenFileName(
+ nullptr, tr("Select an avatar"), picturesFolder, tr("All Files (*)"));
+
+ if (fileName.isEmpty())
+ return;
+
+ QMimeDatabase db;
+ QMimeType mime = db.mimeTypeForFile(fileName, QMimeDatabase::MatchContent);
+
+ const auto format = mime.name().split("/")[0];
+
+ QFile file{fileName, this};
+ if (format != "image") {
+ emit displayError(tr("The selected file is not an image"));
+ return;
+ }
+
+ if (!file.open(QIODevice::ReadOnly)) {
+ emit displayError(tr("Error while reading file: %1").arg(file.errorString()));
+ return;
+ }
+
+ isLoading_ = true;
+ emit loadingChanged();
+
+ // Events emitted from the http callbacks (different threads) will
+ // be queued back into the UI thread through this proxy object.
+ auto proxy = std::make_shared<ThreadProxy>();
+ connect(proxy.get(), &ThreadProxy::error, this, &RoomSettings::displayError);
+ connect(proxy.get(), &ThreadProxy::stopLoading, this, &RoomSettings::stopLoading);
+
+ const auto bin = file.peek(file.size());
+ const auto payload = std::string(bin.data(), bin.size());
+ const auto dimensions = QImageReader(&file).size();
+
+ // First we need to create a new mxc URI
+ // (i.e upload media to the Matrix content repository) for the new avatar.
+ http::client()->upload(
+ payload,
+ mime.name().toStdString(),
+ QFileInfo(fileName).fileName().toStdString(),
+ [proxy = std::move(proxy),
+ dimensions,
+ payload,
+ mimetype = mime.name().toStdString(),
+ size = payload.size(),
+ room_id = roomid_.toStdString(),
+ content = std::move(bin)](const mtx::responses::ContentURI &res,
+ mtx::http::RequestErr err) {
+ if (err) {
+ emit proxy->stopLoading();
+ emit proxy->error(
+ tr("Failed to upload image: %s")
+ .arg(QString::fromStdString(err->matrix_error.error)));
+ return;
+ }
+
+ using namespace mtx::events;
+ state::Avatar avatar_event;
+ avatar_event.image_info.w = dimensions.width();
+ avatar_event.image_info.h = dimensions.height();
+ avatar_event.image_info.mimetype = mimetype;
+ avatar_event.image_info.size = size;
+ avatar_event.url = res.content_uri;
+
+ http::client()->send_state_event(
+ room_id,
+ avatar_event,
+ [content = std::move(content), proxy = std::move(proxy)](
+ const mtx::responses::EventId &, mtx::http::RequestErr err) {
+ if (err) {
+ emit proxy->error(
+ tr("Failed to upload image: %s")
+ .arg(QString::fromStdString(err->matrix_error.error)));
+ return;
+ }
+
+ emit proxy->stopLoading();
+ });
+ });
+}
diff --git a/src/ui/RoomSettings.h b/src/ui/RoomSettings.h
new file mode 100644
index 00000000..25c6e588
--- /dev/null
+++ b/src/ui/RoomSettings.h
@@ -0,0 +1,135 @@
+#pragma once
+
+#include <QLabel>
+#include <QObject>
+#include <QPushButton>
+#include <QString>
+
+#include <mtx/events/guest_access.hpp>
+
+#include "CacheStructs.h"
+
+class TextField;
+
+/// Convenience class which connects events emmited from threads
+/// outside of main with the UI code.
+class ThreadProxy : public QObject
+{
+ Q_OBJECT
+
+signals:
+ void error(const QString &msg);
+ void nameEventSent(const QString &);
+ void topicEventSent(const QString &);
+ void stopLoading();
+};
+
+class EditModal : public QWidget
+{
+ Q_OBJECT
+
+public:
+ EditModal(const QString &roomId, QWidget *parent = nullptr);
+
+ void setFields(const QString &roomName, const QString &roomTopic);
+
+signals:
+ void nameChanged(const QString &roomName);
+ void topicChanged(const QString &topic);
+
+private slots:
+ void topicEventSent(const QString &topic);
+ void nameEventSent(const QString &name);
+ void error(const QString &msg);
+
+ void applyClicked();
+
+private:
+ QString roomId_;
+ QString initialName_;
+ QString initialTopic_;
+
+ QLabel *errorField_;
+
+ TextField *nameInput_;
+ TextField *topicInput_;
+
+ QPushButton *applyBtn_;
+ QPushButton *cancelBtn_;
+};
+
+class RoomSettings : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString roomId READ roomId CONSTANT)
+ Q_PROPERTY(QString roomVersion READ roomVersion CONSTANT)
+ Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
+ Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged)
+ Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY avatarUrlChanged)
+ Q_PROPERTY(int memberCount READ memberCount CONSTANT)
+ Q_PROPERTY(int notifications READ notifications NOTIFY notificationsChanged)
+ Q_PROPERTY(int accessJoinRules READ accessJoinRules NOTIFY accessJoinRulesChanged)
+ Q_PROPERTY(bool isLoading READ isLoading NOTIFY loadingChanged)
+ Q_PROPERTY(bool canChangeAvatar READ canChangeAvatar CONSTANT)
+ Q_PROPERTY(bool canChangeJoinRules READ canChangeJoinRules CONSTANT)
+ Q_PROPERTY(bool canChangeNameAndTopic READ canChangeNameAndTopic CONSTANT)
+ Q_PROPERTY(bool isEncryptionEnabled READ isEncryptionEnabled NOTIFY encryptionChanged)
+ Q_PROPERTY(bool respondsToKeyRequests READ respondsToKeyRequests NOTIFY keyRequestsChanged)
+
+public:
+ RoomSettings(QString roomid, QObject *parent = nullptr);
+
+ QString roomId() const;
+ QString roomName() const;
+ QString roomTopic() const;
+ QString roomVersion() const;
+ QString roomAvatarUrl();
+ int memberCount() const;
+ int notifications();
+ int accessJoinRules();
+ bool respondsToKeyRequests();
+ bool isLoading() const;
+ //! Whether the user has enough power level to send m.room.join_rules events.
+ bool canChangeJoinRules() const;
+ //! Whether the user has enough power level to send m.room.name & m.room.topic events.
+ bool canChangeNameAndTopic() const;
+ //! Whether the user has enough power level to send m.room.avatar event.
+ bool canChangeAvatar() const;
+ bool isEncryptionEnabled() const;
+
+ Q_INVOKABLE void enableEncryption();
+ Q_INVOKABLE void updateAvatar();
+ Q_INVOKABLE void openEditModal();
+ Q_INVOKABLE void changeAccessRules(int index);
+ Q_INVOKABLE void changeNotifications(int currentIndex);
+ Q_INVOKABLE void changeKeyRequestsPreference(bool isOn);
+
+signals:
+ void loadingChanged();
+ void roomNameChanged();
+ void roomTopicChanged();
+ void avatarUrlChanged();
+ void encryptionChanged();
+ void keyRequestsChanged();
+ void notificationsChanged();
+ void accessJoinRulesChanged();
+ void displayError(const QString &errorMessage);
+
+public slots:
+ void stopLoading();
+ void avatarChanged();
+
+private:
+ void retrieveRoomInfo();
+ void updateAccessRules(const std::string &room_id,
+ const mtx::events::state::JoinRules &,
+ const mtx::events::state::GuestAccess &);
+
+private:
+ QString roomid_;
+ bool usesEncryption_ = false;
+ bool isLoading_ = false;
+ RoomInfo info_;
+ int notifications_ = 0;
+ int accessRules_ = 0;
+};
\ No newline at end of file
|