diff --git a/src/AvatarProvider.cc b/src/AvatarProvider.cc
new file mode 100644
index 00000000..7481b781
--- /dev/null
+++ b/src/AvatarProvider.cc
@@ -0,0 +1,83 @@
+/*
+ * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include "AvatarProvider.h"
+
+QSharedPointer<MatrixClient> AvatarProvider::client_;
+
+QMap<QString, QImage> AvatarProvider::userAvatars_;
+QMap<QString, QUrl> AvatarProvider::avatarUrls_;
+QMap<QString, QList<TimelineItem *>> AvatarProvider::toBeResolved_;
+
+void AvatarProvider::init(QSharedPointer<MatrixClient> client)
+{
+ client_ = client;
+
+ connect(client_.data(), &MatrixClient::userAvatarRetrieved, &AvatarProvider::updateAvatar);
+}
+
+void AvatarProvider::updateAvatar(const QString &uid, const QImage &img)
+{
+ if (toBeResolved_.contains(uid)) {
+ auto items = toBeResolved_[uid];
+
+ // Update all the timeline items with the resolved avatar.
+ for (const auto item : items)
+ item->setUserAvatar(img);
+
+ toBeResolved_.remove(uid);
+ }
+
+ userAvatars_.insert(uid, img);
+}
+
+void AvatarProvider::resolve(const QString &userId, TimelineItem *item)
+{
+ if (userAvatars_.contains(userId)) {
+ auto img = userAvatars_[userId];
+
+ item->setUserAvatar(img);
+
+ return;
+ }
+
+ if (avatarUrls_.contains(userId)) {
+ // Add the current timeline item to the waiting list for this avatar.
+ if (!toBeResolved_.contains(userId)) {
+ client_->fetchUserAvatar(userId, avatarUrls_[userId]);
+
+ QList<TimelineItem *> timelineItems;
+ timelineItems.push_back(item);
+
+ toBeResolved_.insert(userId, timelineItems);
+ } else {
+ toBeResolved_[userId].push_back(item);
+ }
+ }
+}
+
+void AvatarProvider::setAvatarUrl(const QString &userId, const QUrl &url)
+{
+ avatarUrls_.insert(userId, url);
+}
+
+void AvatarProvider::clear()
+{
+ userAvatars_.clear();
+ avatarUrls_.clear();
+ toBeResolved_.clear();
+}
diff --git a/src/ChatPage.cc b/src/ChatPage.cc
index 0b09693b..4e9120d2 100644
--- a/src/ChatPage.cc
+++ b/src/ChatPage.cc
@@ -27,6 +27,7 @@
#include "AliasesEventContent.h"
#include "AvatarEventContent.h"
+#include "AvatarProvider.h"
#include "CanonicalAliasEventContent.h"
#include "CreateEventContent.h"
#include "HistoryVisibilityEventContent.h"
@@ -173,6 +174,8 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client, QWidget *parent)
SIGNAL(ownAvatarRetrieved(const QPixmap &)),
this,
SLOT(setOwnAvatar(const QPixmap &)));
+
+ AvatarProvider::init(client);
}
void ChatPage::logout()
@@ -203,6 +206,8 @@ void ChatPage::logout()
settingsManager_.clear();
room_avatars_.clear();
+ AvatarProvider::clear();
+
emit close();
}
@@ -300,6 +305,14 @@ void ChatPage::initialSyncCompleted(const SyncResponse &response)
state_manager_.insert(it.key(), room_state);
settingsManager_.insert(it.key(), QSharedPointer<RoomSettings>(new RoomSettings(it.key())));
+
+ for (const auto membership : room_state.memberships) {
+ auto uid = membership.sender();
+ auto url = membership.content().avatarUrl();
+
+ if (!url.toString().isEmpty())
+ AvatarProvider::setAvatarUrl(uid, url);
+ }
}
view_manager_->initialize(response.rooms());
diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc
index a605623f..927db541 100644
--- a/src/MatrixClient.cc
+++ b/src/MatrixClient.cc
@@ -287,6 +287,29 @@ void MatrixClient::onRoomAvatarResponse(QNetworkReply *reply)
emit roomAvatarRetrieved(roomid, pixmap);
}
+void MatrixClient::onUserAvatarResponse(QNetworkReply *reply)
+{
+ reply->deleteLater();
+
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ if (status == 0 || status >= 400) {
+ qWarning() << reply->errorString();
+ return;
+ }
+
+ auto data = reply->readAll();
+
+ if (data.size() == 0)
+ return;
+
+ auto roomid = reply->property("userid").toString();
+
+ QImage img;
+ img.loadFromData(data);
+
+ emit userAvatarRetrieved(roomid, img);
+}
void MatrixClient::onGetOwnAvatarResponse(QNetworkReply *reply)
{
reply->deleteLater();
@@ -392,6 +415,9 @@ void MatrixClient::onResponse(QNetworkReply *reply)
case Endpoint::RoomAvatar:
onRoomAvatarResponse(reply);
break;
+ case Endpoint::UserAvatar:
+ onUserAvatarResponse(reply);
+ break;
case Endpoint::GetOwnAvatar:
onGetOwnAvatarResponse(reply);
break;
@@ -591,6 +617,32 @@ void MatrixClient::fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url
reply->setProperty("endpoint", static_cast<int>(Endpoint::RoomAvatar));
}
+void MatrixClient::fetchUserAvatar(const QString &userId, const QUrl &avatarUrl)
+{
+ QList<QString> url_parts = avatarUrl.toString().split("mxc://");
+
+ if (url_parts.size() != 2) {
+ qDebug() << "Invalid format for user avatar " << avatarUrl.toString();
+ return;
+ }
+
+ QUrlQuery query;
+ query.addQueryItem("width", "128");
+ query.addQueryItem("height", "128");
+ query.addQueryItem("method", "crop");
+
+ QString media_url = QString("%1/_matrix/media/r0/thumbnail/%2").arg(getHomeServer().toString(), url_parts[1]);
+
+ QUrl endpoint(media_url);
+ endpoint.setQuery(query);
+
+ QNetworkRequest avatar_request(endpoint);
+
+ QNetworkReply *reply = get(avatar_request);
+ reply->setProperty("userid", userId);
+ reply->setProperty("endpoint", static_cast<int>(Endpoint::UserAvatar));
+}
+
void MatrixClient::downloadImage(const QString &event_id, const QUrl &url)
{
QNetworkRequest image_request(url);
diff --git a/src/TimelineItem.cc b/src/TimelineItem.cc
index b1c58e6a..cf8d5e85 100644
--- a/src/TimelineItem.cc
+++ b/src/TimelineItem.cc
@@ -17,8 +17,10 @@
#include <QDateTime>
#include <QDebug>
+#include <QFontDatabase>
#include <QRegExp>
+#include "AvatarProvider.h"
#include "ImageItem.h"
#include "TimelineItem.h"
#include "TimelineViewManager.h"
@@ -29,65 +31,119 @@ static const QString URL_HTML = "<a href=\"\\1\" style=\"color: #333333\">\\1</a
namespace events = matrix::events;
namespace msgs = matrix::events::messages;
+void TimelineItem::init()
+{
+ userAvatar_ = nullptr;
+ timestamp_ = nullptr;
+ userName_ = nullptr;
+ body_ = nullptr;
+
+ QFontDatabase db;
+
+ bodyFont_ = db.font("Open Sans", "Regular", 10);
+ usernameFont_ = db.font("Open Sans", "Bold", 10);
+ timestampFont_ = db.font("Open Sans", "Regular", 7);
+
+ topLayout_ = new QHBoxLayout(this);
+ sideLayout_ = new QVBoxLayout();
+ mainLayout_ = new QVBoxLayout();
+ headerLayout_ = new QHBoxLayout();
+
+ topLayout_->setContentsMargins(7, 0, 0, 0);
+ topLayout_->setSpacing(9);
+
+ topLayout_->addLayout(sideLayout_);
+ topLayout_->addLayout(mainLayout_, 1);
+}
+
TimelineItem::TimelineItem(const QString &userid, const QString &color, QString body, QWidget *parent)
: QWidget(parent)
{
+ init();
+
body.replace(URL_REGEX, URL_HTML);
+ auto displayName = TimelineViewManager::displayName(userid);
generateTimestamp(QDateTime::currentDateTime());
- generateBody(TimelineViewManager::displayName(userid), color, body);
- setupLayout();
+ generateBody(displayName, color, body);
+
+ setupAvatarLayout(displayName);
+
+ mainLayout_->addLayout(headerLayout_);
+ mainLayout_->addWidget(body_);
+ mainLayout_->setMargin(0);
+ mainLayout_->setSpacing(0);
+
+ AvatarProvider::resolve(userid, this);
}
TimelineItem::TimelineItem(QString body, QWidget *parent)
: QWidget(parent)
{
+ init();
+
body.replace(URL_REGEX, URL_HTML);
generateTimestamp(QDateTime::currentDateTime());
generateBody(body);
- setupLayout();
+
+ setupSimpleLayout();
+
+ mainLayout_->addWidget(body_);
+ mainLayout_->setMargin(0);
+ mainLayout_->setSpacing(2);
}
TimelineItem::TimelineItem(ImageItem *image, const events::MessageEvent<msgs::Image> &event, const QString &color, QWidget *parent)
: QWidget(parent)
{
+ init();
+
auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp());
+ auto displayName = TimelineViewManager::displayName(event.sender());
+
generateTimestamp(timestamp);
- generateBody(TimelineViewManager::displayName(event.sender()), color, "");
+ generateBody(displayName, color, "");
- top_layout_ = new QHBoxLayout();
- top_layout_->setMargin(0);
- top_layout_->addWidget(time_label_);
+ setupAvatarLayout(displayName);
- auto right_layout = new QVBoxLayout();
- right_layout->addWidget(content_label_);
- right_layout->addWidget(image);
+ auto imageLayout = new QHBoxLayout();
+ imageLayout->addWidget(image);
+ imageLayout->addStretch(1);
- top_layout_->addLayout(right_layout);
- top_layout_->addStretch(1);
+ mainLayout_->addLayout(headerLayout_);
+ mainLayout_->addLayout(imageLayout);
+ mainLayout_->setContentsMargins(0, 4, 0, 0);
+ mainLayout_->setSpacing(0);
- setLayout(top_layout_);
+ AvatarProvider::resolve(event.sender(), this);
}
TimelineItem::TimelineItem(ImageItem *image, const events::MessageEvent<msgs::Image> &event, QWidget *parent)
: QWidget(parent)
{
+ init();
+
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);
+ setupSimpleLayout();
- setLayout(top_layout_);
+ auto imageLayout = new QHBoxLayout();
+ imageLayout->setMargin(0);
+ imageLayout->addWidget(image);
+ imageLayout->addStretch(1);
+
+ mainLayout_->addLayout(imageLayout);
+ mainLayout_->setContentsMargins(0, 4, 0, 0);
+ mainLayout_->setSpacing(2);
}
TimelineItem::TimelineItem(const events::MessageEvent<msgs::Notice> &event, bool with_sender, const QString &color, QWidget *parent)
: QWidget(parent)
{
+ init();
+
auto body = event.content().body().trimmed().toHtmlEscaped();
auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp());
@@ -96,17 +152,34 @@ TimelineItem::TimelineItem(const events::MessageEvent<msgs::Notice> &event, bool
body.replace(URL_REGEX, URL_HTML);
body = "<i style=\"color: #565E5E\">" + body + "</i>";
- if (with_sender)
- generateBody(TimelineViewManager::displayName(event.sender()), color, body);
- else
+ if (with_sender) {
+ auto displayName = TimelineViewManager::displayName(event.sender());
+
+ generateBody(displayName, color, body);
+ setupAvatarLayout(displayName);
+
+ mainLayout_->addLayout(headerLayout_);
+ mainLayout_->addWidget(body_);
+ mainLayout_->setMargin(0);
+ mainLayout_->setSpacing(0);
+
+ AvatarProvider::resolve(event.sender(), this);
+ } else {
generateBody(body);
- setupLayout();
+ setupSimpleLayout();
+
+ mainLayout_->addWidget(body_);
+ mainLayout_->setMargin(0);
+ mainLayout_->setSpacing(2);
+ }
}
TimelineItem::TimelineItem(const events::MessageEvent<msgs::Text> &event, bool with_sender, const QString &color, QWidget *parent)
: QWidget(parent)
{
+ init();
+
auto body = event.content().body().trimmed().toHtmlEscaped();
auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp());
@@ -114,34 +187,45 @@ TimelineItem::TimelineItem(const events::MessageEvent<msgs::Text> &event, bool w
body.replace(URL_REGEX, URL_HTML);
- if (with_sender)
- generateBody(TimelineViewManager::displayName(event.sender()), color, body);
- else
+ if (with_sender) {
+ auto displayName = TimelineViewManager::displayName(event.sender());
+ generateBody(displayName, color, body);
+
+ setupAvatarLayout(displayName);
+
+ mainLayout_->addLayout(headerLayout_);
+ mainLayout_->addWidget(body_);
+ mainLayout_->setMargin(0);
+ mainLayout_->setSpacing(0);
+
+ AvatarProvider::resolve(event.sender(), this);
+ } else {
generateBody(body);
- setupLayout();
+ setupSimpleLayout();
+
+ mainLayout_->addWidget(body_);
+ mainLayout_->setMargin(0);
+ mainLayout_->setSpacing(2);
+ }
}
+// Only the body is displayed.
void TimelineItem::generateBody(const QString &body)
{
- content_label_ = new QLabel(this);
- content_label_->setWordWrap(true);
- content_label_->setAlignment(Qt::AlignTop);
- content_label_->setStyleSheet("margin: 0;");
- QString content(
- "<html>"
- "<head/>"
- "<body>"
- " <span style=\"font-size: 10pt; color: #171919;\">"
- " %1"
- " </span>"
- "</body>"
- "</html>");
- content_label_->setText(content.arg(replaceEmoji(body)));
- content_label_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction);
- content_label_->setOpenExternalLinks(true);
+ QString content("<span style=\"color: #171919;\">%1</span>");
+
+ body_ = new QLabel(this);
+ body_->setWordWrap(true);
+ body_->setFont(bodyFont_);
+ body_->setText(content.arg(replaceEmoji(body)));
+ body_->setAlignment(Qt::AlignTop);
+
+ body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction);
+ body_->setOpenExternalLinks(true);
}
+// The username/timestamp is displayed along with the message body.
void TimelineItem::generateBody(const QString &userid, const QString &color, const QString &body)
{
auto sender = userid;
@@ -150,64 +234,35 @@ void TimelineItem::generateBody(const QString &userid, const QString &color, con
if (userid.split(":")[0].split("@").size() > 1)
sender = userid.split(":")[0].split("@")[1];
- content_label_ = new QLabel(this);
- content_label_->setWordWrap(true);
- content_label_->setAlignment(Qt::AlignTop);
- content_label_->setStyleSheet("margin: 0;");
- QString content(
- "<html>"
- "<head/>"
- "<body>"
- " <span style=\"font-size: 10pt; font-weight: 600; color: %1\">"
- " %2"
- " </span>"
- " <span style=\"font-size: 10pt; color: #171919;\">"
- " %3"
- " </span>"
- "</body>"
- "</html>");
- content_label_->setText(content.arg(color).arg(sender).arg(replaceEmoji(body)));
- content_label_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction);
- content_label_->setOpenExternalLinks(true);
-}
-
-void TimelineItem::generateTimestamp(const QDateTime &time)
-{
- auto local_time = time.toString("HH:mm");
+ QString userContent("<span style=\"color: %1\"> %2 </span>");
+ QString bodyContent("<span style=\"color: #171717;\"> %1 </span>");
- time_label_ = new QLabel(this);
- QString msg(
- "<html>"
- "<head/>"
- "<body>"
- " <span style=\"font-size: 7pt; color: #5d6565;\">"
- " %1"
- " </span>"
- "</body>"
- "</html>");
- time_label_->setText(msg.arg(local_time));
- time_label_->setStyleSheet("margin-left: 7px; margin-right: 7px; margin-top: 0;");
- time_label_->setAlignment(Qt::AlignTop);
-}
+ userName_ = new QLabel(this);
+ userName_->setFont(usernameFont_);
+ userName_->setText(userContent.arg(color).arg(sender));
+ userName_->setAlignment(Qt::AlignTop);
-void TimelineItem::setupLayout()
-{
- if (time_label_ == nullptr) {
- qWarning() << "TimelineItem: Time label is not initialized";
+ if (body.isEmpty())
return;
- }
- if (content_label_ == nullptr) {
- qWarning() << "TimelineItem: Content label is not initialized";
- return;
- }
+ body_ = new QLabel(this);
+ body_->setFont(bodyFont_);
+ body_->setWordWrap(true);
+ body_->setAlignment(Qt::AlignTop);
+ body_->setText(bodyContent.arg(replaceEmoji(body)));
+ body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction);
+ body_->setOpenExternalLinks(true);
+}
- top_layout_ = new QHBoxLayout();
- top_layout_->setMargin(0);
- top_layout_->addWidget(time_label_);
- top_layout_->addWidget(content_label_, 1);
+void TimelineItem::generateTimestamp(const QDateTime &time)
+{
+ QString msg("<span style=\"color: #5d6565;\"> %1 </span>");
- setLayout(top_layout_);
+ timestamp_ = new QLabel(this);
+ timestamp_->setFont(timestampFont_);
+ timestamp_->setText(msg.arg(time.toString("HH:mm")));
+ timestamp_->setAlignment(Qt::AlignTop);
+ timestamp_->setStyleSheet("margin-top: 2px;");
}
QString TimelineItem::replaceEmoji(const QString &body)
@@ -227,6 +282,46 @@ QString TimelineItem::replaceEmoji(const QString &body)
return fmtBody;
}
+void TimelineItem::setupAvatarLayout(const QString &userName)
+{
+ topLayout_->setContentsMargins(7, 6, 0, 0);
+
+ userAvatar_ = new Avatar(this);
+ userAvatar_->setLetter(QChar(userName[0]).toUpper());
+ userAvatar_->setBackgroundColor(QColor("#eee"));
+ userAvatar_->setTextColor(QColor("black"));
+ userAvatar_->setSize(32);
+
+ // TODO: The provided user name should be a UserId class
+ if (userName[0] == '@' && userName.size() > 1)
+ userAvatar_->setLetter(QChar(userName[1]).toUpper());
+
+ sideLayout_->addWidget(userAvatar_);
+ sideLayout_->addStretch(1);
+ sideLayout_->setMargin(0);
+ sideLayout_->setSpacing(0);
+
+ headerLayout_->addWidget(userName_);
+ headerLayout_->addWidget(timestamp_, 1);
+ headerLayout_->setMargin(0);
+}
+
+void TimelineItem::setupSimpleLayout()
+{
+ sideLayout_->addWidget(timestamp_);
+ sideLayout_->addStretch(1);
+
+ topLayout_->setContentsMargins(9, 0, 0, 0);
+}
+
+void TimelineItem::setUserAvatar(const QImage &avatar)
+{
+ if (userAvatar_ == nullptr)
+ return;
+
+ userAvatar_->setImage(avatar);
+}
+
TimelineItem::~TimelineItem()
{
}
diff --git a/src/main.cc b/src/main.cc
index e6d4c4e7..bf165cab 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -36,9 +36,7 @@ int main(int argc, char *argv[])
QFontDatabase::addApplicationFont(":/fonts/fonts/OpenSans/OpenSans-Regular.ttf");
QFontDatabase::addApplicationFont(":/fonts/fonts/OpenSans/OpenSans-Italic.ttf");
QFontDatabase::addApplicationFont(":/fonts/fonts/OpenSans/OpenSans-Bold.ttf");
- QFontDatabase::addApplicationFont(":/fonts/fonts/OpenSans/OpenSans-BoldItalic.ttf");
QFontDatabase::addApplicationFont(":/fonts/fonts/OpenSans/OpenSans-Semibold.ttf");
- QFontDatabase::addApplicationFont(":/fonts/fonts/OpenSans/OpenSans-SemiboldItalic.ttf");
QFontDatabase::addApplicationFont(":/fonts/fonts/EmojiOne/emojione-android.ttf");
app.setWindowIcon(QIcon(":/logos/nheko.png"));
|