Remove old timeline

This commit is contained in:
Nicolas Werner 2019-11-09 03:06:10 +01:00
parent e8f8182844
commit 91d1f19058
24 changed files with 413 additions and 5420 deletions

View File

@ -192,16 +192,9 @@ set(SRC_FILES
src/emoji/Provider.cpp
# Timeline
src/timeline2/TimelineViewManager.cpp
src/timeline2/TimelineModel.cpp
src/timeline2/DelegateChooser.cpp
#src/timeline/TimelineViewManager.cpp
#src/timeline/TimelineItem.cpp
#src/timeline/TimelineView.cpp
#src/timeline/widgets/AudioItem.cpp
#src/timeline/widgets/FileItem.cpp
#src/timeline/widgets/ImageItem.cpp
#src/timeline/widgets/VideoItem.cpp
src/timeline/TimelineViewManager.cpp
src/timeline/TimelineModel.cpp
src/timeline/DelegateChooser.cpp
# UI components
src/ui/Avatar.cpp
@ -339,16 +332,9 @@ qt5_wrap_cpp(MOC_HEADERS
src/emoji/PickButton.h
# Timeline
src/timeline2/TimelineViewManager.h
src/timeline2/TimelineModel.h
src/timeline2/DelegateChooser.h
#src/timeline/TimelineItem.h
#src/timeline/TimelineView.h
#src/timeline/TimelineViewManager.h
#src/timeline/widgets/AudioItem.h
#src/timeline/widgets/FileItem.h
#src/timeline/widgets/ImageItem.h
#src/timeline/widgets/VideoItem.h
src/timeline/TimelineViewManager.h
src/timeline/TimelineModel.h
src/timeline/DelegateChooser.h
# UI components
src/ui/Avatar.h

View File

@ -44,7 +44,7 @@
#include "dialogs/ReadReceipts.h"
#include "popups/UserMentions.h"
#include "timeline2/TimelineViewManager.h"
#include "timeline/TimelineViewManager.h"
// TODO: Needs to be updated with an actual secret.
static const std::string STORAGE_SECRET_KEY("secret");

View File

@ -4,10 +4,6 @@
#include "Cache.h"
#include "RoomInfoListItem.h"
#include "timeline/widgets/AudioItem.h"
#include "timeline/widgets/FileItem.h"
#include "timeline/widgets/ImageItem.h"
#include "timeline/widgets/VideoItem.h"
#include <QCoreApplication>
#include <QDateTime>
@ -94,7 +90,7 @@ messageDescription(const QString &username = "",
using Video = mtx::events::RoomEvent<mtx::events::msg::Video>;
using Encrypted = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>;
if (std::is_same<T, AudioItem>::value || std::is_same<T, Audio>::value) {
if (std::is_same<T, Audio>::value) {
if (isLocal)
return QCoreApplication::translate("message-description sent:",
"You sent an audio clip");
@ -102,7 +98,7 @@ messageDescription(const QString &username = "",
return QCoreApplication::translate("message-description sent:",
"%1 sent an audio clip")
.arg(username);
} else if (std::is_same<T, ImageItem>::value || std::is_same<T, Image>::value) {
} else if (std::is_same<T, Image>::value) {
if (isLocal)
return QCoreApplication::translate("message-description sent:",
"You sent an image");
@ -110,7 +106,7 @@ messageDescription(const QString &username = "",
return QCoreApplication::translate("message-description sent:",
"%1 sent an image")
.arg(username);
} else if (std::is_same<T, FileItem>::value || std::is_same<T, File>::value) {
} else if (std::is_same<T, File>::value) {
if (isLocal)
return QCoreApplication::translate("message-description sent:",
"You sent a file");
@ -118,7 +114,7 @@ messageDescription(const QString &username = "",
return QCoreApplication::translate("message-description sent:",
"%1 sent a file")
.arg(username);
} else if (std::is_same<T, VideoItem>::value || std::is_same<T, Video>::value) {
} else if (std::is_same<T, Video>::value) {
if (isLocal)
return QCoreApplication::translate("message-description sent:",
"You sent a video");
@ -126,7 +122,7 @@ messageDescription(const QString &username = "",
return QCoreApplication::translate("message-description sent:",
"%1 sent a video")
.arg(username);
} else if (std::is_same<T, StickerItem>::value || std::is_same<T, Sticker>::value) {
} else if (std::is_same<T, Sticker>::value) {
if (isLocal)
return QCoreApplication::translate("message-description sent:",
"You sent a sticker");

View File

@ -1,4 +1,5 @@
#include <QAbstractSlider>
#include <QLabel>
#include <QListWidgetItem>
#include <QPainter>
#include <QPushButton>

View File

@ -1,960 +0,0 @@
/*
* 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 <functional>
#include <QContextMenuEvent>
#include <QDesktopServices>
#include <QFontDatabase>
#include <QMenu>
#include <QTimer>
#include <QtGlobal>
#include "ChatPage.h"
#include "Config.h"
#include "Logging.h"
#include "MainWindow.h"
#include "Olm.h"
#include "ui/Avatar.h"
#include "ui/Painter.h"
#include "ui/TextLabel.h"
#include "timeline/TimelineItem.h"
#include "timeline/widgets/AudioItem.h"
#include "timeline/widgets/FileItem.h"
#include "timeline/widgets/ImageItem.h"
#include "timeline/widgets/VideoItem.h"
#include "dialogs/RawMessage.h"
#include "mtx/identifiers.hpp"
constexpr int MSG_RIGHT_MARGIN = 7;
constexpr int MSG_PADDING = 20;
StatusIndicator::StatusIndicator(QWidget *parent)
: QWidget(parent)
{
lockIcon_.addFile(":/icons/icons/ui/lock.png");
clockIcon_.addFile(":/icons/icons/ui/clock.png");
checkmarkIcon_.addFile(":/icons/icons/ui/checkmark.png");
doubleCheckmarkIcon_.addFile(":/icons/icons/ui/double-tick-indicator.png");
}
void
StatusIndicator::paintIcon(QPainter &p, QIcon &icon)
{
auto pixmap = icon.pixmap(width());
QPainter painter(&pixmap);
painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
painter.fillRect(pixmap.rect(), p.pen().color());
QIcon(pixmap).paint(&p, rect(), Qt::AlignCenter, QIcon::Normal);
}
void
StatusIndicator::paintEvent(QPaintEvent *)
{
if (state_ == StatusIndicatorState::Empty)
return;
Painter p(this);
PainterHighQualityEnabler hq(p);
p.setPen(iconColor_);
switch (state_) {
case StatusIndicatorState::Sent: {
paintIcon(p, clockIcon_);
break;
}
case StatusIndicatorState::Encrypted:
paintIcon(p, lockIcon_);
break;
case StatusIndicatorState::Received: {
paintIcon(p, checkmarkIcon_);
break;
}
case StatusIndicatorState::Read: {
paintIcon(p, doubleCheckmarkIcon_);
break;
}
case StatusIndicatorState::Empty:
break;
}
}
void
StatusIndicator::setState(StatusIndicatorState state)
{
state_ = state;
switch (state) {
case StatusIndicatorState::Encrypted:
setToolTip(tr("Encrypted"));
break;
case StatusIndicatorState::Received:
setToolTip(tr("Delivered"));
break;
case StatusIndicatorState::Read:
setToolTip(tr("Seen"));
break;
case StatusIndicatorState::Sent:
setToolTip(tr("Sent"));
break;
case StatusIndicatorState::Empty:
setToolTip("");
break;
}
update();
}
void
TimelineItem::adjustMessageLayoutForWidget()
{
messageLayout_->addLayout(widgetLayout_, 1);
actionLayout_->addWidget(replyBtn_);
actionLayout_->addWidget(contextBtn_);
messageLayout_->addLayout(actionLayout_);
messageLayout_->addWidget(statusIndicator_);
messageLayout_->addWidget(timestamp_);
actionLayout_->setAlignment(replyBtn_, Qt::AlignTop | Qt::AlignRight);
actionLayout_->setAlignment(contextBtn_, Qt::AlignTop | Qt::AlignRight);
messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop);
messageLayout_->setAlignment(timestamp_, Qt::AlignTop);
messageLayout_->setAlignment(actionLayout_, Qt::AlignTop);
mainLayout_->addLayout(messageLayout_);
}
void
TimelineItem::adjustMessageLayout()
{
messageLayout_->addWidget(body_, 1);
actionLayout_->addWidget(replyBtn_);
actionLayout_->addWidget(contextBtn_);
messageLayout_->addLayout(actionLayout_);
messageLayout_->addWidget(statusIndicator_);
messageLayout_->addWidget(timestamp_);
actionLayout_->setAlignment(replyBtn_, Qt::AlignTop | Qt::AlignRight);
actionLayout_->setAlignment(contextBtn_, Qt::AlignTop | Qt::AlignRight);
messageLayout_->setAlignment(statusIndicator_, Qt::AlignTop);
messageLayout_->setAlignment(timestamp_, Qt::AlignTop);
messageLayout_->setAlignment(actionLayout_, Qt::AlignTop);
mainLayout_->addLayout(messageLayout_);
}
void
TimelineItem::init()
{
userAvatar_ = nullptr;
timestamp_ = nullptr;
userName_ = nullptr;
body_ = nullptr;
auto buttonSize_ = 32;
contextMenu_ = new QMenu(this);
showReadReceipts_ = new QAction("Read receipts", this);
markAsRead_ = new QAction("Mark as read", this);
viewRawMessage_ = new QAction("View raw message", this);
redactMsg_ = new QAction("Redact message", this);
contextMenu_->addAction(showReadReceipts_);
contextMenu_->addAction(viewRawMessage_);
contextMenu_->addAction(markAsRead_);
contextMenu_->addAction(redactMsg_);
connect(showReadReceipts_, &QAction::triggered, this, [this]() {
if (!event_id_.isEmpty())
MainWindow::instance()->openReadReceiptsDialog(event_id_);
});
connect(this, &TimelineItem::eventRedacted, this, [this](const QString &event_id) {
emit ChatPage::instance()->removeTimelineEvent(room_id_, event_id);
});
connect(this, &TimelineItem::redactionFailed, this, [](const QString &msg) {
emit ChatPage::instance()->showNotification(msg);
});
connect(redactMsg_, &QAction::triggered, this, [this]() {
if (!event_id_.isEmpty())
http::client()->redact_event(
room_id_.toStdString(),
event_id_.toStdString(),
[this](const mtx::responses::EventId &, mtx::http::RequestErr err) {
if (err) {
emit redactionFailed(tr("Message redaction failed: %1")
.arg(QString::fromStdString(
err->matrix_error.error)));
return;
}
emit eventRedacted(event_id_);
});
});
connect(
ChatPage::instance(), &ChatPage::themeChanged, this, &TimelineItem::refreshAuthorColor);
connect(markAsRead_, &QAction::triggered, this, &TimelineItem::sendReadReceipt);
connect(viewRawMessage_, &QAction::triggered, this, &TimelineItem::openRawMessageViewer);
colorGenerating_ = new QFutureWatcher<QString>(this);
connect(colorGenerating_,
&QFutureWatcher<QString>::finished,
this,
&TimelineItem::finishedGeneratingColor);
topLayout_ = new QHBoxLayout(this);
mainLayout_ = new QVBoxLayout;
messageLayout_ = new QHBoxLayout;
actionLayout_ = new QHBoxLayout;
messageLayout_->setContentsMargins(0, 0, MSG_RIGHT_MARGIN, 0);
messageLayout_->setSpacing(MSG_PADDING);
actionLayout_->setContentsMargins(13, 1, 13, 0);
actionLayout_->setSpacing(0);
topLayout_->setContentsMargins(
conf::timeline::msgLeftMargin, conf::timeline::msgTopMargin, 0, 0);
topLayout_->setSpacing(0);
topLayout_->addLayout(mainLayout_);
mainLayout_->setContentsMargins(conf::timeline::headerLeftMargin, 0, 0, 0);
mainLayout_->setSpacing(0);
replyBtn_ = new FlatButton(this);
replyBtn_->setToolTip(tr("Reply"));
replyBtn_->setFixedSize(buttonSize_, buttonSize_);
replyBtn_->setCornerRadius(buttonSize_ / 2);
QIcon reply_icon;
reply_icon.addFile(":/icons/icons/ui/mail-reply.png");
replyBtn_->setIcon(reply_icon);
replyBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));
connect(replyBtn_, &FlatButton::clicked, this, &TimelineItem::replyAction);
contextBtn_ = new FlatButton(this);
contextBtn_->setToolTip(tr("Options"));
contextBtn_->setFixedSize(buttonSize_, buttonSize_);
contextBtn_->setCornerRadius(buttonSize_ / 2);
QIcon context_icon;
context_icon.addFile(":/icons/icons/ui/vertical-ellipsis.png");
contextBtn_->setIcon(context_icon);
contextBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));
contextBtn_->setMenu(contextMenu_);
timestampFont_.setPointSizeF(timestampFont_.pointSizeF() * 0.9);
timestampFont_.setFamily("Monospace");
timestampFont_.setStyleHint(QFont::Monospace);
QFontMetrics tsFm(timestampFont_);
statusIndicator_ = new StatusIndicator(this);
statusIndicator_->setFixedWidth(tsFm.height() - tsFm.leading());
statusIndicator_->setFixedHeight(tsFm.height() - tsFm.leading());
parentWidget()->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
}
/*
* For messages created locally.
*/
TimelineItem::TimelineItem(mtx::events::MessageType ty,
const QString &userid,
QString body,
bool withSender,
const QString &room_id,
QWidget *parent)
: QWidget(parent)
, message_type_(ty)
, room_id_{room_id}
{
init();
addReplyAction();
auto displayName = Cache::displayName(room_id_, userid);
auto timestamp = QDateTime::currentDateTime();
// Generate the html body to be rendered.
auto formatted_body = utils::markdownToHtml(body);
// Escape html if the input is not formatted.
if (formatted_body == body.trimmed().toHtmlEscaped())
formatted_body = body.toHtmlEscaped();
QString emptyEventId;
if (ty == mtx::events::MessageType::Emote) {
formatted_body = QString("<em>%1</em>").arg(formatted_body);
descriptionMsg_ = {emptyEventId,
"",
userid,
QString("* %1 %2").arg(displayName).arg(body),
utils::descriptiveTime(timestamp),
timestamp};
} else {
descriptionMsg_ = {emptyEventId,
"You: ",
userid,
body,
utils::descriptiveTime(timestamp),
timestamp};
}
formatted_body = utils::linkifyMessage(formatted_body);
formatted_body.replace("mx-reply", "div");
generateTimestamp(timestamp);
if (withSender) {
generateBody(userid, displayName, formatted_body);
setupAvatarLayout(displayName);
setUserAvatar(userid);
} else {
generateBody(formatted_body);
setupSimpleLayout();
}
adjustMessageLayout();
}
TimelineItem::TimelineItem(ImageItem *image,
const QString &userid,
bool withSender,
const QString &room_id,
QWidget *parent)
: QWidget{parent}
, message_type_(mtx::events::MessageType::Image)
, room_id_{room_id}
{
init();
setupLocalWidgetLayout<ImageItem>(image, userid, withSender);
addSaveImageAction(image);
}
TimelineItem::TimelineItem(FileItem *file,
const QString &userid,
bool withSender,
const QString &room_id,
QWidget *parent)
: QWidget{parent}
, message_type_(mtx::events::MessageType::File)
, room_id_{room_id}
{
init();
setupLocalWidgetLayout<FileItem>(file, userid, withSender);
}
TimelineItem::TimelineItem(AudioItem *audio,
const QString &userid,
bool withSender,
const QString &room_id,
QWidget *parent)
: QWidget{parent}
, message_type_(mtx::events::MessageType::Audio)
, room_id_{room_id}
{
init();
setupLocalWidgetLayout<AudioItem>(audio, userid, withSender);
}
TimelineItem::TimelineItem(VideoItem *video,
const QString &userid,
bool withSender,
const QString &room_id,
QWidget *parent)
: QWidget{parent}
, message_type_(mtx::events::MessageType::Video)
, room_id_{room_id}
{
init();
setupLocalWidgetLayout<VideoItem>(video, userid, withSender);
}
TimelineItem::TimelineItem(ImageItem *image,
const mtx::events::RoomEvent<mtx::events::msg::Image> &event,
bool with_sender,
const QString &room_id,
QWidget *parent)
: QWidget(parent)
, message_type_(mtx::events::MessageType::Image)
, room_id_{room_id}
{
setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Image>, ImageItem>(
image, event, with_sender);
markOwnMessagesAsReceived(event.sender);
addSaveImageAction(image);
}
TimelineItem::TimelineItem(StickerItem *image,
const mtx::events::Sticker &event,
bool with_sender,
const QString &room_id,
QWidget *parent)
: QWidget(parent)
, room_id_{room_id}
{
setupWidgetLayout<mtx::events::Sticker, StickerItem>(image, event, with_sender);
markOwnMessagesAsReceived(event.sender);
addSaveImageAction(image);
}
TimelineItem::TimelineItem(FileItem *file,
const mtx::events::RoomEvent<mtx::events::msg::File> &event,
bool with_sender,
const QString &room_id,
QWidget *parent)
: QWidget(parent)
, message_type_(mtx::events::MessageType::File)
, room_id_{room_id}
{
setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::File>, FileItem>(
file, event, with_sender);
markOwnMessagesAsReceived(event.sender);
}
TimelineItem::TimelineItem(AudioItem *audio,
const mtx::events::RoomEvent<mtx::events::msg::Audio> &event,
bool with_sender,
const QString &room_id,
QWidget *parent)
: QWidget(parent)
, message_type_(mtx::events::MessageType::Audio)
, room_id_{room_id}
{
setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Audio>, AudioItem>(
audio, event, with_sender);
markOwnMessagesAsReceived(event.sender);
}
TimelineItem::TimelineItem(VideoItem *video,
const mtx::events::RoomEvent<mtx::events::msg::Video> &event,
bool with_sender,
const QString &room_id,
QWidget *parent)
: QWidget(parent)
, message_type_(mtx::events::MessageType::Video)
, room_id_{room_id}
{
setupWidgetLayout<mtx::events::RoomEvent<mtx::events::msg::Video>, VideoItem>(
video, event, with_sender);
markOwnMessagesAsReceived(event.sender);
}
/*
* Used to display remote notice messages.
*/
TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &event,
bool with_sender,
const QString &room_id,
QWidget *parent)
: QWidget(parent)
, message_type_(mtx::events::MessageType::Notice)
, room_id_{room_id}
{
init();
addReplyAction();
markOwnMessagesAsReceived(event.sender);
event_id_ = QString::fromStdString(event.event_id);
const auto sender = QString::fromStdString(event.sender);
const auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());
auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();
descriptionMsg_ = {event_id_,
Cache::displayName(room_id_, sender),
sender,
" sent a notification",
utils::descriptiveTime(timestamp),
timestamp};
generateTimestamp(timestamp);
if (with_sender) {
auto displayName = Cache::displayName(room_id_, sender);
generateBody(sender, displayName, formatted_body);
setupAvatarLayout(displayName);
setUserAvatar(sender);
} else {
generateBody(formatted_body);
setupSimpleLayout();
}
adjustMessageLayout();
}
/*
* Used to display remote emote messages.
*/
TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> &event,
bool with_sender,
const QString &room_id,
QWidget *parent)
: QWidget(parent)
, message_type_(mtx::events::MessageType::Emote)
, room_id_{room_id}
{
init();
addReplyAction();
markOwnMessagesAsReceived(event.sender);
event_id_ = QString::fromStdString(event.event_id);
const auto sender = QString::fromStdString(event.sender);
auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());
auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();
auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
auto displayName = Cache::displayName(room_id_, sender);
formatted_body = QString("<em>%1</em>").arg(formatted_body);
descriptionMsg_ = {event_id_,
"",
sender,
QString("* %1 %2").arg(displayName).arg(body),
utils::descriptiveTime(timestamp),
timestamp};
generateTimestamp(timestamp);
if (with_sender) {
generateBody(sender, displayName, formatted_body);
setupAvatarLayout(displayName);
setUserAvatar(sender);
} else {
generateBody(formatted_body);
setupSimpleLayout();
}
adjustMessageLayout();
}
/*
* Used to display remote text messages.
*/
TimelineItem::TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text> &event,
bool with_sender,
const QString &room_id,
QWidget *parent)
: QWidget(parent)
, message_type_(mtx::events::MessageType::Text)
, room_id_{room_id}
{
init();
addReplyAction();
markOwnMessagesAsReceived(event.sender);
event_id_ = QString::fromStdString(event.event_id);
const auto sender = QString::fromStdString(event.sender);
auto formatted_body = utils::linkifyMessage(utils::getMessageBody(event).trimmed());
auto body = QString::fromStdString(event.content.body).trimmed().toHtmlEscaped();
auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
auto displayName = Cache::displayName(room_id_, sender);
QSettings settings;
descriptionMsg_ = {event_id_,
sender == settings.value("auth/user_id") ? "You" : displayName,
sender,
QString(": %1").arg(body),
utils::descriptiveTime(timestamp),
timestamp};
generateTimestamp(timestamp);
if (with_sender) {
generateBody(sender, displayName, formatted_body);
setupAvatarLayout(displayName);
setUserAvatar(sender);
} else {
generateBody(formatted_body);
setupSimpleLayout();
}
adjustMessageLayout();
}
TimelineItem::~TimelineItem()
{
colorGenerating_->cancel();
colorGenerating_->waitForFinished();
}
void
TimelineItem::markSent()
{
statusIndicator_->setState(StatusIndicatorState::Sent);
}
void
TimelineItem::markOwnMessagesAsReceived(const std::string &sender)
{
QSettings settings;
if (sender == settings.value("auth/user_id").toString().toStdString())
statusIndicator_->setState(StatusIndicatorState::Received);
}
void
TimelineItem::markRead()
{
if (statusIndicator_->state() != StatusIndicatorState::Encrypted)
statusIndicator_->setState(StatusIndicatorState::Read);
}
void
TimelineItem::markReceived(bool isEncrypted)
{
isReceived_ = true;
if (isEncrypted)
statusIndicator_->setState(StatusIndicatorState::Encrypted);
else
statusIndicator_->setState(StatusIndicatorState::Received);
sendReadReceipt();
}
// Only the body is displayed.
void
TimelineItem::generateBody(const QString &body)
{
body_ = new TextLabel(utils::replaceEmoji(body), this);
body_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction);
connect(body_, &TextLabel::userProfileTriggered, this, [](const QString &user_id) {
MainWindow::instance()->openUserProfile(user_id,
ChatPage::instance()->currentRoom());
});
}
void
TimelineItem::refreshAuthorColor()
{
// Cancel and wait if we are already generating the color.
if (colorGenerating_->isRunning()) {
colorGenerating_->cancel();
colorGenerating_->waitForFinished();
}
if (userName_) {
// generate user's unique color.
std::function<QString()> generate = [this]() {
QString userColor = utils::generateContrastingHexColor(
userName_->toolTip(), backgroundColor().name());
return userColor;
};
QString userColor = Cache::userColor(userName_->toolTip());
// If the color is empty, then generate it asynchronously
if (userColor.isEmpty()) {
colorGenerating_->setFuture(QtConcurrent::run(generate));
} else {
userName_->setStyleSheet("QLabel { color : " + userColor + "; }");
}
}
}
void
TimelineItem::finishedGeneratingColor()
{
nhlog::ui()->debug("finishedGeneratingColor for: {}", userName_->toolTip().toStdString());
QString userColor = colorGenerating_->result();
if (!userColor.isEmpty()) {
// another TimelineItem might have inserted in the meantime.
if (Cache::userColor(userName_->toolTip()).isEmpty()) {
Cache::insertUserColor(userName_->toolTip(), userColor);
}
userName_->setStyleSheet("QLabel { color : " + userColor + "; }");
}
}
// The username/timestamp is displayed along with the message body.
void
TimelineItem::generateBody(const QString &user_id, const QString &displayname, const QString &body)
{
generateUserName(user_id, displayname);
generateBody(body);
}
void
TimelineItem::generateUserName(const QString &user_id, const QString &displayname)
{
auto sender = displayname;
if (displayname.startsWith("@")) {
// TODO: Fix this by using a UserId type.
if (displayname.split(":")[0].split("@").size() > 1)
sender = displayname.split(":")[0].split("@")[1];
}
QFont usernameFont;
usernameFont.setPointSizeF(usernameFont.pointSizeF() * 1.1);
usernameFont.setWeight(QFont::Medium);
QFontMetrics fm(usernameFont);
userName_ = new QLabel(this);
userName_->setFont(usernameFont);
userName_->setText(utils::replaceEmoji(fm.elidedText(sender, Qt::ElideRight, 500)));
userName_->setToolTip(user_id);
userName_->setToolTipDuration(1500);
userName_->setAttribute(Qt::WA_Hover);
userName_->setAlignment(Qt::AlignLeft | Qt::AlignTop);
#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
// width deprecated in 5.13:
userName_->setFixedWidth(QFontMetrics(userName_->font()).width(userName_->text()));
#else
userName_->setFixedWidth(
QFontMetrics(userName_->font()).horizontalAdvance(userName_->text()));
#endif
// Set the user color asynchronously if it hasn't been generated yet,
// otherwise this will just set it.
refreshAuthorColor();
auto filter = new UserProfileFilter(user_id, userName_);
userName_->installEventFilter(filter);
userName_->setCursor(Qt::PointingHandCursor);
connect(filter, &UserProfileFilter::hoverOn, this, [this]() {
QFont f = userName_->font();
f.setUnderline(true);
userName_->setFont(f);
});
connect(filter, &UserProfileFilter::hoverOff, this, [this]() {
QFont f = userName_->font();
f.setUnderline(false);
userName_->setFont(f);
});
connect(filter, &UserProfileFilter::clicked, this, [this, user_id]() {
MainWindow::instance()->openUserProfile(user_id, room_id_);
});
}
void
TimelineItem::generateTimestamp(const QDateTime &time)
{
timestamp_ = new QLabel(this);
timestamp_->setFont(timestampFont_);
timestamp_->setText(
QString("<span style=\"color: #999\"> %1 </span>").arg(time.toString("HH:mm")));
}
void
TimelineItem::setupAvatarLayout(const QString &userName)
{
topLayout_->setContentsMargins(
conf::timeline::msgLeftMargin, conf::timeline::msgAvatarTopMargin, 0, 0);
QFont f;
f.setPointSizeF(f.pointSizeF());
userAvatar_ = new Avatar(this, QFontMetrics(f).height() * 2);
userAvatar_->setLetter(QChar(userName[0]).toUpper());
// 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 | Qt::AlignLeft);
if (userName_)
mainLayout_->insertWidget(0, userName_, Qt::AlignTop | Qt::AlignLeft);
}
void
TimelineItem::setupSimpleLayout()
{
QFont f;
f.setPointSizeF(f.pointSizeF());
topLayout_->setContentsMargins(conf::timeline::msgLeftMargin +
QFontMetrics(f).height() * 2 + 2,
conf::timeline::msgTopMargin,
0,
0);
}
void
TimelineItem::setUserAvatar(const QString &userid)
{
if (userAvatar_ == nullptr)
return;
userAvatar_->setImage(room_id_, userid);
}
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);
}
}
void
TimelineItem::addReplyAction()
{
if (contextMenu_) {
auto replyAction = new QAction("Reply", this);
contextMenu_->addAction(replyAction);
connect(replyAction, &QAction::triggered, this, &TimelineItem::replyAction);
}
}
void
TimelineItem::replyAction()
{
if (!body_)
return;
RelatedInfo related;
related.type = message_type_;
related.quoted_body = body_->toPlainText();
related.quoted_user = descriptionMsg_.userid;
related.related_event = eventId().toStdString();
related.room = room_id_;
emit ChatPage::instance()->messageReply(related);
}
void
TimelineItem::addKeyRequestAction()
{
if (contextMenu_) {
auto requestKeys = new QAction("Request encryption keys", this);
contextMenu_->addAction(requestKeys);
connect(requestKeys, &QAction::triggered, this, [this]() {
olm::request_keys(room_id_.toStdString(), event_id_.toStdString());
});
}
}
void
TimelineItem::addAvatar()
{
if (userAvatar_)
return;
// TODO: should be replaced with the proper event struct.
auto userid = descriptionMsg_.userid;
auto displayName = Cache::displayName(room_id_, userid);
generateUserName(userid, displayName);
setupAvatarLayout(displayName);
setUserAvatar(userid);
}
void
TimelineItem::sendReadReceipt() const
{
if (!event_id_.isEmpty())
http::client()->read_event(room_id_.toStdString(),
event_id_.toStdString(),
[this](mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn(
"failed to read_event ({}, {})",
room_id_.toStdString(),
event_id_.toStdString());
}
});
}
void
TimelineItem::openRawMessageViewer() const
{
const auto event_id = event_id_.toStdString();
const auto room_id = room_id_.toStdString();
auto proxy = std::make_shared<EventProxy>();
connect(proxy.get(), &EventProxy::eventRetrieved, this, [](const nlohmann::json &obj) {
auto dialog = new dialogs::RawMessage{QString::fromStdString(obj.dump(4))};
Q_UNUSED(dialog);
});
http::client()->get_event(
room_id,
event_id,
[event_id, room_id, proxy = std::move(proxy)](
const mtx::events::collections::TimelineEvents &res, mtx::http::RequestErr err) {
using namespace mtx::events;
if (err) {
nhlog::net()->warn(
"failed to retrieve event {} from {}", event_id, room_id);
return;
}
try {
emit proxy->eventRetrieved(utils::serialize_event(res));
} catch (const nlohmann::json::exception &e) {
nhlog::net()->warn(
"failed to serialize event ({}, {})", room_id, event_id);
}
});
}

View File

@ -1,389 +0,0 @@
/*
* 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 <QApplication>
#include <QDateTime>
#include <QHBoxLayout>
#include <QLabel>
#include <QLayout>
#include <QPainter>
#include <QSettings>
#include <QTimer>
#include <QtConcurrent>
#include "mtx/events.hpp"
#include "AvatarProvider.h"
#include "RoomInfoListItem.h"
#include "Utils.h"
#include "Cache.h"
#include "MatrixClient.h"
#include "ui/FlatButton.h"
class ImageItem;
class StickerItem;
class AudioItem;
class VideoItem;
class FileItem;
class Avatar;
class TextLabel;
enum class StatusIndicatorState
{
//! The encrypted message was received by the server.
Encrypted,
//! The plaintext message was received by the server.
Received,
//! At least one of the participants has read the message.
Read,
//! The client sent the message. Not yet received.
Sent,
//! When the message is loaded from cache or backfill.
Empty,
};
//!
//! Used to notify the user about the status of a message.
//!
class StatusIndicator : public QWidget
{
Q_OBJECT
public:
explicit StatusIndicator(QWidget *parent);
void setState(StatusIndicatorState state);
StatusIndicatorState state() const { return state_; }
protected:
void paintEvent(QPaintEvent *event) override;
private:
void paintIcon(QPainter &p, QIcon &icon);
QIcon lockIcon_;
QIcon clockIcon_;
QIcon checkmarkIcon_;
QIcon doubleCheckmarkIcon_;
QColor iconColor_ = QColor("#999");
StatusIndicatorState state_ = StatusIndicatorState::Empty;
static constexpr int MaxWidth = 24;
};
class EventProxy : public QObject
{
Q_OBJECT
signals:
void eventRetrieved(const nlohmann::json &);
};
class UserProfileFilter : public QObject
{
Q_OBJECT
public:
explicit UserProfileFilter(const QString &user_id, QLabel *parent)
: QObject(parent)
, user_id_{user_id}
{}
signals:
void hoverOff();
void hoverOn();
void clicked();
protected:
bool eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::MouseButtonRelease) {
emit clicked();
return true;
} else if (event->type() == QEvent::HoverLeave) {
emit hoverOff();
return true;
} else if (event->type() == QEvent::HoverEnter) {
emit hoverOn();
return true;
}
return QObject::eventFilter(obj, event);
}
private:
QString user_id_;
};
class TimelineItem : public QWidget
{
Q_OBJECT
Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor)
public:
TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Notice> &e,
bool with_sender,
const QString &room_id,
QWidget *parent = 0);
TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Text> &e,
bool with_sender,
const QString &room_id,
QWidget *parent = 0);
TimelineItem(const mtx::events::RoomEvent<mtx::events::msg::Emote> &e,
bool with_sender,
const QString &room_id,
QWidget *parent = 0);
// For local messages.
// m.text & m.emote
TimelineItem(mtx::events::MessageType ty,
const QString &userid,
QString body,
bool withSender,
const QString &room_id,
QWidget *parent = 0);
// m.image
TimelineItem(ImageItem *item,
const QString &userid,
bool withSender,
const QString &room_id,
QWidget *parent = 0);
TimelineItem(FileItem *item,
const QString &userid,
bool withSender,
const QString &room_id,
QWidget *parent = 0);
TimelineItem(AudioItem *item,
const QString &userid,
bool withSender,
const QString &room_id,
QWidget *parent = 0);
TimelineItem(VideoItem *item,
const QString &userid,
bool withSender,
const QString &room_id,
QWidget *parent = 0);
TimelineItem(ImageItem *img,
const mtx::events::RoomEvent<mtx::events::msg::Image> &e,
bool with_sender,
const QString &room_id,
QWidget *parent);
TimelineItem(StickerItem *img,
const mtx::events::Sticker &e,
bool with_sender,
const QString &room_id,
QWidget *parent);
TimelineItem(FileItem *file,
const mtx::events::RoomEvent<mtx::events::msg::File> &e,
bool with_sender,
const QString &room_id,
QWidget *parent);
TimelineItem(AudioItem *audio,
const mtx::events::RoomEvent<mtx::events::msg::Audio> &e,
bool with_sender,
const QString &room_id,
QWidget *parent);
TimelineItem(VideoItem *video,
const mtx::events::RoomEvent<mtx::events::msg::Video> &e,
bool with_sender,
const QString &room_id,
QWidget *parent);
~TimelineItem();
void setBackgroundColor(const QColor &color) { backgroundColor_ = color; }
QColor backgroundColor() const { return backgroundColor_; }
void setUserAvatar(const QString &userid);
DescInfo descriptionMessage() const { return descriptionMsg_; }
QString eventId() const { return event_id_; }
void setEventId(const QString &event_id) { event_id_ = event_id; }
void markReceived(bool isEncrypted);
void markRead();
void markSent();
bool isReceived() { return isReceived_; };
void setRoomId(QString room_id) { room_id_ = room_id; }
void sendReadReceipt() const;
void openRawMessageViewer() const;
void replyAction();
//! Add a user avatar for this event.
void addAvatar();
void addKeyRequestAction();
signals:
void eventRedacted(const QString &event_id);
void redactionFailed(const QString &msg);
public slots:
void refreshAuthorColor();
void finishedGeneratingColor();
protected:
void paintEvent(QPaintEvent *event) override;
void contextMenuEvent(QContextMenuEvent *event) override;
private:
//! If we are the sender of the message the event wil be marked as received by the server.
void markOwnMessagesAsReceived(const std::string &sender);
void init();
//! Add a context menu option to save the image of the timeline item.
void addSaveImageAction(ImageItem *image);
//! Add the reply action in the context menu for widgets that support it.
void addReplyAction();
template<class Widget>
void setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender);
template<class Event, class Widget>
void setupWidgetLayout(Widget *widget, const Event &event, bool withSender);
void generateBody(const QString &body);
void generateBody(const QString &user_id, const QString &displayname, const QString &body);
void generateTimestamp(const QDateTime &time);
void generateUserName(const QString &userid, const QString &displayname);
void setupAvatarLayout(const QString &userName);
void setupSimpleLayout();
void adjustMessageLayout();
void adjustMessageLayoutForWidget();
//! Whether or not the event associated with the widget
//! has been acknowledged by the server.
bool isReceived_ = false;
QFutureWatcher<QString> *colorGenerating_;
QString event_id_;
mtx::events::MessageType message_type_ = mtx::events::MessageType::Unknown;
QString room_id_;
DescInfo descriptionMsg_;
QMenu *contextMenu_;
QAction *showReadReceipts_;
QAction *markAsRead_;
QAction *redactMsg_;
QAction *viewRawMessage_;
QAction *replyMsg_;
QHBoxLayout *topLayout_ = nullptr;
QHBoxLayout *messageLayout_ = nullptr;
QHBoxLayout *actionLayout_ = nullptr;
QVBoxLayout *mainLayout_ = nullptr;
QHBoxLayout *widgetLayout_ = nullptr;
Avatar *userAvatar_;
QFont timestampFont_;
StatusIndicator *statusIndicator_;
QLabel *timestamp_;
QLabel *userName_;
TextLabel *body_;
QColor backgroundColor_;
FlatButton *replyBtn_;
FlatButton *contextBtn_;
};
template<class Widget>
void
TimelineItem::setupLocalWidgetLayout(Widget *widget, const QString &userid, bool withSender)
{
auto displayName = Cache::displayName(room_id_, userid);
auto timestamp = QDateTime::currentDateTime();
descriptionMsg_ = {"", // No event_id up until this point.
"You",
userid,
QString(" %1").arg(utils::messageDescription<Widget>()),
utils::descriptiveTime(timestamp),
timestamp};
generateTimestamp(timestamp);
widgetLayout_ = new QHBoxLayout;
widgetLayout_->setContentsMargins(0, 2, 0, 2);
widgetLayout_->addWidget(widget);
widgetLayout_->addStretch(1);
if (withSender) {
generateBody(userid, displayName, "");
setupAvatarLayout(displayName);
setUserAvatar(userid);
} else {
setupSimpleLayout();
}
adjustMessageLayoutForWidget();
}
template<class Event, class Widget>
void
TimelineItem::setupWidgetLayout(Widget *widget, const Event &event, bool withSender)
{
init();
// if (event.type == mtx::events::EventType::RoomMessage) {
// message_type_ = mtx::events::getMessageType(event.content.msgtype);
//}
// TODO: Fix this.
message_type_ = mtx::events::MessageType::Unknown;
event_id_ = QString::fromStdString(event.event_id);
const auto sender = QString::fromStdString(event.sender);
auto timestamp = QDateTime::fromMSecsSinceEpoch(event.origin_server_ts);
auto displayName = Cache::displayName(room_id_, sender);
QSettings settings;
descriptionMsg_ = {event_id_,
sender == settings.value("auth/user_id") ? "You" : displayName,
sender,
QString(" %1").arg(utils::messageDescription<Widget>()),
utils::descriptiveTime(timestamp),
timestamp};
generateTimestamp(timestamp);
widgetLayout_ = new QHBoxLayout();
widgetLayout_->setContentsMargins(0, 2, 0, 2);
widgetLayout_->addWidget(widget);
widgetLayout_->addStretch(1);
if (withSender) {
generateBody(sender, displayName, "");
setupAvatarLayout(displayName);
setUserAvatar(sender);
} else {
setupSimpleLayout();
}
adjustMessageLayoutForWidget();
}

File diff suppressed because it is too large Load Diff

View File

@ -1,449 +0,0 @@
/*
* 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 <QApplication>
#include <QLayout>
#include <QList>
#include <QQueue>
#include <QScrollArea>
#include <QScrollBar>
#include <QStyle>
#include <QStyleOption>
#include <QTimer>
#include <mtx/events.hpp>
#include <mtx/responses/messages.hpp>
#include "../Utils.h"
#include "MatrixClient.h"
#include "timeline/TimelineItem.h"
class StateKeeper
{
public:
StateKeeper(std::function<void()> &&fn)
: fn_(std::move(fn))
{}
~StateKeeper() { fn_(); }
private:
std::function<void()> fn_;
};
struct DecryptionResult
{
//! The decrypted content as a normal plaintext event.
utils::TimelineEvent event;
//! Whether or not the decryption was successful.
bool isDecrypted = false;
};
class FloatingButton;
struct DescInfo;
// Contains info about a message shown in the history view
// but not yet confirmed by the homeserver through sync.
struct PendingMessage
{
mtx::events::MessageType ty;
std::string txn_id;
RelatedInfo related;
QString body;
QString filename;
QString mime;
uint64_t media_size;
QString event_id;
TimelineItem *widget;
QSize dimensions;
bool is_encrypted = false;
};
template<class MessageT>
MessageT
toRoomMessage(const PendingMessage &) = delete;
template<>
mtx::events::msg::Audio
toRoomMessage<mtx::events::msg::Audio>(const PendingMessage &m);
template<>
mtx::events::msg::Emote
toRoomMessage<mtx::events::msg::Emote>(const PendingMessage &m);
template<>
mtx::events::msg::File
toRoomMessage<mtx::events::msg::File>(const PendingMessage &);
template<>
mtx::events::msg::Image
toRoomMessage<mtx::events::msg::Image>(const PendingMessage &m);
template<>
mtx::events::msg::Text
toRoomMessage<mtx::events::msg::Text>(const PendingMessage &);
template<>
mtx::events::msg::Video
toRoomMessage<mtx::events::msg::Video>(const PendingMessage &m);
// In which place new TimelineItems should be inserted.
enum class TimelineDirection
{
Top,
Bottom,
};
class TimelineView : public QWidget
{
Q_OBJECT
public:
TimelineView(const mtx::responses::Timeline &timeline,
const QString &room_id,
QWidget *parent = 0);
TimelineView(const QString &room_id, QWidget *parent = 0);
// Add new events at the end of the timeline.
void addEvents(const mtx::responses::Timeline &timeline);
void addUserMessage(mtx::events::MessageType ty,
const QString &body,
const RelatedInfo &related);
void addUserMessage(mtx::events::MessageType ty, const QString &msg);
template<class Widget, mtx::events::MessageType MsgType>
void addUserMessage(const QString &url,
const QString &filename,
const QString &mime,
uint64_t size,
const QSize &dimensions = QSize());
void updatePendingMessage(const std::string &txn_id, const QString &event_id);
void scrollDown();
//! Remove an item from the timeline with the given Event ID.
void removeEvent(const QString &event_id);
void setPrevBatchToken(const QString &token) { prev_batch_token_ = token; }
public slots:
void sliderRangeChanged(int min, int max);
void sliderMoved(int position);
void fetchHistory();
// Add old events at the top of the timeline.
void addBackwardsEvents(const mtx::responses::Messages &msgs);
// Whether or not the initial batch has been loaded.
bool hasLoaded() { return scroll_layout_->count() > 0 || isTimelineFinished; }
void handleFailedMessage(const std::string &txn_id);
private slots:
void sendNextPendingMessage();
signals:
void updateLastTimelineMessage(const QString &user, const DescInfo &info);
void messagesRetrieved(const mtx::responses::Messages &res);
void messageFailed(const std::string &txn_id);
void messageSent(const std::string &txn_id, const QString &event_id);
void markReadEvents(const std::vector<QString> &event_ids);
protected:
void paintEvent(QPaintEvent *event) override;
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override;
bool event(QEvent *event) override;
private:
using TimelineEvent = mtx::events::collections::TimelineEvents;
//! Mark our own widgets as read if they have more than one receipt.
void displayReadReceipts(std::vector<TimelineEvent> events);
//! Determine if the start of the timeline is reached from the response of /messages.
bool isStartOfTimeline(const mtx::responses::Messages &msgs);
QWidget *relativeWidget(QWidget *item, int dt) const;
DecryptionResult parseEncryptedEvent(
const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e);
void handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
const std::map<std::string, std::string> &room_key,
const std::map<std::string, DevicePublicKeys> &pks,
const std::string &user_id,
const mtx::responses::ClaimKeys &res,
mtx::http::RequestErr err);
//! Callback for all message sending.
void sendRoomMessageHandler(const std::string &txn_id,
const mtx::responses::EventId &res,
mtx::http::RequestErr err);
void prepareEncryptedMessage(const PendingMessage &msg);
//! Call the /messages endpoint to fill the timeline.
void getMessages();
//! HACK: Fixing layout flickering when adding to the bottom
//! of the timeline.
void pushTimelineItem(QWidget *item, TimelineDirection dir)
{
setUpdatesEnabled(false);
item->hide();
if (dir == TimelineDirection::Top)
scroll_layout_->insertWidget(0, item);
else
scroll_layout_->addWidget(item);
QTimer::singleShot(0, this, [item, this]() {
item->show();
item->adjustSize();
setUpdatesEnabled(true);
});
}
//! Decides whether or not to show or hide the scroll down button.
void toggleScrollDownButton();
void init();
void addTimelineItem(QWidget *item,
TimelineDirection direction = TimelineDirection::Bottom);
void updateLastSender(const QString &user_id, TimelineDirection direction);
void notifyForLastEvent();
void notifyForLastEvent(const TimelineEvent &event);
//! Keep track of the sender and the timestamp of the current message.
void saveLastMessageInfo(const QString &sender, const QDateTime &datetime)
{
lastSender_ = sender;
lastMsgTimestamp_ = datetime;
}
void saveFirstMessageInfo(const QString &sender, const QDateTime &datetime)
{
firstSender_ = sender;
firstMsgTimestamp_ = datetime;
}
//! Keep track of the sender and the timestamp of the current message.
void saveMessageInfo(const QString &sender,
uint64_t origin_server_ts,
TimelineDirection direction);
TimelineEvent findFirstViewableEvent(const std::vector<TimelineEvent> &events);
TimelineEvent findLastViewableEvent(const std::vector<TimelineEvent> &events);
//! Mark the last event as read.
void readLastEvent() const;
//! Whether or not the scrollbar is visible (non-zero height).
bool isScrollbarActivated() { return scroll_area_->verticalScrollBar()->value() != 0; }
//! Retrieve the event id of the last item.
QString getLastEventId() const;
template<class Event, class Widget>
TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction);
// TODO: Remove this eventually.
template<class Event>
TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction);
// For events with custom display widgets.
template<class Event, class Widget>
TimelineItem *createTimelineItem(const Event &event, bool withSender);
// For events without custom display widgets.
// TODO: All events should have custom widgets.
template<class Event>
TimelineItem *createTimelineItem(const Event &event, bool withSender);
// Used to determine whether or not we should prefix a message with the
// sender's name.
bool isSenderRendered(const QString &user_id,
uint64_t origin_server_ts,
TimelineDirection direction);
bool isPendingMessage(const std::string &txn_id,
const QString &sender,
const QString &userid);
void removePendingMessage(const std::string &txn_id);
bool isDuplicate(const QString &event_id) { return eventIds_.contains(event_id); }
void handleNewUserMessage(PendingMessage msg);
bool isDateDifference(const QDateTime &first,
const QDateTime &second = QDateTime::currentDateTime()) const;
// Return nullptr if the event couldn't be parsed.
QWidget *parseMessageEvent(const mtx::events::collections::TimelineEvents &event,
TimelineDirection direction);
//! Store the event id associated with the given widget.
void saveEventId(QWidget *widget);
//! Remove all widgets from the timeline layout.
void clearTimeline();
QVBoxLayout *top_layout_;
QVBoxLayout *scroll_layout_;
QScrollArea *scroll_area_;
QWidget *scroll_widget_;
QString firstSender_;
QDateTime firstMsgTimestamp_;
QString lastSender_;
QDateTime lastMsgTimestamp_;
QString room_id_;
QString prev_batch_token_;
QString local_user_;
bool isPaginationInProgress_ = false;
// Keeps track whether or not the user has visited the view.
bool isInitialized = false;
bool isTimelineFinished = false;
bool isInitialSync = true;
const int SCROLL_BAR_GAP = 200;
QTimer *paginationTimer_;
int scroll_height_ = 0;
int previous_max_height_ = 0;
int oldPosition_;
int oldHeight_;
FloatingButton *scrollDownBtn_;
TimelineDirection lastMessageDirection_;
//! Messages received by sync not added to the timeline.
std::vector<TimelineEvent> bottomMessages_;
//! Messages received by /messages not added to the timeline.
std::vector<TimelineEvent> topMessages_;
//! Render the given timeline events to the bottom of the timeline.
void renderBottomEvents(const std::vector<TimelineEvent> &events);
//! Render the given timeline events to the top of the timeline.
void renderTopEvents(const std::vector<TimelineEvent> &events);
// The events currently rendered. Used for duplicate detection.
QMap<QString, QWidget *> eventIds_;
QQueue<PendingMessage> pending_msgs_;
QList<PendingMessage> pending_sent_msgs_;
};
template<class Widget, mtx::events::MessageType MsgType>
void
TimelineView::addUserMessage(const QString &url,
const QString &filename,
const QString &mime,
uint64_t size,
const QSize &dimensions)
{
auto with_sender = (lastSender_ != local_user_) || isDateDifference(lastMsgTimestamp_);
auto trimmed = QFileInfo{filename}.fileName(); // Trim file path.
auto widget = new Widget(url, trimmed, size, this);
TimelineItem *view_item =
new TimelineItem(widget, local_user_, with_sender, room_id_, scroll_widget_);
addTimelineItem(view_item);
lastMessageDirection_ = TimelineDirection::Bottom;
// Keep track of the sender and the timestamp of the current message.
saveLastMessageInfo(local_user_, QDateTime::currentDateTime());
PendingMessage message;
message.ty = MsgType;
message.txn_id = http::client()->generate_txn_id();
message.body = url;
message.filename = trimmed;
message.mime = mime;
message.media_size = size;
message.widget = view_item;
message.dimensions = dimensions;
handleNewUserMessage(message);
}
template<class Event>
TimelineItem *
TimelineView::createTimelineItem(const Event &event, bool withSender)
{
TimelineItem *item = new TimelineItem(event, withSender, room_id_, scroll_widget_);
return item;
}
template<class Event, class Widget>
TimelineItem *
TimelineView::createTimelineItem(const Event &event, bool withSender)
{
auto eventWidget = new Widget(event);
auto item = new TimelineItem(eventWidget, event, withSender, room_id_, scroll_widget_);
return item;
}
template<class Event>
TimelineItem *
TimelineView::processMessageEvent(const Event &event, TimelineDirection direction)
{
const auto event_id = QString::fromStdString(event.event_id);
const auto sender = QString::fromStdString(event.sender);
const auto txn_id = event.unsigned_data.transaction_id;
if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) ||
isDuplicate(event_id)) {
removePendingMessage(txn_id);
return nullptr;
}
auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction);
saveMessageInfo(sender, event.origin_server_ts, direction);
auto item = createTimelineItem<Event>(event, with_sender);
eventIds_[event_id] = item;
return item;
}
template<class Event, class Widget>
TimelineItem *
TimelineView::processMessageEvent(const Event &event, TimelineDirection direction)
{
const auto event_id = QString::fromStdString(event.event_id);
const auto sender = QString::fromStdString(event.sender);
const auto txn_id = event.unsigned_data.transaction_id;
if ((!txn_id.empty() && isPendingMessage(txn_id, sender, local_user_)) ||
isDuplicate(event_id)) {
removePendingMessage(txn_id);
return nullptr;
}
auto with_sender = isSenderRendered(sender, event.origin_server_ts, direction);
saveMessageInfo(sender, event.origin_server_ts, direction);
auto item = createTimelineItem<Event, Widget>(event, with_sender);
eventIds_[event_id] = item;
return item;
}

View File

@ -1,94 +1,339 @@
/*
* 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 "TimelineViewManager.h"
#include <random>
#include <QFileDialog>
#include <QMetaType>
#include <QMimeDatabase>
#include <QPalette>
#include <QQmlContext>
#include <QStandardPaths>
#include <QApplication>
#include <QFileInfo>
#include <QSettings>
#include "Cache.h"
#include "ChatPage.h"
#include "ColorImageProvider.h"
#include "DelegateChooser.h"
#include "Logging.h"
#include "Utils.h"
#include "timeline/TimelineView.h"
#include "timeline/TimelineViewManager.h"
#include "timeline/widgets/AudioItem.h"
#include "timeline/widgets/FileItem.h"
#include "timeline/widgets/ImageItem.h"
#include "timeline/widgets/VideoItem.h"
#include "MxcImageProvider.h"
#include "UserSettingsPage.h"
#include "dialogs/ImageOverlay.h"
void
TimelineViewManager::updateColorPalette()
{
UserSettings settings;
if (settings.theme() == "light") {
QPalette lightActive(/*windowText*/ QColor("#333"),
/*button*/ QColor("#333"),
/*light*/ QColor(),
/*dark*/ QColor(220, 220, 220, 120),
/*mid*/ QColor(),
/*text*/ QColor("#333"),
/*bright_text*/ QColor(),
/*base*/ QColor("white"),
/*window*/ QColor("white"));
view->rootContext()->setContextProperty("currentActivePalette", lightActive);
view->rootContext()->setContextProperty("currentInactivePalette", lightActive);
} else if (settings.theme() == "dark") {
QPalette darkActive(/*windowText*/ QColor("#caccd1"),
/*button*/ QColor("#caccd1"),
/*light*/ QColor(),
/*dark*/ QColor(45, 49, 57, 120),
/*mid*/ QColor(),
/*text*/ QColor("#caccd1"),
/*bright_text*/ QColor(),
/*base*/ QColor("#202228"),
/*window*/ QColor("#202228"));
darkActive.setColor(QPalette::Highlight, QColor("#e7e7e9"));
view->rootContext()->setContextProperty("currentActivePalette", darkActive);
view->rootContext()->setContextProperty("currentInactivePalette", darkActive);
} else {
view->rootContext()->setContextProperty("currentActivePalette", QPalette());
view->rootContext()->setContextProperty("currentInactivePalette", nullptr);
}
}
TimelineViewManager::TimelineViewManager(QWidget *parent)
: QStackedWidget(parent)
{}
: imgProvider(new MxcImageProvider())
, colorImgProvider(new ColorImageProvider())
{
qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
"com.github.nheko",
1,
0,
"MtxEvent",
"Can't instantiate enum!");
qmlRegisterType<DelegateChoice>("com.github.nheko", 1, 0, "DelegateChoice");
qmlRegisterType<DelegateChooser>("com.github.nheko", 1, 0, "DelegateChooser");
#ifdef USE_QUICK_VIEW
view = new QQuickView();
container = QWidget::createWindowContainer(view, parent);
#else
view = new QQuickWidget(parent);
container = view;
view->setResizeMode(QQuickWidget::SizeRootObjectToView);
container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) {
nhlog::ui()->debug("Status changed to {}", status);
});
#endif
container->setMinimumSize(200, 200);
view->rootContext()->setContextProperty("timelineManager", this);
updateColorPalette();
view->engine()->addImageProvider("MxcImage", imgProvider);
view->engine()->addImageProvider("colorimage", colorImgProvider);
view->setSource(QUrl("qrc:///qml/TimelineView.qml"));
connect(dynamic_cast<ChatPage *>(parent),
&ChatPage::themeChanged,
this,
&TimelineViewManager::updateColorPalette);
}
void
TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
{
for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) {
// addRoom will only add the room, if it doesn't exist
addRoom(QString::fromStdString(it->first));
models.value(QString::fromStdString(it->first))->addEvents(it->second.timeline);
}
}
void
TimelineViewManager::addRoom(const QString &room_id)
{
if (!models.contains(room_id))
models.insert(room_id,
QSharedPointer<TimelineModel>(new TimelineModel(this, room_id)));
}
void
TimelineViewManager::setHistoryView(const QString &room_id)
{
nhlog::ui()->info("Trying to activate room {}", room_id.toStdString());
auto room = models.find(room_id);
if (room != models.end()) {
timeline_ = room.value().data();
emit activeTimelineChanged(timeline_);
nhlog::ui()->info("Activated room {}", room_id.toStdString());
}
}
void
TimelineViewManager::openImageOverlay(QString mxcUrl,
QString originalFilename,
QString mimeType,
qml_mtx_events::EventType eventType) const
{
QQuickImageResponse *imgResponse =
imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize());
connect(imgResponse,
&QQuickImageResponse::finished,
this,
[this, mxcUrl, originalFilename, mimeType, eventType, imgResponse]() {
if (!imgResponse->errorString().isEmpty()) {
nhlog::ui()->error("Error when retrieving image for overlay: {}",
imgResponse->errorString().toStdString());
return;
}
auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image());
auto imgDialog = new dialogs::ImageOverlay(pixmap);
imgDialog->show();
connect(imgDialog,
&dialogs::ImageOverlay::saving,
this,
[this, mxcUrl, originalFilename, mimeType, eventType]() {
saveMedia(mxcUrl, originalFilename, mimeType, eventType);
});
});
}
void
TimelineViewManager::saveMedia(QString mxcUrl,
QString originalFilename,
QString mimeType,
qml_mtx_events::EventType eventType) const
{
QString dialogTitle;
if (eventType == qml_mtx_events::EventType::ImageMessage) {
dialogTitle = tr("Save image");
} else if (eventType == qml_mtx_events::EventType::VideoMessage) {
dialogTitle = tr("Save video");
} else if (eventType == qml_mtx_events::EventType::AudioMessage) {
dialogTitle = tr("Save audio");
} else {
dialogTitle = tr("Save file");
}
QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString();
auto filename =
QFileDialog::getSaveFileName(container, dialogTitle, originalFilename, filterString);
if (filename.isEmpty())
return;
const auto url = mxcUrl.toStdString();
http::client()->download(
url,
[filename, url](const std::string &data,
const std::string &,
const std::string &,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to retrieve image {}: {} {}",
url,
err->matrix_error.error,
static_cast<int>(err->status_code));
return;
}
try {
QFile file(filename);
if (!file.open(QIODevice::WriteOnly))
return;
file.write(QByteArray(data.data(), data.size()));
file.close();
} catch (const std::exception &e) {
nhlog::ui()->warn("Error while saving file to: {}", e.what());
}
});
}
void
TimelineViewManager::cacheMedia(QString mxcUrl, QString mimeType)
{
// If the message is a link to a non mxcUrl, don't download it
if (!mxcUrl.startsWith("mxc://")) {
emit mediaCached(mxcUrl, mxcUrl);
return;
}
QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix();
const auto url = mxcUrl.toStdString();
QFileInfo filename(QString("%1/media_cache/%2.%3")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
.arg(QString(mxcUrl).remove("mxc://"))
.arg(suffix));
if (QDir::cleanPath(filename.path()) != filename.path()) {
nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url);
return;
}
QDir().mkpath(filename.path());
if (filename.isReadable()) {
emit mediaCached(mxcUrl, filename.filePath());
return;
}
http::client()->download(
url,
[this, mxcUrl, filename, url](const std::string &data,
const std::string &,
const std::string &,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to retrieve image {}: {} {}",
url,
err->matrix_error.error,
static_cast<int>(err->status_code));
return;
}
try {
QFile file(filename.filePath());
if (!file.open(QIODevice::WriteOnly))
return;
file.write(QByteArray(data.data(), data.size()));
file.close();
} catch (const std::exception &e) {
nhlog::ui()->warn("Error while saving file to: {}", e.what());
}
emit mediaCached(mxcUrl, filename.filePath());
});
}
void
TimelineViewManager::updateReadReceipts(const QString &room_id,
const std::vector<QString> &event_ids)
{
if (timelineViewExists(room_id)) {
auto view = views_[room_id];
if (view)
emit view->markReadEvents(event_ids);
auto room = models.find(room_id);
if (room != models.end()) {
room.value()->markEventsAsRead(event_ids);
}
}
void
TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id)
TimelineViewManager::initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs)
{
auto view = views_[room_id];
for (const auto &e : msgs) {
addRoom(e.first);
if (view)
view->removeEvent(event_id);
models.value(e.first)->addEvents(e.second);
}
}
void
TimelineViewManager::queueTextMessage(const QString &msg)
{
if (active_room_.isEmpty())
return;
mtx::events::msg::Text text = {};
text.body = msg.trimmed().toStdString();
text.format = "org.matrix.custom.html";
text.formatted_body = utils::markdownToHtml(msg).toStdString();
auto room_id = active_room_;
auto view = views_[room_id];
view->addUserMessage(mtx::events::MessageType::Text, msg);
}
void
TimelineViewManager::queueEmoteMessage(const QString &msg)
{
if (active_room_.isEmpty())
return;
auto room_id = active_room_;
auto view = views_[room_id];
view->addUserMessage(mtx::events::MessageType::Emote, msg);
if (timeline_)
timeline_->sendMessage(text);
}
void
TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related)
{
if (active_room_.isEmpty())
return;
mtx::events::msg::Text text = {};
auto room_id = active_room_;
auto view = views_[room_id];
QString body;
bool firstLine = true;
for (const auto &line : related.quoted_body.split("\n")) {
if (firstLine) {
firstLine = false;
body = QString("> <%1> %2\n").arg(related.quoted_user).arg(line);
} else {
body = QString("%1\n> %2\n").arg(body).arg(line);
}
}
view->addUserMessage(mtx::events::MessageType::Text, reply, related);
text.body = QString("%1\n%2").arg(body).arg(reply).toStdString();
text.format = "org.matrix.custom.html";
text.formatted_body =
utils::getFormattedQuoteBody(related, utils::markdownToHtml(reply)).toStdString();
text.relates_to.in_reply_to.event_id = related.related_event;
if (timeline_)
timeline_->sendMessage(text);
}
void
TimelineViewManager::queueEmoteMessage(const QString &msg)
{
auto html = utils::markdownToHtml(msg);
mtx::events::msg::Emote emote;
emote.body = msg.trimmed().toStdString();
if (html != msg.trimmed().toHtmlEscaped())
emote.formatted_body = html.toStdString();
if (timeline_)
timeline_->sendMessage(emote);
}
void
@ -96,18 +341,17 @@ TimelineViewManager::queueImageMessage(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t size,
uint64_t dsize,
const QSize &dimensions)
{
if (!timelineViewExists(roomid)) {
nhlog::ui()->warn("Cannot send m.image message to a non-managed view");
return;
}
auto view = views_[roomid];
view->addUserMessage<ImageItem, mtx::events::MessageType::Image>(
url, filename, mime, size, dimensions);
mtx::events::msg::Image image;
image.info.mimetype = mime.toStdString();
image.info.size = dsize;
image.body = filename.toStdString();
image.url = url.toStdString();
image.info.h = dimensions.height();
image.info.w = dimensions.width();
models.value(roomid)->sendMessage(image);
}
void
@ -115,16 +359,14 @@ TimelineViewManager::queueFileMessage(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t size)
uint64_t dsize)
{
if (!timelineViewExists(roomid)) {
nhlog::ui()->warn("cannot send m.file message to a non-managed view");
return;
}
auto view = views_[roomid];
view->addUserMessage<FileItem, mtx::events::MessageType::File>(url, filename, mime, size);
mtx::events::msg::File file;
file.info.mimetype = mime.toStdString();
file.info.size = dsize;
file.body = filename.toStdString();
file.url = url.toStdString();
models.value(roomid)->sendMessage(file);
}
void
@ -132,16 +374,14 @@ TimelineViewManager::queueAudioMessage(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t size)
uint64_t dsize)
{
if (!timelineViewExists(roomid)) {
nhlog::ui()->warn("cannot send m.audio message to a non-managed view");
return;
}
auto view = views_[roomid];
view->addUserMessage<AudioItem, mtx::events::MessageType::Audio>(url, filename, mime, size);
mtx::events::msg::Audio audio;
audio.info.mimetype = mime.toStdString();
audio.info.size = dsize;
audio.body = filename.toStdString();
audio.url = url.toStdString();
models.value(roomid)->sendMessage(audio);
}
void
@ -149,192 +389,12 @@ TimelineViewManager::queueVideoMessage(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t size)
uint64_t dsize)
{
if (!timelineViewExists(roomid)) {
nhlog::ui()->warn("cannot send m.video message to a non-managed view");
return;
}
auto view = views_[roomid];
view->addUserMessage<VideoItem, mtx::events::MessageType::Video>(url, filename, mime, size);
}
void
TimelineViewManager::initialize(const mtx::responses::Rooms &rooms)
{
for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) {
addRoom(it->second, QString::fromStdString(it->first));
}
sync(rooms);
}
void
TimelineViewManager::initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs)
{
for (auto it = msgs.cbegin(); it != msgs.cend(); ++it) {
if (timelineViewExists(it->first))
return;
// Create a history view with the room events.
TimelineView *view = new TimelineView(it->second, it->first);
views_.emplace(it->first, QSharedPointer<TimelineView>(view));
connect(view,
&TimelineView::updateLastTimelineMessage,
this,
&TimelineViewManager::updateRoomsLastMessage);
// Add the view in the widget stack.
addWidget(view);
}
}
void
TimelineViewManager::initialize(const std::vector<std::string> &rooms)
{
for (const auto &roomid : rooms)
addRoom(QString::fromStdString(roomid));
}
void
TimelineViewManager::addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id)
{
if (timelineViewExists(room_id))
return;
// Create a history view with the room events.
TimelineView *view = new TimelineView(room.timeline, room_id);
views_.emplace(room_id, QSharedPointer<TimelineView>(view));
connect(view,
&TimelineView::updateLastTimelineMessage,
this,
&TimelineViewManager::updateRoomsLastMessage);
// Add the view in the widget stack.
addWidget(view);
}
void
TimelineViewManager::addRoom(const QString &room_id)
{
if (timelineViewExists(room_id))
return;
// Create a history view without any events.
TimelineView *view = new TimelineView(room_id);
views_.emplace(room_id, QSharedPointer<TimelineView>(view));
connect(view,
&TimelineView::updateLastTimelineMessage,
this,
&TimelineViewManager::updateRoomsLastMessage);
// Add the view in the widget stack.
addWidget(view);
}
void
TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
{
for (const auto &room : rooms.join) {
auto roomid = QString::fromStdString(room.first);
if (!timelineViewExists(roomid)) {
nhlog::ui()->warn("ignoring event from unknown room: {}",
roomid.toStdString());
continue;
}
auto view = views_.at(roomid);
view->addEvents(room.second.timeline);
}
}
void
TimelineViewManager::setHistoryView(const QString &room_id)
{
if (!timelineViewExists(room_id)) {
nhlog::ui()->warn("room from RoomList is not present in ViewManager: {}",
room_id.toStdString());
return;
}
active_room_ = room_id;
auto view = views_.at(room_id);
setCurrentWidget(view.data());
view->fetchHistory();
view->scrollDown();
}
QString
TimelineViewManager::chooseRandomColor()
{
std::random_device random_device;
std::mt19937 engine{random_device()};
std::uniform_real_distribution<float> dist(0, 1);
float hue = dist(engine);
float saturation = 0.9;
float value = 0.7;
int hue_i = hue * 6;
float f = hue * 6 - hue_i;
float p = value * (1 - saturation);
float q = value * (1 - f * saturation);
float t = value * (1 - (1 - f) * saturation);
float r = 0;
float g = 0;
float b = 0;
if (hue_i == 0) {
r = value;
g = t;
b = p;
} else if (hue_i == 1) {
r = q;
g = value;
b = p;
} else if (hue_i == 2) {
r = p;
g = value;
b = t;
} else if (hue_i == 3) {
r = p;
g = q;
b = value;
} else if (hue_i == 4) {
r = t;
g = p;
b = value;
} else if (hue_i == 5) {
r = value;
g = p;
b = q;
}
int ri = r * 256;
int gi = g * 256;
int bi = b * 256;
QColor color(ri, gi, bi);
return color.name();
}
bool
TimelineViewManager::hasLoaded() const
{
return std::all_of(views_.cbegin(), views_.cend(), [](const auto &view) {
return view.second->hasLoaded();
});
mtx::events::msg::Video video;
video.info.mimetype = mime.toStdString();
video.info.size = dsize;
video.body = filename.toStdString();
video.url = url.toStdString();
models.value(roomid)->sendMessage(video);
}

View File

@ -1,69 +1,80 @@
/*
* 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 <QQuickView>
#include <QQuickWidget>
#include <QSharedPointer>
#include <QStackedWidget>
#include <QWidget>
#include <mtx.hpp>
#include <mtx/responses.hpp>
#include "Cache.h"
#include "Logging.h"
#include "TimelineModel.h"
#include "Utils.h"
class QFile;
// temporary for stubs
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
class RoomInfoListItem;
class TimelineView;
struct DescInfo;
struct SavedMessages;
class MxcImageProvider;
class ColorImageProvider;
class TimelineViewManager : public QStackedWidget
class TimelineViewManager : public QObject
{
Q_OBJECT
Q_PROPERTY(
TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged)
public:
TimelineViewManager(QWidget *parent);
// Initialize with timeline events.
void initialize(const mtx::responses::Rooms &rooms);
// Empty initialization.
void initialize(const std::vector<std::string> &rooms);
void addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id);
void addRoom(const QString &room_id);
TimelineViewManager(QWidget *parent = 0);
QWidget *getWidget() const { return container; }
void sync(const mtx::responses::Rooms &rooms);
void clearAll() { views_.clear(); }
void addRoom(const QString &room_id);
// Check if all the timelines have been loaded.
bool hasLoaded() const;
void clearAll() { models.clear(); }
static QString chooseRandomColor();
Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; }
void openImageOverlay(QString mxcUrl,
QString originalFilename,
QString mimeType,
qml_mtx_events::EventType eventType) const;
void saveMedia(QString mxcUrl,
QString originalFilename,
QString mimeType,
qml_mtx_events::EventType eventType) const;
Q_INVOKABLE void cacheMedia(QString mxcUrl, QString mimeType);
// Qml can only pass enum as int
Q_INVOKABLE void openImageOverlay(QString mxcUrl,
QString originalFilename,
QString mimeType,
int eventType) const
{
openImageOverlay(
mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType);
}
Q_INVOKABLE void saveMedia(QString mxcUrl,
QString originalFilename,
QString mimeType,
int eventType) const
{
saveMedia(mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType);
}
signals:
void clearRoomMessageCount(QString roomid);
void updateRoomsLastMessage(const QString &user, const DescInfo &info);
void updateRoomsLastMessage(QString roomid, const DescInfo &info);
void activeTimelineChanged(TimelineModel *timeline);
void mediaCached(QString mxcUrl, QString cacheUrl);
public slots:
void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
void removeTimelineEvent(const QString &room_id, const QString &event_id);
void initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs);
void setHistoryView(const QString &room_id);
void updateColorPalette();
void queueTextMessage(const QString &msg);
void queueReplyMessage(const QString &reply, const RelatedInfo &related);
void queueEmoteMessage(const QString &msg);
@ -90,9 +101,17 @@ public slots:
uint64_t dsize);
private:
//! Check if the given room id is managed by a TimelineView.
bool timelineViewExists(const QString &id) { return views_.find(id) != views_.end(); }
#ifdef USE_QUICK_VIEW
QQuickView *view;
#else
QQuickWidget *view;
#endif
QWidget *container;
TimelineModel *timeline_ = nullptr;
MxcImageProvider *imgProvider;
ColorImageProvider *colorImgProvider;
QString active_room_;
std::map<QString, QSharedPointer<TimelineView>> views_;
QHash<QString, QSharedPointer<TimelineModel>> models;
};
#pragma GCC diagnostic pop

View File

@ -1,236 +0,0 @@
/*
* 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 <QBrush>
#include <QDesktopServices>
#include <QFile>
#include <QFileDialog>
#include <QPainter>
#include <QPixmap>
#include <QtGlobal>
#include "Logging.h"
#include "MatrixClient.h"
#include "Utils.h"
#include "timeline/widgets/AudioItem.h"
constexpr int MaxWidth = 400;
constexpr int Height = 70;
constexpr int IconRadius = 22;
constexpr int IconDiameter = IconRadius * 2;
constexpr int HorizontalPadding = 12;
constexpr int TextPadding = 15;
constexpr int ActionIconRadius = IconRadius - 4;
constexpr double VerticalPadding = Height - 2 * IconRadius;
constexpr double IconYCenter = Height / 2;
constexpr double IconXCenter = HorizontalPadding + IconRadius;
void
AudioItem::init()
{
setMouseTracking(true);
setCursor(Qt::PointingHandCursor);
setAttribute(Qt::WA_Hover, true);
playIcon_.addFile(":/icons/icons/ui/play-sign.png");
pauseIcon_.addFile(":/icons/icons/ui/pause-symbol.png");
player_ = new QMediaPlayer;
player_->setMedia(QUrl(url_));
player_->setVolume(100);
player_->setNotifyInterval(1000);
connect(player_, &QMediaPlayer::stateChanged, this, [this](QMediaPlayer::State state) {
if (state == QMediaPlayer::StoppedState) {
state_ = AudioState::Play;
player_->setMedia(QUrl(url_));
update();
}
});
setFixedHeight(Height);
}
AudioItem::AudioItem(const mtx::events::RoomEvent<mtx::events::msg::Audio> &event, QWidget *parent)
: QWidget(parent)
, url_{QUrl(QString::fromStdString(event.content.url))}
, text_{QString::fromStdString(event.content.body)}
, event_{event}
{
readableFileSize_ = utils::humanReadableFileSize(event.content.info.size);
init();
}
AudioItem::AudioItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent)
: QWidget(parent)
, url_{url}
, text_{filename}
{
readableFileSize_ = utils::humanReadableFileSize(size);
init();
}
QSize
AudioItem::sizeHint() const
{
return QSize(MaxWidth, Height);
}
void
AudioItem::mousePressEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton)
return;
auto point = event->pos();
// Click on the download icon.
if (QRect(HorizontalPadding, VerticalPadding / 2, IconDiameter, IconDiameter)
.contains(point)) {
if (state_ == AudioState::Play) {
state_ = AudioState::Pause;
player_->play();
} else {
state_ = AudioState::Play;
player_->pause();
}
update();
} else {
filenameToSave_ = QFileDialog::getSaveFileName(this, tr("Save File"), text_);
if (filenameToSave_.isEmpty())
return;
auto proxy = std::make_shared<MediaProxy>();
connect(proxy.get(), &MediaProxy::fileDownloaded, this, &AudioItem::fileDownloaded);
http::client()->download(
url_.toString().toStdString(),
[proxy = std::move(proxy), url = url_](const std::string &data,
const std::string &,
const std::string &,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->info("failed to retrieve m.audio content: {}",
url.toString().toStdString());
return;
}
emit proxy->fileDownloaded(QByteArray(data.data(), data.size()));
});
}
}
void
AudioItem::fileDownloaded(const QByteArray &data)
{
try {
QFile file(filenameToSave_);
if (!file.open(QIODevice::WriteOnly))
return;
file.write(data);
file.close();
} catch (const std::exception &e) {
nhlog::ui()->warn("error while saving file: {}", e.what());
}
}
void
AudioItem::resizeEvent(QResizeEvent *event)
{
QFont font;
font.setWeight(QFont::Medium);
QFontMetrics fm(font);
#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
const int computedWidth = std::min(
fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth);
#else
const int computedWidth =
std::min(fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding,
(double)MaxWidth);
#endif
resize(computedWidth, Height);
event->accept();
}
void
AudioItem::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
QFont font;
font.setWeight(QFont::Medium);
QFontMetrics fm(font);
QPainterPath path;
path.addRoundedRect(QRectF(0, 0, width(), height()), 10, 10);
painter.setPen(Qt::NoPen);
painter.fillPath(path, backgroundColor_);
painter.drawPath(path);
QPainterPath circle;
circle.addEllipse(QPoint(IconXCenter, IconYCenter), IconRadius, IconRadius);
painter.setPen(Qt::NoPen);
painter.fillPath(circle, iconColor_);
painter.drawPath(circle);
QIcon icon_;
if (state_ == AudioState::Play)
icon_ = playIcon_;
else
icon_ = pauseIcon_;
icon_.paint(&painter,
QRect(IconXCenter - ActionIconRadius / 2,
IconYCenter - ActionIconRadius / 2,
ActionIconRadius,
ActionIconRadius),
Qt::AlignCenter,
QIcon::Normal);
const int textStartX = HorizontalPadding + 2 * IconRadius + TextPadding;
const int textStartY = VerticalPadding + fm.ascent() / 2;
// Draw the filename.
QString elidedText = fm.elidedText(
text_, Qt::ElideRight, width() - HorizontalPadding * 2 - TextPadding - 2 * IconRadius);
painter.setFont(font);
painter.setPen(QPen(textColor_));
painter.drawText(QPoint(textStartX, textStartY), elidedText);
// Draw the filesize.
font.setWeight(QFont::Normal);
painter.setFont(font);
painter.setPen(QPen(textColor_));
painter.drawText(QPoint(textStartX, textStartY + 1.5 * fm.ascent()), readableFileSize_);
}

View File

@ -1,104 +0,0 @@
/*
* 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 <QEvent>
#include <QIcon>
#include <QMediaPlayer>
#include <QMouseEvent>
#include <QSharedPointer>
#include <QWidget>
#include <mtx.hpp>
class AudioItem : public QWidget
{
Q_OBJECT
Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor)
Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor)
Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor)
Q_PROPERTY(QColor durationBackgroundColor WRITE setDurationBackgroundColor READ
durationBackgroundColor)
Q_PROPERTY(QColor durationForegroundColor WRITE setDurationForegroundColor READ
durationForegroundColor)
public:
AudioItem(const mtx::events::RoomEvent<mtx::events::msg::Audio> &event,
QWidget *parent = nullptr);
AudioItem(const QString &url,
const QString &filename,
uint64_t size,
QWidget *parent = nullptr);
QSize sizeHint() const override;
void setTextColor(const QColor &color) { textColor_ = color; }
void setIconColor(const QColor &color) { iconColor_ = color; }
void setBackgroundColor(const QColor &color) { backgroundColor_ = color; }
void setDurationBackgroundColor(const QColor &color) { durationBgColor_ = color; }
void setDurationForegroundColor(const QColor &color) { durationFgColor_ = color; }
QColor textColor() const { return textColor_; }
QColor iconColor() const { return iconColor_; }
QColor backgroundColor() const { return backgroundColor_; }
QColor durationBackgroundColor() const { return durationBgColor_; }
QColor durationForegroundColor() const { return durationFgColor_; }
protected:
void paintEvent(QPaintEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
private slots:
void fileDownloaded(const QByteArray &data);
private:
void init();
enum class AudioState
{
Play,
Pause,
};
AudioState state_ = AudioState::Play;
QUrl url_;
QString text_;
QString readableFileSize_;
QString filenameToSave_;
mtx::events::RoomEvent<mtx::events::msg::Audio> event_;
QMediaPlayer *player_;
QIcon playIcon_;
QIcon pauseIcon_;
QColor textColor_ = QColor("white");
QColor iconColor_ = QColor("#38A3D8");
QColor backgroundColor_ = QColor("#333");
QColor durationBgColor_ = QColor("black");
QColor durationFgColor_ = QColor("blue");
};

View File

@ -1,221 +0,0 @@
/*
* 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 <QBrush>
#include <QDesktopServices>
#include <QFile>
#include <QFileDialog>
#include <QPainter>
#include <QPixmap>
#include <QtGlobal>
#include "Logging.h"
#include "MatrixClient.h"
#include "Utils.h"
#include "timeline/widgets/FileItem.h"
constexpr int MaxWidth = 400;
constexpr int Height = 70;
constexpr int IconRadius = 22;
constexpr int IconDiameter = IconRadius * 2;
constexpr int HorizontalPadding = 12;
constexpr int TextPadding = 15;
constexpr int DownloadIconRadius = IconRadius - 4;
constexpr double VerticalPadding = Height - 2 * IconRadius;
constexpr double IconYCenter = Height / 2;
constexpr double IconXCenter = HorizontalPadding + IconRadius;
void
FileItem::init()
{
setMouseTracking(true);
setCursor(Qt::PointingHandCursor);
setAttribute(Qt::WA_Hover, true);
icon_.addFile(":/icons/icons/ui/arrow-pointing-down.png");
setFixedHeight(Height);
}
FileItem::FileItem(const mtx::events::RoomEvent<mtx::events::msg::File> &event, QWidget *parent)
: QWidget(parent)
, url_{QString::fromStdString(event.content.url)}
, text_{QString::fromStdString(event.content.body)}
, event_{event}
{
readableFileSize_ = utils::humanReadableFileSize(event.content.info.size);
init();
}
FileItem::FileItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent)
: QWidget(parent)
, url_{url}
, text_{filename}
{
readableFileSize_ = utils::humanReadableFileSize(size);
init();
}
void
FileItem::openUrl()
{
if (url_.toString().isEmpty())
return;
auto urlToOpen = utils::mxcToHttp(
url_, QString::fromStdString(http::client()->server()), http::client()->port());
if (!QDesktopServices::openUrl(urlToOpen))
nhlog::ui()->warn("Could not open url: {}", urlToOpen.toStdString());
}
QSize
FileItem::sizeHint() const
{
return QSize(MaxWidth, Height);
}
void
FileItem::mousePressEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton)
return;
auto point = event->pos();
// Click on the download icon.
if (QRect(HorizontalPadding, VerticalPadding / 2, IconDiameter, IconDiameter)
.contains(point)) {
filenameToSave_ = QFileDialog::getSaveFileName(this, tr("Save File"), text_);
if (filenameToSave_.isEmpty())
return;
auto proxy = std::make_shared<MediaProxy>();
connect(proxy.get(), &MediaProxy::fileDownloaded, this, &FileItem::fileDownloaded);
http::client()->download(
url_.toString().toStdString(),
[proxy = std::move(proxy), url = url_](const std::string &data,
const std::string &,
const std::string &,
mtx::http::RequestErr err) {
if (err) {
nhlog::ui()->warn("failed to retrieve m.file content: {}",
url.toString().toStdString());
return;
}
emit proxy->fileDownloaded(QByteArray(data.data(), data.size()));
});
} else {
openUrl();
}
}
void
FileItem::fileDownloaded(const QByteArray &data)
{
try {
QFile file(filenameToSave_);
if (!file.open(QIODevice::WriteOnly))
return;
file.write(data);
file.close();
} catch (const std::exception &e) {
nhlog::ui()->warn("Error while saving file to: {}", e.what());
}
}
void
FileItem::resizeEvent(QResizeEvent *event)
{
QFont font;
font.setWeight(QFont::Medium);
QFontMetrics fm(font);
#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
const int computedWidth = std::min(
fm.width(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding, (double)MaxWidth);
#else
const int computedWidth =
std::min(fm.horizontalAdvance(text_) + 2 * IconRadius + VerticalPadding * 2 + TextPadding,
(double)MaxWidth);
#endif
resize(computedWidth, Height);
event->accept();
}
void
FileItem::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
QFont font;
font.setWeight(QFont::Medium);
QFontMetrics fm(font);
QPainterPath path;
path.addRoundedRect(QRectF(0, 0, width(), height()), 10, 10);
painter.setPen(Qt::NoPen);
painter.fillPath(path, backgroundColor_);
painter.drawPath(path);
QPainterPath circle;
circle.addEllipse(QPoint(IconXCenter, IconYCenter), IconRadius, IconRadius);
painter.setPen(Qt::NoPen);
painter.fillPath(circle, iconColor_);
painter.drawPath(circle);
icon_.paint(&painter,
QRect(IconXCenter - DownloadIconRadius / 2,
IconYCenter - DownloadIconRadius / 2,
DownloadIconRadius,
DownloadIconRadius),
Qt::AlignCenter,
QIcon::Normal);
const int textStartX = HorizontalPadding + 2 * IconRadius + TextPadding;
const int textStartY = VerticalPadding + fm.ascent() / 2;
// Draw the filename.
QString elidedText = fm.elidedText(
text_, Qt::ElideRight, width() - HorizontalPadding * 2 - TextPadding - 2 * IconRadius);
painter.setFont(font);
painter.setPen(QPen(textColor_));
painter.drawText(QPoint(textStartX, textStartY), elidedText);
// Draw the filesize.
font.setWeight(QFont::Normal);
painter.setFont(font);
painter.setPen(QPen(textColor_));
painter.drawText(QPoint(textStartX, textStartY + 1.5 * fm.ascent()), readableFileSize_);
}

View File

@ -1,79 +0,0 @@
/*
* 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 <QEvent>
#include <QIcon>
#include <QMouseEvent>
#include <QSharedPointer>
#include <QWidget>
#include <mtx.hpp>
class FileItem : public QWidget
{
Q_OBJECT
Q_PROPERTY(QColor textColor WRITE setTextColor READ textColor)
Q_PROPERTY(QColor iconColor WRITE setIconColor READ iconColor)
Q_PROPERTY(QColor backgroundColor WRITE setBackgroundColor READ backgroundColor)
public:
FileItem(const mtx::events::RoomEvent<mtx::events::msg::File> &event,
QWidget *parent = nullptr);
FileItem(const QString &url,
const QString &filename,
uint64_t size,
QWidget *parent = nullptr);
QSize sizeHint() const override;
void setTextColor(const QColor &color) { textColor_ = color; }
void setIconColor(const QColor &color) { iconColor_ = color; }
void setBackgroundColor(const QColor &color) { backgroundColor_ = color; }
QColor textColor() const { return textColor_; }
QColor iconColor() const { return iconColor_; }
QColor backgroundColor() const { return backgroundColor_; }
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
private slots:
void fileDownloaded(const QByteArray &data);
private:
void openUrl();
void init();
QUrl url_;
QString text_;
QString readableFileSize_;
QString filenameToSave_;
mtx::events::RoomEvent<mtx::events::msg::File> event_;
QIcon icon_;
QColor textColor_ = QColor("white");
QColor iconColor_ = QColor("#38A3D8");
QColor backgroundColor_ = QColor("#333");
};

View File

@ -1,267 +0,0 @@
/*
* 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 <QBrush>
#include <QDesktopServices>
#include <QFileDialog>
#include <QFileInfo>
#include <QPainter>
#include <QPixmap>
#include <QUuid>
#include <QtGlobal>
#include "Config.h"
#include "ImageItem.h"
#include "Logging.h"
#include "MatrixClient.h"
#include "Utils.h"
#include "dialogs/ImageOverlay.h"
void
ImageItem::downloadMedia(const QUrl &url)
{
auto proxy = std::make_shared<MediaProxy>();
connect(proxy.get(), &MediaProxy::imageDownloaded, this, &ImageItem::setImage);
http::client()->download(url.toString().toStdString(),
[proxy = std::move(proxy), url](const std::string &data,
const std::string &,
const std::string &,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn(
"failed to retrieve image {}: {} {}",
url.toString().toStdString(),
err->matrix_error.error,
static_cast<int>(err->status_code));
return;
}
QPixmap img;
img.loadFromData(QByteArray(data.data(), data.size()));
emit proxy->imageDownloaded(img);
});
}
void
ImageItem::saveImage(const QString &filename, const QByteArray &data)
{
try {
QFile file(filename);
if (!file.open(QIODevice::WriteOnly))
return;
file.write(data);
file.close();
} catch (const std::exception &e) {
nhlog::ui()->warn("Error while saving file to: {}", e.what());
}
}
void
ImageItem::init()
{
setMouseTracking(true);
setCursor(Qt::PointingHandCursor);
setAttribute(Qt::WA_Hover, true);
downloadMedia(url_);
}
ImageItem::ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &event, QWidget *parent)
: QWidget(parent)
, event_{event}
{
url_ = QString::fromStdString(event.content.url);
text_ = QString::fromStdString(event.content.body);
init();
}
ImageItem::ImageItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent)
: QWidget(parent)
, url_{url}
, text_{filename}
{
Q_UNUSED(size);
init();
}
void
ImageItem::openUrl()
{
if (url_.toString().isEmpty())
return;
auto urlToOpen = utils::mxcToHttp(
url_, QString::fromStdString(http::client()->server()), http::client()->port());
if (!QDesktopServices::openUrl(urlToOpen))
nhlog::ui()->warn("could not open url: {}", urlToOpen.toStdString());
}
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;
scaled_image_ = utils::scaleDown(max_width_, max_height_, image_);
width_ = scaled_image_.width();
height_ = scaled_image_.height();
setFixedSize(width_, height_);
update();
}
void
ImageItem::mousePressEvent(QMouseEvent *event)
{
if (!isInteractive_) {
event->accept();
return;
}
if (event->button() != Qt::LeftButton)
return;
if (image_.isNull()) {
openUrl();
return;
}
if (textRegion_.contains(event->pos())) {
openUrl();
} else {
auto imgDialog = new dialogs::ImageOverlay(image_);
imgDialog->show();
connect(imgDialog, &dialogs::ImageOverlay::saving, this, &ImageItem::saveAs);
}
}
void
ImageItem::resizeEvent(QResizeEvent *event)
{
if (!image_)
return QWidget::resizeEvent(event);
scaled_image_ = utils::scaleDown(max_width_, max_height_, image_);
width_ = scaled_image_.width();
height_ = scaled_image_.height();
setFixedSize(width_, height_);
}
void
ImageItem::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
QFont font;
QFontMetrics metrics(font);
const int fontHeight = metrics.height() + metrics.ascent();
if (image_.isNull()) {
QString elidedText = metrics.elidedText(text_, Qt::ElideRight, max_width_ - 10);
#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
setFixedSize(metrics.width(elidedText), fontHeight);
#else
setFixedSize(metrics.horizontalAdvance(elidedText), fontHeight);
#endif
painter.setFont(font);
painter.setPen(QPen(QColor(66, 133, 244)));
painter.drawText(QPoint(0, fontHeight / 2), elidedText);
return;
}
imageRegion_ = QRectF(0, 0, width_, height_);
QPainterPath path;
path.addRoundedRect(imageRegion_, 5, 5);
painter.setPen(Qt::NoPen);
painter.fillPath(path, scaled_image_);
painter.drawPath(path);
// Bottom text section
if (isInteractive_ && underMouse()) {
const int textBoxHeight = fontHeight / 2 + 6;
textRegion_ = QRectF(0, height_ - textBoxHeight, width_, textBoxHeight);
QPainterPath textPath;
textPath.addRoundedRect(textRegion_, 0, 0);
painter.fillPath(textPath, QColor(40, 40, 40, 140));
QString elidedText = metrics.elidedText(text_, Qt::ElideRight, width_ - 10);
font.setWeight(QFont::Medium);
painter.setFont(font);
painter.setPen(QPen(QColor(Qt::white)));
textRegion_.adjust(5, 0, 5, 0);
painter.drawText(textRegion_, Qt::AlignVCenter, elidedText);
}
}
void
ImageItem::saveAs()
{
auto filename = QFileDialog::getSaveFileName(this, tr("Save image"), text_);
if (filename.isEmpty())
return;
const auto url = url_.toString().toStdString();
auto proxy = std::make_shared<MediaProxy>();
connect(proxy.get(), &MediaProxy::imageSaved, this, &ImageItem::saveImage);
http::client()->download(
url,
[proxy = std::move(proxy), filename, url](const std::string &data,
const std::string &,
const std::string &,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to retrieve image {}: {} {}",
url,
err->matrix_error.error,
static_cast<int>(err->status_code));
return;
}
emit proxy->imageSaved(filename, QByteArray(data.data(), data.size()));
});
}

View File

@ -1,104 +0,0 @@
/*
* 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 <QEvent>
#include <QMouseEvent>
#include <QSharedPointer>
#include <QWidget>
#include <mtx.hpp>
namespace dialogs {
class ImageOverlay;
}
class ImageItem : public QWidget
{
Q_OBJECT
public:
ImageItem(const mtx::events::RoomEvent<mtx::events::msg::Image> &event,
QWidget *parent = nullptr);
ImageItem(const QString &url,
const QString &filename,
uint64_t size,
QWidget *parent = nullptr);
QSize sizeHint() const override;
public slots:
//! Show a save as dialog for the image.
void saveAs();
void setImage(const QPixmap &image);
void saveImage(const QString &filename, const QByteArray &data);
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
//! Whether the user can interact with the displayed image.
bool isInteractive_ = true;
private:
void init();
void openUrl();
void downloadMedia(const QUrl &url);
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;
QRectF textRegion_;
QRectF imageRegion_;
mtx::events::RoomEvent<mtx::events::msg::Image> event_;
};
class StickerItem : public ImageItem
{
Q_OBJECT
public:
StickerItem(const mtx::events::Sticker &event, QWidget *parent = nullptr)
: ImageItem{QString::fromStdString(event.content.url),
QString::fromStdString(event.content.body),
event.content.info.size,
parent}
, event_{event}
{
isInteractive_ = false;
setCursor(Qt::ArrowCursor);
setMouseTracking(false);
setAttribute(Qt::WA_Hover, false);
}
private:
mtx::events::Sticker event_;
};

View File

@ -1,65 +0,0 @@
/*
* 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 <QLabel>
#include <QVBoxLayout>
#include "Config.h"
#include "MatrixClient.h"
#include "Utils.h"
#include "timeline/widgets/VideoItem.h"
void
VideoItem::init()
{
url_ = utils::mxcToHttp(
url_, QString::fromStdString(http::client()->server()), http::client()->port());
}
VideoItem::VideoItem(const mtx::events::RoomEvent<mtx::events::msg::Video> &event, QWidget *parent)
: QWidget(parent)
, url_{QString::fromStdString(event.content.url)}
, text_{QString::fromStdString(event.content.body)}
, event_{event}
{
readableFileSize_ = utils::humanReadableFileSize(event.content.info.size);
init();
auto layout = new QVBoxLayout(this);
layout->setMargin(0);
layout->setSpacing(0);
QString link = QString("<a href=%1>%2</a>").arg(url_.toString()).arg(text_);
label_ = new QLabel(link, this);
label_->setMargin(0);
label_->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction);
label_->setOpenExternalLinks(true);
layout->addWidget(label_);
}
VideoItem::VideoItem(const QString &url, const QString &filename, uint64_t size, QWidget *parent)
: QWidget(parent)
, url_{url}
, text_{filename}
{
readableFileSize_ = utils::humanReadableFileSize(size);
init();
}

View File

@ -1,51 +0,0 @@
/*
* 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 <QEvent>
#include <QLabel>
#include <QSharedPointer>
#include <QUrl>
#include <QWidget>
#include <mtx.hpp>
class VideoItem : public QWidget
{
Q_OBJECT
public:
VideoItem(const mtx::events::RoomEvent<mtx::events::msg::Video> &event,
QWidget *parent = nullptr);
VideoItem(const QString &url,
const QString &filename,
uint64_t size,
QWidget *parent = nullptr);
private:
void init();
QUrl url_;
QString text_;
QString readableFileSize_;
QLabel *label_;
mtx::events::RoomEvent<mtx::events::msg::Video> event_;
};

View File

@ -1,400 +0,0 @@
#include "TimelineViewManager.h"
#include <QFileDialog>
#include <QMetaType>
#include <QMimeDatabase>
#include <QPalette>
#include <QQmlContext>
#include <QStandardPaths>
#include "ChatPage.h"
#include "ColorImageProvider.h"
#include "DelegateChooser.h"
#include "Logging.h"
#include "MxcImageProvider.h"
#include "UserSettingsPage.h"
#include "dialogs/ImageOverlay.h"
void
TimelineViewManager::updateColorPalette()
{
UserSettings settings;
if (settings.theme() == "light") {
QPalette lightActive(/*windowText*/ QColor("#333"),
/*button*/ QColor("#333"),
/*light*/ QColor(),
/*dark*/ QColor(220, 220, 220, 120),
/*mid*/ QColor(),
/*text*/ QColor("#333"),
/*bright_text*/ QColor(),
/*base*/ QColor("white"),
/*window*/ QColor("white"));
view->rootContext()->setContextProperty("currentActivePalette", lightActive);
view->rootContext()->setContextProperty("currentInactivePalette", lightActive);
} else if (settings.theme() == "dark") {
QPalette darkActive(/*windowText*/ QColor("#caccd1"),
/*button*/ QColor("#caccd1"),
/*light*/ QColor(),
/*dark*/ QColor(45, 49, 57, 120),
/*mid*/ QColor(),
/*text*/ QColor("#caccd1"),
/*bright_text*/ QColor(),
/*base*/ QColor("#202228"),
/*window*/ QColor("#202228"));
darkActive.setColor(QPalette::Highlight, QColor("#e7e7e9"));
view->rootContext()->setContextProperty("currentActivePalette", darkActive);
view->rootContext()->setContextProperty("currentInactivePalette", darkActive);
} else {
view->rootContext()->setContextProperty("currentActivePalette", QPalette());
view->rootContext()->setContextProperty("currentInactivePalette", nullptr);
}
}
TimelineViewManager::TimelineViewManager(QWidget *parent)
: imgProvider(new MxcImageProvider())
, colorImgProvider(new ColorImageProvider())
{
qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
"com.github.nheko",
1,
0,
"MtxEvent",
"Can't instantiate enum!");
qmlRegisterType<DelegateChoice>("com.github.nheko", 1, 0, "DelegateChoice");
qmlRegisterType<DelegateChooser>("com.github.nheko", 1, 0, "DelegateChooser");
#ifdef USE_QUICK_VIEW
view = new QQuickView();
container = QWidget::createWindowContainer(view, parent);
#else
view = new QQuickWidget(parent);
container = view;
view->setResizeMode(QQuickWidget::SizeRootObjectToView);
container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
connect(view, &QQuickWidget::statusChanged, this, [](QQuickWidget::Status status) {
nhlog::ui()->debug("Status changed to {}", status);
});
#endif
container->setMinimumSize(200, 200);
view->rootContext()->setContextProperty("timelineManager", this);
updateColorPalette();
view->engine()->addImageProvider("MxcImage", imgProvider);
view->engine()->addImageProvider("colorimage", colorImgProvider);
view->setSource(QUrl("qrc:///qml/TimelineView.qml"));
connect(dynamic_cast<ChatPage *>(parent),
&ChatPage::themeChanged,
this,
&TimelineViewManager::updateColorPalette);
}
void
TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
{
for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) {
// addRoom will only add the room, if it doesn't exist
addRoom(QString::fromStdString(it->first));
models.value(QString::fromStdString(it->first))->addEvents(it->second.timeline);
}
}
void
TimelineViewManager::addRoom(const QString &room_id)
{
if (!models.contains(room_id))
models.insert(room_id,
QSharedPointer<TimelineModel>(new TimelineModel(this, room_id)));
}
void
TimelineViewManager::setHistoryView(const QString &room_id)
{
nhlog::ui()->info("Trying to activate room {}", room_id.toStdString());
auto room = models.find(room_id);
if (room != models.end()) {
timeline_ = room.value().data();
emit activeTimelineChanged(timeline_);
nhlog::ui()->info("Activated room {}", room_id.toStdString());
}
}
void
TimelineViewManager::openImageOverlay(QString mxcUrl,
QString originalFilename,
QString mimeType,
qml_mtx_events::EventType eventType) const
{
QQuickImageResponse *imgResponse =
imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize());
connect(imgResponse,
&QQuickImageResponse::finished,
this,
[this, mxcUrl, originalFilename, mimeType, eventType, imgResponse]() {
if (!imgResponse->errorString().isEmpty()) {
nhlog::ui()->error("Error when retrieving image for overlay: {}",
imgResponse->errorString().toStdString());
return;
}
auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image());
auto imgDialog = new dialogs::ImageOverlay(pixmap);
imgDialog->show();
connect(imgDialog,
&dialogs::ImageOverlay::saving,
this,
[this, mxcUrl, originalFilename, mimeType, eventType]() {
saveMedia(mxcUrl, originalFilename, mimeType, eventType);
});
});
}
void
TimelineViewManager::saveMedia(QString mxcUrl,
QString originalFilename,
QString mimeType,
qml_mtx_events::EventType eventType) const
{
QString dialogTitle;
if (eventType == qml_mtx_events::EventType::ImageMessage) {
dialogTitle = tr("Save image");
} else if (eventType == qml_mtx_events::EventType::VideoMessage) {
dialogTitle = tr("Save video");
} else if (eventType == qml_mtx_events::EventType::AudioMessage) {
dialogTitle = tr("Save audio");
} else {
dialogTitle = tr("Save file");
}
QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString();
auto filename =
QFileDialog::getSaveFileName(container, dialogTitle, originalFilename, filterString);
if (filename.isEmpty())
return;
const auto url = mxcUrl.toStdString();
http::client()->download(
url,
[filename, url](const std::string &data,
const std::string &,
const std::string &,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to retrieve image {}: {} {}",
url,
err->matrix_error.error,
static_cast<int>(err->status_code));
return;
}
try {
QFile file(filename);
if (!file.open(QIODevice::WriteOnly))
return;
file.write(QByteArray(data.data(), data.size()));
file.close();
} catch (const std::exception &e) {
nhlog::ui()->warn("Error while saving file to: {}", e.what());
}
});
}
void
TimelineViewManager::cacheMedia(QString mxcUrl, QString mimeType)
{
// If the message is a link to a non mxcUrl, don't download it
if (!mxcUrl.startsWith("mxc://")) {
emit mediaCached(mxcUrl, mxcUrl);
return;
}
QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix();
const auto url = mxcUrl.toStdString();
QFileInfo filename(QString("%1/media_cache/%2.%3")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
.arg(QString(mxcUrl).remove("mxc://"))
.arg(suffix));
if (QDir::cleanPath(filename.path()) != filename.path()) {
nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url);
return;
}
QDir().mkpath(filename.path());
if (filename.isReadable()) {
emit mediaCached(mxcUrl, filename.filePath());
return;
}
http::client()->download(
url,
[this, mxcUrl, filename, url](const std::string &data,
const std::string &,
const std::string &,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to retrieve image {}: {} {}",
url,
err->matrix_error.error,
static_cast<int>(err->status_code));
return;
}
try {
QFile file(filename.filePath());
if (!file.open(QIODevice::WriteOnly))
return;
file.write(QByteArray(data.data(), data.size()));
file.close();
} catch (const std::exception &e) {
nhlog::ui()->warn("Error while saving file to: {}", e.what());
}
emit mediaCached(mxcUrl, filename.filePath());
});
}
void
TimelineViewManager::updateReadReceipts(const QString &room_id,
const std::vector<QString> &event_ids)
{
auto room = models.find(room_id);
if (room != models.end()) {
room.value()->markEventsAsRead(event_ids);
}
}
void
TimelineViewManager::initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs)
{
for (const auto &e : msgs) {
addRoom(e.first);
models.value(e.first)->addEvents(e.second);
}
}
void
TimelineViewManager::queueTextMessage(const QString &msg)
{
mtx::events::msg::Text text = {};
text.body = msg.trimmed().toStdString();
text.format = "org.matrix.custom.html";
text.formatted_body = utils::markdownToHtml(msg).toStdString();
if (timeline_)
timeline_->sendMessage(text);
}
void
TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related)
{
mtx::events::msg::Text text = {};
QString body;
bool firstLine = true;
for (const auto &line : related.quoted_body.split("\n")) {
if (firstLine) {
firstLine = false;
body = QString("> <%1> %2\n").arg(related.quoted_user).arg(line);
} else {
body = QString("%1\n> %2\n").arg(body).arg(line);
}
}
text.body = QString("%1\n%2").arg(body).arg(reply).toStdString();
text.format = "org.matrix.custom.html";
text.formatted_body =
utils::getFormattedQuoteBody(related, utils::markdownToHtml(reply)).toStdString();
text.relates_to.in_reply_to.event_id = related.related_event;
if (timeline_)
timeline_->sendMessage(text);
}
void
TimelineViewManager::queueEmoteMessage(const QString &msg)
{
auto html = utils::markdownToHtml(msg);
mtx::events::msg::Emote emote;
emote.body = msg.trimmed().toStdString();
if (html != msg.trimmed().toHtmlEscaped())
emote.formatted_body = html.toStdString();
if (timeline_)
timeline_->sendMessage(emote);
}
void
TimelineViewManager::queueImageMessage(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t dsize,
const QSize &dimensions)
{
mtx::events::msg::Image image;
image.info.mimetype = mime.toStdString();
image.info.size = dsize;
image.body = filename.toStdString();
image.url = url.toStdString();
image.info.h = dimensions.height();
image.info.w = dimensions.width();
models.value(roomid)->sendMessage(image);
}
void
TimelineViewManager::queueFileMessage(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t dsize)
{
mtx::events::msg::File file;
file.info.mimetype = mime.toStdString();
file.info.size = dsize;
file.body = filename.toStdString();
file.url = url.toStdString();
models.value(roomid)->sendMessage(file);
}
void
TimelineViewManager::queueAudioMessage(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t dsize)
{
mtx::events::msg::Audio audio;
audio.info.mimetype = mime.toStdString();
audio.info.size = dsize;
audio.body = filename.toStdString();
audio.url = url.toStdString();
models.value(roomid)->sendMessage(audio);
}
void
TimelineViewManager::queueVideoMessage(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t dsize)
{
mtx::events::msg::Video video;
video.info.mimetype = mime.toStdString();
video.info.size = dsize;
video.body = filename.toStdString();
video.url = url.toStdString();
models.value(roomid)->sendMessage(video);
}

View File

@ -1,117 +0,0 @@
#pragma once
#include <QQuickView>
#include <QQuickWidget>
#include <QSharedPointer>
#include <QWidget>
#include <mtx/responses.hpp>
#include "Cache.h"
#include "Logging.h"
#include "TimelineModel.h"
#include "Utils.h"
// temporary for stubs
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
class MxcImageProvider;
class ColorImageProvider;
class TimelineViewManager : public QObject
{
Q_OBJECT
Q_PROPERTY(
TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged)
public:
TimelineViewManager(QWidget *parent = 0);
QWidget *getWidget() const { return container; }
void sync(const mtx::responses::Rooms &rooms);
void addRoom(const QString &room_id);
void clearAll() { models.clear(); }
Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; }
void openImageOverlay(QString mxcUrl,
QString originalFilename,
QString mimeType,
qml_mtx_events::EventType eventType) const;
void saveMedia(QString mxcUrl,
QString originalFilename,
QString mimeType,
qml_mtx_events::EventType eventType) const;
Q_INVOKABLE void cacheMedia(QString mxcUrl, QString mimeType);
// Qml can only pass enum as int
Q_INVOKABLE void openImageOverlay(QString mxcUrl,
QString originalFilename,
QString mimeType,
int eventType) const
{
openImageOverlay(
mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType);
}
Q_INVOKABLE void saveMedia(QString mxcUrl,
QString originalFilename,
QString mimeType,
int eventType) const
{
saveMedia(mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType);
}
signals:
void clearRoomMessageCount(QString roomid);
void updateRoomsLastMessage(QString roomid, const DescInfo &info);
void activeTimelineChanged(TimelineModel *timeline);
void mediaCached(QString mxcUrl, QString cacheUrl);
public slots:
void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
void initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs);
void setHistoryView(const QString &room_id);
void updateColorPalette();
void queueTextMessage(const QString &msg);
void queueReplyMessage(const QString &reply, const RelatedInfo &related);
void queueEmoteMessage(const QString &msg);
void queueImageMessage(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t dsize,
const QSize &dimensions);
void queueFileMessage(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t dsize);
void queueAudioMessage(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t dsize);
void queueVideoMessage(const QString &roomid,
const QString &filename,
const QString &url,
const QString &mime,
uint64_t dsize);
private:
#ifdef USE_QUICK_VIEW
QQuickView *view;
#else
QQuickWidget *view;
#endif
QWidget *container;
TimelineModel *timeline_ = nullptr;
MxcImageProvider *imgProvider;
ColorImageProvider *colorImgProvider;
QHash<QString, QSharedPointer<TimelineModel>> models;
};
#pragma GCC diagnostic pop