summary refs log tree commit diff
path: root/src/timeline
diff options
context:
space:
mode:
authorKonstantinos Sideris <sideris.konstantin@gmail.com>2018-07-17 16:37:25 +0300
committerKonstantinos Sideris <sideris.konstantin@gmail.com>2018-07-17 16:37:25 +0300
commit0e814da91c8e041897a4c3f7e6e9234bbc7c6f7a (patch)
tree21f655d30630fe77ba48d07e4b357e2b6c6a5730 /src/timeline
parentMerge pull request #372 from bebehei/notification (diff)
downloadnheko-0e814da91c8e041897a4c3f7e6e9234bbc7c6f7a.tar.xz
Move all files under src/
Diffstat (limited to 'src/timeline')
-rw-r--r--src/timeline/TimelineItem.cpp (renamed from src/timeline/TimelineItem.cc)8
-rw-r--r--src/timeline/TimelineItem.h380
-rw-r--r--src/timeline/TimelineView.cpp (renamed from src/timeline/TimelineView.cc)8
-rw-r--r--src/timeline/TimelineView.h426
-rw-r--r--src/timeline/TimelineViewManager.cpp (renamed from src/timeline/TimelineViewManager.cc)2
-rw-r--r--src/timeline/TimelineViewManager.h94
-rw-r--r--src/timeline/widgets/AudioItem.cpp (renamed from src/timeline/widgets/AudioItem.cc)2
-rw-r--r--src/timeline/widgets/AudioItem.h107
-rw-r--r--src/timeline/widgets/FileItem.cpp (renamed from src/timeline/widgets/FileItem.cc)2
-rw-r--r--src/timeline/widgets/FileItem.h82
-rw-r--r--src/timeline/widgets/ImageItem.cpp (renamed from src/timeline/widgets/ImageItem.cc)4
-rw-r--r--src/timeline/widgets/ImageItem.h108
-rw-r--r--src/timeline/widgets/VideoItem.cpp (renamed from src/timeline/widgets/VideoItem.cc)0
-rw-r--r--src/timeline/widgets/VideoItem.h51
14 files changed, 1261 insertions, 13 deletions
diff --git a/src/timeline/TimelineItem.cc b/src/timeline/TimelineItem.cpp

index d756ca26..88ab1963 100644 --- a/src/timeline/TimelineItem.cc +++ b/src/timeline/TimelineItem.cpp
@@ -20,12 +20,12 @@ #include <QMenu> #include <QTimer> -#include "Avatar.h" #include "ChatPage.h" #include "Config.h" -#include "Logging.hpp" -#include "Olm.hpp" -#include "Painter.h" +#include "Logging.h" +#include "Olm.h" +#include "ui/Avatar.h" +#include "ui/Painter.h" #include "timeline/TimelineItem.h" #include "timeline/widgets/AudioItem.h" diff --git a/src/timeline/TimelineItem.h b/src/timeline/TimelineItem.h new file mode 100644
index 00000000..d3cab0a0 --- /dev/null +++ b/src/timeline/TimelineItem.h
@@ -0,0 +1,380 @@ +/* + * 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 <QAbstractTextDocumentLayout> +#include <QApplication> +#include <QDateTime> +#include <QHBoxLayout> +#include <QLabel> +#include <QLayout> +#include <QPainter> +#include <QSettings> +#include <QStyle> +#include <QStyleOption> +#include <QTextBrowser> +#include <QTimer> + +#include "AvatarProvider.h" +#include "RoomInfoListItem.h" +#include "Utils.h" + +#include "Cache.h" +#include "MatrixClient.h" + +class ImageItem; +class StickerItem; +class AudioItem; +class VideoItem; +class FileItem; +class Avatar; + +enum class StatusIndicatorState +{ + //! The encrypted message was received by the server. + Encrypted, + //! The plaintext message was received by the server. + Received, + //! 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); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + void paintIcon(QPainter &p, QIcon &icon); + + QIcon lockIcon_; + QIcon clockIcon_; + QIcon checkmarkIcon_; + + QColor iconColor_ = QColor("#999"); + + StatusIndicatorState state_ = StatusIndicatorState::Empty; + + static constexpr int MaxWidth = 24; +}; + +class TextLabel : public QTextBrowser +{ + Q_OBJECT + +public: + TextLabel(const QString &text, QWidget *parent = 0) + : QTextBrowser(parent) + { + setText(text); + setOpenExternalLinks(true); + + // Make it look and feel like an ordinary label. + setReadOnly(true); + setFrameStyle(QFrame::NoFrame); + QPalette pal = palette(); + pal.setColor(QPalette::Base, Qt::transparent); + setPalette(pal); + + // Wrap anywhere but prefer words, adjust minimum height on the fly. + setLineWrapMode(QTextEdit::WidgetWidth); + setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); + connect(document()->documentLayout(), + &QAbstractTextDocumentLayout::documentSizeChanged, + this, + &TextLabel::adjustHeight); + document()->setDocumentMargin(0); + + setFixedHeight(20); + } + + void wheelEvent(QWheelEvent *event) override { event->ignore(); } + +private slots: + void adjustHeight(const QSizeF &size) { setFixedHeight(size.height()); } +}; + +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(); + +protected: + bool eventFilter(QObject *obj, QEvent *event) + { + if (event->type() == QEvent::MouseButtonRelease) { + // QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event); + // TODO: Open user profile + 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 +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); + + void setUserAvatar(const QImage &pixmap); + 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 markSent(); + bool isReceived() { return isReceived_; }; + void setRoomId(QString room_id) { room_id_ = room_id; } + void sendReadReceipt() const; + + //! Add a user avatar for this event. + void addAvatar(); + void addKeyRequestAction(); + +signals: + void eventRedacted(const QString &event_id); + void redactionFailed(const QString &msg); + +protected: + void paintEvent(QPaintEvent *event) override; + void contextMenuEvent(QContextMenuEvent *event) override; + +private: + 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 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; + + QString replaceEmoji(const QString &body); + QString event_id_; + QString room_id_; + + DescInfo descriptionMsg_; + + QMenu *contextMenu_; + QAction *showReadReceipts_; + QAction *markAsRead_; + QAction *redactMsg_; + QAction *replyMsg_; + + QHBoxLayout *topLayout_ = nullptr; + QHBoxLayout *messageLayout_ = nullptr; + QVBoxLayout *mainLayout_ = nullptr; + QHBoxLayout *widgetLayout_ = nullptr; + + Avatar *userAvatar_; + + QFont font_; + QFont usernameFont_; + + StatusIndicator *statusIndicator_; + + QLabel *timestamp_; + QLabel *userName_; + TextLabel *body_; +}; + +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_ = {"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); + + AvatarProvider::resolve( + room_id_, userid, this, [this](const QImage &img) { setUserAvatar(img); }); + } else { + setupSimpleLayout(); + } + + adjustMessageLayoutForWidget(); +} + +template<class Event, class Widget> +void +TimelineItem::setupWidgetLayout(Widget *widget, const Event &event, bool withSender) +{ + init(); + + 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_ = {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); + + AvatarProvider::resolve( + room_id_, sender, this, [this](const QImage &img) { setUserAvatar(img); }); + } else { + setupSimpleLayout(); + } + + adjustMessageLayoutForWidget(); +} diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cpp
index 207844e4..a8c04807 100644 --- a/src/timeline/TimelineView.cc +++ b/src/timeline/TimelineView.cpp
@@ -22,12 +22,12 @@ #include "Cache.h" #include "ChatPage.h" #include "Config.h" -#include "FloatingButton.h" -#include "InfoMessage.hpp" -#include "Logging.hpp" -#include "Olm.hpp" +#include "Logging.h" +#include "Olm.h" #include "UserSettingsPage.h" #include "Utils.h" +#include "ui/FloatingButton.h" +#include "ui/InfoMessage.h" #include "timeline/TimelineView.h" #include "timeline/widgets/AudioItem.h" diff --git a/src/timeline/TimelineView.h b/src/timeline/TimelineView.h new file mode 100644
index 00000000..7b269063 --- /dev/null +++ b/src/timeline/TimelineView.h
@@ -0,0 +1,426 @@ +/* + * 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 <QStyle> +#include <QStyleOption> +#include <QTimer> + +#include <mtx/events.hpp> +#include <mtx/responses/messages.hpp> + +#include "MatrixClient.h" +#include "timeline/TimelineItem.h" +#include "ui/ScrollBar.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; + 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 &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() > 1 || 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); + +protected: + void paintEvent(QPaintEvent *event) override; + void showEvent(QShowEvent *event) override; + bool event(QEvent *event) override; + +private: + using TimelineEvent = mtx::events::collections::TimelineEvents; + + 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) + { + item->hide(); + scroll_layout_->addWidget(item); + QTimer::singleShot(0, this, [item]() { item->show(); }); + }; + + //! 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); + + QVBoxLayout *top_layout_; + QVBoxLayout *scroll_layout_; + + QScrollArea *scroll_area_; + ScrollBar *scrollbar_; + 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; +} diff --git a/src/timeline/TimelineViewManager.cc b/src/timeline/TimelineViewManager.cpp
index c8e00b66..1decab35 100644 --- a/src/timeline/TimelineViewManager.cc +++ b/src/timeline/TimelineViewManager.cpp
@@ -22,7 +22,7 @@ #include <QSettings> #include "Cache.h" -#include "Logging.hpp" +#include "Logging.h" #include "timeline/TimelineView.h" #include "timeline/TimelineViewManager.h" #include "timeline/widgets/AudioItem.h" diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h new file mode 100644
index 00000000..f3c099c1 --- /dev/null +++ b/src/timeline/TimelineViewManager.h
@@ -0,0 +1,94 @@ +/* + * 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 <QSharedPointer> +#include <QStackedWidget> + +#include <mtx.hpp> + +class QFile; + +class RoomInfoListItem; +class TimelineView; +struct DescInfo; +struct SavedMessages; + +class TimelineViewManager : public QStackedWidget +{ + Q_OBJECT + +public: + TimelineViewManager(QWidget *parent); + + // Initialize with timeline events. + void initialize(const mtx::responses::Rooms &rooms); + // Empty initialization. + void initialize(const std::vector<std::string> &rooms); + + void addRoom(const mtx::responses::JoinedRoom &room, const QString &room_id); + void addRoom(const QString &room_id); + + void sync(const mtx::responses::Rooms &rooms); + void clearAll() { views_.clear(); } + + // Check if all the timelines have been loaded. + bool hasLoaded() const; + + static QString chooseRandomColor(); + +signals: + void clearRoomMessageCount(QString roomid); + void updateRoomsLastMessage(const QString &user, const DescInfo &info); + +public slots: + void removeTimelineEvent(const QString &room_id, const QString &event_id); + void initWithMessages(const std::map<QString, mtx::responses::Timeline> &msgs); + + void setHistoryView(const QString &room_id); + void queueTextMessage(const QString &msg); + 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: + //! Check if the given room id is managed by a TimelineView. + bool timelineViewExists(const QString &id) { return views_.find(id) != views_.end(); } + + QString active_room_; + std::map<QString, QSharedPointer<TimelineView>> views_; +}; diff --git a/src/timeline/widgets/AudioItem.cc b/src/timeline/widgets/AudioItem.cpp
index 2ed4f4c0..1e3eb0f0 100644 --- a/src/timeline/widgets/AudioItem.cc +++ b/src/timeline/widgets/AudioItem.cpp
@@ -22,7 +22,7 @@ #include <QPainter> #include <QPixmap> -#include "Logging.hpp" +#include "Logging.h" #include "MatrixClient.h" #include "Utils.h" diff --git a/src/timeline/widgets/AudioItem.h b/src/timeline/widgets/AudioItem.h new file mode 100644
index 00000000..7b0781a2 --- /dev/null +++ b/src/timeline/widgets/AudioItem.h
@@ -0,0 +1,107 @@ +/* + * 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; + +signals: + void fileDownloadedCb(const QByteArray &data); + +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"); +}; diff --git a/src/timeline/widgets/FileItem.cc b/src/timeline/widgets/FileItem.cpp
index b4555b2f..f8d3272d 100644 --- a/src/timeline/widgets/FileItem.cc +++ b/src/timeline/widgets/FileItem.cpp
@@ -22,7 +22,7 @@ #include <QPainter> #include <QPixmap> -#include "Logging.hpp" +#include "Logging.h" #include "MatrixClient.h" #include "Utils.h" diff --git a/src/timeline/widgets/FileItem.h b/src/timeline/widgets/FileItem.h new file mode 100644
index 00000000..66543e79 --- /dev/null +++ b/src/timeline/widgets/FileItem.h
@@ -0,0 +1,82 @@ +/* + * 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_; } + +signals: + void fileDownloadedCb(const QByteArray &data); + +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"); +}; diff --git a/src/timeline/widgets/ImageItem.cc b/src/timeline/widgets/ImageItem.cpp
index b7adb0fa..19b445db 100644 --- a/src/timeline/widgets/ImageItem.cc +++ b/src/timeline/widgets/ImageItem.cpp
@@ -24,11 +24,11 @@ #include <QUuid> #include "Config.h" -#include "Logging.hpp" +#include "ImageItem.h" +#include "Logging.h" #include "MatrixClient.h" #include "Utils.h" #include "dialogs/ImageOverlay.h" -#include "timeline/widgets/ImageItem.h" void ImageItem::downloadMedia(const QUrl &url) diff --git a/src/timeline/widgets/ImageItem.h b/src/timeline/widgets/ImageItem.h new file mode 100644
index 00000000..e9d823f4 --- /dev/null +++ b/src/timeline/widgets/ImageItem.h
@@ -0,0 +1,108 @@ +/* + * 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); + +signals: + void imageDownloaded(const QPixmap &img); + void imageSaved(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_; +}; diff --git a/src/timeline/widgets/VideoItem.cc b/src/timeline/widgets/VideoItem.cpp
index daf181b2..daf181b2 100644 --- a/src/timeline/widgets/VideoItem.cc +++ b/src/timeline/widgets/VideoItem.cpp
diff --git a/src/timeline/widgets/VideoItem.h b/src/timeline/widgets/VideoItem.h new file mode 100644
index 00000000..26fa1c35 --- /dev/null +++ b/src/timeline/widgets/VideoItem.h
@@ -0,0 +1,51 @@ +/* + * 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_; +};