Newer
Older
/*
* 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 <QApplication>
#include "RoomMessages.h"
#include "ScrollBar.h"
#include "Sync.h"
#include "TimelineItem.h"
#include "TimelineView.h"
namespace events = matrix::events;
static bool
isRedactedEvent(const QJsonObject &event)
{
if (event.contains("redacted_because"))
return true;
if (event.contains("unsigned") &&
event.value("unsigned").toObject().contains("redacted_because"))
return true;
return false;
}
TimelineView::TimelineView(const Timeline &timeline,
QSharedPointer<MatrixClient> client,
const QString &room_id,
QWidget *parent)
, room_id_{room_id}
, client_{client}
TimelineView::TimelineView(QSharedPointer<MatrixClient> client,
const QString &room_id,
QWidget *parent)
, room_id_{room_id}
, client_{client}
void
TimelineView::sliderRangeChanged(int min, int max)
if (!scroll_area_->verticalScrollBar()->isVisible()) {
scroll_area_->verticalScrollBar()->setValue(max);
return;
}
// If the scrollbar is close to the bottom and a new message
// is added we move the scrollbar.
if (max - scroll_area_->verticalScrollBar()->value() < SCROLL_BAR_GAP) {
scroll_area_->verticalScrollBar()->setValue(max);
int currentHeight = scroll_widget_->size().height();
int diff = currentHeight - oldHeight_;
int newPosition = oldPosition_ + diff;
// Keep the scroll bar to the bottom if it hasn't been activated yet.
if (oldPosition_ == 0 && !scroll_area_->verticalScrollBar()->isVisible())
newPosition = max;
if (lastMessageDirection_ == TimelineDirection::Top)
scroll_area_->verticalScrollBar()->setValue(newPosition);
bool hasEnoughMessages = scroll_area_->verticalScrollBar()->isVisible();
if (!hasEnoughMessages && !isTimelineFinished) {
isPaginationInProgress_ = true;
client_->messages(room_id_, prev_batch_token_);
paginationTimer_->start(500);
return;
}
int current = scroll_area_->verticalScrollBar()->value();
int max = scroll_area_->verticalScrollBar()->maximum();
// The first time we enter the room move the scroll bar to the bottom.
if (!isInitialized) {
scroll_area_->verticalScrollBar()->setValue(max);
isInitialized = true;
return;
}
// If the gap is small enough move the scroll bar down. e.g when a new
// message appears.
if (max - current < SCROLL_BAR_GAP)
scroll_area_->verticalScrollBar()->setValue(max);
void
TimelineView::sliderMoved(int position)
if (!scroll_area_->verticalScrollBar()->isVisible())
return;
const int maxScroll = scroll_area_->verticalScrollBar()->maximum();
const int currentScroll = scroll_area_->verticalScrollBar()->value();
if (maxScroll - currentScroll > SCROLL_BAR_GAP) {
scrollDownBtn_->show();
scrollDownBtn_->raise();
} else {
scrollDownBtn_->hide();
}
// The scrollbar is high enough so we can start retrieving old events.
if (position < SCROLL_BAR_GAP) {
if (isTimelineFinished)
return;
// Prevent user from moving up when there is pagination in
// progress.
// TODO: Keep a map of the event ids to filter out duplicates.
if (isPaginationInProgress_)
return;
isPaginationInProgress_ = true;
// FIXME: Maybe move this to TimelineViewManager to remove the
// extra calls?
client_->messages(room_id_, prev_batch_token_);
}
void
TimelineView::addBackwardsEvents(const QString &room_id, const RoomMessages &msgs)
if (msgs.chunk().count() == 0) {
isTimelineFinished = true;
return;
}
isTimelineFinished = false;
QList<TimelineItem *> items;
// Reset the sender of the first message in the timeline
// cause we're about to insert a new one.
firstSender_.clear();
// Parse in reverse order to determine where we should not show sender's
// name.
auto ii = msgs.chunk().size();
while (ii != 0) {
--ii;
TimelineItem *item =
parseMessageEvent(msgs.chunk().at(ii).toObject(), TimelineDirection::Top);
if (item != nullptr)
items.push_back(item);
}
// Reverse again to render them.
std::reverse(items.begin(), items.end());
oldPosition_ = scroll_area_->verticalScrollBar()->value();
oldHeight_ = scroll_widget_->size().height();
for (const auto &item : items)
addTimelineItem(item, TimelineDirection::Top);
lastMessageDirection_ = TimelineDirection::Top;
QApplication::processEvents();
prev_batch_token_ = msgs.end();
isPaginationInProgress_ = false;
// Exclude the top stretch.
if (!msgs.chunk().isEmpty() && scroll_layout_->count() > 1)
notifyForLastEvent();
// If this batch is the first being rendered (i.e the first and the last
// events originate from this batch), set the last sender.
if (lastSender_.isEmpty() && !items.isEmpty())
lastSender_ = items.constFirst()->descriptionMessage().userid;
TimelineItem *
TimelineView::parseMessageEvent(const QJsonObject &event, TimelineDirection direction)
events::EventType ty = events::extractEventType(event);
if (ty == events::EventType::RoomMessage) {
events::MessageEventType msg_type = events::extractMessageEventType(event);
if (msg_type == events::MessageEventType::Text) {
events::MessageEvent<msgs::Text> text;
try {
text.deserialize(event);
} catch (const DeserializationException &e) {
qWarning() << e.what() << event;
return nullptr;
}
if (isDuplicate(text.eventId()))
return nullptr;
QString txnid = text.unsignedData().transactionId();
if (!txnid.isEmpty() &&
isPendingMessage(txnid, text.sender(), local_user_)) {
auto with_sender = isSenderRendered(text.sender(), direction);
return createTimelineItem(text, with_sender);
} else if (msg_type == events::MessageEventType::Notice) {
events::MessageEvent<msgs::Notice> notice;
try {
notice.deserialize(event);
} catch (const DeserializationException &e) {
qWarning() << e.what() << event;
return nullptr;
}
if (isDuplicate(notice.eventId()))
return nullptr;
auto with_sender = isSenderRendered(notice.sender(), direction);
return createTimelineItem(notice, with_sender);
} else if (msg_type == events::MessageEventType::Image) {
events::MessageEvent<msgs::Image> img;
try {
img.deserialize(event);
} catch (const DeserializationException &e) {
qWarning() << e.what() << event;
return nullptr;
}
if (isDuplicate(img.eventId()))
return nullptr;
QString txnid = img.unsignedData().transactionId();
if (!txnid.isEmpty() &&
isPendingMessage(txnid, img.sender(), local_user_)) {
auto with_sender = isSenderRendered(img.sender(), direction);
} else if (msg_type == events::MessageEventType::Emote) {
events::MessageEvent<msgs::Emote> emote;
try {
emote.deserialize(event);
} catch (const DeserializationException &e) {
qWarning() << e.what() << event;
return nullptr;
}
if (isDuplicate(emote.eventId()))
return nullptr;
eventIds_[emote.eventId()] = true;
QString txnid = emote.unsignedData().transactionId();
if (!txnid.isEmpty() &&
isPendingMessage(txnid, emote.sender(), local_user_)) {
auto with_sender = isSenderRendered(emote.sender(), direction);
updateLastSender(emote.sender(), direction);
return createTimelineItem(emote, with_sender);
} else if (msg_type == events::MessageEventType::Unknown) {
// TODO Handle redacted messages.
// Silenced for now.
if (!isRedactedEvent(event))
qWarning() << "Unknown message type" << event;
int
TimelineView::addEvents(const Timeline &timeline)
QSettings settings;
QString localUser = settings.value("auth/user_id").toString();
for (const auto &event : timeline.events()) {
TimelineItem *item = parseMessageEvent(event.toObject(), TimelineDirection::Bottom);
if (item != nullptr) {
addTimelineItem(item, TimelineDirection::Bottom);
if (localUser != event.toObject().value("sender").toString())
message_count += 1;
}
}
lastMessageDirection_ = TimelineDirection::Bottom;
QApplication::processEvents();
if (isInitialSync) {
prev_batch_token_ = timeline.previousBatch();
isInitialSync = false;
}
// Exclude the top stretch.
if (!timeline.events().isEmpty() && scroll_layout_->count() > 1)
notifyForLastEvent();
if (isActiveWindow() && isVisible() && timeline.events().size() > 0)
readLastEvent();
QSettings settings;
local_user_ = settings.value("auth/user_id").toString();
QIcon icon;
icon.addFile(":/icons/icons/ui/angle-arrow-down.png");
scrollDownBtn_ = new FloatingButton(icon, this);
scrollDownBtn_->setBackgroundColor(QColor("#F5F5F5"));
scrollDownBtn_->setForegroundColor(QColor("black"));
scrollDownBtn_->hide();
connect(scrollDownBtn_, &QPushButton::clicked, this, [=]() {
const int max = scroll_area_->verticalScrollBar()->maximum();
scroll_area_->verticalScrollBar()->setValue(max);
});
top_layout_ = new QVBoxLayout(this);
top_layout_->setSpacing(0);
top_layout_->setMargin(0);
scroll_area_ = new QScrollArea(this);
scroll_area_->setWidgetResizable(true);
scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollbar_ = new ScrollBar(scroll_area_);
scroll_area_->setVerticalScrollBar(scrollbar_);
scroll_widget_ = new QWidget(this);
scroll_layout_ = new QVBoxLayout(scroll_widget_);
scroll_layout_->setContentsMargins(15, 0, 15, 15);
scroll_layout_->addStretch(1);
scroll_layout_->setSpacing(0);
scroll_layout_->setObjectName("timelinescrollarea");
paginationTimer_ = new QTimer(this);
connect(paginationTimer_, &QTimer::timeout, this, &TimelineView::fetchHistory);
connect(client_.data(),
&MatrixClient::messagesRetrieved,
this,
&TimelineView::addBackwardsEvents);
connect(scroll_area_->verticalScrollBar(),
SIGNAL(valueChanged(int)),
this,
SLOT(sliderMoved(int)));
connect(scroll_area_->verticalScrollBar(),
SIGNAL(rangeChanged(int, int)),
this,
SLOT(sliderRangeChanged(int, int)));
void
TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction)
if (direction == TimelineDirection::Bottom)
lastSender_ = user_id;
else
firstSender_ = user_id;
bool
TimelineView::isSenderRendered(const QString &user_id, TimelineDirection direction)
if (direction == TimelineDirection::Bottom)
return lastSender_ != user_id;
else
return firstSender_ != user_id;
TimelineView::createTimelineItem(const events::MessageEvent<msgs::Image> &event, bool with_sender)
auto image = new ImageItem(client_, event);
auto item = new TimelineItem(image, event, with_sender, scroll_widget_);
TimelineView::createTimelineItem(const events::MessageEvent<msgs::Notice> &event, bool with_sender)
TimelineItem *item = new TimelineItem(event, with_sender, scroll_widget_);
return item;
TimelineView::createTimelineItem(const events::MessageEvent<msgs::Text> &event, bool with_sender)
TimelineItem *item = new TimelineItem(event, with_sender, scroll_widget_);
return item;
TimelineItem *
TimelineView::createTimelineItem(const events::MessageEvent<msgs::Emote> &event, bool with_sender)
{
TimelineItem *item = new TimelineItem(event, with_sender, scroll_widget_);
return item;
}
void
TimelineView::addTimelineItem(TimelineItem *item, TimelineDirection direction)
if (direction == TimelineDirection::Bottom)
scroll_layout_->addWidget(item);
else
scroll_layout_->insertWidget(1, item);
void
TimelineView::updatePendingMessage(int txn_id, QString event_id)
if (pending_msgs_.head().txn_id == txn_id) { // We haven't received it yet
msg.event_id = event_id;
pending_sent_msgs_.append(msg);
TimelineView::addUserMessage(matrix::events::MessageEventType ty, const QString &body)
auto user_id = settings.value("auth/user_id").toString();
TimelineItem *view_item = new TimelineItem(ty, user_id, body, with_sender, scroll_widget_);
lastMessageDirection_ = TimelineDirection::Bottom;
QApplication::processEvents();
int txn_id = client_->incrementTransactionId();
PendingMessage message(ty, txn_id, body, "", "", view_item);
handleNewUserMessage(message);
TimelineView::addUserMessage(const QString &url, const QString &filename)
{
QSettings settings;
auto user_id = settings.value("auth/user_id").toString();
auto with_sender = lastSender_ != user_id;
auto image = new ImageItem(client_, url, filename, this);
TimelineItem *view_item = new TimelineItem(image, user_id, with_sender, scroll_widget_);
scroll_layout_->addWidget(view_item);
lastMessageDirection_ = TimelineDirection::Bottom;
QApplication::processEvents();
int txn_id = client_->incrementTransactionId();
PendingMessage message(
matrix::events::MessageEventType::Image, txn_id, url, filename, "", view_item);
handleNewUserMessage(message);
}
void
TimelineView::handleNewUserMessage(PendingMessage msg)
{
pending_msgs_.enqueue(msg);
if (pending_msgs_.size() == 1 && pending_sent_msgs_.size() == 0)
sendNextPendingMessage();
}
void
TimelineView::sendNextPendingMessage()
{
if (pending_msgs_.size() == 0)
return;
PendingMessage &m = pending_msgs_.head();
switch (m.ty) {
case matrix::events::MessageEventType::Image:
client_->sendRoomMessage(
m.ty, m.txn_id, room_id_, QFileInfo(m.filename).fileName(), m.body);
break;
default:
client_->sendRoomMessage(m.ty, m.txn_id, room_id_, m.body);
break;
}
auto lastItem = scroll_layout_->itemAt(scroll_layout_->count() - 1);
auto *lastTimelineItem = qobject_cast<TimelineItem *>(lastItem->widget());
if (lastTimelineItem)
emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage());
else
qWarning() << "Cast to TimelineView failed" << room_id_;
TimelineView::isPendingMessage(const QString &txnid,
const QString &sender,
const QString &local_userid)
{
if (sender != local_userid)
return false;
for (const auto &msg : pending_msgs_) {
if (QString::number(msg.txn_id) == txnid)
return true;
}
for (const auto &msg : pending_sent_msgs_) {
if (QString::number(msg.txn_id) == txnid)
return true;
}
return false;
}
void
TimelineView::removePendingMessage(const QString &txnid)
for (auto it = pending_sent_msgs_.begin(); it != pending_sent_msgs_.end(); ++it) {
if (QString::number(it->txn_id) == txnid) {
int index = std::distance(pending_sent_msgs_.begin(), it);
pending_sent_msgs_.removeAt(index);
return;
}
}
for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) {
if (QString::number(it->txn_id) == txnid) {
int index = std::distance(pending_msgs_.begin(), it);
void
TimelineView::handleFailedMessage(int txnid)
{
Q_UNUSED(txnid);
// Note: We do this even if the message has already been echoed.
QTimer::singleShot(500, this, SLOT(sendNextPendingMessage()));
}
void
TimelineView::paintEvent(QPaintEvent *)
{
QStyleOption opt;
opt.init(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
void
TimelineView::readLastEvent() const
{
const auto eventId = getLastEventId();
if (!eventId.isEmpty())
client_->readEvent(room_id_, eventId);
}
QString
TimelineView::getLastEventId() const
{
auto index = scroll_layout_->count();
// Search backwards for the first event that has a valid event id.
while (index > 0) {
--index;
auto lastItem = scroll_layout_->itemAt(index);
auto *lastTimelineItem = qobject_cast<TimelineItem *>(lastItem->widget());
if (lastTimelineItem && !lastTimelineItem->eventId().isEmpty())
return lastTimelineItem->eventId();
}
return QString("");
}
void
TimelineView::showEvent(QShowEvent *event)
{
readLastEvent();
QWidget::showEvent(event);
}
bool
TimelineView::event(QEvent *event)
{
if (event->type() == QEvent::WindowActivate) {
QTimer::singleShot(1000, this, [=]() {
emit clearUnreadMessageCount(room_id_);
readLastEvent();
});
}
return QWidget::event(event);
}