/* * nheko Copyright (C) 2017 Konstantinos Sideris * * 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 . */ #pragma once #include #include #include #include #include #include #include #include #include #include #include "MatrixClient.h" #include "ScrollBar.h" #include "TimelineItem.h" 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; int txn_id; QString body; QString filename; QString event_id; TimelineItem *widget; PendingMessage(mtx::events::MessageType ty, int txn_id, QString body, QString filename, QString event_id, TimelineItem *widget) : ty(ty) , txn_id(txn_id) , body(body) , filename(filename) , event_id(event_id) , widget(widget) {} }; // 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, QSharedPointer client, const QString &room_id, QWidget *parent = 0); TimelineView(QSharedPointer client, const QString &room_id, QWidget *parent = 0); // Add new events at the end of the timeline. int addEvents(const mtx::responses::Timeline &timeline); void addUserMessage(mtx::events::MessageType ty, const QString &msg); template void addUserMessage( const QString &url, const QString &filename, const QSharedPointer data = QSharedPointer(nullptr)); void updatePendingMessage(int txn_id, QString event_id); void scrollDown(); void addDateSeparator(QDateTime datetime, int position); 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 QString &room_id, const mtx::responses::Messages &msgs); // Whether or not the initial batch has been loaded. bool hasLoaded() { return scroll_layout_->count() > 1 || isTimelineFinished; } void handleFailedMessage(int txnid); private slots: void sendNextPendingMessage(); signals: void updateLastTimelineMessage(const QString &user, const DescInfo &info); void clearUnreadMessageCount(const QString &room_id); protected: void paintEvent(QPaintEvent *event) override; void showEvent(QShowEvent *event) override; bool event(QEvent *event) override; private: using TimelineEvent = mtx::events::collections::TimelineEvents; //! HACK: Fixing layout flickering when adding to the bottom //! of the timeline. void pushTimelineItem(TimelineItem *item) { item->hide(); scroll_layout_->addWidget(item); QTimer::singleShot(0, this, [=]() { item->show(); }); }; //! Decides whether or not to show or hide the scroll down button. void toggleScrollDownButton(); void init(); void addTimelineItem(TimelineItem *item, TimelineDirection direction); void updateLastSender(const QString &user_id, TimelineDirection direction); void notifyForLastEvent(); void notifyForLastEvent(const TimelineEvent &event); void readLastEvent() const; bool isScrollbarActivated() { return scroll_area_->verticalScrollBar()->value() != 0; } QString getLastEventId() const; QString getEventSender(const mtx::events::collections::TimelineEvents &event) const; template TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction); // TODO: Remove this eventually. template TimelineItem *processMessageEvent(const Event &event, TimelineDirection direction); // For events with custom display widgets. template TimelineItem *createTimelineItem(const Event &event, bool withSender); // For events without custom display widgets. // TODO: All events should have custom widgets. template 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, TimelineDirection direction); bool isPendingMessage(const QString &txnid, const QString &sender, const QString &userid); void removePendingMessage(const QString &txnid); bool isDuplicate(const QString &event_id) { return eventIds_.contains(event_id); } void handleNewUserMessage(PendingMessage msg); // Return nullptr if the event couldn't be parsed. TimelineItem *parseMessageEvent(const mtx::events::collections::TimelineEvents &event, TimelineDirection direction); QVBoxLayout *top_layout_; QVBoxLayout *scroll_layout_; QScrollArea *scroll_area_; ScrollBar *scrollbar_; QWidget *scroll_widget_; QString lastSender_; QString firstSender_; 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 bottomMessages_; //! Render the given timeline events to the bottom of the timeline. void renderBottomEvents(const std::vector &events); //! Decide if the given timeline event can be rendered. inline bool isViewable(const TimelineEvent &event) const; //! Decide if the given event should trigger a notification. inline bool isNotifiable(const TimelineEvent &event) const; // The events currently rendered. Used for duplicate detection. QMap eventIds_; QQueue pending_msgs_; QList pending_sent_msgs_; QSharedPointer client_; }; template void TimelineView::addUserMessage(const QString &url, const QString &filename, const QSharedPointer data) { auto with_sender = lastSender_ != local_user_; auto widget = new Widget(client_, url, data, filename, this); TimelineItem *view_item = new TimelineItem(widget, local_user_, with_sender, scroll_widget_); pushTimelineItem(view_item); lastMessageDirection_ = TimelineDirection::Bottom; QApplication::processEvents(); lastSender_ = local_user_; int txn_id = client_->incrementTransactionId(); PendingMessage message(MsgType, txn_id, url, filename, "", view_item); handleNewUserMessage(message); } template TimelineItem * TimelineView::createTimelineItem(const Event &event, bool withSender) { TimelineItem *item = new TimelineItem(event, withSender, scroll_widget_); return item; } template TimelineItem * TimelineView::createTimelineItem(const Event &event, bool withSender) { auto eventWidget = new Widget(client_, event); auto item = new TimelineItem(eventWidget, event, withSender, scroll_widget_); return item; } template TimelineItem * TimelineView::processMessageEvent(const Event &event, TimelineDirection direction) { const auto event_id = QString::fromStdString(event.event_id); const auto sender = QString::fromStdString(event.sender); if (isDuplicate(event_id)) return nullptr; eventIds_[event_id] = true; const QString txnid = QString::fromStdString(event.unsigned_data.transaction_id); if (!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) { removePendingMessage(txnid); return nullptr; } auto with_sender = isSenderRendered(sender, direction); updateLastSender(sender, direction); return createTimelineItem(event, with_sender); } template TimelineItem * TimelineView::processMessageEvent(const Event &event, TimelineDirection direction) { const auto event_id = QString::fromStdString(event.event_id); const auto sender = QString::fromStdString(event.sender); if (isDuplicate(event_id)) return nullptr; eventIds_[event_id] = true; const QString txnid = QString::fromStdString(event.unsigned_data.transaction_id); if (!txnid.isEmpty() && isPendingMessage(txnid, sender, local_user_)) { removePendingMessage(txnid); return nullptr; } auto with_sender = isSenderRendered(sender, direction); updateLastSender(sender, direction); return createTimelineItem(event, with_sender); }