diff --git a/CHANGELOG.md b/CHANGELOG.md index 83c759e..f4fb579 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,28 @@ # Changelog -## Squawk 0.2.1 (UNRELEASED) +## Squawk 0.2.2 (May 05, 2022) +### Bug fixes +- now when you remove an account it actually gets removed +- segfault on unitialized Availability in some rare occesions +- fixed crash when you open a dialog with someone that has only error messages in archive +- message height is now calculated correctly on Chinese and Japanese paragraphs +- the app doesn't crash on SIGINT anymore + +### Improvements +- there is a way to disable an account and it wouldn't connect when you change availability +- if you cancel password query an account becomes inactive and doesn't annoy you anymore +- if you filled password field and chose KWallet as a storage Squawk wouldn't ask you again for the same password +- if left the password field empty and chose KWallet as a storage Squawk will try to get that passord from KWallet before asking you to input it +- accounts now connect to the server asyncronously - if one is stopped on password prompt another is connecting +- actualized translations, added English localization file + +### New features +- new "About" window with links, license, gratitudes +- if the authentication failed Squawk will ask againg for your password and login +- now there is an amount of unread messages showing on top of Squawk launcher icon +- notifications now have buttons to open a conversation or to mark that message as read + +## Squawk 0.2.1 (Apr 02, 2022) ### Bug fixes - build in release mode now no longer spams warnings - build now correctly installs all build plugin libs @@ -11,12 +33,13 @@ ### Improvements - reduced amount of places where platform specific path separator is used - now message input is automatically focused when you open a dialog or a room +- what() method on unhandled exception now actually tells what happened ### New features - the settings are here! You con config different stuff from there - now it's possible to set up different qt styles from settings -- if you have KConfig nad KConfigWidgets packages installed - you can chose from global color schemes -- it's possible now to chose a folder where squawk is going to store downloaded files +- if you have KConfig nad KConfigWidgets packages installed - you can choose from global color schemes +- it's possible now to choose a folder where squawk is going to store downloaded files - now you can correct your message ## Squawk 0.2.0 (Jan 10, 2022) diff --git a/CMakeLists.txt b/CMakeLists.txt index 717cf91..75011d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.4) -project(squawk VERSION 0.2.1 LANGUAGES CXX) +project(squawk VERSION 0.2.2 LANGUAGES CXX) cmake_policy(SET CMP0076 NEW) cmake_policy(SET CMP0079 NEW) @@ -148,6 +148,7 @@ if(CMAKE_COMPILER_IS_GNUCXX) target_compile_options(squawk PRIVATE ${COMPILE_OPTIONS}) endif(CMAKE_COMPILER_IS_GNUCXX) +add_subdirectory(main) add_subdirectory(core) add_subdirectory(external/simpleCrypt) add_subdirectory(packaging) @@ -159,6 +160,8 @@ add_subdirectory(ui) # Install the executable install(TARGETS squawk DESTINATION ${CMAKE_INSTALL_BINDIR}) +install(FILES README.md DESTINATION ${CMAKE_INSTALL_DATADIR}/macaw.me/squawk) +install(FILES LICENSE.md DESTINATION ${CMAKE_INSTALL_DATADIR}/macaw.me/squawk) if (CMAKE_BUILD_TYPE STREQUAL "Release") if (APPLE) diff --git a/LICENSE.md b/LICENSE.md index 85c7c69..32b38d4 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -595,17 +595,17 @@ pointer to where the full notice is found. Copyright (C) - + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - + You should have received a copy of the GNU General Public License along with this program. If not, see . diff --git a/README.md b/README.md index 486d4fe..3e20568 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![AUR version](https://img.shields.io/aur/version/squawk?style=flat-square)](https://aur.archlinux.org/packages/squawk/) [![Liberapay patrons](https://img.shields.io/liberapay/patrons/macaw.me?logo=liberapay&style=flat-square)](https://liberapay.com/macaw.me) -![Squawk screenshot](https://macaw.me/images/squawk/0.2.0.png) +![Squawk screenshot](https://macaw.me/images/squawk/0.2.2.png) ### Prerequisites diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 9369cb7..6c7a3b5 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -6,14 +6,12 @@ endif(WIN32) target_sources(squawk PRIVATE account.cpp account.h - adapterFuctions.cpp - archive.cpp - archive.h + adapterfunctions.cpp + adapterfunctions.h conference.cpp conference.h contact.cpp contact.h - main.cpp networkaccess.cpp networkaccess.h rosteritem.cpp @@ -22,13 +20,10 @@ target_sources(squawk PRIVATE signalcatcher.h squawk.cpp squawk.h - storage.cpp - storage.h - urlstorage.cpp - urlstorage.h ) target_include_directories(squawk PRIVATE ${LMDB_INCLUDE_DIRS}) add_subdirectory(handlers) +add_subdirectory(storage) add_subdirectory(passwordStorageEngines) diff --git a/core/account.cpp b/core/account.cpp index 91d0f2b..3b9d7ec 100644 --- a/core/account.cpp +++ b/core/account.cpp @@ -22,7 +22,7 @@ using namespace Core; -Account::Account(const QString& p_login, const QString& p_server, const QString& p_password, const QString& p_name, NetworkAccess* p_net, QObject* parent): +Account::Account(const QString& p_login, const QString& p_server, const QString& p_password, const QString& p_name, bool p_active, NetworkAccess* p_net, QObject* parent): QObject(parent), name(p_name), archiveQueries(), @@ -41,13 +41,15 @@ Account::Account(const QString& p_login, const QString& p_server, const QString& rcpm(new QXmppMessageReceiptManager()), reconnectScheduled(false), reconnectTimer(new QTimer), - avatarHash(), - avatarType(), - ownVCardRequestInProgress(false), network(p_net), passwordType(Shared::AccountPassword::plain), + lastError(Error::none), + pepSupport(false), + active(p_active), + notReadyPassword(false), mh(new MessageHandler(this)), - rh(new RosterHandler(this)) + rh(new RosterHandler(this)), + vh(new VCardHandler(this)) { config.setUser(p_login); config.setDomain(p_server); @@ -73,10 +75,6 @@ Account::Account(const QString& p_login, const QString& p_server, const QString& client.addExtension(mm); client.addExtension(bm); - - QObject::connect(vm, &QXmppVCardManager::vCardReceived, this, &Account::onVCardReceived); - //QObject::connect(&vm, &QXmppVCardManager::clientVCardReceived, this, &Account::onOwnVCardReceived); //for some reason it doesn't work, launching from common handler - client.addExtension(um); QObject::connect(um, &QXmppUploadRequestManager::slotReceived, mh, &MessageHandler::onUploadSlotReceived); QObject::connect(um, &QXmppUploadRequestManager::requestFailed, mh, &MessageHandler::onUploadSlotRequestFailed); @@ -91,62 +89,18 @@ Account::Account(const QString& p_login, const QString& p_server, const QString& client.addExtension(rcpm); QObject::connect(rcpm, &QXmppMessageReceiptManager::messageDelivered, mh, &MessageHandler::onReceiptReceived); - - QString path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); - path += "/" + name; - QDir dir(path); - - if (!dir.exists()) { - bool res = dir.mkpath(path); - if (!res) { - qDebug() << "Couldn't create a cache directory for account" << name; - throw 22; - } - } - - QFile* avatar = new QFile(path + "/avatar.png"); - QString type = "png"; - if (!avatar->exists()) { - delete avatar; - avatar = new QFile(path + "/avatar.jpg"); - type = "jpg"; - if (!avatar->exists()) { - delete avatar; - avatar = new QFile(path + "/avatar.jpeg"); - type = "jpeg"; - if (!avatar->exists()) { - delete avatar; - avatar = new QFile(path + "/avatar.gif"); - type = "gif"; - } - } - } - - if (avatar->exists()) { - if (avatar->open(QFile::ReadOnly)) { - QCryptographicHash sha1(QCryptographicHash::Sha1); - sha1.addData(avatar); - avatarHash = sha1.result(); - avatarType = type; - } - } - if (avatarType.size() != 0) { - presence.setVCardUpdateType(QXmppPresence::VCardUpdateValidPhoto); - presence.setPhotoHash(avatarHash.toUtf8()); - } else { - presence.setVCardUpdateType(QXmppPresence::VCardUpdateNotReady); - } - reconnectTimer->setSingleShot(true); QObject::connect(reconnectTimer, &QTimer::timeout, this, &Account::onReconnectTimer); -// QXmppLogger* logger = new QXmppLogger(this); -// logger->setLoggingType(QXmppLogger::SignalLogging); -// client.setLogger(logger); -// -// QObject::connect(logger, &QXmppLogger::message, this, [](QXmppLogger::MessageType type, const QString& text){ -// qDebug() << text; -// }); + if (name == "Test") { + QXmppLogger* logger = new QXmppLogger(this); + logger->setLoggingType(QXmppLogger::SignalLogging); + client.setLogger(logger); + + QObject::connect(logger, &QXmppLogger::message, this, [](QXmppLogger::MessageType type, const QString& text){ + qDebug() << text; + }); + } } Account::~Account() @@ -160,6 +114,7 @@ Account::~Account() QObject::disconnect(network, &NetworkAccess::downloadFileComplete, mh, &MessageHandler::onDownloadFileComplete); QObject::disconnect(network, &NetworkAccess::loadFileError, mh, &MessageHandler::onLoadFileError); + delete vh; delete mh; delete rh; @@ -185,7 +140,12 @@ void Core::Account::connect() reconnectTimer->stop(); } if (state == Shared::ConnectionState::disconnected) { - client.connectToServer(config, presence); + if (notReadyPassword) { + emit needPassword(); + } else { + client.connectToServer(config, presence); + } + } else { qDebug("An attempt to connect an account which is already connected, skipping"); } @@ -224,6 +184,7 @@ void Core::Account::onClientStateChange(QXmppClient::State st) dm->requestItems(getServer()); dm->requestInfo(getServer()); } + lastError = Error::none; emit connectionStateChanged(state); } } else { @@ -255,45 +216,17 @@ void Core::Account::onClientStateChange(QXmppClient::State st) void Core::Account::reconnect() { - if (state == Shared::ConnectionState::connected && !reconnectScheduled) { - reconnectScheduled = true; - reconnectTimer->start(500); - client.disconnectFromServer(); - } else { - qDebug() << "An attempt to reconnect account" << getName() << "which was not connected"; + if (!reconnectScheduled) { //TODO define behavior if It was connection or disconnecting + if (state == Shared::ConnectionState::connected) { + reconnectScheduled = true; + reconnectTimer->start(500); + client.disconnectFromServer(); + } else { + qDebug() << "An attempt to reconnect account" << getName() << "which was not connected"; + } } } -QString Core::Account::getName() const { - return name;} - -QString Core::Account::getLogin() const { - return config.user();} - -QString Core::Account::getPassword() const { - return config.password();} - -QString Core::Account::getServer() const { - return config.domain();} - -Shared::AccountPassword Core::Account::getPasswordType() const { - return passwordType;} - -void Core::Account::setPasswordType(Shared::AccountPassword pt) { - passwordType = pt; } - -void Core::Account::setLogin(const QString& p_login) { - config.setUser(p_login);} - -void Core::Account::setName(const QString& p_name) { - name = p_name;} - -void Core::Account::setPassword(const QString& p_password) { - config.setPassword(p_password);} - -void Core::Account::setServer(const QString& p_server) { - config.setDomain(p_server);} - Shared::Availability Core::Account::getAvailability() const { if (state == Shared::ConnectionState::connected) { @@ -325,32 +258,11 @@ void Core::Account::onPresenceReceived(const QXmppPresence& p_presence) QString jid = comps.front().toLower(); QString resource = comps.back(); - QString myJid = getLogin() + "@" + getServer(); - - if (jid == myJid) { + if (jid == getBareJid()) { if (resource == getResource()) { emit availabilityChanged(static_cast(p_presence.availableStatusType())); } else { - if (!ownVCardRequestInProgress) { - switch (p_presence.vCardUpdateType()) { - case QXmppPresence::VCardUpdateNone: //this presence has nothing to do with photo - break; - case QXmppPresence::VCardUpdateNotReady: //let's say the photo didn't change here - break; - case QXmppPresence::VCardUpdateNoPhoto: //there is no photo, need to drop if any - if (avatarType.size() > 0) { - vm->requestClientVCard(); - ownVCardRequestInProgress = true; - } - break; - case QXmppPresence::VCardUpdateValidPhoto: //there is a photo, need to load - if (avatarHash != p_presence.photoHash()) { - vm->requestClientVCard(); - ownVCardRequestInProgress = true; - } - break; - } - } + vh->handleOtherPresenceOfMyAccountChange(p_presence); } } else { RosterItem* item = rh->getRosterItem(jid); @@ -392,18 +304,6 @@ void Core::Account::onPresenceReceived(const QXmppPresence& p_presence) } } -QString Core::Account::getResource() const { - return config.resource();} - -void Core::Account::setResource(const QString& p_resource) { - config.setResource(p_resource);} - -QString Core::Account::getFullJid() const { - return getLogin() + "@" + getServer() + "/" + getResource();} - -void Core::Account::sendMessage(const Shared::Message& data) { - mh->sendMessage(data);} - void Core::Account::onMamMessageReceived(const QString& queryId, const QXmppMessage& msg) { if (msg.id().size() > 0 && (msg.body().size() > 0 || msg.outOfBandUrl().size() > 0)) { @@ -517,6 +417,7 @@ void Core::Account::onClientError(QXmppClient::Error err) qDebug() << "Error"; QString errorText; QString errorType; + lastError = Error::other; switch (err) { case QXmppClient::SocketError: errorText = client.socketErrorString(); @@ -558,6 +459,7 @@ void Core::Account::onClientError(QXmppClient::Error err) break; case QXmppStanza::Error::NotAuthorized: errorText = "Authentication error"; + lastError = Error::authentication; break; #if (QXMPP_VERSION) < QT_VERSION_CHECK(1, 3, 0) case QXmppStanza::Error::PaymentRequired: @@ -667,6 +569,166 @@ void Core::Account::setRoomJoined(const QString& jid, bool joined) conf->setJoined(joined); } +void Core::Account::onDiscoveryItemsReceived(const QXmppDiscoveryIq& items) +{ + if (items.from() == getServer()) { + std::set needToRequest; + qDebug() << "Server items list received for account " << name << ":"; + for (QXmppDiscoveryIq::Item item : items.items()) { + QString jid = item.jid(); + if (jid != getServer()) { + qDebug() << " Node" << jid; + needToRequest.insert(jid); + } else { + qDebug() << " " << item.node().toStdString().c_str(); + } + } + + for (const QString& jid : needToRequest) { + dm->requestInfo(jid); + } + } +} + +void Core::Account::onDiscoveryInfoReceived(const QXmppDiscoveryIq& info) +{ + if (info.from() == getServer()) { + bool enableCC = false; + qDebug() << "Server info received for account" << name; + QStringList features = info.features(); + qDebug() << "List of supported features of the server " << getServer() << ":"; + for (const QString& feature : features) { + qDebug() << " " << feature.toStdString().c_str(); + if (feature == "urn:xmpp:carbons:2") { + enableCC = true; + } + } + + if (enableCC) { + qDebug() << "Enabling carbon copies for account" << name; + cm->setCarbonsEnabled(true); + } + + qDebug() << "Requesting account" << name << "capabilities"; + dm->requestInfo(getBareJid()); + } else if (info.from() == getBareJid()) { + qDebug() << "Received capabilities for account" << name << ":"; + QList identities = info.identities(); + bool pepSupported = false; + for (const QXmppDiscoveryIq::Identity& identity : identities) { + QString type = identity.type(); + qDebug() << " " << identity.category() << type; + if (type == "pep") { + pepSupported = true; + } + } + rh->setPepSupport(pepSupported); + } else { + qDebug() << "Received info for account" << name << "about" << info.from(); + QList identities = info.identities(); + for (const QXmppDiscoveryIq::Identity& identity : identities) { + qDebug() << " " << identity.name() << identity.category() << identity.type(); + } + } +} + +void Core::Account::handleDisconnection() +{ + cm->setCarbonsEnabled(false); + rh->handleOffline(); + vh->handleOffline(); + archiveQueries.clear(); +} + +void Core::Account::onContactHistoryResponse(const std::list& list, bool last) +{ + RosterItem* contact = static_cast(sender()); + + qDebug() << "Collected history for contact " << contact->jid << list.size() << "elements"; + if (last) { + qDebug() << "The response contains the first accounted message"; + } + emit responseArchive(contact->jid, list, last); +} + +bool Core::Account::getActive() const { + return active;} + +void Core::Account::setActive(bool p_active) { + if (active != p_active) { + active = p_active; + + emit changed({ + {"active", active} + }); + } +} + +QString Core::Account::getResource() const { + return config.resource();} + +void Core::Account::setResource(const QString& p_resource) { + config.setResource(p_resource);} + +QString Core::Account::getBareJid() const { + return getLogin() + "@" + getServer();} + +QString Core::Account::getFullJid() const { + return getBareJid() + "/" + getResource();} + +QString Core::Account::getName() const { + return name;} + +QString Core::Account::getLogin() const { + return config.user();} + +QString Core::Account::getPassword() const { + return config.password();} + +QString Core::Account::getServer() const { + return config.domain();} + +Shared::AccountPassword Core::Account::getPasswordType() const { + return passwordType;} + +void Core::Account::setPasswordType(Shared::AccountPassword pt) { + passwordType = pt; } + +void Core::Account::setLogin(const QString& p_login) { + config.setUser(p_login);} + +void Core::Account::setName(const QString& p_name) { + name = p_name;} + +void Core::Account::setPassword(const QString& p_password) { + config.setPassword(p_password); + notReadyPassword = false; +} + +void Core::Account::setServer(const QString& p_server) { + config.setDomain(p_server);} + +void Core::Account::sendMessage(const Shared::Message& data) { + mh->sendMessage(data);} + +void Core::Account::requestChangeMessage(const QString& jid, const QString& messageId, const QMap& data){ + mh->requestChangeMessage(jid, messageId, data);} + +void Core::Account::resendMessage(const QString& jid, const QString& id) { + mh->resendMessage(jid, id);} + +void Core::Account::replaceMessage(const QString& originalId, const Shared::Message& data) { + mh->sendMessage(data, false, originalId);} + +void Core::Account::requestVCard(const QString& jid) { + vh->requestVCard(jid);} + +void Core::Account::uploadVCard(const Shared::VCard& card) { + vh->uploadVCard(card);} + +QString Core::Account::getAvatarPath() const { + return vh->getAvatarPath();} + void Core::Account::removeRoomRequest(const QString& jid){ rh->removeRoomRequest(jid);} @@ -689,253 +751,9 @@ void Core::Account::renameContactRequest(const QString& jid, const QString& newN } } -void Core::Account::onVCardReceived(const QXmppVCardIq& card) -{ - QString id = card.from(); - QStringList comps = id.split("/"); - QString jid = comps.front().toLower(); - QString resource(""); - if (comps.size() > 1) { - resource = comps.back(); - } - pendingVCardRequests.erase(id); - RosterItem* item = rh->getRosterItem(jid); - - if (item == 0) { - if (jid == getLogin() + "@" + getServer()) { - onOwnVCardReceived(card); - } else { - qDebug() << "received vCard" << jid << "doesn't belong to any of known contacts or conferences, skipping"; - } - return; - } - - Shared::VCard vCard = item->handleResponseVCard(card, resource); - - emit receivedVCard(jid, vCard); -} +void Core::Account::invalidatePassword() { + notReadyPassword = true;} -void Core::Account::onOwnVCardReceived(const QXmppVCardIq& card) -{ - QByteArray ava = card.photo(); - bool avaChanged = false; - QString path = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" + name + "/"; - if (ava.size() > 0) { - QCryptographicHash sha1(QCryptographicHash::Sha1); - sha1.addData(ava); - QString newHash(sha1.result()); - QMimeDatabase db; - QMimeType newType = db.mimeTypeForData(ava); - if (avatarType.size() > 0) { - if (avatarHash != newHash) { - QString oldPath = path + "avatar." + avatarType; - QFile oldAvatar(oldPath); - bool oldToRemove = false; - if (oldAvatar.exists()) { - if (oldAvatar.rename(oldPath + ".bak")) { - oldToRemove = true; - } else { - qDebug() << "Received new avatar for account" << name << "but can't get rid of the old one, doing nothing"; - } - } - QFile newAvatar(path + "avatar." + newType.preferredSuffix()); - if (newAvatar.open(QFile::WriteOnly)) { - newAvatar.write(ava); - newAvatar.close(); - avatarHash = newHash; - avatarType = newType.preferredSuffix(); - avaChanged = true; - } else { - qDebug() << "Received new avatar for account" << name << "but can't save it"; - if (oldToRemove) { - qDebug() << "rolling back to the old avatar"; - if (!oldAvatar.rename(oldPath)) { - qDebug() << "Couldn't roll back to the old avatar in account" << name; - } - } - } - } - } else { - QFile newAvatar(path + "avatar." + newType.preferredSuffix()); - if (newAvatar.open(QFile::WriteOnly)) { - newAvatar.write(ava); - newAvatar.close(); - avatarHash = newHash; - avatarType = newType.preferredSuffix(); - avaChanged = true; - } else { - qDebug() << "Received new avatar for account" << name << "but can't save it"; - } - } - } else { - if (avatarType.size() > 0) { - QFile oldAvatar(path + "avatar." + avatarType); - if (!oldAvatar.remove()) { - qDebug() << "Received vCard for account" << name << "without avatar, but can't get rid of the file, doing nothing"; - } else { - avatarType = ""; - avatarHash = ""; - avaChanged = true; - } - } - } - - if (avaChanged) { - QMap change; - if (avatarType.size() > 0) { - presence.setPhotoHash(avatarHash.toUtf8()); - presence.setVCardUpdateType(QXmppPresence::VCardUpdateValidPhoto); - change.insert("avatarPath", path + "avatar." + avatarType); - } else { - presence.setPhotoHash(""); - presence.setVCardUpdateType(QXmppPresence::VCardUpdateNoPhoto); - change.insert("avatarPath", ""); - } - client.setClientPresence(presence); - emit changed(change); - } - - ownVCardRequestInProgress = false; - - Shared::VCard vCard; - initializeVCard(vCard, card); - - if (avatarType.size() > 0) { - vCard.setAvatarType(Shared::Avatar::valid); - vCard.setAvatarPath(path + "avatar." + avatarType); - } else { - vCard.setAvatarType(Shared::Avatar::empty); - } - - emit receivedVCard(getLogin() + "@" + getServer(), vCard); -} - -QString Core::Account::getAvatarPath() const -{ - if (avatarType.size() == 0) { - return ""; - } else { - return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" + name + "/" + "avatar." + avatarType; - } -} - -void Core::Account::requestVCard(const QString& jid) -{ - if (pendingVCardRequests.find(jid) == pendingVCardRequests.end()) { - qDebug() << "requesting vCard" << jid; - if (jid == getLogin() + "@" + getServer()) { - if (!ownVCardRequestInProgress) { - vm->requestClientVCard(); - ownVCardRequestInProgress = true; - } - } else { - vm->requestVCard(jid); - pendingVCardRequests.insert(jid); - } - } -} - -void Core::Account::uploadVCard(const Shared::VCard& card) -{ - QXmppVCardIq iq; - initializeQXmppVCard(iq, card); - - if (card.getAvatarType() != Shared::Avatar::empty) { - QString newPath = card.getAvatarPath(); - QString oldPath = getAvatarPath(); - QByteArray data; - QString type; - if (newPath != oldPath) { - QFile avatar(newPath); - if (!avatar.open(QFile::ReadOnly)) { - qDebug() << "An attempt to upload new vCard to account" << name - << "but it wasn't possible to read file" << newPath - << "which was supposed to be new avatar, uploading old avatar"; - if (avatarType.size() > 0) { - QFile oA(oldPath); - if (!oA.open(QFile::ReadOnly)) { - qDebug() << "Couldn't read old avatar of account" << name << ", uploading empty avatar"; - } else { - data = oA.readAll(); - } - } - } else { - data = avatar.readAll(); - } - } else { - if (avatarType.size() > 0) { - QFile oA(oldPath); - if (!oA.open(QFile::ReadOnly)) { - qDebug() << "Couldn't read old avatar of account" << name << ", uploading empty avatar"; - } else { - data = oA.readAll(); - } - } - } - - if (data.size() > 0) { - QMimeDatabase db; - type = db.mimeTypeForData(data).name(); - iq.setPhoto(data); - iq.setPhotoType(type); - } - } - - vm->setClientVCard(iq); - onOwnVCardReceived(iq); -} - -void Core::Account::onDiscoveryItemsReceived(const QXmppDiscoveryIq& items) -{ - for (QXmppDiscoveryIq::Item item : items.items()) { - if (item.jid() != getServer()) { - dm->requestInfo(item.jid()); - } - } -} - -void Core::Account::onDiscoveryInfoReceived(const QXmppDiscoveryIq& info) -{ - qDebug() << "Discovery info received for account" << name; - if (info.from() == getServer()) { - if (info.features().contains("urn:xmpp:carbons:2")) { - qDebug() << "Enabling carbon copies for account" << name; - cm->setCarbonsEnabled(true); - } - } -} - -void Core::Account::handleDisconnection() -{ - cm->setCarbonsEnabled(false); - rh->handleOffline(); - archiveQueries.clear(); - pendingVCardRequests.clear(); - Shared::VCard vCard; //just to show, that there is now more pending request - for (const QString& jid : pendingVCardRequests) { - emit receivedVCard(jid, vCard); //need to show it better in the future, like with an error - } - pendingVCardRequests.clear(); - ownVCardRequestInProgress = false; -} - -void Core::Account::onContactHistoryResponse(const std::list& list, bool last) -{ - RosterItem* contact = static_cast(sender()); - - qDebug() << "Collected history for contact " << contact->jid << list.size() << "elements"; - if (last) { - qDebug() << "The response contains the first accounted message"; - } - emit responseArchive(contact->jid, list, last); -} - -void Core::Account::requestChangeMessage(const QString& jid, const QString& messageId, const QMap& data){ - mh->requestChangeMessage(jid, messageId, data);} - -void Core::Account::resendMessage(const QString& jid, const QString& id) { - mh->resendMessage(jid, id);} - -void Core::Account::replaceMessage(const QString& originalId, const Shared::Message& data) { - mh->sendMessage(data, false, originalId);} +Core::Account::Error Core::Account::getLastError() const { + return lastError;} diff --git a/core/account.h b/core/account.h index 664b547..2c9ec70 100644 --- a/core/account.h +++ b/core/account.h @@ -39,7 +39,6 @@ #include #include #include -#include #include #include @@ -50,6 +49,7 @@ #include "handlers/messagehandler.h" #include "handlers/rosterhandler.h" +#include "handlers/vcardhandler.h" namespace Core { @@ -59,12 +59,20 @@ class Account : public QObject Q_OBJECT friend class MessageHandler; friend class RosterHandler; + friend class VCardHandler; public: + enum class Error { + authentication, + other, + none + }; + Account( const QString& p_login, const QString& p_server, const QString& p_password, const QString& p_name, + bool p_active, NetworkAccess* p_net, QObject* parent = 0); ~Account(); @@ -76,8 +84,12 @@ public: QString getPassword() const; QString getResource() const; QString getAvatarPath() const; + QString getBareJid() const; + QString getFullJid() const; Shared::Availability getAvailability() const; Shared::AccountPassword getPasswordType() const; + Error getLastError() const; + bool getActive() const; void setName(const QString& p_name); void setLogin(const QString& p_login); @@ -86,8 +98,8 @@ public: void setResource(const QString& p_resource); void setAvailability(Shared::Availability avail); void setPasswordType(Shared::AccountPassword pt); - QString getFullJid() const; void sendMessage(const Shared::Message& data); + void setActive(bool p_active); void requestArchive(const QString& jid, int count, const QString& before); void subscribeToContact(const QString& jid, const QString& reason); void unsubscribeFromContact(const QString& jid, const QString& reason); @@ -105,6 +117,7 @@ public: void uploadVCard(const Shared::VCard& card); void resendMessage(const QString& jid, const QString& id); void replaceMessage(const QString& originalId, const Shared::Message& data); + void invalidatePassword(); public slots: void connect(); @@ -137,6 +150,7 @@ signals: void receivedVCard(const QString& jid, const Shared::VCard& card); void uploadFile(const QFileInfo& file, const QUrl& set, const QUrl& get, QMap headers); void uploadFileError(const QString& jid, const QString& messageId, const QString& error); + void needPassword(); private: QString name; @@ -157,16 +171,16 @@ private: bool reconnectScheduled; QTimer* reconnectTimer; - std::set pendingVCardRequests; - - QString avatarHash; - QString avatarType; - bool ownVCardRequestInProgress; NetworkAccess* network; Shared::AccountPassword passwordType; + Error lastError; + bool pepSupport; + bool active; + bool notReadyPassword; MessageHandler* mh; RosterHandler* rh; + VCardHandler* vh; private slots: void onClientStateChange(QXmppClient::State state); @@ -179,9 +193,6 @@ private slots: void onMamResultsReceived(const QString &queryId, const QXmppResultSetReply &resultSetReply, bool complete); void onMamLog(QXmppLogger::MessageType type, const QString &msg); - - void onVCardReceived(const QXmppVCardIq& card); - void onOwnVCardReceived(const QXmppVCardIq& card); void onDiscoveryItemsReceived (const QXmppDiscoveryIq& items); void onDiscoveryInfoReceived (const QXmppDiscoveryIq& info); @@ -191,9 +202,6 @@ private: void handleDisconnection(); void onReconnectTimer(); }; - -void initializeVCard(Shared::VCard& vCard, const QXmppVCardIq& card); -void initializeQXmppVCard(QXmppVCardIq& card, const Shared::VCard& vCard); } diff --git a/core/adapterFuctions.cpp b/core/adapterfunctions.cpp similarity index 98% rename from core/adapterFuctions.cpp rename to core/adapterfunctions.cpp index 3d84dfb..eec5a9f 100644 --- a/core/adapterFuctions.cpp +++ b/core/adapterfunctions.cpp @@ -1,5 +1,5 @@ /* - * Squawk messenger. + * Squawk messenger. * Copyright (C) 2019 Yury Gubich * * This program is free software: you can redistribute it and/or modify @@ -15,10 +15,8 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -#ifndef CORE_ADAPTER_FUNCTIONS_H -#define CORE_ADAPTER_FUNCTIONS_H -#include "account.h" +#include "adapterfunctions.h" void Core::initializeVCard(Shared::VCard& vCard, const QXmppVCardIq& card) { @@ -271,5 +269,3 @@ void Core::initializeQXmppVCard(QXmppVCardIq& iq, const Shared::VCard& card) { iq.setEmails(emails); iq.setPhones(phs); } - -#endif // CORE_ADAPTER_FUNCTIONS_H diff --git a/core/adapterfunctions.h b/core/adapterfunctions.h new file mode 100644 index 0000000..6e50a75 --- /dev/null +++ b/core/adapterfunctions.h @@ -0,0 +1,32 @@ +/* + * Squawk messenger. + * Copyright (C) 2019 Yury Gubich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef CORE_ADAPTER_FUNCTIONS_H +#define CORE_ADAPTER_FUNCTIONS_H + +#include +#include + +namespace Core { + +void initializeVCard(Shared::VCard& vCard, const QXmppVCardIq& card); +void initializeQXmppVCard(QXmppVCardIq& card, const Shared::VCard& vCard); + +} + + +#endif // CORE_ADAPTER_FUNCTIONS_H diff --git a/core/handlers/CMakeLists.txt b/core/handlers/CMakeLists.txt index 6da2ef3..fb67953 100644 --- a/core/handlers/CMakeLists.txt +++ b/core/handlers/CMakeLists.txt @@ -3,4 +3,6 @@ target_sources(squawk PRIVATE messagehandler.h rosterhandler.cpp rosterhandler.h + vcardhandler.cpp + vcardhandler.h ) diff --git a/core/handlers/messagehandler.cpp b/core/handlers/messagehandler.cpp index 0555873..b6d32b9 100644 --- a/core/handlers/messagehandler.cpp +++ b/core/handlers/messagehandler.cpp @@ -176,7 +176,7 @@ void Core::MessageHandler::initializeMessage(Shared::Message& target, const QXmp target.setForwarded(forwarded); if (guessing) { - if (target.getFromJid() == acc->getLogin() + "@" + acc->getServer()) { + if (target.getFromJid() == acc->getBareJid()) { outgoing = true; } else { outgoing = false; diff --git a/core/handlers/rosterhandler.cpp b/core/handlers/rosterhandler.cpp index ce5f1b7..6a233d6 100644 --- a/core/handlers/rosterhandler.cpp +++ b/core/handlers/rosterhandler.cpp @@ -26,7 +26,8 @@ Core::RosterHandler::RosterHandler(Core::Account* account): conferences(), groups(), queuedContacts(), - outOfRosterContacts() + outOfRosterContacts(), + pepSupport(false) { connect(acc->rm, &QXmppRosterManager::rosterReceived, this, &RosterHandler::onRosterReceived); connect(acc->rm, &QXmppRosterManager::itemAdded, this, &RosterHandler::onRosterItemAdded); @@ -51,8 +52,7 @@ Core::RosterHandler::~RosterHandler() void Core::RosterHandler::onRosterReceived() { - acc->vm->requestClientVCard(); //TODO need to make sure server actually supports vCards - acc->ownVCardRequestInProgress = true; + acc->requestVCard(acc->getBareJid()); //TODO need to make sure server actually supports vCards QStringList bj = acc->rm->getRosterBareJids(); for (int i = 0; i < bj.size(); ++i) { @@ -588,4 +588,13 @@ void Core::RosterHandler::handleOffline() pair.second->clearArchiveRequests(); pair.second->downgradeDatabaseState(); } + setPepSupport(false); +} + + +void Core::RosterHandler::setPepSupport(bool support) +{ + if (pepSupport != support) { + pepSupport = support; + } } diff --git a/core/handlers/rosterhandler.h b/core/handlers/rosterhandler.h index b1dfc45..02bbc98 100644 --- a/core/handlers/rosterhandler.h +++ b/core/handlers/rosterhandler.h @@ -64,6 +64,7 @@ public: void storeConferences(); void clearConferences(); + void setPepSupport(bool support); private slots: void onRosterReceived(); @@ -107,6 +108,7 @@ private: std::map> groups; std::map queuedContacts; std::set outOfRosterContacts; + bool pepSupport; }; } diff --git a/core/handlers/vcardhandler.cpp b/core/handlers/vcardhandler.cpp new file mode 100644 index 0000000..2a8d65c --- /dev/null +++ b/core/handlers/vcardhandler.cpp @@ -0,0 +1,312 @@ +// Squawk messenger. +// Copyright (C) 2019 Yury Gubich +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "vcardhandler.h" +#include "core/account.h" + +Core::VCardHandler::VCardHandler(Account* account): + QObject(), + acc(account), + ownVCardRequestInProgress(false), + pendingVCardRequests(), + avatarHash(), + avatarType() +{ + connect(acc->vm, &QXmppVCardManager::vCardReceived, this, &VCardHandler::onVCardReceived); + //for some reason it doesn't work, launching from common handler + //connect(acc->vm, &QXmppVCardManager::clientVCardReceived, this, &VCardHandler::onOwnVCardReceived); + + QString path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); + path += "/" + acc->name; + QDir dir(path); + + if (!dir.exists()) { + bool res = dir.mkpath(path); + if (!res) { + qDebug() << "Couldn't create a cache directory for account" << acc->name; + throw 22; + } + } + + QFile* avatar = new QFile(path + "/avatar.png"); + QString type = "png"; + if (!avatar->exists()) { + delete avatar; + avatar = new QFile(path + "/avatar.jpg"); + type = "jpg"; + if (!avatar->exists()) { + delete avatar; + avatar = new QFile(path + "/avatar.jpeg"); + type = "jpeg"; + if (!avatar->exists()) { + delete avatar; + avatar = new QFile(path + "/avatar.gif"); + type = "gif"; + } + } + } + + if (avatar->exists()) { + if (avatar->open(QFile::ReadOnly)) { + QCryptographicHash sha1(QCryptographicHash::Sha1); + sha1.addData(avatar); + avatarHash = sha1.result(); + avatarType = type; + } + } + if (avatarType.size() != 0) { + acc->presence.setVCardUpdateType(QXmppPresence::VCardUpdateValidPhoto); + acc->presence.setPhotoHash(avatarHash.toUtf8()); + } else { + acc->presence.setVCardUpdateType(QXmppPresence::VCardUpdateNotReady); + } +} + +Core::VCardHandler::~VCardHandler() +{ + +} + +void Core::VCardHandler::onVCardReceived(const QXmppVCardIq& card) +{ + QString id = card.from(); + QStringList comps = id.split("/"); + QString jid = comps.front().toLower(); + QString resource(""); + if (comps.size() > 1) { + resource = comps.back(); + } + pendingVCardRequests.erase(id); + RosterItem* item = acc->rh->getRosterItem(jid); + + if (item == 0) { + if (jid == acc->getBareJid()) { + onOwnVCardReceived(card); + } else { + qDebug() << "received vCard" << jid << "doesn't belong to any of known contacts or conferences, skipping"; + } + return; + } + + Shared::VCard vCard = item->handleResponseVCard(card, resource); + + emit acc->receivedVCard(jid, vCard); +} + +void Core::VCardHandler::onOwnVCardReceived(const QXmppVCardIq& card) +{ + QByteArray ava = card.photo(); + bool avaChanged = false; + QString path = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" + acc->name + "/"; + if (ava.size() > 0) { + QCryptographicHash sha1(QCryptographicHash::Sha1); + sha1.addData(ava); + QString newHash(sha1.result()); + QMimeDatabase db; + QMimeType newType = db.mimeTypeForData(ava); + if (avatarType.size() > 0) { + if (avatarHash != newHash) { + QString oldPath = path + "avatar." + avatarType; + QFile oldAvatar(oldPath); + bool oldToRemove = false; + if (oldAvatar.exists()) { + if (oldAvatar.rename(oldPath + ".bak")) { + oldToRemove = true; + } else { + qDebug() << "Received new avatar for account" << acc->name << "but can't get rid of the old one, doing nothing"; + } + } + QFile newAvatar(path + "avatar." + newType.preferredSuffix()); + if (newAvatar.open(QFile::WriteOnly)) { + newAvatar.write(ava); + newAvatar.close(); + avatarHash = newHash; + avatarType = newType.preferredSuffix(); + avaChanged = true; + } else { + qDebug() << "Received new avatar for account" << acc->name << "but can't save it"; + if (oldToRemove) { + qDebug() << "rolling back to the old avatar"; + if (!oldAvatar.rename(oldPath)) { + qDebug() << "Couldn't roll back to the old avatar in account" << acc->name; + } + } + } + } + } else { + QFile newAvatar(path + "avatar." + newType.preferredSuffix()); + if (newAvatar.open(QFile::WriteOnly)) { + newAvatar.write(ava); + newAvatar.close(); + avatarHash = newHash; + avatarType = newType.preferredSuffix(); + avaChanged = true; + } else { + qDebug() << "Received new avatar for account" << acc->name << "but can't save it"; + } + } + } else { + if (avatarType.size() > 0) { + QFile oldAvatar(path + "avatar." + avatarType); + if (!oldAvatar.remove()) { + qDebug() << "Received vCard for account" << acc->name << "without avatar, but can't get rid of the file, doing nothing"; + } else { + avatarType = ""; + avatarHash = ""; + avaChanged = true; + } + } + } + + if (avaChanged) { + QMap change; + if (avatarType.size() > 0) { + acc->presence.setPhotoHash(avatarHash.toUtf8()); + acc->presence.setVCardUpdateType(QXmppPresence::VCardUpdateValidPhoto); + change.insert("avatarPath", path + "avatar." + avatarType); + } else { + acc->presence.setPhotoHash(""); + acc->presence.setVCardUpdateType(QXmppPresence::VCardUpdateNoPhoto); + change.insert("avatarPath", ""); + } + acc->client.setClientPresence(acc->presence); + emit acc->changed(change); + } + + ownVCardRequestInProgress = false; + + Shared::VCard vCard; + initializeVCard(vCard, card); + + if (avatarType.size() > 0) { + vCard.setAvatarType(Shared::Avatar::valid); + vCard.setAvatarPath(path + "avatar." + avatarType); + } else { + vCard.setAvatarType(Shared::Avatar::empty); + } + + emit acc->receivedVCard(acc->getBareJid(), vCard); +} + +void Core::VCardHandler::handleOffline() +{ + pendingVCardRequests.clear(); + Shared::VCard vCard; //just to show, that there is now more pending request + for (const QString& jid : pendingVCardRequests) { + emit acc->receivedVCard(jid, vCard); //need to show it better in the future, like with an error + } + pendingVCardRequests.clear(); + ownVCardRequestInProgress = false; +} + +void Core::VCardHandler::requestVCard(const QString& jid) +{ + if (pendingVCardRequests.find(jid) == pendingVCardRequests.end()) { + qDebug() << "requesting vCard" << jid; + if (jid == acc->getBareJid()) { + if (!ownVCardRequestInProgress) { + acc->vm->requestClientVCard(); + ownVCardRequestInProgress = true; + } + } else { + acc->vm->requestVCard(jid); + pendingVCardRequests.insert(jid); + } + } +} + +void Core::VCardHandler::handleOtherPresenceOfMyAccountChange(const QXmppPresence& p_presence) +{ + if (!ownVCardRequestInProgress) { + switch (p_presence.vCardUpdateType()) { + case QXmppPresence::VCardUpdateNone: //this presence has nothing to do with photo + break; + case QXmppPresence::VCardUpdateNotReady: //let's say the photo didn't change here + break; + case QXmppPresence::VCardUpdateNoPhoto: //there is no photo, need to drop if any + if (avatarType.size() > 0) { + acc->vm->requestClientVCard(); + ownVCardRequestInProgress = true; + } + break; + case QXmppPresence::VCardUpdateValidPhoto: //there is a photo, need to load + if (avatarHash != p_presence.photoHash()) { + acc->vm->requestClientVCard(); + ownVCardRequestInProgress = true; + } + break; + } + } +} + +void Core::VCardHandler::uploadVCard(const Shared::VCard& card) +{ + QXmppVCardIq iq; + initializeQXmppVCard(iq, card); + + if (card.getAvatarType() != Shared::Avatar::empty) { + QString newPath = card.getAvatarPath(); + QString oldPath = getAvatarPath(); + QByteArray data; + QString type; + if (newPath != oldPath) { + QFile avatar(newPath); + if (!avatar.open(QFile::ReadOnly)) { + qDebug() << "An attempt to upload new vCard to account" << acc->name + << "but it wasn't possible to read file" << newPath + << "which was supposed to be new avatar, uploading old avatar"; + if (avatarType.size() > 0) { + QFile oA(oldPath); + if (!oA.open(QFile::ReadOnly)) { + qDebug() << "Couldn't read old avatar of account" << acc->name << ", uploading empty avatar"; + } else { + data = oA.readAll(); + } + } + } else { + data = avatar.readAll(); + } + } else { + if (avatarType.size() > 0) { + QFile oA(oldPath); + if (!oA.open(QFile::ReadOnly)) { + qDebug() << "Couldn't read old avatar of account" << acc->name << ", uploading empty avatar"; + } else { + data = oA.readAll(); + } + } + } + + if (data.size() > 0) { + QMimeDatabase db; + type = db.mimeTypeForData(data).name(); + iq.setPhoto(data); + iq.setPhotoType(type); + } + } + + acc->vm->setClientVCard(iq); + onOwnVCardReceived(iq); +} + +QString Core::VCardHandler::getAvatarPath() const +{ + if (avatarType.size() == 0) { + return ""; + } else { + return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/" + acc->name + "/" + "avatar." + avatarType; + } +} diff --git a/core/handlers/vcardhandler.h b/core/handlers/vcardhandler.h new file mode 100644 index 0000000..4febb69 --- /dev/null +++ b/core/handlers/vcardhandler.h @@ -0,0 +1,65 @@ +// Squawk messenger. +// Copyright (C) 2019 Yury Gubich +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef CORE_VCARDHANDLER_H +#define CORE_VCARDHANDLER_H + +#include + +#include +#include + +#include + +#include +#include + +/** + * @todo write docs + */ + +namespace Core { + +class Account; + +class VCardHandler : public QObject +{ + Q_OBJECT +public: + VCardHandler(Account* account); + ~VCardHandler(); + + void handleOffline(); + void requestVCard(const QString& jid); + void handleOtherPresenceOfMyAccountChange(const QXmppPresence& p_presence); + void uploadVCard(const Shared::VCard& card); + QString getAvatarPath() const; + +private slots: + void onVCardReceived(const QXmppVCardIq& card); + void onOwnVCardReceived(const QXmppVCardIq& card); + +private: + Account* acc; + + bool ownVCardRequestInProgress; + std::set pendingVCardRequests; + QString avatarHash; + QString avatarType; +}; +} + +#endif // CORE_VCARDHANDLER_H diff --git a/core/main.cpp b/core/main.cpp deleted file mode 100644 index f842c80..0000000 --- a/core/main.cpp +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Squawk messenger. - * Copyright (C) 2019 Yury Gubich - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "../shared/global.h" -#include "../shared/messageinfo.h" -#include "../shared/pathcheck.h" -#include "../ui/squawk.h" -#include "signalcatcher.h" -#include "squawk.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -int main(int argc, char *argv[]) -{ - qRegisterMetaType("Shared::Message"); - qRegisterMetaType("Shared::MessageInfo"); - qRegisterMetaType("Shared::VCard"); - qRegisterMetaType>("std::list"); - qRegisterMetaType>("std::list"); - qRegisterMetaType>("QSet"); - qRegisterMetaType("Shared::ConnectionState"); - qRegisterMetaType("Shared::Availability"); - - QApplication app(argc, argv); - SignalCatcher sc(&app); -#ifdef Q_OS_WIN - // Windows need an organization name for QSettings to work - // https://doc.qt.io/qt-5/qsettings.html#basic-usage - { - const QString& orgName = QApplication::organizationName(); - if (orgName.isNull() || orgName.isEmpty()) { - QApplication::setOrganizationName("squawk"); - } - } -#endif - QApplication::setApplicationName("squawk"); - QApplication::setApplicationDisplayName("Squawk"); - QApplication::setApplicationVersion("0.2.1"); - - QTranslator qtTranslator; - qtTranslator.load("qt_" + QLocale::system().name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath)); - app.installTranslator(&qtTranslator); - - QTranslator myappTranslator; - QStringList shares = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); - bool found = false; - for (QString share : shares) { - found = myappTranslator.load(QLocale(), QLatin1String("squawk"), ".", share + "/l10n"); - if (found) { - break; - } - } - if (!found) { - myappTranslator.load(QLocale(), QLatin1String("squawk"), ".", QCoreApplication::applicationDirPath()); - } - - app.installTranslator(&myappTranslator); - - QIcon icon; - icon.addFile(":images/logo.svg", QSize(16, 16)); - icon.addFile(":images/logo.svg", QSize(24, 24)); - icon.addFile(":images/logo.svg", QSize(32, 32)); - icon.addFile(":images/logo.svg", QSize(48, 48)); - icon.addFile(":images/logo.svg", QSize(64, 64)); - icon.addFile(":images/logo.svg", QSize(96, 96)); - icon.addFile(":images/logo.svg", QSize(128, 128)); - icon.addFile(":images/logo.svg", QSize(256, 256)); - icon.addFile(":images/logo.svg", QSize(512, 512)); - QApplication::setWindowIcon(icon); - - new Shared::Global(); //translates enums - - QSettings settings; - QVariant vs = settings.value("style"); - if (vs.isValid()) { - QString style = vs.toString().toLower(); - if (style != "system") { - Shared::Global::setStyle(style); - } - } - if (Shared::Global::supported("colorSchemeTools")) { - QVariant vt = settings.value("theme"); - if (vt.isValid()) { - QString theme = vt.toString(); - if (theme.toLower() != "system") { - Shared::Global::setTheme(theme); - } - } - } - QString path = Shared::downloadsPathCheck(); - if (path.size() > 0) { - settings.setValue("downloadsPath", path); - } else { - qDebug() << "couldn't initialize directory for downloads, quitting"; - return -1; - } - - - Squawk w; - w.show(); - - Core::Squawk* squawk = new Core::Squawk(); - QThread* coreThread = new QThread(); - squawk->moveToThread(coreThread); - - QObject::connect(&sc, &SignalCatcher::interrupt, &app, &QApplication::closeAllWindows); - - QObject::connect(coreThread, &QThread::started, squawk, &Core::Squawk::start); - QObject::connect(&app, &QApplication::lastWindowClosed, squawk, &Core::Squawk::stop); - QObject::connect(&app, &QApplication::lastWindowClosed, &w, &Squawk::writeSettings); - //QObject::connect(&app, &QApplication::aboutToQuit, &w, &QMainWindow::close); - QObject::connect(squawk, &Core::Squawk::quit, squawk, &Core::Squawk::deleteLater); - QObject::connect(squawk, &Core::Squawk::quit, coreThread, &QThread::quit, Qt::QueuedConnection); - QObject::connect(coreThread, &QThread::finished, &app, &QApplication::quit, Qt::QueuedConnection); - - QObject::connect(&w, &Squawk::newAccountRequest, squawk, &Core::Squawk::newAccountRequest); - QObject::connect(&w, &Squawk::modifyAccountRequest, squawk, &Core::Squawk::modifyAccountRequest); - QObject::connect(&w, &Squawk::removeAccountRequest, squawk, &Core::Squawk::removeAccountRequest); - QObject::connect(&w, &Squawk::connectAccount, squawk, &Core::Squawk::connectAccount); - QObject::connect(&w, &Squawk::disconnectAccount, squawk, &Core::Squawk::disconnectAccount); - QObject::connect(&w, &Squawk::changeState, squawk, &Core::Squawk::changeState); - QObject::connect(&w, &Squawk::sendMessage, squawk,&Core::Squawk::sendMessage); - QObject::connect(&w, &Squawk::replaceMessage, squawk,&Core::Squawk::replaceMessage); - QObject::connect(&w, &Squawk::resendMessage, squawk,&Core::Squawk::resendMessage); - QObject::connect(&w, &Squawk::requestArchive, squawk, &Core::Squawk::requestArchive); - QObject::connect(&w, &Squawk::subscribeContact, squawk, &Core::Squawk::subscribeContact); - QObject::connect(&w, &Squawk::unsubscribeContact, squawk, &Core::Squawk::unsubscribeContact); - QObject::connect(&w, &Squawk::addContactRequest, squawk, &Core::Squawk::addContactRequest); - QObject::connect(&w, &Squawk::removeContactRequest, squawk, &Core::Squawk::removeContactRequest); - QObject::connect(&w, &Squawk::setRoomJoined, squawk, &Core::Squawk::setRoomJoined); - QObject::connect(&w, &Squawk::setRoomAutoJoin, squawk, &Core::Squawk::setRoomAutoJoin); - QObject::connect(&w, &Squawk::removeRoomRequest, squawk, &Core::Squawk::removeRoomRequest); - QObject::connect(&w, &Squawk::addRoomRequest, squawk, &Core::Squawk::addRoomRequest); - QObject::connect(&w, &Squawk::fileDownloadRequest, squawk, &Core::Squawk::fileDownloadRequest); - QObject::connect(&w, &Squawk::addContactToGroupRequest, squawk, &Core::Squawk::addContactToGroupRequest); - QObject::connect(&w, &Squawk::removeContactFromGroupRequest, squawk, &Core::Squawk::removeContactFromGroupRequest); - QObject::connect(&w, &Squawk::renameContactRequest, squawk, &Core::Squawk::renameContactRequest); - QObject::connect(&w, &Squawk::requestVCard, squawk, &Core::Squawk::requestVCard); - QObject::connect(&w, &Squawk::uploadVCard, squawk, &Core::Squawk::uploadVCard); - QObject::connect(&w, &Squawk::responsePassword, squawk, &Core::Squawk::responsePassword); - QObject::connect(&w, &Squawk::localPathInvalid, squawk, &Core::Squawk::onLocalPathInvalid); - QObject::connect(&w, &Squawk::changeDownloadsPath, squawk, &Core::Squawk::changeDownloadsPath); - - QObject::connect(squawk, &Core::Squawk::newAccount, &w, &Squawk::newAccount); - QObject::connect(squawk, &Core::Squawk::addContact, &w, &Squawk::addContact); - QObject::connect(squawk, &Core::Squawk::changeAccount, &w, &Squawk::changeAccount); - QObject::connect(squawk, &Core::Squawk::removeAccount, &w, &Squawk::removeAccount); - QObject::connect(squawk, &Core::Squawk::addGroup, &w, &Squawk::addGroup); - QObject::connect(squawk, &Core::Squawk::removeGroup, &w, &Squawk::removeGroup); - QObject::connect(squawk, qOverload(&Core::Squawk::removeContact), - &w, qOverload(&Squawk::removeContact)); - QObject::connect(squawk, qOverload(&Core::Squawk::removeContact), - &w, qOverload(&Squawk::removeContact)); - QObject::connect(squawk, &Core::Squawk::changeContact, &w, &Squawk::changeContact); - QObject::connect(squawk, &Core::Squawk::addPresence, &w, &Squawk::addPresence); - QObject::connect(squawk, &Core::Squawk::removePresence, &w, &Squawk::removePresence); - QObject::connect(squawk, &Core::Squawk::stateChanged, &w, &Squawk::stateChanged); - QObject::connect(squawk, &Core::Squawk::accountMessage, &w, &Squawk::accountMessage); - QObject::connect(squawk, &Core::Squawk::changeMessage, &w, &Squawk::changeMessage); - QObject::connect(squawk, &Core::Squawk::responseArchive, &w, &Squawk::responseArchive); - QObject::connect(squawk, &Core::Squawk::addRoom, &w, &Squawk::addRoom); - QObject::connect(squawk, &Core::Squawk::changeRoom, &w, &Squawk::changeRoom); - QObject::connect(squawk, &Core::Squawk::removeRoom, &w, &Squawk::removeRoom); - QObject::connect(squawk, &Core::Squawk::addRoomParticipant, &w, &Squawk::addRoomParticipant); - QObject::connect(squawk, &Core::Squawk::changeRoomParticipant, &w, &Squawk::changeRoomParticipant); - QObject::connect(squawk, &Core::Squawk::removeRoomParticipant, &w, &Squawk::removeRoomParticipant); - QObject::connect(squawk, &Core::Squawk::fileDownloadComplete, &w, &Squawk::fileDownloadComplete); - QObject::connect(squawk, &Core::Squawk::fileUploadComplete, &w, &Squawk::fileUploadComplete); - QObject::connect(squawk, &Core::Squawk::fileProgress, &w, &Squawk::fileProgress); - QObject::connect(squawk, &Core::Squawk::fileError, &w, &Squawk::fileError); - QObject::connect(squawk, &Core::Squawk::responseVCard, &w, &Squawk::responseVCard); - QObject::connect(squawk, &Core::Squawk::requestPassword, &w, &Squawk::requestPassword); - QObject::connect(squawk, &Core::Squawk::ready, &w, &Squawk::readSettings); - - coreThread->start(); - int result = app.exec(); - - if (coreThread->isRunning()) { - //coreThread->wait(); - //todo if I uncomment that, the app will no quit if it has reconnected at least once - //it feels like a symptom of something badly desinged in the core coreThread - //need to investigate; - } - - return result; -} - diff --git a/core/networkaccess.h b/core/networkaccess.h index 0b7bb7d..6ddfa99 100644 --- a/core/networkaccess.h +++ b/core/networkaccess.h @@ -30,7 +30,7 @@ #include -#include "urlstorage.h" +#include "storage/urlstorage.h" #include "shared/pathcheck.h" namespace Core { diff --git a/core/rosteritem.cpp b/core/rosteritem.cpp index b1951d6..1b8d1e6 100644 --- a/core/rosteritem.cpp +++ b/core/rosteritem.cpp @@ -124,15 +124,19 @@ void Core::RosterItem::nextRequest() if (requestedCount != -1) { bool last = false; if (archiveState == beginning || archiveState == complete) { - QString firstId = archive->oldestId(); - if (responseCache.size() == 0) { - if (requestedBefore == firstId) { - last = true; - } - } else { - if (responseCache.front().getId() == firstId) { - last = true; + try { + QString firstId = archive->oldestId(); + if (responseCache.size() == 0) { + if (requestedBefore == firstId) { + last = true; + } + } else { + if (responseCache.front().getId() == firstId) { + last = true; + } } + } catch (const Archive::Empty& e) { + last = true; } } else if (archiveState == empty && responseCache.size() == 0) { last = true; @@ -171,8 +175,12 @@ void Core::RosterItem::performRequest(int count, const QString& before) requestCache.emplace_back(requestedCount, before); requestedCount = -1; } - Shared::Message msg = archive->newest(); - emit needHistory("", getId(msg), msg.getTime()); + try { + Shared::Message msg = archive->newest(); + emit needHistory("", getId(msg), msg.getTime()); + } catch (const Archive::Empty& e) { //this can happen when the only message in archive is not server stored (error, for example) + emit needHistory(before, ""); + } } break; case end: diff --git a/core/rosteritem.h b/core/rosteritem.h index 237a46a..5f99017 100644 --- a/core/rosteritem.h +++ b/core/rosteritem.h @@ -34,7 +34,8 @@ #include "shared/enums.h" #include "shared/message.h" #include "shared/vcard.h" -#include "archive.h" +#include "storage/archive.h" +#include "adapterfunctions.h" namespace Core { diff --git a/core/squawk.cpp b/core/squawk.cpp index af131d5..0f8fe9f 100644 --- a/core/squawk.cpp +++ b/core/squawk.cpp @@ -26,8 +26,8 @@ Core::Squawk::Squawk(QObject* parent): QObject(parent), accounts(), amap(), + state(Shared::Availability::offline), network(), - waitingForAccounts(0), isInitialized(false) #ifdef WITH_KWALLET ,kwallet() @@ -42,7 +42,7 @@ Core::Squawk::Squawk(QObject* parent): if (kwallet.supportState() == PSE::KWallet::success) { connect(&kwallet, &PSE::KWallet::opened, this, &Squawk::onWalletOpened); connect(&kwallet, &PSE::KWallet::rejectPassword, this, &Squawk::onWalletRejectPassword); - connect(&kwallet, &PSE::KWallet::responsePassword, this, &Squawk::onWalletResponsePassword); + connect(&kwallet, &PSE::KWallet::responsePassword, this, &Squawk::responsePassword); Shared::Global::setSupported("KWallet", true); } @@ -97,6 +97,7 @@ void Core::Squawk::stop() settings.setValue("password", password); settings.setValue("resource", acc->getResource()); settings.setValue("passwordType", static_cast(ap)); + settings.setValue("active", acc->getActive()); } settings.endArray(); settings.endGroup(); @@ -124,8 +125,9 @@ void Core::Squawk::newAccountRequest(const QMap& map) QString password = map.value("password").toString(); QString resource = map.value("resource").toString(); int passwordType = map.value("passwordType").toInt(); + bool active = map.value("active").toBool(); - addAccount(login, server, password, name, resource, Shared::Global::fromInt(passwordType)); + addAccount(login, server, password, name, resource, active, Shared::Global::fromInt(passwordType)); } void Core::Squawk::addAccount( @@ -133,13 +135,15 @@ void Core::Squawk::addAccount( const QString& server, const QString& password, const QString& name, - const QString& resource, - Shared::AccountPassword passwordType -) + const QString& resource, + bool active, + Shared::AccountPassword passwordType) { - QSettings settings; - - Account* acc = new Account(login, server, password, name, &network); + if (amap.count(name) > 0) { + qDebug() << "An attempt to add account" << name << "but an account with such name already exist, ignoring"; + return; + } + Account* acc = new Account(login, server, password, name, active, &network); acc->setResource(resource); acc->setPasswordType(passwordType); accounts.push_back(acc); @@ -148,6 +152,8 @@ void Core::Squawk::addAccount( connect(acc, &Account::connectionStateChanged, this, &Squawk::onAccountConnectionStateChanged); connect(acc, &Account::changed, this, &Squawk::onAccountChanged); connect(acc, &Account::error, this, &Squawk::onAccountError); + connect(acc, &Account::needPassword, this, &Squawk::onAccountNeedPassword); + connect(acc, &Account::availabilityChanged, this, &Squawk::onAccountAvailabilityChanged); connect(acc, &Account::addContact, this, &Squawk::onAccountAddContact); connect(acc, &Account::addGroup, this, &Squawk::onAccountAddGroup); @@ -185,20 +191,44 @@ void Core::Squawk::addAccount( {"offline", QVariant::fromValue(Shared::Availability::offline)}, {"error", ""}, {"avatarPath", acc->getAvatarPath()}, - {"passwordType", QVariant::fromValue(passwordType)} + {"passwordType", QVariant::fromValue(passwordType)}, + {"active", active} }; emit newAccount(map); + + switch (passwordType) { + case Shared::AccountPassword::alwaysAsk: + case Shared::AccountPassword::kwallet: + if (password == "") { + acc->invalidatePassword(); + break; + } + default: + break; + } + + if (state != Shared::Availability::offline) { + acc->setAvailability(state); + if (acc->getActive()) { + acc->connect(); + } + } } void Core::Squawk::changeState(Shared::Availability p_state) { if (state != p_state) { + for (std::deque::iterator itr = accounts.begin(), end = accounts.end(); itr != end; ++itr) { + Account* acc = *itr; + acc->setAvailability(p_state); + if (state == Shared::Availability::offline && acc->getActive()) { + acc->connect(); + } + } state = p_state; - } - - for (std::deque::iterator itr = accounts.begin(), end = accounts.end(); itr != end; ++itr) { - (*itr)->setAvailability(state); + + emit stateChanged(p_state); } } @@ -209,7 +239,10 @@ void Core::Squawk::connectAccount(const QString& account) qDebug("An attempt to connect non existing account, skipping"); return; } - itr->second->connect(); + itr->second->setActive(true); + if (state != Shared::Availability::offline) { + itr->second->connect(); + } } void Core::Squawk::disconnectAccount(const QString& account) @@ -220,14 +253,18 @@ void Core::Squawk::disconnectAccount(const QString& account) return; } + itr->second->setActive(false); itr->second->disconnect(); } void Core::Squawk::onAccountConnectionStateChanged(Shared::ConnectionState p_state) { Account* acc = static_cast(sender()); - emit changeAccount(acc->getName(), {{"state", QVariant::fromValue(p_state)}}); - + emit changeAccount(acc->getName(), { + {"state", QVariant::fromValue(p_state)}, + {"error", ""} + }); + #ifdef WITH_KWALLET if (p_state == Shared::ConnectionState::connected) { if (acc->getPasswordType() == Shared::AccountPassword::kwallet && kwallet.supportState() == PSE::KWallet::success) { @@ -235,33 +272,6 @@ void Core::Squawk::onAccountConnectionStateChanged(Shared::ConnectionState p_sta } } #endif - - Accounts::const_iterator itr = accounts.begin(); - bool es = true; - bool ea = true; - Shared::ConnectionState cs = (*itr)->getState(); - Shared::Availability av = (*itr)->getAvailability(); - itr++; - for (Accounts::const_iterator end = accounts.end(); itr != end; itr++) { - Account* item = *itr; - if (item->getState() != cs) { - es = false; - } - if (item->getAvailability() != av) { - ea = false; - } - } - - if (es) { - if (cs == Shared::ConnectionState::disconnected) { - state = Shared::Availability::offline; - emit stateChanged(state); - } else if (ea) { - state = av; - emit stateChanged(state); - } - } - } void Core::Squawk::onAccountAddContact(const QString& jid, const QString& group, const QMap& data) @@ -391,6 +401,7 @@ void Core::Squawk::modifyAccountRequest(const QString& name, const QMapgetState(); QMap::const_iterator mItr; bool needToReconnect = false; + bool wentReconnecting = false; mItr = map.find("login"); if (mItr != map.end()) { @@ -416,8 +427,16 @@ void Core::Squawk::modifyAccountRequest(const QString& name, const QMapreconnect(); + bool activeChanged = false; + mItr = map.find("active"); + if (mItr == map.end() || mItr->toBool() == acc->getActive()) { + if (needToReconnect && st != Shared::ConnectionState::disconnected) { + acc->reconnect(); + wentReconnecting = true; + } + } else { + acc->setActive(mItr->toBool()); + activeChanged = true; } mItr = map.find("login"); @@ -454,6 +473,14 @@ void Core::Squawk::modifyAccountRequest(const QString& name, const QMapgetActive()) { + acc->connect(); + } else if (!wentReconnecting && acc->getActive() && acc->getLastError() == Account::Error::authentication) { + acc->connect(); + } + } + emit changeAccount(name, map); } @@ -461,6 +488,10 @@ void Core::Squawk::onAccountError(const QString& text) { Account* acc = static_cast(sender()); emit changeAccount(acc->getName(), {{"error", text}}); + + if (acc->getLastError() == Account::Error::authentication) { + emit requestPassword(acc->getName(), true); + } } void Core::Squawk::removeAccountRequest(const QString& name) @@ -675,6 +706,70 @@ void Core::Squawk::uploadVCard(const QString& account, const Shared::VCard& card itr->second->uploadVCard(card); } +void Core::Squawk::readSettings() +{ + QSettings settings; + settings.beginGroup("core"); + int size = settings.beginReadArray("accounts"); + for (int i = 0; i < size; ++i) { + settings.setArrayIndex(i); + Shared::AccountPassword passwordType = + Shared::Global::fromInt( + settings.value("passwordType", static_cast(Shared::AccountPassword::plain)).toInt() + ); + + QString password = settings.value("password", "").toString(); + if (passwordType == Shared::AccountPassword::jammed) { + SimpleCrypt crypto(passwordHash); + password = crypto.decryptToString(password); + } + + addAccount( + settings.value("login").toString(), + settings.value("server").toString(), + password, + settings.value("name").toString(), + settings.value("resource").toString(), + settings.value("active").toBool(), + passwordType + ); + } + settings.endArray(); + settings.endGroup(); + + qDebug() << "Squawk core is ready"; + emit ready(); +} + +void Core::Squawk::onAccountNeedPassword() +{ + Account* acc = static_cast(sender()); + switch (acc->getPasswordType()) { + case Shared::AccountPassword::alwaysAsk: + emit requestPassword(acc->getName(), false); + break; + case Shared::AccountPassword::kwallet: { +#ifdef WITH_KWALLET + if (kwallet.supportState() == PSE::KWallet::success) { + kwallet.requestReadPassword(acc->getName()); + } else { +#endif + emit requestPassword(acc->getName(), false); +#ifdef WITH_KWALLET + } +#endif + break; + } + default: + break; //should never happen; + } +} + +void Core::Squawk::onWalletRejectPassword(const QString& login) +{ + emit requestPassword(login, false); +} + void Core::Squawk::responsePassword(const QString& account, const QString& password) { AccountsMap::const_iterator itr = amap.find(account); @@ -682,96 +777,12 @@ void Core::Squawk::responsePassword(const QString& account, const QString& passw qDebug() << "An attempt to set password to non existing account" << account << ", skipping"; return; } - itr->second->setPassword(password); + Account* acc = itr->second; + acc->setPassword(password); emit changeAccount(account, {{"password", password}}); - accountReady(); -} - -void Core::Squawk::readSettings() -{ - QSettings settings; - settings.beginGroup("core"); - int size = settings.beginReadArray("accounts"); - waitingForAccounts = size; - for (int i = 0; i < size; ++i) { - settings.setArrayIndex(i); - parseAccount( - settings.value("login").toString(), - settings.value("server").toString(), - settings.value("password", "").toString(), - settings.value("name").toString(), - settings.value("resource").toString(), - Shared::Global::fromInt(settings.value("passwordType", static_cast(Shared::AccountPassword::plain)).toInt()) - ); + if (state != Shared::Availability::offline && acc->getActive()) { + acc->connect(); } - settings.endArray(); - settings.endGroup(); -} - -void Core::Squawk::accountReady() -{ - --waitingForAccounts; - - if (waitingForAccounts == 0) { - emit ready(); - } -} - -void Core::Squawk::parseAccount( - const QString& login, - const QString& server, - const QString& password, - const QString& name, - const QString& resource, - Shared::AccountPassword passwordType -) -{ - switch (passwordType) { - case Shared::AccountPassword::plain: - addAccount(login, server, password, name, resource, passwordType); - accountReady(); - break; - case Shared::AccountPassword::jammed: { - SimpleCrypt crypto(passwordHash); - QString decrypted = crypto.decryptToString(password); - addAccount(login, server, decrypted, name, resource, passwordType); - accountReady(); - } - break; - case Shared::AccountPassword::alwaysAsk: - addAccount(login, server, QString(), name, resource, passwordType); - emit requestPassword(name); - break; - case Shared::AccountPassword::kwallet: { - addAccount(login, server, QString(), name, resource, passwordType); -#ifdef WITH_KWALLET - if (kwallet.supportState() == PSE::KWallet::success) { - kwallet.requestReadPassword(name); - } else { -#endif - emit requestPassword(name); -#ifdef WITH_KWALLET - } -#endif - } - } -} - -void Core::Squawk::onWalletRejectPassword(const QString& login) -{ - emit requestPassword(login); -} - -void Core::Squawk::onWalletResponsePassword(const QString& login, const QString& password) -{ - AccountsMap::const_iterator itr = amap.find(login); - if (itr == amap.end()) { - qDebug() << "An attempt to set password to non existing account" << login << ", skipping"; - return; - } - itr->second->setPassword(password); - emit changeAccount(login, {{"password", password}}); - accountReady(); } void Core::Squawk::onAccountUploadFileError(const QString& jid, const QString id, const QString& errorText) diff --git a/core/squawk.h b/core/squawk.h index 6cd251f..c82b1c8 100644 --- a/core/squawk.h +++ b/core/squawk.h @@ -86,7 +86,7 @@ signals: void responseVCard(const QString& jid, const Shared::VCard& card); void changeMessage(const QString& account, const QString& jid, const QString& id, const QMap& data); - void requestPassword(const QString& account); + void requestPassword(const QString& account, bool authernticationError); public slots: void start(); @@ -134,7 +134,6 @@ private: AccountsMap amap; Shared::Availability state; NetworkAccess network; - uint8_t waitingForAccounts; bool isInitialized; #ifdef WITH_KWALLET @@ -148,6 +147,7 @@ private slots: const QString& password, const QString& name, const QString& resource, + bool active, Shared::AccountPassword passwordType ); @@ -172,22 +172,22 @@ private slots: void onAccountChangeRoomPresence(const QString& jid, const QString& nick, const QMap& data); void onAccountRemoveRoomPresence(const QString& jid, const QString& nick); void onAccountChangeMessage(const QString& jid, const QString& id, const QMap& data); + void onAccountNeedPassword(); void onAccountUploadFileError(const QString& jid, const QString id, const QString& errorText); void onWalletOpened(bool success); - void onWalletResponsePassword(const QString& login, const QString& password); void onWalletRejectPassword(const QString& login); private: void readSettings(); - void accountReady(); void parseAccount( const QString& login, const QString& server, const QString& password, const QString& name, const QString& resource, + bool active, Shared::AccountPassword passwordType ); diff --git a/core/storage/CMakeLists.txt b/core/storage/CMakeLists.txt new file mode 100644 index 0000000..4c263d5 --- /dev/null +++ b/core/storage/CMakeLists.txt @@ -0,0 +1,8 @@ +target_sources(squawk PRIVATE + archive.cpp + archive.h + storage.cpp + storage.h + urlstorage.cpp + urlstorage.h +) diff --git a/core/archive.cpp b/core/storage/archive.cpp similarity index 100% rename from core/archive.cpp rename to core/storage/archive.cpp diff --git a/core/archive.h b/core/storage/archive.h similarity index 100% rename from core/archive.h rename to core/storage/archive.h diff --git a/core/storage.cpp b/core/storage/storage.cpp similarity index 100% rename from core/storage.cpp rename to core/storage/storage.cpp diff --git a/core/storage.h b/core/storage/storage.h similarity index 100% rename from core/storage.h rename to core/storage/storage.h diff --git a/core/urlstorage.cpp b/core/storage/urlstorage.cpp similarity index 100% rename from core/urlstorage.cpp rename to core/storage/urlstorage.cpp diff --git a/core/urlstorage.h b/core/storage/urlstorage.h similarity index 100% rename from core/urlstorage.h rename to core/storage/urlstorage.h diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..3c23932 --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,7 @@ +target_sources(squawk PRIVATE + main.cpp + application.cpp + application.h + dialogqueue.cpp + dialogqueue.h +) diff --git a/main/application.cpp b/main/application.cpp new file mode 100644 index 0000000..216e322 --- /dev/null +++ b/main/application.cpp @@ -0,0 +1,566 @@ +// Squawk messenger. +// Copyright (C) 2019 Yury Gubich +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "application.h" + +Application::Application(Core::Squawk* p_core): + QObject(), + availability(Shared::Availability::offline), + core(p_core), + squawk(nullptr), + notifications("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications"), + roster(), + conversations(), + dialogueQueue(roster), + nowQuitting(false), + destroyingSquawk(false), + storage() +{ + connect(&roster, &Models::Roster::unnoticedMessage, this, &Application::notify); + connect(&roster, &Models::Roster::unreadMessagesCountChanged, this, &Application::unreadMessagesCountChanged); + + + //connecting myself to the backed + connect(this, &Application::changeState, core, &Core::Squawk::changeState); + connect(this, &Application::setRoomJoined, core, &Core::Squawk::setRoomJoined); + connect(this, &Application::setRoomAutoJoin, core, &Core::Squawk::setRoomAutoJoin); + connect(this, &Application::subscribeContact, core, &Core::Squawk::subscribeContact); + connect(this, &Application::unsubscribeContact, core, &Core::Squawk::unsubscribeContact); + connect(this, &Application::replaceMessage, core, &Core::Squawk::replaceMessage); + connect(this, &Application::sendMessage, core, &Core::Squawk::sendMessage); + connect(this, &Application::resendMessage, core, &Core::Squawk::resendMessage); + connect(&roster, &Models::Roster::requestArchive, + std::bind(&Core::Squawk::requestArchive, core, std::placeholders::_1, std::placeholders::_2, 20, std::placeholders::_3)); + + connect(&dialogueQueue, &DialogQueue::modifyAccountRequest, core, &Core::Squawk::modifyAccountRequest); + connect(&dialogueQueue, &DialogQueue::responsePassword, core, &Core::Squawk::responsePassword); + connect(&dialogueQueue, &DialogQueue::disconnectAccount, core, &Core::Squawk::disconnectAccount); + + connect(&roster, &Models::Roster::fileDownloadRequest, core, &Core::Squawk::fileDownloadRequest); + connect(&roster, &Models::Roster::localPathInvalid, core, &Core::Squawk::onLocalPathInvalid); + + + //coonecting backend to myself + connect(core, &Core::Squawk::stateChanged, this, &Application::stateChanged); + + connect(core, &Core::Squawk::accountMessage, &roster, &Models::Roster::addMessage); + connect(core, &Core::Squawk::responseArchive, &roster, &Models::Roster::responseArchive); + connect(core, &Core::Squawk::changeMessage, &roster, &Models::Roster::changeMessage); + + connect(core, &Core::Squawk::newAccount, &roster, &Models::Roster::addAccount); + connect(core, &Core::Squawk::changeAccount, this, &Application::changeAccount); + connect(core, &Core::Squawk::removeAccount, this, &Application::removeAccount); + + connect(core, &Core::Squawk::addContact, this, &Application::addContact); + connect(core, &Core::Squawk::addGroup, this, &Application::addGroup); + connect(core, &Core::Squawk::removeGroup, &roster, &Models::Roster::removeGroup); + connect(core, qOverload(&Core::Squawk::removeContact), + &roster, qOverload(&Models::Roster::removeContact)); + connect(core, qOverload(&Core::Squawk::removeContact), + &roster, qOverload(&Models::Roster::removeContact)); + connect(core, &Core::Squawk::changeContact, &roster, &Models::Roster::changeContact); + connect(core, &Core::Squawk::addPresence, &roster, &Models::Roster::addPresence); + connect(core, &Core::Squawk::removePresence, &roster, &Models::Roster::removePresence); + + connect(core, &Core::Squawk::addRoom, &roster, &Models::Roster::addRoom); + connect(core, &Core::Squawk::changeRoom, &roster, &Models::Roster::changeRoom); + connect(core, &Core::Squawk::removeRoom, &roster, &Models::Roster::removeRoom); + connect(core, &Core::Squawk::addRoomParticipant, &roster, &Models::Roster::addRoomParticipant); + connect(core, &Core::Squawk::changeRoomParticipant, &roster, &Models::Roster::changeRoomParticipant); + connect(core, &Core::Squawk::removeRoomParticipant, &roster, &Models::Roster::removeRoomParticipant); + + + connect(core, &Core::Squawk::fileDownloadComplete, std::bind(&Models::Roster::fileComplete, &roster, std::placeholders::_1, false)); + connect(core, &Core::Squawk::fileUploadComplete, std::bind(&Models::Roster::fileComplete, &roster, std::placeholders::_1, true)); + connect(core, &Core::Squawk::fileProgress, &roster, &Models::Roster::fileProgress); + connect(core, &Core::Squawk::fileError, &roster, &Models::Roster::fileError); + + connect(core, &Core::Squawk::requestPassword, this, &Application::requestPassword); + connect(core, &Core::Squawk::ready, this, &Application::readSettings); + + QDBusConnection sys = QDBusConnection::sessionBus(); + sys.connect( + "org.freedesktop.Notifications", + "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", + "NotificationClosed", + this, + SLOT(onNotificationClosed(quint32, quint32)) + ); + sys.connect( + "org.freedesktop.Notifications", + "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", + "ActionInvoked", + this, + SLOT(onNotificationInvoked(quint32, const QString&)) + ); +} + +Application::~Application() {} + +void Application::quit() +{ + if (!nowQuitting) { + nowQuitting = true; + emit quitting(); + + writeSettings(); + unreadMessagesCountChanged(0); //this notification persist in the desktop, for now I'll zero it on quit not to confuse people + for (Conversations::const_iterator itr = conversations.begin(), end = conversations.end(); itr != end; ++itr) { + disconnect(itr->second, &Conversation::destroyed, this, &Application::onConversationClosed); + itr->second->close(); + } + conversations.clear(); + dialogueQueue.quit(); + + if (squawk != nullptr) { + squawk->close(); + } + if (!destroyingSquawk) { + checkForTheLastWindow(); + } + } +} + +void Application::checkForTheLastWindow() +{ + if (QApplication::topLevelWidgets().size() > 0) { + emit readyToQuit(); + } else { + connect(qApp, &QApplication::lastWindowClosed, this, &Application::readyToQuit); + } +} + +void Application::createMainWindow() +{ + if (squawk == nullptr) { + squawk = new Squawk(roster); + + connect(squawk, &Squawk::notify, this, &Application::notify); + connect(squawk, &Squawk::changeSubscription, this, &Application::changeSubscription); + connect(squawk, &Squawk::openedConversation, this, &Application::onSquawkOpenedConversation); + connect(squawk, &Squawk::openConversation, this, &Application::openConversation); + connect(squawk, &Squawk::changeState, this, &Application::setState); + connect(squawk, &Squawk::closing, this, &Application::onSquawkClosing); + + connect(squawk, &Squawk::modifyAccountRequest, core, &Core::Squawk::modifyAccountRequest); + connect(squawk, &Squawk::newAccountRequest, core, &Core::Squawk::newAccountRequest); + connect(squawk, &Squawk::removeAccountRequest, core, &Core::Squawk::removeAccountRequest); + connect(squawk, &Squawk::connectAccount, core, &Core::Squawk::connectAccount); + connect(squawk, &Squawk::disconnectAccount, core, &Core::Squawk::disconnectAccount); + + connect(squawk, &Squawk::addContactRequest, core, &Core::Squawk::addContactRequest); + connect(squawk, &Squawk::removeContactRequest, core, &Core::Squawk::removeContactRequest); + connect(squawk, &Squawk::removeRoomRequest, core, &Core::Squawk::removeRoomRequest); + connect(squawk, &Squawk::addRoomRequest, core, &Core::Squawk::addRoomRequest); + connect(squawk, &Squawk::addContactToGroupRequest, core, &Core::Squawk::addContactToGroupRequest); + connect(squawk, &Squawk::removeContactFromGroupRequest, core, &Core::Squawk::removeContactFromGroupRequest); + connect(squawk, &Squawk::renameContactRequest, core, &Core::Squawk::renameContactRequest); + connect(squawk, &Squawk::requestVCard, core, &Core::Squawk::requestVCard); + connect(squawk, &Squawk::uploadVCard, core, &Core::Squawk::uploadVCard); + connect(squawk, &Squawk::changeDownloadsPath, core, &Core::Squawk::changeDownloadsPath); + + connect(core, &Core::Squawk::responseVCard, squawk, &Squawk::responseVCard); + + dialogueQueue.setParentWidnow(squawk); + squawk->stateChanged(availability); + squawk->show(); + } +} + +void Application::onSquawkClosing() +{ + dialogueQueue.setParentWidnow(nullptr); + + if (!nowQuitting) { + disconnect(core, &Core::Squawk::responseVCard, squawk, &Squawk::responseVCard); + } + + destroyingSquawk = true; + squawk->deleteLater(); + squawk = nullptr; + + //for now + quit(); +} + +void Application::onSquawkDestroyed() { + destroyingSquawk = false; + if (nowQuitting) { + checkForTheLastWindow(); + } +} + + +void Application::notify(const QString& account, const Shared::Message& msg) +{ + QString jid = msg.getPenPalJid(); + QString name = QString(roster.getContactName(account, jid)); + QString path = QString(roster.getContactIconPath(account, jid, msg.getPenPalResource())); + QVariantList args; + args << QString(); + + uint32_t notificationId = qHash(msg.getId()); + args << notificationId; + if (path.size() > 0) { + args << path; + } else { + args << QString("mail-message"); //TODO should here better be unknown user icon? + } + if (msg.getType() == Shared::Message::groupChat) { + args << msg.getFromResource() + tr(" from ") + name; + } else { + args << name; + } + + QString body(msg.getBody()); + QString oob(msg.getOutOfBandUrl()); + if (body == oob) { + body = tr("Attached file"); + } + + args << body; + args << QStringList({ + "markAsRead", tr("Mark as Read"), + "openConversation", tr("Open conversation") + }); + args << QVariantMap({ + {"desktop-entry", qApp->desktopFileName()}, + {"category", QString("message")}, + {"urgency", 1}, + // {"sound-file", "/path/to/macaw/squawk"}, + {"sound-name", QString("message-new-instant")} + }); + args << -1; + notifications.callWithArgumentList(QDBus::AutoDetect, "Notify", args); + + storage.insert(std::make_pair(notificationId, std::make_pair(Models::Roster::ElId(account, name), msg.getId()))); + + if (squawk != nullptr) { + QApplication::alert(squawk); + } +} + +void Application::onNotificationClosed(quint32 id, quint32 reason) +{ + Notifications::const_iterator itr = storage.find(id); + if (itr != storage.end()) { + if (reason == 2) { //dissmissed by user (https://specifications.freedesktop.org/notification-spec/latest/ar01s09.html) + //TODO may ba also mark as read? + } + if (reason != 1) { //just expired, can be activated again from history, so no removing for now + storage.erase(id); + qDebug() << "Notification" << id << "was closed"; + } + } +} + +void Application::onNotificationInvoked(quint32 id, const QString& action) +{ + qDebug() << "Notification" << id << action << "request"; + Notifications::const_iterator itr = storage.find(id); + if (itr != storage.end()) { + if (action == "markAsRead") { + roster.markMessageAsRead(itr->second.first, itr->second.second); + } else if (action == "openConversation") { + focusConversation(itr->second.first, "", itr->second.second); + } + } +} + +void Application::unreadMessagesCountChanged(int count) +{ + QDBusMessage signal = QDBusMessage::createSignal("/", "com.canonical.Unity.LauncherEntry", "Update"); + signal << qApp->desktopFileName() + QLatin1String(".desktop"); + signal << QVariantMap ({ + {"count-visible", count != 0}, + {"count", count} + }); + QDBusConnection::sessionBus().send(signal); +} + +void Application::focusConversation(const Models::Roster::ElId& id, const QString& resource, const QString& messageId) +{ + if (squawk != nullptr) { + if (squawk->currentConversationId() != id) { + QModelIndex index = roster.getContactIndex(id.account, id.name, resource); + squawk->select(index); + } + + if (squawk->isMinimized()) { + squawk->showNormal(); + } else { + squawk->show(); + } + squawk->raise(); + squawk->activateWindow(); + } else { + openConversation(id, resource); + } + + //TODO focus messageId; +} + +void Application::setState(Shared::Availability p_availability) +{ + if (availability != p_availability) { + availability = p_availability; + emit changeState(availability); + } +} + +void Application::stateChanged(Shared::Availability state) +{ + availability = state; + if (squawk != nullptr) { + squawk->stateChanged(state); + } +} + +void Application::readSettings() +{ + QSettings settings; + settings.beginGroup("ui"); + int avail; + if (settings.contains("availability")) { + avail = settings.value("availability").toInt(); + } else { + avail = static_cast(Shared::Availability::online); + } + settings.endGroup(); + + setState(Shared::Global::fromInt(avail)); + createMainWindow(); +} + +void Application::writeSettings() +{ + QSettings settings; + settings.setValue("availability", static_cast(availability)); +} + +void Application::requestPassword(const QString& account, bool authenticationError) { + if (authenticationError) { + dialogueQueue.addAction(account, DialogQueue::askCredentials); + } else { + dialogueQueue.addAction(account, DialogQueue::askPassword); + } + +} +void Application::onConversationClosed() +{ + Conversation* conv = static_cast(sender()); + Models::Roster::ElId id(conv->getAccount(), conv->getJid()); + Conversations::const_iterator itr = conversations.find(id); + if (itr != conversations.end()) { + conversations.erase(itr); + } + if (conv->isMuc) { + Room* room = static_cast(conv); + if (!room->autoJoined()) { + emit setRoomJoined(id.account, id.name, false); + } + } +} + +void Application::changeSubscription(const Models::Roster::ElId& id, bool subscribe) +{ + Models::Item::Type type = roster.getContactType(id); + + switch (type) { + case Models::Item::contact: + if (subscribe) { + emit subscribeContact(id.account, id.name, ""); + } else { + emit unsubscribeContact(id.account, id.name, ""); + } + break; + case Models::Item::room: + setRoomAutoJoin(id.account, id.name, subscribe); + if (!isConverstationOpened(id)) { + emit setRoomJoined(id.account, id.name, subscribe); + } + break; + default: + break; + } +} + +void Application::subscribeConversation(Conversation* conv) +{ + connect(conv, &Conversation::destroyed, this, &Application::onConversationClosed); + connect(conv, &Conversation::sendMessage, this, &Application::onConversationMessage); + connect(conv, &Conversation::replaceMessage, this, &Application::onConversationReplaceMessage); + connect(conv, &Conversation::resendMessage, this, &Application::onConversationResend); + connect(conv, &Conversation::notifyableMessage, this, &Application::notify); +} + +void Application::openConversation(const Models::Roster::ElId& id, const QString& resource) +{ + Conversations::const_iterator itr = conversations.find(id); + Models::Account* acc = roster.getAccount(id.account); + Conversation* conv = nullptr; + bool created = false; + if (itr != conversations.end()) { + conv = itr->second; + } else { + Models::Element* el = roster.getElement(id); + if (el != nullptr) { + if (el->type == Models::Item::room) { + created = true; + Models::Room* room = static_cast(el); + conv = new Room(acc, room); + if (!room->getJoined()) { + emit setRoomJoined(id.account, id.name, true); + } + } else if (el->type == Models::Item::contact) { + created = true; + conv = new Chat(acc, static_cast(el)); + } + } + } + + if (conv != nullptr) { + if (created) { + conv->setAttribute(Qt::WA_DeleteOnClose); + subscribeConversation(conv); + conversations.insert(std::make_pair(id, conv)); + } + + conv->show(); + conv->raise(); + conv->activateWindow(); + + if (resource.size() > 0) { + conv->setPalResource(resource); + } + } +} + +void Application::onConversationMessage(const Shared::Message& msg) +{ + Conversation* conv = static_cast(sender()); + QString acc = conv->getAccount(); + + roster.addMessage(acc, msg); + emit sendMessage(acc, msg); +} + +void Application::onConversationReplaceMessage(const QString& originalId, const Shared::Message& msg) +{ + Conversation* conv = static_cast(sender()); + QString acc = conv->getAccount(); + + roster.changeMessage(acc, msg.getPenPalJid(), originalId, { + {"state", static_cast(Shared::Message::State::pending)} + }); + emit replaceMessage(acc, originalId, msg); +} + +void Application::onConversationResend(const QString& id) +{ + Conversation* conv = static_cast(sender()); + QString acc = conv->getAccount(); + QString jid = conv->getJid(); + + emit resendMessage(acc, jid, id); +} + +void Application::onSquawkOpenedConversation() { + subscribeConversation(squawk->currentConversation); + Models::Roster::ElId id = squawk->currentConversationId(); + + const Models::Element* el = roster.getElementConst(id); + if (el != nullptr && el->isRoom() && !static_cast(el)->getJoined()) { + emit setRoomJoined(id.account, id.name, true); + } +} + +void Application::removeAccount(const QString& account) +{ + Conversations::const_iterator itr = conversations.begin(); + while (itr != conversations.end()) { + if (itr->first.account == account) { + Conversations::const_iterator lItr = itr; + ++itr; + Conversation* conv = lItr->second; + disconnect(conv, &Conversation::destroyed, this, &Application::onConversationClosed); + conv->close(); + conversations.erase(lItr); + } else { + ++itr; + } + } + + if (squawk != nullptr && squawk->currentConversationId().account == account) { + squawk->closeCurrentConversation(); + } + + roster.removeAccount(account); +} + +void Application::changeAccount(const QString& account, const QMap& data) +{ + for (QMap::const_iterator itr = data.begin(), end = data.end(); itr != end; ++itr) { + QString attr = itr.key(); + roster.updateAccount(account, attr, *itr); + } +} + +void Application::addContact(const QString& account, const QString& jid, const QString& group, const QMap& data) +{ + roster.addContact(account, jid, group, data); + + if (squawk != nullptr) { + QSettings settings; + settings.beginGroup("ui"); + settings.beginGroup("roster"); + settings.beginGroup(account); + if (settings.value("expanded", false).toBool()) { + QModelIndex ind = roster.getAccountIndex(account); + squawk->expand(ind); + } + settings.endGroup(); + settings.endGroup(); + settings.endGroup(); + } +} + +void Application::addGroup(const QString& account, const QString& name) +{ + roster.addGroup(account, name); + + if (squawk != nullptr) { + QSettings settings; + settings.beginGroup("ui"); + settings.beginGroup("roster"); + settings.beginGroup(account); + if (settings.value("expanded", false).toBool()) { + QModelIndex ind = roster.getAccountIndex(account); + squawk->expand(ind); + if (settings.value(name + "/expanded", false).toBool()) { + squawk->expand(roster.getGroupIndex(account, name)); + } + } + settings.endGroup(); + settings.endGroup(); + settings.endGroup(); + } +} + +bool Application::isConverstationOpened(const Models::Roster::ElId& id) const { + return (conversations.count(id) > 0) || (squawk != nullptr && squawk->currentConversationId() == id);} diff --git a/main/application.h b/main/application.h new file mode 100644 index 0000000..301edc4 --- /dev/null +++ b/main/application.h @@ -0,0 +1,118 @@ +// Squawk messenger. +// Copyright (C) 2019 Yury Gubich +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef APPLICATION_H +#define APPLICATION_H + +#include + +#include +#include + +#include "dialogqueue.h" +#include "core/squawk.h" + +#include "ui/squawk.h" +#include "ui/models/roster.h" +#include "ui/widgets/conversation.h" + +#include "shared/message.h" +#include "shared/enums.h" + +/** + * @todo write docs + */ +class Application : public QObject +{ + Q_OBJECT +public: + Application(Core::Squawk* core); + ~Application(); + + bool isConverstationOpened(const Models::Roster::ElId& id) const; + +signals: + void sendMessage(const QString& account, const Shared::Message& data); + void replaceMessage(const QString& account, const QString& originalId, const Shared::Message& data); + void resendMessage(const QString& account, const QString& jid, const QString& id); + + void changeState(Shared::Availability state); + + void setRoomJoined(const QString& account, const QString& jid, bool joined); + void setRoomAutoJoin(const QString& account, const QString& jid, bool joined); + void subscribeContact(const QString& account, const QString& jid, const QString& reason); + void unsubscribeContact(const QString& account, const QString& jid, const QString& reason); + + void quitting(); + void readyToQuit(); + +public slots: + void readSettings(); + void quit(); + +protected slots: + void notify(const QString& account, const Shared::Message& msg); + void unreadMessagesCountChanged(int count); + void setState(Shared::Availability availability); + + void changeAccount(const QString& account, const QMap& data); + void removeAccount(const QString& account); + void openConversation(const Models::Roster::ElId& id, const QString& resource = ""); + + void addGroup(const QString& account, const QString& name); + void addContact(const QString& account, const QString& jid, const QString& group, const QMap& data); + + void requestPassword(const QString& account, bool authenticationError); + + void writeSettings(); + +private slots: + void onConversationClosed(); + void changeSubscription(const Models::Roster::ElId& id, bool subscribe); + void onSquawkOpenedConversation(); + void onConversationMessage(const Shared::Message& msg); + void onConversationReplaceMessage(const QString& originalId, const Shared::Message& msg); + void onConversationResend(const QString& id); + void stateChanged(Shared::Availability state); + void onSquawkClosing(); + void onSquawkDestroyed(); + void onNotificationClosed(quint32 id, quint32 reason); + void onNotificationInvoked(quint32 id, const QString& action); + + +private: + void createMainWindow(); + void subscribeConversation(Conversation* conv); + void checkForTheLastWindow(); + void focusConversation(const Models::Roster::ElId& id, const QString& resource = "", const QString& messageId = ""); + +private: + typedef std::map Conversations; + typedef std::map> Notifications; + + Shared::Availability availability; + Core::Squawk* core; + Squawk* squawk; + QDBusInterface notifications; + Models::Roster roster; + Conversations conversations; + DialogQueue dialogueQueue; + bool nowQuitting; + bool destroyingSquawk; + Notifications storage; +}; + +#endif // APPLICATION_H diff --git a/main/dialogqueue.cpp b/main/dialogqueue.cpp new file mode 100644 index 0000000..d7b4570 --- /dev/null +++ b/main/dialogqueue.cpp @@ -0,0 +1,187 @@ +// Squawk messenger. +// Copyright (C) 2019 Yury Gubich +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "dialogqueue.h" +#include + +DialogQueue::DialogQueue(const Models::Roster& p_roster): + QObject(), + currentSource(), + currentAction(none), + queue(), + collection(queue.get<0>()), + sequence(queue.get<1>()), + prompt(nullptr), + parent(nullptr), + roster(p_roster) +{ +} + +DialogQueue::~DialogQueue() +{ +} + +void DialogQueue::quit() +{ + queue.clear(); + if (currentAction != none) { + actionDone(); + } +} + +void DialogQueue::setParentWidnow(QMainWindow* p_parent) +{ + parent = p_parent; +} + +bool DialogQueue::addAction(const QString& source, DialogQueue::Action action) +{ + if (action == none) { + return false; + } + if (currentAction == none) { + currentAction = action; + currentSource = source; + performNextAction(); + return true; + } else { + if (currentAction != action || currentSource != source) { + std::pair result = queue.emplace(source, action); + return result.second; + } else { + return false; + } + } +} + +bool DialogQueue::cancelAction(const QString& source, DialogQueue::Action action) +{ + if (source == currentSource && action == currentAction) { + actionDone(); + return true; + } else { + Collection::iterator itr = collection.find(ActionId{source, action}); + if (itr != collection.end()) { + collection.erase(itr); + return true; + } else { + return false; + } + } +} + +void DialogQueue::performNextAction() +{ + switch (currentAction) { + case none: + actionDone(); + break; + case askPassword: { + QInputDialog* dialog = new QInputDialog(parent); + prompt = dialog; + connect(dialog, &QDialog::accepted, this, &DialogQueue::onPropmtAccepted); + connect(dialog, &QDialog::rejected, this, &DialogQueue::onPropmtRejected); + dialog->setInputMode(QInputDialog::TextInput); + dialog->setTextEchoMode(QLineEdit::Password); + dialog->setLabelText(tr("Input the password for account %1").arg(currentSource)); + dialog->setWindowTitle(tr("Password for account %1").arg(currentSource)); + dialog->setTextValue(""); + dialog->exec(); + } + break; + case askCredentials: { + CredentialsPrompt* dialog = new CredentialsPrompt(parent); + prompt = dialog; + connect(dialog, &QDialog::accepted, this, &DialogQueue::onPropmtAccepted); + connect(dialog, &QDialog::rejected, this, &DialogQueue::onPropmtRejected); + const Models::Account* acc = roster.getAccountConst(currentSource); + dialog->setAccount(currentSource); + dialog->setLogin(acc->getLogin()); + dialog->setPassword(acc->getPassword()); + dialog->exec(); + } + break; + } +} + +void DialogQueue::onPropmtAccepted() +{ + switch (currentAction) { + case none: + break; + case askPassword: { + QInputDialog* dialog = static_cast(prompt); + emit responsePassword(currentSource, dialog->textValue()); + } + break; + case askCredentials: { + CredentialsPrompt* dialog = static_cast(prompt); + emit modifyAccountRequest(currentSource, { + {"login", dialog->getLogin()}, + {"password", dialog->getPassword()} + }); + } + break; + } + actionDone(); +} + +void DialogQueue::onPropmtRejected() +{ + switch (currentAction) { + case none: + break; + case askPassword: + case askCredentials: + emit disconnectAccount(currentSource); + break; + } + actionDone(); +} + +void DialogQueue::actionDone() +{ + prompt->deleteLater(); + prompt = nullptr; + + if (queue.empty()) { + currentAction = none; + currentSource = ""; + } else { + Sequence::iterator itr = sequence.begin(); + currentAction = itr->action; + currentSource = itr->source; + sequence.erase(itr); + performNextAction(); + } +} + +DialogQueue::ActionId::ActionId(const QString& p_source, DialogQueue::Action p_action): + source(p_source), + action(p_action) {} + +bool DialogQueue::ActionId::operator < (const DialogQueue::ActionId& other) const +{ + if (action == other.action) { + return source < other.source; + } else { + return action < other.action; + } +} + +DialogQueue::ActionId::ActionId(const DialogQueue::ActionId& other): + source(other.source), + action(other.action) {} diff --git a/main/dialogqueue.h b/main/dialogqueue.h new file mode 100644 index 0000000..b0da9dc --- /dev/null +++ b/main/dialogqueue.h @@ -0,0 +1,101 @@ +// Squawk messenger. +// Copyright (C) 2019 Yury Gubich +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef DIALOGQUEUE_H +#define DIALOGQUEUE_H + +#include +#include +#include + +#include +#include +#include + +#include +#include + +class DialogQueue : public QObject +{ + Q_OBJECT +public: + enum Action { + none, + askPassword, + askCredentials + }; + + DialogQueue(const Models::Roster& roster); + ~DialogQueue(); + + bool addAction(const QString& source, Action action); + bool cancelAction(const QString& source, Action action); + +signals: + void modifyAccountRequest(const QString&, const QMap&); + void responsePassword(const QString& account, const QString& password); + void disconnectAccount(const QString&); + +public: + void setParentWidnow(QMainWindow* parent); + void quit(); + +private: + void performNextAction(); + void actionDone(); + +private slots: + void onPropmtAccepted(); + void onPropmtRejected(); + +private: + QString currentSource; + Action currentAction; + + struct ActionId { + public: + ActionId(const QString& p_source, Action p_action); + ActionId(const ActionId& other); + + const QString source; + const Action action; + + bool operator < (const ActionId& other) const; + }; + + typedef boost::multi_index_container < + ActionId, + boost::multi_index::indexed_by < + boost::multi_index::ordered_unique < + boost::multi_index::identity + >, + boost::multi_index::sequenced<> + > + > Queue; + + typedef Queue::nth_index<0>::type Collection; + typedef Queue::nth_index<1>::type Sequence; + + Queue queue; + Collection& collection; + Sequence& sequence; + + QDialog* prompt; + QMainWindow* parent; + const Models::Roster& roster; +}; + +#endif // DIALOGQUEUE_H diff --git a/main/main.cpp b/main/main.cpp new file mode 100644 index 0000000..60b3c83 --- /dev/null +++ b/main/main.cpp @@ -0,0 +1,140 @@ +/* + * Squawk messenger. + * Copyright (C) 2019 Yury Gubich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "shared/global.h" +#include "shared/messageinfo.h" +#include "shared/pathcheck.h" +#include "main/application.h" +#include "core/signalcatcher.h" +#include "core/squawk.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char *argv[]) +{ + qRegisterMetaType("Shared::Message"); + qRegisterMetaType("Shared::MessageInfo"); + qRegisterMetaType("Shared::VCard"); + qRegisterMetaType>("std::list"); + qRegisterMetaType>("std::list"); + qRegisterMetaType>("QSet"); + qRegisterMetaType("Shared::ConnectionState"); + qRegisterMetaType("Shared::Availability"); + + QApplication app(argc, argv); + SignalCatcher sc(&app); + + QApplication::setApplicationName("squawk"); + QApplication::setOrganizationName("macaw.me"); + QApplication::setApplicationDisplayName("Squawk"); + QApplication::setApplicationVersion("0.2.2"); + app.setDesktopFileName("squawk"); + + QTranslator qtTranslator; + qtTranslator.load("qt_" + QLocale::system().name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath)); + app.installTranslator(&qtTranslator); + + QTranslator myappTranslator; + QStringList shares = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); + bool found = false; + for (QString share : shares) { + found = myappTranslator.load(QLocale(), QLatin1String("squawk"), ".", share + "/l10n"); + if (found) { + break; + } + } + if (!found) { + myappTranslator.load(QLocale(), QLatin1String("squawk"), ".", QCoreApplication::applicationDirPath()); + } + + app.installTranslator(&myappTranslator); + + QIcon icon; + icon.addFile(":images/logo.svg", QSize(16, 16)); + icon.addFile(":images/logo.svg", QSize(24, 24)); + icon.addFile(":images/logo.svg", QSize(32, 32)); + icon.addFile(":images/logo.svg", QSize(48, 48)); + icon.addFile(":images/logo.svg", QSize(64, 64)); + icon.addFile(":images/logo.svg", QSize(96, 96)); + icon.addFile(":images/logo.svg", QSize(128, 128)); + icon.addFile(":images/logo.svg", QSize(256, 256)); + icon.addFile(":images/logo.svg", QSize(512, 512)); + QApplication::setWindowIcon(icon); + + new Shared::Global(); //translates enums + + QSettings settings; + QVariant vs = settings.value("style"); + if (vs.isValid()) { + QString style = vs.toString().toLower(); + if (style != "system") { + Shared::Global::setStyle(style); + } + } + if (Shared::Global::supported("colorSchemeTools")) { + QVariant vt = settings.value("theme"); + if (vt.isValid()) { + QString theme = vt.toString(); + if (theme.toLower() != "system") { + Shared::Global::setTheme(theme); + } + } + } + QString path = Shared::downloadsPathCheck(); + if (path.size() > 0) { + settings.setValue("downloadsPath", path); + } else { + qDebug() << "couldn't initialize directory for downloads, quitting"; + return -1; + } + + Core::Squawk* squawk = new Core::Squawk(); + QThread* coreThread = new QThread(); + squawk->moveToThread(coreThread); + + Application application(squawk); + + QObject::connect(&sc, &SignalCatcher::interrupt, &application, &Application::quit); + + QObject::connect(coreThread, &QThread::started, squawk, &Core::Squawk::start); + QObject::connect(&application, &Application::quitting, squawk, &Core::Squawk::stop); + //QObject::connect(&app, &QApplication::aboutToQuit, &w, &QMainWindow::close); + QObject::connect(squawk, &Core::Squawk::quit, squawk, &Core::Squawk::deleteLater); + QObject::connect(squawk, &Core::Squawk::destroyed, coreThread, &QThread::quit, Qt::QueuedConnection); + QObject::connect(coreThread, &QThread::finished, &app, &QApplication::quit, Qt::QueuedConnection); + + coreThread->start(); + int result = app.exec(); + + if (coreThread->isRunning()) { + //coreThread->wait(); + //todo if I uncomment that, the app will not quit if it has reconnected at least once + //it feels like a symptom of something badly desinged in the core thread + //need to investigate; + } + + return result; +} + diff --git a/packaging/Archlinux/PKGBUILD b/packaging/Archlinux/PKGBUILD index a8da388..7db43ff 100644 --- a/packaging/Archlinux/PKGBUILD +++ b/packaging/Archlinux/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: Yury Gubich pkgname=squawk -pkgver=0.2.0 +pkgver=0.2.2 pkgrel=1 pkgdesc="An XMPP desktop messenger, written on pure c++ (qt)" arch=('i686' 'x86_64') @@ -14,7 +14,7 @@ optdepends=('kwallet: secure password storage (requires rebuild)' 'kio: better show in folder action (requires rebuild)') source=("$pkgname-$pkgver.tar.gz") -sha256sums=('8e93d3dbe1fc35cfecb7783af409c6a264244d11609b2241d4fe77d43d068419') +sha256sums=('e4fa2174a3ba95159cc3b0bac3f00550c9e0ce971c55334e2662696a4543fc7e') build() { cd "$srcdir/squawk" cmake . -D CMAKE_INSTALL_PREFIX=/usr -D CMAKE_BUILD_TYPE=Release @@ -22,5 +22,5 @@ build() { } package() { cd "$srcdir/squawk" - DESTDIR="$pkgdir/" cmake --build . --target install + DESTDIR="$pkgdir/" cmake --build . --target install } diff --git a/shared/global.cpp b/shared/global.cpp index 122bc79..6519952 100644 --- a/shared/global.cpp +++ b/shared/global.cpp @@ -19,6 +19,7 @@ #include "global.h" #include "enums.h" +#include "ui/models/roster.h" Shared::Global* Shared::Global::instance = 0; const std::set Shared::Global::supportedImagesExts = {"png", "jpg", "webp", "jpeg", "gif", "svg"}; diff --git a/shared/global.h b/shared/global.h index 2056639..ebed931 100644 --- a/shared/global.h +++ b/shared/global.h @@ -47,7 +47,6 @@ namespace Shared { class Global { Q_DECLARE_TR_FUNCTIONS(Global) - public: struct FileInfo { enum class Preview { @@ -64,7 +63,7 @@ namespace Shared { }; Global(); - + static Global* getInstance(); static QString getName(Availability av); static QString getName(ConnectionState cs); diff --git a/shared/utils.cpp b/shared/utils.cpp index a7a4ecb..518d288 100644 --- a/shared/utils.cpp +++ b/shared/utils.cpp @@ -40,5 +40,5 @@ QString Shared::processMessageBody(const QString& msg) { QString processed = msg.toHtmlEscaped(); processed.replace(urlReg, "\\1"); - return "

" + processed + "

"; + return "

" + processed + "

"; } diff --git a/shared/utils.h b/shared/utils.h index 564e2e6..0329cee 100644 --- a/shared/utils.h +++ b/shared/utils.h @@ -69,6 +69,12 @@ static const std::vector colorPalette = { QColor(17, 17, 80), QColor(54, 54, 94) }; + +enum class Hover { + nothing, + text, + anchor +}; } #endif // SHARED_UTILS_H diff --git a/translations/CMakeLists.txt b/translations/CMakeLists.txt index 86d2a8c..f70fe2b 100644 --- a/translations/CMakeLists.txt +++ b/translations/CMakeLists.txt @@ -1,11 +1,12 @@ find_package(Qt5LinguistTools) set(TS_FILES + squawk.en.ts squawk.ru.ts squawk.pt_BR.ts ) qt5_add_translation(QM_FILES ${TS_FILES}) add_custom_target(translations ALL DEPENDS ${QM_FILES}) -install(FILES ${QM_FILES} DESTINATION ${CMAKE_INSTALL_DATADIR}/squawk/l10n) +install(FILES ${QM_FILES} DESTINATION ${CMAKE_INSTALL_DATADIR}/macaw.me/squawk/l10n) add_dependencies(${CMAKE_PROJECT_NAME} translations) diff --git a/translations/squawk.en.ts b/translations/squawk.en.ts new file mode 100644 index 0000000..b219af0 --- /dev/null +++ b/translations/squawk.en.ts @@ -0,0 +1,1420 @@ + + + + + About + + + + About Squawk + About window header + About Squawk + + + + + Squawk + Squawk + + + + + About + Tab title + About + + + + + XMPP (jabber) messenger + XMPP (jabber) messenger + + + + + (c) 2019 - 2022, Yury Gubich + (c) 2019 - 2022, Yury Gubich + + + + + <a href="https://git.macaw.me/blue/squawk">Project site</a> + <a href="https://git.macaw.me/blue/squawk">Project site</a> + + + + + <a href="https://git.macaw.me/blue/squawk/src/branch/master/LICENSE.md">License: GNU General Public License version 3</a> + <a href="https://git.macaw.me/blue/squawk/src/branch/master/LICENSE.md">License: GNU General Public License version 3</a> + + + + + Components + Tab header + Components + + + + + + + + + Version + Version + + + + + + + + + 0.0.0 + 0.0.0 + + + + + Report Bugs + Report Bugs + + + + + Please report any bug you find! +To report bugs you can use: + Please report any bug you find! +To report bugs you can use: + + + + + <a href="https://git.macaw.me/blue/squawk/issues">Project bug tracker</> + <a href="https://git.macaw.me/blue/squawk/issues">Project bug tracker</> + + + + + XMPP (<a href="xmpp:blue@macaw.me">blue@macaw.me</a>) + XMPP (<a href="xmpp:blue@macaw.me">blue@macaw.me</a>) + + + + + E-Mail (<a href="mailto:blue@macaw.me">blue@macaw.me</a>) + E-Mail (<a href="mailto:blue@macaw.me">blue@macaw.me</a>) + + + + + Thanks To + Thanks to + + + + + Vae + Vae + + + + + Major refactoring, bug fixes, constructive criticism + Major refactoring, bug fixes, constructive criticism + + + + + Shunf4 + Shunf4 + + + + + Major refactoring, bug fixes, build adaptations for Windows and MacOS + Major refactoring, bug fixes, build adaptations for Windows and MacOS + + + + + Bruno F. Fontes + Bruno F. Fontes + + + + + Brazilian Portuguese translation + Brazilian Portuguese translation + + + + + (built against %1) + (built against %1) + + + + License + License + + + + Account + + + + Account + Window title + Account + + + + + Your account login + Tooltip + Your account login + + + + + john_smith1987 + Login placeholder + john_smith1987 + + + + + Server + Server + + + + + A server address of your account. Like 404.city or macaw.me + Tooltip + A server address of your account. Like 404.city or macaw.me + + + + + macaw.me + Placeholder + macaw.me + + + + + Login + Login + + + + + Password + Password + + + + + Password of your account + Tooltip + Password of your account + + + + + Name + Name + + + + + Just a name how would you call this account, doesn't affect anything + Just a name how would you call this account, doesn't affect anything (cant be changed) + + + + + John + Placeholder + John + + + + + Resource + Resource + + + + + A resource name like "Home" or "Work" + Tooltip + A resource name like "Home" or "Work" + + + + + QXmpp + Default resource + QXmpp + + + + + Password storage + Password storage + + + + + Active + Active + + + + + enable + enable + + + + Accounts + + + + Accounts + Accounts + + + + + Delete + Delete + + + + + Add + Add + + + + + Edit + Edit + + + + + Change password + Change password + + + + + Connect + Connect + + + + Deactivate + Deactivate + + + + + Activate + Activate + + + + Application + + + from + from + + + + Attached file + Attached file + + + + Mark as Read + Mark as Read + + + + Open conversation + Open conversation + + + + Conversation + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Noto Sans'; font-size:8pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Noto Sans'; font-size:8pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> + + + + + Type your message here... + Placeholder + Type your message here... + + + + Paste Image + Paste Image + + + + Drop files here to attach them to your message + Drop files here to attach them to your message + + + + Chose a file to send + Chose a file to send + + + + Try sending again + Try sending again + + + + Copy selected + Copy selected + + + + Copy message + Copy message + + + + Open + Open + + + + Show in folder + Show in folder + + + + Edit + Edit + + + + Editing message... + Editing message... + + + + CredentialsPrompt + + + + Authentication error: %1 + Window title + Authentication error: %1 + + + + + Couldn't authenticate account %1: login or password is icorrect. +Would you like to check them and try again? + Couldn't authenticate account %1: login or password is incorrect. +Would you like to check them and try again? + + + + + Login + Login + + + + + Your account login (without @server.domain) + Tooltip + Your account login (without @server.domain) + + + + + Password + Password + + + + + Your password + Password + + + + DialogQueue + + + Input the password for account %1 + Input the password for account %1 + + + + Password for account %1 + Password for account %1 + + + + Global + + + Online + Availability + Online + + + + Away + Availability + Away + + + + Absent + Availability + Absent + + + + Busy + Availability + Busy + + + + Chatty + Availability + Chatty + + + + Invisible + Availability + Invisible + + + + Offline + Availability + Offline + + + + Disconnected + ConnectionState + Disconnected + + + + Connecting + ConnectionState + Connecting + + + + Connected + ConnectionState + Connected + + + + Error + ConnectionState + Error + + + + None + SubscriptionState + None + + + + From + SubscriptionState + From + + + + To + SubscriptionState + To + + + + Both + SubscriptionState + Both + + + + Unknown + SubscriptionState + Unknown + + + + Unspecified + Affiliation + Unspecified + + + + Outcast + Affiliation + Outcast + + + + Nobody + Affiliation + Nobody + + + + Member + Affiliation + Member + + + + Admin + Affiliation + Admin + + + + Owner + Affiliation + Owner + + + + Unspecified + Role + Unspecified + + + + Nobody + Role + Nobody + + + + Visitor + Role + Visitor + + + + Participant + Role + Participant + + + + Moderator + Role + Moderator + + + + Pending + MessageState + Pending + + + + Sent + MessageState + Sent + + + + Delivered + MessageState + Delivered + + + + Error + MessageState + Error + + + + Plain + AccountPassword + Plain + + + + Jammed + AccountPassword + Jammed + + + + Always Ask + AccountPassword + Always Ask + + + + KWallet + AccountPassword + KWallet + + + + Your password is going to be stored in config file in plain text + AccountPasswordDescription + Your password is going to be stored in config file in plain text + + + + Your password is going to be stored in config file but jammed with constant encryption key you can find in program source code. It might look like encryption but it's not + AccountPasswordDescription + Your password is going to be stored in config file but jammed with constant encryption key you can find in program source code. It might look like encryption but it's not + + + + Squawk is going to query you for the password on every start of the program + AccountPasswordDescription + Squawk is going to query you for the password on every start of the program + + + + Your password is going to be stored in KDE wallet storage (KWallet). You're going to be queried for permissions + AccountPasswordDescription + Your password is going to be stored in KDE wallet storage (KWallet). You're going to be queried for permissions + + + + JoinConference + + + + Join new conference + Join new conference + + + + + JID + JID + + + + + Room JID + Room JID + + + + + identifier@conference.server.org + identifier@conference.server.org + + + + + Account + Account + + + + + Join on login + Join on login + + + + + If checked Squawk will try to join this conference on login + If checked Squawk will try to join this conference on login + + + + + Nick name + Nick name + + + + + Your nick name for that conference. If you leave this field empty your account name will be used as a nick name + Your nick name for that conference. If you leave this field empty your account name will be used as a nick name + + + + + John + John + + + + MessageLine + + + + Download + Download + + + + Models::Room + + + Subscribed + Subscribed + + + + Temporarily unsubscribed + Temporarily unsubscribed + + + + Temporarily subscribed + Temporarily subscribed + + + + Unsubscribed + Unsubscribed + + + + Models::Roster + + + New messages + New messages + + + + + + New messages: + New messages: + + + + + Jabber ID: + Jabber ID: + + + + + + Availability: + Availability: + + + + + + Status: + Status: + + + + + + Subscription: + Subscription: + + + + Affiliation: + Affiliation: + + + + Role: + Role: + + + + Online contacts: + Online contacts: + + + + Total contacts: + Total contacts: + + + + Members: + Members: + + + + NewContact + + + + Add new contact + Window title + Add new contact + + + + + Account + Account + + + + + An account that is going to have new contact + An account that is going to have new contact + + + + + JID + JID + + + + + Jabber id of your new contact + Jabber id of your new contact + + + + + name@server.dmn + Placeholder + name@server.dmn + + + + + Name + Name + + + + + The way this new contact will be labeled in your roster (optional) + The way this new contact will be labeled in your roster (optional) + + + + + John Smith + John Smith + + + + PageAppearance + + + + Theme + Style + + + + + Color scheme + Color scheme + + + + + + + System + System + + + + PageGeneral + + + + Downloads path + Downloads path + + + + + Browse + Browse + + + + Select where downloads folder is going to be + Select where downloads folder is going to be + + + + Settings + + + + Preferences + Window title + Preferences + + + + + + + General + General + + + + + Appearance + Appearance + + + + + Apply + Apply + + + + + Cancel + Cancel + + + + + Ok + Ok + + + + Squawk + + + + squawk + Squawk + + + + + Please select a contact to start chatting + Please select a contact to start chatting + + + + + Settings + Settings + + + + + Squawk + Menu bar entry + Squawk + + + + + Help + Help + + + + + Accounts + Accounts + + + + + Quit + Quit + + + + + Add contact + Add contact + + + + + Add conference + Join conference + + + + + Preferences + Preferences + + + + + About Squawk + About Squawk + + + + Deactivate + Deactivate + + + + Activate + Activate + + + + + VCard + VCard + + + + + + Remove + Remove + + + + Open dialog + Open dialog + + + + + Unsubscribe + Unsubscribe + + + + + Subscribe + Subscribe + + + + Rename + Rename + + + + Input new name for %1 +or leave it empty for the contact +to be displayed as %1 + Input new name for %1 +or leave it empty for the contact +to be displayed as %1 + + + + Renaming %1 + Renaming %1 + + + + Groups + Groups + + + + New group + New group + + + + New group name + New group name + + + + Add %1 to a new group + Add %1 to a new group + + + + Open conversation + Open conversation + + + + %1 account card + %1 account card + + + + %1 contact card + %1 contact card + + + + Downloading vCard + Downloading vCard + + + + VCard + + + + Received 12.07.2007 at 17.35 + Never updated + + + + + + + General + General + + + + + Organization + Organization + + + + + Middle name + Middle name + + + + + First name + First name + + + + + Last name + Last name + + + + + Nick name + Nick name + + + + + Birthday + Birthday + + + + + Organization name + Organization name + + + + + Unit / Department + Unit / Department + + + + + Role / Profession + Role / Profession + + + + + Job title + Job title + + + + + Full name + Full name + + + + + Personal information + Personal information + + + + + + + Contact + Contact + + + + + Addresses + Addresses + + + + + E-Mail addresses + E-Mail addresses + + + + + Jabber ID + Jabber ID + + + + + Web site + Web site + + + + + Phone numbers + Phone numbers + + + + + + + Description + Description + + + + + Set avatar + Set avatar + + + + + Clear avatar + Clear avatar + + + + Account %1 card + Account %1 card + + + + Contact %1 card + Contact %1 card + + + + Received %1 at %2 + Received %1 at %2 + + + + Chose your new avatar + Chose your new avatar + + + + Images (*.png *.jpg *.jpeg) + Images (*.png *.jpg *.jpeg) + + + + Add email address + Add email address + + + + Unset this email as preferred + Unset this email as preferred + + + + Set this email as preferred + Set this email as preferred + + + + Remove selected email addresses + Remove selected email addresses + + + + Copy selected emails to clipboard + Copy selected emails to clipboard + + + + Add phone number + Add phone number + + + + Unset this phone as preferred + Unset this phone as preferred + + + + Set this phone as preferred + Set this phone as preferred + + + + Remove selected phone numbers + Remove selected phone numbers + + + + Copy selected phones to clipboard + Copy selected phones to clipboard + + + diff --git a/translations/squawk.pt_BR.ts b/translations/squawk.pt_BR.ts index 4330979..6041678 100644 --- a/translations/squawk.pt_BR.ts +++ b/translations/squawk.pt_BR.ts @@ -1,6 +1,108 @@ + + About + + About Squawk + Sorbe Squawk + + + Squawk + Squawk + + + About + Tab title + Sobre + + + XMPP (jabber) messenger + XMPP (jabber) mensageiro + + + (c) 2019 - 2022, Yury Gubich + (c) 2019 - 2022, Yury Gubich + + + <a href="https://git.macaw.me/blue/squawk">Project site</a> + <a href="https://git.macaw.me/blue/squawk">Site do projeto</a> + + + <a href="https://git.macaw.me/blue/squawk/src/branch/master/LICENSE.md">License: GNU General Public License version 3</a> + <a href="https://git.macaw.me/blue/squawk/src/branch/master/LICENSE.md">Licença: GNU General Public License versão 3</a> + + + Components + Componentes + + + Version + Versão + + + 0.0.0 + 0.0.0 + + + Report Bugs + Relatório de erros + + + Please report any bug you find! +To report bugs you can use: + Por favor reportar qualquer erro que você encontrar! +Para relatar bugs você pode usar: + + + <a href="https://git.macaw.me/blue/squawk/issues">Project bug tracker</> + <a href="https://git.macaw.me/blue/squawk/issues">Rastreador de bugs do projeto</> + + + XMPP (<a href="xmpp:blue@macaw.me">blue@macaw.me</a>) + XMPP (<a href="xmpp:blue@macaw.me">blue@macaw.me</a>) + + + E-Mail (<a href="mailto:blue@macaw.me">blue@macaw.me</a>) + E-Mail (<a href="mailto:blue@macaw.me">blue@macaw.me</a>) + + + Thanks To + Graças ao + + + Vae + Vae + + + Major refactoring, bug fixes, constructive criticism + Refatoração importante, correção de erros, críticas construtivas + + + Shunf4 + Shunf4 + + + Major refactoring, bug fixes, build adaptations for Windows and MacOS + Refatoração importante, correção de erros, adaptações de construção para Windows e MacOS + + + Bruno F. Fontes + Bruno F. Fontes + + + Brazilian Portuguese translation + Tradução para o português do Brasil + + + (built against %1) + (Versão durante a compilação %1) + + + License + Licença + + Account @@ -10,10 +112,12 @@ Your account login + Tooltip Suas informações de login john_smith1987 + Login placeholder josé_silva1987 @@ -22,10 +126,12 @@ A server address of your account. Like 404.city or macaw.me + Tooltip O endereço do servidor da sua conta, como o 404.city ou o macaw.me macaw.me + Placeholder macaw.me @@ -38,6 +144,7 @@ Password of your account + Tooltip Senha da sua conta @@ -46,10 +153,11 @@ Just a name how would you call this account, doesn't affect anything - Apenas um nome para identificar esta conta. Não influencia em nada + Apenas um nome para identificar esta conta. Não influencia em nada (não pode ser mudado) John + Placeholder José @@ -58,6 +166,7 @@ A resource name like "Home" or "Work" + Tooltip Um nome de recurso como "Casa" ou "Trabalho" @@ -69,6 +178,14 @@ Password storage Armazenamento de senha + + Active + Ativo + + + enable + habilitar + Accounts @@ -98,30 +215,135 @@ Disconnect - Desconectar + Desconectar + + + Deactivate + Desativar + + + Activate + Ativar + + + + Application + + from + de + + + Attached file + Arquivo anexado + + + Mark as Read + Marcar como lido + + + Open conversation + Abrir conversa Conversation Type your message here... + Placeholder Digite sua mensagem aqui... Chose a file to send Escolha um arquivo para enviar + + Drop files here to attach them to your message + Arraste seus arquivos aqui para anexá-los a sua mensagem + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Liberation Sans'; font-size:10pt; font-weight:400; font-style:normal;"> +</style></head><body style=" font-family:'Noto Sans'; font-size:8pt; font-weight:400; font-style:normal;"> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Noto Sans'; font-size:8pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> - Drop files here to attach them to your message - Arraste seus arquivos aqui para anexá-los a sua mensagem + Paste Image + Colar imagem + + + Try sending again + Tente enviar de novo + + + Copy selected + Copiar selecionado + + + Copy message + Copiar mensagem + + + Open + Abrir + + + Show in folder + Show in explorer + + + Edit + Editar + + + Editing message... + Messae está sendo editado... + + + + CredentialsPrompt + + Authentication error: %1 + Window title + Erro de autenticação: %1 + + + Couldn't authenticate account %1: login or password is icorrect. +Would you like to check them and try again? + Não foi possível autenticar a conta %1: login ou senha incorretos. +Deseja verificá-los e tentar novamente? + + + Login + Usuário + + + Your account login (without @server.domain) + Suas informações de login (sem @server.domain) + + + Password + Senha + + + Your password + Senha da sua conta + + + + DialogQueue + + Input the password for account %1 + Digite a senha para a conta %1 + + + Password for account %1 + Senha para a conta %1 @@ -370,14 +592,14 @@ p, li { white-space: pre-wrap; } Message Open - Abrir + Abrir MessageLine Downloading... - Baixando... + Baixando... Download @@ -386,28 +608,28 @@ p, li { white-space: pre-wrap; } Error uploading file: %1 You can try again - Error fazendo upload do arquivo: + Error fazendo upload do arquivo: %1 Você pode tentar novamente Upload - Upload + Upload Error downloading file: %1 You can try again - Erro baixando arquivo: + Erro baixando arquivo: %1 Você pode tentar novamente Uploading... - Fazendo upload... + Fazendo upload... Push the button to download the file - Pressione o botão para baixar o arquivo + Pressione o botão para baixar o arquivo @@ -481,7 +703,7 @@ Você pode tentar novamente NewContact Add new contact - Заголовок окна + Window title Adicionar novo contato @@ -502,7 +724,7 @@ Você pode tentar novamente name@server.dmn - Placeholder поля ввода JID + Placeholder nome@servidor.com.br @@ -518,6 +740,66 @@ Você pode tentar novamente José Silva + + PageAppearance + + Theme + Estilo + + + Color scheme + Esquema de cores + + + System + Do sistema + + + + PageGeneral + + Downloads path + Pasta de downloads + + + Browse + 6 / 5,000 +Translation results +Navegar + + + Select where downloads folder is going to be + Selecione onde a pasta de downloads ficará + + + + Settings + + Preferences + Window title + Preferências + + + General + Geral + + + Appearance + Aparência + + + Apply + Aplicar + + + Cancel + Cancelar + + + Ok + Feito + + Squawk @@ -530,6 +812,7 @@ Você pode tentar novamente Squawk + Menu bar entry Squawk @@ -550,11 +833,11 @@ Você pode tentar novamente Disconnect - Desconectar + Desconectar Connect - Conectar + Conectar VCard @@ -627,26 +910,46 @@ ser exibido com Attached file - Arquivo anexado + Arquivo anexado Input the password for account %1 - Digite a senha para a conta %1 + Digite a senha para a conta %1 Password for account %1 - Senha para a conta %1 + Senha para a conta %1 Please select a contact to start chatting Por favor selecione um contato para começar a conversar + + Help + Ajuda + + + Preferences + Preferências + + + About Squawk + Sorbe Squawk + + + Deactivate + Desativar + + + Activate + Ativar + VCard Received 12.07.2007 at 17.35 - Recebido 12/07/2007 às 17:35 + Nunca atualizado General @@ -682,7 +985,7 @@ ser exibido com Unit / Department - Unidade/Departamento + Unidade / Departamento Role / Profession diff --git a/translations/squawk.ru.ts b/translations/squawk.ru.ts index e3b4d52..39dbfac 100644 --- a/translations/squawk.ru.ts +++ b/translations/squawk.ru.ts @@ -1,6 +1,108 @@ + + About + + About Squawk + О Программе Squawk + + + Squawk + Squawk + + + About + Tab title + Общее + + + XMPP (jabber) messenger + XMPP (jabber) мессенджер + + + (c) 2019 - 2022, Yury Gubich + (c) 2019 - 2022, Юрий Губич + + + <a href="https://git.macaw.me/blue/squawk">Project site</a> + <a href="https://git.macaw.me/blue/squawk">Сайт проекта</a> + + + <a href="https://git.macaw.me/blue/squawk/src/branch/master/LICENSE.md">License: GNU General Public License version 3</a> + <a href="https://git.macaw.me/blue/squawk/src/branch/master/LICENSE.md">Лицензия: GNU General Public License версия 3</a> + + + Components + Компоненты + + + Version + Версия + + + 0.0.0 + 0.0.0 + + + Report Bugs + Сообщать об ошибках + + + Please report any bug you find! +To report bugs you can use: + Пожалуйста, сообщайте о любых ошибках! +Способы сообщить об ошибках: + + + <a href="https://git.macaw.me/blue/squawk/issues">Project bug tracker</> + <a href="https://git.macaw.me/blue/squawk/issues">Баг-трекер проекта</> + + + XMPP (<a href="xmpp:blue@macaw.me">blue@macaw.me</a>) + XMPP (<a href="xmpp:blue@macaw.me">blue@macaw.me</a>) + + + E-Mail (<a href="mailto:blue@macaw.me">blue@macaw.me</a>) + E-Mail (<a href="mailto:blue@macaw.me">blue@macaw.me</a>) + + + Thanks To + Благодарности + + + Vae + Vae + + + Major refactoring, bug fixes, constructive criticism + Крупный рефакторинг, исправление ошибок, конструктивная критика + + + Shunf4 + Shunf4 + + + Major refactoring, bug fixes, build adaptations for Windows and MacOS + Крупный рефакторинг, исправление ошибок, адаптация сборки под Windows and MacOS + + + Bruno F. Fontes + Bruno F. Fontes + + + Brazilian Portuguese translation + Перевод на Португальский (Бразилия) + + + (built against %1) + (версия при сборке %1) + + + License + Лицензия + + Account @@ -10,10 +112,12 @@ Your account login + Tooltip Имя пользователя Вашей учетной записи john_smith1987 + Login placeholder ivan_ivanov1987 @@ -22,10 +126,12 @@ A server address of your account. Like 404.city or macaw.me + Tooltip Адресс сервера вашей учетной записи (выглядит как 404.city или macaw.me) macaw.me + Placeholder macaw.me @@ -38,6 +144,7 @@ Password of your account + Tooltip Пароль вашей учетной записи @@ -46,10 +153,11 @@ Just a name how would you call this account, doesn't affect anything - Просто имя, то как Вы называете свою учетную запись, может быть любым + Просто имя, то как Вы называете свою учетную запись, может быть любым (нельзя поменять) John + Placeholder Иван @@ -58,6 +166,7 @@ A resource name like "Home" or "Work" + Tooltip Имя этой программы для ваших контактов, может быть "Home" или "Phone" @@ -69,6 +178,14 @@ Password storage Хранение пароля + + Active + Активен + + + enable + включен + Accounts @@ -98,30 +215,139 @@ Disconnect - Отключить + Отключить + + + Deactivate + Деактивировать + + + Activate + Активировать + + + + Application + + from + от + + + Attached file + Прикрепленный файл + + + Mark as Read + Пометить прочитанным + + + Open conversation + Открыть окно беседы Conversation Type your message here... + Placeholder Введите сообщение... Chose a file to send Выберите файл для отправки + + Drop files here to attach them to your message + Бросьте файлы сюда для того что бы прикрепить их к сообщению + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Liberation Sans'; font-size:10pt; font-weight:400; font-style:normal;"> +</style></head><body style=" font-family:'Noto Sans'; font-size:8pt; font-weight:400; font-style:normal;"> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Noto Sans'; font-size:8pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> - Drop files here to attach them to your message - Бросьте файлы сюда для того что бы прикрепить их к сообщению + Paste Image + Вставить изображение + + + Try sending again + Отправить снова + + + Copy selected + Скопировать выделенное + + + Copy message + Скопировать сообщение + + + Open + Открыть + + + Show in folder + Показать в проводнике + + + Edit + Редактировать + + + Editing message... + Сообщение редактируется... + + + + CredentialsPrompt + + Authentication error: %1 + Window title + Ошибка аутентификации: %1 + + + Couldn't authenticate account %1: login or password is icorrect. +Would you like to check them and try again? + Не получилось аутентифицировать +учетную запись %1: +имя пользователя или пароль введены неверно. +Желаете ли проверить их и +попробовать аутентифицироваться еще раз? + + + Login + Имя учетной записи + + + Your account login (without @server.domain) + Tooltip + Имя вашей учтетной записи (без @server.domain) + + + Password + Пароль + + + Your password + Ваш пароль + + + + DialogQueue + + Input the password for account %1 + Введите пароль для учетной записи %1 + + + Password for account %1 + Пароль для учетной записи %1 @@ -370,14 +596,14 @@ p, li { white-space: pre-wrap; } Message Open - Открыть + Открыть MessageLine Downloading... - Скачивается... + Скачивается... Download @@ -386,28 +612,28 @@ p, li { white-space: pre-wrap; } Error uploading file: %1 You can try again - Ошибка загрузки файла на сервер: + Ошибка загрузки файла на сервер: %1 Для того, что бы попробовать снова нажмите на кнопку Upload - Загрузить + Загрузить Error downloading file: %1 You can try again - Ошибка скачивания файла: + Ошибка скачивания файла: %1 Вы можете попробовать снова Uploading... - Загружается... + Загружается... Push the button to download the file - Нажмите на кнопку что бы загрузить файл + Нажмите на кнопку что бы загрузить файл @@ -481,7 +707,7 @@ You can try again NewContact Add new contact - Заголовок окна + Window title Добавление нового контакта @@ -502,7 +728,7 @@ You can try again name@server.dmn - Placeholder поля ввода JID + Placeholder name@server.dmn @@ -518,6 +744,64 @@ You can try again Иван Иванов + + PageAppearance + + Theme + Оформление + + + Color scheme + Цветовая схема + + + System + Системная + + + + PageGeneral + + Downloads path + Папка для сохраненных файлов + + + Browse + Выбрать + + + Select where downloads folder is going to be + Выберете папку, в которую будут сохраняться файлы + + + + Settings + + Preferences + Window title + Настройки + + + General + Общее + + + Appearance + Внешний вид + + + Apply + Применить + + + Cancel + Отменить + + + Ok + Готово + + Squawk @@ -530,6 +814,7 @@ You can try again Squawk + Menu bar entry Squawk @@ -550,11 +835,11 @@ You can try again Disconnect - Отключить + Отключить Connect - Подключить + Подключить VCard @@ -627,20 +912,40 @@ to be displayed as %1 Attached file - Прикрепленный файл + Прикрепленный файл Input the password for account %1 - Введите пароль для учетной записи %1 + Введите пароль для учетной записи %1 Password for account %1 - Пароль для учетной записи %1 + Пароль для учетной записи %1 Please select a contact to start chatting Выберите контакт или группу что бы начать переписку + + Help + Помощь + + + Preferences + Настройки + + + About Squawk + О Программе Squawk + + + Deactivate + Деактивировать + + + Activate + Активировать + VCard diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt index 36207b6..296c289 100644 --- a/ui/CMakeLists.txt +++ b/ui/CMakeLists.txt @@ -1,4 +1,8 @@ -target_sources(squawk PRIVATE squawk.cpp squawk.h squawk.ui) +target_sources(squawk PRIVATE + squawk.cpp + squawk.h + squawk.ui +) add_subdirectory(models) add_subdirectory(utils) diff --git a/ui/models/account.cpp b/ui/models/account.cpp index 43cb3ed..cf1efb4 100644 --- a/ui/models/account.cpp +++ b/ui/models/account.cpp @@ -32,7 +32,8 @@ Models::Account::Account(const QMap& data, Models::Item* pare state(Shared::ConnectionState::disconnected), availability(Shared::Availability::offline), passwordType(Shared::AccountPassword::plain), - wasEverConnected(false) + wasEverConnected(false), + active(false) { QMap::const_iterator sItr = data.find("state"); if (sItr != data.end()) { @@ -46,6 +47,10 @@ Models::Account::Account(const QMap& data, Models::Item* pare if (pItr != data.end()) { setPasswordType(pItr.value().toUInt()); } + QMap::const_iterator acItr = data.find("active"); + if (acItr != data.end()) { + setActive(acItr.value().toBool()); + } } Models::Account::~Account() @@ -176,6 +181,8 @@ QVariant Models::Account::data(int column) const return avatarPath; case 9: return Shared::Global::getName(passwordType); + case 10: + return active; default: return QVariant(); } @@ -183,7 +190,7 @@ QVariant Models::Account::data(int column) const int Models::Account::columnCount() const { - return 10; + return 11; } void Models::Account::update(const QString& field, const QVariant& value) @@ -208,6 +215,8 @@ void Models::Account::update(const QString& field, const QVariant& value) setAvatarPath(value.toString()); } else if (field == "passwordType") { setPasswordType(value.toUInt()); + } else if (field == "active") { + setActive(value.toBool()); } } @@ -281,3 +290,16 @@ void Models::Account::setPasswordType(unsigned int pt) { setPasswordType(Shared::Global::fromInt(pt)); } + +bool Models::Account::getActive() const +{ + return active; +} + +void Models::Account::setActive(bool p_active) +{ + if (active != p_active) { + active = p_active; + changed(10); + } +} diff --git a/ui/models/account.h b/ui/models/account.h index 3d2310f..ab2b629 100644 --- a/ui/models/account.h +++ b/ui/models/account.h @@ -58,6 +58,9 @@ namespace Models { void setAvatarPath(const QString& path); QString getAvatarPath() const; + + void setActive(bool active); + bool getActive() const; void setAvailability(Shared::Availability p_avail); void setAvailability(unsigned int p_avail); @@ -91,6 +94,7 @@ namespace Models { Shared::Availability availability; Shared::AccountPassword passwordType; bool wasEverConnected; + bool active; protected slots: void toOfflineState() override; diff --git a/ui/models/accounts.cpp b/ui/models/accounts.cpp index 4343481..463ab40 100644 --- a/ui/models/accounts.cpp +++ b/ui/models/accounts.cpp @@ -48,6 +48,10 @@ QVariant Models::Accounts::data (const QModelIndex& index, int role) const answer = Shared::connectionStateIcon(accs[index.row()]->getState()); } break; + case Qt::ForegroundRole: + if (!accs[index.row()]->getActive()) { + answer = qApp->palette().brush(QPalette::Disabled, QPalette::Text); + } default: break; } diff --git a/ui/models/contact.cpp b/ui/models/contact.cpp index a0c70ac..d5c7dc4 100644 --- a/ui/models/contact.cpp +++ b/ui/models/contact.cpp @@ -155,6 +155,16 @@ void Models::Contact::removePresence(const QString& name) } } +Models::Presence * Models::Contact::getPresence(const QString& name) +{ + QMap::iterator itr = presences.find(name); + if (itr == presences.end()) { + return nullptr; + } else { + return itr.value(); + } +} + void Models::Contact::refresh() { QDateTime lastActivity; diff --git a/ui/models/contact.h b/ui/models/contact.h index a8b80a3..c4fc131 100644 --- a/ui/models/contact.h +++ b/ui/models/contact.h @@ -51,6 +51,7 @@ public: void addPresence(const QString& name, const QMap& data); void removePresence(const QString& name); + Presence* getPresence(const QString& name); QString getContactName() const; QString getStatus() const; diff --git a/ui/models/element.cpp b/ui/models/element.cpp index 4e741a4..acea46f 100644 --- a/ui/models/element.cpp +++ b/ui/models/element.cpp @@ -134,6 +134,11 @@ unsigned int Models::Element::getMessagesCount() const return feed->unreadMessagesCount(); } +bool Models::Element::markMessageAsRead(const QString& id) const +{ + return feed->markMessageAsRead(id); +} + void Models::Element::addMessage(const Shared::Message& data) { feed->addMessage(data); @@ -171,6 +176,7 @@ void Models::Element::fileError(const QString& messageId, const QString& error, void Models::Element::onFeedUnreadMessagesCountChanged() { + emit unreadMessagesCountChanged(); if (type == contact) { changed(4); } else if (type == room) { diff --git a/ui/models/element.h b/ui/models/element.h index 94d67cb..c6d3d6e 100644 --- a/ui/models/element.h +++ b/ui/models/element.h @@ -42,6 +42,7 @@ public: void addMessage(const Shared::Message& data); void changeMessage(const QString& id, const QMap& data); unsigned int getMessagesCount() const; + bool markMessageAsRead(const QString& id) const; void responseArchive(const std::list list, bool last); bool isRoom() const; void fileProgress(const QString& messageId, qreal value, bool up); @@ -52,6 +53,7 @@ signals: void requestArchive(const QString& before); void fileDownloadRequest(const QString& url); void unnoticedMessage(const QString& account, const Shared::Message& msg); + void unreadMessagesCountChanged(); void localPathInvalid(const QString& path); protected: diff --git a/ui/models/room.cpp b/ui/models/room.cpp index a6a36d0..4aaa07e 100644 --- a/ui/models/room.cpp +++ b/ui/models/room.cpp @@ -264,6 +264,16 @@ void Models::Room::removeParticipant(const QString& p_name) } } +Models::Participant * Models::Room::getParticipant(const QString& p_name) +{ + std::map::const_iterator itr = participants.find(p_name); + if (itr == participants.end()) { + return nullptr; + } else { + return itr->second; + } +} + void Models::Room::handleParticipantUpdate(std::map::const_iterator itr, const QMap& data) { Participant* part = itr->second; diff --git a/ui/models/room.h b/ui/models/room.h index a51a537..707b35b 100644 --- a/ui/models/room.h +++ b/ui/models/room.h @@ -58,6 +58,7 @@ public: void addParticipant(const QString& name, const QMap& data); void changeParticipant(const QString& name, const QMap& data); void removeParticipant(const QString& name); + Participant* getParticipant(const QString& name); void toOfflineState() override; QString getDisplayedName() const override; diff --git a/ui/models/roster.cpp b/ui/models/roster.cpp index 588fb1d..fbb7e52 100644 --- a/ui/models/roster.cpp +++ b/ui/models/roster.cpp @@ -276,6 +276,18 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const break; } break; + case Qt::ForegroundRole: + switch (item->type) { + case Item::account: { + Account* acc = static_cast(item); + if (!acc->getActive()) { + result = qApp->palette().brush(QPalette::Disabled, QPalette::Text); + } + } + break; + default: + break; + } default: break; } @@ -451,6 +463,7 @@ void Models::Roster::addContact(const QString& account, const QString& jid, cons connect(contact, &Contact::fileDownloadRequest, this, &Roster::fileDownloadRequest); connect(contact, &Contact::unnoticedMessage, this, &Roster::unnoticedMessage); connect(contact, &Contact::localPathInvalid, this, &Roster::localPathInvalid); + connect(contact, &Contact::unreadMessagesCountChanged, this, &Roster::recalculateUnreadMessages); contacts.insert(std::make_pair(id, contact)); } else { contact = itr->second; @@ -536,8 +549,8 @@ void Models::Roster::removeGroup(const QString& account, const QString& name) void Models::Roster::changeContact(const QString& account, const QString& jid, const QMap& data) { - Element* el = getElement({account, jid}); - if (el != NULL) { + Element* el = getElement(ElId(account, jid)); + if (el != nullptr) { for (QMap::const_iterator itr = data.begin(), end = data.end(); itr != end; ++itr) { el->update(itr.key(), itr.value()); } @@ -546,8 +559,8 @@ void Models::Roster::changeContact(const QString& account, const QString& jid, c void Models::Roster::changeMessage(const QString& account, const QString& jid, const QString& id, const QMap& data) { - Element* el = getElement({account, jid}); - if (el != NULL) { + Element* el = getElement(ElId(account, jid)); + if (el != nullptr) { el->changeMessage(id, data); } else { qDebug() << "A request to change a message of the contact " << jid << " in the account " << account << " but it wasn't found"; @@ -694,8 +707,8 @@ void Models::Roster::removePresence(const QString& account, const QString& jid, void Models::Roster::addMessage(const QString& account, const Shared::Message& data) { - Element* el = getElement({account, data.getPenPalJid()}); - if (el != NULL) { + Element* el = getElement(ElId(account, data.getPenPalJid())); + if (el != nullptr) { el->addMessage(data); } } @@ -751,7 +764,7 @@ void Models::Roster::removeAccount(const QString& account) acc->deleteLater(); } -QString Models::Roster::getContactName(const QString& account, const QString& jid) +QString Models::Roster::getContactName(const QString& account, const QString& jid) const { ElId id(account, jid); std::map::const_iterator cItr = contacts.find(id); @@ -789,10 +802,11 @@ void Models::Roster::addRoom(const QString& account, const QString jid, const QM } Room* room = new Room(acc, jid, data); - connect(room, &Contact::requestArchive, this, &Roster::onElementRequestArchive); - connect(room, &Contact::fileDownloadRequest, this, &Roster::fileDownloadRequest); - connect(room, &Contact::unnoticedMessage, this, &Roster::unnoticedMessage); - connect(room, &Contact::localPathInvalid, this, &Roster::localPathInvalid); + connect(room, &Room::requestArchive, this, &Roster::onElementRequestArchive); + connect(room, &Room::fileDownloadRequest, this, &Roster::fileDownloadRequest); + connect(room, &Room::unnoticedMessage, this, &Roster::unnoticedMessage); + connect(room, &Room::localPathInvalid, this, &Roster::localPathInvalid); + connect(room, &Room::unreadMessagesCountChanged, this, &Roster::recalculateUnreadMessages); rooms.insert(std::make_pair(id, room)); acc->appendChild(room); } @@ -895,7 +909,7 @@ bool Models::Roster::groupHasContact(const QString& account, const QString& grou } } -QString Models::Roster::getContactIconPath(const QString& account, const QString& jid, const QString& resource) +QString Models::Roster::getContactIconPath(const QString& account, const QString& jid, const QString& resource) const { ElId id(account, jid); std::map::const_iterator cItr = contacts.find(id); @@ -915,9 +929,36 @@ QString Models::Roster::getContactIconPath(const QString& account, const QString return path; } -Models::Account * Models::Roster::getAccount(const QString& name) +Models::Account * Models::Roster::getAccount(const QString& name) { + return const_cast(getAccountConst(name));} + +const Models::Account * Models::Roster::getAccountConst(const QString& name) const { + return accounts.at(name);} + +const Models::Element * Models::Roster::getElementConst(const Models::Roster::ElId& id) const { - return accounts.find(name)->second; + std::map::const_iterator cItr = contacts.find(id); + + if (cItr != contacts.end()) { + return cItr->second; + } else { + std::map::const_iterator rItr = rooms.find(id); + if (rItr != rooms.end()) { + return rItr->second; + } + } + + return nullptr; +} + +bool Models::Roster::markMessageAsRead(const Models::Roster::ElId& elementId, const QString& messageId) +{ + const Element* el = getElementConst(elementId); + if (el != nullptr) { + return el->markMessageAsRead(messageId); + } else { + return false; + } } QModelIndex Models::Roster::getAccountIndex(const QString& name) @@ -936,7 +977,7 @@ QModelIndex Models::Roster::getGroupIndex(const QString& account, const QString& if (itr == accounts.end()) { return QModelIndex(); } else { - std::map::const_iterator gItr = groups.find({account, name}); + std::map::const_iterator gItr = groups.find(ElId(account, name)); if (gItr == groups.end()) { return QModelIndex(); } else { @@ -946,6 +987,48 @@ QModelIndex Models::Roster::getGroupIndex(const QString& account, const QString& } } +QModelIndex Models::Roster::getContactIndex(const QString& account, const QString& jid, const QString& resource) +{ + std::map::const_iterator itr = accounts.find(account); + if (itr == accounts.end()) { + return QModelIndex(); + } else { + Account* acc = itr->second; + QModelIndex accIndex = index(acc->row(), 0, QModelIndex()); + std::map::const_iterator cItr = contacts.find(ElId(account, jid)); + if (cItr != contacts.end()) { + QModelIndex contactIndex = index(acc->getContact(jid), 0, accIndex); + if (resource.size() == 0) { + return contactIndex; + } else { + Presence* pres = cItr->second->getPresence(resource); + if (pres != nullptr) { + return index(pres->row(), 0, contactIndex); + } else { + return contactIndex; + } + } + } else { + std::map::const_iterator rItr = rooms.find(ElId(account, jid)); + if (rItr != rooms.end()) { + QModelIndex roomIndex = index(rItr->second->row(), 0, accIndex); + if (resource.size() == 0) { + return roomIndex; + } else { + Participant* part = rItr->second->getParticipant(resource); + if (part != nullptr) { + return index(part->row(), 0, roomIndex); + } else { + return roomIndex; + } + } + } else { + return QModelIndex(); + } + } + } +} + void Models::Roster::onElementRequestArchive(const QString& before) { Element* el = static_cast(sender()); @@ -956,7 +1039,7 @@ void Models::Roster::responseArchive(const QString& account, const QString& jid, { ElId id(account, jid); Element* el = getElement(id); - if (el != NULL) { + if (el != nullptr) { el->responseArchive(list, last); } } @@ -964,8 +1047,8 @@ void Models::Roster::responseArchive(const QString& account, const QString& jid, void Models::Roster::fileProgress(const std::list& msgs, qreal value, bool up) { for (const Shared::MessageInfo& info : msgs) { - Element* el = getElement({info.account, info.jid}); - if (el != NULL) { + Element* el = getElement(ElId(info.account, info.jid)); + if (el != nullptr) { el->fileProgress(info.messageId, value, up); } } @@ -974,8 +1057,8 @@ void Models::Roster::fileProgress(const std::list& msgs, qr void Models::Roster::fileComplete(const std::list& msgs, bool up) { for (const Shared::MessageInfo& info : msgs) { - Element* el = getElement({info.account, info.jid}); - if (el != NULL) { + Element* el = getElement(ElId(info.account, info.jid)); + if (el != nullptr) { el->fileComplete(info.messageId, up); } } @@ -984,8 +1067,8 @@ void Models::Roster::fileComplete(const std::list& msgs, bo void Models::Roster::fileError(const std::list& msgs, const QString& err, bool up) { for (const Shared::MessageInfo& info : msgs) { - Element* el = getElement({info.account, info.jid}); - if (el != NULL) { + Element* el = getElement(ElId(info.account, info.jid)); + if (el != nullptr) { el->fileError(info.messageId, err, up); } } @@ -993,20 +1076,20 @@ void Models::Roster::fileError(const std::list& msgs, const Models::Element * Models::Roster::getElement(const Models::Roster::ElId& id) { - std::map::iterator cItr = contacts.find(id); - - if (cItr != contacts.end()) { - return cItr->second; - } else { - std::map::iterator rItr = rooms.find(id); - if (rItr != rooms.end()) { - return rItr->second; - } - } - - return NULL; + return const_cast(getElementConst(id)); } +Models::Item::Type Models::Roster::getContactType(const Models::Roster::ElId& id) const +{ + const Models::Element* el = getElementConst(id); + if (el == nullptr) { + return Item::root; + } + + return el->type; +} + + void Models::Roster::onAccountReconnected() { Account* acc = static_cast(sender()); @@ -1019,3 +1102,14 @@ void Models::Roster::onAccountReconnected() } } +void Models::Roster::recalculateUnreadMessages() +{ + int count(0); + for (const std::pair& pair : contacts) { + count += pair.second->getMessagesCount(); + } + for (const std::pair& pair : rooms) { + count += pair.second->getMessagesCount(); + } + emit unreadMessagesCountChanged(count); +} diff --git a/ui/models/roster.h b/ui/models/roster.h index 08d5afc..efc50f2 100644 --- a/ui/models/roster.h +++ b/ui/models/roster.h @@ -46,6 +46,7 @@ public: Roster(QObject* parent = 0); ~Roster(); +public slots: void addAccount(const QMap &data); void updateAccount(const QString& account, const QString& field, const QVariant& value); void removeAccount(const QString& account); @@ -65,7 +66,12 @@ public: void addRoomParticipant(const QString& account, const QString& jid, const QString& name, const QMap& data); void changeRoomParticipant(const QString& account, const QString& jid, const QString& name, const QMap& data); void removeRoomParticipant(const QString& account, const QString& jid, const QString& name); - QString getContactName(const QString& account, const QString& jid); + +public: + QString getContactName(const QString& account, const QString& jid) const; + Item::Type getContactType(const Models::Roster::ElId& id) const; + const Element* getElementConst(const ElId& id) const; + Element* getElement(const ElId& id); QVariant data ( const QModelIndex& index, int role ) const override; Qt::ItemFlags flags(const QModelIndex &index) const override; @@ -77,10 +83,13 @@ public: std::deque groupList(const QString& account) const; bool groupHasContact(const QString& account, const QString& group, const QString& contactJID) const; - QString getContactIconPath(const QString& account, const QString& jid, const QString& resource); + QString getContactIconPath(const QString& account, const QString& jid, const QString& resource) const; Account* getAccount(const QString& name); + const Account* getAccountConst(const QString& name) const; QModelIndex getAccountIndex(const QString& name); QModelIndex getGroupIndex(const QString& account, const QString& name); + QModelIndex getContactIndex(const QString& account, const QString& jid, const QString& resource = ""); + bool markMessageAsRead(const ElId& elementId, const QString& messageId); void responseArchive(const QString& account, const QString& jid, const std::list& list, bool last); void fileProgress(const std::list& msgs, qreal value, bool up); @@ -92,12 +101,10 @@ public: signals: void requestArchive(const QString& account, const QString& jid, const QString& before); void fileDownloadRequest(const QString& url); + void unreadMessagesCountChanged(int count); void unnoticedMessage(const QString& account, const Shared::Message& msg); void localPathInvalid(const QString& path); -private: - Element* getElement(const ElId& id); - private slots: void onAccountDataChanged(const QModelIndex& tl, const QModelIndex& br, const QVector& roles); void onAccountReconnected(); @@ -109,7 +116,8 @@ private slots: void onChildIsAboutToBeMoved(Item* source, int first, int last, Item* destination, int newIndex); void onChildMoved(); void onElementRequestArchive(const QString& before); - + void recalculateUnreadMessages(); + private: Item* root; std::map accounts; diff --git a/ui/squawk.cpp b/ui/squawk.cpp index 3ebb6a5..a118f9c 100644 --- a/ui/squawk.cpp +++ b/ui/squawk.cpp @@ -21,19 +21,16 @@ #include #include -Squawk::Squawk(QWidget *parent) : +Squawk::Squawk(Models::Roster& p_rosterModel, QWidget *parent) : QMainWindow(parent), m_ui(new Ui::Squawk), - accounts(0), - preferences(0), - rosterModel(), - conversations(), + accounts(nullptr), + preferences(nullptr), + about(nullptr), + rosterModel(p_rosterModel), contextMenu(new QMenu()), - dbus("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications", QDBusConnection::sessionBus()), vCards(), - requestedAccountsForPasswords(), - prompt(0), - currentConversation(0), + currentConversation(nullptr), restoreSelection(), needToRestore(false) { @@ -54,24 +51,22 @@ Squawk::Squawk(QWidget *parent) : m_ui->comboBox->addItem(Shared::availabilityIcon(av), Shared::Global::getName(av)); } m_ui->comboBox->setCurrentIndex(static_cast(Shared::Availability::offline)); - + createTrayIcon(); + connect(m_ui->actionAccounts, &QAction::triggered, this, &Squawk::onAccounts); connect(m_ui->actionPreferences, &QAction::triggered, this, &Squawk::onPreferences); connect(m_ui->actionAddContact, &QAction::triggered, this, &Squawk::onNewContact); connect(m_ui->actionAddConference, &QAction::triggered, this, &Squawk::onNewConference); - connect(m_ui->actionQuit, &QAction::triggered, this, &Squawk::close); + connect(m_ui->actionQuit, &QAction::triggered, [this]() { hide(); close(); }); // Actually closing connect(m_ui->comboBox, qOverload(&QComboBox::activated), this, &Squawk::onComboboxActivated); //connect(m_ui->roster, &QTreeView::doubleClicked, this, &Squawk::onRosterItemDoubleClicked); connect(m_ui->roster, &QTreeView::customContextMenuRequested, this, &Squawk::onRosterContextMenu); connect(m_ui->roster, &QTreeView::collapsed, this, &Squawk::onItemCollepsed); connect(m_ui->roster->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &Squawk::onRosterSelectionChanged); - connect(&rosterModel, &Models::Roster::unnoticedMessage, this, &Squawk::onUnnoticedMessage); connect(rosterModel.accountsModel, &Models::Accounts::sizeChanged, this, &Squawk::onAccountsSizeChanged); - connect(&rosterModel, &Models::Roster::requestArchive, this, &Squawk::onRequestArchive); - connect(&rosterModel, &Models::Roster::fileDownloadRequest, this, &Squawk::fileDownloadRequest); - connect(&rosterModel, &Models::Roster::localPathInvalid, this, &Squawk::localPathInvalid); connect(contextMenu, &QMenu::aboutToHide, this, &Squawk::onContextAboutToHide); + connect(m_ui->actionAboutSquawk, &QAction::triggered, this, &Squawk::onAboutSquawkCalled); //m_ui->mainToolBar->addWidget(m_ui->comboBox); if (testAttribute(Qt::WA_TranslucentBackground)) { @@ -95,13 +90,38 @@ Squawk::Squawk(QWidget *parent) : settings.endGroup(); } +QSystemTrayIcon* Squawk::trayIcon; + Squawk::~Squawk() { delete contextMenu; + delete trayIcon; } +void Squawk::createTrayIcon() +{ + QSettings settings; + trayIcon = new QSystemTrayIcon(this); + trayIcon->setIcon(QApplication::windowIcon()); + + QMenu * menu = new QMenu(this); + QAction * viewWindow = new QAction("Open Main Window", this);//TODO add translations + QAction * quitAction = new QAction("Quit", this); + + connect(viewWindow, SIGNAL(triggered()), this, SLOT(show())); + connect(quitAction, &QAction::triggered, [this]() { hide(); close(); }); // Actually closing + + menu->addAction(viewWindow); + menu->addAction(quitAction); + + trayIcon->setContextMenu(menu); + if(settings.value("trayIconCheckbox").toBool()) + trayIcon->show(); +} + + void Squawk::onAccounts() { - if (accounts == 0) { + if (accounts == nullptr) { accounts = new Accounts(rosterModel.accountsModel); accounts->setAttribute(Qt::WA_DeleteOnClose); connect(accounts, &Accounts::destroyed, this, &Squawk::onAccountsClosed); @@ -121,7 +141,7 @@ void Squawk::onAccounts() void Squawk::onPreferences() { - if (preferences == 0) { + if (preferences == nullptr) { preferences = new Settings(); preferences->setAttribute(Qt::WA_DeleteOnClose); connect(preferences, &Settings::destroyed, this, &Squawk::onPreferencesClosed); @@ -189,151 +209,54 @@ void Squawk::onJoinConferenceAccepted() void Squawk::closeEvent(QCloseEvent* event) { - if (accounts != 0) { - accounts->close(); + QSettings settings; + if(this->isVisible() && settings.value("trayIconCheckbox").toBool()){ + event->ignore(); + this->hide(); + + } else { + if (accounts != nullptr) { + accounts->close(); + } + if (preferences != nullptr) { + preferences->close(); + } + if (about != nullptr) { + about->close(); + } + + for (std::map::const_iterator itr = vCards.begin(), end = vCards.end(); itr != end; ++itr) { + disconnect(itr->second, &VCard::destroyed, this, &Squawk::onVCardClosed); + itr->second->close(); + } + vCards.clear(); + writeSettings(); + emit closing(); + + QMainWindow::closeEvent(event); } - if (preferences != 0) { - preferences->close(); - } - - for (Conversations::const_iterator itr = conversations.begin(), end = conversations.end(); itr != end; ++itr) { - disconnect(itr->second, &Conversation::destroyed, this, &Squawk::onConversationClosed); - itr->second->close(); - } - conversations.clear(); - - for (std::map::const_iterator itr = vCards.begin(), end = vCards.end(); itr != end; ++itr) { - disconnect(itr->second, &VCard::destroyed, this, &Squawk::onVCardClosed); - itr->second->close(); - } - vCards.clear(); - - QMainWindow::closeEvent(event); } +void Squawk::onAccountsClosed() { + accounts = nullptr;} -void Squawk::onAccountsClosed() -{ - accounts = 0; -} +void Squawk::onPreferencesClosed() { + preferences = nullptr;} -void Squawk::onPreferencesClosed() -{ - preferences = 0; -} - -void Squawk::newAccount(const QMap& account) -{ - rosterModel.addAccount(account); -} +void Squawk::onAboutSquawkClosed() { + about = nullptr;} void Squawk::onComboboxActivated(int index) { Shared::Availability av = Shared::Global::fromInt(index); - if (av != Shared::Availability::offline) { - int size = rosterModel.accountsModel->rowCount(QModelIndex()); - if (size > 0) { - emit changeState(av); - for (int i = 0; i < size; ++i) { - Models::Account* acc = rosterModel.accountsModel->getAccount(i); - if (acc->getState() == Shared::ConnectionState::disconnected) { - emit connectAccount(acc->getName()); - } - } - } else { - m_ui->comboBox->setCurrentIndex(static_cast(Shared::Availability::offline)); - } - } else { - emit changeState(av); - int size = rosterModel.accountsModel->rowCount(QModelIndex()); - for (int i = 0; i != size; ++i) { - Models::Account* acc = rosterModel.accountsModel->getAccount(i); - if (acc->getState() != Shared::ConnectionState::disconnected) { - emit disconnectAccount(acc->getName()); - } - } - } + emit changeState(av); } -void Squawk::changeAccount(const QString& account, const QMap& data) -{ - for (QMap::const_iterator itr = data.begin(), end = data.end(); itr != end; ++itr) { - QString attr = itr.key(); - rosterModel.updateAccount(account, attr, *itr); - } -} +void Squawk::expand(const QModelIndex& index) { + m_ui->roster->expand(index);} -void Squawk::addContact(const QString& account, const QString& jid, const QString& group, const QMap& data) -{ - rosterModel.addContact(account, jid, group, data); - - QSettings settings; - settings.beginGroup("ui"); - settings.beginGroup("roster"); - settings.beginGroup(account); - if (settings.value("expanded", false).toBool()) { - QModelIndex ind = rosterModel.getAccountIndex(account); - m_ui->roster->expand(ind); - } - settings.endGroup(); - settings.endGroup(); - settings.endGroup(); -} - -void Squawk::addGroup(const QString& account, const QString& name) -{ - rosterModel.addGroup(account, name); - - QSettings settings; - settings.beginGroup("ui"); - settings.beginGroup("roster"); - settings.beginGroup(account); - if (settings.value("expanded", false).toBool()) { - QModelIndex ind = rosterModel.getAccountIndex(account); - m_ui->roster->expand(ind); - if (settings.value(name + "/expanded", false).toBool()) { - m_ui->roster->expand(rosterModel.getGroupIndex(account, name)); - } - } - settings.endGroup(); - settings.endGroup(); - settings.endGroup(); -} - -void Squawk::removeGroup(const QString& account, const QString& name) -{ - rosterModel.removeGroup(account, name); -} - -void Squawk::changeContact(const QString& account, const QString& jid, const QMap& data) -{ - rosterModel.changeContact(account, jid, data); -} - -void Squawk::removeContact(const QString& account, const QString& jid) -{ - rosterModel.removeContact(account, jid); -} - -void Squawk::removeContact(const QString& account, const QString& jid, const QString& group) -{ - rosterModel.removeContact(account, jid, group); -} - -void Squawk::addPresence(const QString& account, const QString& jid, const QString& name, const QMap& data) -{ - rosterModel.addPresence(account, jid, name, data); -} - -void Squawk::removePresence(const QString& account, const QString& jid, const QString& name) -{ - rosterModel.removePresence(account, jid, name); -} - -void Squawk::stateChanged(Shared::Availability state) -{ - m_ui->comboBox->setCurrentIndex(static_cast(state)); -} +void Squawk::stateChanged(Shared::Availability state) { + m_ui->comboBox->setCurrentIndex(static_cast(state));} void Squawk::onRosterItemDoubleClicked(const QModelIndex& item) { @@ -342,214 +265,35 @@ void Squawk::onRosterItemDoubleClicked(const QModelIndex& item) if (node->type == Models::Item::reference) { node = static_cast(node)->dereference(); } - Models::Contact* contact = 0; - Models::Room* room = 0; - QString res; - Models::Roster::ElId* id = 0; + Models::Contact* contact = nullptr; + Models::Room* room = nullptr; switch (node->type) { case Models::Item::contact: contact = static_cast(node); - id = new Models::Roster::ElId(contact->getAccountName(), contact->getJid()); + emit openConversation(Models::Roster::ElId(contact->getAccountName(), contact->getJid())); break; case Models::Item::presence: contact = static_cast(node->parentItem()); - id = new Models::Roster::ElId(contact->getAccountName(), contact->getJid()); - res = node->getName(); + emit openConversation(Models::Roster::ElId(contact->getAccountName(), contact->getJid()), node->getName()); break; case Models::Item::room: room = static_cast(node); - id = new Models::Roster::ElId(room->getAccountName(), room->getJid()); + emit openConversation(Models::Roster::ElId(room->getAccountName(), room->getJid())); break; default: m_ui->roster->expand(item); break; } - - if (id != 0) { - Conversations::const_iterator itr = conversations.find(*id); - Models::Account* acc = rosterModel.getAccount(id->account); - Conversation* conv = 0; - bool created = false; - if (itr != conversations.end()) { - conv = itr->second; - } else if (contact != 0) { - created = true; - conv = new Chat(acc, contact); - } else if (room != 0) { - created = true; - conv = new Room(acc, room); - - if (!room->getJoined()) { - emit setRoomJoined(id->account, id->name, true); - } - } - - if (conv != 0) { - if (created) { - conv->setAttribute(Qt::WA_DeleteOnClose); - subscribeConversation(conv); - conversations.insert(std::make_pair(*id, conv)); - } - - conv->show(); - conv->raise(); - conv->activateWindow(); - - if (res.size() > 0) { - conv->setPalResource(res); - } - } - - delete id; - } } } -void Squawk::onConversationClosed(QObject* parent) +void Squawk::closeCurrentConversation() { - Conversation* conv = static_cast(sender()); - Models::Roster::ElId id(conv->getAccount(), conv->getJid()); - Conversations::const_iterator itr = conversations.find(id); - if (itr != conversations.end()) { - conversations.erase(itr); - } - if (conv->isMuc) { - Room* room = static_cast(conv); - if (!room->autoJoined()) { - emit setRoomJoined(id.account, id.name, false); - } - } -} - -void Squawk::fileProgress(const std::list msgs, qreal value, bool up) -{ - rosterModel.fileProgress(msgs, value, up); -} - -void Squawk::fileDownloadComplete(const std::list msgs, const QString& path) -{ - rosterModel.fileComplete(msgs, false); -} - -void Squawk::fileError(const std::list msgs, const QString& error, bool up) -{ - rosterModel.fileError(msgs, error, up); -} - -void Squawk::fileUploadComplete(const std::list msgs, const QString& url, const QString& path) -{ - rosterModel.fileComplete(msgs, true); -} - -void Squawk::accountMessage(const QString& account, const Shared::Message& data) -{ - rosterModel.addMessage(account, data); -} - -void Squawk::onUnnoticedMessage(const QString& account, const Shared::Message& msg) -{ - notify(account, msg); //Telegram does this way - notifies even if the app is visible - QApplication::alert(this); -} - -void Squawk::changeMessage(const QString& account, const QString& jid, const QString& id, const QMap& data) -{ - rosterModel.changeMessage(account, jid, id, data); -} - -void Squawk::notify(const QString& account, const Shared::Message& msg) -{ - QString name = QString(rosterModel.getContactName(account, msg.getPenPalJid())); - QString path = QString(rosterModel.getContactIconPath(account, msg.getPenPalJid(), msg.getPenPalResource())); - QVariantList args; - args << QString(QCoreApplication::applicationName()); - args << QVariant(QVariant::UInt); //TODO some normal id - if (path.size() > 0) { - args << path; - } else { - args << QString("mail-message"); //TODO should here better be unknown user icon? - } - if (msg.getType() == Shared::Message::groupChat) { - args << msg.getFromResource() + " from " + name; - } else { - args << name; - } - - QString body(msg.getBody()); - QString oob(msg.getOutOfBandUrl()); - if (body == oob) { - body = tr("Attached file"); - } - - args << body; - args << QStringList(); - args << QVariantMap(); - args << 3000; - dbus.callWithArgumentList(QDBus::AutoDetect, "Notify", args); -} - -void Squawk::onConversationMessage(const Shared::Message& msg) -{ - Conversation* conv = static_cast(sender()); - QString acc = conv->getAccount(); - - rosterModel.addMessage(acc, msg); - emit sendMessage(acc, msg); -} - -void Squawk::onConversationReplaceMessage(const QString& originalId, const Shared::Message& msg) -{ - Conversation* conv = static_cast(sender()); - QString acc = conv->getAccount(); - - rosterModel.changeMessage(acc, msg.getPenPalJid(), originalId, { - {"state", static_cast(Shared::Message::State::pending)} - }); - emit replaceMessage(acc, originalId, msg); -} - -void Squawk::onConversationResend(const QString& id) -{ - Conversation* conv = static_cast(sender()); - QString acc = conv->getAccount(); - QString jid = conv->getJid(); - - emit resendMessage(acc, jid, id); -} - -void Squawk::onRequestArchive(const QString& account, const QString& jid, const QString& before) -{ - emit requestArchive(account, jid, 20, before); //TODO amount as a settings value -} - -void Squawk::responseArchive(const QString& account, const QString& jid, const std::list& list, bool last) -{ - rosterModel.responseArchive(account, jid, list, last); -} - -void Squawk::removeAccount(const QString& account) -{ - Conversations::const_iterator itr = conversations.begin(); - while (itr != conversations.end()) { - if (itr->first.account == account) { - Conversations::const_iterator lItr = itr; - ++itr; - Conversation* conv = lItr->second; - disconnect(conv, &Conversation::destroyed, this, &Squawk::onConversationClosed); - conv->close(); - conversations.erase(lItr); - } else { - ++itr; - } - } - - if (currentConversation != 0 && currentConversation->getAccount() == account) { + if (currentConversation != nullptr) { currentConversation->deleteLater(); - currentConversation = 0; + currentConversation = nullptr; m_ui->filler->show(); } - - rosterModel.removeAccount(account); } void Squawk::onRosterContextMenu(const QPoint& point) @@ -569,17 +313,12 @@ void Squawk::onRosterContextMenu(const QPoint& point) hasMenu = true; QString name = acc->getName(); - if (acc->getState() != Shared::ConnectionState::disconnected) { - QAction* con = contextMenu->addAction(Shared::icon("network-disconnect"), tr("Disconnect")); - con->setEnabled(active); - connect(con, &QAction::triggered, [this, name]() { - emit disconnectAccount(name); - }); + if (acc->getActive()) { + QAction* con = contextMenu->addAction(Shared::icon("network-disconnect"), tr("Deactivate")); + connect(con, &QAction::triggered, std::bind(&Squawk::disconnectAccount, this, name)); } else { - QAction* con = contextMenu->addAction(Shared::icon("network-connect"), tr("Connect")); - connect(con, &QAction::triggered, [this, name]() { - emit connectAccount(name); - }); + QAction* con = contextMenu->addAction(Shared::icon("network-connect"), tr("Activate")); + connect(con, &QAction::triggered, std::bind(&Squawk::connectAccount, this, name)); } QAction* card = contextMenu->addAction(Shared::icon("user-properties"), tr("VCard")); @@ -587,22 +326,18 @@ void Squawk::onRosterContextMenu(const QPoint& point) connect(card, &QAction::triggered, std::bind(&Squawk::onActivateVCard, this, name, acc->getBareJid(), true)); QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove")); - remove->setEnabled(active); - connect(remove, &QAction::triggered, [this, name]() { - emit removeAccount(name); - }); - + connect(remove, &QAction::triggered, std::bind(&Squawk::removeAccountRequest, this, name)); } break; case Models::Item::contact: { Models::Contact* cnt = static_cast(item); + Models::Roster::ElId id(cnt->getAccountName(), cnt->getJid()); + QString cntName = cnt->getName(); hasMenu = true; QAction* dialog = contextMenu->addAction(Shared::icon("mail-message"), tr("Open dialog")); dialog->setEnabled(active); - connect(dialog, &QAction::triggered, [this, index]() { - onRosterItemDoubleClicked(index); - }); + connect(dialog, &QAction::triggered, std::bind(&Squawk::onRosterItemDoubleClicked, this, index)); Shared::SubscriptionState state = cnt->getState(); switch (state) { @@ -610,9 +345,7 @@ void Squawk::onRosterContextMenu(const QPoint& point) case Shared::SubscriptionState::to: { QAction* unsub = contextMenu->addAction(Shared::icon("news-unsubscribe"), tr("Unsubscribe")); unsub->setEnabled(active); - connect(unsub, &QAction::triggered, [this, cnt]() { - emit unsubscribeContact(cnt->getAccountName(), cnt->getJid(), ""); - }); + connect(unsub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, false)); } break; case Shared::SubscriptionState::from: @@ -620,75 +353,68 @@ void Squawk::onRosterContextMenu(const QPoint& point) case Shared::SubscriptionState::none: { QAction* sub = contextMenu->addAction(Shared::icon("news-subscribe"), tr("Subscribe")); sub->setEnabled(active); - connect(sub, &QAction::triggered, [this, cnt]() { - emit subscribeContact(cnt->getAccountName(), cnt->getJid(), ""); - }); + connect(sub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, true)); } } - QString accName = cnt->getAccountName(); - QString cntJID = cnt->getJid(); - QString cntName = cnt->getName(); QAction* rename = contextMenu->addAction(Shared::icon("edit-rename"), tr("Rename")); rename->setEnabled(active); - connect(rename, &QAction::triggered, [this, cntName, accName, cntJID]() { + connect(rename, &QAction::triggered, [this, cntName, id]() { QInputDialog* dialog = new QInputDialog(this); - connect(dialog, &QDialog::accepted, [this, dialog, cntName, accName, cntJID]() { + connect(dialog, &QDialog::accepted, [this, dialog, cntName, id]() { QString newName = dialog->textValue(); if (newName != cntName) { - emit renameContactRequest(accName, cntJID, newName); + emit renameContactRequest(id.account, id.name, newName); } dialog->deleteLater(); }); connect(dialog, &QDialog::rejected, dialog, &QObject::deleteLater); dialog->setInputMode(QInputDialog::TextInput); - dialog->setLabelText(tr("Input new name for %1\nor leave it empty for the contact \nto be displayed as %1").arg(cntJID)); - dialog->setWindowTitle(tr("Renaming %1").arg(cntJID)); + dialog->setLabelText(tr("Input new name for %1\nor leave it empty for the contact \nto be displayed as %1").arg(id.name)); + dialog->setWindowTitle(tr("Renaming %1").arg(id.name)); dialog->setTextValue(cntName); dialog->exec(); }); QMenu* groupsMenu = contextMenu->addMenu(Shared::icon("group"), tr("Groups")); - std::deque groupList = rosterModel.groupList(accName); + std::deque groupList = rosterModel.groupList(id.account); for (QString groupName : groupList) { QAction* gr = groupsMenu->addAction(groupName); gr->setCheckable(true); - gr->setChecked(rosterModel.groupHasContact(accName, groupName, cntJID)); + gr->setChecked(rosterModel.groupHasContact(id.account, groupName, id.name)); gr->setEnabled(active); - connect(gr, &QAction::toggled, [this, accName, groupName, cntJID](bool checked) { + connect(gr, &QAction::toggled, [this, groupName, id](bool checked) { if (checked) { - emit addContactToGroupRequest(accName, cntJID, groupName); + emit addContactToGroupRequest(id.account, id.name, groupName); } else { - emit removeContactFromGroupRequest(accName, cntJID, groupName); + emit removeContactFromGroupRequest(id.account, id.name, groupName); } }); } QAction* newGroup = groupsMenu->addAction(Shared::icon("group-new"), tr("New group")); newGroup->setEnabled(active); - connect(newGroup, &QAction::triggered, [this, accName, cntJID]() { + connect(newGroup, &QAction::triggered, [this, id]() { QInputDialog* dialog = new QInputDialog(this); - connect(dialog, &QDialog::accepted, [this, dialog, accName, cntJID]() { - emit addContactToGroupRequest(accName, cntJID, dialog->textValue()); + connect(dialog, &QDialog::accepted, [this, dialog, id]() { + emit addContactToGroupRequest(id.account, id.name, dialog->textValue()); dialog->deleteLater(); }); connect(dialog, &QDialog::rejected, dialog, &QObject::deleteLater); dialog->setInputMode(QInputDialog::TextInput); dialog->setLabelText(tr("New group name")); - dialog->setWindowTitle(tr("Add %1 to a new group").arg(cntJID)); + dialog->setWindowTitle(tr("Add %1 to a new group").arg(id.name)); dialog->exec(); }); QAction* card = contextMenu->addAction(Shared::icon("user-properties"), tr("VCard")); card->setEnabled(active); - connect(card, &QAction::triggered, std::bind(&Squawk::onActivateVCard, this, accName, cnt->getJid(), false)); + connect(card, &QAction::triggered, std::bind(&Squawk::onActivateVCard, this, id.account, id.name, false)); QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove")); remove->setEnabled(active); - connect(remove, &QAction::triggered, [this, cnt]() { - emit removeContactRequest(cnt->getAccountName(), cnt->getJid()); - }); + connect(remove, &QAction::triggered, std::bind(&Squawk::removeContactRequest, this, id.account, id.name)); } break; @@ -707,32 +433,16 @@ void Squawk::onRosterContextMenu(const QPoint& point) if (room->getAutoJoin()) { QAction* unsub = contextMenu->addAction(Shared::icon("news-unsubscribe"), tr("Unsubscribe")); unsub->setEnabled(active); - connect(unsub, &QAction::triggered, [this, id]() { - emit setRoomAutoJoin(id.account, id.name, false); - if (conversations.find(id) == conversations.end() - && (currentConversation == 0 || currentConversation->getId() != id) - ) { //to leave the room if it's not opened in a conversation window - emit setRoomJoined(id.account, id.name, false); - } - }); + connect(unsub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, false)); } else { - QAction* unsub = contextMenu->addAction(Shared::icon("news-subscribe"), tr("Subscribe")); - unsub->setEnabled(active); - connect(unsub, &QAction::triggered, [this, id]() { - emit setRoomAutoJoin(id.account, id.name, true); - if (conversations.find(id) == conversations.end() - && (currentConversation == 0 || currentConversation->getId() != id) - ) { //to join the room if it's not already joined - emit setRoomJoined(id.account, id.name, true); - } - }); + QAction* sub = contextMenu->addAction(Shared::icon("news-subscribe"), tr("Subscribe")); + sub->setEnabled(active); + connect(sub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, true)); } QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove")); remove->setEnabled(active); - connect(remove, &QAction::triggered, [this, id]() { - emit removeRoomRequest(id.account, id.name); - }); + connect(remove, &QAction::triggered, std::bind(&Squawk::removeRoomRequest, this, id.account, id.name)); } break; default: @@ -744,36 +454,6 @@ void Squawk::onRosterContextMenu(const QPoint& point) } } -void Squawk::addRoom(const QString& account, const QString jid, const QMap& data) -{ - rosterModel.addRoom(account, jid, data); -} - -void Squawk::changeRoom(const QString& account, const QString jid, const QMap& data) -{ - rosterModel.changeRoom(account, jid, data); -} - -void Squawk::removeRoom(const QString& account, const QString jid) -{ - rosterModel.removeRoom(account, jid); -} - -void Squawk::addRoomParticipant(const QString& account, const QString& jid, const QString& name, const QMap& data) -{ - rosterModel.addRoomParticipant(account, jid, name, data); -} - -void Squawk::changeRoomParticipant(const QString& account, const QString& jid, const QString& name, const QMap& data) -{ - rosterModel.changeRoomParticipant(account, jid, name, data); -} - -void Squawk::removeRoomParticipant(const QString& account, const QString& jid, const QString& name) -{ - rosterModel.removeRoomParticipant(account, jid, name); -} - void Squawk::responseVCard(const QString& jid, const Shared::VCard& card) { std::map::const_iterator itr = vCards.find(jid); @@ -831,74 +511,40 @@ void Squawk::onVCardSave(const Shared::VCard& card, const QString& account) widget->deleteLater(); } -void Squawk::readSettings() -{ - QSettings settings; - settings.beginGroup("ui"); - - if (settings.contains("availability")) { - int avail = settings.value("availability").toInt(); - m_ui->comboBox->setCurrentIndex(avail); - emit stateChanged(Shared::Global::fromInt(avail)); - - int size = settings.beginReadArray("connectedAccounts"); - for (int i = 0; i < size; ++i) { - settings.setArrayIndex(i); - emit connectAccount(settings.value("name").toString()); //TODO this is actually not needed, stateChanged event already connects everything you have - } // need to fix that - settings.endArray(); - } - settings.endGroup(); -} - void Squawk::writeSettings() { QSettings settings; settings.beginGroup("ui"); - settings.beginGroup("window"); - settings.setValue("geometry", saveGeometry()); - settings.setValue("state", saveState()); - settings.endGroup(); - - settings.setValue("splitter", m_ui->splitter->saveState()); - - settings.setValue("availability", m_ui->comboBox->currentIndex()); - settings.beginWriteArray("connectedAccounts"); - int size = rosterModel.accountsModel->rowCount(QModelIndex()); - for (int i = 0; i < size; ++i) { - Models::Account* acc = rosterModel.accountsModel->getAccount(i); - if (acc->getState() != Shared::ConnectionState::disconnected) { - settings.setArrayIndex(i); - settings.setValue("name", acc->getName()); - } - } - settings.endArray(); - - settings.remove("roster"); - settings.beginGroup("roster"); - for (int i = 0; i < size; ++i) { - QModelIndex acc = rosterModel.index(i, 0, QModelIndex()); - Models::Account* account = rosterModel.accountsModel->getAccount(i); - QString accName = account->getName(); - settings.beginGroup(accName); - - settings.setValue("expanded", m_ui->roster->isExpanded(acc)); - std::deque groups = rosterModel.groupList(accName); - for (const QString& groupName : groups) { - settings.beginGroup(groupName); - QModelIndex gIndex = rosterModel.getGroupIndex(accName, groupName); - settings.setValue("expanded", m_ui->roster->isExpanded(gIndex)); - settings.endGroup(); - } - + settings.beginGroup("window"); + settings.setValue("geometry", saveGeometry()); + settings.setValue("state", saveState()); + settings.endGroup(); + + settings.setValue("splitter", m_ui->splitter->saveState()); + settings.remove("roster"); + settings.beginGroup("roster"); + int size = rosterModel.accountsModel->rowCount(QModelIndex()); + for (int i = 0; i < size; ++i) { + QModelIndex acc = rosterModel.index(i, 0, QModelIndex()); + Models::Account* account = rosterModel.accountsModel->getAccount(i); + QString accName = account->getName(); + settings.beginGroup(accName); + + settings.setValue("expanded", m_ui->roster->isExpanded(acc)); + std::deque groups = rosterModel.groupList(accName); + for (const QString& groupName : groups) { + settings.beginGroup(groupName); + QModelIndex gIndex = rosterModel.getGroupIndex(accName, groupName); + settings.setValue("expanded", m_ui->roster->isExpanded(gIndex)); + settings.endGroup(); + } + + settings.endGroup(); + } settings.endGroup(); - } - settings.endGroup(); settings.endGroup(); settings.sync(); - - qDebug() << "Saved settings"; } void Squawk::onItemCollepsed(const QModelIndex& index) @@ -920,60 +566,6 @@ void Squawk::onItemCollepsed(const QModelIndex& index) } } -void Squawk::requestPassword(const QString& account) -{ - requestedAccountsForPasswords.push_back(account); - checkNextAccountForPassword(); -} - -void Squawk::checkNextAccountForPassword() -{ - if (prompt == 0 && requestedAccountsForPasswords.size() > 0) { - prompt = new QInputDialog(this); - QString accName = requestedAccountsForPasswords.front(); - connect(prompt, &QDialog::accepted, this, &Squawk::onPasswordPromptAccepted); - connect(prompt, &QDialog::rejected, this, &Squawk::onPasswordPromptRejected); - prompt->setInputMode(QInputDialog::TextInput); - prompt->setTextEchoMode(QLineEdit::Password); - prompt->setLabelText(tr("Input the password for account %1").arg(accName)); - prompt->setWindowTitle(tr("Password for account %1").arg(accName)); - prompt->setTextValue(""); - prompt->exec(); - } -} - -void Squawk::onPasswordPromptAccepted() -{ - emit responsePassword(requestedAccountsForPasswords.front(), prompt->textValue()); - onPasswordPromptDone(); -} - -void Squawk::onPasswordPromptDone() -{ - prompt->deleteLater(); - prompt = 0; - requestedAccountsForPasswords.pop_front(); - checkNextAccountForPassword(); -} - -void Squawk::onPasswordPromptRejected() -{ - //for now it's the same on reject and on accept, but one day I'm gonna make - //"Asking for the password again on the authentication failure" feature - //and here I'll be able to break the circle of password requests - emit responsePassword(requestedAccountsForPasswords.front(), prompt->textValue()); - onPasswordPromptDone(); -} - -void Squawk::subscribeConversation(Conversation* conv) -{ - connect(conv, &Conversation::destroyed, this, &Squawk::onConversationClosed); - connect(conv, &Conversation::sendMessage, this, &Squawk::onConversationMessage); - connect(conv, &Conversation::replaceMessage, this, &Squawk::onConversationReplaceMessage); - connect(conv, &Conversation::resendMessage, this, &Squawk::onConversationResend); - connect(conv, &Conversation::notifyableMessage, this, &Squawk::notify); -} - void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIndex& previous) { if (restoreSelection.isValid() && restoreSelection == current) { @@ -986,10 +578,10 @@ void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIn if (node->type == Models::Item::reference) { node = static_cast(node)->dereference(); } - Models::Contact* contact = 0; - Models::Room* room = 0; + Models::Contact* contact = nullptr; + Models::Room* room = nullptr; QString res; - Models::Roster::ElId* id = 0; + Models::Roster::ElId* id = nullptr; bool hasContext = true; switch (node->type) { case Models::Item::contact: @@ -1018,7 +610,7 @@ void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIn } if (hasContext && QGuiApplication::mouseButtons() & Qt::RightButton) { - if (id != 0) { + if (id != nullptr) { delete id; } needToRestore = true; @@ -1026,10 +618,10 @@ void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIn return; } - if (id != 0) { - if (currentConversation != 0) { + if (id != nullptr) { + if (currentConversation != nullptr) { if (currentConversation->getId() == *id) { - if (contact != 0) { + if (contact != nullptr) { currentConversation->setPalResource(res); } return; @@ -1041,20 +633,16 @@ void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIn } Models::Account* acc = rosterModel.getAccount(id->account); - if (contact != 0) { + if (contact != nullptr) { currentConversation = new Chat(acc, contact); - } else if (room != 0) { + } else if (room != nullptr) { currentConversation = new Room(acc, room); - - if (!room->getJoined()) { - emit setRoomJoined(id->account, id->name, true); - } } if (!testAttribute(Qt::WA_TranslucentBackground)) { currentConversation->setFeedFrames(true, false, true, true); } - subscribeConversation(currentConversation); + emit openedConversation(); if (res.size() > 0) { currentConversation->setPalResource(res); @@ -1064,18 +652,10 @@ void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIn delete id; } else { - if (currentConversation != 0) { - currentConversation->deleteLater(); - currentConversation = 0; - m_ui->filler->show(); - } + closeCurrentConversation(); } } else { - if (currentConversation != 0) { - currentConversation->deleteLater(); - currentConversation = 0; - m_ui->filler->show(); - } + closeCurrentConversation(); } } @@ -1086,3 +666,31 @@ void Squawk::onContextAboutToHide() m_ui->roster->selectionModel()->setCurrentIndex(restoreSelection, QItemSelectionModel::ClearAndSelect); } } + +void Squawk::onAboutSquawkCalled() +{ + if (about == nullptr) { + about = new About(); + about->setAttribute(Qt::WA_DeleteOnClose); + connect(about, &Settings::destroyed, this, &Squawk::onAboutSquawkClosed); + } else { + about->raise(); + about->activateWindow(); + } + about->show(); +} + +Models::Roster::ElId Squawk::currentConversationId() const +{ + if (currentConversation == nullptr) { + return Models::Roster::ElId(); + } else { + return Models::Roster::ElId(currentConversation->getAccount(), currentConversation->getJid()); + } +} + +void Squawk::select(QModelIndex index) +{ + m_ui->roster->scrollTo(index, QAbstractItemView::EnsureVisible); + m_ui->roster->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect); +} diff --git a/ui/squawk.h b/ui/squawk.h index 7bd2e10..40b7f61 100644 --- a/ui/squawk.h +++ b/ui/squawk.h @@ -22,16 +22,16 @@ #include #include #include -#include #include #include +#include #include #include #include #include -#include "widgets/accounts.h" +#include "widgets/accounts/accounts.h" #include "widgets/chat.h" #include "widgets/room.h" #include "widgets/newcontact.h" @@ -39,103 +39,79 @@ #include "models/roster.h" #include "widgets/vcard/vcard.h" #include "widgets/settings/settings.h" +#include "widgets/about.h" #include "shared/shared.h" +#include "shared/global.h" namespace Ui { class Squawk; } +class Application; + class Squawk : public QMainWindow { Q_OBJECT - + friend class Application; public: - explicit Squawk(QWidget *parent = nullptr); + explicit Squawk(Models::Roster& rosterModel, QWidget *parent = nullptr); ~Squawk() override; signals: + void closing(); void newAccountRequest(const QMap&); - void modifyAccountRequest(const QString&, const QMap&); void removeAccountRequest(const QString&); void connectAccount(const QString&); void disconnectAccount(const QString&); void changeState(Shared::Availability state); - void sendMessage(const QString& account, const Shared::Message& data); - void replaceMessage(const QString& account, const QString& originalId, const Shared::Message& data); - void resendMessage(const QString& account, const QString& jid, const QString& id); - void requestArchive(const QString& account, const QString& jid, int count, const QString& before); - void subscribeContact(const QString& account, const QString& jid, const QString& reason); - void unsubscribeContact(const QString& account, const QString& jid, const QString& reason); void removeContactRequest(const QString& account, const QString& jid); void addContactRequest(const QString& account, const QString& jid, const QString& name, const QSet& groups); void addContactToGroupRequest(const QString& account, const QString& jid, const QString& groupName); void removeContactFromGroupRequest(const QString& account, const QString& jid, const QString& groupName); void renameContactRequest(const QString& account, const QString& jid, const QString& newName); - void setRoomJoined(const QString& account, const QString& jid, bool joined); - void setRoomAutoJoin(const QString& account, const QString& jid, bool joined); void addRoomRequest(const QString& account, const QString& jid, const QString& nick, const QString& password, bool autoJoin); void removeRoomRequest(const QString& account, const QString& jid); - void fileDownloadRequest(const QString& url); void requestVCard(const QString& account, const QString& jid); void uploadVCard(const QString& account, const Shared::VCard& card); - void responsePassword(const QString& account, const QString& password); - void localPathInvalid(const QString& path); void changeDownloadsPath(const QString& path); + + void notify(const QString& account, const Shared::Message& msg); + void changeSubscription(const Models::Roster::ElId& id, bool subscribe); + void openedConversation(); + void openConversation(const Models::Roster::ElId& id, const QString& resource = ""); + + void modifyAccountRequest(const QString&, const QMap&); +public: + Models::Roster::ElId currentConversationId() const; + void closeCurrentConversation(); + static QSystemTrayIcon *trayIcon; + public slots: void writeSettings(); - void readSettings(); - void newAccount(const QMap& account); - void changeAccount(const QString& account, const QMap& data); - void removeAccount(const QString& account); - void addGroup(const QString& account, const QString& name); - void removeGroup(const QString& account, const QString& name); - void addContact(const QString& account, const QString& jid, const QString& group, const QMap& data); - void removeContact(const QString& account, const QString& jid, const QString& group); - void removeContact(const QString& account, const QString& jid); - void changeContact(const QString& account, const QString& jid, const QMap& data); - void addPresence(const QString& account, const QString& jid, const QString& name, const QMap& data); - void removePresence(const QString& account, const QString& jid, const QString& name); void stateChanged(Shared::Availability state); - void accountMessage(const QString& account, const Shared::Message& data); - void responseArchive(const QString& account, const QString& jid, const std::list& list, bool last); - void addRoom(const QString& account, const QString jid, const QMap& data); - void changeRoom(const QString& account, const QString jid, const QMap& data); - void removeRoom(const QString& account, const QString jid); - void addRoomParticipant(const QString& account, const QString& jid, const QString& name, const QMap& data); - void changeRoomParticipant(const QString& account, const QString& jid, const QString& name, const QMap& data); - void removeRoomParticipant(const QString& account, const QString& jid, const QString& name); - void fileError(const std::list msgs, const QString& error, bool up); - void fileProgress(const std::list msgs, qreal value, bool up); - void fileDownloadComplete(const std::list msgs, const QString& path); - void fileUploadComplete(const std::list msgs, const QString& url, const QString& path); void responseVCard(const QString& jid, const Shared::VCard& card); - void changeMessage(const QString& account, const QString& jid, const QString& id, const QMap& data); - void requestPassword(const QString& account); + void select(QModelIndex index); private: - typedef std::map Conversations; + void createTrayIcon(); + QScopedPointer m_ui; Accounts* accounts; Settings* preferences; - Models::Roster rosterModel; - Conversations conversations; + About* about; + Models::Roster& rosterModel; QMenu* contextMenu; - QDBusInterface dbus; std::map vCards; - std::deque requestedAccountsForPasswords; - QInputDialog* prompt; Conversation* currentConversation; QModelIndex restoreSelection; bool needToRestore; protected: void closeEvent(QCloseEvent * event) override; - -protected slots: - void notify(const QString& account, const Shared::Message& msg); + void expand(const QModelIndex& index); private slots: void onAccounts(); @@ -147,29 +123,17 @@ private slots: void onAccountsSizeChanged(unsigned int size); void onAccountsClosed(); void onPreferencesClosed(); - void onConversationClosed(QObject* parent = 0); void onVCardClosed(); void onVCardSave(const Shared::VCard& card, const QString& account); void onActivateVCard(const QString& account, const QString& jid, bool edition = false); void onComboboxActivated(int index); void onRosterItemDoubleClicked(const QModelIndex& item); - void onConversationMessage(const Shared::Message& msg); - void onConversationReplaceMessage(const QString& originalId, const Shared::Message& msg); - void onConversationResend(const QString& id); - void onRequestArchive(const QString& account, const QString& jid, const QString& before); void onRosterContextMenu(const QPoint& point); void onItemCollepsed(const QModelIndex& index); - void onPasswordPromptAccepted(); - void onPasswordPromptRejected(); void onRosterSelectionChanged(const QModelIndex& current, const QModelIndex& previous); void onContextAboutToHide(); - - void onUnnoticedMessage(const QString& account, const Shared::Message& msg); - -private: - void checkNextAccountForPassword(); - void onPasswordPromptDone(); - void subscribeConversation(Conversation* conv); + void onAboutSquawkCalled(); + void onAboutSquawkClosed(); }; #endif // SQUAWK_H diff --git a/ui/squawk.ui b/ui/squawk.ui index 840dfee..a8b0730 100644 --- a/ui/squawk.ui +++ b/ui/squawk.ui @@ -201,8 +201,15 @@ + + + Help + + + + @@ -248,12 +255,18 @@ - + + .. Preferences + + + About Squawk + + diff --git a/ui/widgets/CMakeLists.txt b/ui/widgets/CMakeLists.txt index f3a2afe..21d9504 100644 --- a/ui/widgets/CMakeLists.txt +++ b/ui/widgets/CMakeLists.txt @@ -1,10 +1,4 @@ target_sources(squawk PRIVATE - account.cpp - account.h - account.ui - accounts.cpp - accounts.h - accounts.ui chat.cpp chat.h conversation.cpp @@ -18,8 +12,12 @@ target_sources(squawk PRIVATE newcontact.ui room.cpp room.h + about.cpp + about.h + about.ui ) add_subdirectory(vcard) add_subdirectory(messageline) add_subdirectory(settings) +add_subdirectory(accounts) diff --git a/ui/widgets/about.cpp b/ui/widgets/about.cpp new file mode 100644 index 0000000..3782a94 --- /dev/null +++ b/ui/widgets/about.cpp @@ -0,0 +1,108 @@ +// Squawk messenger. +// Copyright (C) 2019 Yury Gubich +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "about.h" +#include "ui_about.h" +#include +#include + +static const std::string QXMPP_VERSION_PATCH(std::to_string(QXMPP_VERSION & 0xff)); +static const std::string QXMPP_VERSION_MINOR(std::to_string((QXMPP_VERSION & 0xff00) >> 8)); +static const std::string QXMPP_VERSION_MAJOR(std::to_string(QXMPP_VERSION >> 16)); +static const QString QXMPP_VERSION_STRING = QString::fromStdString(QXMPP_VERSION_MAJOR + "." + QXMPP_VERSION_MINOR + "." + QXMPP_VERSION_PATCH); + +About::About(QWidget* parent): + QWidget(parent), + m_ui(new Ui::About), + license(nullptr) +{ + m_ui->setupUi(this); + m_ui->versionValue->setText(QApplication::applicationVersion()); + m_ui->qtVersionValue->setText(qVersion()); + m_ui->qtBuiltAgainstVersion->setText(tr("(built against %1)").arg(QT_VERSION_STR)); + + m_ui->qxmppVersionValue->setText(QXmppVersion()); + m_ui->qxmppBuiltAgainstVersion->setText(tr("(built against %1)").arg(QXMPP_VERSION_STRING)); + + setWindowFlag(Qt::Tool); + + connect(m_ui->licenceLink, &QLabel::linkActivated, this, &About::onLicenseActivated); +} + +About::~About() { + if (license != nullptr) { + license->deleteLater(); + } +}; + +void About::onLicenseActivated() +{ + if (license == nullptr) { + QFile file; + bool found = false; + QStringList shares = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); + for (const QString& path : shares) { + file.setFileName(path + "/LICENSE.md"); + + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + found = true; + break; + } + } + if (!found) { + qDebug() << "couldn't read license file, bailing"; + return; + } + + license = new QWidget(); + license->setWindowTitle(tr("License")); + QVBoxLayout* layout = new QVBoxLayout(license); + QLabel* text = new QLabel(license); + QScrollArea* area = new QScrollArea(license); + text->setTextFormat(Qt::MarkdownText); + text->setWordWrap(true); + text->setOpenExternalLinks(true); + text->setMargin(5); + area->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + + layout->addWidget(area); + license->setAttribute(Qt::WA_DeleteOnClose); + connect(license, &QWidget::destroyed, this, &About::onLicenseClosed); + + QTextStream in(&file); + QString line; + QString licenseText(""); + while (!in.atEnd()) { + line = in.readLine(); + licenseText.append(line + "\n"); + } + text->setText(licenseText); + file.close(); + + area->setWidget(text); + + } else { + license->raise(); + license->activateWindow(); + } + + license->show(); +} + +void About::onLicenseClosed() +{ + license = nullptr; +} diff --git a/ui/widgets/about.h b/ui/widgets/about.h new file mode 100644 index 0000000..1506b7f --- /dev/null +++ b/ui/widgets/about.h @@ -0,0 +1,51 @@ +// Squawk messenger. +// Copyright (C) 2019 Yury Gubich +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef ABOUT_H +#define ABOUT_H + +#include +#include +#include +#include +#include +#include + +namespace Ui +{ +class About; +} + +/** + * @todo write docs + */ +class About : public QWidget +{ + Q_OBJECT +public: + About(QWidget* parent = nullptr); + ~About(); + +protected slots: + void onLicenseActivated(); + void onLicenseClosed(); + +private: + QScopedPointer m_ui; + QWidget* license; +}; + +#endif // ABOUT_H diff --git a/ui/widgets/about.ui b/ui/widgets/about.ui new file mode 100644 index 0000000..e7b9ce4 --- /dev/null +++ b/ui/widgets/about.ui @@ -0,0 +1,680 @@ + + + About + + + + 0 + 0 + 375 + 290 + + + + About Squawk + + + + 0 + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + 12 + + + + Squawk + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + + 50 + 50 + + + + + + + :/images/logo.svg + + + true + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + 0 + + + false + + + + About + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + XMPP (jabber) messenger + + + + + + + (c) 2019 - 2022, Yury Gubich + + + + + + + <a href="https://git.macaw.me/blue/squawk">Project site</a> + + + Qt::RichText + + + true + + + + + + + <a href="https://git.macaw.me/blue/squawk/src/branch/master/LICENSE.md">License: GNU General Public License version 3</a> + + + Qt::RichText + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + QFrame::NoFrame + + + Qt::ScrollBarAlwaysOff + + + true + + + Components + + + + + 0 + 0 + 355 + 181 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 2 + + + 0 + + + + + + true + + + + Version + + + + + + + + true + + + + 0.0.0 + + + + + + + + true + + + + (built against 0.0.0) + + + + + + + + 75 + true + + + + Qt + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + <a href="https://www.qt.io/">www.qt.io</a> + + + true + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 2 + + + 0 + + + + + + true + + + + Version + + + + + + + + true + + + + 0.0.0 + + + + + + + + true + + + + (built against 0.0.0) + + + + + + + + 75 + true + + + + QXmpp + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + <a href="https://github.com/qxmpp-project/qxmpp">github.com/qxmpp-project/qxmpp</a> + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Report Bugs + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Please report any bug you find! +To report bugs you can use: + + + + + + + <a href="https://git.macaw.me/blue/squawk/issues">Project bug tracker</> + + + true + + + + + + + XMPP (<a href="xmpp:blue@macaw.me">blue@macaw.me</a>) + + + true + + + + + + + E-Mail (<a href="mailto:blue@macaw.me">blue@macaw.me</a>) + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + QFrame::NoFrame + + + Qt::ScrollBarAlwaysOff + + + true + + + Thanks To + + + + + 0 + 0 + 355 + 181 + + + + + 10 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 75 + true + + + + Vae + + + + + + + + true + + + + Major refactoring, bug fixes, constructive criticism + + + true + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 75 + true + + + + Shunf4 + + + + + + + + true + + + + Major refactoring, bug fixes, build adaptations for Windows and MacOS + + + true + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 75 + true + + + + Bruno F. Fontes + + + + + + + + true + + + + Brazilian Portuguese translation + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + + + + Version + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + 0.0.0 + + + + + + + + + + diff --git a/ui/widgets/accounts/CMakeLists.txt b/ui/widgets/accounts/CMakeLists.txt new file mode 100644 index 0000000..970985d --- /dev/null +++ b/ui/widgets/accounts/CMakeLists.txt @@ -0,0 +1,11 @@ +target_sources(squawk PRIVATE + account.cpp + account.h + account.ui + accounts.cpp + accounts.h + accounts.ui + credentialsprompt.cpp + credentialsprompt.h + credentialsprompt.ui + ) diff --git a/ui/widgets/account.cpp b/ui/widgets/accounts/account.cpp similarity index 98% rename from ui/widgets/account.cpp rename to ui/widgets/accounts/account.cpp index ba3af6b..164af6c 100644 --- a/ui/widgets/account.cpp +++ b/ui/widgets/accounts/account.cpp @@ -53,6 +53,7 @@ QMap Account::value() const map["name"] = m_ui->name->text(); map["resource"] = m_ui->resource->text(); map["passwordType"] = m_ui->passwordType->currentIndex(); + map["active"] = m_ui->active->isChecked(); return map; } diff --git a/ui/widgets/account.h b/ui/widgets/accounts/account.h similarity index 100% rename from ui/widgets/account.h rename to ui/widgets/accounts/account.h diff --git a/ui/widgets/account.ui b/ui/widgets/accounts/account.ui similarity index 91% rename from ui/widgets/account.ui rename to ui/widgets/accounts/account.ui index a1879bc..b7f9f26 100644 --- a/ui/widgets/account.ui +++ b/ui/widgets/accounts/account.ui @@ -7,7 +7,7 @@ 0 0 438 - 342 + 345 @@ -34,7 +34,7 @@ 6 - + Your account login @@ -44,14 +44,14 @@ - + Server - + A server address of your account. Like 404.city or macaw.me @@ -61,21 +61,21 @@ - + Login - + Password - + Password of your account @@ -97,14 +97,14 @@ - + Name - + Just a name how would you call this account, doesn't affect anything @@ -114,14 +114,14 @@ - + Resource - + A resource name like "Home" or "Work" @@ -131,17 +131,17 @@ - + Password storage - + - + @@ -157,6 +157,23 @@ + + + + Active + + + + + + + enable + + + true + + + diff --git a/ui/widgets/accounts.cpp b/ui/widgets/accounts/accounts.cpp similarity index 94% rename from ui/widgets/accounts.cpp rename to ui/widgets/accounts/accounts.cpp index 7f4a135..82a8ca0 100644 --- a/ui/widgets/accounts.cpp +++ b/ui/widgets/accounts/accounts.cpp @@ -83,7 +83,8 @@ void Accounts::onEditButton() {"server", mAcc->getServer()}, {"name", mAcc->getName()}, {"resource", mAcc->getResource()}, - {"passwordType", QVariant::fromValue(mAcc->getPasswordType())} + {"passwordType", QVariant::fromValue(mAcc->getPasswordType())}, + {"active", mAcc->getActive()} }); acc->lockId(); connect(acc, &Account::accepted, this, &Accounts::onAccountAccepted); @@ -118,17 +119,17 @@ void Accounts::updateConnectButton() bool allConnected = true; for (int i = 0; i < selectionSize && allConnected; ++i) { const Models::Account* mAcc = model->getAccount(sm->selectedRows().at(i).row()); - allConnected = mAcc->getState() == Shared::ConnectionState::connected; + allConnected = allConnected && mAcc->getActive(); } if (allConnected) { toDisconnect = true; - m_ui->connectButton->setText(tr("Disconnect")); + m_ui->connectButton->setText(tr("Deactivate")); } else { toDisconnect = false; - m_ui->connectButton->setText(tr("Connect")); + m_ui->connectButton->setText(tr("Activate")); } } else { - m_ui->connectButton->setText(tr("Connect")); + m_ui->connectButton->setText(tr("Activate")); toDisconnect = false; m_ui->connectButton->setEnabled(false); } diff --git a/ui/widgets/accounts.h b/ui/widgets/accounts/accounts.h similarity index 98% rename from ui/widgets/accounts.h rename to ui/widgets/accounts/accounts.h index 9fd0b57..6d5eb95 100644 --- a/ui/widgets/accounts.h +++ b/ui/widgets/accounts/accounts.h @@ -24,7 +24,7 @@ #include #include "account.h" -#include "../models/accounts.h" +#include "ui/models/accounts.h" namespace Ui { diff --git a/ui/widgets/accounts.ui b/ui/widgets/accounts/accounts.ui similarity index 100% rename from ui/widgets/accounts.ui rename to ui/widgets/accounts/accounts.ui diff --git a/ui/widgets/accounts/credentialsprompt.cpp b/ui/widgets/accounts/credentialsprompt.cpp new file mode 100644 index 0000000..3d1bafa --- /dev/null +++ b/ui/widgets/accounts/credentialsprompt.cpp @@ -0,0 +1,60 @@ +// Squawk messenger. +// Copyright (C) 2019 Yury Gubich +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "credentialsprompt.h" +#include "ui_credentialsprompt.h" + +CredentialsPrompt::CredentialsPrompt(QWidget* parent): + QDialog(parent), + m_ui(new Ui::CredentialsPrompt), + title(), + message() +{ + m_ui->setupUi(this); + + title = windowTitle(); + message = m_ui->message->text(); +} + +CredentialsPrompt::~CredentialsPrompt() +{ +} + +void CredentialsPrompt::setAccount(const QString& account) +{ + m_ui->message->setText(message.arg(account)); + setWindowTitle(title.arg(account)); +} + +QString CredentialsPrompt::getLogin() const +{ + return m_ui->login->text(); +} + +QString CredentialsPrompt::getPassword() const +{ + return m_ui->password->text(); +} + +void CredentialsPrompt::setLogin(const QString& login) +{ + m_ui->login->setText(login); +} + +void CredentialsPrompt::setPassword(const QString& password) +{ + m_ui->password->setText(password); +} diff --git a/ui/widgets/accounts/credentialsprompt.h b/ui/widgets/accounts/credentialsprompt.h new file mode 100644 index 0000000..ce9a791 --- /dev/null +++ b/ui/widgets/accounts/credentialsprompt.h @@ -0,0 +1,52 @@ +// Squawk messenger. +// Copyright (C) 2019 Yury Gubich +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef CREDENTIALSPROMPT_H +#define CREDENTIALSPROMPT_H + +#include +#include + +namespace Ui +{ +class CredentialsPrompt; +} + +/** + * @todo write docs + */ +class CredentialsPrompt : public QDialog +{ + Q_OBJECT + +public: + CredentialsPrompt(QWidget* parent = nullptr); + ~CredentialsPrompt(); + + void setAccount(const QString& account); + void setLogin(const QString& login); + void setPassword(const QString& password); + + QString getLogin() const; + QString getPassword() const; + +private: + QScopedPointer m_ui; + QString title; + QString message; +}; + +#endif // CREDENTIALSPROMPT_H diff --git a/ui/widgets/accounts/credentialsprompt.ui b/ui/widgets/accounts/credentialsprompt.ui new file mode 100644 index 0000000..2ad4d8d --- /dev/null +++ b/ui/widgets/accounts/credentialsprompt.ui @@ -0,0 +1,144 @@ + + + CredentialsPrompt + + + + 0 + 0 + 318 + 229 + + + + Authentication error: %1 + + + + + + true + + + + 0 + 0 + + + + Couldn't authenticate account %1: login or password is icorrect. +Would you like to check them and try again? + + + Qt::AlignCenter + + + true + + + + + + + + + Login + + + + + + + Your account login (without @server.domain) + + + + + + + Password + + + + + + + Your password + + + Qt::ImhHiddenText|Qt::ImhNoAutoUppercase|Qt::ImhNoPredictiveText|Qt::ImhSensitiveData + + + + + + QLineEdit::Password + + + false + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + CredentialsPrompt + accept() + + + 20 + 20 + + + 20 + 20 + + + + + buttonBox + rejected() + CredentialsPrompt + reject() + + + 20 + 20 + + + 20 + 20 + + + + + diff --git a/ui/widgets/conversation.cpp b/ui/widgets/conversation.cpp index 4e5e007..b2c7a5f 100644 --- a/ui/widgets/conversation.cpp +++ b/ui/widgets/conversation.cpp @@ -26,7 +26,7 @@ #include #include #include -#include +#include #include #include #include @@ -498,6 +498,26 @@ void Conversation::onFeedContext(const QPoint& pos) emit resendMessage(id); }); } + + QString selected = feed->getSelectedText(); + if (selected.size() > 0) { + showMenu = true; + QAction* copy = contextMenu->addAction(Shared::icon("edit-copy"), tr("Copy selected")); + connect(copy, &QAction::triggered, [selected] () { + QClipboard* cb = QApplication::clipboard(); + cb->setText(selected); + }); + } + + QString body = item->getBody(); + if (body.size() > 0) { + showMenu = true; + QAction* copy = contextMenu->addAction(Shared::icon("edit-copy"), tr("Copy message")); + connect(copy, &QAction::triggered, [body] () { + QClipboard* cb = QApplication::clipboard(); + cb->setText(body); + }); + } QString path = Shared::resolvePath(item->getAttachPath()); if (path.size() > 0) { diff --git a/ui/widgets/messageline/feedview.cpp b/ui/widgets/messageline/feedview.cpp index de7f56f..69b5093 100644 --- a/ui/widgets/messageline/feedview.cpp +++ b/ui/widgets/messageline/feedview.cpp @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include #include "messagedelegate.h" @@ -50,7 +52,13 @@ FeedView::FeedView(QWidget* parent): modelState(Models::MessageFeed::complete), progress(), dividerFont(), - dividerMetrics(dividerFont) + dividerMetrics(dividerFont), + mousePressed(false), + dragging(false), + hovered(Shared::Hover::nothing), + dragStartPoint(), + dragEndPoint(), + selectedText() { horizontalScrollBar()->setRange(0, 0); verticalScrollBar()->setSingleStep(approximateSingleMessageHeight); @@ -162,7 +170,7 @@ void FeedView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottom void FeedView::updateGeometries() { - qDebug() << "updateGeometries"; + //qDebug() << "updateGeometries"; QScrollBar* bar = verticalScrollBar(); const QStyle* st = style(); @@ -343,6 +351,7 @@ void FeedView::paintEvent(QPaintEvent* event) QDateTime lastDate; bool first = true; + QRect viewportRect = vp->rect(); for (const QModelIndex& index : toRener) { QDateTime currentDate = index.data(Models::MessageFeed::Date).toDateTime(); option.rect = visualRect(index); @@ -356,7 +365,10 @@ void FeedView::paintEvent(QPaintEvent* event) } first = false; } - bool mouseOver = option.rect.contains(cursor) && vp->rect().contains(cursor); + QRect stripe = option.rect; + stripe.setLeft(0); + stripe.setWidth(viewportRect.width()); + bool mouseOver = stripe.contains(cursor) && viewportRect.contains(cursor); option.state.setFlag(QStyle::State_MouseOver, mouseOver); itemDelegate(index)->paint(&painter, option, index); @@ -403,13 +415,151 @@ void FeedView::verticalScrollbarValueChanged(int value) QAbstractItemView::verticalScrollbarValueChanged(vo); } +void FeedView::setAnchorHovered(Shared::Hover type) +{ + if (hovered != type) { + hovered = type; + switch (hovered) { + case Shared::Hover::nothing: + setCursor(Qt::ArrowCursor); + break; + case Shared::Hover::text: + setCursor(Qt::IBeamCursor); + break; + case Shared::Hover::anchor: + setCursor(Qt::PointingHandCursor); + break; + } + } +} + void FeedView::mouseMoveEvent(QMouseEvent* event) { if (!isVisible()) { return; } + + dragEndPoint = event->localPos().toPoint(); + if (mousePressed) { + QPoint distance = dragStartPoint - dragEndPoint; + if (distance.manhattanLength() > 5) { + dragging = true; + } + } QAbstractItemView::mouseMoveEvent(event); + + if (specialDelegate) { + MessageDelegate* del = static_cast(itemDelegate()); + if (dragging) { + QModelIndex index = indexAt(dragStartPoint); + if (index.isValid()) { + QRect rect = visualRect(index); + if (rect.contains(dragStartPoint)) { + QString selected = del->mouseDrag(dragStartPoint, dragEndPoint, index, rect); + if (selectedText != selected) { + selectedText = selected; + setDirtyRegion(rect); + } + } + } + } else { + QModelIndex index = indexAt(dragEndPoint); + if (index.isValid()) { + QRect rect = visualRect(index); + if (rect.contains(dragEndPoint)) { + setAnchorHovered(del->hoverType(dragEndPoint, index, rect)); + } else { + setAnchorHovered(Shared::Hover::nothing); + } + } else { + setAnchorHovered(Shared::Hover::nothing); + } + } + } +} + +void FeedView::mousePressEvent(QMouseEvent* event) +{ + QAbstractItemView::mousePressEvent(event); + + mousePressed = event->button() == Qt::LeftButton; + if (mousePressed) { + dragStartPoint = event->localPos().toPoint(); + if (specialDelegate && specialModel) { + MessageDelegate* del = static_cast(itemDelegate()); + QString lastSelectedId = del->clearSelection(); + if (lastSelectedId.size()) { + Models::MessageFeed* feed = static_cast(model()); + QModelIndex index = feed->modelIndexById(lastSelectedId); + if (index.isValid()) { + setDirtyRegion(visualRect(index)); + } + } + } + } +} + +void FeedView::mouseDoubleClickEvent(QMouseEvent* event) +{ + QAbstractItemView::mouseDoubleClickEvent(event); + mousePressed = event->button() == Qt::LeftButton; + if (mousePressed) { + dragStartPoint = event->localPos().toPoint(); + if (specialDelegate && specialModel) { + MessageDelegate* del = static_cast(itemDelegate()); + QString lastSelectedId = del->clearSelection(); + selectedText = ""; + if (lastSelectedId.size()) { + Models::MessageFeed* feed = static_cast(model()); + QModelIndex index = feed->modelIndexById(lastSelectedId); + if (index.isValid()) { + setDirtyRegion(visualRect(index)); + } + } + + QModelIndex index = indexAt(dragStartPoint); + QRect rect = visualRect(index); + if (rect.contains(dragStartPoint)) { + selectedText = del->leftDoubleClick(dragStartPoint, index, rect); + if (selectedText.size() > 0) { + setDirtyRegion(rect); + } + } + } + } +} + +void FeedView::mouseReleaseEvent(QMouseEvent* event) +{ + QAbstractItemView::mouseReleaseEvent(event); + + if (mousePressed) { + if (!dragging && specialDelegate) { + QPoint point = event->localPos().toPoint(); + QModelIndex index = indexAt(point); + if (index.isValid()) { + QRect rect = visualRect(index); + MessageDelegate* del = static_cast(itemDelegate()); + if (rect.contains(point)) { + del->leftClick(point, index, rect); + } + } + } + dragging = false; + mousePressed = false; + } +} + +void FeedView::keyPressEvent(QKeyEvent* event) +{ + QKeyEvent *key_event = static_cast(event); + if (key_event->matches(QKeySequence::Copy)) { + if (selectedText.size() > 0) { + QClipboard* cb = QApplication::clipboard(); + cb->setText(selectedText); + } + } } void FeedView::resizeEvent(QResizeEvent* event) @@ -456,6 +606,7 @@ void FeedView::setItemDelegate(QAbstractItemDelegate* delegate) elementMargin = MessageDelegate::margin; connect(del, &MessageDelegate::buttonPushed, this, &FeedView::onMessageButtonPushed); connect(del, &MessageDelegate::invalidPath, this, &FeedView::onMessageInvalidPath); + connect(del, &MessageDelegate::openLink, &QDesktopServices::openUrl); } else { specialDelegate = false; elementMargin = 0; @@ -520,3 +671,8 @@ void FeedView::onModelSyncStateChange(Models::MessageFeed::SyncState state) scheduleDelayedItemsLayout(); } } + +QString FeedView::getSelectedText() const +{ + return selectedText; +} diff --git a/ui/widgets/messageline/feedview.h b/ui/widgets/messageline/feedview.h index 8bcd913..4194849 100644 --- a/ui/widgets/messageline/feedview.h +++ b/ui/widgets/messageline/feedview.h @@ -20,12 +20,15 @@ #define FEEDVIEW_H #include +#include +#include #include #include #include #include +#include /** * @todo write docs @@ -48,6 +51,7 @@ public: void setModel(QAbstractItemModel * model) override; QFont getFont() const; + QString getSelectedText() const; signals: void resized(); @@ -68,12 +72,17 @@ protected: void paintEvent(QPaintEvent * event) override; void updateGeometries() override; void mouseMoveEvent(QMouseEvent * event) override; + void mousePressEvent(QMouseEvent * event) override; + void mouseReleaseEvent(QMouseEvent * event) override; + void mouseDoubleClickEvent(QMouseEvent * event) override; + void keyPressEvent(QKeyEvent * event) override; void resizeEvent(QResizeEvent * event) override; private: bool tryToCalculateGeometriesWithNoScrollbars(const QStyleOptionViewItem& option, const QAbstractItemModel* model, uint32_t totalHeight); void positionProgress(); void drawDateDevider(int top, const QDateTime& date, QPainter& painter); + void setAnchorHovered(Shared::Hover type); private: struct Hint { @@ -93,6 +102,12 @@ private: Progress progress; QFont dividerFont; QFontMetrics dividerMetrics; + bool mousePressed; + bool dragging; + Shared::Hover hovered; + QPoint dragStartPoint; + QPoint dragEndPoint; + QString selectedText; static const std::set geometryChangingRoles; diff --git a/ui/widgets/messageline/messagedelegate.cpp b/ui/widgets/messageline/messagedelegate.cpp index 15a5e46..f935057 100644 --- a/ui/widgets/messageline/messagedelegate.cpp +++ b/ui/widgets/messageline/messagedelegate.cpp @@ -22,6 +22,9 @@ #include #include #include +#include +#include +#include #include "messagedelegate.h" #include "messagefeed.h" @@ -41,7 +44,7 @@ MessageDelegate::MessageDelegate(QObject* parent): bodyFont(), nickFont(), dateFont(), - bodyMetrics(bodyFont), + bodyRenderer(new QTextDocument()), nickMetrics(nickFont), dateMetrics(dateFont), buttonHeight(0), @@ -51,11 +54,14 @@ MessageDelegate::MessageDelegate(QObject* parent): bars(new std::map()), statusIcons(new std::map()), pencilIcons(new std::map()), - bodies(new std::map()), previews(new std::map()), idsToKeep(new std::set()), - clearingWidgets(false) + clearingWidgets(false), + currentId(""), + selection(0, 0) { + bodyRenderer->setDocumentMargin(0); + QPushButton btn(QCoreApplication::translate("MessageLine", "Download")); buttonHeight = btn.sizeHint().height(); buttonWidth = btn.sizeHint().width(); @@ -82,10 +88,6 @@ MessageDelegate::~MessageDelegate() delete pair.second; } - for (const std::pair& pair: *bodies){ - delete pair.second; - } - for (const std::pair& pair: *previews){ delete pair.second; } @@ -95,8 +97,8 @@ MessageDelegate::~MessageDelegate() delete idsToKeep; delete buttons; delete bars; - delete bodies; delete previews; + delete bodyRenderer; } void MessageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const @@ -123,11 +125,6 @@ void MessageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& optio opt.displayAlignment = Qt::AlignRight | Qt::AlignTop; } - QSize bodySize(0, 0); - if (data.text.size() > 0) { - bodySize = bodyMetrics.boundingRect(opt.rect, Qt::TextWordWrap, data.text).size(); - } - QRect rect; if (ntds) { painter->setFont(nickFont); @@ -170,15 +167,7 @@ void MessageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& optio painter->restore(); QWidget* vp = static_cast(painter->device()); - if (data.text.size() > 0) { - QLabel* body = getBody(data); - body->setParent(vp); - body->setMinimumSize(bodySize); - body->setMaximumSize(bodySize); - body->move(opt.rect.left(), opt.rect.y()); - body->show(); - opt.rect.adjust(0, bodySize.height() + textMargin, 0, 0); - } + paintBody(data, painter, opt); painter->setFont(dateFont); QColor q = painter->pen().color(); QString dateString = data.date.toLocalTime().toString("hh:mm"); @@ -306,7 +295,12 @@ QSize MessageDelegate::sizeHint(const QStyleOptionViewItem& option, const QModel Models::FeedItem data = qvariant_cast(vi); QSize messageSize(0, 0); if (data.text.size() > 0) { - messageSize = bodyMetrics.boundingRect(messageRect, Qt::TextWordWrap, data.text).size(); + bodyRenderer->setPlainText(data.text); + bodyRenderer->setTextWidth(messageRect.size().width()); + + QSizeF size = bodyRenderer->size(); + size.setWidth(bodyRenderer->idealWidth()); + messageSize = QSize(std::ceil(size.width()), std::ceil(size.height())); messageSize.rheight() += textMargin; } @@ -367,6 +361,183 @@ QSize MessageDelegate::sizeHint(const QStyleOptionViewItem& option, const QModel return messageSize; } +QRect MessageDelegate::getHoveredMessageBodyRect(const QModelIndex& index, const Models::FeedItem& data, const QRect& sizeHint) const +{ + QRect localHint = sizeHint.adjusted(bubbleMargin, bubbleMargin + margin, -bubbleMargin, -bubbleMargin / 2); + if (needToDrawSender(index, data)) { + localHint.adjust(0, nickMetrics.lineSpacing() + textMargin, 0, 0); + } + + int attachHeight = 0; + switch (data.attach.state) { + case Models::none: + break; + case Models::uploading: + attachHeight += Preview::calculateAttachSize(Shared::resolvePath(data.attach.localPath), localHint).height() + textMargin; + [[fallthrough]]; + case Models::downloading: + attachHeight += barHeight + textMargin; + break; + case Models::remote: + attachHeight += buttonHeight + textMargin; + break; + case Models::ready: + case Models::local: { + QSize aSize = Preview::calculateAttachSize(Shared::resolvePath(data.attach.localPath), localHint); + attachHeight += aSize.height() + textMargin; + } + break; + case Models::errorDownload: { + QSize commentSize = dateMetrics.boundingRect(localHint, Qt::TextWordWrap, data.attach.error).size(); + attachHeight += commentSize.height() + buttonHeight + textMargin * 2; + } + break; + case Models::errorUpload: { + QSize aSize = Preview::calculateAttachSize(Shared::resolvePath(data.attach.localPath), localHint); + QSize commentSize = dateMetrics.boundingRect(localHint, Qt::TextWordWrap, data.attach.error).size(); + attachHeight += aSize.height() + commentSize.height() + textMargin * 2; + } + break; + } + + int bottomSize = std::max(dateMetrics.lineSpacing(), statusIconSize); + localHint.adjust(0, attachHeight, 0, -(bottomSize + textMargin)); + + return localHint; +} + +QString MessageDelegate::getAnchor(const QPoint& point, const QModelIndex& index, const QRect& sizeHint) const +{ + QVariant vi = index.data(Models::MessageFeed::Bulk); + Models::FeedItem data = qvariant_cast(vi); + if (data.text.size() > 0) { + QRect localHint = getHoveredMessageBodyRect(index, data, sizeHint); + + if (localHint.contains(point)) { + QPoint translated = point - localHint.topLeft(); + + bodyRenderer->setHtml(Shared::processMessageBody(data.text)); + bodyRenderer->setTextWidth(localHint.size().width()); + + return bodyRenderer->documentLayout()->anchorAt(translated); + } + } + + return QString(); +} + +void MessageDelegate::leftClick(const QPoint& point, const QModelIndex& index, const QRect& sizeHint) const +{ + QString anchor = getAnchor(point, index, sizeHint); + if (anchor.size() > 0) { + emit openLink(anchor); + } +} + +QString MessageDelegate::leftDoubleClick(const QPoint& point, const QModelIndex& index, const QRect& sizeHint) +{ + QVariant vi = index.data(Models::MessageFeed::Bulk); + Models::FeedItem data = qvariant_cast(vi); + if (data.text.size() > 0) { + QRect localHint = getHoveredMessageBodyRect(index, data, sizeHint); + + if (localHint.contains(point)) { + QPoint translated = point - localHint.topLeft(); + + bodyRenderer->setHtml(Shared::processMessageBody(data.text)); + bodyRenderer->setTextWidth(localHint.size().width()); + + QAbstractTextDocumentLayout* lay = bodyRenderer->documentLayout(); + + int position = lay->hitTest(translated, Qt::HitTestAccuracy::FuzzyHit); + QTextCursor cursor(bodyRenderer); + cursor.setPosition(position, QTextCursor::MoveAnchor); + cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor); + cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor); + + selection.first = cursor.anchor(); + selection.second = cursor.position(); + currentId = data.id; + + if (selection.first != selection.second) { + return cursor.selectedText(); + } + } + } + return ""; +} + +Shared::Hover MessageDelegate::hoverType(const QPoint& point, const QModelIndex& index, const QRect& sizeHint) const +{ + QVariant vi = index.data(Models::MessageFeed::Bulk); + Models::FeedItem data = qvariant_cast(vi); + if (data.text.size() > 0) { + QRect localHint = getHoveredMessageBodyRect(index, data, sizeHint); + + if (localHint.contains(point)) { + QPoint translated = point - localHint.topLeft(); + + bodyRenderer->setHtml(Shared::processMessageBody(data.text)); + bodyRenderer->setTextWidth(localHint.size().width()); + + QAbstractTextDocumentLayout* lay = bodyRenderer->documentLayout(); + QString anchor = lay->anchorAt(translated); + + if (anchor.size() > 0) { + return Shared::Hover::anchor; + } else { + int position = lay->hitTest(translated, Qt::HitTestAccuracy::ExactHit); + if (position != -1) { + return Shared::Hover::text; + } + } + } + } + return Shared::Hover::nothing; +} + +QString MessageDelegate::mouseDrag(const QPoint& start, const QPoint& end, const QModelIndex& index, const QRect& sizeHint) +{ + QVariant vi = index.data(Models::MessageFeed::Bulk); + Models::FeedItem data = qvariant_cast(vi); + if (data.text.size() > 0) { + QRect localHint = getHoveredMessageBodyRect(index, data, sizeHint); + + if (localHint.contains(start)) { + QPoint tl = localHint.topLeft(); + QPoint first = start - tl; + QPoint last = end - tl; + last.setX(std::max(last.x(), 0)); + last.setX(std::min(last.x(), localHint.width() - 1)); + last.setY(std::max(last.y(), 0)); + last.setY(std::min(last.y(), localHint.height())); + + bodyRenderer->setHtml(Shared::processMessageBody(data.text)); + bodyRenderer->setTextWidth(localHint.size().width()); + selection.first = bodyRenderer->documentLayout()->hitTest(first, Qt::HitTestAccuracy::FuzzyHit); + selection.second = bodyRenderer->documentLayout()->hitTest(last, Qt::HitTestAccuracy::FuzzyHit); + + currentId = data.id; + + if (selection.first != selection.second) { + QTextCursor cursor(bodyRenderer); + cursor.setPosition(selection.first, QTextCursor::MoveAnchor); + cursor.setPosition(selection.second, QTextCursor::KeepAnchor); + return cursor.selectedText(); + } + } + } + return ""; +} + +QString MessageDelegate::clearSelection() +{ + QString lastSelectedId = currentId; + currentId = ""; + selection = std::pair(0, 0); + return lastSelectedId; +} + void MessageDelegate::initializeFonts(const QFont& font) { bodyFont = font; @@ -390,9 +561,11 @@ void MessageDelegate::initializeFonts(const QFont& font) dateFont.setPointSize(dateFont.pointSize() * dateFontMultiplier); } - bodyMetrics = QFontMetrics(bodyFont); + bodyFont.setKerning(false); nickMetrics = QFontMetrics(nickFont); dateMetrics = QFontMetrics(dateFont); + + bodyRenderer->setDefaultFont(bodyFont); Preview::initializeFont(bodyFont); } @@ -572,34 +745,6 @@ QLabel * MessageDelegate::getPencilIcon(const Models::FeedItem& data) const return result; } -QLabel * MessageDelegate::getBody(const Models::FeedItem& data) const -{ - std::map::const_iterator itr = bodies->find(data.id); - QLabel* result = 0; - - if (itr != bodies->end()) { - result = itr->second; - } else { - result = new QLabel(); - result->setFont(bodyFont); - result->setContextMenuPolicy(Qt::NoContextMenu); - result->setWordWrap(true); - result->setOpenExternalLinks(true); - result->setTextInteractionFlags(result->textInteractionFlags() | Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse); - bodies->insert(std::make_pair(data.id, result)); - } - - result->setText(Shared::processMessageBody(data.text)); - - return result; -} - -void MessageDelegate::beginClearWidgets() -{ - idsToKeep->clear(); - clearingWidgets = true; -} - template void removeElements(std::map* elements, std::set* idsToKeep) { std::set toRemove; @@ -614,6 +759,41 @@ void removeElements(std::map* elements, std::set* idsToKee } } +int MessageDelegate::paintBody(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const +{ + if (data.text.size() > 0) { + bodyRenderer->setHtml(Shared::processMessageBody(data.text)); + bodyRenderer->setTextWidth(option.rect.size().width()); + painter->save(); + painter->translate(option.rect.topLeft()); + + if (data.id == currentId) { + QTextCursor cursor(bodyRenderer); + cursor.setPosition(selection.first, QTextCursor::MoveAnchor); + cursor.setPosition(selection.second, QTextCursor::KeepAnchor); + QTextCharFormat format = cursor.charFormat(); + format.setBackground(option.palette.color(QPalette::Active, QPalette::Highlight)); + format.setForeground(option.palette.color(QPalette::Active, QPalette::HighlightedText)); + cursor.setCharFormat(format); + } + + bodyRenderer->drawContents(painter); + + painter->restore(); + QSize bodySize(std::ceil(bodyRenderer->idealWidth()), std::ceil(bodyRenderer->size().height())); + + option.rect.adjust(0, bodySize.height() + textMargin, 0, 0); + return bodySize.width(); + } + return 0; +} + +void MessageDelegate::beginClearWidgets() +{ + idsToKeep->clear(); + clearingWidgets = true; +} + void MessageDelegate::endClearWidgets() { if (clearingWidgets) { @@ -621,7 +801,6 @@ void MessageDelegate::endClearWidgets() removeElements(bars, idsToKeep); removeElements(statusIcons, idsToKeep); removeElements(pencilIcons, idsToKeep); - removeElements(bodies, idsToKeep); removeElements(previews, idsToKeep); idsToKeep->clear(); @@ -649,8 +828,3 @@ void MessageDelegate::clearHelperWidget(const Models::FeedItem& data) const } } } - -// void MessageDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const -// { -// -// } diff --git a/ui/widgets/messageline/messagedelegate.h b/ui/widgets/messageline/messagedelegate.h index 9225412..322fb76 100644 --- a/ui/widgets/messageline/messagedelegate.h +++ b/ui/widgets/messageline/messagedelegate.h @@ -29,6 +29,7 @@ #include #include #include +#include #include "shared/icons.h" #include "shared/global.h" @@ -56,6 +57,11 @@ public: bool editorEvent(QEvent * event, QAbstractItemModel * model, const QStyleOptionViewItem & option, const QModelIndex & index) override; void endClearWidgets(); void beginClearWidgets(); + void leftClick(const QPoint& point, const QModelIndex& index, const QRect& sizeHint) const; + QString leftDoubleClick(const QPoint& point, const QModelIndex& index, const QRect& sizeHint); + Shared::Hover hoverType(const QPoint& point, const QModelIndex& index, const QRect& sizeHint) const; + QString mouseDrag(const QPoint& start, const QPoint& end, const QModelIndex& index, const QRect& sizeHint); + QString clearSelection(); static int avatarHeight; static int margin; @@ -63,23 +69,28 @@ public: signals: void buttonPushed(const QString& messageId) const; void invalidPath(const QString& messageId) const; + void openLink(const QString& href) const; protected: int paintButton(QPushButton* btn, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const; int paintBar(QProgressBar* bar, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const; int paintPreview(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const; int paintComment(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const; + int paintBody(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const; void paintAvatar(const Models::FeedItem& data, const QModelIndex& index, const QStyleOptionViewItem& option, QPainter* painter) const; void paintBubble(const Models::FeedItem& data, QPainter* painter, const QStyleOptionViewItem& option) const; + QPushButton* getButton(const Models::FeedItem& data) const; QProgressBar* getBar(const Models::FeedItem& data) const; QLabel* getStatusIcon(const Models::FeedItem& data) const; QLabel* getPencilIcon(const Models::FeedItem& data) const; - QLabel* getBody(const Models::FeedItem& data) const; void clearHelperWidget(const Models::FeedItem& data) const; bool needToDrawAvatar(const QModelIndex& index, const Models::FeedItem& data, const QStyleOptionViewItem& option) const; bool needToDrawSender(const QModelIndex& index, const Models::FeedItem& data) const; + + QRect getHoveredMessageBodyRect(const QModelIndex& index, const Models::FeedItem& data, const QRect& sizeHint) const; + QString getAnchor(const QPoint& point, const QModelIndex& index, const QRect& sizeHint) const; protected slots: void onButtonPushed() const; @@ -93,7 +104,7 @@ private: QFont bodyFont; QFont nickFont; QFont dateFont; - QFontMetrics bodyMetrics; + QTextDocument* bodyRenderer; QFontMetrics nickMetrics; QFontMetrics dateMetrics; @@ -105,11 +116,11 @@ private: std::map* bars; std::map* statusIcons; std::map* pencilIcons; - std::map* bodies; std::map* previews; std::set* idsToKeep; bool clearingWidgets; - + QString currentId; + std::pair selection; }; #endif // MESSAGEDELEGATE_H diff --git a/ui/widgets/messageline/messagefeed.cpp b/ui/widgets/messageline/messagefeed.cpp index 33fbdd4..ad67bb3 100644 --- a/ui/widgets/messageline/messagefeed.cpp +++ b/ui/widgets/messageline/messagefeed.cpp @@ -163,6 +163,12 @@ void Models::MessageFeed::changeMessage(const QString& id, const QMapgetForwarded() && changeRoles.count(MessageRoles::Text) > 0) { + unreadMessages->insert(id); + emit unreadMessagesCountChanged(); + emit unnoticedMessage(*msg); + } } } @@ -312,12 +318,7 @@ QVariant Models::MessageFeed::data(const QModelIndex& index, int role) const case Bulk: { FeedItem item; item.id = msg->getId(); - - std::set::const_iterator umi = unreadMessages->find(item.id); - if (umi != unreadMessages->end()) { - unreadMessages->erase(umi); - emit unreadMessagesCountChanged(); - } + markMessageAsRead(item.id); item.sentByMe = sentByMe(*msg); item.date = msg->getTime(); @@ -367,6 +368,17 @@ int Models::MessageFeed::rowCount(const QModelIndex& parent) const return storage.size(); } +bool Models::MessageFeed::markMessageAsRead(const QString& id) const +{ + std::set::const_iterator umi = unreadMessages->find(id); + if (umi != unreadMessages->end()) { + unreadMessages->erase(umi); + emit unreadMessagesCountChanged(); + return true; + } + return false; +} + unsigned int Models::MessageFeed::unreadMessagesCount() const { return unreadMessages->size(); diff --git a/ui/widgets/messageline/messagefeed.h b/ui/widgets/messageline/messagefeed.h index c9701ae..db174d2 100644 --- a/ui/widgets/messageline/messagefeed.h +++ b/ui/widgets/messageline/messagefeed.h @@ -57,6 +57,8 @@ public: void changeMessage(const QString& id, const QMap& data); void removeMessage(const QString& id); Shared::Message getMessage(const QString& id); + QModelIndex modelIndexById(const QString& id) const; + QModelIndex modelIndexByTime(const QString& id, const QDateTime& time) const; QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; @@ -72,6 +74,7 @@ public: void reportLocalPathInvalid(const QString& messageId); unsigned int unreadMessagesCount() const; + bool markMessageAsRead(const QString& id) const; void fileProgress(const QString& messageId, qreal value, bool up); void fileError(const QString& messageId, const QString& error, bool up); void fileComplete(const QString& messageId, bool up); @@ -125,8 +128,6 @@ protected: bool sentByMe(const Shared::Message& msg) const; Attachment fillAttach(const Shared::Message& msg) const; Edition fillCorrection(const Shared::Message& msg) const; - QModelIndex modelIndexById(const QString& id) const; - QModelIndex modelIndexByTime(const QString& id, const QDateTime& time) const; std::set detectChanges(const Shared::Message& msg, const QMap& data) const; private: diff --git a/ui/widgets/settings/pagegeneral.cpp b/ui/widgets/settings/pagegeneral.cpp index 9ed46a2..7608fcb 100644 --- a/ui/widgets/settings/pagegeneral.cpp +++ b/ui/widgets/settings/pagegeneral.cpp @@ -18,6 +18,7 @@ #include "pagegeneral.h" #include "ui_pagegeneral.h" +#include "ui/squawk.h" PageGeneral::PageGeneral(QWidget* parent): QWidget(parent), @@ -28,7 +29,10 @@ PageGeneral::PageGeneral(QWidget* parent): QSettings settings; m_ui->downloadsPathInput->setText(settings.value("downloadsPath").toString()); + m_ui->trayIconCheckbox->setChecked(settings.value("trayIconCheckbox").toBool()); + connect(m_ui->downloadsPathButton, &QPushButton::clicked, this, &PageGeneral::onBrowseButtonClicked); + connect(m_ui->trayIconCheckbox, &QCheckBox::stateChanged, this, &PageGeneral::onTrayIconCheckboxChecked); } PageGeneral::~PageGeneral() @@ -76,3 +80,9 @@ void PageGeneral::onDialogDestroyed() { dialog = nullptr; } + +void PageGeneral::onTrayIconCheckboxChecked(){ + QSettings settings; + settings.setValue("trayIconCheckbox", m_ui->trayIconCheckbox->isChecked()); + Squawk::trayIcon->setVisible(m_ui->trayIconCheckbox->isChecked()); +} diff --git a/ui/widgets/settings/pagegeneral.h b/ui/widgets/settings/pagegeneral.h index 7f58d71..4f6197e 100644 --- a/ui/widgets/settings/pagegeneral.h +++ b/ui/widgets/settings/pagegeneral.h @@ -47,6 +47,7 @@ private slots: void onBrowseButtonClicked(); void onDialogAccepted(); void onDialogDestroyed(); + void onTrayIconCheckboxChecked(); private: QScopedPointer m_ui; diff --git a/ui/widgets/settings/pagegeneral.ui b/ui/widgets/settings/pagegeneral.ui index e412668..af944d7 100644 --- a/ui/widgets/settings/pagegeneral.ui +++ b/ui/widgets/settings/pagegeneral.ui @@ -39,6 +39,19 @@ + + + + Qt::LeftToRight + + + Hide Squawk to tray + + + false + + +