forked from mirror/nheko
parent
e8f8182844
commit
91d1f19058
@ -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); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
@ -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
@ -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; |
|
||||||
} |
|
@ -1,340 +1,400 @@ |
|||||||
/*
|
#include "TimelineViewManager.h" |
||||||
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> |
|
||||||
* |
#include <QFileDialog> |
||||||
* This program is free software: you can redistribute it and/or modify |
#include <QMetaType> |
||||||
* it under the terms of the GNU General Public License as published by |
#include <QMimeDatabase> |
||||||
* the Free Software Foundation, either version 3 of the License, or |
#include <QPalette> |
||||||
* (at your option) any later version. |
#include <QQmlContext> |
||||||
* |
#include <QStandardPaths> |
||||||
* This program is distributed in the hope that it will be useful, |
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
#include "ChatPage.h" |
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
#include "ColorImageProvider.h" |
||||||
* GNU General Public License for more details. |
#include "DelegateChooser.h" |
||||||
* |
|
||||||
* 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 <random> |
|
||||||
|
|
||||||
#include <QApplication> |
|
||||||
#include <QFileInfo> |
|
||||||
#include <QSettings> |
|
||||||
|
|
||||||
#include "Cache.h" |
|
||||||
#include "Logging.h" |
#include "Logging.h" |
||||||
#include "Utils.h" |
#include "MxcImageProvider.h" |
||||||
#include "timeline/TimelineView.h" |
#include "UserSettingsPage.h" |
||||||
#include "timeline/TimelineViewManager.h" |
#include "dialogs/ImageOverlay.h" |
||||||
#include "timeline/widgets/AudioItem.h" |
|
||||||
#include "timeline/widgets/FileItem.h" |
|
||||||
#include "timeline/widgets/ImageItem.h" |
|
||||||
#include "timeline/widgets/VideoItem.h" |
|
||||||
|
|
||||||
TimelineViewManager::TimelineViewManager(QWidget *parent) |
|
||||||
: QStackedWidget(parent) |
|
||||||
{} |
|
||||||
|
|
||||||
void |
void |
||||||
TimelineViewManager::updateReadReceipts(const QString &room_id, |
TimelineViewManager::updateColorPalette() |
||||||
const std::vector<QString> &event_ids) |
|
||||||
{ |
{ |
||||||
if (timelineViewExists(room_id)) { |
UserSettings settings; |
||||||
auto view = views_[room_id]; |
if (settings.theme() == "light") { |
||||||
if (view) |
QPalette lightActive(/*windowText*/ QColor("#333"), |
||||||
emit view->markReadEvents(event_ids); |
/*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); |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
void |
TimelineViewManager::TimelineViewManager(QWidget *parent) |
||||||
TimelineViewManager::removeTimelineEvent(const QString &room_id, const QString &event_id) |
: imgProvider(new MxcImageProvider()) |
||||||
|
, colorImgProvider(new ColorImageProvider()) |
||||||
{ |
{ |
||||||
auto view = views_[room_id]; |
qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject, |
||||||
|
"com.github.nheko", |
||||||
if (view) |
1, |
||||||
view->removeEvent(event_id); |
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 |
void |
||||||
TimelineViewManager::queueTextMessage(const QString &msg) |
TimelineViewManager::sync(const mtx::responses::Rooms &rooms) |
||||||
{ |
{ |
||||||
if (active_room_.isEmpty()) |
for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { |
||||||
return; |
// addRoom will only add the room, if it doesn't exist
|
||||||
|
addRoom(QString::fromStdString(it->first)); |
||||||
auto room_id = active_room_; |
models.value(QString::fromStdString(it->first))->addEvents(it->second.timeline); |
||||||
auto view = views_[room_id]; |
} |
||||||
|
|
||||||
view->addUserMessage(mtx::events::MessageType::Text, msg); |
|
||||||
} |
} |
||||||
|
|
||||||
void |
void |
||||||
TimelineViewManager::queueEmoteMessage(const QString &msg) |
TimelineViewManager::addRoom(const QString &room_id) |
||||||
{ |
{ |
||||||
if (active_room_.isEmpty()) |
if (!models.contains(room_id)) |
||||||
return; |
models.insert(room_id, |
||||||
|
QSharedPointer<TimelineModel>(new TimelineModel(this, room_id))); |
||||||
auto room_id = active_room_; |
|
||||||
auto view = views_[room_id]; |
|
||||||
|
|
||||||
view->addUserMessage(mtx::events::MessageType::Emote, msg); |
|
||||||
} |
} |
||||||
|
|
||||||
void |
void |
||||||
TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related) |
TimelineViewManager::setHistoryView(const QString &room_id) |
||||||
{ |
{ |
||||||
if (active_room_.isEmpty()) |
nhlog::ui()->info("Trying to activate room {}", room_id.toStdString()); |
||||||
return; |
|
||||||
|
|
||||||
auto room_id = active_room_; |
auto room = models.find(room_id); |
||||||
auto view = views_[room_id]; |
if (room != models.end()) { |
||||||
|
timeline_ = room.value().data(); |
||||||
view->addUserMessage(mtx::events::MessageType::Text, reply, related); |
emit activeTimelineChanged(timeline_); |
||||||
|
nhlog::ui()->info("Activated room {}", room_id.toStdString()); |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
void |
void |
||||||
TimelineViewManager::queueImageMessage(const QString &roomid, |
TimelineViewManager::openImageOverlay(QString mxcUrl, |
||||||
const QString &filename, |
QString originalFilename, |
||||||
const QString &url, |
QString mimeType, |
||||||
const QString &mime, |
qml_mtx_events::EventType eventType) const |
||||||
uint64_t size, |
|
||||||
const QSize &dimensions) |
|
||||||
{ |
{ |
||||||
if (!timelineViewExists(roomid)) { |
QQuickImageResponse *imgResponse = |
||||||
nhlog::ui()->warn("Cannot send m.image message to a non-managed view"); |
imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize()); |
||||||
return; |
connect(imgResponse, |
||||||
} |
&QQuickImageResponse::finished, |
||||||
|
this, |
||||||
auto view = views_[roomid]; |
[this, mxcUrl, originalFilename, mimeType, eventType, imgResponse]() { |
||||||
|
if (!imgResponse->errorString().isEmpty()) { |
||||||
view->addUserMessage<ImageItem, mtx::events::MessageType::Image>( |
nhlog::ui()->error("Error when retrieving image for overlay: {}", |
||||||
url, filename, mime, size, dimensions); |
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 |
void |
||||||
TimelineViewManager::queueFileMessage(const QString &roomid, |
TimelineViewManager::saveMedia(QString mxcUrl, |
||||||
const QString &filename, |
QString originalFilename, |
||||||
const QString &url, |
QString mimeType, |
||||||
const QString &mime, |
qml_mtx_events::EventType eventType) const |
||||||
uint64_t size) |
|
||||||
{ |
{ |
||||||
if (!timelineViewExists(roomid)) { |
QString dialogTitle; |
||||||
nhlog::ui()->warn("cannot send m.file message to a non-managed view"); |
if (eventType == qml_mtx_events::EventType::ImageMessage) { |
||||||
return; |
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"); |
||||||
} |
} |
||||||
|
|
||||||
auto view = views_[roomid]; |
QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); |
||||||
|
|
||||||
|
auto filename = |
||||||
|
QFileDialog::getSaveFileName(container, dialogTitle, originalFilename, filterString); |
||||||
|
|
||||||
|
if (filename.isEmpty()) |
||||||
|
return; |
||||||
|
|
||||||
view->addUserMessage<FileItem, mtx::events::MessageType::File>(url, filename, mime, size); |
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 |
void |
||||||
TimelineViewManager::queueAudioMessage(const QString &roomid, |
TimelineViewManager::cacheMedia(QString mxcUrl, QString mimeType) |
||||||
const QString &filename, |
|
||||||
const QString &url, |
|
||||||
const QString &mime, |
|
||||||
uint64_t size) |
|
||||||
{ |
{ |
||||||
if (!timelineViewExists(roomid)) { |
// If the message is a link to a non mxcUrl, don't download it
|
||||||
nhlog::ui()->warn("cannot send m.audio message to a non-managed view"); |
if (!mxcUrl.startsWith("mxc://")) { |
||||||
|
emit mediaCached(mxcUrl, mxcUrl); |
||||||
return; |
return; |
||||||
} |
} |
||||||
|
|
||||||
auto view = views_[roomid]; |
QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); |
||||||
|
|
||||||
view->addUserMessage<AudioItem, mtx::events::MessageType::Audio>(url, filename, mime, size); |
|
||||||
} |
|
||||||
|
|
||||||
void |
const auto url = mxcUrl.toStdString(); |
||||||
TimelineViewManager::queueVideoMessage(const QString &roomid, |
QFileInfo filename(QString("%1/media_cache/%2.%3") |
||||||
const QString &filename, |
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) |
||||||
const QString &url, |
.arg(QString(mxcUrl).remove("mxc://")) |
||||||
const QString &mime, |
.arg(suffix)); |
||||||
uint64_t size) |
if (QDir::cleanPath(filename.path()) != filename.path()) { |
||||||
{ |
nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); |
||||||
if (!timelineViewExists(roomid)) { |
|
||||||
nhlog::ui()->warn("cannot send m.video message to a non-managed view"); |
|
||||||
return; |
return; |
||||||
} |
} |
||||||
|
|
||||||
auto view = views_[roomid]; |
QDir().mkpath(filename.path()); |
||||||
|
|
||||||
view->addUserMessage<VideoItem, mtx::events::MessageType::Video>(url, filename, mime, size); |
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 |
void |
||||||
TimelineViewManager::initialize(const mtx::responses::Rooms &rooms) |
TimelineViewManager::updateReadReceipts(const QString &room_id, |
||||||
|
const std::vector<QString> &event_ids) |
||||||
{ |
{ |
||||||
for (auto it = rooms.join.cbegin(); it != rooms.join.cend(); ++it) { |
auto room = models.find(room_id); |
||||||
addRoom(it->second, QString::fromStdString(it->first)); |
if (room != models.end()) { |
||||||
|
room.value()->markEventsAsRead(event_ids); |
||||||
} |
} |
||||||
|
|
||||||
sync(rooms); |
|
||||||
} |
} |
||||||
|
|
||||||
void |
void |
||||||
TimelineViewManager::initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs) |
TimelineViewManager::initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs) |
||||||
{ |
{ |
||||||
for (auto it = msgs.cbegin(); it != msgs.cend(); ++it) { |
for (const auto &e : msgs) { |
||||||
if (timelineViewExists(it->first)) |
addRoom(e.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, |
models.value(e.first)->addEvents(e.second); |
||||||
&TimelineView::updateLastTimelineMessage, |
|
||||||
this, |
|
||||||
&TimelineViewManager::updateRoomsLastMessage); |
|
||||||
|
|
||||||
// Add the view in the widget stack.
|
|
||||||
addWidget(view); |
|
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
void |
void |
||||||
TimelineViewManager::initialize(const std::vector<std::string> &rooms) |
TimelineViewManager::queueTextMessage(const QString &msg) |
||||||
{ |
{ |
||||||
for (const auto &roomid : rooms) |
mtx::events::msg::Text text = {}; |
||||||
addRoom(QString::fromStdString(roomid)); |
text.body = msg.trimmed().toStdString(); |
||||||
|
text.format = "org.matrix.custom.html"; |
||||||
|
text.formatted_body = utils::markdownToHtml(msg).toStdString(); |
||||||
|
|
||||||
|
if (timeline_) |
||||||
|
timeline_->sendMessage(text); |
||||||
} |
} |
||||||
|
|
||||||
void |
void |
||||||
TimelineViewManager::addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id) |
TimelineViewManager::queueReplyMessage(const QString &reply, const RelatedInfo &related) |
||||||
{ |
{ |
||||||
if (timelineViewExists(room_id)) |
mtx::events::msg::Text text = {}; |
||||||
return; |
|
||||||
|
QString body; |
||||||
// Create a history view with the room events.
|
bool firstLine = true; |
||||||
TimelineView *view = new TimelineView(room.timeline, room_id); |
for (const auto &line : related.quoted_body.split("\n")) { |
||||||
views_.emplace(room_id, QSharedPointer<TimelineView>(view)); |
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); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
connect(view, |
text.body = QString("%1\n%2").arg(body).arg(reply).toStdString(); |
||||||
&TimelineView::updateLastTimelineMessage, |
text.format = "org.matrix.custom.html"; |
||||||
this, |
text.formatted_body = |
||||||
&TimelineViewManager::updateRoomsLastMessage); |
utils::getFormattedQuoteBody(related, utils::markdownToHtml(reply)).toStdString(); |
||||||
|
text.relates_to.in_reply_to.event_id = related.related_event; |
||||||
|
|
||||||
// Add the view in the widget stack.
|
if (timeline_) |
||||||
addWidget(view); |
timeline_->sendMessage(text); |
||||||
} |
} |
||||||
|
|
||||||
void |
void |
||||||
TimelineViewManager::addRoom(const QString &room_id) |
TimelineViewManager::queueEmoteMessage(const QString &msg) |
||||||
{ |
{ |
||||||
if (timelineViewExists(room_id)) |
auto html = utils::markdownToHtml(msg); |
||||||
return; |
|
||||||
|
|
||||||
// Create a history view without any events.
|
mtx::events::msg::Emote emote; |
||||||
TimelineView *view = new TimelineView(room_id); |
emote.body = msg.trimmed().toStdString(); |
||||||
views_.emplace(room_id, QSharedPointer<TimelineView>(view)); |
|
||||||
|
|
||||||
connect(view, |
if (html != msg.trimmed().toHtmlEscaped()) |
||||||
&TimelineView::updateLastTimelineMessage, |
emote.formatted_body = html.toStdString(); |
||||||
this, |
|
||||||
&TimelineViewManager::updateRoomsLastMessage); |
|
||||||
|
|
||||||
// Add the view in the widget stack.
|
if (timeline_) |
||||||
addWidget(view); |
timeline_->sendMessage(emote); |
||||||
} |
} |
||||||
|
|
||||||
void |
void |
||||||
TimelineViewManager::sync(const mtx::responses::Rooms &rooms) |
TimelineViewManager::queueImageMessage(const QString &roomid, |
||||||
|
const QString &filename, |
||||||
|
const QString &url, |
||||||
|
const QString &mime, |
||||||
|
uint64_t dsize, |
||||||
|
const QSize &dimensions) |
||||||
{ |
{ |
||||||
for (const auto &room : rooms.join) { |
mtx::events::msg::Image image; |
||||||
auto roomid = QString::fromStdString(room.first); |
image.info.mimetype = mime.toStdString(); |
||||||
|
image.info.size = dsize; |
||||||
if (!timelineViewExists(roomid)) { |
image.body = filename.toStdString(); |
||||||
nhlog::ui()->warn("ignoring event from unknown room: {}", |
image.url = url.toStdString(); |
||||||
roomid.toStdString()); |
image.info.h = dimensions.height(); |
||||||
continue; |
image.info.w = dimensions.width(); |
||||||
} |
models.value(roomid)->sendMessage(image); |
||||||
|
|
||||||
auto view = views_.at(roomid); |
|
||||||
|
|
||||||
view->addEvents(room.second.timeline); |
|
||||||
} |
|
||||||
} |
} |
||||||
|
|
||||||
void |
void |
||||||
TimelineViewManager::setHistoryView(const QString &room_id) |
TimelineViewManager::queueFileMessage(const QString &roomid, |
||||||
|
const QString &filename, |
||||||
|
const QString &url, |
||||||
|
const QString &mime, |
||||||
|
uint64_t dsize) |
||||||
{ |
{ |
||||||
if (!timelineViewExists(room_id)) { |
mtx::events::msg::File file; |
||||||
nhlog::ui()->warn("room from RoomList is not present in ViewManager: {}", |
file.info.mimetype = mime.toStdString(); |
||||||
room_id.toStdString()); |
file.info.size = dsize; |
||||||
return; |
file.body = filename.toStdString(); |
||||||
} |
file.url = url.toStdString(); |
||||||
|
models.value(roomid)->sendMessage(file); |
||||||
active_room_ = room_id; |
|
||||||
auto view = views_.at(room_id); |
|
||||||
|
|
||||||
setCurrentWidget(view.data()); |
|
||||||
|
|
||||||
view->fetchHistory(); |
|
||||||
view->scrollDown(); |
|
||||||
} |
} |
||||||
|
|
||||||
QString |
void |
||||||
TimelineViewManager::chooseRandomColor() |
TimelineViewManager::queueAudioMessage(const QString &roomid, |
||||||
|
const QString &filename, |
||||||
|
const QString &url, |
||||||
|
const QString &mime, |
||||||
|
uint64_t dsize) |
||||||
{ |
{ |
||||||
std::random_device random_device; |
mtx::events::msg::Audio audio; |
||||||
std::mt19937 engine{random_device()}; |
audio.info.mimetype = mime.toStdString(); |
||||||
std::uniform_real_distribution<float> dist(0, 1); |
audio.info.size = dsize; |
||||||
|
audio.body = filename.toStdString(); |
||||||
float hue = dist(engine); |
audio.url = url.toStdString(); |
||||||
float saturation = 0.9; |
models.value(roomid)->sendMessage(audio); |
||||||
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 |
void |
||||||
TimelineViewManager::hasLoaded() const |
TimelineViewManager::queueVideoMessage(const QString &roomid, |
||||||
|
const QString &filename, |
||||||
|
const QString &url, |
||||||
|
const QString &mime, |
||||||
|
uint64_t dsize) |
||||||
{ |
{ |
||||||
return std::all_of(views_.cbegin(), views_.cend(), [](const auto &view) { |
mtx::events::msg::Video video; |
||||||
return view.second->hasLoaded(); |
video.info.mimetype = mime.toStdString(); |
||||||
}); |
video.info.size = dsize; |
||||||
|
video.body = filename.toStdString(); |
||||||
|
video.url = url.toStdString(); |
||||||
|
models.value(roomid)->sendMessage(video); |
||||||
} |
} |
||||||
|
@ -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_); |
|
||||||
} |
|
@ -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"); |
|
||||||
}; |
|
@ -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_); |
|
||||||
} |
|
@ -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"); |
|
||||||
}; |
|
@ -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())); |
|
||||||
}); |
|
||||||
} |
|
@ -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_; |
|
||||||
}; |
|
@ -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(); |
|
||||||
} |
|
@ -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_; |
|
||||||
}; |
|
@ -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); |
|
||||||
} |
|
@ -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 |
|
Loading…
Reference in new issue