diff --git a/CMakeLists.txt b/CMakeLists.txt index f07b7019..01de678a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,6 +76,7 @@ set(SRC_FILES src/EmojiPanel.cc src/EmojiPickButton.cc src/EmojiProvider.cc + src/ImageItem.cc src/TimelineItem.cc src/TimelineView.cc src/TimelineViewManager.cc @@ -127,6 +128,7 @@ qt5_wrap_cpp(MOC_HEADERS include/EmojiItemDelegate.h include/EmojiPanel.h include/EmojiPickButton.h + include/ImageItem.h include/TimelineItem.h include/TimelineView.h include/TimelineViewManager.h diff --git a/include/ImageItem.h b/include/ImageItem.h new file mode 100644 index 00000000..7dc8773f --- /dev/null +++ b/include/ImageItem.h @@ -0,0 +1,73 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TIMELINE_IMAGE_ITEM_H +#define TIMELINE_IMAGE_ITEM_H + +#include +#include +#include +#include + +#include "MatrixClient.h" + +class ImageItem : public QWidget +{ + Q_OBJECT +public: + ImageItem(QSharedPointer client, + const Event &event, + const QString &body, + const QUrl &url, + QWidget *parent = nullptr); + + void setImage(const QPixmap &image); + + QSize sizeHint() const override; + +protected: + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + +private slots: + void imageDownloaded(const QString &event_id, const QPixmap &img); + +private: + void scaleImage(); + void openUrl(); + + int max_width_ = 500; + int max_height_ = 300; + + int width_; + int height_; + + QPixmap scaled_image_; + QPixmap image_; + + QUrl url_; + QString text_; + + int bottom_height_ = 30; + + Event event_; + + QSharedPointer client_; +}; + +#endif // TIMELINE_IMAGE_ITEM_H diff --git a/include/MatrixClient.h b/include/MatrixClient.h index 8d517b9a..ad768eeb 100644 --- a/include/MatrixClient.h +++ b/include/MatrixClient.h @@ -42,6 +42,7 @@ public: void versions() noexcept; void fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url); void fetchOwnAvatar(const QUrl &avatar_url); + void downloadImage(const QString &event_id, const QUrl &url); inline QString getHomeServer(); inline int transactionId(); @@ -68,6 +69,7 @@ signals: void roomAvatarRetrieved(const QString &roomid, const QPixmap &img); void ownAvatarRetrieved(const QPixmap &img); + void imageDownloaded(const QString &event_id, const QPixmap &img); // Returned profile data for the user's account. void getOwnProfileResponse(const QUrl &avatar_url, const QString &display_name); @@ -84,6 +86,7 @@ private: GetOwnProfile, GetOwnAvatar, GetProfile, + Image, InitialSync, Login, Logout, @@ -105,6 +108,7 @@ private: void onInitialSyncResponse(QNetworkReply *reply); void onSyncResponse(QNetworkReply *reply); void onRoomAvatarResponse(QNetworkReply *reply); + void onImageResponse(QNetworkReply *reply); // Client API prefix. QString api_url_; diff --git a/include/TimelineItem.h b/include/TimelineItem.h index 9af02597..626687ac 100644 --- a/include/TimelineItem.h +++ b/include/TimelineItem.h @@ -23,6 +23,7 @@ #include #include "Sync.h" +#include "ImageItem.h" class TimelineItem : public QWidget { @@ -35,6 +36,10 @@ public: TimelineItem(const QString &userid, const QString &color, const QString &body, QWidget *parent = 0); TimelineItem(const QString &body, QWidget *parent = 0); + // For inline images. + TimelineItem(ImageItem *image, const Event &event, const QString &color, QWidget *parent); + TimelineItem(ImageItem *image, const Event &event, QWidget *parent); + ~TimelineItem(); private: diff --git a/include/TimelineView.h b/include/TimelineView.h index e1254ff0..4400c361 100644 --- a/include/TimelineView.h +++ b/include/TimelineView.h @@ -49,11 +49,13 @@ class TimelineView : public QWidget Q_OBJECT public: - explicit TimelineView(QWidget *parent = 0); - explicit TimelineView(const QList &events, QWidget *parent = 0); + TimelineView(QSharedPointer client, QWidget *parent = 0); + TimelineView(const QList &events, QSharedPointer client, QWidget *parent = 0); ~TimelineView(); + // FIXME: Reduce the parameters void addHistoryItem(const Event &event, const QString &color, bool with_sender); + void addImageItem(const QString &body, const QUrl &url, const Event &event, const QString &color, bool with_sender); int addEvents(const QList &events); void addUserTextMessage(const QString &msg, int txn_id); void updatePendingMessage(int txn_id, QString event_id); @@ -76,6 +78,7 @@ private: QString last_sender_; QList pending_msgs_; + QSharedPointer client_; }; #endif // HISTORY_VIEW_H diff --git a/src/ImageItem.cc b/src/ImageItem.cc new file mode 100644 index 00000000..8298926d --- /dev/null +++ b/src/ImageItem.cc @@ -0,0 +1,175 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +#include "ImageItem.h" + +ImageItem::ImageItem(QSharedPointer client, const Event &event, const QString &body, const QUrl &url, QWidget *parent) + : QWidget(parent) + , url_{url} + , text_{body} + , event_{event} + , client_{client} +{ + setMaximumSize(max_width_, max_height_); + setMouseTracking(true); + setCursor(Qt::PointingHandCursor); + setStyleSheet("background-color: blue"); + + QList 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(), media_params); + + client_.data()->downloadImage(event.eventId(), url_); + + connect(client_.data(), + SIGNAL(imageDownloaded(const QString &, const QPixmap &)), + this, + SLOT(imageDownloaded(const QString &, const QPixmap &))); +} + +void ImageItem::imageDownloaded(const QString &event_id, const QPixmap &img) +{ + if (event_id != event_.eventId()) + return; + + setImage(img); +} + +void ImageItem::openUrl() +{ + if (url_.toString().isEmpty()) + return; + + if (!QDesktopServices::openUrl(url_)) + qWarning() << "Could not open url" << url_.toString(); +} + +void ImageItem::scaleImage() +{ + if (image_.isNull()) + return; + + 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); + + if (min_aspect_ratio > 1) { + width_ = image_.width(); + height_ = image_.height(); + } else { + width_ = image_.width() * min_aspect_ratio; + height_ = image_.height() * min_aspect_ratio; + } + + setMinimumSize(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_); + + return QSize(width_, height_); +} + +void ImageItem::setImage(const QPixmap &image) +{ + 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 + qDebug() << "Opening image overlay. Not implemented yet."; +} + +void ImageItem::resizeEvent(QResizeEvent *event) +{ + Q_UNUSED(event); + + scaleImage(); +} + +void ImageItem::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + QFont font("Open Sans"); + font.setPixelSize(12); + + QFontMetrics metrics(font); + int fontHeight = metrics.height(); + + if (image_.isNull()) { + int height = fontHeight + 10; + + setMinimumSize(max_width_, fontHeight + 10); + + QString elidedText = metrics.elidedText(text_, Qt::ElideRight, max_width_ - 10); + + painter.setFont(font); + painter.setPen(QPen(QColor(66, 133, 244))); + painter.drawText(QPoint(0, height / 2 + 2), elidedText); + + return; + } + + painter.fillRect(QRect(0, 0, width_, height_), scaled_image_); + + // 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); + + painter.setFont(font); + painter.setPen(QPen(QColor("white"))); + painter.drawText(QPoint(5, height_ - fontHeight / 2), elidedText); +} diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc index 381f5023..6b4a81bb 100644 --- a/src/MatrixClient.cc +++ b/src/MatrixClient.cc @@ -309,6 +309,30 @@ void MatrixClient::onGetOwnAvatarResponse(QNetworkReply *reply) emit ownAvatarRetrieved(pixmap); } +void MatrixClient::onImageResponse(QNetworkReply *reply) +{ + reply->deleteLater(); + + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (status == 0 || status >= 400) { + qWarning() << reply->errorString(); + return; + } + + auto img = reply->readAll(); + + if (img.size() == 0) + return; + + QPixmap pixmap; + pixmap.loadFromData(img); + + auto event_id = reply->property("event_id").toString(); + + emit imageDownloaded(event_id, pixmap); +} + void MatrixClient::onResponse(QNetworkReply *reply) { switch (reply->property("endpoint").toInt()) { @@ -327,6 +351,9 @@ void MatrixClient::onResponse(QNetworkReply *reply) case Endpoint::GetOwnProfile: onGetOwnProfileResponse(reply); break; + case Endpoint::Image: + onImageResponse(reply); + break; case Endpoint::InitialSync: onInitialSyncResponse(reply); break; @@ -528,6 +555,15 @@ void MatrixClient::fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url reply->setProperty("endpoint", Endpoint::RoomAvatar); } +void MatrixClient::downloadImage(const QString &event_id, const QUrl &url) +{ + QNetworkRequest image_request(url); + + QNetworkReply *reply = get(image_request); + reply->setProperty("event_id", event_id); + reply->setProperty("endpoint", Endpoint::Image); +} + void MatrixClient::fetchOwnAvatar(const QUrl &avatar_url) { QList url_parts = avatar_url.toString().split("mxc://"); diff --git a/src/TimelineItem.cc b/src/TimelineItem.cc index 607522a3..4d33db70 100644 --- a/src/TimelineItem.cc +++ b/src/TimelineItem.cc @@ -18,6 +18,7 @@ #include #include +#include "ImageItem.h" #include "TimelineItem.h" TimelineItem::TimelineItem(const QString &userid, const QString &color, const QString &body, QWidget *parent) @@ -36,6 +37,42 @@ TimelineItem::TimelineItem(const QString &body, QWidget *parent) setupLayout(); } +TimelineItem::TimelineItem(ImageItem *image, const Event &event, const QString &color, QWidget *parent) + : QWidget(parent) +{ + auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp()); + generateTimestamp(timestamp); + generateBody(event.sender(), color, ""); + + top_layout_ = new QHBoxLayout(); + top_layout_->setMargin(0); + top_layout_->addWidget(time_label_); + + auto right_layout = new QVBoxLayout(); + right_layout->addWidget(content_label_); + right_layout->addWidget(image); + + top_layout_->addLayout(right_layout); + top_layout_->addStretch(1); + + setLayout(top_layout_); +} + +TimelineItem::TimelineItem(ImageItem *image, const Event &event, QWidget *parent) + : QWidget(parent) +{ + auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp()); + generateTimestamp(timestamp); + + top_layout_ = new QHBoxLayout(); + top_layout_->setMargin(0); + top_layout_->addWidget(time_label_); + top_layout_->addWidget(image, 1); + top_layout_->addStretch(1); + + setLayout(top_layout_); +} + TimelineItem::TimelineItem(const Event &event, bool with_sender, const QString &color, QWidget *parent) : QWidget(parent) { diff --git a/src/TimelineView.cc b/src/TimelineView.cc index 00a42557..95c7a351 100644 --- a/src/TimelineView.cc +++ b/src/TimelineView.cc @@ -21,19 +21,22 @@ #include #include +#include "ImageItem.h" #include "TimelineItem.h" #include "TimelineView.h" #include "TimelineViewManager.h" -TimelineView::TimelineView(const QList &events, QWidget *parent) +TimelineView::TimelineView(const QList &events, QSharedPointer client, QWidget *parent) : QWidget(parent) + , client_{client} { init(); addEvents(events); } -TimelineView::TimelineView(QWidget *parent) +TimelineView::TimelineView(QSharedPointer client, QWidget *parent) : QWidget(parent) + , client_{client} { init(); } @@ -73,6 +76,28 @@ int TimelineView::addEvents(const QList &events) addHistoryItem(event, color, with_sender); last_sender_ = event.sender(); + message_count += 1; + } else if (msg_type == "m.image") { + // TODO: Move this into serialization. + if (!event.content().contains("url")) { + qWarning() << "Missing url from m.image event" << event.content(); + continue; + } + + if (!event.content().contains("body")) { + qWarning() << "Missing body from m.image event" << event.content(); + continue; + } + + QUrl url(event.content().value("url").toString()); + QString body(event.content().value("body").toString()); + + auto with_sender = last_sender_ != event.sender(); + auto color = TimelineViewManager::getUserColor(event.sender()); + + addImageItem(body, url, event, color, with_sender); + + last_sender_ = event.sender(); message_count += 1; } } @@ -111,6 +136,23 @@ void TimelineView::init() SLOT(sliderRangeChanged(int, int))); } +void TimelineView::addImageItem(const QString &body, + const QUrl &url, + const Event &event, + const QString &color, + bool with_sender) +{ + auto image = new ImageItem(client_, event, body, url); + + if (with_sender) { + auto item = new TimelineItem(image, event, color, scroll_widget_); + scroll_layout_->addWidget(item); + } else { + auto item = new TimelineItem(image, event, scroll_widget_); + scroll_layout_->addWidget(item); + } +} + void TimelineView::addHistoryItem(const Event &event, const QString &color, bool with_sender) { TimelineItem *item = new TimelineItem(event, with_sender, color, scroll_widget_); diff --git a/src/TimelineViewManager.cc b/src/TimelineViewManager.cc index 3783b250..ddb142d3 100644 --- a/src/TimelineViewManager.cc +++ b/src/TimelineViewManager.cc @@ -81,7 +81,7 @@ void TimelineViewManager::initialize(const Rooms &rooms) auto events = it.value().timeline().events(); // Create a history view with the room events. - TimelineView *view = new TimelineView(events); + TimelineView *view = new TimelineView(events, client_); views_.insert(it.key(), view); // Add the view in the widget stack.