summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/ChatPage.cc12
-rw-r--r--src/ImageItem.cc216
-rw-r--r--src/LoginPage.cc439
-rw-r--r--src/MainWindow.cc278
-rw-r--r--src/MatrixClient.cc109
-rw-r--r--src/TextInputWidget.cc106
-rw-r--r--src/TimelineItem.cc33
-rw-r--r--src/TimelineView.cc67
-rw-r--r--src/TimelineViewManager.cc18
-rw-r--r--src/ui/CircularProgress.cc201
-rw-r--r--src/ui/LoadingIndicator.cc86
11 files changed, 869 insertions, 696 deletions
diff --git a/src/ChatPage.cc b/src/ChatPage.cc

index d393a65d..6bfbf400 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc
@@ -153,6 +153,18 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client, QWidget *parent) view_manager_, SLOT(sendEmoteMessage(const QString &))); + connect(text_input_, &TextInputWidget::uploadImage, this, [=](QString filename) { + client_->uploadImage(current_room_, filename); + }); + + connect(client_.data(), + &MatrixClient::imageUploaded, + this, + [=](QString roomid, QString filename, QString url) { + text_input_->hideUploadSpinner(); + view_manager_->sendImageMessage(roomid, filename, url); + }); + connect(client_.data(), SIGNAL(roomAvatarRetrieved(const QString &, const QPixmap &)), this, diff --git a/src/ImageItem.cc b/src/ImageItem.cc
index 77523465..e84a2a9f 100644 --- a/src/ImageItem.cc +++ b/src/ImageItem.cc
@@ -18,6 +18,7 @@ #include <QBrush> #include <QDebug> #include <QDesktopServices> +#include <QFileInfo> #include <QImage> #include <QPainter> #include <QPixmap> @@ -26,169 +27,198 @@ #include "ImageOverlayDialog.h" namespace events = matrix::events; -namespace msgs = matrix::events::messages; +namespace msgs = matrix::events::messages; ImageItem::ImageItem(QSharedPointer<MatrixClient> client, - const events::MessageEvent<msgs::Image> &event, - QWidget *parent) + const events::MessageEvent<msgs::Image> &event, + QWidget *parent) : QWidget(parent) , event_{ event } , client_{ client } { - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - setAttribute(Qt::WA_Hover, true); + setMouseTracking(true); + setCursor(Qt::PointingHandCursor); + setAttribute(Qt::WA_Hover, true); - url_ = event.msgContent().url(); - text_ = event.content().body(); + url_ = event.msgContent().url(); + text_ = event.content().body(); - QList<QString> url_parts = url_.toString().split("mxc://"); + QList<QString> url_parts = url_.toString().split("mxc://"); - if (url_parts.size() != 2) { - qDebug() << "Invalid format for image" << url_.toString(); - return; - } + if (url_parts.size() != 2) { + qDebug() << "Invalid format for image" << url_.toString(); + return; + } - QString media_params = url_parts[1]; - url_ = QString("%1/_matrix/media/r0/download/%2").arg(client_.data()->getHomeServer().toString(), media_params); + QString media_params = url_parts[1]; + url_ = QString("%1/_matrix/media/r0/download/%2") + .arg(client_.data()->getHomeServer().toString(), media_params); - client_.data()->downloadImage(event.eventId(), url_); + client_.data()->downloadImage(event.eventId(), url_); - connect(client_.data(), - SIGNAL(imageDownloaded(const QString &, const QPixmap &)), - this, - SLOT(imageDownloaded(const QString &, const QPixmap &))); + connect(client_.data(), + SIGNAL(imageDownloaded(const QString &, const QPixmap &)), + this, + SLOT(imageDownloaded(const QString &, const QPixmap &))); +} + +ImageItem::ImageItem(QSharedPointer<MatrixClient> client, + const QString &url, + const QString &filename, + QWidget *parent) + : QWidget(parent) + , url_{ url } + , text_{ QFileInfo(filename).fileName() } + , client_{ client } +{ + setMouseTracking(true); + setCursor(Qt::PointingHandCursor); + setAttribute(Qt::WA_Hover, true); + + QList<QString> url_parts = url_.toString().split("mxc://"); + + if (url_parts.size() != 2) { + qDebug() << "Invalid format for image" << url_.toString(); + return; + } + + QString media_params = url_parts[1]; + url_ = QString("%1/_matrix/media/r0/download/%2") + .arg(client_.data()->getHomeServer().toString(), media_params); + + setImage(QPixmap(filename)); } void ImageItem::imageDownloaded(const QString &event_id, const QPixmap &img) { - if (event_id != event_.eventId()) - return; + if (event_id != event_.eventId()) + return; - setImage(img); + setImage(img); } void ImageItem::openUrl() { - if (url_.toString().isEmpty()) - return; + if (url_.toString().isEmpty()) + return; - if (!QDesktopServices::openUrl(url_)) - qWarning() << "Could not open url" << url_.toString(); + if (!QDesktopServices::openUrl(url_)) + qWarning() << "Could not open url" << url_.toString(); } void ImageItem::scaleImage() { - if (image_.isNull()) - return; + if (image_.isNull()) + return; - auto width_ratio = (double)max_width_ / (double)image_.width(); - auto height_ratio = (double)max_height_ / (double)image_.height(); + auto width_ratio = (double)max_width_ / (double)image_.width(); + auto height_ratio = (double)max_height_ / (double)image_.height(); - auto min_aspect_ratio = std::min(width_ratio, height_ratio); + auto min_aspect_ratio = std::min(width_ratio, height_ratio); - if (min_aspect_ratio > 1) { - width_ = image_.width(); - height_ = image_.height(); - } else { - width_ = image_.width() * min_aspect_ratio; - height_ = image_.height() * min_aspect_ratio; - } + if (min_aspect_ratio > 1) { + width_ = image_.width(); + height_ = image_.height(); + } else { + width_ = image_.width() * min_aspect_ratio; + height_ = image_.height() * min_aspect_ratio; + } - setFixedSize(width_, height_); - scaled_image_ = image_.scaled(width_, height_, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + setFixedSize(width_, height_); + scaled_image_ = + image_.scaled(width_, height_, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); } QSize ImageItem::sizeHint() const { - if (image_.isNull()) - return QSize(max_width_, bottom_height_); + if (image_.isNull()) + return QSize(max_width_, bottom_height_); - return QSize(width_, height_); + return QSize(width_, height_); } void ImageItem::setImage(const QPixmap &image) { - image_ = image; - scaleImage(); - update(); + image_ = image; + scaleImage(); + update(); } void ImageItem::mousePressEvent(QMouseEvent *event) { - if (event->button() != Qt::LeftButton) - return; - - if (image_.isNull()) { - openUrl(); - return; - } - - auto point = event->pos(); - - // Click on the text box. - if (QRect(0, height_ - bottom_height_, width_, bottom_height_).contains(point)) { - openUrl(); - } else { - auto image_dialog = new ImageOverlayDialog(image_, this); - image_dialog->show(); - } + if (event->button() != Qt::LeftButton) + return; + + if (image_.isNull()) { + openUrl(); + return; + } + + auto point = event->pos(); + + // Click on the text box. + if (QRect(0, height_ - bottom_height_, width_, bottom_height_).contains(point)) { + openUrl(); + } else { + auto image_dialog = new ImageOverlayDialog(image_, this); + image_dialog->show(); + } } void ImageItem::resizeEvent(QResizeEvent *event) { - Q_UNUSED(event); + Q_UNUSED(event); - scaleImage(); + scaleImage(); } void ImageItem::paintEvent(QPaintEvent *event) { - Q_UNUSED(event); + Q_UNUSED(event); - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); - QFont font("Open Sans"); - font.setPixelSize(12); + QFont font("Open Sans"); + font.setPixelSize(12); - QFontMetrics metrics(font); - int fontHeight = metrics.height(); + QFontMetrics metrics(font); + int fontHeight = metrics.height(); - if (image_.isNull()) { - int height = fontHeight + 10; + if (image_.isNull()) { + int height = fontHeight + 10; - QString elidedText = metrics.elidedText(text_, Qt::ElideRight, max_width_ - 10); + QString elidedText = metrics.elidedText(text_, Qt::ElideRight, max_width_ - 10); - setFixedSize(metrics.width(elidedText), fontHeight + 10); + setFixedSize(metrics.width(elidedText), fontHeight + 10); - painter.setFont(font); - painter.setPen(QPen(QColor(66, 133, 244))); - painter.drawText(QPoint(0, height / 2 + 2), elidedText); + painter.setFont(font); + painter.setPen(QPen(QColor(66, 133, 244))); + painter.drawText(QPoint(0, height / 2 + 2), elidedText); - return; - } + return; + } - painter.fillRect(QRect(0, 0, width_, height_), scaled_image_); + painter.fillRect(QRect(0, 0, width_, height_), scaled_image_); - if (underMouse()) { - // Bottom text section - painter.fillRect(QRect(0, height_ - bottom_height_, width_, bottom_height_), - QBrush(QColor(33, 33, 33, 128))); + if (underMouse()) { + // Bottom text section + painter.fillRect(QRect(0, height_ - bottom_height_, width_, bottom_height_), + QBrush(QColor(33, 33, 33, 128))); - QString elidedText = metrics.elidedText(text_, Qt::ElideRight, width_ - 10); + QString elidedText = metrics.elidedText(text_, Qt::ElideRight, width_ - 10); - font.setWeight(500); - painter.setFont(font); - painter.setPen(QPen(QColor("white"))); - painter.drawText(QPoint(5, height_ - fontHeight / 2), elidedText); - } + font.setWeight(500); + painter.setFont(font); + painter.setPen(QPen(QColor("white"))); + painter.drawText(QPoint(5, height_ - fontHeight / 2), elidedText); + } } diff --git a/src/LoginPage.cc b/src/LoginPage.cc
index c4048f98..87485878 100644 --- a/src/LoginPage.cc +++ b/src/LoginPage.cc
@@ -26,274 +26,275 @@ LoginPage::LoginPage(QSharedPointer<MatrixClient> client, QWidget *parent) , inferredServerAddress_() , client_{ client } { - setStyleSheet("background-color: #f9f9f9"); - - top_layout_ = new QVBoxLayout(); - - top_bar_layout_ = new QHBoxLayout(); - top_bar_layout_->setSpacing(0); - top_bar_layout_->setMargin(0); - - back_button_ = new FlatButton(this); - back_button_->setMinimumSize(QSize(30, 30)); - back_button_->setForegroundColor("#333333"); - - top_bar_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter); - top_bar_layout_->addStretch(1); - - QIcon icon; - icon.addFile(":/icons/icons/left-angle.png", QSize(), QIcon::Normal, QIcon::Off); - - back_button_->setIcon(icon); - back_button_->setIconSize(QSize(24, 24)); - - QIcon advanced_settings_icon; - advanced_settings_icon.addFile(":/icons/icons/cog.png", QSize(), QIcon::Normal, QIcon::Off); - - logo_ = new QLabel(this); - logo_->setPixmap(QPixmap(":/logos/nheko-128.png")); - - logo_layout_ = new QHBoxLayout(); - logo_layout_->setContentsMargins(0, 0, 0, 20); - logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter); - - form_wrapper_ = new QHBoxLayout(); - form_widget_ = new QWidget(); - form_widget_->setMinimumSize(QSize(350, 200)); - - form_layout_ = new QVBoxLayout(); - form_layout_->setSpacing(20); - form_layout_->setContentsMargins(0, 0, 0, 30); - form_widget_->setLayout(form_layout_); - - form_wrapper_->addStretch(1); - form_wrapper_->addWidget(form_widget_); - form_wrapper_->addStretch(1); - - matrixid_input_ = new TextField(this); - matrixid_input_->setTextColor("#333333"); - matrixid_input_->setLabel(tr("Matrix ID")); - matrixid_input_->setInkColor("#555459"); - matrixid_input_->setBackgroundColor("#f9f9f9"); - matrixid_input_->setPlaceholderText(tr("e.g @joe:matrix.org")); - - spinner_ = new CircularProgress(this); - spinner_->setColor("#acc7dc"); - spinner_->setSize(32); - spinner_->setMaximumWidth(spinner_->width()); - spinner_->hide(); - - errorIcon_ = new QLabel(this); - errorIcon_->setPixmap(QPixmap(":/icons/icons/error.png")); - errorIcon_->hide(); - - matrixidLayout_ = new QHBoxLayout(); - matrixidLayout_->addWidget(matrixid_input_, 0, Qt::AlignVCenter); - - password_input_ = new TextField(this); - password_input_->setTextColor("#333333"); - password_input_->setLabel(tr("Password")); - password_input_->setInkColor("#555459"); - password_input_->setBackgroundColor("#f9f9f9"); - password_input_->setEchoMode(QLineEdit::Password); - - serverInput_ = new TextField(this); - serverInput_->setTextColor("#333333"); - serverInput_->setLabel("Homeserver address"); - serverInput_->setInkColor("#555459"); - serverInput_->setBackgroundColor("#f9f9f9"); - serverInput_->setPlaceholderText("matrix.org"); - serverInput_->hide(); - - serverLayout_ = new QHBoxLayout(); - serverLayout_->addWidget(serverInput_, 0, Qt::AlignVCenter); - - form_layout_->addLayout(matrixidLayout_); - form_layout_->addWidget(password_input_, Qt::AlignHCenter, 0); - form_layout_->addLayout(serverLayout_); - - button_layout_ = new QHBoxLayout(); - button_layout_->setSpacing(0); - button_layout_->setContentsMargins(0, 0, 0, 30); - - login_button_ = new RaisedButton(tr("LOGIN"), this); - login_button_->setBackgroundColor(QColor("#333333")); - login_button_->setForegroundColor(QColor("white")); - login_button_->setMinimumSize(350, 65); - login_button_->setFontSize(20); - login_button_->setCornerRadius(3); - - button_layout_->addStretch(1); - button_layout_->addWidget(login_button_); - button_layout_->addStretch(1); - - QFont font; - font.setPixelSize(conf::fontSize); - - error_label_ = new QLabel(this); - error_label_->setFont(font); - error_label_->setStyleSheet("color: #E22826"); - - top_layout_->addLayout(top_bar_layout_); - top_layout_->addStretch(1); - top_layout_->addLayout(logo_layout_); - top_layout_->addLayout(form_wrapper_); - top_layout_->addStretch(1); - top_layout_->addLayout(button_layout_); - top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter); - top_layout_->addStretch(1); - - setLayout(top_layout_); - - connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); - connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked())); - connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); - connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); - connect(serverInput_, SIGNAL(returnPressed()), login_button_, SLOT(click())); - connect(client_.data(), SIGNAL(loginError(QString)), this, SLOT(loginError(QString))); - connect(matrixid_input_, SIGNAL(editingFinished()), this, SLOT(onMatrixIdEntered())); - connect(client_.data(), SIGNAL(versionError(QString)), this, SLOT(versionError(QString))); - connect(client_.data(), SIGNAL(versionSuccess()), this, SLOT(versionSuccess())); - connect(serverInput_, SIGNAL(editingFinished()), this, SLOT(onServerAddressEntered())); + setStyleSheet("background-color: #f9f9f9"); + + top_layout_ = new QVBoxLayout(); + + top_bar_layout_ = new QHBoxLayout(); + top_bar_layout_->setSpacing(0); + top_bar_layout_->setMargin(0); + + back_button_ = new FlatButton(this); + back_button_->setMinimumSize(QSize(30, 30)); + back_button_->setForegroundColor("#333333"); + + top_bar_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter); + top_bar_layout_->addStretch(1); + + QIcon icon; + icon.addFile(":/icons/icons/left-angle.png", QSize(), QIcon::Normal, QIcon::Off); + + back_button_->setIcon(icon); + back_button_->setIconSize(QSize(24, 24)); + + QIcon advanced_settings_icon; + advanced_settings_icon.addFile(":/icons/icons/cog.png", QSize(), QIcon::Normal, QIcon::Off); + + logo_ = new QLabel(this); + logo_->setPixmap(QPixmap(":/logos/nheko-128.png")); + + logo_layout_ = new QHBoxLayout(); + logo_layout_->setContentsMargins(0, 0, 0, 20); + logo_layout_->addWidget(logo_, 0, Qt::AlignHCenter); + + form_wrapper_ = new QHBoxLayout(); + form_widget_ = new QWidget(); + form_widget_->setMinimumSize(QSize(350, 200)); + + form_layout_ = new QVBoxLayout(); + form_layout_->setSpacing(20); + form_layout_->setContentsMargins(0, 0, 0, 30); + form_widget_->setLayout(form_layout_); + + form_wrapper_->addStretch(1); + form_wrapper_->addWidget(form_widget_); + form_wrapper_->addStretch(1); + + matrixid_input_ = new TextField(this); + matrixid_input_->setTextColor("#333333"); + matrixid_input_->setLabel(tr("Matrix ID")); + matrixid_input_->setInkColor("#555459"); + matrixid_input_->setBackgroundColor("#f9f9f9"); + matrixid_input_->setPlaceholderText(tr("e.g @joe:matrix.org")); + + spinner_ = new LoadingIndicator(this); + spinner_->setColor("#acc7dc"); + spinner_->setFixedHeight(40); + spinner_->setFixedWidth(40); + spinner_->hide(); + + errorIcon_ = new QLabel(this); + errorIcon_->setPixmap(QPixmap(":/icons/icons/error.png")); + errorIcon_->hide(); + + matrixidLayout_ = new QHBoxLayout(); + matrixidLayout_->addWidget(matrixid_input_, 0, Qt::AlignVCenter); + + password_input_ = new TextField(this); + password_input_->setTextColor("#333333"); + password_input_->setLabel(tr("Password")); + password_input_->setInkColor("#555459"); + password_input_->setBackgroundColor("#f9f9f9"); + password_input_->setEchoMode(QLineEdit::Password); + + serverInput_ = new TextField(this); + serverInput_->setTextColor("#333333"); + serverInput_->setLabel("Homeserver address"); + serverInput_->setInkColor("#555459"); + serverInput_->setBackgroundColor("#f9f9f9"); + serverInput_->setPlaceholderText("matrix.org"); + serverInput_->hide(); + + serverLayout_ = new QHBoxLayout(); + serverLayout_->addWidget(serverInput_, 0, Qt::AlignVCenter); + + form_layout_->addLayout(matrixidLayout_); + form_layout_->addWidget(password_input_, Qt::AlignHCenter, 0); + form_layout_->addLayout(serverLayout_); + + button_layout_ = new QHBoxLayout(); + button_layout_->setSpacing(0); + button_layout_->setContentsMargins(0, 0, 0, 30); + + login_button_ = new RaisedButton(tr("LOGIN"), this); + login_button_->setBackgroundColor(QColor("#333333")); + login_button_->setForegroundColor(QColor("white")); + login_button_->setMinimumSize(350, 65); + login_button_->setFontSize(20); + login_button_->setCornerRadius(3); + + button_layout_->addStretch(1); + button_layout_->addWidget(login_button_); + button_layout_->addStretch(1); + + QFont font; + font.setPixelSize(conf::fontSize); + + error_label_ = new QLabel(this); + error_label_->setFont(font); + error_label_->setStyleSheet("color: #E22826"); + + top_layout_->addLayout(top_bar_layout_); + top_layout_->addStretch(1); + top_layout_->addLayout(logo_layout_); + top_layout_->addLayout(form_wrapper_); + top_layout_->addStretch(1); + top_layout_->addLayout(button_layout_); + top_layout_->addWidget(error_label_, 0, Qt::AlignHCenter); + top_layout_->addStretch(1); + + setLayout(top_layout_); + + connect(back_button_, SIGNAL(clicked()), this, SLOT(onBackButtonClicked())); + connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked())); + connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); + connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); + connect(serverInput_, SIGNAL(returnPressed()), login_button_, SLOT(click())); + connect(client_.data(), SIGNAL(loginError(QString)), this, SLOT(loginError(QString))); + connect(matrixid_input_, SIGNAL(editingFinished()), this, SLOT(onMatrixIdEntered())); + connect(client_.data(), SIGNAL(versionError(QString)), this, SLOT(versionError(QString))); + connect(client_.data(), SIGNAL(versionSuccess()), this, SLOT(versionSuccess())); + connect(serverInput_, SIGNAL(editingFinished()), this, SLOT(onServerAddressEntered())); } void LoginPage::loginError(QString error) { - error_label_->setText(error); + error_label_->setText(error); } bool LoginPage::isMatrixIdValid() { - int pos = 0; - auto matrix_id = matrixid_input_->text(); + int pos = 0; + auto matrix_id = matrixid_input_->text(); - return InputValidator::Id.validate(matrix_id, pos) == QValidator::Acceptable; + return InputValidator::Id.validate(matrix_id, pos) == QValidator::Acceptable; } void LoginPage::onMatrixIdEntered() { - error_label_->setText(""); - - if (!isMatrixIdValid()) { - loginError("You have entered an invalid Matrix ID e.g @joe:matrix.org"); - return; - } else if (password_input_->text().isEmpty()) { - loginError(tr("Empty password")); - } - - QString homeServer = matrixid_input_->text().split(":").at(1); - if (homeServer != inferredServerAddress_) { - serverInput_->hide(); - serverLayout_->removeWidget(errorIcon_); - errorIcon_->hide(); - if (serverInput_->isVisible()) { - matrixidLayout_->removeWidget(spinner_); - serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); - spinner_->show(); - } else { - serverLayout_->removeWidget(spinner_); - matrixidLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); - spinner_->show(); - } - - inferredServerAddress_ = homeServer; - serverInput_->setText(homeServer); - client_->setServer(homeServer); - client_->versions(); - } + error_label_->setText(""); + + if (!isMatrixIdValid()) { + loginError("You have entered an invalid Matrix ID e.g @joe:matrix.org"); + return; + } else if (password_input_->text().isEmpty()) { + loginError(tr("Empty password")); + } + + QString homeServer = matrixid_input_->text().split(":").at(1); + if (homeServer != inferredServerAddress_) { + serverInput_->hide(); + serverLayout_->removeWidget(errorIcon_); + errorIcon_->hide(); + if (serverInput_->isVisible()) { + matrixidLayout_->removeWidget(spinner_); + serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); + spinner_->start(); + } else { + serverLayout_->removeWidget(spinner_); + matrixidLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); + spinner_->start(); + } + + inferredServerAddress_ = homeServer; + serverInput_->setText(homeServer); + client_->setServer(homeServer); + client_->versions(); + } } void LoginPage::onServerAddressEntered() { - error_label_->setText(""); - client_->setServer(serverInput_->text()); - client_->versions(); - - serverLayout_->removeWidget(errorIcon_); - errorIcon_->hide(); - serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); - spinner_->show(); + error_label_->setText(""); + client_->setServer(serverInput_->text()); + client_->versions(); + + serverLayout_->removeWidget(errorIcon_); + errorIcon_->hide(); + serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); + spinner_->start(); } void LoginPage::versionError(QString error) { - // Matrix homeservers are often kept on a subdomain called 'matrix' - // so let's try that next, unless the address was set explicitly or the domain part of the username already - // points to this subdomain - QUrl currentServer = client_->getHomeServer(); - QString mxidAddress = matrixid_input_->text().split(":").at(1); - if (currentServer.host() == inferredServerAddress_ && !currentServer.host().startsWith("matrix")) { - error_label_->setText(""); - currentServer.setHost(QString("matrix.") + currentServer.host()); - serverInput_->setText(currentServer.host()); - client_->setServer(currentServer.host()); - client_->versions(); - return; - } - - error_label_->setText(error); - serverInput_->show(); - - spinner_->hide(); - serverLayout_->removeWidget(spinner_); - serverLayout_->addWidget(errorIcon_, 0, Qt::AlignVCenter | Qt::AlignRight); - errorIcon_->show(); - matrixidLayout_->removeWidget(spinner_); + // Matrix homeservers are often kept on a subdomain called 'matrix' + // so let's try that next, unless the address was set explicitly or the domain part of the + // username already points to this subdomain + QUrl currentServer = client_->getHomeServer(); + QString mxidAddress = matrixid_input_->text().split(":").at(1); + if (currentServer.host() == inferredServerAddress_ && + !currentServer.host().startsWith("matrix")) { + error_label_->setText(""); + currentServer.setHost(QString("matrix.") + currentServer.host()); + serverInput_->setText(currentServer.host()); + client_->setServer(currentServer.host()); + client_->versions(); + return; + } + + error_label_->setText(error); + serverInput_->show(); + + spinner_->stop(); + serverLayout_->removeWidget(spinner_); + serverLayout_->addWidget(errorIcon_, 0, Qt::AlignVCenter | Qt::AlignRight); + errorIcon_->show(); + matrixidLayout_->removeWidget(spinner_); } void LoginPage::versionSuccess() { - serverLayout_->removeWidget(spinner_); - matrixidLayout_->removeWidget(spinner_); - spinner_->hide(); + serverLayout_->removeWidget(spinner_); + matrixidLayout_->removeWidget(spinner_); + spinner_->stop(); - if (serverInput_->isVisible()) - serverInput_->hide(); + if (serverInput_->isVisible()) + serverInput_->hide(); } void LoginPage::onLoginButtonClicked() { - error_label_->setText(""); - - if (!isMatrixIdValid()) { - loginError("You have entered an invalid Matrix ID e.g @joe:matrix.org"); - } else if (password_input_->text().isEmpty()) { - loginError("Empty password"); - } else { - QString user = matrixid_input_->text().split(":").at(0).split("@").at(1); - QString password = password_input_->text(); - client_->setServer(serverInput_->text()); - client_->login(user, password); - } + error_label_->setText(""); + + if (!isMatrixIdValid()) { + loginError("You have entered an invalid Matrix ID e.g @joe:matrix.org"); + } else if (password_input_->text().isEmpty()) { + loginError("Empty password"); + } else { + QString user = matrixid_input_->text().split(":").at(0).split("@").at(1); + QString password = password_input_->text(); + client_->setServer(serverInput_->text()); + client_->login(user, password); + } } void LoginPage::reset() { - matrixid_input_->clear(); - password_input_->clear(); - serverInput_->clear(); + matrixid_input_->clear(); + password_input_->clear(); + serverInput_->clear(); - spinner_->hide(); - errorIcon_->hide(); - serverLayout_->removeWidget(spinner_); - serverLayout_->removeWidget(errorIcon_); - matrixidLayout_->removeWidget(spinner_); + spinner_->stop(); + errorIcon_->hide(); + serverLayout_->removeWidget(spinner_); + serverLayout_->removeWidget(errorIcon_); + matrixidLayout_->removeWidget(spinner_); - inferredServerAddress_.clear(); + inferredServerAddress_.clear(); } void LoginPage::onBackButtonClicked() { - emit backButtonClicked(); + emit backButtonClicked(); } LoginPage::~LoginPage() diff --git a/src/MainWindow.cc b/src/MainWindow.cc
index 1567e8ba..c76657d7 100644 --- a/src/MainWindow.cc +++ b/src/MainWindow.cc
@@ -15,8 +15,8 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#include "MainWindow.h" #include "Config.h" +#include "MainWindow.h" #include <QLayout> #include <QNetworkReply> @@ -30,217 +30,225 @@ MainWindow::MainWindow(QWidget *parent) , progress_modal_{ nullptr } , spinner_{ nullptr } { - QSizePolicy sizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); - setSizePolicy(sizePolicy); - setWindowTitle("nheko"); - setObjectName("MainWindow"); - setStyleSheet("QWidget#MainWindow {background-color: #f9f9f9}"); + QSizePolicy sizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + setSizePolicy(sizePolicy); + setWindowTitle("nheko"); + setObjectName("MainWindow"); + setStyleSheet("QWidget#MainWindow {background-color: #f9f9f9}"); - restoreWindowSize(); - setMinimumSize(QSize(conf::window::minWidth, conf::window::minHeight)); + restoreWindowSize(); + setMinimumSize(QSize(conf::window::minWidth, conf::window::minHeight)); - QFont font("Open Sans"); - font.setPixelSize(conf::fontSize); - font.setStyleStrategy(QFont::PreferAntialias); - setFont(font); + QFont font("Open Sans"); + font.setPixelSize(conf::fontSize); + font.setStyleStrategy(QFont::PreferAntialias); + setFont(font); - client_ = QSharedPointer<MatrixClient>(new MatrixClient("matrix.org")); - trayIcon_ = new TrayIcon(":/logos/nheko-32.png", this); + client_ = QSharedPointer<MatrixClient>(new MatrixClient("matrix.org")); + trayIcon_ = new TrayIcon(":/logos/nheko-32.png", this); - welcome_page_ = new WelcomePage(this); - login_page_ = new LoginPage(client_, this); - register_page_ = new RegisterPage(client_, this); - chat_page_ = new ChatPage(client_, this); + welcome_page_ = new WelcomePage(this); + login_page_ = new LoginPage(client_, this); + register_page_ = new RegisterPage(client_, this); + chat_page_ = new ChatPage(client_, this); - // Initialize sliding widget manager. - sliding_stack_ = new SlidingStackWidget(this); - sliding_stack_->addWidget(welcome_page_); - sliding_stack_->addWidget(login_page_); - sliding_stack_->addWidget(register_page_); - sliding_stack_->addWidget(chat_page_); + // Initialize sliding widget manager. + sliding_stack_ = new SlidingStackWidget(this); + sliding_stack_->addWidget(welcome_page_); + sliding_stack_->addWidget(login_page_); + sliding_stack_->addWidget(register_page_); + sliding_stack_->addWidget(chat_page_); - setCentralWidget(sliding_stack_); + setCentralWidget(sliding_stack_); - connect(welcome_page_, SIGNAL(userLogin()), this, SLOT(showLoginPage())); - connect(welcome_page_, SIGNAL(userRegister()), this, SLOT(showRegisterPage())); + connect(welcome_page_, SIGNAL(userLogin()), this, SLOT(showLoginPage())); + connect(welcome_page_, SIGNAL(userRegister()), this, SLOT(showRegisterPage())); - connect(login_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); - connect(register_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); + connect(login_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); + connect(register_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); - connect(chat_page_, SIGNAL(close()), this, SLOT(showWelcomePage())); - connect(chat_page_, SIGNAL(changeWindowTitle(QString)), this, SLOT(setWindowTitle(QString))); - connect(chat_page_, SIGNAL(unreadMessages(int)), trayIcon_, SLOT(setUnreadCount(int))); + connect(chat_page_, SIGNAL(close()), this, SLOT(showWelcomePage())); + connect( + chat_page_, SIGNAL(changeWindowTitle(QString)), this, SLOT(setWindowTitle(QString))); + connect(chat_page_, SIGNAL(unreadMessages(int)), trayIcon_, SLOT(setUnreadCount(int))); - connect(trayIcon_, - SIGNAL(activated(QSystemTrayIcon::ActivationReason)), - this, - SLOT(iconActivated(QSystemTrayIcon::ActivationReason))); + connect(trayIcon_, + SIGNAL(activated(QSystemTrayIcon::ActivationReason)), + this, + SLOT(iconActivated(QSystemTrayIcon::ActivationReason))); - connect(chat_page_, SIGNAL(contentLoaded()), this, SLOT(removeOverlayProgressBar())); + connect(chat_page_, SIGNAL(contentLoaded()), this, SLOT(removeOverlayProgressBar())); - connect(client_.data(), - SIGNAL(loginSuccess(QString, QString, QString)), - this, - SLOT(showChatPage(QString, QString, QString))); + connect(client_.data(), + SIGNAL(loginSuccess(QString, QString, QString)), + this, + SLOT(showChatPage(QString, QString, QString))); - QSettings settings; + QSettings settings; - if (hasActiveUser()) { - QString token = settings.value("auth/access_token").toString(); - QString home_server = settings.value("auth/home_server").toString(); - QString user_id = settings.value("auth/user_id").toString(); + if (hasActiveUser()) { + QString token = settings.value("auth/access_token").toString(); + QString home_server = settings.value("auth/home_server").toString(); + QString user_id = settings.value("auth/user_id").toString(); - showChatPage(user_id, home_server, token); - } + showChatPage(user_id, home_server, token); + } } void MainWindow::restoreWindowSize() { - QSettings settings; - int savedWidth = settings.value("window/width").toInt(); - int savedheight = settings.value("window/height").toInt(); - - if (savedWidth == 0 || savedheight == 0) - resize(conf::window::width, conf::window::height); - else - resize(savedWidth, savedheight); + QSettings settings; + int savedWidth = settings.value("window/width").toInt(); + int savedheight = settings.value("window/height").toInt(); + + if (savedWidth == 0 || savedheight == 0) + resize(conf::window::width, conf::window::height); + else + resize(savedWidth, savedheight); } void MainWindow::saveCurrentWindowSize() { - QSettings settings; - QSize current = size(); + QSettings settings; + QSize current = size(); - settings.setValue("window/width", current.width()); - settings.setValue("window/height", current.height()); + settings.setValue("window/width", current.width()); + settings.setValue("window/height", current.height()); } void MainWindow::removeOverlayProgressBar() { - QTimer *timer = new QTimer(this); - timer->setSingleShot(true); + QTimer *timer = new QTimer(this); + timer->setSingleShot(true); - connect(timer, &QTimer::timeout, [=]() { - timer->deleteLater(); + connect(timer, &QTimer::timeout, [=]() { + timer->deleteLater(); - if (progress_modal_ != nullptr) { - progress_modal_->deleteLater(); - progress_modal_->fadeOut(); - } + if (progress_modal_ != nullptr) { + progress_modal_->deleteLater(); + progress_modal_->fadeOut(); + } - if (spinner_ != nullptr) - spinner_->deleteLater(); + if (spinner_ != nullptr) + spinner_->deleteLater(); - progress_modal_ = nullptr; - spinner_ = nullptr; - }); + spinner_->stop(); - timer->start(500); + progress_modal_ = nullptr; + spinner_ = nullptr; + }); + + timer->start(500); } void MainWindow::showChatPage(QString userid, QString homeserver, QString token) { - QSettings settings; - settings.setValue("auth/access_token", token); - settings.setValue("auth/home_server", homeserver); - settings.setValue("auth/user_id", userid); - - int index = sliding_stack_->getWidgetIndex(chat_page_); - int modalOpacityDuration = 300; - - // If we go directly from the welcome page don't show an animation. - if (sliding_stack_->currentIndex() == 0) { - sliding_stack_->setCurrentIndex(index); - modalOpacityDuration = 0; - } else { - sliding_stack_->slideInIndex(index, SlidingStackWidget::AnimationDirection::LEFT_TO_RIGHT); - } - - if (spinner_ == nullptr) { - spinner_ = new CircularProgress(this); - spinner_->setColor("#acc7dc"); - spinner_->setSize(100); - } - - if (progress_modal_ == nullptr) { - progress_modal_ = new OverlayModal(this, spinner_); - progress_modal_->fadeIn(); - progress_modal_->setDuration(modalOpacityDuration); - } - - login_page_->reset(); - chat_page_->bootstrap(userid, homeserver, token); - - instance_ = this; + QSettings settings; + settings.setValue("auth/access_token", token); + settings.setValue("auth/home_server", homeserver); + settings.setValue("auth/user_id", userid); + + int index = sliding_stack_->getWidgetIndex(chat_page_); + int modalOpacityDuration = 300; + + // If we go directly from the welcome page don't show an animation. + if (sliding_stack_->currentIndex() == 0) { + sliding_stack_->setCurrentIndex(index); + modalOpacityDuration = 0; + } else { + sliding_stack_->slideInIndex(index, + SlidingStackWidget::AnimationDirection::LEFT_TO_RIGHT); + } + + if (spinner_ == nullptr) { + spinner_ = new LoadingIndicator(this); + spinner_->setColor("#acc7dc"); + spinner_->setFixedHeight(120); + spinner_->setFixedWidth(120); + spinner_->start(); + } + + if (progress_modal_ == nullptr) { + progress_modal_ = new OverlayModal(this, spinner_); + progress_modal_->fadeIn(); + progress_modal_->setDuration(modalOpacityDuration); + } + + login_page_->reset(); + chat_page_->bootstrap(userid, homeserver, token); + + instance_ = this; } void MainWindow::showWelcomePage() { - int index = sliding_stack_->getWidgetIndex(welcome_page_); - - if (sliding_stack_->currentIndex() == sliding_stack_->getWidgetIndex(login_page_)) - sliding_stack_->slideInIndex(index, SlidingStackWidget::AnimationDirection::RIGHT_TO_LEFT); - else - sliding_stack_->slideInIndex(index, SlidingStackWidget::AnimationDirection::LEFT_TO_RIGHT); + int index = sliding_stack_->getWidgetIndex(welcome_page_); + + if (sliding_stack_->currentIndex() == sliding_stack_->getWidgetIndex(login_page_)) + sliding_stack_->slideInIndex(index, + SlidingStackWidget::AnimationDirection::RIGHT_TO_LEFT); + else + sliding_stack_->slideInIndex(index, + SlidingStackWidget::AnimationDirection::LEFT_TO_RIGHT); } void MainWindow::showLoginPage() { - int index = sliding_stack_->getWidgetIndex(login_page_); - sliding_stack_->slideInIndex(index, SlidingStackWidget::AnimationDirection::LEFT_TO_RIGHT); + int index = sliding_stack_->getWidgetIndex(login_page_); + sliding_stack_->slideInIndex(index, SlidingStackWidget::AnimationDirection::LEFT_TO_RIGHT); } void MainWindow::showRegisterPage() { - int index = sliding_stack_->getWidgetIndex(register_page_); - sliding_stack_->slideInIndex(index, SlidingStackWidget::AnimationDirection::RIGHT_TO_LEFT); + int index = sliding_stack_->getWidgetIndex(register_page_); + sliding_stack_->slideInIndex(index, SlidingStackWidget::AnimationDirection::RIGHT_TO_LEFT); } void MainWindow::closeEvent(QCloseEvent *event) { - if (isVisible()) { - event->ignore(); - hide(); - } + if (isVisible()) { + event->ignore(); + hide(); + } } void MainWindow::iconActivated(QSystemTrayIcon::ActivationReason reason) { - switch (reason) { - case QSystemTrayIcon::Trigger: - if (!isVisible()) { - show(); - } else { - hide(); - } - break; - default: - break; - } + switch (reason) { + case QSystemTrayIcon::Trigger: + if (!isVisible()) { + show(); + } else { + hide(); + } + break; + default: + break; + } } bool MainWindow::hasActiveUser() { - QSettings settings; + QSettings settings; - return settings.contains("auth/access_token") && settings.contains("auth/home_server") && - settings.contains("auth/user_id"); + return settings.contains("auth/access_token") && settings.contains("auth/home_server") && + settings.contains("auth/user_id"); } MainWindow * MainWindow::instance() { - return instance_; + return instance_; } MainWindow::~MainWindow() diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc
index e053642d..2e4f7c47 100644 --- a/src/MatrixClient.cc +++ b/src/MatrixClient.cc
@@ -16,6 +16,8 @@ */ #include <QDebug> +#include <QFile> +#include <QImageReader> #include <QJsonArray> #include <QJsonDocument> #include <QJsonObject> @@ -34,11 +36,10 @@ MatrixClient::MatrixClient(QString server, QObject *parent) : QNetworkAccessManager(parent) + , clientApiUrl_{ "/_matrix/client/r0" } + , mediaApiUrl_{ "/_matrix/media/r0" } + , server_{ "https://" + server } { - server_ = "https://" + server; - api_url_ = "/_matrix/client/r0"; - token_ = ""; - QSettings settings; txn_id_ = settings.value("client/transaction_id", 1).toInt(); @@ -237,6 +238,42 @@ MatrixClient::onInitialSyncResponse(QNetworkReply *reply) } void +MatrixClient::onImageUploadResponse(QNetworkReply *reply) +{ + reply->deleteLater(); + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (status == 0 || status >= 400) { + emit syncFailed(reply->errorString()); + return; + } + + auto data = reply->readAll(); + + if (data.isEmpty()) + return; + + auto json = QJsonDocument::fromJson(data); + + if (!json.isObject()) { + qDebug() << "Media upload: Response is not a json object."; + return; + } + + QJsonObject object = json.object(); + if (!object.contains("content_uri")) { + qDebug() << "Media upload: Missing content_uri key"; + qDebug() << object; + return; + } + + emit imageUploaded(reply->property("room_id").toString(), + reply->property("filename").toString(), + object.value("content_uri").toString()); +} + +void MatrixClient::onSyncResponse(QNetworkReply *reply) { reply->deleteLater(); @@ -450,6 +487,9 @@ MatrixClient::onResponse(QNetworkReply *reply) case Endpoint::InitialSync: onInitialSyncResponse(reply); break; + case Endpoint::ImageUpload: + onImageUploadResponse(reply); + break; case Endpoint::Sync: onSyncResponse(reply); break; @@ -477,7 +517,7 @@ void MatrixClient::login(const QString &username, const QString &password) noexcept { QUrl endpoint(server_); - endpoint.setPath(api_url_ + "/login"); + endpoint.setPath(clientApiUrl_ + "/login"); QNetworkRequest request(endpoint); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); @@ -495,7 +535,7 @@ MatrixClient::logout() noexcept query.addQueryItem("access_token", token_); QUrl endpoint(server_); - endpoint.setPath(api_url_ + "/logout"); + endpoint.setPath(clientApiUrl_ + "/logout"); endpoint.setQuery(query); QNetworkRequest request(endpoint); @@ -515,7 +555,7 @@ MatrixClient::registerUser(const QString &user, const QString &pass, const QStri query.addQueryItem("kind", "user"); QUrl endpoint(server_); - endpoint.setPath(api_url_ + "/register"); + endpoint.setPath(clientApiUrl_ + "/register"); endpoint.setQuery(query); QNetworkRequest request(QString(endpoint.toEncoded())); @@ -549,7 +589,7 @@ MatrixClient::sync() noexcept query.addQueryItem("since", next_batch_); QUrl endpoint(server_); - endpoint.setPath(api_url_ + "/sync"); + endpoint.setPath(clientApiUrl_ + "/sync"); endpoint.setQuery(query); QNetworkRequest request(QString(endpoint.toEncoded())); @@ -561,32 +601,35 @@ MatrixClient::sync() noexcept void MatrixClient::sendRoomMessage(matrix::events::MessageEventType ty, const QString &roomid, - const QString &msg) noexcept + const QString &msg, + const QString &url) noexcept { QUrlQuery query; query.addQueryItem("access_token", token_); QUrl endpoint(server_); - endpoint.setPath(api_url_ + + endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/send/m.room.message/%2").arg(roomid).arg(txn_id_)); endpoint.setQuery(query); QString msgType(""); + QJsonObject body; switch (ty) { case matrix::events::MessageEventType::Text: - msgType = "m.text"; + body = { { "msgtype", "m.text" }, { "body", msg } }; break; case matrix::events::MessageEventType::Emote: - msgType = "m.emote"; + body = { { "msgtype", "m.emote" }, { "body", msg } }; break; - default: - msgType = "m.text"; + case matrix::events::MessageEventType::Image: + body = { { "msgtype", "m.image" }, { "body", msg }, { "url", url } }; break; + default: + qDebug() << "SendRoomMessage: Unknown message type for" << msg; + return; } - QJsonObject body{ { "msgtype", msgType }, { "body", msg } }; - QNetworkRequest request(QString(endpoint.toEncoded())); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); @@ -617,7 +660,7 @@ MatrixClient::initialSync() noexcept query.addQueryItem("access_token", token_); QUrl endpoint(server_); - endpoint.setPath(api_url_ + "/sync"); + endpoint.setPath(clientApiUrl_ + "/sync"); endpoint.setQuery(query); QNetworkRequest request(QString(endpoint.toEncoded())); @@ -650,7 +693,7 @@ MatrixClient::getOwnProfile() noexcept query.addQueryItem("access_token", token_); QUrl endpoint(server_); - endpoint.setPath(api_url_ + "/profile/" + userid); + endpoint.setPath(clientApiUrl_ + "/profile/" + userid); endpoint.setQuery(query); QNetworkRequest request(QString(endpoint.toEncoded())); @@ -762,7 +805,7 @@ MatrixClient::messages(const QString &room_id, const QString &from_token, int li query.addQueryItem("limit", QString::number(limit)); QUrl endpoint(server_); - endpoint.setPath(api_url_ + QString("/rooms/%1/messages").arg(room_id)); + endpoint.setPath(clientApiUrl_ + QString("/rooms/%1/messages").arg(room_id)); endpoint.setQuery(query); QNetworkRequest request(QString(endpoint.toEncoded())); @@ -771,3 +814,31 @@ MatrixClient::messages(const QString &room_id, const QString &from_token, int li reply->setProperty("endpoint", static_cast<int>(Endpoint::Messages)); reply->setProperty("room_id", room_id); } + +void +MatrixClient::uploadImage(const QString &roomid, const QString &filename) +{ + QUrlQuery query; + query.addQueryItem("access_token", token_); + + QUrl endpoint(server_); + endpoint.setPath(mediaApiUrl_ + "/upload"); + endpoint.setQuery(query); + + QFile file(filename); + if (!file.open(QIODevice::ReadWrite)) { + qDebug() << "Error while reading" << filename; + return; + } + + auto imgFormat = QString(QImageReader::imageFormat(filename)); + + QNetworkRequest request(QString(endpoint.toEncoded())); + request.setHeader(QNetworkRequest::ContentLengthHeader, file.size()); + request.setHeader(QNetworkRequest::ContentTypeHeader, QString("image/%1").arg(imgFormat)); + + QNetworkReply *reply = post(request, file.readAll()); + reply->setProperty("endpoint", static_cast<int>(Endpoint::ImageUpload)); + reply->setProperty("room_id", roomid); + reply->setProperty("filename", filename); +} diff --git a/src/TextInputWidget.cc b/src/TextInputWidget.cc
index bd74186e..ce208feb 100644 --- a/src/TextInputWidget.cc +++ b/src/TextInputWidget.cc
@@ -17,6 +17,8 @@ #include <QDebug> #include <QFile> +#include <QFileDialog> +#include <QImageReader> #include <QPainter> #include <QStyleOption> @@ -47,17 +49,23 @@ TextInputWidget::TextInputWidget(QWidget *parent) setCursor(Qt::ArrowCursor); setStyleSheet("background-color: #f8fbfe; height: 45px;"); - top_layout_ = new QHBoxLayout(); - top_layout_->setSpacing(0); - top_layout_->setMargin(0); - - send_file_button_ = new FlatButton(this); + topLayout_ = new QHBoxLayout(); + topLayout_->setSpacing(2); + topLayout_->setMargin(4); QIcon send_file_icon; send_file_icon.addFile(":/icons/icons/clip-dark.png", QSize(), QIcon::Normal, QIcon::Off); - send_file_button_->setForegroundColor(QColor("#acc7dc")); - send_file_button_->setIcon(send_file_icon); - send_file_button_->setIconSize(QSize(24, 24)); + + sendFileBtn_ = new FlatButton(this); + sendFileBtn_->setForegroundColor(QColor("#acc7dc")); + sendFileBtn_->setIcon(send_file_icon); + sendFileBtn_->setIconSize(QSize(24, 24)); + + spinner_ = new LoadingIndicator(this); + spinner_->setColor("#acc7dc"); + spinner_->setFixedHeight(40); + spinner_->setFixedWidth(40); + spinner_->hide(); QFont font; font.setPixelSize(conf::fontSize); @@ -68,33 +76,34 @@ TextInputWidget::TextInputWidget(QWidget *parent) input_->setPlaceholderText(tr("Write a message...")); input_->setStyleSheet("color: #333333; border-radius: 0; padding-top: 10px;"); - send_message_button_ = new FlatButton(this); - send_message_button_->setForegroundColor(QColor("#acc7dc")); + sendMessageBtn_ = new FlatButton(this); + sendMessageBtn_->setForegroundColor(QColor("#acc7dc")); QIcon send_message_icon; send_message_icon.addFile( ":/icons/icons/share-dark.png", QSize(), QIcon::Normal, QIcon::Off); - send_message_button_->setIcon(send_message_icon); - send_message_button_->setIconSize(QSize(24, 24)); + sendMessageBtn_->setIcon(send_message_icon); + sendMessageBtn_->setIconSize(QSize(24, 24)); - emoji_button_ = new EmojiPickButton(this); - emoji_button_->setForegroundColor(QColor("#acc7dc")); + emojiBtn_ = new EmojiPickButton(this); + emojiBtn_->setForegroundColor(QColor("#acc7dc")); QIcon emoji_icon; emoji_icon.addFile(":/icons/icons/smile.png", QSize(), QIcon::Normal, QIcon::Off); - emoji_button_->setIcon(emoji_icon); - emoji_button_->setIconSize(QSize(24, 24)); + emojiBtn_->setIcon(emoji_icon); + emojiBtn_->setIconSize(QSize(24, 24)); - top_layout_->addWidget(send_file_button_); - top_layout_->addWidget(input_); - top_layout_->addWidget(emoji_button_); - top_layout_->addWidget(send_message_button_); + topLayout_->addWidget(sendFileBtn_); + topLayout_->addWidget(input_); + topLayout_->addWidget(emojiBtn_); + topLayout_->addWidget(sendMessageBtn_); - setLayout(top_layout_); + setLayout(topLayout_); - connect(send_message_button_, SIGNAL(clicked()), this, SLOT(onSendButtonClicked())); - connect(input_, SIGNAL(enterPressed()), send_message_button_, SIGNAL(clicked())); - connect(emoji_button_, + connect(sendMessageBtn_, SIGNAL(clicked()), this, SLOT(onSendButtonClicked())); + connect(sendFileBtn_, SIGNAL(clicked()), this, SLOT(openFileSelection())); + connect(input_, SIGNAL(enterPressed()), sendMessageBtn_, SIGNAL(clicked())); + connect(emojiBtn_, SIGNAL(emojiSelected(const QString &)), this, SLOT(addSelectedEmoji(const QString &))); @@ -155,6 +164,55 @@ TextInputWidget::parseEmoteCommand(const QString &cmd) return QString(""); } +void +TextInputWidget::openFileSelection() +{ + QStringList supportedFiles; + supportedFiles << "jpeg" + << "gif" + << "png" + << "bmp" + << "tiff" + << "webp"; + + auto fileName = QFileDialog::getOpenFileName( + this, + tr("Select an image"), + "", + tr("Image Files (*.bmp *.gif *.jpg *.jpeg *.png *.tiff *.webp)")); + + if (fileName.isEmpty()) + return; + + auto imageFormat = QString(QImageReader::imageFormat(fileName)); + if (!supportedFiles.contains(imageFormat)) { + qDebug() << "Unsupported image format for" << fileName; + return; + } + + emit uploadImage(fileName); + showUploadSpinner(); +} + +void +TextInputWidget::showUploadSpinner() +{ + topLayout_->removeWidget(sendFileBtn_); + sendFileBtn_->hide(); + + topLayout_->insertWidget(0, spinner_); + spinner_->start(); +} + +void +TextInputWidget::hideUploadSpinner() +{ + topLayout_->removeWidget(spinner_); + topLayout_->insertWidget(0, sendFileBtn_); + sendFileBtn_->show(); + spinner_->stop(); +} + TextInputWidget::~TextInputWidget() { } diff --git a/src/TimelineItem.cc b/src/TimelineItem.cc
index 9d24a96c..92351d63 100644 --- a/src/TimelineItem.cc +++ b/src/TimelineItem.cc
@@ -107,6 +107,39 @@ TimelineItem::TimelineItem(events::MessageEventType ty, mainLayout_->addWidget(body_); } +TimelineItem::TimelineItem(ImageItem *image, + const QString &userid, + bool withSender, + QWidget *parent) + : QWidget{ parent } +{ + init(); + + auto displayName = TimelineViewManager::displayName(userid); + auto timestamp = QDateTime::currentDateTime(); + + descriptionMsg_ = { "You", userid, " sent an image", descriptiveTime(timestamp) }; + + generateTimestamp(timestamp); + + auto imageLayout = new QHBoxLayout(); + imageLayout->setMargin(0); + imageLayout->addWidget(image); + imageLayout->addStretch(1); + + if (withSender) { + generateBody(displayName, ""); + setupAvatarLayout(displayName); + mainLayout_->addLayout(headerLayout_); + + AvatarProvider::resolve(userid, this); + } else { + setupSimpleLayout(); + } + + mainLayout_->addLayout(imageLayout); +} + /* * Used to display images. The avatar and the username are displayed. */ diff --git a/src/TimelineView.cc b/src/TimelineView.cc
index 518676ac..600ccb94 100644 --- a/src/TimelineView.cc +++ b/src/TimelineView.cc
@@ -223,8 +223,9 @@ TimelineView::parseMessageEvent(const QJsonObject &event, TimelineDirection dire eventIds_[text.eventId()] = true; - if (isPendingMessage(text, local_user_)) { - removePendingMessage(text); + if (isPendingMessage( + text.eventId(), text.content().body(), text.sender(), local_user_)) { + removePendingMessage(text.eventId(), text.content().body()); return nullptr; } @@ -245,7 +246,6 @@ TimelineView::parseMessageEvent(const QJsonObject &event, TimelineDirection dire if (isDuplicate(notice.eventId())) return nullptr; - ; eventIds_[notice.eventId()] = true; @@ -269,6 +269,12 @@ TimelineView::parseMessageEvent(const QJsonObject &event, TimelineDirection dire eventIds_[img.eventId()] = true; + if (isPendingMessage( + img.eventId(), img.msgContent().url(), img.sender(), local_user_)) { + removePendingMessage(img.eventId(), img.msgContent().url()); + return nullptr; + } + auto with_sender = isSenderRendered(img.sender(), direction); updateLastSender(img.sender(), direction); @@ -289,8 +295,11 @@ TimelineView::parseMessageEvent(const QJsonObject &event, TimelineDirection dire eventIds_[emote.eventId()] = true; - if (isPendingMessage(emote, local_user_)) { - removePendingMessage(emote); + if (isPendingMessage(emote.eventId(), + emote.content().body(), + emote.sender(), + local_user_)) { + removePendingMessage(emote.eventId(), emote.content().body()); return nullptr; } @@ -472,6 +481,24 @@ TimelineView::addUserMessage(matrix::events::MessageEventType ty, const QString } void +TimelineView::addUserMessage(const QString &url, const QString &filename, int txn_id) +{ + QSettings settings; + auto user_id = settings.value("auth/user_id").toString(); + auto with_sender = lastSender_ != user_id; + + auto image = new ImageItem(client_, url, filename, this); + + TimelineItem *view_item = new TimelineItem(image, user_id, with_sender, scroll_widget_); + scroll_layout_->addWidget(view_item); + + lastSender_ = user_id; + + PendingMessage message(txn_id, url, "", view_item); + pending_msgs_.push_back(message); +} + +void TimelineView::notifyForLastEvent() { auto lastItem = scroll_layout_->itemAt(scroll_layout_->count() - 1); @@ -482,3 +509,33 @@ TimelineView::notifyForLastEvent() else qWarning() << "Cast to TimelineView failed" << room_id_; } + +bool +TimelineView::isPendingMessage(const QString &eventid, + const QString &body, + const QString &sender, + const QString &local_userid) +{ + if (sender != local_userid) + return false; + + for (const auto &msg : pending_msgs_) { + if (msg.event_id == eventid || msg.body == body) + return true; + } + + return false; +} + +void +TimelineView::removePendingMessage(const QString &eventid, const QString &body) +{ + for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); it++) { + int index = std::distance(pending_msgs_.begin(), it); + + if (it->event_id == eventid || it->body == body) { + pending_msgs_.removeAt(index); + break; + } + } +} diff --git a/src/TimelineViewManager.cc b/src/TimelineViewManager.cc
index 0bb56bf9..2adbba34 100644 --- a/src/TimelineViewManager.cc +++ b/src/TimelineViewManager.cc
@@ -19,6 +19,7 @@ #include <QApplication> #include <QDebug> +#include <QFileInfo> #include <QSettings> #include <QStackedWidget> #include <QWidget> @@ -73,6 +74,23 @@ TimelineViewManager::sendEmoteMessage(const QString &msg) } void +TimelineViewManager::sendImageMessage(const QString &roomid, + const QString &filename, + const QString &url) +{ + if (!views_.contains(roomid)) { + qDebug() << "Cannot send m.image message to a non-managed view"; + return; + } + + auto view = views_[roomid]; + + view->addUserMessage(url, filename, client_->transactionId()); + client_->sendRoomMessage( + matrix::events::MessageEventType::Image, roomid, QFileInfo(filename).fileName(), url); +} + +void TimelineViewManager::clearAll() { for (auto view : views_) diff --git a/src/ui/CircularProgress.cc b/src/ui/CircularProgress.cc deleted file mode 100644
index 425ece13..00000000 --- a/src/ui/CircularProgress.cc +++ /dev/null
@@ -1,201 +0,0 @@ -#include <QPainter> -#include <QParallelAnimationGroup> -#include <QPen> -#include <QPropertyAnimation> - -#include "CircularProgress.h" -#include "Theme.h" - -CircularProgress::CircularProgress(QWidget *parent) - : QProgressBar{ parent } - , progress_type_{ ui::ProgressType::IndeterminateProgress } - , width_{ 6.25 } - , size_{ 64 } - , duration_{ 3050 } -{ - delegate_ = new CircularProgressDelegate(this); - - setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); - - auto group = new QParallelAnimationGroup(this); - group->setLoopCount(-1); - - auto length_animation = new QPropertyAnimation(this); - length_animation->setPropertyName("dashLength"); - length_animation->setTargetObject(delegate_); - length_animation->setEasingCurve(QEasingCurve::InOutQuad); - length_animation->setStartValue(0.1); - length_animation->setKeyValueAt(0.15, 3); - length_animation->setKeyValueAt(0.6, 20); - length_animation->setKeyValueAt(0.7, 20); - length_animation->setEndValue(20); - length_animation->setDuration(duration_); - - auto offset_animation = new QPropertyAnimation(this); - offset_animation->setPropertyName("dashOffset"); - offset_animation->setTargetObject(delegate_); - offset_animation->setEasingCurve(QEasingCurve::InOutSine); - offset_animation->setStartValue(0); - offset_animation->setKeyValueAt(0.15, 0); - offset_animation->setKeyValueAt(0.6, -7); - offset_animation->setKeyValueAt(0.7, -7); - offset_animation->setEndValue(-25); - offset_animation->setDuration(duration_); - - auto angle_animation = new QPropertyAnimation(this); - angle_animation->setPropertyName("angle"); - angle_animation->setTargetObject(delegate_); - angle_animation->setStartValue(0); - angle_animation->setEndValue(360); - angle_animation->setDuration(duration_); - - group->addAnimation(length_animation); - group->addAnimation(offset_animation); - group->addAnimation(angle_animation); - - group->start(); -} - -void -CircularProgress::setProgressType(ui::ProgressType type) -{ - progress_type_ = type; - update(); -} - -void -CircularProgress::setLineWidth(qreal width) -{ - width_ = width; - update(); - updateGeometry(); -} - -void -CircularProgress::setSize(int size) -{ - size_ = size; - update(); - updateGeometry(); -} - -ui::ProgressType -CircularProgress::progressType() const -{ - return progress_type_; -} - -qreal -CircularProgress::lineWidth() const -{ - return width_; -} - -int -CircularProgress::size() const -{ - return size_; -} - -void -CircularProgress::setColor(const QColor &color) -{ - color_ = color; -} - -QColor -CircularProgress::color() const -{ - if (!color_.isValid()) { - return QColor("red"); - } - - return color_; -} - -QSize -CircularProgress::sizeHint() const -{ - const qreal s = size_ + width_ + 8; - return QSize(s, s); -} - -void -CircularProgress::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - /* - * If the progress bar is disabled draw an X instead - */ - if (!isEnabled()) { - QPen pen; - pen.setCapStyle(Qt::RoundCap); - pen.setWidthF(lineWidth()); - pen.setColor("gray"); - - auto center = rect().center(); - - painter.setPen(pen); - painter.drawLine(center - QPointF(20, 20), center + QPointF(20, 20)); - painter.drawLine(center + QPointF(20, -20), center - QPointF(20, -20)); - - return; - } - - if (progress_type_ == ui::ProgressType::IndeterminateProgress) { - painter.translate(width() / 2, height() / 2); - painter.rotate(delegate_->angle()); - } - - QPen pen; - pen.setCapStyle(Qt::RoundCap); - pen.setWidthF(width_); - pen.setColor(color()); - - if (ui::ProgressType::IndeterminateProgress == progress_type_) { - QVector<qreal> pattern; - pattern << delegate_->dashLength() * size_ / 50 << 30 * size_ / 50; - - pen.setDashOffset(delegate_->dashOffset() * size_ / 50); - pen.setDashPattern(pattern); - - painter.setPen(pen); - - painter.drawEllipse(QPoint(0, 0), size_ / 2, size_ / 2); - } else { - painter.setPen(pen); - - const qreal x = (width() - size_) / 2; - const qreal y = (height() - size_) / 2; - - const qreal a = 360 * (value() - minimum()) / (maximum() - minimum()); - - QPainterPath path; - path.arcMoveTo(x, y, size_, size_, 0); - path.arcTo(x, y, size_, size_, 0, a); - - painter.drawPath(path); - } -} - -CircularProgress::~CircularProgress() -{ -} - -CircularProgressDelegate::CircularProgressDelegate(CircularProgress *parent) - : QObject(parent) - , progress_(parent) - , dash_offset_(0) - , dash_length_(89) - , angle_(0) -{ - Q_ASSERT(parent); -} - -CircularProgressDelegate::~CircularProgressDelegate() -{ -} diff --git a/src/ui/LoadingIndicator.cc b/src/ui/LoadingIndicator.cc new file mode 100644
index 00000000..0fafaf23 --- /dev/null +++ b/src/ui/LoadingIndicator.cc
@@ -0,0 +1,86 @@ +#include "LoadingIndicator.h" + +#include <QDebug> +#include <QPoint> +#include <QtGlobal> + +LoadingIndicator::LoadingIndicator(QWidget *parent) + : QWidget(parent) + , interval_(70) + , angle_(0) + , color_(Qt::black) +{ + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + setFocusPolicy(Qt::NoFocus); + + timer_ = new QTimer(); + connect(timer_, SIGNAL(timeout()), this, SLOT(onTimeout())); +} + +LoadingIndicator::~LoadingIndicator() +{ + stop(); + + delete timer_; +} + +void +LoadingIndicator::paintEvent(QPaintEvent *e) +{ + Q_UNUSED(e) + + if (!timer_->isActive()) + return; + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + int width = qMin(this->width(), this->height()); + + int outerRadius = (width - 4) * 0.5f; + int innerRadius = outerRadius * 0.78f; + + int capsuleRadius = (outerRadius - innerRadius) / 2; + + for (int i = 0; i < 8; i++) { + QColor color = color_; + + color.setAlphaF(1.0f - (i / 8.0f)); + + painter.setPen(Qt::NoPen); + painter.setBrush(color); + + qreal radius = capsuleRadius * (1.0f - (i / 16.0f)); + + painter.save(); + + painter.translate(rect().center()); + painter.rotate(angle_ - i * 45.0f); + + QPointF center = QPointF(-capsuleRadius, -innerRadius); + painter.drawEllipse(center, radius * 2, radius * 2); + + painter.restore(); + } +} + +void +LoadingIndicator::start() +{ + timer_->start(interval_); + show(); +} + +void +LoadingIndicator::stop() +{ + timer_->stop(); + hide(); +} + +void +LoadingIndicator::onTimeout() +{ + angle_ = (angle_ + 45) % 360; + update(); +}