Newer
Older
/*
* 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 <QFontDatabase>
#include "timeline/TimelineItem.h"
#include "timeline/widgets/AudioItem.h"
#include "timeline/widgets/FileItem.h"
#include "timeline/widgets/ImageItem.h"
#include "timeline/widgets/VideoItem.h"
constexpr const static char *CHECKMARK = "✓";
userAvatar_ = nullptr;
timestamp_ = nullptr;
userName_ = nullptr;
body_ = nullptr;
usernameFont_ = font_;
usernameFont_.setWeight(60);
contextMenu_ = new QMenu(this);
showReadReceipts_ = new QAction("Read receipts", this);
markAsRead_ = new QAction("Mark as read", this);
redactMsg_ = new QAction("Redact message", this);
contextMenu_->addAction(showReadReceipts_);
contextMenu_->addAction(markAsRead_);
contextMenu_->addAction(redactMsg_);
connect(showReadReceipts_, &QAction::triggered, this, [this]() {
if (!event_id_.isEmpty())
ChatPage::instance()->showReadReceipts(event_id_);
});
connect(redactMsg_, &QAction::triggered, this, [this]() {
if (!event_id_.isEmpty())
ChatPage::instance()->redactEvent(room_id_, event_id_);
});
connect(markAsRead_, &QAction::triggered, this, [this]() { sendReadReceipt(); });
topLayout_ = new QHBoxLayout(this);
mainLayout_ = new QVBoxLayout;
messageLayout_ = new QHBoxLayout;
messageLayout_->setContentsMargins(0, 0, 20, 4);
messageLayout_->setSpacing(20);
topLayout_->setContentsMargins(conf::timeline::msgMargin, conf::timeline::msgMargin, 0, 0);
topLayout_->setSpacing(0);
topLayout_->addLayout(mainLayout_, 1);
mainLayout_->setContentsMargins(conf::timeline::headerLeftMargin, 0, 0, 0);
mainLayout_->setSpacing(0);
QFont checkmarkFont;
checkmarkFont.setPixelSize(conf::timeline::fonts::timestamp);
// Setting fixed width for checkmark because systems may have a differing width for a
// space and the Unicode checkmark.
checkmark_->setFont(checkmarkFont);
checkmark_->setFixedWidth(QFontMetrics{checkmarkFont}.width(CHECKMARK));
* For messages created locally.
TimelineItem::TimelineItem(mtx::events::MessageType ty,
const QString &userid,
QString body,
bool withSender,
QWidget *parent)
auto displayName = TimelineViewManager::displayName(userid);
auto timestamp = QDateTime::currentDateTime();
if (ty == mtx::events::MessageType::Emote) {
body = QString("* %1 %2").arg(displayName).arg(body);
Konstantinos Sideris
committed
descriptionMsg_ = {"", userid, body, utils::descriptiveTime(timestamp), timestamp};
Konstantinos Sideris
committed
descriptionMsg_ = {
"You: ", userid, body, utils::descriptiveTime(timestamp), timestamp};
body.replace(conf::strings::url_regex, conf::strings::url_html);
body.replace("\n", "<br/>");
generateTimestamp(timestamp);
if (withSender) {
generateBody(displayName, body);
setupAvatarLayout(displayName);
messageLayout_->addLayout(headerLayout_, 1);
AvatarProvider::resolve(userid, [this](const QImage &img) { setUserAvatar(img); });
} else {
generateBody(body);
setupSimpleLayout();
messageLayout_->addWidget(body_, 1);
messageLayout_->addWidget(checkmark_);
messageLayout_->addWidget(timestamp_);
mainLayout_->addLayout(messageLayout_);
TimelineItem::TimelineItem(ImageItem *image,
const QString &userid,
bool withSender,
QWidget *parent)
setupLocalWidgetLayout<ImageItem>(image, userid, "sent an image", withSender);
addSaveImageAction(image);
TimelineItem::TimelineItem(FileItem *file, const QString &userid, bool withSender, QWidget *parent)
: QWidget{parent}
{
init();
setupLocalWidgetLayout<FileItem>(file, userid, "sent a file", withSender);
TimelineItem::TimelineItem(AudioItem *audio,
const QString &userid,
bool withSender,
QWidget *parent)
: QWidget{parent}
{
init();
setupLocalWidgetLayout<AudioItem>(audio, userid, "sent an audio clip", withSender);
}
TimelineItem::TimelineItem(VideoItem *video,
const QString &userid,
bool withSender,
QWidget *parent)
: QWidget{parent}
{
init();
setupLocalWidgetLayout<VideoItem>(video, userid, "sent a video clip", withSender);
}
TimelineItem::TimelineItem(ImageItem *image,
const mtx::events::RoomEvent<mtx::events::msg::Image> &event,
setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Image>, ImageItem>(
image, event, " sent an image", with_sender);
addSaveImageAction(image);
TimelineItem::TimelineItem(FileItem *file,
const mtx::events::RoomEvent<mtx::events::msg::File> &event,
bool with_sender,
QWidget *parent)
: QWidget(parent)
{
setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::File>, FileItem>(
file, event, " sent a file", with_sender);
}
TimelineItem::TimelineItem(AudioItem *audio,
const mtx::events::RoomEvent<mtx::events::msg::Audio> &event,
bool with_sender,
QWidget *parent)
: QWidget(parent)
{
setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Audio>, AudioItem>(
audio, event, " sent an audio clip", with_sender);
TimelineItem::TimelineItem(VideoItem *video,
const mtx::events::RoomEvent<mtx::events::msg::Video> &event,
bool with_sender,
QWidget *parent)
: QWidget(parent)
{
setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Video>, VideoItem>(
video, event, " sent a video clip", with_sender);
}
/*
* Used to display remote notice messages.
*/
TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &event,
event_id_ = QString::fromStdString(event.event_id);
const auto sender = QString::fromStdString(event.sender);
const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();
descriptionMsg_ = {TimelineViewManager::displayName(sender),
sender,
Konstantinos Sideris
committed
utils::descriptiveTime(timestamp),
body.replace(conf::strings::url_regex, conf::strings::url_html);
body.replace("\n", "<br/>");
body = "<i>" + body + "</i>";
auto displayName = TimelineViewManager::displayName(sender);
generateBody(displayName, body);
setupAvatarLayout(displayName);
messageLayout_->addLayout(headerLayout_, 1);
AvatarProvider::resolve(sender, [this](const QImage &img) { setUserAvatar(img); });
} else {
generateBody(body);
setupSimpleLayout();
messageLayout_->addWidget(body_, 1);
messageLayout_->addWidget(checkmark_);
messageLayout_->addWidget(timestamp_);
mainLayout_->addLayout(messageLayout_);
/*
* Used to display remote emote messages.
*/
TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> &event,
bool with_sender,
QWidget *parent)
: QWidget(parent)
{
init();
event_id_ = QString::fromStdString(event.event_id);
const auto sender = QString::fromStdString(event.sender);
auto body = QString::fromStdString(event.content.body).trimmed();
auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
auto displayName = TimelineViewManager::displayName(sender);
auto emoteMsg = QString("* %1 %2").arg(displayName).arg(body);
Konstantinos Sideris
committed
descriptionMsg_ = {"", sender, emoteMsg, utils::descriptiveTime(timestamp), timestamp};
generateTimestamp(timestamp);
emoteMsg.replace(conf::strings::url_regex, conf::strings::url_html);
emoteMsg.replace("\n", "<br/>");
if (with_sender) {
generateBody(displayName, emoteMsg);
setupAvatarLayout(displayName);
messageLayout_->addLayout(headerLayout_, 1);
AvatarProvider::resolve(sender, [this](const QImage &img) { setUserAvatar(img); });
} else {
generateBody(emoteMsg);
setupSimpleLayout();
messageLayout_->addWidget(body_, 1);
messageLayout_->addWidget(checkmark_);
messageLayout_->addWidget(timestamp_);
mainLayout_->addLayout(messageLayout_);
/*
* Used to display remote text messages.
*/
TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text> &event,
event_id_ = QString::fromStdString(event.event_id);
const auto sender = QString::fromStdString(event.sender);
auto body = QString::fromStdString(event.content.body).trimmed();
auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
auto displayName = TimelineViewManager::displayName(sender);
descriptionMsg_ = {sender == settings.value("auth/user_id") ? "You" : displayName,
sender,
Konstantinos Sideris
committed
utils::descriptiveTime(timestamp),
body.replace(conf::strings::url_regex, conf::strings::url_html);
body.replace("\n", "<br/>");
if (with_sender) {
generateBody(displayName, body);
setupAvatarLayout(displayName);
messageLayout_->addLayout(headerLayout_, 1);
AvatarProvider::resolve(sender, [this](const QImage &img) { setUserAvatar(img); });
} else {
generateBody(body);
setupSimpleLayout();
messageLayout_->addWidget(body_, 1);
messageLayout_->addWidget(checkmark_);
messageLayout_->addWidget(timestamp_);
mainLayout_->addLayout(messageLayout_);
Konstantinos Sideris
committed
void
TimelineItem::markReceived()
{
checkmark_->setText(CHECKMARK);
checkmark_->setAlignment(Qt::AlignTop);
sendReadReceipt();
Konstantinos Sideris
committed
}
// Only the body is displayed.
void
TimelineItem::generateBody(const QString &body)
QString content("<span>%1</span>");
body_ = new QLabel(this);
body_->setFont(font_);
body_->setWordWrap(true);
body_->setText(content.arg(replaceEmoji(body)));
body_->setMargin(0);
body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction);
body_->setOpenExternalLinks(true);
// The username/timestamp is displayed along with the message body.
TimelineItem::generateBody(const QString &userid, const QString &body)
if (userid.startsWith("@")) {
// TODO: Fix this by using a UserId type.
if (userid.split(":")[0].split("@").size() > 1)
sender = userid.split(":")[0].split("@")[1];
}
userName_->setFont(usernameFont_);
userName_->setText(fm.elidedText(sender, Qt::ElideRight, 500));
void
TimelineItem::generateTimestamp(const QDateTime &time)
QFont timestampFont;
timestampFont.setPixelSize(conf::timeline::fonts::timestamp);
QFontMetrics fm(timestampFont);
int topMargin = QFontMetrics(font_).ascent() - fm.ascent();
timestamp_->setAlignment(Qt::AlignTop);
timestamp_->setText(
QString("<span style=\"color: #999\"> %1 </span>").arg(time.toString("HH:mm")));
timestamp_->setContentsMargins(0, topMargin, 0, 0);
timestamp_->setStyleSheet(
QString("font-size: %1px;").arg(conf::timeline::fonts::timestamp));
QString
TimelineItem::replaceEmoji(const QString &body)
QVector<uint> utf32_string = body.toUcs4();
for (auto &code : utf32_string) {
// TODO: Be more precise here.
if (code > 9000)
fmtBody += QString("<span style=\"font-family: Emoji "
"One; font-size: %1px\">")
.arg(conf::emojiSize) +
QString::fromUcs4(&code, 1) + "</span>";
fmtBody += QString::fromUcs4(&code, 1);
void
TimelineItem::setupAvatarLayout(const QString &userName)
topLayout_->setContentsMargins(conf::timeline::msgMargin, conf::timeline::msgMargin, 0, 0);
userAvatar_ = new Avatar(this);
userAvatar_->setLetter(QChar(userName[0]).toUpper());
userAvatar_->setSize(conf::timeline::avatarSize);
// TODO: The provided user name should be a UserId class
if (userName[0] == '@' && userName.size() > 1)
userAvatar_->setLetter(QChar(userName[1]).toUpper());
topLayout_->insertWidget(0, userAvatar_);
topLayout_->setAlignment(userAvatar_, Qt::AlignTop);
topLayout_->setContentsMargins(conf::timeline::msgMargin + conf::timeline::avatarSize + 2,
conf::timeline::msgMargin,
void
TimelineItem::setUserAvatar(const QImage &avatar)
void
TimelineItem::contextMenuEvent(QContextMenuEvent *event)
{
if (contextMenu_)
contextMenu_->exec(event->globalPos());
void
TimelineItem::paintEvent(QPaintEvent *)
{
QStyleOption opt;
opt.init(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}
void
TimelineItem::addSaveImageAction(ImageItem *image)
{
if (contextMenu_) {
auto saveImage = new QAction("Save image", this);
contextMenu_->addAction(saveImage);
connect(saveImage, &QAction::triggered, image, &ImageItem::saveAs);
}
}
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
void
TimelineItem::addAvatar()
{
if (userAvatar_)
return;
// TODO: should be replaced with the proper event struct.
auto userid = descriptionMsg_.userid;
auto displayName = TimelineViewManager::displayName(userid);
QFontMetrics fm(usernameFont_);
userName_ = new QLabel(this);
userName_->setFont(usernameFont_);
userName_->setText(fm.elidedText(displayName, Qt::ElideRight, 500));
QWidget *widget = nullptr;
// Extract the widget before we delete its layout.
if (widgetLayout_)
widget = widgetLayout_->itemAt(0)->widget();
// Remove all items from the layout.
QLayoutItem *item;
while ((item = messageLayout_->takeAt(0)) != 0)
delete item;
setupAvatarLayout(displayName);
// Restore widget's layout.
if (widget) {
widgetLayout_ = new QHBoxLayout();
widgetLayout_->setContentsMargins(0, 5, 0, 0);
widgetLayout_->addWidget(widget);
widgetLayout_->addStretch(1);
headerLayout_->addLayout(widgetLayout_);
}
messageLayout_->addLayout(headerLayout_, 1);
messageLayout_->addWidget(checkmark_);
messageLayout_->addWidget(timestamp_);
AvatarProvider::resolve(userid, [this](const QImage &img) { setUserAvatar(img); });
}