Experimental support for user avatars in timeline

This commit is contained in:
Konstantinos Sideris 2017-06-05 02:14:05 +03:00
parent b8c8fed655
commit 95c492bad8
9 changed files with 413 additions and 102 deletions

View File

@ -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

47
include/AvatarProvider.h Normal file
View File

@ -0,0 +1,47 @@
/*
* 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_;
};

View File

@ -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);

View File

@ -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 *top_layout_;
QHBoxLayout *headerLayout_; // Username (&) Timestamp
QLabel *time_label_;
QLabel *content_label_;
Avatar *userAvatar_;
QLabel *timestamp_;
QLabel *userName_;
QLabel *body_;
QFont bodyFont_;
QFont usernameFont_;
QFont timestampFont_;
};

83
src/AvatarProvider.cc Normal file
View File

@ -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();
}

View File

@ -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());

View File

@ -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);

View File

@ -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>");
userName_ = new QLabel(this);
userName_->setFont(usernameFont_);
userName_->setText(userContent.arg(color).arg(sender));
userName_->setAlignment(Qt::AlignTop);
if (body.isEmpty())
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);
}
void TimelineItem::generateTimestamp(const QDateTime &time)
{
auto local_time = time.toString("HH:mm");
QString msg("<span style=\"color: #5d6565;\"> %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);
}
void TimelineItem::setupLayout()
{
if (time_label_ == nullptr) {
qWarning() << "TimelineItem: Time label is not initialized";
return;
}
if (content_label_ == nullptr) {
qWarning() << "TimelineItem: Content label is not initialized";
return;
}
top_layout_ = new QHBoxLayout();
top_layout_->setMargin(0);
top_layout_->addWidget(time_label_);
top_layout_->addWidget(content_label_, 1);
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()
{
}

View File

@ -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"));