diff --git a/src/Cache.cpp b/src/Cache.cpp
index 8cfc4b55..3a388bb9 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -1338,7 +1338,7 @@ Cache::getLastMessageInfo(lmdb::txn &txn, const std::string &room_id)
auto time = QDateTime::fromMSecsSinceEpoch(ts);
fallbackDesc = DescInfo{QString::fromStdString(obj["event"]["event_id"]),
local_user,
- tr("You joined this room"),
+ tr("You joined this room."),
utils::descriptiveTime(time),
ts,
time};
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 19339e59..bfefd7bb 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -666,7 +666,7 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
"This can have different reasons. Please open an "
"issue and try to use an older version in the mean "
"time. Alternatively you can try deleting the cache "
- "manually"));
+ "manually."));
QCoreApplication::quit();
}
loadStateFromCache();
@@ -994,8 +994,12 @@ ChatPage::trySync()
const auto err_code = mtx::errors::to_string(err->matrix_error.errcode);
const int status_code = static_cast<int>(err->status_code);
- if (http::is_logged_in() && err->matrix_error.errcode ==
- mtx::errors::ErrorCode::M_UNKNOWN_TOKEN) {
+ if ((http::is_logged_in() &&
+ (err->matrix_error.errcode ==
+ mtx::errors::ErrorCode::M_UNKNOWN_TOKEN ||
+ err->matrix_error.errcode ==
+ mtx::errors::ErrorCode::M_MISSING_TOKEN)) ||
+ !http::is_logged_in()) {
emit dropToLoginPageCb(msg);
return;
}
@@ -1086,7 +1090,7 @@ ChatPage::createRoom(const mtx::requests::CreateRoom &req)
}
emit showNotification(
- tr("Room %1 created").arg(QString::fromStdString(res.room_id.to_string())));
+ tr("Room %1 created.").arg(QString::fromStdString(res.room_id.to_string())));
});
}
diff --git a/src/LoginPage.cpp b/src/LoginPage.cpp
index 20fb3888..bb329699 100644
--- a/src/LoginPage.cpp
+++ b/src/LoginPage.cpp
@@ -15,28 +15,35 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+#include <QDesktopServices>
#include <QPainter>
#include <QStyleOption>
#include <mtx/identifiers.hpp>
+#include <mtx/requests.hpp>
#include <mtx/responses/login.hpp>
#include "Config.h"
#include "Logging.h"
#include "LoginPage.h"
#include "MatrixClient.h"
+#include "SSOHandler.h"
#include "ui/FlatButton.h"
#include "ui/LoadingIndicator.h"
#include "ui/OverlayModal.h"
#include "ui/RaisedButton.h"
#include "ui/TextField.h"
+Q_DECLARE_METATYPE(LoginPage::LoginMethod)
+
using namespace mtx::identifiers;
LoginPage::LoginPage(QWidget *parent)
: QWidget(parent)
, inferredServerAddress_()
{
+ qRegisterMetaType<LoginPage::LoginMethod>("LoginPage::LoginMethod");
+
top_layout_ = new QVBoxLayout();
top_bar_layout_ = new QHBoxLayout();
@@ -81,6 +88,14 @@ LoginPage::LoginPage(QWidget *parent)
matrixid_input_ = new TextField(this);
matrixid_input_->setLabel(tr("Matrix ID"));
matrixid_input_->setPlaceholderText(tr("e.g @joe:matrix.org"));
+ matrixid_input_->setToolTip(
+ tr("Your login name. A mxid should start with @ followed by the user id. After the user "
+ "id you need to include your server name after a :.\nYou can also put your homeserver "
+ "address there, if your server doesn't support .well-known lookup.\nExample: "
+ "@user:server.my\nIf Nheko fails to discover your homeserver, it will show you a "
+ "field to enter the server manually."));
+ matrixid_input_->setValidator(
+ new QRegularExpressionValidator(QRegularExpression("@.+?:.{3,}"), this));
spinner_ = new LoadingIndicator(this);
spinner_->setFixedHeight(40);
@@ -97,13 +112,19 @@ LoginPage::LoginPage(QWidget *parent)
password_input_ = new TextField(this);
password_input_->setLabel(tr("Password"));
password_input_->setEchoMode(QLineEdit::Password);
+ password_input_->setToolTip("Your password.");
deviceName_ = new TextField(this);
deviceName_->setLabel(tr("Device name"));
+ deviceName_->setToolTip(
+ tr("A name for this device, which will be shown to others, when verifying your devices. "
+ "If none is provided, a random string is used for privacy purposes."));
serverInput_ = new TextField(this);
serverInput_->setLabel("Homeserver address");
serverInput_->setPlaceholderText("matrix.org");
+ serverInput_->setToolTip(tr("The address that can be used to contact you homeservers "
+ "client API.\nExample: https://server.my:8787"));
serverInput_->hide();
serverLayout_ = new QHBoxLayout();
@@ -212,7 +233,8 @@ LoginPage::onMatrixIdEntered()
emit versionErrorCb(tr("Autodiscovery failed. Unknown error when "
"requesting .well-known."));
nhlog::net()->error("Autodiscovery failed. Unknown error when "
- "requesting .well-known.");
+ "requesting .well-known. {}",
+ err->error_code.message());
return;
}
@@ -249,7 +271,16 @@ LoginPage::checkHomeserverVersion()
return;
}
- emit versionOkCb();
+ http::client()->get_login(
+ [this](mtx::responses::LoginFlows flows, mtx::http::RequestErr err) {
+ if (err || flows.flows.empty())
+ emit versionOkCb(LoginMethod::Password);
+
+ if (flows.flows[0].type == mtx::user_interactive::auth_types::sso)
+ emit versionOkCb(LoginMethod::SSO);
+ else
+ emit versionOkCb(LoginMethod::Password);
+ });
});
}
@@ -280,12 +311,22 @@ LoginPage::versionError(const QString &error)
}
void
-LoginPage::versionOk()
+LoginPage::versionOk(LoginMethod loginMethod_)
{
+ this->loginMethod = loginMethod_;
+
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"));
+ }
+
if (serverInput_->isVisible())
serverInput_->hide();
}
@@ -303,29 +344,68 @@ LoginPage::onLoginButtonClicked()
return loginError("You have entered an invalid Matrix ID e.g @joe:matrix.org");
}
- if (password_input_->text().isEmpty())
- return loginError(tr("Empty password"));
+ if (loginMethod == LoginMethod::Password) {
+ if (password_input_->text().isEmpty())
+ return loginError(tr("Empty password"));
- http::client()->login(
- user.localpart(),
- password_input_->text().toStdString(),
- deviceName_->text().trimmed().isEmpty() ? initialDeviceName()
- : deviceName_->text().toStdString(),
- [this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
- if (err) {
- emit loginError(QString::fromStdString(err->matrix_error.error));
- emit errorOccurred();
- return;
- }
+ http::client()->login(
+ user.localpart(),
+ password_input_->text().toStdString(),
+ deviceName_->text().trimmed().isEmpty() ? initialDeviceName()
+ : deviceName_->text().toStdString(),
+ [this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
+ if (err) {
+ emit loginError(QString::fromStdString(err->matrix_error.error));
+ emit errorOccurred();
+ return;
+ }
- if (res.well_known) {
- http::client()->set_server(res.well_known->homeserver.base_url);
- nhlog::net()->info("Login requested to user server: " +
- res.well_known->homeserver.base_url);
- }
+ if (res.well_known) {
+ http::client()->set_server(res.well_known->homeserver.base_url);
+ nhlog::net()->info("Login requested to user server: " +
+ res.well_known->homeserver.base_url);
+ }
- emit loginOk(res);
- });
+ emit loginOk(res);
+ });
+ } else {
+ auto sso = new SSOHandler();
+ connect(sso, &SSOHandler::ssoSuccess, this, [this, sso](std::string token) {
+ mtx::requests::Login req{};
+ req.token = token;
+ req.type = mtx::user_interactive::auth_types::token;
+ req.device_id = deviceName_->text().trimmed().isEmpty()
+ ? initialDeviceName()
+ : deviceName_->text().toStdString();
+ http::client()->login(
+ req, [this](const mtx::responses::Login &res, mtx::http::RequestErr err) {
+ if (err) {
+ emit loginError(
+ QString::fromStdString(err->matrix_error.error));
+ emit errorOccurred();
+ return;
+ }
+
+ if (res.well_known) {
+ http::client()->set_server(
+ res.well_known->homeserver.base_url);
+ nhlog::net()->info("Login requested to user server: " +
+ res.well_known->homeserver.base_url);
+ }
+
+ emit loginOk(res);
+ });
+ sso->deleteLater();
+ });
+ connect(sso, &SSOHandler::ssoFailed, this, [this, sso]() {
+ emit loginError(tr("SSO login failed"));
+ emit errorOccurred();
+ sso->deleteLater();
+ });
+
+ QDesktopServices::openUrl(
+ QString::fromStdString(http::client()->login_sso_redirect(sso->url())));
+ }
emit loggingIn();
}
@@ -335,6 +415,7 @@ LoginPage::reset()
{
matrixid_input_->clear();
password_input_->clear();
+ password_input_->show();
serverInput_->clear();
spinner_->stop();
diff --git a/src/LoginPage.h b/src/LoginPage.h
index 4b84abfc..8a402aea 100644
--- a/src/LoginPage.h
+++ b/src/LoginPage.h
@@ -38,6 +38,12 @@ class LoginPage : public QWidget
Q_OBJECT
public:
+ enum class LoginMethod
+ {
+ Password,
+ SSO,
+ };
+
LoginPage(QWidget *parent = nullptr);
void reset();
@@ -50,7 +56,7 @@ signals:
//! Used to trigger the corresponding slot outside of the main thread.
void versionErrorCb(const QString &err);
void loginErrorCb(const QString &err);
- void versionOkCb();
+ void versionOkCb(LoginPage::LoginMethod method);
void loginOk(const mtx::responses::Login &res);
@@ -77,7 +83,7 @@ private slots:
// Callback for errors produced during server probing
void versionError(const QString &error_message);
// Callback for successful server probing
- void versionOk();
+ void versionOk(LoginPage::LoginMethod method);
private:
bool isMatrixIdValid();
@@ -123,4 +129,5 @@ private:
TextField *password_input_;
TextField *deviceName_;
TextField *serverInput_;
+ LoginMethod loginMethod = LoginMethod::Password;
};
diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp
index 2833381d..03e9ab34 100644
--- a/src/RegisterPage.cpp
+++ b/src/RegisterPage.cpp
@@ -85,17 +85,26 @@ RegisterPage::RegisterPage(QWidget *parent)
username_input_ = new TextField();
username_input_->setLabel(tr("Username"));
+ username_input_->setValidator(
+ new QRegularExpressionValidator(QRegularExpression("[a-z0-9._=/-]+"), this));
+ username_input_->setToolTip(tr("The username must not be empty, and must contain only the "
+ "characters a-z, 0-9, ., _, =, -, and /."));
password_input_ = new TextField();
password_input_->setLabel(tr("Password"));
password_input_->setEchoMode(QLineEdit::Password);
+ password_input_->setToolTip(tr("Please choose a secure password. The exact requirements "
+ "for password strength may depend on your server."));
password_confirmation_ = new TextField();
password_confirmation_->setLabel(tr("Password confirmation"));
password_confirmation_->setEchoMode(QLineEdit::Password);
server_input_ = new TextField();
- server_input_->setLabel(tr("Home Server"));
+ server_input_->setLabel(tr("Homeserver"));
+ server_input_->setToolTip(
+ tr("A server that allows registration. Since matrix is decentralized, you need to first "
+ "find a server you can register on or host your own."));
form_layout_->addWidget(username_input_, Qt::AlignHCenter, nullptr);
form_layout_->addWidget(password_input_, Qt::AlignHCenter, nullptr);
diff --git a/src/SSOHandler.cpp b/src/SSOHandler.cpp
new file mode 100644
index 00000000..cacbbaa9
--- /dev/null
+++ b/src/SSOHandler.cpp
@@ -0,0 +1,53 @@
+#include "SSOHandler.h"
+
+#include <QTimer>
+
+#include <thread>
+
+#include "Logging.h"
+
+SSOHandler::SSOHandler(QObject *)
+{
+ QTimer::singleShot(120000, this, &SSOHandler::ssoFailed);
+
+ using namespace httplib;
+
+ svr.set_logger([](const Request &req, const Response &res) {
+ nhlog::net()->info("req: {}, res: {}", req.path, res.status);
+ });
+
+ svr.Get("/sso", [this](const Request &req, Response &res) {
+ if (req.has_param("loginToken")) {
+ auto val = req.get_param_value("loginToken");
+ res.set_content("SSO success", "text/plain");
+ emit ssoSuccess(val);
+ } else {
+ res.set_content("Missing loginToken for SSO login!", "text/plain");
+ emit ssoFailed();
+ }
+ });
+
+ std::thread t([this]() {
+ this->port = svr.bind_to_any_port("localhost");
+ svr.listen_after_bind();
+ });
+ t.detach();
+
+ while (!svr.is_running()) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
+ }
+}
+
+SSOHandler::~SSOHandler()
+{
+ svr.stop();
+ while (svr.is_running()) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
+ }
+}
+
+std::string
+SSOHandler::url() const
+{
+ return "http://localhost:" + std::to_string(port) + "/sso";
+}
diff --git a/src/SSOHandler.h b/src/SSOHandler.h
new file mode 100644
index 00000000..325b7a58
--- /dev/null
+++ b/src/SSOHandler.h
@@ -0,0 +1,24 @@
+#include "httplib.h"
+
+#include <QObject>
+#include <string>
+
+class SSOHandler : public QObject
+{
+ Q_OBJECT
+
+public:
+ SSOHandler(QObject *parent = nullptr);
+
+ ~SSOHandler();
+
+ std::string url() const;
+
+signals:
+ void ssoSuccess(std::string token);
+ void ssoFailed();
+
+private:
+ httplib::Server svr;
+ int port = 0;
+};
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 4068148b..f8334d9b 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -224,6 +224,7 @@ TimelineModel::roleNames() const
{Id, "id"},
{State, "state"},
{IsEncrypted, "isEncrypted"},
+ {IsRoomEncrypted, "isRoomEncrypted"},
{ReplyTo, "replyTo"},
{Reactions, "reactions"},
{RoomId, "roomId"},
@@ -294,6 +295,10 @@ TimelineModel::data(const QString &id, int role) const
if (isReply)
formattedBody_ = formattedBody_.remove(replyFallback);
}
+
+ formattedBody_.replace("<img src=\"mxc://", "<img src=\"image://mxcImage/");
+ formattedBody_.replace("<img src=\"mxc://", "<img src=\"image://mxcImage/");
+
return QVariant(utils::replaceEmoji(
utils::linkifyMessage(utils::escapeBlacklistedHtml(formattedBody_))));
}
@@ -346,6 +351,9 @@ TimelineModel::data(const QString &id, int role) const
return std::holds_alternative<
mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(events[id]);
}
+ case IsRoomEncrypted: {
+ return cache::isRoomEncrypted(room_id_.toStdString());
+ }
case ReplyTo:
return QVariant(QString::fromStdString(in_reply_to_event(event)));
case Reactions:
@@ -383,6 +391,7 @@ TimelineModel::data(const QString &id, int role) const
m.insert(names[Id], data(id, static_cast<int>(Id)));
m.insert(names[State], data(id, static_cast<int>(State)));
m.insert(names[IsEncrypted], data(id, static_cast<int>(IsEncrypted)));
+ m.insert(names[IsRoomEncrypted], data(id, static_cast<int>(IsRoomEncrypted)));
m.insert(names[ReplyTo], data(id, static_cast<int>(ReplyTo)));
m.insert(names[RoomName], data(id, static_cast<int>(RoomName)));
m.insert(names[RoomTopic], data(id, static_cast<int>(RoomTopic)));
@@ -566,7 +575,7 @@ TimelineModel::updateLastMessage()
room_id_,
DescInfo{QString::fromStdString(mtx::accessors::event_id(event)),
QString::fromStdString(http::client()->user_id().to_string()),
- tr("You joined this room"),
+ tr("You joined this room."),
utils::descriptiveTime(time),
ts,
time});
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 65f6b38c..ea7eaffd 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -159,6 +159,7 @@ public:
Id,
State,
IsEncrypted,
+ IsRoomEncrypted,
ReplyTo,
Reactions,
RoomId,
diff --git a/src/ui/TextField.cpp b/src/ui/TextField.cpp
index 4bb7596a..27584693 100644
--- a/src/ui/TextField.cpp
+++ b/src/ui/TextField.cpp
@@ -147,7 +147,10 @@ QColor
TextField::underlineColor() const
{
if (!underline_color_.isValid()) {
- return QPalette().color(QPalette::Highlight);
+ if (hasAcceptableInput() || !isModified())
+ return QPalette().color(QPalette::Highlight);
+ else
+ return Qt::red;
}
return underline_color_;
|