diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a589c2d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.Ruqola.pro.user diff --git a/Desktop.qml b/Desktop.qml index 81cd4248..4dce8897 100644 --- a/Desktop.qml +++ b/Desktop.qml @@ -1,326 +1,350 @@ // 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.Ruqola 1.0 import KDE.Ruqola.DDPClient 1.0 import KDE.Ruqola.Notification 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: (Ruqola.loginStatus == DDPClient.LoginFailed || Ruqola.loginStatus == DDPClient.LoggedOut) // visible: (Ruqola.loginStatus != DDPClient.LoggedIn) anchors.fill:parent z: 10 serverURL: Ruqola.serverURL username: Ruqola.userName onAccepted: { Ruqola.password = loginTab.password; Ruqola.userName = loginTab.username; Ruqola.serverURL = loginTab.serverURL; Ruqola.tryLogin(); } } BusyIndicator { id: busy anchors.centerIn: parent visible: Ruqola.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, " + Ruqola.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: Ruqola.roomModel() - - visible: parent.visible - + visible: parent.visible selectedRoomID: appid.selectedRoomID; - onRoomSelected: { if (roomID == selectedRoomID) { return; } console.log("Choosing room", roomID); appid.selectedRoomID = roomID; activeChat.model = Ruqola.getModelForRoom(roomID) topicWidget.selectedRoom = Ruqola.getRoom(roomID) } onCountChanged: { // 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; } - } + } //RoomsView 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!"; -// } -// } + anchors.bottom: input.top + id: chatView Rectangle { id: topicWidget color: "#fff" + anchors.top: parent.top anchors.right: parent.right anchors.left: parent.left height: nameLabel.height + topicLabel.height property var selectedRoom; Text { id: nameLabel text: "#" + parent.selectedRoom.name font.pointSize: 18 verticalAlignment: Text.AlignVCenter anchors.leftMargin: 20 height: 40 // height: font.pixelSize + 10 anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right } Text { id: topicLabel text: topicWidget.selectedRoom.topic anchors.top: nameLabel.bottom anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right horizontalAlignment: Text.AlignHCenter height: font.pixelSize + 10 } } ScrollView { anchors.right: parent.right anchors.left: parent.left anchors.top: topicWidget.bottom anchors.bottom: parent.bottom verticalScrollBarPolicy: Qt.ScrollBarAlwaysOn // visible: parent.visible && (Ruqola.loginStatus != DDPClient.LoggingIn) // visible: !greeter.visible ListView { id: activeChat // model: Ruqola.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 + //width: parent.width } } } - } //Item + } //Item chatView - TextField { - id: messageLine - anchors.right: parent.right - anchors.left: roomsList.right + Item { anchors.bottom: parent.bottom - placeholderText: if (Ruqola.loginStatus != DDPClient.LoggedIn || (selectedRoomID=="")){ - qsTr("Please Select a room") - } - else{ - qsTr("Enter message") - } - height: 2.7*font.pixelSize - - onAccepted: { - if (text != "" && Ruqola.loginStatus == DDPClient.LoggedIn && !(selectedRoomID=="")) { - Ruqola.sendMessage(selectedRoomID, text); - text = ""; + anchors.left: roomsList.right + anchors.right: parent.right + id: input + height: 40 + + TextField { + id: messageLine + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.top: parent.top + anchors.right: emoticonsButton.left + placeholderText: if (Ruqola.loginStatus != DDPClient.LoggedIn || (selectedRoomID=="")){ + qsTr("Please Select a room") + } + else{ + qsTr("Enter message") + } + +// height: 2.7*font.pixelSize + property string type: "text"; + onAccepted: { + if (text != "" && Ruqola.loginStatus == DDPClient.LoggedIn && !(selectedRoomID=="")) { + Ruqola.sendMessage(selectedRoomID, text, type); + text = ""; + } } } - } + + Button { + anchors.bottom: parent.bottom + anchors.top: parent.top + anchors.right: attachmentsButton.left + width: 50 + id : emoticonsButton + iconName: "emoticonsButton" + iconSource: "qrc:/Emoticon.png" + visible: true + } + + Button { + anchors.bottom: parent.bottom + anchors.top: parent.top + anchors.right: parent.right + width: 50 + id : attachmentsButton + iconName: "attachmentsButton" + iconSource: "qrc:/attach-button.jpg" + visible: true + onClicked: Ruqola.attachmentButtonClicked(); + } + + }//Item input + }// mainWidget Item + + Image { + id: receivedImage + source:" " + width: 60 + height: 80 + fillMode: Image.PreserveAspectFit +// visible: //only when an image is recieved from server + sourceSize.width: 1024 + sourceSize.height: 1024 + } Rectangle { z: -10 anchors.fill: parent color: "white" } onClosing: { console.log("Minimizing to systray..."); hide(); - systrayIcon.windowVisible = visible; } function toggleShow() { if (visible) { hide(); - systrayIcon.windowVisible = visible; } else { show(); raise(); requestActivate(); - systrayIcon.windowVisible = visible; } } -// function notificationMessageClicked() { -// if (!visible) { -// show(); -// raise(); -// requestActivate(); -// systrayIcon.windowVisible = visible; -// } -// } - Component.onCompleted: { systrayIcon.activated.connect(toggleShow); systrayIcon.messageClicked.connect(toggleShow); // roomsList.model = Ruqola.roomModel(); // timer.start(); // timer.fire(); } /* Timer { id: timer interval: 1000 onTriggered: { // console.log("FIRE"); switch (Ruqola.loginStatus) { case Ruqola.NotConnected: statusText = qsTr("Not connected."); break; case Ruqola.LoggedIn: statusText = qsTr("Connected to " + Ruqola.serverURL); break; } } repeat: true }*/ // onStatusTextChanged: timer.restart(); } diff --git a/attach-button.jpg b/attach-button.jpg new file mode 100644 index 00000000..7fecf41d Binary files /dev/null and b/attach-button.jpg differ diff --git a/main.cpp b/main.cpp index 1bc56fa0..993371e8 100644 --- a/main.cpp +++ b/main.cpp @@ -1,42 +1,43 @@ #include #include // only if desktop #include "src/roommodel.h" #include "src/rocketchatbackend.h" #include "src/ruqola.h" #include "src/notification.h" #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.Ruqola", 1, 0, "Ruqola", ruqola_singletontype_provider); qmlRegisterType("KDE.Ruqola.MessageModel", 1, 0, "MessageModel"); qmlRegisterType("KDE.Ruqola.DDPClient", 1, 0, "DDPClient"); qmlRegisterType("KDE.Ruqola.RoomModel", 1, 0, "RoomModel"); qmlRegisterType("KDE.Ruqola.RoomWrapper", 1, 0, "RoomWrapper"); qmlRegisterType("KDE.Ruqola.Notification", 1, 0, "Notification"); RocketChatBackend c; QQmlApplicationEngine engine; QQmlContext *ctxt = engine.rootContext(); ctxt->setContextProperty("systrayIcon", Ruqola::self()->notification()); +// ctxt->setContextProperty("attachmentButton", Ruqola::self()->attachmentButtonClicked()); engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); if (engine.rootObjects().isEmpty()) return -1; return app.exec(); } diff --git a/qml.qrc b/qml.qrc index 9afee098..35beca4b 100644 --- a/qml.qrc +++ b/qml.qrc @@ -1,15 +1,16 @@ Desktop.qml JSONListModel.qml RoomModel.qml jsonpath.js main.qml Message.qml RoomsView.qml RoomDelegate.qml Login.qml marked.js systray.png + attach-button.jpg diff --git a/src/ddpclient.cpp b/src/ddpclient.cpp index 76632339..5cdf2861 100644 --- a/src/ddpclient.cpp +++ b/src/ddpclient.cpp @@ -1,305 +1,344 @@ /* * * 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 "ddpclient.h" #include #include #include #include +#include #include "ruqola.h" void process_test(QJsonDocument doc) { qDebug() << "Callback test:" << doc; qDebug() << "End callback"; } void login_callback(QJsonDocument doc) { qDebug() << "LOGIN:" << doc; Ruqola::self()->setAuthToken(doc.object().value("token").toString()); qDebug() << "End callback"; } void DDPClient::resume_login_callback(QJsonDocument doc) { qDebug() << "LOGIN:" << doc; Ruqola::self()->setAuthToken(doc.object().value("token").toString()); qDebug() << "End callback"; } void empty_callback(QJsonDocument doc) { Q_UNUSED(doc); } DDPClient::DDPClient(const QString& url, QObject* parent) : QObject(parent), m_url(url), m_uid(1), m_loginJob(0), m_loginStatus(NotConnected), m_connected(false), m_attemptedPasswordLogin(false), m_attemptedTokenLogin(false) { m_webSocket.ignoreSslErrors(); connect(&m_webSocket, &QWebSocket::connected, this, &DDPClient::onWSConnected); connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &DDPClient::onTextMessageReceived); connect(&m_webSocket, &QWebSocket::disconnected, this, &DDPClient::WSclosed); connect(Ruqola::self(), &Ruqola::serverURLChanged, this, &DDPClient::onServerURLChange); if (!url.isEmpty()) { m_webSocket.open(QUrl("wss://"+url+"/websocket")); } qDebug() << "Trying to connect to URL" << url; } DDPClient::~DDPClient() { m_webSocket.close(); } void DDPClient::onServerURLChange() { if (Ruqola::self()->serverURL() != m_url || !m_webSocket.isValid()) { if (m_webSocket.isValid()) { m_webSocket.flush(); m_webSocket.close(); } m_url = Ruqola::self()->serverURL(); m_webSocket.open(QUrl("wss://"+m_url+"/websocket")); connect(&m_webSocket, &QWebSocket::connected, this, &DDPClient::onWSConnected); qDebug() << "Reconnecting" << m_url; //<< m_webSocket.st; } } DDPClient::LoginStatus DDPClient::loginStatus() const { return m_loginStatus; } bool DDPClient::isConnected() const { return m_connected; } bool DDPClient::isLoggedIn() const { return m_loginStatus == LoggedIn; } unsigned int DDPClient::method(const QString& m, const QJsonDocument& params) { return method(m, params, empty_callback); } unsigned int DDPClient::method(const QString& method, const QJsonDocument& params, std::function callback) { QJsonObject json; json["msg"] = "method"; json["method"] = method; json["id"] = QString::number(m_uid); if (params.isArray()){ json["params"] = params.array(); } else if (params.isObject()) { QJsonArray arr; arr.append(params.object()); json["params"] = arr; // params.object(); } qint64 bytes = m_webSocket.sendTextMessage(QJsonDocument(json).toJson(QJsonDocument::Compact)); if (bytes < json.length()) { qDebug() << "ERROR! I couldn't send all of my message. This is a bug! (try again)"; qDebug() << m_webSocket.isValid() << m_webSocket.error() << m_webSocket.requestUrl(); } else { qDebug() << "Successfully sent " << json; } //callback(QJsonDocument::fromJson(json.toUtf8())); m_callbackHash[m_uid] = callback; m_uid++; return m_uid - 1 ; } void DDPClient::subscribe(const QString& collection, const QJsonArray& params) { QJsonObject json; json["msg"] = "sub"; json["id"] = QString::number(m_uid); json["name"] = collection; json["params"] = params; qint64 bytes = m_webSocket.sendTextMessage(QJsonDocument(json).toJson(QJsonDocument::Compact)); if (bytes < json.length()) { qDebug() << "ERROR! I couldn't send all of my message. This is a bug! (try again)"; } m_uid++; } void DDPClient::onTextMessageReceived(QString message) { QJsonDocument response = QJsonDocument::fromJson(message.toUtf8()); if (!response.isNull() && response.isObject()) { QJsonObject root = response.object(); - QString messageType = root.value("msg").toString(); + qDebug() << "--------------------"; + qDebug() << "--------------------"; +// qDebug() << "Root is- " << root; + if (messageType == "updated") { - } else if (messageType == "result") { - -// qDebug() << "got a result" << root; + } else if (messageType == "result") { unsigned id = root.value("id").toString().toInt(); - if (m_callbackHash.contains(id)) { + if (m_callbackHash.contains(id)) { std::function callback = m_callbackHash.take(id); + + QJsonDocument res = QJsonDocument(root.value("result").toObject()); + QJsonObject result = res.object(); + QString type = result.value("type").toString(); + QString msg = result.value("msg").toString(); + + QByteArray base64Image; + QImage image; + + QString path = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); + QDir dir(path); + if (!dir.exists()){ + dir.mkdir(path); + qDebug() << "Directory created at " << path; + } + QDir::setCurrent(path); + + const QDateTime currentTime = QDateTime::currentDateTime(); + const QString timestamp = currentTime.toString(QLatin1String("yyyyMMdd-hhmmsszzz")); + const QString filename = QString::fromLatin1("%1.jpg").arg(timestamp); + + if (type == "image"){ + qDebug() << "I am here yay"; + base64Image.append(msg); + image.loadFromData(QByteArray::fromBase64(base64Image), "JPG"); + if ( !image.isNull() ){ + qDebug() << "Saving Image to " << path; + if (image.save(filename, "JPEG") ){ + qDebug() << "Image saved successfully"; + } else { + qDebug() << "Image NOT saved"; + } + } else{ + qDebug() << "Image is NULL"; + } + } else if (type == "text"){ + + } + callback( QJsonDocument(root.value("result").toObject()) ); - } + } emit result(id, QJsonDocument(root.value("result").toObject())); - + if (id == m_loginJob) { if (root.value("error").toObject().value("error").toInt() == 403) { qDebug() << "Wrong password or token expired"; login(); // Let's keep trying to log in } else { Ruqola::self()->setAuthToken(root.value("result").toObject().value("token").toString()); setLoginStatus(DDPClient::LoggedIn); } // emit loggedInChanged(); } } else if (messageType == "connected") { qDebug() << "Connected"; m_connected = true; emit connectedChanged(); setLoginStatus(DDPClient::LoggingIn); - login(); // Try to resume auth token login - - + login(); // Try to resume auth token login } else if (messageType == "error") { qDebug() << "ERROR!!" << message; } else if (messageType == "ping") { qDebug() << "Ping - Pong"; QJsonObject pong; pong["msg"] = "pong"; m_webSocket.sendBinaryMessage(QJsonDocument(pong).toJson(QJsonDocument::Compact)); } else if (messageType == "added"){ qDebug() << "ADDING" <password().isEmpty()) { // If we have a password and we couldn't log in, let's stop here if (m_attemptedPasswordLogin) { setLoginStatus(LoginFailed); return; } m_attemptedPasswordLogin = true; QJsonObject user; user["username"] = Ruqola::self()->userName(); QJsonObject json; json["password"] = Ruqola::self()->password(); json["user"] = user; m_loginJob = method("login", QJsonDocument(json)); } else if (!Ruqola::self()->authToken().isEmpty() && !m_attemptedTokenLogin) { m_attemptedPasswordLogin = true; QJsonObject json; json["resume"] = Ruqola::self()->authToken(); m_loginJob = method("login", QJsonDocument(json)); } else { setLoginStatus(LoginFailed); } } void DDPClient::logOut() { // setLoginStatus(NotConnected); m_webSocket.close(); } void DDPClient::onWSConnected() { qDebug() << "Websocket connected at URL" << m_url; QJsonArray supportedVersions; supportedVersions.append("1"); QJsonObject protocol; protocol["msg"] = "connect"; protocol["version"] = "1"; protocol["support"] = supportedVersions; // QString json("{\"msg\":\"connect\", \"version\": \"1\", \"support\": [\"1\"]}"); QByteArray serialize = QJsonDocument(protocol).toJson(QJsonDocument::Compact); qint64 bytes = m_webSocket.sendTextMessage(serialize); if (bytes < serialize.length()) { qDebug() << "ERROR! I couldn't send all of my message. This is a bug! (try again)"; } else { qDebug() << "Successfully sent " << serialize; } } void DDPClient::WSclosed() { qDebug() << "WebSocket CLOSED" << m_webSocket.closeReason() << m_webSocket.error() << m_webSocket.closeCode(); setLoginStatus(NotConnected); // m_connected = false; } diff --git a/src/messagemodel.cpp b/src/messagemodel.cpp index f3820512..34c62a0c 100644 --- a/src/messagemodel.cpp +++ b/src/messagemodel.cpp @@ -1,211 +1,236 @@ /* * * 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 #include #include #include #include // #include #include #include #include "messagemodel.h" #include "ruqola.h" Message MessageModel::fromJSon(const QJsonObject& o) { Message message; - message.username = o["username"].toString(); + + message.messageID = o["messageID"].toString(); + message.roomID = o["roomID"].toString(); message.message = o["message"].toString(); - message.userID = o["userID"].toString(); message.timestamp = (qint64) o["timestamp"].toDouble(); + message.username = o["username"].toString(); + message.userID = o["userID"].toString(); + message.updatedAt = (qint64) o["updatedAt"].toDouble(); + message.editedAt = (qint64) o["editedAt"].toDouble(); + message.editedByUsername = o["editedByUsername"].toString(); + message.editedByUserID = o["editedByUserID"].toString(); + message.url = o["url"].toString(); + message.meta = o["meta"].toString(); + message.headers = o["headers"].toString(); + message.parsedUrl = o["parsedUrl"].toString(); + message.image_url = o["image_url"].toString(); + message.color = o["color"].toString(); + message.alias = o["alias"].toString(); + message.avatar = o["avatar"].toString(); + message.groupable = o["groupable"].toBool(); + message.parseUrls = o["parseUrls"].toBool(); + message.systemMessage = o["systemMessage"].toBool(); message.systemMessageType = o["type"].toString(); - message.roomID = o["roomID"].toString(); - message.messageID = o["messageID"].toString(); + return message; } QByteArray MessageModel::serialize(const Message& message) { QJsonDocument d; QJsonObject o; - o["username"] = message.username; + + o["messageID"] = message.messageID; + o["roomID"] = message.roomID; o["message"] = message.message; - o["userID"] = message.userID; o["timestamp"] = message.timestamp; + o["username"] = message.username; + o["userID"] = message.userID; + o["updatedAt"] = message.updatedAt; + o["editedAt"] = message.editedAt; + o["editedByUsername"] = message.editedByUsername; + o["editedByUserID"] = message.editedByUserID; + o["url"] = message.url; + o["meta"] = message.meta; + o["headers"] = message.headers; + o["parsedUrl"] = message.parsedUrl; + o["image_url"] = message.image_url; + o["color"] = message.color; + o["alias"] = message.alias; + o["avatar"] = message.avatar; + o["groupable"] = message.groupable; + o["parseUrls"] = message.parseUrls; + o["systemMessage"] = message.systemMessage; o["type"] = message.systemMessageType; - o["roomID"] = message.roomID; - o["messageID"] = message.messageID; + d.setObject(o); return d.toBinaryData(); } MessageModel::MessageModel(const QString &roomID, QObject* parent) : QAbstractListModel(parent), m_roomID(roomID) { qDebug() << "Creating message Model"; QDir cacheDir(Ruqola::self()->cacheBasePath()+"/rooms_cache"); // load cache if (QFile::exists(cacheDir.absoluteFilePath(roomID)) && !roomID.isEmpty()) { QFile f(cacheDir.absoluteFilePath(roomID)); 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); Message m = MessageModel::fromJSon(QJsonDocument::fromBinaryData(arr).object()); addMessage(m); // m_allMessages[m.timestamp] = m; // qDebug() << m.message; } } } } MessageModel::~MessageModel() { QDir cacheDir(Ruqola::self()->cacheBasePath()+"/rooms_cache"); qDebug() << "Caching to..." << cacheDir.path(); if (!cacheDir.exists(cacheDir.path())) { cacheDir.mkpath(cacheDir.path()); } QFile f(cacheDir.absoluteFilePath(m_roomID)); if (f.open(QIODevice::WriteOnly)) { QDataStream out(&f); foreach (const Message m, m_allMessages) { QByteArray ms = MessageModel::serialize(m); out.writeBytes(ms, ms.size()); } } } QHash MessageModel::roleNames() const { QHash roles; roles[MessageText] = "messageText"; roles[Username] = "username"; roles[Timestamp] = "timestamp"; roles[UserID] = "userID"; roles[SystemMessage] = "systemMessage"; roles[SystemMessageType] = "type"; return roles; } qint64 MessageModel::lastTimestamp() const { if (m_allMessages.size()) { qDebug() << "returning timestamp" << m_allMessages.last().timestamp; return m_allMessages.last().timestamp; } else { return 0; } } int MessageModel::rowCount(const QModelIndex& parent) const { // qDebug() << "C++ asked for rowcount " << m_allMessages.size(); // if (m_allMessages.contains(m_currentRoom)) { return m_allMessages.size(); (void)parent; } void MessageModel::addMessage(const Message& message) { - // Don't add empty messages? + // Don't add empty messages if (message.message.isEmpty()) { return; } -// qDebug() << "MessageModel::addMessage called"; - auto existingMessage = qFind(m_allMessages.begin(), m_allMessages.end(), message); bool present = (existingMessage != m_allMessages.end()); - - auto i = qUpperBound(m_allMessages.begin(), m_allMessages.end(), - message); - + auto i = qUpperBound(m_allMessages.begin(), m_allMessages.end(), message); int pos = i-m_allMessages.begin(); - bool messageChanged = false; // if (qFind(m_allMessages.begin(), m_allMessages.end(), message) != m_allMessages.end()) { if (present){ // if (pos != m_allMessages.size()) { // we're at the end // qDebug() << "detecting a message change"; messageChanged = true; //Figure out a better way to update just the really changed message } else { beginInsertRows(QModelIndex(), pos, pos); } if (messageChanged) { m_allMessages.replace(pos-1, message); } else { m_allMessages.insert(i, message); } if (messageChanged) { emit dataChanged(createIndex(1, 1), createIndex(pos, 1)); } else { endInsertRows(); } } QVariant MessageModel::data(const QModelIndex& index, int role) const { int idx = index.row();//-1; if (role == MessageModel::Username) { -// qDebug() << "C++ returning username" << -// m_allMessages[m_currentRoom].values().at(idx).username(); return m_allMessages.at(idx).username; } else if (role == MessageModel::MessageText) { return m_allMessages.at(idx).message; } else if (role == MessageModel::Timestamp) { return QVariant(m_allMessages.at(idx).timestamp); } else if (role == MessageModel::UserID) { return QVariant(m_allMessages.at(idx).userID); } else if (role == MessageModel::SystemMessage) { -// qDebug() << "System message?" << m_allMessages.at(idx).systemMessage; return QVariant(m_allMessages.at(idx).systemMessage); } else if (role == MessageModel::SystemMessageType) { return QVariant(m_allMessages.at(idx).systemMessageType); } else { return QVariant(""); } } // #include "messagelist.moc" diff --git a/src/messagemodel.h b/src/messagemodel.h index f53490ba..a66366d9 100644 --- a/src/messagemodel.h +++ b/src/messagemodel.h @@ -1,98 +1,158 @@ /* * * 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 MESSAGEMODEL_H #define MESSAGEMODEL_H #include #include #include #include #include #include class Message { public: // To be used in ID find: message ID inline bool operator==(const Message &other) const { return other.messageID == messageID; } // To be used in sorted insert: timestamp inline bool operator<(const Message &other) const { return timestamp < other.timestamp; } + //Message Object Fields + + // _id QString messageID; - QString username; - QString userID; + // rid + QString roomID; + + // msg QString message; + + // ts qint64 timestamp; + + // u + QString username; + QString userID; + + // _updatedAt + qint64 updatedAt; + + // editedAt + qint64 editedAt; + + // editedBy + QString editedByUsername; + QString editedByUserID; + + // urls + QString url; + QString meta; + QString headers; + QString parsedUrl; + + // attachments + QString image_url; + QString color; + + // alias + QString alias; + + // avatar + QString avatar; + + // groupable + bool groupable; + + // parseUrls + bool parseUrls; + bool systemMessage = false; - QString roomID; QString systemMessageType; + }; class MessageModel : public QAbstractListModel { Q_OBJECT public: enum MessageRoles { Username = Qt::UserRole + 1, MessageText, Timestamp, UserID, SystemMessage, - SystemMessageType + SystemMessageType, + MessageID, + RoomID, + UpdatedAt, + EditedAt, + EditedByUserName, + EditedByUserID, + Url, + Meta, + Headers, + ParsedUrl, + Image_url, + Color, + Alias, + Avatar, + Groupable, + ParseUrls }; MessageModel(const QString &roomID = "no_room", QObject *parent = 0); virtual ~MessageModel(); void addMessage(const Message& message); virtual int rowCount(const QModelIndex & parent = QModelIndex()) const; virtual QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const; qint64 lastTimestamp() const; static Message fromJSon(const QJsonObject &source); static QByteArray serialize(const Message &message); protected: virtual QHash roleNames() const; private: const QString m_roomID; QVector m_allMessages; // QMap m_allMessages; // QMap m_allMessages; QString m_writableLocation; QFile *cacheWriter; }; #endif diff --git a/src/notification.cpp b/src/notification.cpp index e3f73636..13756387 100644 --- a/src/notification.cpp +++ b/src/notification.cpp @@ -1,100 +1,60 @@ /* * * 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 "notification.h" #include "ruqola.h" #include #include #include -bool Notification::windowVisible() const -{ - return m_windowVisible; -} - -void Notification::setWindowVisible(bool value) -{ - if (m_windowVisible != value){ - m_windowVisible = value; - emit windowVisibleChanged(); - } -} - -QString Notification::message() const -{ - return m_message; -} - -void Notification::setMessage(const QString &message) -{ - if (m_message != message){ - m_message = message; - emit messageChanged(); - } -} - //create actions in Menu void Notification::createActions(){ m_quitAction = new QAction(tr("&Quit"), this); connect(m_quitAction, &QAction::triggered, qApp, &QCoreApplication::quit); } //create systrayIcon void Notification::createTrayIcon(){ if (!QSystemTrayIcon::isSystemTrayAvailable()) { QMessageBox::critical(0, QObject::tr("Systray"), QObject::tr("Cannot detect SystemTray on this system.")); return; } - + m_trayIconMenu = new QMenu(); m_trayIconMenu->addAction(m_quitAction); m_trayIconMenu->addSeparator(); - + setContextMenu(m_trayIconMenu); setToolTip("Ruqola"); setIcon(QIcon(":/systray.png")); setVisible(true); } -void Notification::updateDesktopNotification() -{ - if (!windowVisible()){ - QString title("New Ruqola Message!"); //This can be enhanced later - showMessage(title, m_message, QSystemTrayIcon::Information, 5000 ); - } -} - -Notification::Notification() - : m_windowVisible(true) -{ +Notification::Notification() { createActions(); createTrayIcon(); - - //connect messageChanged signal to updateDesktopNotification Slot - connect(this, &Notification::messageChanged, this, &Notification::updateDesktopNotification); - } diff --git a/src/notification.h b/src/notification.h index e148033a..a4ffefa5 100644 --- a/src/notification.h +++ b/src/notification.h @@ -1,66 +1,46 @@ /* * * 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 NOTIFICATION_H #define NOTIFICATION_H #include #include #include class Notification: public QSystemTrayIcon { Q_OBJECT - Q_PROPERTY(bool windowVisible READ windowVisible WRITE setWindowVisible NOTIFY windowVisibleChanged) - Q_PROPERTY(QString message READ message WRITE setMessage NOTIFY messageChanged) public: - - void setWindowVisible(bool val); - bool windowVisible() const; - - void setMessage(const QString &message); - QString message() const; - Notification(); -signals: - void windowVisibleChanged(); - void messageChanged(); - -private slots: - void updateDesktopNotification(); - private: void createActions(); void createTrayIcon(); QAction *m_quitAction; QMenu *m_trayIconMenu; - bool m_windowVisible; - - //Notification message - QString m_message; - }; + #endif // NOTIFICATION_H diff --git a/src/rocketchatbackend.cpp b/src/rocketchatbackend.cpp index 0c90c16b..eed05bce 100644 --- a/src/rocketchatbackend.cpp +++ b/src/rocketchatbackend.cpp @@ -1,265 +1,278 @@ /* * * 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 "ruqola.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) { RoomModel *model = Ruqola::self()->roomModel(); 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() != "d") { QString roomID = room.value("_id").toString(); MessageModel *roomModel = Ruqola::self()->getModelForRoom(roomID); // let's be extra safe around crashes if (Ruqola::self()->loginStatus() == DDPClient::LoggedIn) { Room r; r.id = roomID; r.name = room["name"].toString(); r.topic = room["topic"].toString(); qDebug() << "Adding room" << r.name << r.id << r.topic; model->addRoom(r); } QJsonArray params; params.append(QJsonValue(roomID)); Ruqola::self()->ddp()->subscribe("stream-room-messages", params); // Load history params.append(QJsonValue(QJsonValue::Null)); params.append(QJsonValue(50)); // Max number of messages to load; QJsonObject dateObject; dateObject["$date"] = QJsonValue(roomModel->lastTimestamp()); params.append(dateObject); Ruqola::self()->ddp()->method("loadHistory", QJsonDocument(params), process_backlog); } } } void subs_callback(QJsonDocument doc) { RoomModel *model = Ruqola::self()->roomModel(); 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() != "d") { QString roomID = room.value("rid").toString(); MessageModel *roomModel = Ruqola::self()->getModelForRoom(roomID); // let's be extra safe around crashes if (Ruqola::self()->loginStatus() == DDPClient::LoggedIn) { Room r; r.id = roomID; r.name = room["name"].toString(); r.topic = room["topic"].toString(); qDebug() << "Adding room" << r.name << r.id << r.topic; model->addRoom(r); } QJsonArray params; params.append(QJsonValue(roomID)); Ruqola::self()->ddp()->subscribe("stream-room-messages", params); // Load history params.append(QJsonValue(QJsonValue::Null)); params.append(QJsonValue(50)); // Max number of messages to load; QJsonObject dateObject; dateObject["$date"] = QJsonValue(roomModel->lastTimestamp()); params.append(dateObject); Ruqola::self()->ddp()->method("loadHistory", QJsonDocument(params), process_backlog); } } } 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.messageID = o.value("_id").toString(); - m.roomID = roomId; + m.roomID = roomId; + m.message = o.value("msg").toString(); m.timestamp = (qint64)o.value("ts").toObject().value("$date").toDouble(); - + m.username = o.value("u").toObject().value("username").toString(); + m.userID = o.value("u").toObject().value("_id").toString(); + m.updatedAt = o.value("_updatedAt").toObject().value("$date").toDouble(); + m.editedAt = o.value("editedAt").toObject().value("$date").toDouble(); + m.editedByUsername = o.value("editedBy").toObject().value("username").toString(); + m.editedByUserID = o.value("editedBy").toObject().value("userID").toString(); + m.url = o.value("urls").toObject().value("url").toString(); + m.meta = o.value("urls").toObject().value("meta").toString(); + m.headers = o.value("urls").toObject().value("headers").toString(); + m.parsedUrl = o.value("urls").toObject().value("parsedUrl").toString(); + m.image_url = o.value("attachments").toObject().value("image_url").toString(); + m.color = o.value("attachments").toObject().value("color").toString(); + m.alias = o.value("alias").toString(); + m.avatar = o.value("avatar").toString(); + m.groupable = o.value("groupable").toBool(); + m.parseUrls = o.value("parseUrls").toBool(); + if (!type.isEmpty()) { m.systemMessage = true; m.systemMessageType = type; } else { m.systemMessage = false; } Ruqola::self()->getModelForRoom(roomId)->addMessage(m); // qDebug() << "RocketChatBackend::processIncomingMessages sending notification"; // //Send notifications only when user is logged in // if ( Ruqola::self()->loginStatus() == DDPClient::LoggedIn) { // QString userName = m.username; // QString message = m.message; // QString param = QString("%1 \n %2").arg(userName).arg(message); // Ruqola::self()->notification()->setMessage(param); // } else { // qDebug() << m.username << " recieved message: " << m.message; // } } } RocketChatBackend::RocketChatBackend(QObject* parent) : QObject(parent) { connect(Ruqola::self(), &Ruqola::loginStatusChanged, this, &RocketChatBackend::onLoginStatusChanged); connect(Ruqola::self(), &Ruqola::userIDChanged, this, &RocketChatBackend::onUserIDChanged); connect(Ruqola::self()->ddp(), &DDPClient::changed, this, &RocketChatBackend::onChanged); connect(Ruqola::self()->ddp(), &DDPClient::added, this, &RocketChatBackend::onAdded); } RocketChatBackend::~RocketChatBackend() { } void RocketChatBackend::onLoginStatusChanged() { if (Ruqola::self()->loginStatus() == DDPClient::LoggedIn) { qDebug() << "GETTING LIST OF ROOMS"; // Ruqola::self()->ddp()->method("subscriptions/get", QJsonDocument::fromJson("{\"$date\": 0}"), rooms_callback); QJsonObject params; params["$date"] = QJsonValue(0); // get ALL rooms we've ever seen Ruqola::self()->ddp()->method("rooms/get", QJsonDocument(params), rooms_callback); // Ruqola::self()->ddp()->subscribe("stream-room-messages", QJsonDocument::fromJson(params.toLatin1())); } } void RocketChatBackend::onLoggedIn() { // if (Ruqola::self()->loginStatus() != DDPClient::LoggedIn) { // qDebug() << "not yet logged in:" << Ruqola::self()->loginStatus(); // return; // } // // get list of rooms // Ruqola::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") { if (object["username"].isNull()) { // it's us! get ID Ruqola::self()->setUserID(object["id"].toString()); } qDebug() << "NEW USER ADDED: " << object.value("userName").toString(); } else if (collection == "rooms") { } else if (collection == "stream-notify-user"){ } } void RocketChatBackend::onChanged(QJsonObject object) { QString collection = object["collection"].toString(); // qDebug() << "ROCKET CHAT BACK onChanged" << 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(); processIncomingMessages(contents); } else if (collection == "users") { qDebug() << "USER CHANGED"; } else if (collection == "rooms") { } else if (collection == "stream-notify-user") { QJsonObject fields = object.value("fields").toObject(); QJsonArray contents = fields.value("args").toArray(); -// Ruqola::self()->notification()->setMessage(contents.at(0).toObject()["text"].toString()); QString message = contents.at(0).toObject()["text"].toString(); Ruqola::self()->notification()->showMessage("New message", message, QSystemTrayIcon::Information, 5000 ); - qDebug() << "New notification" << object.value("fields").toObject(); } } void RocketChatBackend::onUserIDChanged() { qDebug() << "subscribing to notification feed"; QJsonArray params; params.append(QJsonValue(QString("%1/%2").arg(Ruqola::self()->userID()).arg(QString("notification")))); Ruqola::self()->ddp()->subscribe("stream-notify-user", params); } diff --git a/src/roommodel.cpp b/src/roommodel.cpp index 688673e2..567f7be1 100644 --- a/src/roommodel.cpp +++ b/src/roommodel.cpp @@ -1,233 +1,249 @@ /* * * 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 "ruqola.h" #include #include Room RoomModel::fromJSon(const QJsonObject& o) { Room r; - r.name = o["name"].toString(); r.id = o["id"].toString(); + r.type = o["t"].toString(); + r.name = o["name"].toString(); + r.userName = o["userName"].toString(); + r.userID = o["userID"].toString(); + r.topic = o["topic"].toString(); + r.mutedUsers = o["muted"].toString(); + r.jitsiTimeout = o["jitsiTimeout"].toDouble(); + r.ro = o["ro"].toBool(); 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["t"] = r.type; + o["name"] = r.name; + o["userName"] = r.userName; + o["userID"] = r.userID; + o["topic"] = r.topic; + o["muted"] = r.mutedUsers; + o["jitsiTimeout"] = r.jitsiTimeout; + o["ro"] = r.ro; 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(Ruqola::self(), &Ruqola::loginStatusChanged, this, &RoomModel::onLoginStatusChanged); } RoomModel::~RoomModel() { QDir cacheDir(Ruqola::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; return new RoomWrapper(r); } // Clear data and refill it with data in the cache, if there is void RoomModel::reset() { if (Ruqola::self()->cacheBasePath().isEmpty()) { return; } clear(); QDir cacheDir(Ruqola::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 { Q_UNUSED(parent); // 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 { 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()); 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 3e928269..c79f99dd 100644 --- a/src/roommodel.h +++ b/src/roommodel.h @@ -1,143 +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 . * */ #ifndef ROOMMODEL_H #define ROOMMODEL_H #include #include class Room { 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, topic, id; + + //Room Object Fields + + // _id + QString id; + + // t (can take values "d" , "c" or "p") + QString type; + + // name + QString name; + + // u + QString userName; + QString userID; + + // topic + QString topic; + + // muted - collection of muted users by its usernames + QString mutedUsers; //QStringList + + // jitsiTimeout + qint64 jitsiTimeout; + + // ro - read-only chat or not + bool ro; + int unread; bool selected = false; }; class RoomWrapper : public QObject { Q_PROPERTY(QString name READ getName NOTIFY nameChanged) Q_PROPERTY(QString topic READ getTopic NOTIFY topicChanged) Q_OBJECT public: RoomWrapper(QObject *parent = 0); RoomWrapper(const Room &r, QObject *parent = 0); QString getName() { return m_name; } QString getTopic() { return m_topic; } signals: void nameChanged(); void topicChanged(); 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 + RoomUnread, + RoomType, + RoomUserName, //created by UserName + RoomUserID, + RoomTopic, + RoomMuted, + RoomJitsiTimeout, + RoomRO }; 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/ruqola.cpp b/src/ruqola.cpp index 2b7c5944..ed902311 100644 --- a/src/ruqola.cpp +++ b/src/ruqola.cpp @@ -1,243 +1,268 @@ -/* +/* * * 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 "ruqola.h" #include "roommodel.h" #include "ddpclient.h" #include "notification.h" - +#include +#include +#include +#include Ruqola *Ruqola::m_self = 0; QString Ruqola::authToken() const { return m_authToken; } QString Ruqola::userName() const { return m_userName; } QString Ruqola::userID() const { return m_userID; } QString Ruqola::password() const { return m_password; } void Ruqola::setAuthToken(const QString& token) { qDebug() << "Setting token to" << token; QSettings s; m_authToken = token; s.setValue("authToken", token); } void Ruqola::setPassword(const QString& password) { m_password = password; } void Ruqola::setUserName(const QString& username) { m_userName = username; QSettings s; s.setValue("username", username); emit userNameChanged(); } void Ruqola::setUserID(const QString& userID) { m_userName = userID; QSettings s; s.setValue("userID", userID); emit userIDChanged(); } RoomModel * Ruqola::roomModel() { if (!m_roomModel) { qDebug() << "creating new RoomModel"; m_roomModel = new RoomModel(this); qDebug() << m_roomModel; } return m_roomModel; } DDPClient * Ruqola::ddp() { if (!m_ddp) { m_ddp = new DDPClient(serverURL()); connect(m_ddp, &DDPClient::loginStatusChanged, this, &Ruqola::loginStatusChanged); // connect(m_ddp, &DDPClient::loginStatusChanged, this, [=](){qDebug() << "Signal received";}); } return m_ddp; } Notification * Ruqola::notification() { if (m_notification == NULL) { m_notification = new Notification(); m_notification->show(); } return m_notification; } +void Ruqola::attachmentButtonClicked() +{ + QString fileName = QFileDialog::getOpenFileName(Q_NULLPTR, + "Select one or more files to open", + QDir::homePath(), + "Images (*.png *.jpeg *.jpg)"); + + qDebug() << "Selected Image " << fileName; + + QFile file(fileName); + if (!file.open(QFile::ReadOnly)) { + qDebug() << "Cannot open the selected file"; + return; + } + const QString message = QString::fromLatin1(file.readAll().toBase64()); + const QString roomID("3cGRyFLWgnPL7B79n"); //hard code roomID for now + const QString type("image"); + sendMessage(roomID, message, type); +} -void Ruqola::sendMessage(const QString &roomID, const QString &message) +void Ruqola::sendMessage(const QString &roomID, const QString &message, const QString &type) { - QString json = "{\"rid\": \"%1\", \"msg\": \"%2\"}"; - json = json.arg(roomID, message); - ddp()->method("sendMessage", QJsonDocument::fromJson(json.toUtf8())); + QJsonObject json; + json["rid"] = roomID; + json["msg"] = message; + json["type"] = type; + + ddp()->method("sendMessage", QJsonDocument(json)); } MessageModel * Ruqola::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 Ruqola::serverURL() const { return m_serverURL; } void Ruqola::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 Ruqola::loginStatus() { if (m_ddp) { return ddp()->loginStatus(); } else { return DDPClient::LoggedOut; } } void Ruqola::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 Ruqola::logOut() { setAuthToken(QString()); setPassword(QString()); 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(); } QString Ruqola::cacheBasePath() const { if (m_serverURL.isEmpty()) { return QString(); } return QStandardPaths::writableLocation(QStandardPaths::CacheLocation)+'/'+m_serverURL; } // QString Ruqola::activeRoom() const // { // return m_activeRoom; // } // void Ruqola::setActiveRoom(const QString& activeRoom) // { // m_activeRoom = activeRoom; // // roomModel()->setActiveRoom(activeRoom); // emit activeRoomChanged(); // } RoomWrapper * Ruqola::getRoom(const QString& roomID) { return roomModel()->findRoom(roomID); } Ruqola::Ruqola(QObject* parent): QObject(parent), m_ddp(0), m_roomModel(0), m_notification(0) { QSettings s; m_serverURL = s.value("serverURL", "demo.rocket.chat").toString(); m_userName = s.value("username").toString(); m_userID = s.value("userID").toString(); m_authToken = s.value("authToken").toString(); } Ruqola * Ruqola::self() { if (!m_self) { m_self = new Ruqola; // Create DDP object so we try to connect at startup m_self->ddp(); // Clear rooms data and refill it with data in the cache, if there is m_self->roomModel()->reset(); // Create systray to show notifications m_self->notification(); } return m_self; } diff --git a/src/ruqola.h b/src/ruqola.h index 55695dd8..00593a35 100644 --- a/src/ruqola.h +++ b/src/ruqola.h @@ -1,118 +1,120 @@ /* * * 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" #include "notification.h" class QString; class Ruqola: public QObject { Q_OBJECT Q_PROPERTY(QString userName READ userName WRITE setUserName NOTIFY userNameChanged) Q_PROPERTY(QString userID READ userID WRITE setUserID NOTIFY userIDChanged) 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 Ruqola* self(); void setUserName(const QString &username); QString userName() const; void setUserID(const QString &userID); QString userID() 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(); Notification * notification(); Q_INVOKABLE RoomModel *roomModel(); - Q_INVOKABLE void sendMessage(const QString &roomID, const QString &message); + Q_INVOKABLE void sendMessage(const QString &roomID, const QString &message, const QString &type); Q_INVOKABLE MessageModel* getModelForRoom(const QString &roomID); Q_INVOKABLE void tryLogin(); Q_INVOKABLE void logOut(); Q_INVOKABLE RoomWrapper* getRoom(const QString &roomID); -// void setRoomModel(); + + Q_INVOKABLE void attachmentButtonClicked(); QString cacheBasePath() const; signals: void userNameChanged(); void userIDChanged(); void serverURLChanged(); void loginStatusChanged(); private: Ruqola(QObject *parent = 0); static Ruqola *m_self; QString m_password; QString m_userName; QString m_userID; QString m_authToken; QString m_serverURL; DDPClient *m_ddp; RoomModel *m_roomModel; Notification *m_notification; QHash< QString, MessageModel * > m_messageModels; }; inline static QObject *ruqola_singletontype_provider(QQmlEngine *engine, QJSEngine *scriptEngine) { Q_UNUSED(engine) Q_UNUSED(scriptEngine) Ruqola *userData = Ruqola::self(); return userData; } #endif // USERDATA_H