Commits (2)
......@@ -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
......
......@@ -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
}
......
// 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
}
}
}
......@@ -64,7 +64,7 @@ ApplicationWindow {
}
Button {
text: "Join"
text: qsTr("Join")
enabled: input.text.match("#.+?:.{3,}")
DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
}
......
......@@ -150,6 +150,7 @@
<file>qml/device-verification/Success.qml</file>
<file>qml/device-verification/Waiting.qml</file>
<file>qml/dialogs/AliasEditor.qml</file>
<file>qml/dialogs/ConfirmJoinRoomDialog.qml</file>
<file>qml/dialogs/CreateDirect.qml</file>
<file>qml/dialogs/CreateRoom.qml</file>
<file>qml/dialogs/HiddenEventsDialog.qml</file>
......
......@@ -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> userSettings, QObject *parent)
connect(this,
&ChatPage::internalKnock,
this,
qOverload<const QString &, const std::vector<std::string> &, QString, bool>(
qOverload<const QString &, const std::vector<std::string> &, 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<std::string> &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)));
......
......@@ -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<std::string> &via,
QString reason = "",
bool failedJoin = false);
QString reason = "",
bool failedJoin = false,
bool promptForConfirmation = true);
void joinRoomVia(const std::string &room_id,
const std::vector<std::string> &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<std::string> &via,
QString reason = "",
bool failedJoin = false);
QString reason = "",
bool failedJoin = false,
bool promptForConfirmation = true);
private slots:
void logout();
......
......@@ -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<DeviceInfo>)
Q_DECLARE_METATYPE(std::vector<mtx::responses::PublicRoomsChunk>)
Q_DECLARE_METATYPE(mtx::responses::PublicRoom)
MainWindow *MainWindow::instance_ = nullptr;
......@@ -142,6 +144,7 @@ MainWindow::registerQmlTypes()
qRegisterMetaType<mtx::events::msg::KeyVerificationReady>();
qRegisterMetaType<mtx::events::msg::KeyVerificationRequest>();
qRegisterMetaType<mtx::events::msg::KeyVerificationStart>();
qRegisterMetaType<mtx::responses::PublicRoom>();
qRegisterMetaType<CombinedImagePackModel *>();
qRegisterMetaType<mtx::events::collections::TimelineEvents>();
qRegisterMetaType<std::vector<DeviceInfo>>();
......@@ -180,6 +183,12 @@ MainWindow::registerQmlTypes()
qmlRegisterType<LoginPage>("im.nheko", 1, 0, "Login");
qmlRegisterType<RegisterPage>("im.nheko", 1, 0, "Registration");
qmlRegisterType<HiddenEvents>("im.nheko", 1, 0, "HiddenEvents");
qmlRegisterUncreatableType<RoomSummary>(
"im.nheko",
1,
0,
"RoomSummary",
QStringLiteral("Please use joinRoom to create a room summary."));
qmlRegisterUncreatableType<AliasEditingModel>(
"im.nheko",
1,
......
......@@ -47,7 +47,7 @@ NotificationsManager::objCxxPostNotification(const QString &room_name,
const QString &informativeText,
const QString &bodyImagePath)
{
UNAuthorizationOptions options = UNAuthorizationOptionAlert + UNAuthorizationOptionSound;
UNAuthorizationOptions options = UNAuthorizationOptionAlert + UNAuthorizationOptionSound + UNAuthorizationOptionBadge;
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center requestAuthorizationWithOptions:options
......
......@@ -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);
}
......
......@@ -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<UserProfile> currentUser_;
};
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "RoomSummary.h"
#include <QMetaType>
#include "ChatPage.h"
#include "MatrixClient.h"
RoomSummary::RoomSummary(std::string roomIdOrAlias_,
std::vector<std::string> vias_,
QString r_,
QObject *p)
: QObject(p)
, roomIdOrAlias(std::move(roomIdOrAlias_))
, vias(std::move(vias_))
, reason_(std::move(r_))
{
auto ctx = std::make_shared<RoomSummaryProxy>();
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_);
}
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <optional>
#include <QObject>
#include <mtx/responses/public_rooms.hpp>
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<std::string> 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<std::string> vias;
std::optional<mtx::responses::PublicRoom> room;
QString reason_;
bool loaded_ = false;
};