diff --git a/Desktop.qml b/Desktop.qml index d90a14e3..f768362e 100644 --- a/Desktop.qml +++ b/Desktop.qml @@ -1,222 +1,221 @@ // Skeleton from https://github.com/achipa/outqross_blog.git // Almost everything has been re-adapted import QtQuick 2.7 import QtQuick.Controls 1.3 import QtQuick.Controls.Styles 1.2 import QtQuick.Window 2.2 import QtQuick.Dialogs 1.2 import QtQuick.Layouts 1.1 import Qt.labs.settings 1.0 import KDE.Ruqola.UserData 1.0 import KDE.Ruqola.DDPClient 1.0 // import "Log.js" as Log // import "Data.js" as Data ApplicationWindow { property int margin: 11 property string statusText property list todos property JSONListModel lists: JSONListModel { } property JSONListModel activeRoom: JSONListModel {} property JSONListModel userRooms: JSONListModel {} property string selectedRoomID; property bool ready; id: appid title: qsTr("Ruqola") width: 640 height: 480 visible: true menuBar: MenuBar { Menu { title: qsTr("&Main") MenuItem { text: qsTr("&Log out") onTriggered: { UserData.logOut(); // loginTab.visible = true; // mainWidget.visible = false; // messageDialog.show(qsTr("Reconnect action triggered")); } } MenuItem { text: qsTr("E&xit") onTriggered: Qt.quit(); shortcut: StandardKey.Quit; } } } // Component.onCompleted : {UserData.tryLogin()}//.log(UserData.loggedIn);} Login { id: loginTab visible: (UserData.loginStatus == DDPClient.LoginFailed || UserData.loginStatus == DDPClient.LoggedOut) anchors.fill:parent z: 10 serverURL: UserData.serverURL username: UserData.userName onAccepted: { UserData.password = loginTab.password; UserData.userName = loginTab.username; UserData.serverURL = loginTab.serverURL; // console.log("") UserData.tryLogin(); } } // statusBar: StatusView { // RowLayout { // Label { text: statusText } // } // } BusyIndicator { id: busy anchors.centerIn: parent visible: UserData.loginStatus == DDPClient.LoggingIn } Item { id: mainWidget anchors.fill: parent visible: !loginTab.visible // visible:true // Component.onCompleted :{ // console.log("debug"); // console.log(UserData.loginStatus); // console.log( DDPClient.LoginFailed); // console.log(UserData.loginStatus != DDPClient.LoginFailed); // } ListView { id: roomsList model: UserData.roomModel() width: 100 - visible: true anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom anchors.margins: 10; delegate: Text { property variant internal_id: id text: name font.bold: (selectedRoomID == id) id: room_chooser MouseArea { anchors.fill: parent onClicked: { console.log("Choosing room", room_chooser.internal_id); selectedRoomID = room_chooser.internal_id; // myModel.currentRoom = selectedRoomID; activeChat.model = UserData.getModelForRoom(selectedRoomID); console.log(activeChat.count); } } } } ScrollView { anchors.right: parent.right anchors.left: roomsList.right anchors.top: parent.top anchors.bottom: messageLine.top verticalScrollBarPolicy: Qt.ScrollBarAlwaysOn ListView { id: activeChat // property string activeRoom: selectedRoomID // visibleArea.yPosition: 1.0-heightRatio onCountChanged: { console.log("changed") // var newIndex = count - 1 // last index // positionViewAtEnd() positionViewAtIndex(count - 1, ListView.Beginning) // currentIndex = newIndex } // Component.onCompleted: positionViewAtEnd() Component.onCompleted: positionViewAtIndex(count - 1, ListView.Beginning) // onSelectedRoomIDChanged: { console.log("CHANGED"); activeChat.positionViewAtEnd(); } // model: myModel anchors.fill:parent visible : count > 0 z: -1 // ScrollBar.vertical: ScrollBar { } delegate: Message { i_messageText: messageText i_username: username i_systemMessage: systemMessage i_systemMessageType: type width: parent.width } } } TextField { id: messageLine anchors.right: parent.right anchors.left: roomsList.right anchors.bottom: parent.bottom placeholderText: qsTr("Enter message") onAccepted: { if (text != "") { UserData.sendMessage(selectedRoomID, text); text = ""; } } } } onClosing: { console.log("Minimizing to systray..."); hide(); } function toggleShow(reason) { // console.log ("Showing"); if (visible) { hide(); } else { show(); raise(); requestActivate(); } } Component.onCompleted: { systrayIcon.activated.connect(toggleShow); // systrayIcon.showMessage("Connected", "We are CONNECTED!"); } Timer { id: timer interval: 3000 onTriggered: statusText = ""; repeat: true } onStatusTextChanged: timer.restart(); } diff --git a/src/rocketchatbackend.cpp b/src/rocketchatbackend.cpp index 5efbe76e..d77b6deb 100644 --- a/src/rocketchatbackend.cpp +++ b/src/rocketchatbackend.cpp @@ -1,172 +1,175 @@ /* * * Copyright 2016 Riccardo Iaconelli * * 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 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 "rocketchatbackend.h" #include #include #include #include "userdata.h" #include "ddpclient.h" void debug_callback(QJsonDocument doc) { qDebug() << "DEBUG:" << doc; } void process_backlog(QJsonDocument messages) { qDebug() << messages.object().value("messages").toArray().size(); RocketChatBackend::processIncomingMessages(messages.object().value("messages").toArray()); } void rooms_callback(QJsonDocument doc) { // qDebug() << doc; RoomModel *model = UserData::self()->roomModel(); // qDebug() << model; // model->reset(); QJsonArray removed = doc.object().value("remove").toArray(); QJsonArray updated = doc.object().value("update").toArray(); for (int i = 0; i < updated.size(); i++) { QJsonObject room = updated.at(i).toObject(); if (room.value("t").toString() == "c") { QString roomID = room.value("rid").toString(); qDebug() << "Adding" << roomID<< room.value("name").toString(); MessageModel *roomModel = UserData::self()->getModelForRoom(roomID); - model->addRoom(roomID, room.value("name").toString()); + // let's be extra safe around crashes + if (UserData::self()->loginStatus() == DDPClient::LoggedIn) { + model->addRoom(roomID, room.value("name").toString()); + } QString params = QString("[\"%1\"]").arg(roomID); UserData::self()->ddp()->subscribe("stream-room-messages", QJsonDocument::fromJson(params.toLatin1())); // Load history QByteArray json = "[\""+roomID.toLatin1() + "\", null, 50, {\"$date\": "+ QString::number(roomModel->lastTimestamp()).toLatin1()+ "}]"; qDebug() << json; UserData::self()->ddp()->method("loadHistory", QJsonDocument::fromJson(json), process_backlog); } } // qDebug() << "DEBUG:" << doc; } void RocketChatBackend::processIncomingMessages(QJsonArray messages) { foreach (const QJsonValue v, messages) { QJsonObject o = v.toObject(); Message m; QString roomId = o.value("rid").toString(); QString type = o.value("t").toString(); m.username = o.value("u").toObject().value("username").toString(); m.userID = o.value("u").toObject().value("_id").toString(); m.message = o.value("msg").toString(); m.roomID = roomId; m.timestamp = (qint64)o.value("ts").toObject().value("$date").toDouble(); if (!type.isEmpty()) { m.systemMessage = true; m.systemMessageType = type; } else { m.systemMessage = false; } UserData::self()->getModelForRoom(roomId)->addMessage(m); } } RocketChatBackend::RocketChatBackend(QObject* parent) : QObject(parent) { // UserData::self()->ddp() = new DDPClient(, this); connect(UserData::self(), &UserData::loginStatusChanged, this, &RocketChatBackend::onLoginStatusChanged); } RocketChatBackend::~RocketChatBackend() { // delete m_rooms; // delete UserData::self()->ddp(); } void RocketChatBackend::onLoginStatusChanged() { if (UserData::self()->loginStatus() == DDPClient::LoggedIn) { connect(UserData::self()->ddp(), &DDPClient::changed, this, &RocketChatBackend::onChanged); connect(UserData::self()->ddp(), &DDPClient::added, this, &RocketChatBackend::onAdded); qDebug() << "GETTING LIST OF ROOMS"; UserData::self()->ddp()->method("subscriptions/get", QJsonDocument::fromJson("{\"$date\": 0}"), rooms_callback); } } void RocketChatBackend::onLoggedIn() { // if (UserData::self()->loginStatus() != DDPClient::LoggedIn) { // qDebug() << "not yet logged in:" << UserData::self()->loginStatus(); // return; // } // // get list of rooms // UserData::self()->ddp()->method("rooms/get", QJsonDocument::fromJson("{\"$date\": 0}"), rooms_callback); } void RocketChatBackend::onAdded(QJsonObject object) { QString collection = object.value("collection").toString(); qDebug() << "ROCKET BACK" << object << collection; if (collection == "stream-room-messages") { } else if (collection == "users") { qDebug() << "NEW USER"; } else if (collection == "rooms") { } } // {"_id":"RhiggypZiepy9M4qL","_updatedAt":{"$date":1482704143952},"alert":false,"ls":{"$date":1482704143952},"name":"tech","open":true,"rid":"tKHaLfB35vo4qAnBp","t":"c","ts":{"$date":1479497659929},"u":{"_id":"po2bcKwPMdtnDCXhX","username":"ruphy"},"unread":0} void RocketChatBackend::onChanged(QJsonObject object) { QString collection = object.value("collection").toString(); qDebug() << "ROCKET BACK" << object << collection; if (collection == "stream-room-messages") { QJsonObject fields = object.value("fields").toObject(); QString roomId = fields.value("eventName").toString(); QJsonArray contents = fields.value("args").toArray(); RocketChatBackend::processIncomingMessages(contents); } else if (collection == "users") { qDebug() << "NEW USER"; } else if (collection == "rooms") { } } diff --git a/src/roommodel.cpp b/src/roommodel.cpp index a346806c..ec969c6a 100644 --- a/src/roommodel.cpp +++ b/src/roommodel.cpp @@ -1,165 +1,177 @@ /* * * Copyright 2016 Riccardo Iaconelli * * 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 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 "roommodel.h" +#include #include #include "userdata.h" Room RoomModel::fromJSon(const QJsonObject& o) { Room r; r.name = o["name"].toString(); r.id = o["id"].toString(); return r; } QByteArray RoomModel::serialize(const Room& r) { QJsonDocument d; QJsonObject o; o["name"] = r.name; o["id"] = r.id; d.setObject(o); return d.toBinaryData(); } RoomModel::RoomModel(QObject* parent) : QAbstractListModel(parent) { - reset(); } RoomModel::~RoomModel() { QDir cacheDir(UserData::self()->cacheBasePath()); if (!cacheDir.exists(cacheDir.path())) { cacheDir.mkpath(cacheDir.path()); } QFile f(cacheDir.absoluteFilePath("rooms")); if (f.open(QIODevice::WriteOnly)) { QDataStream out(&f); foreach (const Room m, m_roomsList) { QByteArray ms = RoomModel::serialize(m); out.writeBytes(ms, ms.size()); } } } +void RoomModel::clear() +{ + if (m_roomsList.size()) { + beginRemoveRows(QModelIndex(), 0, rowCount()-1); + m_roomsList.clear(); + QAbstractItemModel::endRemoveRows(); + } +} + +// Clear data and refill it it with data in the cache, if there is void RoomModel::reset() { - if (UserData::self()->serverURL().isEmpty()) { + if (UserData::self()->cacheBasePath().isEmpty()) { return; } - beginResetModel(); - m_roomsList.clear(); - endResetModel(); + clear(); + +// beginResetModel(); +// m_roomsList.clear(); +// endResetModel(); QDir cacheDir(UserData::self()->cacheBasePath()); // load cache if (cacheDir.exists(cacheDir.path())) { QFile f(cacheDir.absoluteFilePath("rooms")); if (f.open(QIODevice::ReadOnly)) { QDataStream in(&f); while (!f.atEnd()) { char * byteArray; quint32 length; in.readBytes(byteArray, length); QByteArray arr = QByteArray::fromRawData(byteArray, length); Room m = RoomModel::fromJSon(QJsonDocument::fromBinaryData(arr).object()); // This cache creates some instabilities // m_roomsList[m.name] = m; // addRoom(m.id, m.name, m.selected); } } } } QHash RoomModel::roleNames() const { QHash roles; roles[RoomName] = "name"; roles[RoomID] = "id"; roles[RoomSelected] = "selected"; return roles; } int RoomModel::rowCount(const QModelIndex & parent) const { return m_roomsList.size(); } QVariant RoomModel::data(const QModelIndex & index, int role) const { Room r = m_roomsList.values().at(index.row()); if (role == RoomModel::RoomName) { return r.name; } else if (role == RoomModel::RoomID) { return r.id; } else if (role == RoomModel::RoomSelected) { return r.selected; } else { return QVariant("0"); } } void RoomModel::addRoom(const QString& roomID, const QString& roomName, bool selected) { // qDebug() << m_roomsList.size(); // return; qDebug() << "Adding room" << roomID << roomName << m_roomsList.keys(); if (roomID.isEmpty()) { return; } bool updating = false; if (m_roomsList.contains(roomName)) { // we are doing an update updating = true; } int size = m_roomsList.size(); if (!updating) { beginInsertRows(index(size), size, (size+1)); } Room r; r.id = roomID; r.name = roomName; r.selected = selected; m_roomsList[roomName] = r; if (updating) { //Figure out a better way to update just the really changed message, not EVERYTHING emit dataChanged(createIndex(1, 1), createIndex(rowCount(), 1)); } else { endInsertRows(); } } // #include "roommodel.moc" diff --git a/src/roommodel.h b/src/roommodel.h index 2ae2aa66..0c5ea871 100644 --- a/src/roommodel.h +++ b/src/roommodel.h @@ -1,68 +1,69 @@ /* * * Copyright 2016 Riccardo Iaconelli * * 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 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 . * */ #ifndef ROOMMODEL_H #define ROOMMODEL_H #include struct Room { QString name; bool selected = false; QString id; }; class RoomModel : public QAbstractListModel { Q_OBJECT public: enum RoomRoles { RoomName = Qt::UserRole + 1, RoomSelected, RoomID }; RoomModel (QObject *parent = 0); virtual ~RoomModel(); virtual int rowCount(const QModelIndex & parent = QModelIndex()) const; virtual QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const; // void setCurrentRoom(const QString &newRoom); // QString getCurrentRoom() const; Q_INVOKABLE void addRoom(const QString& roomID, const QString& roomName, bool selected = false); static Room fromJSon(const QJsonObject &source); static QByteArray serialize(const Room &r); void reset(); + void clear(); protected: virtual QHash roleNames() const; private: QHash< QString, Room > m_roomsList; }; #endif // ROOMMODEL_H diff --git a/src/userdata.cpp b/src/userdata.cpp index 89b89888..f8eb0a60 100644 --- a/src/userdata.cpp +++ b/src/userdata.cpp @@ -1,189 +1,198 @@ /* * * Copyright 2016 Riccardo Iaconelli * * 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 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 "userdata.h" #include "roommodel.h" #include "ddpclient.h" UserData *UserData::m_self = 0; QString UserData::authToken() const { return m_authToken; } QString UserData::userName() const { return m_userName; } QString UserData::password() const { return m_password; } void UserData::setAuthToken(const QString& token) { qDebug() << "Setting token to" << token; QSettings s; m_authToken = token; s.setValue("authToken", token); } void UserData::setPassword(const QString& password) { m_password = password; } void UserData::setUserName(const QString& username) { m_userName = username; QSettings s; s.setValue("username", username); emit userNameChanged(); } RoomModel * UserData::roomModel() { if (!m_roomModel) { qDebug() << "creating new RoomModel"; m_roomModel = new RoomModel; qDebug() << m_roomModel; // m_roomModel->reset(); } return m_roomModel; } DDPClient * UserData::ddp() { if (!m_ddp) { m_ddp = new DDPClient(serverURL()); connect(m_ddp, &DDPClient::loginStatusChanged, this, &UserData::loginStatusChanged); // connect(m_ddp, &DDPClient::loginStatusChanged, this, [=](){qDebug() << "Signal received";}); } return m_ddp; } void UserData::sendMessage(const QString &roomID, const QString &message) { QString json = "{\"rid\": \"%1\", \"msg\": \"%2\"}"; json = json.arg(roomID, message); ddp()->method("sendMessage", QJsonDocument::fromJson(json.toUtf8())); } MessageModel * UserData::getModelForRoom(const QString& roomID) { if (m_messageModels.contains(roomID)) { return m_messageModels.value(roomID); } else { // qDebug() << "Creating a new model"; m_messageModels[roomID] = new MessageModel(roomID, this); return m_messageModels[roomID]; } } QString UserData::serverURL() const { return m_serverURL; } void UserData::setServerURL(const QString& serverURL) { if (m_serverURL == serverURL) { return; } QSettings s; s.setValue("serverURL", serverURL); m_serverURL = serverURL; // m_roomModel->reset(); emit serverURLChanged(); } DDPClient::LoginStatus UserData::loginStatus() { if (m_ddp) { return ddp()->loginStatus(); } else { return DDPClient::LoggedOut; } } void UserData::tryLogin() { qDebug() << "Attempting login" << userName() << "on" << serverURL(); -// ddp()->login(); - // Reset data + + // Reset model views foreach (const QString key, m_messageModels.keys()) { MessageModel *m = m_messageModels.take(key); delete m; } delete m_ddp; m_ddp = 0; -// delete m_roomModel; - ddp(); // This creates a new ddp() object. DDP will automatically try to connect and login. + // In the meantime, load cache... + m_roomModel->reset(); + + // This creates a new ddp() object. + // DDP will automatically try to connect and login. + ddp(); } void UserData::logOut() { setAuthToken(QString()); setPassword(QString()); // m_ddp->logOut(); foreach (const QString key, m_messageModels.keys()) { MessageModel *m = m_messageModels.take(key); delete m; } delete m_ddp; m_ddp = 0; emit loginStatusChanged(); + + m_roomModel->clear(); // m_roomModel->reset(); // RoomModel -> reset(); } QString UserData::cacheBasePath() const { + if (m_serverURL.isEmpty()) { + return QString(); + } return QStandardPaths::writableLocation(QStandardPaths::CacheLocation)+'/'+m_serverURL; } UserData::UserData(QObject* parent) : QObject(parent), m_ddp(0), m_roomModel(0) { QSettings s; m_serverURL = s.value("serverURL", "demo.rocket.chat").toString(); m_userName = s.value("username").toString(); m_authToken = s.value("authToken").toString(); // roomModel()->reset(); } UserData * UserData::self() { if (!m_self) { m_self = new UserData; m_self->ddp(); // Create DDP object so we try to connect at startup } return m_self; }