diff --git a/CMakeLists.txt b/CMakeLists.txt index 33e3dccd4..cbdc6a889 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,7 @@ set(quaternion_SRCS client/quaternionroom.cpp client/message.cpp client/imageprovider.cpp + client/activitydetector.cpp client/logindialog.cpp client/mainwindow.cpp client/roomlistdock.cpp diff --git a/client/activitydetector.cpp b/client/activitydetector.cpp new file mode 100644 index 000000000..7efada9ca --- /dev/null +++ b/client/activitydetector.cpp @@ -0,0 +1,67 @@ +/************************************************************************** + * * + * Copyright (C) 2016 Malte Brandy * + * * + * 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 . * + * * + **************************************************************************/ + +#include "activitydetector.h" +#include "mainwindow.h" +#include "chatroomwidget.h" +#include "models/messageeventmodel.h" +#include + +ActivityDetector::ActivityDetector(QApplication* a, MainWindow* w): m_app(a), m_mainWindow(w), m_enabled(false), m_messageEventModel(w->getChatRoomWidget()->getMessageEventModel()) +{ + connect(this, &ActivityDetector::triggered, m_messageEventModel, &MessageEventModel::markShownAsRead); + connect(m_messageEventModel, &MessageEventModel::lastShownIndexChanged, this, &ActivityDetector::updateEnabled); + connect(m_messageEventModel, &MessageEventModel::readMarkerIndexChanged, this, &ActivityDetector::updateEnabled); +} + +void ActivityDetector::updateEnabled() +{ + setEnabled(m_messageEventModel->awaitingMarkRead()); +} + +void ActivityDetector::setEnabled(bool enabled) +{ + if (enabled && !m_enabled) { + m_app->installEventFilter(this); + m_mainWindow->setMouseTracking(true); + m_enabled = true; + qDebug() << "enabling ActivityDetector"; + } + if (!enabled && m_enabled) { + m_mainWindow->setMouseTracking(false); + m_app->removeEventFilter(this); + m_enabled = false; + qDebug() << "disabling ActivityDetector"; + } +} + +bool ActivityDetector::eventFilter(QObject* obj, QEvent* ev) +{ + switch (ev->type()) + { + case QEvent::KeyPress: + case QEvent::FocusIn: + case QEvent::MouseMove: + case QEvent::MouseButtonPress: + emit triggered(); + setEnabled(false); + default:; + } + return QObject::eventFilter(obj, ev); +} diff --git a/client/activitydetector.h b/client/activitydetector.h new file mode 100644 index 000000000..cea671d7c --- /dev/null +++ b/client/activitydetector.h @@ -0,0 +1,49 @@ +/************************************************************************** + * * + * Copyright (C) 2016 Malte Brandy * + * * + * 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 + +class MainWindow; +class MessageEventModel; + +class ActivityDetector : public QObject +{ + Q_OBJECT + + public: + ActivityDetector(QApplication* a, MainWindow* c); + + public slots: + void updateEnabled(); + void setEnabled(bool enabled); + + signals: + void triggered(); + + protected: + bool eventFilter(QObject* obj, QEvent* ev); + + private: + QApplication* m_app; + MainWindow* m_mainWindow; + bool m_enabled; + MessageEventModel* m_messageEventModel; +}; diff --git a/client/chatroomwidget.cpp b/client/chatroomwidget.cpp index 95cdc62da..60c3de4f2 100644 --- a/client/chatroomwidget.cpp +++ b/client/chatroomwidget.cpp @@ -94,7 +94,6 @@ ChatRoomWidget::ChatRoomWidget(QWidget* parent) QObject* rootItem = m_quickView->rootObject(); connect( rootItem, SIGNAL(getPreviousContent()), this, SLOT(getPreviousContent()) ); - m_chatEdit = new ChatEdit(this); connect( m_chatEdit, &QLineEdit::returnPressed, this, &ChatRoomWidget::sendLine ); @@ -116,9 +115,9 @@ ChatRoomWidget::~ChatRoomWidget() { } -void ChatRoomWidget::lookAtRoom() +MessageEventModel* ChatRoomWidget::getMessageEventModel() { - m_messageModel->markShownAsRead(); + return m_messageModel; } void ChatRoomWidget::enableDebug() diff --git a/client/chatroomwidget.h b/client/chatroomwidget.h index f4d2680ff..480618e46 100644 --- a/client/chatroomwidget.h +++ b/client/chatroomwidget.h @@ -47,7 +47,7 @@ class ChatRoomWidget: public QWidget void enableDebug(); void triggerCompletion(); void cancelCompletion(); - void lookAtRoom(); + MessageEventModel* getMessageEventModel(); signals: void joinRoomNeedsInteraction(); diff --git a/client/main.cpp b/client/main.cpp index eb45f97f2..d5cad0528 100644 --- a/client/main.cpp +++ b/client/main.cpp @@ -23,32 +23,7 @@ #include #include "mainwindow.h" - -class ActivityDetector : public QObject -{ - public: - ActivityDetector(MainWindow* c): m_mainWindow(c) - { - c->setMouseTracking(true); - }; - protected: - bool eventFilter(QObject* obj, QEvent* ev) - { - switch (ev->type()) - { - case QEvent::KeyPress: - case QEvent::FocusIn: - case QEvent::MouseMove: - case QEvent::MouseButtonPress: - m_mainWindow->activity(); - default:; - } - return QObject::eventFilter(obj, ev); - } - private: - MainWindow* m_mainWindow; -}; - +#include "activitydetector.h" int main( int argc, char* argv[] ) { @@ -80,8 +55,7 @@ int main( int argc, char* argv[] ) MainWindow window; if( debugEnabled ) window.enableDebug(); - ActivityDetector ad(&window); - app.installEventFilter(&ad); + ActivityDetector ad(&app, &window); window.show(); return app.exec(); diff --git a/client/mainwindow.cpp b/client/mainwindow.cpp index 8896f1115..bd624d243 100644 --- a/client/mainwindow.cpp +++ b/client/mainwindow.cpp @@ -67,9 +67,9 @@ MainWindow::~MainWindow() { } -void MainWindow::activity() +ChatRoomWidget* MainWindow::getChatRoomWidget() { - chatRoomWidget->lookAtRoom(); + return chatRoomWidget; } void MainWindow::createMenu() diff --git a/client/mainwindow.h b/client/mainwindow.h index f67341bb4..96962d5e8 100644 --- a/client/mainwindow.h +++ b/client/mainwindow.h @@ -43,9 +43,9 @@ class MainWindow: public QMainWindow virtual ~MainWindow(); void enableDebug(); - void activity(); void setConnection(QuaternionConnection* newConnection); + ChatRoomWidget* getChatRoomWidget(); protected: virtual void closeEvent(QCloseEvent* event) override; diff --git a/client/models/messageeventmodel.cpp b/client/models/messageeventmodel.cpp index 6f3f5cd0a..1d5f7253e 100644 --- a/client/models/messageeventmodel.cpp +++ b/client/models/messageeventmodel.cpp @@ -66,6 +66,7 @@ MessageEventModel::MessageEventModel(QObject* parent) : QAbstractListModel(parent) , m_currentRoom(nullptr) , lastShownIndex(-1) + , m_readMarkerIndex(-1) { } MessageEventModel::~MessageEventModel() @@ -83,25 +84,34 @@ void MessageEventModel::changeRoom(QuaternionRoom* room) m_currentRoom = room; lastShownIndex = -1; + m_readMarkerIndex = -1; if( room ) { using namespace QMatrixClient; - connect(m_currentRoom, &Room::aboutToAddNewMessages, + connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [=](const Events& events) { beginInsertRows(QModelIndex(), rowCount(), rowCount() + events.size() - 1); }); - connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, + connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [=](const Events& events) { beginInsertRows(QModelIndex(), 0, events.size() - 1); }); - connect(m_currentRoom, &Room::addedMessages, - this, &MessageEventModel::endInsertRows); + connect(m_currentRoom, &Room::addedMessages, this, + [=]() + { + endInsertRows(); + updateReadMarkerIndex(); + }); + connect(m_currentRoom, &Room::readMarkerPromoted, + this, &MessageEventModel::updateReadMarkerIndex); qDebug() << "connected" << room; } endResetModel(); + emit lastShownIndexChanged(lastShownIndex); + updateReadMarkerIndex(); } int MessageEventModel::rowCount(const QModelIndex& parent) const @@ -329,9 +339,33 @@ QVariant MessageEventModel::data(const QModelIndex& index, int role) const void MessageEventModel::markShownAsRead() { - if (m_currentRoom && lastShownIndex > -1) + if (m_currentRoom && lastShownIndex > -1 && lastShownIndex < m_currentRoom->messages().count()) { auto lastShownMessage = m_currentRoom->messages().at(lastShownIndex); m_currentRoom->markMessagesAsRead(lastShownMessage->messageEvent()->id()); } } + +void MessageEventModel::updateReadMarkerIndex() +{ + if( !m_currentRoom ) + return; + for (int newReadMarkerIndex = m_readMarkerIndex; newReadMarkerIndex < m_currentRoom->messages().count() ; ++newReadMarkerIndex) + { + if (newReadMarkerIndex >= 0 && m_currentRoom->readMarkerEventId() == m_currentRoom->messages().at(newReadMarkerIndex)->messageEvent()->id()) + { + if (newReadMarkerIndex != m_readMarkerIndex) + { + m_readMarkerIndex = newReadMarkerIndex; + emit readMarkerIndexChanged(newReadMarkerIndex); + } + return; + } + } +} + +bool MessageEventModel::awaitingMarkRead() +{ + qDebug() << "readMarker: " << m_readMarkerIndex << "lastShown: " << lastShownIndex; + return m_readMarkerIndex < lastShownIndex; +} diff --git a/client/models/messageeventmodel.h b/client/models/messageeventmodel.h index e3420a45c..b3c910f3b 100644 --- a/client/models/messageeventmodel.h +++ b/client/models/messageeventmodel.h @@ -32,6 +32,7 @@ class MessageEventModel: public QAbstractListModel Q_OBJECT Q_PROPERTY(QuaternionRoom* room MEMBER m_currentRoom CONSTANT) Q_PROPERTY(int lastShownIndex MEMBER lastShownIndex NOTIFY lastShownIndexChanged) + Q_PROPERTY(int readMarkerIndex MEMBER m_readMarkerIndex NOTIFY readMarkerIndexChanged) public: MessageEventModel(QObject* parent = nullptr); virtual ~MessageEventModel(); @@ -42,15 +43,20 @@ class MessageEventModel: public QAbstractListModel QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QHash roleNames() const override; + bool awaitingMarkRead(); + signals: void lastShownIndexChanged(int newValue); + void readMarkerIndexChanged(int newValue); public slots: void markShownAsRead(); + void updateReadMarkerIndex(); private: QuaternionRoom* m_currentRoom; int lastShownIndex; + int m_readMarkerIndex; }; #endif // LOGMESSAGEMODEL_H diff --git a/client/qml/chat.qml b/client/qml/chat.qml index afe2e7cb4..9bb8bc6c6 100644 --- a/client/qml/chat.qml +++ b/client/qml/chat.qml @@ -139,8 +139,9 @@ Rectangle { y + message.height - 1 < chatView.contentY + chatView.height property bool newlyShown: shown && - messageModel.lastShownIndex !== -1 && - index > messageModel.lastShownIndex + messageModel.lastShownIndex != -1 && + index > messageModel.lastShownIndex && + index > messageModel.readMarkerIndex function promoteLastShownIndex() { if (index > messageModel.lastShownIndex) { @@ -292,23 +293,20 @@ Rectangle { } Rectangle { id: scrollindicator - opacity: chatView.nowAtYEnd ? 0 : 0.5 color: defaultPalette.text height: 30 radius: height/2 width: height anchors.left: parent.left anchors.bottom: parent.bottom - anchors.leftMargin: width/2 - anchors.bottomMargin: chatView.nowAtYEnd ? -height : height/2 - Behavior on opacity { - NumberAnimation { duration: 300 } - } - Behavior on anchors.bottomMargin { - NumberAnimation { duration: 300 } - } + anchors.leftMargin: height/2 + anchors.bottomMargin: -height Image { - anchors.fill: parent + id: scrolldownarrow + anchors.left: parent.left + anchors.top: parent.top + height: parent.height + width: height source: "qrc:///scrolldown.svg" } MouseArea { @@ -316,5 +314,179 @@ Rectangle { onClicked: root.scrollToBottom() cursorShape: Qt.PointingHandCursor } + Label { + id: scrolldowntext + text: "unread messages below" + anchors.verticalCenter: parent.verticalCenter + anchors.left: scrolldownarrow.right + anchors.leftMargin: scrolldownarrow.width/2 + } + state: chatView.nowAtYEnd ? "hide" : (messageModel.readMarkerIndex == chatView.count - 1 ? "show": "extend") + states: [ + State { + name: "show" + PropertyChanges { target:scrollindicator; opacity: 0.5 } + PropertyChanges { target:scrollindicator; color: defaultPalette.text } + PropertyChanges { target:scrolldowntext; opacity: 0 } + PropertyChanges { target:scrollindicator; width: scrollindicator.height } + PropertyChanges { target:scrollindicator; anchors.bottomMargin: scrollindicator.height/2 } + }, + State { + name: "extend" + PropertyChanges { target:scrollindicator; opacity: 1 } + PropertyChanges { target:scrollindicator; color: "#ffaaaa" } + PropertyChanges { target:scrolldowntext; opacity: 1 } + PropertyChanges { target:scrollindicator; width: scrolldowntext.width + scrollindicator.height*2 } + PropertyChanges { target:scrollindicator; anchors.bottomMargin: scrollindicator.height/2 } + }, + State { + name: "hide" + PropertyChanges { target:scrollindicator; opacity: 0 } + PropertyChanges { target:scrollindicator; color: defaultPalette.text } + PropertyChanges { target:scrolldowntext; opacity: 0 } + PropertyChanges { target:scrollindicator; width: scrollindicator.height } + PropertyChanges { target:scrollindicator; anchors.bottomMargin: -scrollindicator.height } + }] + transitions: [ + Transition { + from: "hide" + to: "show" + ParallelAnimation{ + NumberAnimation{target: scrollindicator; property: "opacity"; duration: 100} + NumberAnimation{target: scrollindicator; property: "anchors.bottomMargin"; duration: 100} + } + }, + Transition { + from: "show" + to: "extend" + SequentialAnimation{ + ParallelAnimation{ + ColorAnimation{target: scrollindicator; duration: 100} + NumberAnimation{target: scrollindicator; property: "opacity"; duration: 100} + } + NumberAnimation{target: scrollindicator; property: "width"; duration: 300} + NumberAnimation{target: scrolldowntext; property: "opacity"; duration: 100} + } + }, + Transition { + from: "extend" + to: "show" + SequentialAnimation{ + NumberAnimation{target: scrolldowntext; property: "opacity"; duration: 100} + NumberAnimation{target: scrollindicator; property: "width"; duration: 300} + ParallelAnimation{ + ColorAnimation{target: scrollindicator; duration: 100} + NumberAnimation{target: scrollindicator; property: "opacity"; duration: 100} + } + } + }, + Transition { + from: "show" + to: "hide" + ParallelAnimation{ + NumberAnimation{target: scrollindicator; property: "opacity"; duration: 100} + NumberAnimation{target: scrollindicator; property: "anchors.bottomMargin"; duration: 100} + } + }, + Transition { + from: "extend" + to: "hide" + SequentialAnimation{ + NumberAnimation{target: scrolldowntext; property: "opacity"; duration: 100} + NumberAnimation{target: scrollindicator; property: "width"; duration: 300} + ColorAnimation{target: scrollindicator; duration: 100} + ParallelAnimation{ + NumberAnimation{target: scrollindicator; property: "opacity"; duration: 100} + NumberAnimation{target: scrollindicator; property: "anchors.bottomMargin"; duration: 100} + } + } + }, + Transition { + from: "hide" + to: "extend" + SequentialAnimation{ + ParallelAnimation{ + NumberAnimation{target: scrollindicator; property: "opacity"; duration: 100} + NumberAnimation{target: scrollindicator; property: "anchors.bottomMargin"; duration: 100} + } + ColorAnimation{target: scrollindicator; duration: 100} + NumberAnimation{target: scrollindicator; property: "width"; duration: 300} + NumberAnimation{target: scrolldowntext; property: "opacity"; duration: 100} + } + } + ] + } + + Rectangle { + id: backlogindicator + property bool show: chatView.count > 0 && (messageModel.readMarkerIndex == -1 || chatView.indexAt(10,chatView.contentY) > messageModel.readMarkerIndex) + color: "#ffaaaa" + height: 30 + width: height + radius: height/2 + anchors.left: parent.left + anchors.top: parent.top + anchors.leftMargin: height/2 + anchors.topMargin: height/2 + + Image { + id: scrolluparrow + anchors.left: parent.left + anchors.top: parent.top + height: parent.height + width: height + source: "qrc:///scrolldown.svg" + transform: Rotation { origin.x: scrolluparrow.height/2; origin.y: scrolluparrow.height/2; angle: 180} + } + Label { + id: scrolluptext + text: "unread messages above" + anchors.verticalCenter: parent.verticalCenter + anchors.left: scrolluparrow.right + anchors.leftMargin: scrolluparrow.width/2 + } + + state: show ? "show" : "hide" + states: [ + State { + name: "show" + PropertyChanges { target:backlogindicator; opacity: 1 } + PropertyChanges { target:scrolluptext; opacity: 1 } + PropertyChanges { target:backlogindicator; width: scrolluptext.width + backlogindicator.height*2 } + PropertyChanges { target:backlogindicator; anchors.topMargin: backlogindicator.height/2 } + }, + State { + name: "hide" + PropertyChanges { target:backlogindicator; opacity: 0 } + PropertyChanges { target:scrolluptext; opacity: 0 } + PropertyChanges { target:backlogindicator; width: backlogindicator.height } + PropertyChanges { target:backlogindicator; anchors.topMargin: -backlogindicator.height } + }] + transitions: [ + Transition { + from: "hide" + to: "show" + SequentialAnimation{ + ParallelAnimation{ + NumberAnimation{target: backlogindicator; property: "opacity"; duration: 100} + NumberAnimation{target: backlogindicator; property: "anchors.topMargin"; duration: 100} + } + NumberAnimation{target: backlogindicator; property: "width"; duration: 300} + NumberAnimation{target: scrolluptext; property: "opacity"; duration: 100} + } + }, + Transition { + from: "show" + to: "hide" + SequentialAnimation{ + NumberAnimation{target: scrolluptext; property: "opacity"; duration: 100} + NumberAnimation{target: backlogindicator; property: "width"; duration: 300} + ParallelAnimation{ + NumberAnimation{target: backlogindicator; property: "opacity"; duration: 100} + NumberAnimation{target: backlogindicator; property: "anchors.topMargin"; duration: 100} + } + } + } + ] } }