diff --git a/CMakeLists.txt b/CMakeLists.txt index 4004d850..136505ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -388,6 +388,8 @@ set(SRC_FILES src/ui/NhekoGlobalObject.h src/ui/RoomSettings.cpp src/ui/RoomSettings.h + src/ui/RoomSummary.cpp + src/ui/RoomSummary.h src/ui/Theme.cpp src/ui/Theme.h src/ui/ThemeManager.cpp @@ -581,7 +583,7 @@ if(USE_BUNDLED_MTXCLIENT) FetchContent_Declare( MatrixClient GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git - GIT_TAG v0.8.0 + GIT_TAG b706492de042455630063c847574bbc5ed5d4641 ) set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "") set(BUILD_LIB_TESTS OFF CACHE INTERNAL "") diff --git a/io.github.NhekoReborn.Nheko.yaml b/io.github.NhekoReborn.Nheko.yaml index 2c2979a9..d6d877d7 100644 --- a/io.github.NhekoReborn.Nheko.yaml +++ b/io.github.NhekoReborn.Nheko.yaml @@ -203,8 +203,8 @@ modules: buildsystem: cmake-ninja name: mtxclient sources: - - commit: 2a1cf15cbda4d3deb7986c9f3b38e6c7aabb0d6f - tag: v0.8.0 + - commit: b706492de042455630063c847574bbc5ed5d4641 + #tag: v0.8.0 type: git url: https://github.com/Nheko-Reborn/mtxclient.git - config-opts: diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index 7cc41db9..fc321136 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -174,6 +174,14 @@ Pane { } + Component { + id: confirmJoinRoomDialog + + ConfirmJoinRoomDialog { + } + + } + Component { id: leaveRoomComponent @@ -241,6 +249,12 @@ Pane { destroyOnClose(dialog); } + function onShowRoomJoinPrompt(summary) { + var dialog = confirmJoinRoomDialog.createObject(timelineRoot, {"summary": summary}); + dialog.show(); + destroyOnClose(dialog); + } + target: Nheko } diff --git a/resources/qml/dialogs/ConfirmJoinRoomDialog.qml b/resources/qml/dialogs/ConfirmJoinRoomDialog.qml new file mode 100644 index 00000000..91f03dcf --- /dev/null +++ b/resources/qml/dialogs/ConfirmJoinRoomDialog.qml @@ -0,0 +1,151 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// SPDX-FileCopyrightText: 2022 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import ".." +import "../ui" +import Qt.labs.platform 1.1 as Platform +import QtQuick 2.15 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.2 +import QtQuick.Window 2.13 +import im.nheko 1.0 + +ApplicationWindow { + id: joinRoomRoot + + required property RoomSummary summary + + title: qsTr("Confirm room join") + modality: Qt.WindowModal + flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint + palette: Nheko.colors + color: Nheko.colors.window + width: 350 + height: content.implicitHeight + Nheko.paddingLarge + footer.implicitHeight + + Shortcut { + sequence: StandardKey.Cancel + onActivated: dbb.rejected() + } + + ColumnLayout { + id: content + spacing: Nheko.paddingMedium + anchors.margins: Nheko.paddingMedium + anchors.fill: parent + + Avatar { + Layout.topMargin: Nheko.paddingMedium + url: summary.roomAvatarUrl.replace("mxc://", "image://MxcImage/") + roomid: summary.roomid + displayName: summary.roomName + height: 130 + width: 130 + Layout.alignment: Qt.AlignHCenter + } + + Spinner { + Layout.alignment: Qt.AlignHCenter + visible: !summary.isLoaded + foreground: Nheko.colors.mid + running: !summary.isLoaded + } + + TextEdit { + readOnly: true + textFormat: TextEdit.RichText + text: summary.roomName + font.pixelSize: fontMetrics.font.pixelSize * 2 + color: Nheko.colors.text + + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + horizontalAlignment: TextEdit.AlignHCenter + wrapMode: TextEdit.Wrap + selectByMouse: true + } + TextEdit { + readOnly: true + textFormat: TextEdit.RichText + text: summary.roomid + font.pixelSize: fontMetrics.font.pixelSize * 0.8 + color: Nheko.colors.text + + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + horizontalAlignment: TextEdit.AlignHCenter + wrapMode: TextEdit.Wrap + selectByMouse: true + } + RowLayout { + spacing: Nheko.paddingMedium + Layout.alignment: Qt.AlignHCenter + + MatrixText { + text: qsTr("%n member(s)", "", summary.memberCount) + } + + ImageButton { + image: ":/icons/icons/ui/people.svg" + enabled: false + } + + } + TextEdit { + readOnly: true + textFormat: TextEdit.RichText + text: summary.roomTopic + color: Nheko.colors.text + + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + horizontalAlignment: TextEdit.AlignHCenter + wrapMode: TextEdit.Wrap + selectByMouse: true + } + + Label { + id: promptLabel + + text: summary.isKnockOnly ? qsTr("This room can't be joined directly. You can however knock on the room and room members can accept or decline this join request. You can additionally provide a reason for them to let you in below:") : qsTr("Do you want to join this room? You can optionally add a reason below:") + color: Nheko.colors.text + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + font.bold: true + } + + MatrixTextField { + id: reason + + focus: true + Layout.fillWidth: true + text: joinRoomRoot.summary.reason + } + + } + + footer: DialogButtonBox { + id: dbb + + standardButtons: DialogButtonBox.Cancel + onAccepted: { + summary.reason = reason.text; + summary.join(); + joinRoomRoot.close(); + } + onRejected: { + joinRoomRoot.close(); + } + + Button { + text: summary.isKnockOnly ? qsTr("Knock") : qsTr("Join") + enabled: input.text.match("#.+?:.{3,}") + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + } + + } + +} diff --git a/resources/qml/dialogs/JoinRoomDialog.qml b/resources/qml/dialogs/JoinRoomDialog.qml index 263481aa..0098370d 100644 --- a/resources/qml/dialogs/JoinRoomDialog.qml +++ b/resources/qml/dialogs/JoinRoomDialog.qml @@ -64,7 +64,7 @@ ApplicationWindow { } Button { - text: "Join" + text: qsTr("Join") enabled: input.text.match("#.+?:.{3,}") DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole } diff --git a/resources/res.qrc b/resources/res.qrc index 7f08c29d..4bdb3cb8 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -150,6 +150,7 @@ qml/device-verification/Success.qml qml/device-verification/Waiting.qml qml/dialogs/AliasEditor.qml + qml/dialogs/ConfirmJoinRoomDialog.qml qml/dialogs/CreateDirect.qml qml/dialogs/CreateRoom.qml qml/dialogs/HiddenEventsDialog.qml diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index baab28d1..d7fbfefa 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -22,6 +22,7 @@ #include "Utils.h" #include "encryption/DeviceVerificationFlow.h" #include "encryption/Olm.h" +#include "ui/RoomSummary.h" #include "ui/Theme.h" #include "ui/UserProfile.h" #include "voip/CallManager.h" @@ -130,7 +131,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QObject *parent) connect(this, &ChatPage::internalKnock, this, - qOverload &, QString, bool>( + qOverload &, QString, bool, bool>( &ChatPage::knockRoom), Qt::QueuedConnection); connect(this, &ChatPage::leftRoom, this, &ChatPage::removeRoom); @@ -697,23 +698,26 @@ void ChatPage::knockRoom(const QString &room, const std::vector &via, QString reason, - bool failedJoin) + bool failedJoin, + bool promptForConfirmation) { const auto room_id = room.toStdString(); bool confirmed = false; - reason = QInputDialog::getText( - nullptr, - tr("Knock on room"), - // clang-format off + if (promptForConfirmation) { + reason = QInputDialog::getText( + nullptr, + tr("Knock on room"), + // clang-format off failedJoin ? tr("You failed to join %1. You can try to knock, so that others can invite you in. Do you want to do so?\nYou may optionally provide a reason for others to accept your knock:").arg(room) : tr("Do you really want to knock on %1? You may optionally provide a reason for others to accept your knock:").arg(room), - // clang-format on - QLineEdit::Normal, - reason, - &confirmed); - if (!confirmed) { - return; + // clang-format on + QLineEdit::Normal, + reason, + &confirmed); + if (!confirmed) { + return; + } } http::client()->knock_room( @@ -742,13 +746,12 @@ ChatPage::joinRoomVia(const std::string &room_id, bool promptForConfirmation, const QString &reason) { - if (promptForConfirmation && - QMessageBox::Yes != - QMessageBox::question( - nullptr, - tr("Confirm join"), - tr("Do you really want to join %1?").arg(QString::fromStdString(room_id)))) + if (promptForConfirmation) { + auto prompt = new RoomSummary(room_id, via, reason); + QQmlEngine::setObjectOwnership(prompt, QQmlEngine::JavaScriptOwnership); + emit showRoomJoinPrompt(prompt); return; + } http::client()->join_room( room_id, @@ -756,7 +759,7 @@ ChatPage::joinRoomVia(const std::string &room_id, [this, room_id, reason, via](const mtx::responses::RoomId &, mtx::http::RequestErr err) { if (err) { if (err->matrix_error.errcode == mtx::errors::ErrorCode::M_FORBIDDEN) - emit internalKnock(QString::fromStdString(room_id), via, reason, true); + emit internalKnock(QString::fromStdString(room_id), via, reason, true, true); else emit showNotification(tr("Failed to join room: %1") .arg(QString::fromStdString(err->matrix_error.error))); diff --git a/src/ChatPage.h b/src/ChatPage.h index af06f02c..c0f0b559 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -24,6 +24,7 @@ #include "CacheCryptoStructs.h" #include "CacheStructs.h" +#include "ui/RoomSummary.h" class TimelineViewManager; class UserSettings; @@ -84,8 +85,9 @@ public slots: void knockRoom(const QString &room, QString reason = "") { knockRoom(room, {}, reason, false); } void knockRoom(const QString &room, const std::vector &via, - QString reason = "", - bool failedJoin = false); + QString reason = "", + bool failedJoin = false, + bool promptForConfirmation = true); void joinRoomVia(const std::string &room_id, const std::vector &via, bool promptForConfirmation = true, @@ -163,10 +165,12 @@ signals: void downloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc, const SecretsToDecrypt &secrets); + void showRoomJoinPrompt(RoomSummary *); void internalKnock(const QString &room, const std::vector &via, - QString reason = "", - bool failedJoin = false); + QString reason = "", + bool failedJoin = false, + bool promptForConfirmation = true); private slots: void logout(); diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index d2e28277..e5032fb2 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -54,6 +54,7 @@ #include "ui/NhekoDropArea.h" #include "ui/NhekoEventObserver.h" #include "ui/NhekoGlobalObject.h" +#include "ui/RoomSummary.h" #include "ui/UIA.h" #include "voip/CallManager.h" #include "voip/WebRTCSession.h" @@ -65,6 +66,7 @@ Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents) Q_DECLARE_METATYPE(std::vector) Q_DECLARE_METATYPE(std::vector) +Q_DECLARE_METATYPE(mtx::responses::PublicRoom) MainWindow *MainWindow::instance_ = nullptr; @@ -142,6 +144,7 @@ MainWindow::registerQmlTypes() qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType>(); @@ -180,6 +183,12 @@ MainWindow::registerQmlTypes() qmlRegisterType("im.nheko", 1, 0, "Login"); qmlRegisterType("im.nheko", 1, 0, "Registration"); qmlRegisterType("im.nheko", 1, 0, "HiddenEvents"); + qmlRegisterUncreatableType( + "im.nheko", + 1, + 0, + "RoomSummary", + QStringLiteral("Please use joinRoom to create a room summary.")); qmlRegisterUncreatableType( "im.nheko", 1, diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp index 2e1aadf0..5891f8cd 100644 --- a/src/ui/NhekoGlobalObject.cpp +++ b/src/ui/NhekoGlobalObject.cpp @@ -22,6 +22,7 @@ Nheko::Nheko() connect( UserSettings::instance().get(), &UserSettings::themeChanged, this, &Nheko::colorsChanged); connect(ChatPage::instance(), &ChatPage::contentLoaded, this, &Nheko::updateUserProfile); + connect(ChatPage::instance(), &ChatPage::showRoomJoinPrompt, this, &Nheko::showRoomJoinPrompt); connect(this, &Nheko::joinRoom, ChatPage::instance(), &ChatPage::joinRoom); } diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h index f9de489d..c8c6f667 100644 --- a/src/ui/NhekoGlobalObject.h +++ b/src/ui/NhekoGlobalObject.h @@ -11,6 +11,7 @@ #include "AliasEditModel.h" #include "PowerlevelsEditModels.h" +#include "RoomSummary.h" #include "Theme.h" #include "UserProfile.h" @@ -76,6 +77,8 @@ signals: void openJoinRoomDialog(); void joinRoom(QString roomId, QString reason = ""); + void showRoomJoinPrompt(RoomSummary *summary); + private: QScopedPointer currentUser_; }; diff --git a/src/ui/RoomSummary.cpp b/src/ui/RoomSummary.cpp new file mode 100644 index 00000000..dc035fe0 --- /dev/null +++ b/src/ui/RoomSummary.cpp @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2022 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "RoomSummary.h" + +#include + +#include "ChatPage.h" +#include "MatrixClient.h" + +RoomSummary::RoomSummary(std::string roomIdOrAlias_, + std::vector vias_, + QString r_, + QObject *p) + : QObject(p) + , roomIdOrAlias(std::move(roomIdOrAlias_)) + , vias(std::move(vias_)) + , reason_(std::move(r_)) +{ + auto ctx = std::make_shared(); + + connect(ctx.get(), &RoomSummaryProxy::failed, this, [this]() { + loaded_ = true; + emit loaded(); + }); + connect( + ctx.get(), &RoomSummaryProxy::loaded, this, [this](const mtx::responses::PublicRoom &resp) { + loaded_ = true; + room = resp; + emit loaded(); + }); + + http::client()->get_summary( + roomIdOrAlias, + [proxy = std::move(ctx)](const mtx::responses::PublicRoom &room, mtx::http::RequestErr e) { + if (e) { + emit proxy->failed(); + } else { + emit proxy->loaded(room); + } + }, + vias); +} + +void +RoomSummary::join() +{ + if (isKnockOnly()) + ChatPage::instance()->knockRoom( + QString::fromStdString(roomIdOrAlias), vias, reason_, false, false); + else + ChatPage::instance()->joinRoomVia(roomIdOrAlias, vias, false, reason_); +} diff --git a/src/ui/RoomSummary.h b/src/ui/RoomSummary.h new file mode 100644 index 00000000..8d6e0e23 --- /dev/null +++ b/src/ui/RoomSummary.h @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2022 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include + +#include + +class RoomSummaryProxy : public QObject +{ + Q_OBJECT + +public: + RoomSummaryProxy() {} +signals: + void loaded(mtx::responses::PublicRoom room); + void failed(); +}; + +class RoomSummary : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString reason READ reason WRITE setReason NOTIFY reasonChanged) + + Q_PROPERTY(QString roomid READ roomid NOTIFY loaded) + Q_PROPERTY(QString roomName READ roomName NOTIFY loaded) + Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY loaded) + Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY loaded) + Q_PROPERTY(bool isInvite READ isInvite NOTIFY loaded) + Q_PROPERTY(bool isSpace READ isInvite NOTIFY loaded) + Q_PROPERTY(bool isKnockOnly READ isKnockOnly NOTIFY loaded) + Q_PROPERTY(bool isLoaded READ isLoaded NOTIFY loaded) + Q_PROPERTY(int memberCount READ memberCount NOTIFY loaded) + +public: + explicit RoomSummary(std::string roomIdOrAlias_, + std::vector vias_, + QString reason_, + QObject *p = nullptr); + + void setReason(const QString &r) + { + reason_ = r; + emit reasonChanged(); + } + QString reason() const { return reason_; } + + QString roomid() const { return room ? QString::fromStdString(room->room_id) : ""; } + QString roomName() const + { + return QString::fromStdString(room ? room->room_id : roomIdOrAlias); + } + QString roomTopic() const { return room ? QString::fromStdString(room->topic) : ""; } + QString roomAvatarUrl() const { return room ? QString::fromStdString(room->avatar_url) : ""; } + bool isInvite() const + { + return room && room->membership == mtx::events::state::Membership::Invite; + } + bool isSpace() const { return room && room->room_type == mtx::events::state::room_type::space; } + int memberCount() const { return room ? room->num_joined_members : 0; } + bool isKnockOnly() const + { + return room && (room->join_rule == mtx::events::state::JoinRule::Knock || + room->join_rule == mtx::events::state::JoinRule::KnockRestricted); + } + + bool isLoaded() const { return room.has_value() || loaded_; } + + Q_INVOKABLE void join(); + +signals: + void loaded(); + void reasonChanged(); + +private: + std::string roomIdOrAlias; + std::vector vias; + std::optional room; + QString reason_; + bool loaded_ = false; +};