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_;
+};
|