Skip to content
Snippets Groups Projects
Commit 95c492ba authored by Konstantinos Sideris's avatar Konstantinos Sideris
Browse files

Experimental support for user avatars in timeline

parent b8c8fed6
No related branches found
No related tags found
No related merge requests found
......@@ -78,6 +78,7 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU")
endif()
set(SRC_FILES
src/AvatarProvider.cc
src/ChatPage.cc
src/Deserializable.cc
src/EmojiCategory.cc
......@@ -160,6 +161,7 @@ include_directories(include/events)
include_directories(include/events/messages)
qt5_wrap_cpp(MOC_HEADERS
include/AvatarProvider.h
include/ChatPage.h
include/EmojiCategory.h
include/EmojiItemDelegate.h
......
/*
* 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/>.
*/
#pragma once
#include <QImage>
#include <QObject>
#include <QSharedPointer>
#include <QUrl>
#include "MatrixClient.h"
#include "TimelineItem.h"
class AvatarProvider : public QObject
{
Q_OBJECT
public:
static void init(QSharedPointer<MatrixClient> client);
static void resolve(const QString &userId, TimelineItem *item);
static void setAvatarUrl(const QString &userId, const QUrl &url);
static void clear();
private:
static void updateAvatar(const QString &uid, const QImage &img);
static QSharedPointer<MatrixClient> client_;
static QMap<QString, QList<TimelineItem *>> toBeResolved_;
static QMap<QString, QImage> userAvatars_;
static QMap<QString, QUrl> avatarUrls_;
};
......@@ -41,6 +41,7 @@ public:
void registerUser(const QString &username, const QString &password, const QString &server) noexcept;
void versions() noexcept;
void fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url);
void fetchUserAvatar(const QString &userId, const QUrl &avatarUrl);
void fetchOwnAvatar(const QUrl &avatar_url);
void downloadImage(const QString &event_id, const QUrl &url);
void messages(const QString &room_id, const QString &from_token) noexcept;
......@@ -69,6 +70,7 @@ signals:
void registerSuccess(const QString &userid, const QString &homeserver, const QString &token);
void roomAvatarRetrieved(const QString &roomid, const QPixmap &img);
void userAvatarRetrieved(const QString &userId, const QImage &img);
void ownAvatarRetrieved(const QPixmap &img);
void imageDownloaded(const QString &event_id, const QPixmap &img);
......@@ -95,6 +97,7 @@ private:
Messages,
Register,
RoomAvatar,
UserAvatar,
SendTextMessage,
Sync,
Versions,
......@@ -111,6 +114,7 @@ private:
void onInitialSyncResponse(QNetworkReply *reply);
void onSyncResponse(QNetworkReply *reply);
void onRoomAvatarResponse(QNetworkReply *reply);
void onUserAvatarResponse(QNetworkReply *reply);
void onImageResponse(QNetworkReply *reply);
void onMessagesResponse(QNetworkReply *reply);
......
......@@ -24,6 +24,7 @@
#include "ImageItem.h"
#include "Sync.h"
#include "Avatar.h"
#include "Image.h"
#include "MessageEvent.h"
#include "Notice.h"
......@@ -46,19 +47,35 @@ public:
TimelineItem(ImageItem *img, const events::MessageEvent<msgs::Image> &e, const QString &color, QWidget *parent);
TimelineItem(ImageItem *img, const events::MessageEvent<msgs::Image> &e, QWidget *parent);
void setUserAvatar(const QImage &pixmap);
~TimelineItem();
private:
void init();
void generateBody(const QString &body);
void generateBody(const QString &userid, const QString &color, const QString &body);
void generateTimestamp(const QDateTime &time);
void setupAvatarLayout(const QString &userName);
void setupSimpleLayout();
QString replaceEmoji(const QString &body);
void setupLayout();
QHBoxLayout *topLayout_;
QVBoxLayout *sideLayout_; // Avatar or Timestamp
QVBoxLayout *mainLayout_; // Header & Message body
QHBoxLayout *headerLayout_; // Username (&) Timestamp
Avatar *userAvatar_;
QHBoxLayout *top_layout_;
QLabel *timestamp_;
QLabel *userName_;
QLabel *body_;
QLabel *time_label_;
QLabel *content_label_;
QFont bodyFont_;
QFont usernameFont_;
QFont timestampFont_;
};
/*
* 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();
}
......@@ -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());
......
......@@ -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);
......
......@@ -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);
}
QString userContent("<span style=\"color: %1\"> %2 </span>");
QString bodyContent("<span style=\"color: #171717;\"> %1 </span>");
void TimelineItem::generateTimestamp(const QDateTime &time)
{
auto local_time = time.toString("HH:mm");
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()
{
}
......@@ -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"));
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment