summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authortrilene <trilene@runbox.com>2021-02-25 12:10:12 -0500
committertrilene <trilene@runbox.com>2021-02-25 12:10:12 -0500
commit55fb00c67b611dfa55054b33e2f29d118e18d00f (patch)
tree714e583f6b6cc889bd81396ec425560bc9529848 /src
parentAdd screen sharing window preview (diff)
parentFix unaligned reads (diff)
downloadnheko-55fb00c67b611dfa55054b33e2f29d118e18d00f.tar.xz
Merge remote-tracking branch 'upstream/master' into screenshare-x11
Diffstat (limited to 'src')
-rw-r--r--src/Cache.cpp46
-rw-r--r--src/ChatPage.cpp43
-rw-r--r--src/ChatPage.h2
-rw-r--r--src/LoginPage.cpp60
-rw-r--r--src/LoginPage.h12
-rw-r--r--src/MainWindow.cpp9
-rw-r--r--src/MainWindow.h2
-rw-r--r--src/RegisterPage.cpp1
-rw-r--r--src/RoomInfoListItem.h2
-rw-r--r--src/RoomList.cpp8
-rw-r--r--src/RoomsModel.cpp69
-rw-r--r--src/RoomsModel.h33
-rw-r--r--src/dialogs/RoomSettings.cpp865
-rw-r--r--src/dialogs/RoomSettings.h150
-rw-r--r--src/notifications/ManagerLinux.cpp2
-rw-r--r--src/timeline/InputBar.cpp47
-rw-r--r--src/timeline/InputBar.h2
-rw-r--r--src/timeline/TimelineModel.cpp48
-rw-r--r--src/timeline/TimelineModel.h6
-rw-r--r--src/timeline/TimelineViewManager.cpp11
-rw-r--r--src/timeline/TimelineViewManager.h1
-rw-r--r--src/ui/RoomSettings.cpp625
-rw-r--r--src/ui/RoomSettings.h135
23 files changed, 1060 insertions, 1119 deletions
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:&#47;&#47;", "<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