diff --git a/Desktop.qml b/Desktop.qml index b7451adc..78b6263e 100644 --- a/Desktop.qml +++ b/Desktop.qml @@ -1,281 +1,285 @@ // Skeleton from https://github.com/achipa/outqross_blog.git // Almost everything has been re-adapted import QtQuick 2.7 import QtQuick.Controls 1.4 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 QtGraphicalEffects 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 string lightGreen: "#6ab141"; property string darkGreen: "#00613a"; property string selectedRoomID: ""; id: appid title: qsTr("Ruqola") width: 800 height: 600 visible: true Shortcut { sequence: StandardKey.Quit context: Qt.ApplicationShortcut onActivated: Qt.quit() } 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; UserData.tryLogin(); } } BusyIndicator { id: busy anchors.centerIn: parent visible: UserData.loginStatus == DDPClient.LoggingIn } Item { id: mainWidget anchors.fill: parent visible: !loginTab.visible Rectangle { id: userBox anchors.top: parent.top width: parent.width anchors.left: parent.left anchors.right: roomsList.right height: 40 color: darkGreen Text { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignRight anchors.rightMargin: 10 anchors.fill: parent font.pointSize: 12 color: "white" text: "Hello, " + UserData.userName } } RoomsView { anchors.top: userBox.bottom anchors.left: parent.left anchors.bottom: parent.bottom anchors.margins: 0 width: 200 height: appid.height id: roomsList model: UserData.roomModel() visible: parent.visible selectedRoomID: appid.selectedRoomID; onRoomSelected: { if (roomID == selectedRoomID) { return; } console.log("Choosing room", roomID); appid.selectedRoomID = roomID; activeChat.model = UserData.getModelForRoom(roomID) + topicWidget.selectedRoom = UserData.getRoom(roomID) } onCountChanged: { - console.log("We have", roomsList.count, "rooms") +// console.log("We have", roomsList.count, "rooms") } LinearGradient { id: greenGradient anchors.fill: parent start: Qt.point(0, 0) end: Qt.point(roomsList.width, 0) gradient: Gradient { GradientStop { position: 0.0; color: "#6ab141" } GradientStop { position: 1.0; color: "#00613a" } } z: -1; } } Item { anchors.right: parent.right anchors.left: roomsList.right anchors.top: parent.top anchors.bottom: messageLine.top // Item { // anchors.fill: parent // id: greeter // visible: false // // visible: selectedRoomID.empty // Text { // text: "Welcome to Ruqola!"; // } // } Rectangle { id: topicWidget color: "#fff" anchors.top: parent.top + + property var selectedRoom; + Text { anchors.fill: parent - text: "#" + appid.selectedRoomID + text: "#" + parent.selectedRoom.name font.pointSize: 18 verticalAlignment: Text.AlignVCenter anchors.leftMargin: 20 } anchors.right: parent.right anchors.left: parent.left height: 40 } ScrollView { anchors.right: parent.right anchors.left: parent.left anchors.top: topicWidget.bottom anchors.bottom: parent.bottom verticalScrollBarPolicy: Qt.ScrollBarAlwaysOn // visible: parent.visible && (UserData.loginStatus != DDPClient.LoggingIn) visible: !greeter.visible ListView { id: activeChat // model: UserData.getModelForRoom(selectedRoomID) 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") height: 2.7*font.pixelSize // font.pointSize: 12 onAccepted: { if (text != "") { UserData.sendMessage(selectedRoomID, text); text = ""; } } } } Rectangle { z: -10 anchors.fill: parent color: "white" } 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); // roomsList.model = UserData.roomModel(); // systrayIcon.showMessage("Connected", "We are CONNECTED!"); timer.start(); timer.fire(); } /* Timer { id: timer interval: 1000 onTriggered: { // console.log("FIRE"); switch (UserData.loginStatus) { case UserData.NotConnected: statusText = qsTr("Not connected."); break; case UserData.LoggedIn: statusText = qsTr("Connected to " + UserData.serverURL); break; } } repeat: true }*/ onStatusTextChanged: timer.restart(); } diff --git a/main.cpp b/main.cpp index 078e0222..2f035da1 100644 --- a/main.cpp +++ b/main.cpp @@ -1,54 +1,55 @@ #include #include // only if deskop #include #include "src/roommodel.h" #include "src/rocketchatbackend.h" #include "src/userdata.h" #include #include #include #include #include int main(int argc, char *argv[]) { QApplication app(argc, argv); app.setWindowIcon(QIcon(":/systray.png")); QCoreApplication::setOrganizationName("KDE"); QCoreApplication::setOrganizationDomain("kde.org"); QCoreApplication::setApplicationName("Ruqola"); qmlRegisterSingletonType("KDE.Ruqola.UserData", 1, 0, "UserData", userdata_singletontype_provider); qmlRegisterType("KDE.Ruqola.Models", 1, 0, "MessageModel"); qmlRegisterType("KDE.Ruqola.DDPClient", 1, 0, "DDPClient"); qmlRegisterType("KDE.Ruqola.Models", 1, 0, "RoomModel"); + qmlRegisterType("KDE.Ruqola.RoomWrapper", 1, 0, "RoomWrapper"); RocketChatBackend c; QQmlApplicationEngine engine; QQmlContext *ctxt = engine.rootContext(); QMenu menu; auto quit = menu.addAction("&Quit"); QObject::connect(quit, &QAction::triggered, &app, &QApplication::quit); QSystemTrayIcon systray; systray.setIcon(QIcon(":/systray.png")); systray.setContextMenu(&menu); systray.setVisible(true); ctxt->setContextProperty("systrayIcon", &systray); // systray.setVisible(true); engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); return app.exec(); } diff --git a/src/roommodel.cpp b/src/roommodel.cpp index e852778e..ca0c2ffe 100644 --- a/src/roommodel.cpp +++ b/src/roommodel.cpp @@ -1,212 +1,238 @@ /* * * 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(); r.unread = o["unread"].toInt(0); return r; } QByteArray RoomModel::serialize(const Room& r) { QJsonDocument d; QJsonObject o; o["name"] = r.name; o["id"] = r.id; o["unread"] = r.unread; d.setObject(o); return d.toBinaryData(); } +RoomWrapper::RoomWrapper(QObject *parent) + : QObject(parent) +{} + +RoomWrapper::RoomWrapper(const Room &r, QObject *parent) + : QObject(parent) +{ + m_name = r.name; + m_topic = r.topic; + m_unread = r.unread; + m_id = r.id; + m_selected = r.selected; +} + RoomModel::RoomModel(QObject* parent) : QAbstractListModel(parent) { // connect(UserData::self(), &UserData::loginStatusChanged, this, &RoomModel::onLoginStatusChanged); } 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(); } } +RoomWrapper* RoomModel::findRoom(const QString& roomID) const +{ + foreach (const Room r, m_roomsList) { + if (r.id == roomID) { + return new RoomWrapper(r); + } + } + Room r; +// RoomWrapper w; + return new RoomWrapper(r); +} + // Clear data and refill it it with data in the cache, if there is void RoomModel::reset() { if (UserData::self()->cacheBasePath().isEmpty()) { return; } clear(); 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()); addRoom(m.id, m.name, m.selected); } } } qDebug() << "Cache Loaded"; } QHash RoomModel::roleNames() const { QHash roles; roles[RoomName] = "name"; roles[RoomID] = "room_id"; roles[RoomSelected] = "selected"; roles[RoomUnread] = "unread"; return roles; } int RoomModel::rowCount(const QModelIndex & parent) const { // if (m_roomsHash.size() > 4) {return 4;} // qDebug() << m_roomsList.size() << "ROOMS"; return m_roomsList.size(); } QVariant RoomModel::data(const QModelIndex & index, int role) const { // qDebug() << "GOT ASKED FOR " << index.row(); Room r = m_roomsList.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::setActiveRoom(const QString& activeRoom) // { // foreach (const QString &id, m_roomsHash.keys()) { // qDebug() << id; // m_roomsHash[id].selected = (id == activeRoom); // } // // emit dataChanged(createIndex(1, 1), createIndex(rowCount(), 1)); // } void RoomModel::addRoom(const QString& roomID, const QString& roomName, bool selected) { if (roomID.isEmpty() || roomName.isEmpty()) { return; } qDebug() << "Adding room" << roomID << roomName; Room r; r.id = roomID; r.name = roomName; r.selected = selected; addRoom(r); } void RoomModel::addRoom(const Room &room) { auto existingRoom = qFind(m_roomsList.begin(), m_roomsList.end(), room); bool present = (existingRoom != m_roomsList.end()); // qDebug() << "Present? "<< present; auto i = qUpperBound(m_roomsList.begin(), m_roomsList.end(), room); int pos = i-m_roomsList.begin(); bool roomChanged = false; qDebug() << pos; // if (qFind(m_roomsList.begin(), m_roomsList.end(), room) != m_roomsList.end() && pos > 0) { if (present) { // qDebug() << (qFind(m_roomsList.begin(), m_roomsList.end(), room) - m_roomsList.begin()); // if (pos != m_roomsList.size()) { // we're at the end // qDebug() << "Room changed!"; roomChanged = true; //Figure out a better way to update just the really changed message } else { beginInsertRows(QModelIndex(), pos, pos); } if (roomChanged) { m_roomsList.replace(pos-1, room); } else { qDebug() << "Inserting room at position" <getModelForRoom(room.id); } // #include "roommodel.moc" diff --git a/src/roommodel.h b/src/roommodel.h index a065d6bd..102f38e3 100644 --- a/src/roommodel.h +++ b/src/roommodel.h @@ -1,92 +1,126 @@ /* * * 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 +#include + class Room { -public: +public: +// Room(const Room &room) +// { +// // this->parent = room.parent(); +// } + // To be used in ID find: message ID inline bool operator==(const Room &other) const { return other.id == id; }; // To be used in sorted insert: timestamp inline bool operator<(const Room &other) const { return name < other.name; }; - + + QString getName() const {return name;}; + QString getTopic() const {return topic;}; + +private: + friend class RoomModel; + friend class RoomWrapper; // When you add a field, please remember to also add relevant code // to the enum declaration, roleNames, fromJSon and serialize QString name; QString topic; int unread; bool selected = false; QString id; }; +class RoomWrapper : public QObject +{ + Q_PROPERTY(QString name READ getName) + Q_PROPERTY(QString topic READ getTopic) + Q_OBJECT + +public: + RoomWrapper(QObject *parent = 0); + RoomWrapper(const Room &r, QObject *parent = 0); + + QString getName() {return m_name;}; + QString getTopic() {return m_topic;}; +private: + QString m_name, m_topic, m_id; + int m_unread; + bool m_selected; +}; + + class RoomModel : public QAbstractListModel { Q_OBJECT public: enum RoomRoles { RoomName = Qt::UserRole + 1, RoomSelected, RoomID, RoomUnread }; 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); void addRoom(const Room& room); + RoomWrapper* findRoom(const QString &roomID) const; + static Room fromJSon(const QJsonObject &source); static QByteArray serialize(const Room &r); // void setActiveRoom(const QString &activeRoom); void reset(); void clear(); protected: virtual QHash roleNames() const; private: QVector m_roomsList; // QHash< QString, Room > m_roomsHash; }; #endif // ROOMMODEL_H diff --git a/src/userdata.cpp b/src/userdata.cpp index 7e55cafc..a7bdd822 100644 --- a/src/userdata.cpp +++ b/src/userdata.cpp @@ -1,212 +1,217 @@ /* * * 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(this); 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)) { // qDebug() << "Returning old model for " << 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(); // Reset model views foreach (const QString key, m_messageModels.keys()) { MessageModel *m = m_messageModels.take(key); delete m; } delete m_ddp; m_ddp = 0; // 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; } // QString UserData::activeRoom() const // { // return m_activeRoom; // } // void UserData::setActiveRoom(const QString& activeRoom) // { // m_activeRoom = activeRoom; // // roomModel()->setActiveRoom(activeRoom); // emit activeRoomChanged(); // } +RoomWrapper * UserData::getRoom(const QString& roomID) +{ + return roomModel()->findRoom(roomID); +} + 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 m_self->roomModel()->reset(); // m_self->getModelForRoom("GENERAL"); } return m_self; } diff --git a/src/userdata.h b/src/userdata.h index 1a048d9a..32c50f2c 100644 --- a/src/userdata.h +++ b/src/userdata.h @@ -1,106 +1,107 @@ /* * * 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 USERDATA_H #define USERDATA_H #include #include #include #include "ddpclient.h" #include "roommodel.h" #include "messagemodel.h" class QString; class UserData: public QObject { Q_OBJECT Q_PROPERTY (QString userName READ userName WRITE setUserName NOTIFY userNameChanged) Q_PROPERTY (QString serverURL READ serverURL WRITE setServerURL NOTIFY serverURLChanged) Q_PROPERTY (QString password WRITE setPassword) // Q_PROPERTY (bool connected READ connected NOTIFY connectedChanged) Q_PROPERTY (DDPClient::LoginStatus loginStatus READ loginStatus NOTIFY loginStatusChanged) // Q_PROPERTY(QString activeRoom READ activeRoom WRITE setActiveRoom NOTIFY activeRoomChanged) public: static UserData* self(); void setUserName(const QString &username); QString userName() const; void setPassword(const QString &password); QString password() const; void setAuthToken(const QString &token); QString authToken() const; bool connected(); DDPClient::LoginStatus loginStatus(); QString serverURL() const; void setServerURL(const QString &serverURL); // QString activeRoom() const; // void setActiveRoom(const QString &activeRoom); DDPClient *ddp(); Q_INVOKABLE RoomModel *roomModel(); Q_INVOKABLE void sendMessage(const QString &roomID, const QString &message); Q_INVOKABLE MessageModel* getModelForRoom(const QString &roomID); Q_INVOKABLE void tryLogin(); Q_INVOKABLE void logOut(); + Q_INVOKABLE RoomWrapper* getRoom(const QString &roomID); // void setRoomModel(); QString cacheBasePath() const; signals: void userNameChanged(); void serverURLChanged(); void loginStatusChanged(); private: UserData(QObject *parent = 0); static UserData *m_self; QString m_password; QString m_userName; QString m_authToken; QString m_serverURL; RoomModel *m_roomModel; DDPClient *m_ddp; QHash< QString, MessageModel * > m_messageModels; }; inline static QObject *userdata_singletontype_provider(QQmlEngine *engine, QJSEngine *scriptEngine) { Q_UNUSED(engine) Q_UNUSED(scriptEngine) UserData *userData = UserData::self(); return userData; } #endif // USERDATA_H